Compare commits

..

No commits in common. "antifascista" and "1bf468a262dfa03925ba50a242fb548d226b1623" have entirely different histories.

171 changed files with 2582 additions and 4080 deletions

View file

@ -1,9 +1,8 @@
## What does this MR do?
<!--Insert the link to a GitHub issue in (), or describe the changes if there is no issue -->
[Issue Link]()
<!-- Is there a lot to say? Consider creating an issue. -->
## Screenshots <!-- Optional, very helpful for the reviewer colleagues from other teams -->
## Screenshots <!-- Optional -->
### Before
@ -13,7 +12,7 @@
![alt text](https://example.com/after.png)
## Code Changes
## Notes
* This MR
**does** <!-- KEEP ONLY ONE -->
@ -59,36 +58,9 @@ How do your performance changes scale on a system of this size?
they are really big customers, and we want to keep their business!)
-->
### Documentation Follow-up Required?
<!-- Keep one of the two sections -->
### Follow-up Required <!-- Optional -->
<!--
If this MR does change:
- How the user experiences or uses the application
- Visual appearance
- Screen flow
- Texts
- How the application is deployed an maintained
- Deployment process
- System requirements
- Command line interfaces
-->
This MR may require follow-up by the documentation team.
/label ~Documentation
<!--
Otherwise
Does your MR require coordination with the documentation/support teams?
If so, apply the label and explain here.
-->
This MR does not require any follow-up.
## QA Checklist (to be filled by the reviewer)
- [ ] Implementation satisfies specification
- [ ] Changes confirmed by manual testing
- [ ] [Code style](https://git.znuny.com/zammad/zammad/-/wikis/Coding-style-guide) is appropriate
- [ ] Performance will not degrade
- [ ] Code is properly covered with tests
- If follow-up by the documentation team is needed:
- [ ] Add a comment with this text
> @<!-- don't treat this as a mention until copied -->MrGeneration please check if this MR requires changes to the documentation. Thanks!

View file

@ -116,6 +116,6 @@ env:
- ZAMMAD_RAILS_PORT=3000
- ZAMMAD_WEBSOCKET_PORT=6042
services:
- postgres:13
- postgres
before_install: contrib/packager.io/preinstall.sh
after_install: contrib/packager.io/postinstall.sh

View file

@ -6,8 +6,6 @@ require:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- rubocop-inflector
- ../config/initializers/inflections.rb
- ./rubocop_zammad.rb
inherit_from:

View file

@ -887,6 +887,10 @@ Metrics/PerceivedComplexity:
- 'test/browser_test_helper.rb'
- 'test/integration/slack_test.rb'
Rails/AssertNot:
Exclude:
- 'test/browser/admin_permissions_granular_vs_full_test.rb'
Rails/CreateTableWithTimestamps:
Exclude:
- 'db/migrate/20120101000001_create_base.rb'

View file

@ -1,7 +1,7 @@
# Change Log
## [5.1.0](https://github.com/zammad/zammad/tree/5.1.0) (2021-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/5.0.0...5.1.0)
## [5.0.0](https://github.com/zammad/zammad/tree/5.0.0) (2021-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/4.1.0...5.0.0)
**Implemented enhancements:**

View file

@ -198,7 +198,6 @@ group :development, :test do
gem 'overcommit'
gem 'rubocop'
gem 'rubocop-faker'
gem 'rubocop-inflector'
gem 'rubocop-performance'
gem 'rubocop-rails'
gem 'rubocop-rspec'

View file

@ -90,7 +90,7 @@ GEM
activerecord (>= 4.2)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
argon2 (2.1.1)
argon2 (2.0.3)
ffi (~> 1.14)
ffi-compiler (~> 1.0)
argon2 (2.0.3-x86_64-linux-musl)
@ -113,7 +113,7 @@ GEM
faraday
async-io (1.32.2)
async
async-pool (0.3.9)
async-pool (0.3.8)
async (>= 1.25)
autoprefixer-rails (10.3.3.0)
execjs (~> 2)
@ -188,7 +188,7 @@ GEM
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.5.4)
doorkeeper (5.5.3)
railties (>= 5)
dotenv (2.7.6)
eco (1.0.0)
@ -302,8 +302,9 @@ GEM
inflection (1.0.0)
iniparse (1.5.0)
interception (0.5)
json (2.5.1)
json (2.5.1-x86_64-linux-musl)
jwt (2.3.0)
jwt (2.2.3)
kgio (2.11.4)
kgio (2.11.4-x86_64-linux-musl)
koala (3.0.0)
@ -443,7 +444,7 @@ GEM
binding_of_caller (~> 1.0)
pry (~> 0.13)
public_suffix (4.0.6)
puma (4.3.10)
puma (4.3.8)
nio4r (~> 2.0)
puma (4.3.8-x86_64-linux-musl)
nio4r (~> 2.0)
@ -521,14 +522,15 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
rszr (0.5.2)
rszr (0.5.2-x86_64-linux-musl)
rubocop (1.22.1)
rubocop (1.21.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.12.0, < 2.0)
rubocop-ast (>= 1.9.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.12.0)
@ -536,14 +538,10 @@ GEM
rubocop-faker (1.1.0)
faker (>= 2.12.0)
rubocop (>= 0.82.0)
rubocop-inflector (0.1.1)
activesupport
rubocop
rubocop-rspec
rubocop-performance (1.11.5)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.12.3)
rubocop-rails (2.12.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
@ -619,7 +617,7 @@ GEM
timers (4.3.3)
tins (1.29.1)
sync
twilio-ruby (5.59.0)
twilio-ruby (5.58.3)
faraday (>= 0.9, < 2.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
@ -766,7 +764,6 @@ DEPENDENCIES
rszr (= 0.5.2)
rubocop
rubocop-faker
rubocop-inflector
rubocop-performance
rubocop-rails
rubocop-rspec
@ -800,4 +797,4 @@ RUBY VERSION
ruby 2.7.3p183
BUNDLED WITH
2.2.27
2.2.20

149
Makefile
View file

@ -1,149 +0,0 @@
SHELL := /bin/bash
.DEFAULT_GOAL := help
# Copiar el archivo de configuración y avisar cuando hay que
# actualizarlo.
.env: .env.example
@test -f $@ || cp -v $< $@
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
@test -f $@ && diff -auN --color $@ $<
include .env
export
# XXX: El espacio antes del comentario cuenta como espacio
args ?=## Argumentos para Hain
commit ?= origin/rails## Commit desde el que actualizar
env ?= staging## Entorno del nodo delegado
sutty ?= $(SUTTY)## Dirección local
delegate ?= $(DELEGATE)## Cambia el nodo delegado
hain ?= $(HAINISH)## Ubicación de Hainish
# El nodo delegado tiene dos entornos, production y staging.
# Dependiendo del entorno que elijamos, se van a generar los assets y el
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
#
# Production es el entorno de panel.sutty.nl
ifeq ($(env),production)
container ?= sutty
## TODO: Cambiar a otra cosa
branch ?= rails
public ?= public
endif
# Staging es el entorno de panel.staging.sutty.nl
ifeq ($(env),staging)
container := staging
branch := staging
public := staging
endif
help: always ## Ayuda
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
@echo -e "Tareas:\n"
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
@echo -e "\nArgumentos:\n"
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
assets: node_modules public/packs/manifest.json.br ## Compilar los assets
test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)"
postgresql: /etc/hosts ## Iniciar la base de datos
pgrep postgres >/dev/null || $(hain) postgresql
serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
$(hain) 'bundle exec ./bin/webpack-dev-server'
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
$(MAKE) rails args=server
rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rails $(args)"
rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rake $(args)"
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
$(hain) 'bundle $(args)'
rubocop: ## Yutea el código que está por ser commiteado
git status --porcelain \
| grep -E "^(A|M)" \
| sed "s/^...//" \
| grep ".rb$$" \
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
audit: ## Encuentra dependencias con vulnerabilidades
$(hain) 'gem install bundler-audit'
$(hain) 'bundle audit --update'
brakeman: ## Busca posibles vulnerabilidades en Sutty
$(MAKE) bundle args='exec brakeman'
yarn: ## Tareas de yarn
$(hain) 'yarn $(args)'
clean: ## Limpieza
rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
build: Gemfile.lock ## Generar la imagen Docker
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
docker tag sutty/$(container):latest sutty:keep
@echo -e "\a"
save: ## Subir la imagen Docker al nodo delegado
time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
date +%F | xargs -I {} git tag -f $(container)-{}
@echo -e "\a"
ota-js: assets ## Actualizar Javascript en el nodo delegado
sudo chgrp -R 82 public/
rsync -avi --delete-after public/ root@$(delegate):/srv/sutty/srv/http/data/_$(public)/
ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
ota: ## Actualizar Rails en el nodo delegado
umask 022; git format-patch $(commit)
scp ./0*.patch $(delegate):/tmp/
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/
scp ./ota.sh $(delegate):/tmp/
ssh $(delegate) docker cp /tmp/patches-$(shell echo $(commit) | cut -d / -f 1) $(container):/tmp/
ssh $(delegate) docker cp /tmp/ota.sh $(container):/usr/local/bin/ota
ssh $(delegate) docker exec $(container) apk add --no-cache patch
ssh $(delegate) docker exec $(container) ota $(commit)
rm ./0*.patch
# Todos los archivos de assets. Si alguno cambia, se van a recompilar
# los assets que luego se suben al nodo delegado.
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
public/packs/manifest.json.br: $(assets)
$(hain) 'PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
# Correr un test en particular por ejemplo
# `make test/models/usuarie_test.rb`
tests := $(shell find test/ -name "*_test.rb")
$(tests): always
$(MAKE) test args="TEST=$@"
# Agrega las direcciones locales al sistema
/etc/hosts: always
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
@grep -q " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 api.$(SUTTY)" | sudo tee -a $@
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
# Instala las dependencias de Javascript
node_modules: package.json
$(MAKE) yarn
# Instala las dependencias de Rails
Gemfile.lock: Gemfile
$(MAKE) bundle args=install
.PHONY: always

View file

@ -1 +1 @@
5.1.x
5.0.x

View file

@ -493,25 +493,6 @@ class App.ControllerTable extends App.Controller
sortable: @dndCallback
))
getGroupByKeyName: (object, groupBy) ->
reference_key = groupBy + '_id'
if reference_key of object
attribute = _.findWhere(object.constructor.configure_attributes, { name: reference_key })
return App[attribute.relation]?.find(object[reference_key])?.displayName() || reference_key
groupBy
sortObjectKeys: (objects, direction) ->
sorted = Object.keys(objects).sort()
switch direction
when 'DESC'
sorted.reverse()
else
sorted
renderTableRows: (sort = false) =>
if sort is true
@sortList()
@ -525,11 +506,11 @@ class App.ControllerTable extends App.Controller
objectsToShow = @objectsOfPage(@pagerShownPage)
if @groupBy
# group by raw (and not printable) value so dates work also
objectsGrouped = _.groupBy(objectsToShow, (object) => object[@getGroupByKeyName(object, @groupBy)])
objectsGrouped = _.groupBy(objectsToShow, (object) => object[@groupBy])
else
objectsGrouped = { '': objectsToShow }
for groupValue in @sortObjectKeys(objectsGrouped, @groupDirection)
for groupValue in Object.keys(objectsGrouped).sort()
groupObjects = objectsGrouped[groupValue]
for object in groupObjects

View file

@ -46,7 +46,7 @@ class App.UiElement.ApplicationUiElement
result = []
for row in selection
if attribute.translate
row.name = App.i18n.translatePlain(row.name)
row.name = App.i18n.translateInline(row.name)
if !_.isEmpty(row.children)
row.children = @getConfigOptionListArray(attribute, row.children)
result.push row
@ -65,7 +65,7 @@ class App.UiElement.ApplicationUiElement
for key in order
name_new = selection[key]
if attribute.translate
name_new = App.i18n.translatePlain(name_new)
name_new = App.i18n.translateInline(name_new)
attribute.options.push {
name: name_new
value: key
@ -162,7 +162,7 @@ class App.UiElement.ApplicationUiElement
nameNew = item.name
if attribute.translate
nameNew = App.i18n.translatePlain(nameNew)
nameNew = App.i18n.translateInline(nameNew)
row =
value: item.id,

View file

@ -23,7 +23,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
organization:
name: 'Organization'
model: 'Organization'
model_show: ['User', 'Organization']
model_show: ['Organization']
'customer.organization':
name: 'Organization'
model: 'Organization'

View file

@ -39,7 +39,6 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
operatorsType =
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to']
'integer$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
'^date': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
'^select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
'^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
'^input$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty']
@ -64,9 +63,8 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
continue
for row in App[groupMeta.model].configure_attributes
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag)
continue if _.contains(['created_at', 'updated_at'], row.name)
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], row.name)
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select'], row.tag)
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title'], row.name)
# ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
@ -130,10 +128,9 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
@buildValueConfigMultiple: (config, meta) ->
if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator)
config.multiple = true
config.nulloption = true
else
config.multiple = false
config.nulloption = false
config.nulloption = false
return config
@HasPreCondition: ->

View file

@ -6,7 +6,7 @@ class App.UiElement.richtext
attribute.value = attribute.value.text
item = $( App.view('generic/richtext')(attribute: attribute, toolButtons: @toolButtons) )
item.find('[contenteditable]').ce(
@contenteditable = item.find('[contenteditable]').ce(
mode: attribute.type
maxlength: attribute.maxlength
buttons: attribute.buttons
@ -21,12 +21,12 @@ class App.UiElement.richtext
new App[plugin.controller](params)
if attribute.upload
attachments = []
@attachments = []
item.append( $( App.view('generic/attachment')(attribute: attribute) ) )
renderFile = (file) ->
renderFile = (file) =>
item.find('.attachments').append(App.view('generic/attachment_item')(file))
attachments.push file
@attachments.push file
if params && params.attachments
for file in params.attachments
@ -46,10 +46,10 @@ class App.UiElement.richtext
, form.form_id)
# remove items
item.find('.attachments').on('click', '.js-delete', (e) ->
item.find('.attachments').on('click', '.js-delete', (e) =>
id = $(e.currentTarget).data('id')
attachments = _.filter(
attachments,
@attachments = _.filter(
@attachments,
(item) ->
return if item.id.toString() is id.toString()
item
@ -71,35 +71,67 @@ class App.UiElement.richtext
element.empty()
)
App.Delay.set( ->
uploader = new App.Html5Upload(
uploadUrl: "#{App.Config.get('api_path')}/attachments"
dropContainer: item.closest('form')
cancelContainer: item.find('.js-cancel')
inputField: item.find('input')
data:
form_id: item.closest('form').find('[name=form_id]').val()
@progressBar = item.find('.attachmentUpload-progressBar')
@progressText = item.find('.js-percentage')
@attachmentPlaceholder = item.find('.attachmentPlaceholder')
@attachmentUpload = item.find('.attachmentUpload')
@attachmentsHolder = item.find('.attachments')
@cancelContainer = item.find('.js-cancel')
onFileStartCallback: ->
item.find('[contenteditable]').trigger('fileUploadStart')
u = => html5Upload.initialize(
uploadUrl: "#{App.Config.get('api_path')}/attachments"
dropContainer: item.closest('form').get(0)
cancelContainer: @cancelContainer
inputField: item.find('input').get(0)
maxSimultaneousUploads: 1,
key: 'File'
data:
form_id: item.closest('form').find('[name=form_id]').val()
onFileAdded: (file) =>
onFileCompletedCallback: (response) ->
renderFile(response.data)
item.find('input').val('')
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
file.on(
onStart: =>
@attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
@cancelContainer.removeClass('hide')
item.find('[contenteditable]').trigger('fileUploadStart')
App.Log.debug 'UiElement.richtext', 'upload start'
onFileAbortedCallback: ->
item.find('input').val('')
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
onAborted: =>
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
item.find('input').val('')
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
attachmentPlaceholder: item.find('.attachmentPlaceholder')
attachmentUpload: item.find('.attachmentUpload')
progressBar: item.find('.attachmentUpload-progressBar')
progressText: item.find('.js-percentage')
)
# Called after received response from the server
onCompleted: (response) =>
response = JSON.parse(response)
uploader.render()
, 100, undefined, 'form_upload')
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
# reset progress bar
@progressBar.width(parseInt(0) + '%')
@progressText.text('')
renderFile(response.data)
item.find('input').val('')
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
# Called during upload progress, first parameter
# is decimal value from 0 to 100.
onProgress: (progress, fileSize, uploadedBytes) =>
@progressBar.width(parseInt(progress) + '%')
@progressText.text(parseInt(progress))
# hide cancel on 90%
if parseInt(progress) >= 90
@cancelContainer.addClass('hide')
App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress)
)
)
App.Delay.set(u, 100, undefined, 'form_upload')
item

View file

@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller
events:
'click .type-tabs .tab': 'changeFormType'
'submit form': 'submit'
'click .form-controls .js-cancel': 'cancel'
'click .js-cancel': 'cancel'
'click .js-active-toggle': 'toggleButton'
types: {
@ -184,11 +184,8 @@ class App.TicketCreate extends App.Controller
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
changed: =>
return true if @hasAttachments()
formCurrent = @formParam( @$('.ticket-create') )
diff = difference(@formDefault, formCurrent)
return false if !diff || _.isEmpty(diff)
return true
@ -464,9 +461,6 @@ class App.TicketCreate extends App.Controller
params: =>
params = @formParam(@$('.main form'))
hasAttachments: =>
@$('.richtext .attachments .attachment').length > 0
submit: (e) =>
e.preventDefault()
@ -569,7 +563,7 @@ class App.TicketCreate extends App.Controller
# save ticket, create article
# check attachment
if article['body']
if !@hasAttachments()
if @$('.richtext .attachments .attachment').length < 1
matchingWord = App.Utils.checkAttachmentReference(article['body'])
if matchingWord
if !confirm(App.i18n.translateContent('You use %s in text but no attachment is attached. Do you want to continue?', matchingWord))

View file

@ -1,6 +1,6 @@
class CoreWorkflow extends App.ControllerSubContent
requiredPermission: 'admin.core_workflow'
header: 'Core Workflows'
header: 'Core Workflow'
constructor: ->
super
@ -54,4 +54,4 @@ class CoreWorkflow extends App.ControllerSubContent
}
return mapping[screen] || screen
App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflows', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin')
App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflow', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin')

View file

@ -27,13 +27,11 @@ class App.KnowledgeBasePublicMenuManager extends App.Controller
{
headline: 'Header menu',
identifier: 'header',
color: kb.color_header,
color_link: kb.color_header_link
color: kb.color_header
},
{
headline: 'Footer menu',
identifier: 'footer',
color_link: 'hsl(207,12%,50%)'
identifier: 'footer'
}
]

View file

@ -1314,7 +1314,7 @@ class Table extends App.Controller
return if ticketListShow[0] || @permissionCheck('ticket.agent')
tickets_count = user.lifetimeCustomerTicketsCount()
@html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0, is_allowed_to_create_ticket: @Config.get('customer_ticket_create'))
@html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0)
if tickets_count == 0
@listenTo user, 'refresh', =>

View file

@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller
formMeta = data.form_meta
# on the following states we want to rerender the ticket:
# - if the object attribute configuration has changed (attribute values, dependecies, filters)
# - if the object attribute configuration has changed (attribute values, restrictions, filters)
# - if the user view has changed (agent/customer)
# - if the ticket permission has changed (read/write/full)
if @view && ( !_.isEqual(@formMeta.configure_attributes, formMeta.configure_attributes) || !_.isEqual(@formMeta.dependencies, formMeta.dependencies) || !_.isEqual(@formMeta.filter, formMeta.filter) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable )
if @view && ( !_.isEqual(@formMeta, formMeta) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable )
@renderDone = false
@view = view
@ -214,7 +214,6 @@ class App.TicketZoom extends App.Controller
# render page
@render(local)
App.Event.trigger('ui::ticket::load', data)
meta: =>

View file

@ -98,7 +98,7 @@ class App.TicketZoomArticleNew extends App.Controller
@controllerBind('ui:rerender', =>
@adjustedTextarea = false
@defaults = @ui.taskGet('article')
@attachments = @defaults.attachments || []
@attachments = @defaults.attachments
@render()
)
@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller
@tokanice(@type)
if @defaults.body or @attachments.length > 0 or @isIE10()
if @defaults.body or @isIE10()
@openTextarea(null, true)
tokanice: (type = 'email') ->
@ -191,30 +191,82 @@ class App.TicketZoomArticleNew extends App.Controller
maxlength: 150000
})
new App.Html5Upload(
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
dropContainer: @$('.article-add')
cancelContainer: @cancelContainer
inputField: @$('.article-attachment input')
html5Upload.initialize(
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
dropContainer: @$('.article-add').get(0)
cancelContainer: @cancelContainer
inputField: @$('.article-attachment input').get(0)
key: 'File'
maxSimultaneousUploads: 1
onFileAdded: (file) =>
onFileStartCallback: =>
@callbackFileUploadStart?()
file.on(
onFileCompletedCallback: (response) =>
@attachments.push response.data
@renderAttachment(response.data)
@$('.article-attachment input').val('')
onStart: =>
@attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
@cancelContainer.removeClass('hide')
@callbackFileUploadStop?()
if @callbackFileUploadStart
@callbackFileUploadStart()
onFileAbortedCallback: =>
@callbackFileUploadStop?()
onAborted: =>
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@$('.article-attachment input').val('')
attachmentPlaceholder: @attachmentPlaceholder
attachmentUpload: @attachmentUpload
progressBar: @progressBar
progressText: @progressText
).render()
if @callbackFileUploadStop
@callbackFileUploadStop()
# Called after received response from the server
onCompleted: (response) =>
response = JSON.parse(response)
@attachments.push response.data
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
# reset progress bar
@progressBar.width(parseInt(0) + '%')
@progressText.text('')
@renderAttachment(response.data)
@$('.article-attachment input').val('')
if @callbackFileUploadStop
@callbackFileUploadStop()
# Called during upload progress, first parameter
# is decimal value from 0 to 100.
onProgress: (progress, fileSize, uploadedBytes) =>
@progressBar.width(parseInt(progress) + '%')
@progressText.text(parseInt(progress))
# hide cancel on 90%
if parseInt(progress) >= 90
@cancelContainer.addClass('hide')
# Called when upload failed
onError: (message) =>
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@$('.article-attachment input').val('')
if @callbackFileUploadStop
@callbackFileUploadStop()
new App.ControllerModal(
head: 'Upload Failed'
buttonCancel: 'Cancel'
buttonCancelClass: 'btn--danger'
buttonSubmit: false
message: message
shown: true
small: true
container: @el.closest('.content')
)
)
)
@bindAttachmentDelete()

View file

@ -119,9 +119,7 @@ class App.FormHandlerCoreWorkflow
valueFound = false
for value in values
# false values are valid values e.g. for boolean fields (be careful)
if value isnt undefined && paramValue isnt undefined
if value && paramValue
if value.toString() == paramValue.toString()
valueFound = true
break

View file

@ -1,21 +1,12 @@
# No usage of a ControllerObserver here because we want to use
# the data of the ticket zoom ajax request which is using the all=true parameter
# and contain the core workflow information as well. Without observer we also
# dont have double rendering because of the zoom (all=true) and observer (full=true) render callback
class Edit extends App.Controller
constructor: (params) ->
super
@controllerBind('ui::ticket::load', (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString()
class Edit extends App.ControllerObserver
model: 'Ticket'
observeNot:
created_at: true
updated_at: true
globalRerender: false
@ticket = App.Ticket.find(@ticket.id)
@formMeta = data.form_meta
@render()
)
@render()
render: =>
defaults = @ticket.attributes()
render: (ticket, diff) =>
defaults = ticket.attributes()
delete defaults.article # ignore article infos
followUpPossible = App.Group.find(defaults.group_id).follow_up_possible
ticketState = App.TicketState.find(defaults.state_id).name
@ -25,13 +16,10 @@ class Edit extends App.Controller
if !_.isEmpty(taskState)
defaults = _.extend(defaults, taskState)
# remove core workflow data because it should trigger a request to get data
# for the new ticket + eventually changed task state
@formMeta.core_workflow = undefined
if followUpPossible == 'new_ticket' && ticketState != 'closed' ||
followUpPossible != 'new_ticket' ||
@permissionCheck('admin') || @ticket.currentView() is 'agent'
@permissionCheck('admin') || ticket.currentView() is 'agent'
@controllerFormSidebarTicket = new App.ControllerForm(
elReplace: @el
model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes }
@ -40,7 +28,7 @@ class Edit extends App.Controller
filter: @formMeta.filter
formMeta: @formMeta
params: defaults
isDisabled: !@ticket.editable()
isDisabled: !ticket.editable()
taskKey: @taskKey
core_workflow: {
callbacks: [@markForm]
@ -56,7 +44,7 @@ class Edit extends App.Controller
filter: @formMeta.filter
formMeta: @formMeta
params: defaults
isDisabled: @ticket.editable()
isDisabled: ticket.editable()
taskKey: @taskKey
core_workflow: {
callbacks: [@markForm]
@ -69,8 +57,8 @@ class Edit extends App.Controller
return if @resetBind
@resetBind = true
@controllerBind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString()
@render()
return if data.ticket_id.toString() isnt ticket.id.toString()
@render(ticket)
)
class SidebarTicket extends App.Controller
@ -140,7 +128,6 @@ class SidebarTicket extends App.Controller
@edit = new Edit(
object_id: @ticket.id
ticket: @ticket
el: localEl.find('.edit')
taskGet: @taskGet
formMeta: @formMeta

View file

@ -1,98 +0,0 @@
class App.Html5Upload extends App.Controller
uploadUrl: null
maxSimultaneousUploads: 1
key: 'File'
data: null
onFileStartCallback: null
onFileCompletedCallback: null
onFileAbortedCallback: null
dropContainer: null
cancelContainer: null
inputField: null
attachmentPlaceholder: null
attachmentUpload: null
progressBar: null
progressText: null
render: =>
html5Upload.initialize(
uploadUrl: @uploadUrl
dropContainer: @dropContainer.get(0)
cancelContainer: @cancelContainer
inputField: @inputField.get(0)
maxSimultaneousUploads: @maxSimultaneousUploads
key: @key
data: @data
onFileAdded: @onFileAdded
)
onFileAdded: (file) =>
file.on(
onStart: @onFileStart
onAborted: @onFileAborted
onCompleted: @onFileCompleted
onProgress: @onFileProgress
onError: @onFileError
)
onFileStart: =>
@attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
@cancelContainer.removeClass('hide')
App.Log.debug 'Html5Upload', 'upload start'
@onFileStartCallback?()
onFileProgress: (progress, fileSize, uploadedBytes) =>
progress = parseInt(progress)
@progressBar.width(progress + '%')
@progressText.text(progress)
# hide cancel on 90%
if progress >= 90
@cancelContainer.addClass('hide')
App.Log.debug 'Html5Upload', 'uploadProgress ', progress
onFileCompleted: (response) =>
response = JSON.parse(response)
@hideFileUploading()
@onFileCompletedCallback?(response)
App.Log.debug 'Html5Upload', 'upload complete', response.data
onFileAborted: =>
@hideFileUploading()
@onFileAbortedCallback?()
App.Log.debug 'Html5Upload', 'upload aborted'
onFileError: (message) =>
@hideFileUploading()
@inputField.val('')
@callbackFileUploadStop?()
new App.ControllerModal(
head: 'Upload Failed'
buttonCancel: 'Cancel'
buttonCancelClass: 'btn--danger'
buttonSubmit: false
message: message || 'Cannot upload file'
shown: true
small: true
container: @inputField.closest('.content')
)
App.Log.debug 'Html5Upload', 'upload error'
hideFileUploading: =>
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@progressBar.width('0%')
@progressText.text('0')

View file

@ -255,7 +255,7 @@
manager.ajaxUpload(manager.uploadsQueue.shift());
}
};
xhr.onabort = function (event) {
xhr.abort = function (event) {
console.log('Upload abort');
// Reduce number of active uploads:
@ -269,7 +269,6 @@
// Triggered when upload fails:
xhr.onerror = function () {
console.log('Upload failed: ', upload.fileName);
upload.events.onError('Upload failed: ' + upload.fileName);
};
// Append additional data if provided:

View file

@ -1,5 +1,5 @@
class App.KnowledgeBase extends App.Model
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'color_header_link', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
@extend Spine.Model.Ajax
@extend App.KnowledgeBaseActions
@url: @apiPath + '/knowledge_bases'
@ -148,17 +148,6 @@ class App.KnowledgeBase extends App.Model
display: false
horizontal: true
shown: true
}, {
name: 'color_header_link'
display: 'Header Link Color'
tag: 'color'
style: 'block'
null: false
screen:
admin_style_color_header_link:
display: false
horizontal: true
shown: true
# Layout picker is disabled in V1
#}, {
# name: 'homepage_layout'

View file

@ -344,12 +344,9 @@ class App.User extends App.Model
@sameOrganization?(requester)
isChangeableBy: (requester) ->
# full access for admins
return true if requester.permission('admin.user')
# forbid non-agents to change users
# allow agents to change customers
return false if !requester.permission('ticket.agent')
# allow agents to change customers only
return false if @permission(['admin.user', 'ticket.agent'])
@permission('ticket.customer')
isDeleteableBy: (requester) ->

View file

@ -6,15 +6,11 @@
<% if @has_any_tickets: %>
<p><%- @T('You have no tickets to display in this overview.') %></p>
<% else: %>
<% if @is_allowed_to_create_ticket: %>
<p><%- @T('You have not created a ticket yet.') %></p>
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
<p><%- @T('Please click the button below to create your first one.') %></p>
<p><%- @T('You have not created a ticket yet.') %></p>
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
<p><%- @T('Please click the button below to create your first one.') %></p>
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
<% else: %>
<p><%- @T('You currently don\'t have any tickets.') %></p>
<% end %>
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
<% end %>
</div>
</div>

View file

@ -14,7 +14,7 @@
</div>
<div class="form-group js-sure">
<h3 class="danger-color"><%- @T('Warning') %></h3>
<p class="danger-color"><%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translatePlain('delete').toUpperCase()) %></p>
<p class="danger-color"><%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translateInline('delete').toUpperCase()) %></p>
<%- @sure_html %>
</div>
</div>

View file

@ -17,7 +17,7 @@
<%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ...
</div>
<div class="attachmentUpload-cancel js-cancel">
<%- @Icon('diagonal-cross') %><%- @T('Cancel Upload') %>
<%- @Icon('diagonal-cross') %></div><%- @T('Cancel Upload') %>
</div>
</div>
<div class="attachmentUpload-progressBar" style="width: 0%"></div>

View file

@ -26,12 +26,11 @@ class App.KnowledgeBaseNewModal extends App.ControllerModal
App.UiElement[attribute.tag].prepareParams?(attribute, dom, params)
applyDefaults: (params) ->
params['iconset'] = 'FontAwesome'
params['color_highlight'] = '#38ae6a'
params['color_header'] = '#f9fafb'
params['color_header_link'] = 'hsl(206,8%,50%)'
params['homepage_layout'] = 'grid'
params['category_layout'] = 'grid'
params['iconset'] = 'FontAwesome'
params['color_highlight'] = '#38ae6a'
params['color_header'] = '#f9fafb'
params['homepage_layout'] = 'grid'
params['category_layout'] = 'grid'
onSubmit: (e) ->
params = @formParams(@el)

View file

@ -14,7 +14,7 @@
<div class="kb-menu-preview">
<div class="label"><%= kb_locale.systemLocale().name %></div>
<div class="kb-menu-preview-container kb-menu-preview-container--<%= location.identifier %>" style="background-color: <%= location.color %>; color: <%= location.color_link %>;">
<div class="kb-menu-preview-container kb-menu-preview-container--<%= location.identifier %>" style="background-color: <%= location.color %>">
<% menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, location.identifier) %>
<% if menu_items.length == 0: %>

View file

@ -1,7 +1,7 @@
<div class="login fullscreen">
<div class="fullscreen-center">
<div class="fullscreen-body">
<p><%- @T('Log in to %s', @C('fqdn')) %></p>
<p><%- @T('Login with %s', @C('fqdn')) %></p>
<% if @C('maintenance_mode'): %>
<div class="hero-unit alert alert--danger js-maintenanceMode"><%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %></div>

View file

@ -3,11 +3,6 @@
</div>
<div class="page-content">
<p>
<%- @T('The installation of packages comes with security implications, because arbitrary code will be executed in the context of the Zammad application.') %>
<br>
<%- @T('Only packages from known, trusted and verfied sources should be installed.') %>
</p>
<p>
<%- @T('After installing, updating or uninstalling packages the following commands need to be executed on the server:') %>
<ul>
@ -53,4 +48,4 @@
<% end %>
</tbody>
</table>
</div>
</div>

View file

@ -2661,7 +2661,7 @@ input.has-error {
}
a {
color: inherit;
color: hsl(206,8%,50%);
}
.label {
@ -7017,8 +7017,8 @@ footer {
.article-new .textBubble {
border-color: #b3b3b3;
border-radius: 5px;
padding-left: 12px;
padding-right: 12px;
padding-left: 0;
padding-right: 0;
cursor: text;
}
@ -7051,23 +7051,13 @@ footer {
padding: 10px 0;
color: #b3b3b3;
overflow: hidden;
@extend .u-textTruncate;
@extend .u-unclickable, .u-textTruncate;
}
.attachments:not(:empty) {
padding: 9px 5px;
border-top: 1px solid hsl(0,0%,93%);
margin: 6px -12px 30px;
}
.ticket-create .attachments:not(:empty) {
margin-left: 0;
margin-right: 0;
margin-bottom: 56px;
}
.ticket-create .attachment--row {
line-height: 1.45;
margin: 6px 0 30px;
}
.attachment.attachment--row {
@ -8452,10 +8442,6 @@ footer {
.dropdown li.with-category, .dropdown.dropdown--actions li.with-category {
line-height: 19.5px;
small {
color: #fff !important;
}
}
.dropdown.dropdown--actions li.with-category {

View file

@ -10,8 +10,8 @@ class ApplicationController < ActionController::Base
include ApplicationController::RendersModels
include ApplicationController::HasUser
include ApplicationController::HasResponseExtentions
include ApplicationController::HasDownload
include ApplicationController::PreventsCsrf
include ApplicationController::HasSecureContentSecurityPolicyForDownloads
include ApplicationController::LogsHttpAccess
include ApplicationController::Authorizes
end

View file

@ -1,44 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
module ApplicationController::HasDownload
extend ActiveSupport::Concern
included do
around_action do |_controller, block|
subscriber = proc do
policy = ActionDispatch::ContentSecurityPolicy.new
policy.default_src :none
# The 'plugin_types' rule is deprecated and should be changed in the future.
policy.plugin_types 'application/pdf'
request.content_security_policy = policy
end
ActiveSupport::Notifications.subscribed(subscriber, 'send_file.action_controller') do
ActiveSupport::Notifications.subscribed(subscriber, 'send_data.action_controller') do
block.call
end
end
end
end
private
def file_id
@file_id ||= params[:id]
end
def download_file
@download_file ||= ::ApplicationController::HasDownload::DownloadFile.new(file_id, disposition: sanitized_disposition)
end
def sanitized_disposition
disposition = params.fetch(:disposition, 'inline')
valid_disposition = %w[inline attachment]
return disposition if valid_disposition.include?(disposition)
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
end
end

View file

@ -1,54 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class ApplicationController::HasDownload::DownloadFile < SimpleDelegator
attr_reader :requested_disposition
def initialize(id, disposition: 'inline')
@requested_disposition = disposition
super(Store.find(id))
end
def disposition
return 'attachment' if forcibly_download_as_binary? || !allowed_inline?
requested_disposition
end
def content_type
return ActiveStorage.binary_content_type if forcibly_download_as_binary?
file_content_type
end
def content(view_type)
return __getobj__.content if view_type.blank? || !preferences[:resizable]
return content_inline if content_inline? && view_type == 'inline'
return content_preview if content_preview? && view_type == 'preview'
__getobj__.content
end
private
def allowed_inline?
ActiveStorage.content_types_allowed_inline.include?(content_type)
end
def forcibly_download_as_binary?
ActiveStorage.content_types_to_serve_as_binary.include?(file_content_type)
end
def file_content_type
@file_content_type ||= preferences['Content-Type'] || preferences['Mime-Type'] || ActiveStorage.binary_content_type
end
def content_inline?
preferences[:content_inline] == true
end
def content_preview?
preferences[:content_preview] == true
end
end

View file

@ -0,0 +1,25 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
module ApplicationController::HasSecureContentSecurityPolicyForDownloads
extend ActiveSupport::Concern
included do
around_action do |_controller, block|
subscriber = proc do
policy = ActionDispatch::ContentSecurityPolicy.new
policy.default_src :none
policy.plugin_types 'application/pdf'
request.content_security_policy = policy
end
ActiveSupport::Notifications.subscribed(subscriber, 'send_file.action_controller') do
ActiveSupport::Notifications.subscribed(subscriber, 'send_data.action_controller') do
block.call
end
end
end
end
end

View file

@ -6,13 +6,14 @@ class AttachmentsController < ApplicationController
prepend_before_action :authentication_check_only, only: %i[show destroy]
def show
view_type = params[:preview] ? 'preview' : nil
content = @file.content_preview if params[:preview] && @file.preferences[:content_preview]
content ||= @file.content
send_data(
download_file.content(view_type),
filename: download_file.filename,
type: download_file.content_type,
disposition: download_file.disposition
content,
filename: @file.filename,
type: @file.preferences['Content-Type'] || @file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: sanitized_disposition
)
end
@ -51,7 +52,7 @@ class AttachmentsController < ApplicationController
end
def destroy
Store.remove_item(download_file.id)
Store.remove_item(@file.id)
render json: {
success: true,
@ -71,8 +72,18 @@ class AttachmentsController < ApplicationController
private
def sanitized_disposition
disposition = params.fetch(:disposition, 'inline')
valid_disposition = %w[inline attachment]
return disposition if valid_disposition.include?(disposition)
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
end
def authorize!
record = download_file&.store_object&.name&.safe_constantize&.find(download_file.o_id)
@file = Store.find(params[:id])
record = @file&.store_object&.name&.safe_constantize&.find(@file.o_id)
authorize(record) if record
rescue Pundit::NotAuthorizedError
raise ActiveRecord::RecordNotFound

View file

@ -156,7 +156,7 @@ class FormController < ApplicationController
end
def token_gen(fingerprint)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32], serializer: JSON)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32])
fingerprint = "#{Base64.strict_encode64(Setting.get('fqdn'))}:#{Time.zone.now.to_i}:#{Base64.strict_encode64(fingerprint)}"
Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint))
end
@ -167,7 +167,7 @@ class FormController < ApplicationController
raise Exceptions::Forbidden
end
begin
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32], serializer: JSON)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32])
result = crypt.decrypt_and_verify(Base64.decode64(token))
rescue
Rails.logger.info 'Invalid token for form!'

View file

@ -175,11 +175,29 @@ class TicketArticlesController < ApplicationController
end
raise Exceptions::Forbidden, 'Requested file id is not linked with article_id.' if !access
# find file
file = Store.find(params[:id])
disposition = sanitized_disposition
content = nil
if params[:view].present? && file.preferences[:resizable] == true
if file.preferences[:content_inline] == true && params[:view] == 'inline'
content = file.content_inline
elsif file.preferences[:content_preview] == true && params[:view] == 'preview'
content = file.content_preview
end
end
if content.blank?
content = file.content
end
send_data(
download_file.content(params[:view]),
filename: download_file.filename,
type: download_file.content_type,
disposition: download_file.disposition
content,
filename: file.filename,
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: disposition
)
end
@ -260,4 +278,14 @@ class TicketArticlesController < ApplicationController
render json: result
end
private
def sanitized_disposition
disposition = params.fetch(:disposition, 'inline')
valid_disposition = %w[inline attachment]
return disposition if valid_disposition.include?(disposition)
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
end
end

View file

@ -722,28 +722,31 @@ curl http://localhost/api/v1/users/image/8d6cca1c6bdc226cf2ba131e264ca2c7 -v -u
=end
def image
# cache image
response.headers['Expires'] = 1.year.from_now.httpdate
response.headers['Cache-Control'] = 'cache, store, max-age=31536000, must-revalidate'
response.headers['Pragma'] = 'cache'
file = Avatar.get_by_hash(params[:hash])
if file
file_content_type = file.preferences['Content-Type'] || file.preferences['Mime-Type']
return serve_default_image if ActiveStorage.content_types_allowed_inline.exclude?(file_content_type)
send_data(
file.content,
filename: file.filename,
type: file_content_type,
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'],
disposition: 'inline'
)
return
end
serve_default_image
# serve default image
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
send_data(
Base64.decode64(image),
filename: 'image.gif',
type: 'image/gif',
disposition: 'inline'
)
end
=begin
@ -775,11 +778,6 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
return
end
if ActiveStorage::Variant::WEB_IMAGE_CONTENT_TYPES.exclude?(file_full[:mime_type])
render json: { error: 'Mime type is invalid' }, status: :unprocessable_entity
return
end
begin
file_resize = StaticAssets.data_url_attributes(params[:avatar_resize])
rescue
@ -1063,15 +1061,4 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
render json: { message: 'ok' }, status: :created
end
def serve_default_image
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
send_data(
Base64.decode64(image),
filename: 'image.gif',
type: 'image/gif',
disposition: 'inline'
)
end
end

View file

@ -4,7 +4,9 @@ class WebhooksController < ApplicationController
prepend_before_action { authentication_check && authorize! }
def preview
ticket = TicketPolicy::ReadScope.new(current_user).resolve.last
access_condition = Ticket.access_condition(current_user, 'read')
ticket = Ticket.where(access_condition).last
render json: JSON.pretty_generate({
ticket: TriggerWebhookJob::RecordPayload.generate(ticket),

View file

@ -3,7 +3,6 @@
class UploadCacheCleanupJob < ApplicationJob
def perform
taskbar_form_ids = Taskbar.with_form_id.filter_map(&:persisted_form_id)
return if store_object_id.blank?
Store.where(store_object_id: store_object_id).where('created_at < ?', 1.month.ago).where.not(o_id: taskbar_form_ids).find_each do |store|
Store.remove_item(store.id)
@ -13,6 +12,6 @@ class UploadCacheCleanupJob < ApplicationJob
private
def store_object_id
Store::Object.lookup(name: 'UploadCache')&.id
Store::Object.lookup(name: 'UploadCache').id
end
end

View file

@ -121,7 +121,7 @@ returns
key = "#{self.class}::aws::#{id}"
cache = Cache.read(key)
return filter_unauthorized_attributes(cache) if cache
return cache if cache
attributes = self.attributes
relevant = %i[has_and_belongs_to_many has_many]
@ -160,7 +160,7 @@ returns
filter_attributes(attributes)
Cache.write(key, attributes)
filter_unauthorized_attributes(attributes)
attributes
end
=begin
@ -234,7 +234,8 @@ returns
end
filter_attributes(attributes)
filter_unauthorized_attributes(attributes)
attributes
end
def filter_attributes(attributes)
@ -242,10 +243,6 @@ returns
attributes.except!('password', 'token', 'tokens', 'token_ids')
end
def filter_unauthorized_attributes(attributes)
attributes
end
=begin
reference if association id check

View file

@ -72,6 +72,7 @@ add avatar by url
=end
def self.add(data)
# lookups
if data[:object]
object_id = ObjectLookup.by_name(data[:object])

View file

@ -7,8 +7,6 @@ class Channel::Driver::Imap < Channel::EmailParser
FETCH_METADATA_TIMEOUT = 2.minutes
FETCH_MSG_TIMEOUT = 4.minutes
EXPUNGE_TIMEOUT = 16.minutes
DEFAULT_TIMEOUT = 45.seconds
CHECK_ONLY_TIMEOUT = 6.seconds
def fetchable?(_channel)
true
@ -112,7 +110,10 @@ example
Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},starttls=#{starttls},folder=#{folder},keep_on_server=#{keep_on_server},auth_type=#{options.fetch(:auth_type, 'LOGIN')})"
# on check, reduce open_timeout to have faster probing
check_type_timeout = check_type == 'check' ? CHECK_ONLY_TIMEOUT : DEFAULT_TIMEOUT
check_type_timeout = 45
if check_type == 'check'
check_type_timeout = 6
end
timeout(check_type_timeout) do
@imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false)

View file

@ -502,7 +502,7 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
path = Rails.root.join('tmp/unprocessable_mail')
files = []
Dir.glob("#{path}/*.eml") do |entry|
ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, File.binread(entry))
ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, IO.binread(entry))
next if ticket.blank?
files.push entry

View file

@ -30,26 +30,10 @@ class CoreWorkflow::Attributes
end
end
def selectable_field?(key)
return if key == 'id'
return if !@payload['params'].key?(key)
# some objects have no attributes like "CoreWorkflow"-object as well.
# attributes only exists in the frontend so we skip this check
return true if object_elements.blank?
object_elements_hash.key?(key)
end
def overwrite_selected(result)
selected_attributes = selected_only.attributes
selected_attributes.each_key do |key|
next if !selectable_field?(key)
# special behaviour for owner id
if key == 'owner_id' && selected_attributes[key].nil?
selected_attributes[key] = 1
end
next if selected_attributes[key].nil?
result[key.to_sym] = selected_attributes[key]
end
@ -71,10 +55,7 @@ class CoreWorkflow::Attributes
# dont use lookup here because the cache will not
# know about new attributes and make crashes
@saved_only ||= payload_class.find_by(id: @payload['params']['id'])
# we use marshal here because clone still uses references and dup can't
# detect changes for the rails object
Marshal.load(Marshal.dump(@saved_only))
@saved_only.dup
end
def saved
@ -87,10 +68,6 @@ class CoreWorkflow::Attributes
end
end
def object_elements_hash
@object_elements_hash ||= object_elements.index_by { |x| x[:name] }
end
def screen_value(attribute, type)
attribute[:screens].dig(@payload['screen'], type)
end

View file

@ -18,26 +18,4 @@ class CoreWorkflow::Result::Backend
def result(backend, field, value = nil)
@result_object.run_backend_value(backend, field, value)
end
def saved_value
# make sure we have a saved object
return if @result_object.attributes.saved_only.blank?
# we only want to have the saved value in the restrictions
# if no changes happend to the form. If the users does changes
# to the form then also the saved value should get removed
return if @result_object.attributes.selected.changed?
# attribute can be blank e.g. in custom development
# or if attribute is only available in the frontend but not
# in the backend
return if attribute.blank?
@result_object.attributes.saved_attribute_value(attribute).to_s
end
def attribute
@attribute ||= @result_object.attributes.object_elements_hash[field]
end
end

View file

@ -3,14 +3,8 @@
class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption
def run
@result_object.result[:restrict_values][field] ||= Array(@result_object.payload['params'][field])
@result_object.result[:restrict_values][field] -= Array(config_value)
@result_object.result[:restrict_values][field] -= Array(@perform_config['remove_option'])
remove_excluded_param_values
true
end
def config_value
result = Array(@perform_config['remove_option'])
result -= Array(saved_value)
result
end
end

View file

@ -5,23 +5,21 @@ class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption
@result_object.result[:restrict_values][field] = if restriction_set?
restrict_values
else
config_value
replace_values
end
remove_excluded_param_values
true
end
def config_value
result = Array(@perform_config['set_fixed_to'])
result |= Array(saved_value)
result
end
def restriction_set?
@result_object.result[:restrict_values][field]
end
def restrict_values
@result_object.result[:restrict_values][field].reject { |v| config_value.exclude?(v) }
@result_object.result[:restrict_values][field].reject { |v| Array(@perform_config['set_fixed_to']).exclude?(v) }
end
def replace_values
Array(@perform_config['set_fixed_to'])
end
end

View file

@ -12,8 +12,6 @@ class Group < ApplicationModel
include HasTicketCreateScreenImpact
include HasSearchIndexBackend
include Group::Assets
belongs_to :email_address, optional: true
belongs_to :signature, optional: true

View file

@ -1,14 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Group
module Assets
extend ActiveSupport::Concern
def filter_unauthorized_attributes(attributes)
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
attributes = super
attributes.slice('id', 'name', 'active')
end
end
end

View file

@ -27,9 +27,8 @@ class KnowledgeBase < ApplicationModel
validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
validates :color_highlight, presence: true, color: true
validates :color_header, presence: true, color: true
validates :color_header_link, presence: true, color: true
validates :color_highlight, presence: true, color: true
validates :color_header, presence: true, color: true
validates :iconset, inclusion: { in: KnowledgeBase::ICONSETS }

View file

@ -678,7 +678,7 @@ to send no browser reload event, pass false
=begin
where attributes are used in conditions
where attributes are used by triggers, overviews or schedulers
result = ObjectManager::Attribute.attribute_to_references_hash
@ -696,36 +696,22 @@ where attributes are used in conditions
=end
def self.attribute_to_references_hash
objects = Trigger.select(:name, :condition) + Overview.select(:name, :condition) + Job.select(:name, :condition)
attribute_list = {}
objects.each do |item|
item.condition.each do |condition_key, _condition_attributes|
attribute_list[condition_key] ||= {}
attribute_list[condition_key][item.class.name] ||= []
next if attribute_list[condition_key][item.class.name].include?(item.name)
attribute_to_references_hash_objects
.map { |elem| elem.select(:name, :condition) }
.flatten
.each do |item|
item.condition.each do |condition_key, _condition_attributes|
attribute_list[condition_key] ||= {}
attribute_list[condition_key][item.class.name] ||= []
next if attribute_list[condition_key][item.class.name].include?(item.name)
attribute_list[condition_key][item.class.name].push item.name
end
attribute_list[condition_key][item.class.name].push item.name
end
end
attribute_list
end
=begin
models that may reference attributes
=end
def self.attribute_to_references_hash_objects
Models.all.keys.select { |elem| elem.include? ChecksConditionValidation }
end
=begin
is certain attribute used by triggers, overviews or schedulers
ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')

View file

@ -43,7 +43,7 @@ class ObjectManager::Element::Backend
end
def screens
@screens ||= attribute.screens.transform_values do |permission_options|
attribute.screens.transform_values do |permission_options|
screen_value(permission_options)
end
end

View file

@ -70,12 +70,5 @@ returns
end
data
end
def filter_unauthorized_attributes(attributes)
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
attributes = super
attributes.slice('id', 'name', 'active')
end
end
end

View file

@ -263,37 +263,32 @@ subsequently in a separate step.
)
end
Transaction.execute do
# store package
if !data[:reinstall]
package_db = Package.create(meta)
Store.add(
object: 'Package',
o_id: package_db.id,
data: package.to_json,
filename: "#{meta[:name]}-#{meta[:version]}.zpm",
preferences: {},
created_by_id: UserInfo.current_user_id || 1,
)
end
# write files
package['files'].each do |file|
if !allowed_file_path?(file['location'])
raise "Can't create file, because of not allowed file location: #{file['location']}!"
end
permission = file['permission'] || '644'
content = Base64.decode64(file['content'])
_write_file(file['location'], permission, content)
end
# update package state
package_db.state = 'installed'
package_db.save
# store package
if !data[:reinstall]
package_db = Package.create(meta)
Store.add(
object: 'Package',
o_id: package_db.id,
data: package.to_json,
filename: "#{meta[:name]}-#{meta[:version]}.zpm",
preferences: {},
created_by_id: UserInfo.current_user_id || 1,
)
end
# write files
package['files'].each do |file|
permission = file['permission'] || '644'
content = Base64.decode64(file['content'])
_write_file(file['location'], permission, content)
end
# update package state
package_db.state = 'installed'
package_db.save
# prebuild assets
package_db
end
@ -488,9 +483,4 @@ execute all pending package migrations at once
true
end
def self.allowed_file_path?(file)
file.exclude?('..') && file.exclude?('%2e%2e')
end
private_class_method :allowed_file_path?
end

View file

@ -2,7 +2,6 @@
class Report::Profile < ApplicationModel
self.table_name = 'report_profiles'
include ChecksConditionValidation
validates :name, presence: true
store :condition

View file

@ -60,13 +60,5 @@ returns
end
data
end
def filter_unauthorized_attributes(attributes)
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
attributes = super
attributes['name'] = "Role_#{id}"
attributes.slice('id', 'name', 'group_ids', 'permission_ids', 'active')
end
end
end

View file

@ -926,16 +926,17 @@ try to find correct name
end
# check if login already exists
base_login = login.downcase.strip
alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ]
alternatives.each do |suffix|
self.login = "#{base_login}#{suffix}"
self.login = login.downcase.strip
check = true
while check
exists = User.find_by(login: login)
return true if !exists || exists.id == id
if exists && exists.id != id
self.login = "#{login}#{rand(999)}" # rubocop:disable Zammad/ForbidRand
else
check = false
end
end
raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
true
end
def check_mail_delivery_failed

View file

@ -110,20 +110,5 @@ returns
end
data
end
def filter_unauthorized_attributes(attributes)
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
# customer assets for the user session
if UserInfo.current_user_id == id
attributes = super
attributes.except!('web', 'phone', 'mobile', 'fax', 'department', 'street', 'zip', 'city', 'country', 'address', 'note')
return attributes
end
# customer assets for other user
attributes = super
attributes.slice('id', 'firstname', 'lastname', 'image', 'image_source', 'active')
end
end
end

View file

@ -13,7 +13,6 @@ class SettingPolicy < ApplicationPolicy
private
def permitted?
return false if record.preferences[:protected]
return true if !record.preferences[:permission]
user.permissions?(record.preferences[:permission])

View file

@ -13,7 +13,7 @@ class TicketPolicy < ApplicationPolicy
super
end
def resolve # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def resolve # rubocop:disable Metrics/AbcSize
raise NoMethodError, <<~ERR.chomp if instance_of?(TicketPolicy::BaseScope)
specify an access type using a subclass of TicketPolicy::BaseScope
ERR
@ -26,19 +26,12 @@ class TicketPolicy < ApplicationPolicy
bind.push(user.group_ids_access(self.class::ACCESS_TYPE))
end
if user.permissions?('ticket.customer')
if user.organization&.shared
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
bind.push(user.id, user.organization.id)
else
sql.push('tickets.customer_id = ?')
bind.push(user.id)
end
end
# The report permission can access all tickets.
if sql.empty? && !user.permissions?('report')
sql.push '0 = 1' # Forbid unlimited access for all other permissions.
if user.organization&.shared
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
bind.push(user.id, user.organization.id)
else
sql.push('tickets.customer_id = ?')
bind.push(user.id)
end
scope.where sql.join(' OR '), *bind

View file

@ -13,14 +13,11 @@ class UserPolicy < ApplicationPolicy
end
def update?
# full access for admins
return true if user.permissions?('admin.user')
# forbid non-agents to change users
return false if !user.permissions?('ticket.agent')
# allow agents to change customers only
return false if record.permissions?(['admin.user', 'ticket.agent'])
# allow agents to change customers
record.permissions?('ticket.customer')
end

View file

@ -28,8 +28,4 @@
.header {
background-color: <%= knowledge_base.color_header %>;
}
.header .menu-item {
color: <%= knowledge_base.color_header_link %>;
}
</style>

View file

@ -5,11 +5,11 @@ Rails.application.config.html_sanitizer_tags_remove_content = %w[
style
comment
meta
script
]
# content of this tags will will be inserted html quoted
Rails.application.config.html_sanitizer_tags_quote_content = %w[
script
]
# only this tags are allowed

View file

@ -21,7 +21,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
# Rails thinks the singularized version of knowledge_bases is knowledge_basis?!
# see: KnowledgeBase.table_name.singularize
inflect.irregular 'base', 'bases'
inflect.singular(%r{(knowledge_base)s$}i, '\1')
inflect.acronym 'SMIME'
inflect.acronym 'GitLab'
inflect.acronym 'GitHub'

View file

@ -228,7 +228,7 @@ function create_webserver_config () {
function setup_elasticsearch () {
echo "# Configuring Elasticsearch..."
ES_CONNECTION="$(zammad run rails r "puts '',Setting.get('es_url')"| tail -n 1 2>> /dev/null)"
ES_CONNECTION="$(zammad run rails r "puts Setting.get('es_url')"| tail -n 1 2>> /dev/null)"
if [ -z "${ES_CONNECTION}" ]; then
echo "-- Nevermind, no es_url is set, leaving Elasticsearch untouched ...!"
@ -274,10 +274,6 @@ function elasticsearch_searchindex_rebuild () {
function update_or_install () {
if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then
echo "# Clear cache..."
zammad run rails r Cache.clear
update_database
update_translations

View file

@ -8,9 +8,8 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
create_table :knowledge_bases do |t|
t.string :iconset, limit: 30, null: false
t.string :color_highlight, limit: 25, null: false
t.string :color_header, limit: 25, null: false
t.string :color_header_link, limit: 25, null: false
t.string :color_highlight, limit: 25, null: false
t.string :color_header, limit: 25, null: false
t.string :homepage_layout, null: false
t.string :category_layout, null: false

View file

@ -75,8 +75,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
def fix_pending_time
pending_time = ObjectManager::Attribute.find_by(name: 'pending_time', object_lookup: ObjectLookup.find_by(name: 'Ticket'))
return if pending_time.blank?
pending_time.data_option.delete('required_if')
pending_time.data_option.delete('shown_if')
pending_time.save
@ -85,8 +83,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
def fix_organization_screens
%w[domain note].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
next if field.blank?
field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = true
@ -97,8 +93,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
def fix_user_screens
%w[email web phone mobile organization_id fax department street zip city country address password vip note role_ids].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
next if field.blank?
field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = true

View file

@ -1,15 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class MaintenanceImproveSettingPreferences < ActiveRecord::Migration[6.0]
def change
return if !Setting.exists?(name: 'system_init_done')
protected_settings = %w[application_secret]
protected_settings.each do |name|
setting = Setting.find_by(name: name)
setting.preferences[:protected] = true
setting.save!
end
end
end

View file

@ -11,8 +11,6 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0]
def fix_organization_screens_create
%w[name shared domain_assignment active].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
next if field.blank?
field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = false
@ -23,8 +21,6 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0]
def fix_user_screens_create
%w[firstname lastname active].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
next if field.blank?
field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = false

View file

@ -1,11 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Issue2619KbHeaderLinkColor < ActiveRecord::Migration[6.0]
def up
return if !Setting.exists?(name: 'system_init_done')
add_column :knowledge_bases, :color_header_link, :string, limit: 25, null: false, default: 'hsl(206,8%,50%)'
change_column_default :knowledge_bases, :color_header_link, nil
KnowledgeBase.reset_column_information
end
end

View file

@ -1,11 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class ReloadAfterCoreWorkflow < ActiveRecord::Migration[6.0]
def up
# return if it's a new setup
return if !Setting.exists?(name: 'system_init_done')
AppVersion.set(true, 'app_version')
end
end

View file

@ -1,11 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class ReloadAfterCoreWorkflowAgain < ActiveRecord::Migration[6.0]
def up
# return if it's a new setup
return if !Setting.exists?(name: 'system_init_done')
AppVersion.set(true, 'app_version')
end
end

View file

@ -1,11 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Issue3787FixJob < ActiveRecord::Migration[6.0]
def change
# return if it's a new setup
return if !Setting.exists?(name: 'system_init_done')
Scheduler.find_by(name: 'Delete old upload cache entries.').update(error_message: nil, status: nil, active: true)
end
end

View file

@ -9,7 +9,6 @@ Setting.create_if_not_exists(
state: SecureRandom.hex(128),
preferences: {
permission: ['admin'],
protected: true,
},
frontend: false
)

View file

@ -1,5 +1,7 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'faker'
# rubocop:disable Rails/Output
module FillDb
@ -53,7 +55,7 @@ or if you only want to create 100 tickets
else
(1..organizations).each do
ActiveRecord::Base.transaction do
organization = Organization.create!(name: "FillOrganization::#{counter}", active: true)
organization = Organization.create!(name: "FillOrganization::#{Faker::Number.number(digits: 6)}", active: true)
organization_pool.push organization
end
end
@ -70,7 +72,7 @@ or if you only want to create 100 tickets
(1..agents).each do
ActiveRecord::Base.transaction do
suffix = counter.to_s
suffix = Faker::Number.number(digits: 5).to_s
user = User.create_or_update(
login: "filldb-agent-#{suffix}",
firstname: "agent #{suffix}",
@ -100,7 +102,7 @@ or if you only want to create 100 tickets
(1..customers).each do
ActiveRecord::Base.transaction do
suffix = counter.to_s
suffix = Faker::Number.number(digits: 5).to_s
organization = nil
if organization_pool.present? && true_or_false.sample
organization = organization_pool.sample
@ -130,7 +132,7 @@ or if you only want to create 100 tickets
else
(1..groups).each do
ActiveRecord::Base.transaction do
group = Group.create!(name: "FillGroup::#{counter}", active: true)
group = Group.create!(name: "FillGroup::#{Faker::Number.number(digits: 6)}", active: true)
group_pool.push group
Role.where(name: 'Agent').first.users.where(active: true).each do |user|
user_groups = user.groups
@ -148,7 +150,7 @@ or if you only want to create 100 tickets
(1..overviews).each do
ActiveRecord::Base.transaction do
Overview.create!(
name: "Filloverview::#{counter}",
name: "Filloverview::#{Faker::Number.number(digits: 6)}",
role_ids: [Role.find_by(name: 'Agent').id],
condition: {
'ticket.state_id' => {
@ -183,7 +185,7 @@ or if you only want to create 100 tickets
customer = customer_pool.sample
agent = agent_pool.sample
ticket = Ticket.create!(
title: "some title äöüß#{counter}",
title: "some title äöüß#{Faker::Number.number(digits: 6)}",
group: group_pool.sample,
customer: customer,
owner: agent,
@ -198,8 +200,8 @@ or if you only want to create 100 tickets
ticket_id: ticket.id,
from: customer.email,
to: 'some_recipient@example.com',
subject: "some subject#{counter}",
message_id: "some@id-#{counter}",
subject: "some subject#{Faker::Number.number(digits: 6)}",
message_id: "some@id-#{Faker::Number.number(digits: 6)}",
body: 'some message ...',
internal: false,
sender: Ticket::Article::Sender.where(name: 'Customer').first,
@ -212,10 +214,5 @@ or if you only want to create 100 tickets
end
end
end
def self.counter
@counter ||= SecureRandom.random_number(1_000_000)
@counter += 1
end
end
# rubocop:enable Rails/Output

View file

@ -1,14 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'uri'
class GitHub
class HttpClient
attr_reader :api_token, :endpoint
def initialize(endpoint, api_token)
raise 'api_token required' if api_token.blank?
raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank?
raise 'endpoint required' if endpoint.blank?
@api_token = api_token
@endpoint = endpoint
@ -32,7 +30,7 @@ class GitHub
if !response.success?
Rails.logger.error response.error
raise 'GitHub request failed! Please have a look at the log file for details'
raise "Error while requesting GitHub GraphQL API: #{response.error}"
end
response.data

View file

@ -1,14 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'uri'
class GitLab
class HttpClient
attr_reader :api_token, :endpoint
def initialize(endpoint, api_token)
raise 'api_token required' if api_token.blank?
raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank?
raise 'endpoint required' if endpoint.blank?
@api_token = api_token
@endpoint = endpoint
@ -32,7 +30,7 @@ class GitLab
if !response.success?
Rails.logger.error response.error
raise 'GitLab request failed! Please have a look at the log file for details'
raise "Error while requesting GitLab GraphQL API: #{response.error}"
end
response.data

View file

@ -161,8 +161,8 @@ satinize html string based on whiltelist
# wrap plain-text URLs in <a> tags
if node.is_a?(Nokogiri::XML::Text) && node.content.present? && node.content.include?(':') && node.ancestors.map(&:name).exclude?('a')
urls = URI.extract(node.content, LINKABLE_URL_SCHEMES)
.map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas
.grep_v(%r{^[^:]+:$}) # URI::extract will match, e.g., 'tel:'
.map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas
.reject { |u| u.match?(%r{^[^:]+:$}) } # URI::extract will match, e.g., 'tel:'
next if urls.blank?

View file

@ -324,8 +324,7 @@ returns
locale: data[:locale],
timezone: data[:timezone],
template: template[:subject],
escape: false,
trusted: true,
escape: false
).render
# strip off the extra newline at the end of the subject to avoid =0A suffixes (see #2726)
@ -335,8 +334,7 @@ returns
objects: data[:objects],
locale: data[:locale],
timezone: data[:timezone],
template: template[:body],
trusted: true,
template: template[:body]
).render
if !data[:raw]
@ -350,8 +348,7 @@ returns
objects: data[:objects],
locale: data[:locale],
timezone: data[:timezone],
template: application_template,
trusted: true,
template: application_template
).render
end
{

View file

@ -13,8 +13,7 @@ examples how to use
locale: 'de-de',
timezone: 'America/Port-au-Prince',
template: 'some template <b>#{ticket.title}</b> {config.fqdn}',
escape: false,
trusted: false, # Allow ERB tags in the template?
escape: false
).render
message_body = NotificationFactory::Renderer.new(
@ -28,20 +27,16 @@ examples how to use
=end
def initialize(objects:, template:, locale: nil, timezone: nil, escape: true, trusted: false) # rubocop:disable Metrics/ParameterLists
def initialize(objects:, template:, locale: nil, timezone: nil, escape: true)
@objects = objects
@locale = locale || Locale.default
@timezone = timezone || Setting.get('timezone_default')
@template = NotificationFactory::Template.new(template, escape, trusted)
@template = NotificationFactory::Template.new(template, escape)
@escape = escape
end
def render
ERB.new(@template.to_s).result(binding)
rescue Exception => e # rubocop:disable Lint/RescueException
raise StandardError, e.message if e.is_a? SyntaxError
raise
end
# d - data of object

View file

@ -46,16 +46,14 @@ returns
locale: data[:locale],
timezone: data[:timezone],
template: template[:subject],
escape: false,
trusted: true
escape: false
).render
message_body = NotificationFactory::Renderer.new(
objects: data[:objects],
locale: data[:locale],
timezone: data[:timezone],
template: template[:body],
escape: false,
trusted: true
escape: false
).render
if !data[:raw]
@ -70,8 +68,7 @@ returns
locale: data[:locale],
timezone: data[:timezone],
template: application_template,
escape: false,
trusted: true
escape: false
).render
end
{

View file

@ -9,21 +9,17 @@ examples how to use
cleaned_template = NotificationFactory::Template.new(
'some template <b>#{ticket.title}</b> #{config.fqdn}',
true,
false, # Allow ERB tags in the template?
).to_s
=end
def initialize(template, escape, trusted)
def initialize(template, escape)
@template = template
@escape = escape
@trusted = trusted
@escape = escape
end
def to_s
result = @template
result.gsub!(%r{<%(?!%)}, '<%%') if !@trusted
result.gsub(%r{\#{\s*(.*?)\s*}}m) do
@template.gsub(%r{\#{\s*(.*?)\s*}}m) do
# some browsers start adding HTML tags
# fixes https://github.com/zammad/zammad/issues/385
input_template = $1.gsub(%r{\A<.+?>\s*|\s*<.+?>\z}, '')

View file

@ -4,7 +4,7 @@ module SessionHelper
def self.json_hash(user)
collections, assets = default_collections(user)
{
session: user.filter_unauthorized_attributes(user.filter_attributes(user.attributes)),
session: user.filter_attributes(user.attributes),
models: models(user),
collections: collections,
assets: assets,

View file

@ -7,11 +7,6 @@ module UserInfo
def self.current_user_id=(user_id)
Thread.current[:user_id] = user_id
Thread.current[:assets] = UserInfo::Assets.new(user_id)
end
def self.assets
Thread.current[:assets]
end
def self.ensure_current_user_id

View file

@ -1,52 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class UserInfo::Assets
LEVEL_CUSTOMER = 1
LEVEL_AGENT = 2
LEVEL_ADMIN = 3
attr_accessor :current_user_id, :level, :filter_attributes, :user
def initialize(current_user_id)
@current_user_id = current_user_id
@user = User.find_by(id: current_user_id) if current_user_id.present?
set_level
end
def admin?
check_level?(UserInfo::Assets::LEVEL_ADMIN)
end
def agent?
check_level?(UserInfo::Assets::LEVEL_AGENT)
end
def customer?
check_level?(UserInfo::Assets::LEVEL_CUSTOMER)
end
def set_level
if user.blank?
self.level = nil
return
end
self.level = UserInfo::Assets::LEVEL_CUSTOMER
Permission.where(id: user.permissions_with_child_ids).each do |permission|
case permission.name
when %r{^admin\.}
self.level = UserInfo::Assets::LEVEL_ADMIN
break
when 'ticket.agent'
self.level = UserInfo::Assets::LEVEL_AGENT
end
end
end
def check_level?(check)
return true if user.blank?
level >= check
end
end

View file

@ -1,16 +0,0 @@
FROM node:8-alpine
ENV GULP_DIR "/tmp/gulp"
RUN apk update && apk add bash
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
CMD bash # If you want to override CMD
RUN npm install -g gulp
COPY docker-entrypoint.sh /
# enable volume to generate build files into the hosts FS
VOLUME ["$GULP_DIR"]
# start
ENTRYPOINT ["/docker-entrypoint.sh"]

View file

@ -1,5 +0,0 @@
# Zammad Chat build
This folder contains a `docker` image and the required files to build the Zammad Chat from coffeescript and eco files. This workaround is required for now because of the outdated NodeJS 8 dependency.
The build process can easily be started by executing the `build.sh` file. There is nothing more to it except of having `docker` installed and running.

View file

@ -1,8 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
docker build --no-cache -t zammad/chat-build:latest .
docker run --rm -v "$(pwd)/:/tmp/gulp" zammad/chat-build:latest

View file

@ -762,11 +762,7 @@ do(window) ->
console.log('p', docType, text)
if docType is 'html'
html = document.createElement('div')
# can't log because might contain malicious content
# @log.debug 'HTML clipboard', text
sanitized = DOMPurify.sanitize(text)
@log.debug 'sanitized HTML clipboard', sanitized
html.innerHTML = sanitized
html.innerHTML = text
match = false
htmlTmp = text
regex = new RegExp('<(/w|w)\:[A-Za-z]')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -718,9 +718,7 @@ do($ = window.jQuery, window) ->
text = text.replace(/<div><\/div>/g, '<div><br></div>')
console.log('p', docType, text)
if docType is 'html'
sanitized = DOMPurify.sanitize(text)
@log.debug 'sanitized HTML clipboard', sanitized
html = $("<div>#{sanitized}</div>")
html = $("<div>#{text}</div>")
match = false
htmlTmp = text
regex = new RegExp('<(/w|w)\:[A-Za-z]')

View file

@ -314,7 +314,6 @@
line-height: 1.4em;
font-size: inherit;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background: none;
@ -330,7 +329,6 @@
.zammad-chat-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
font-family: inherit;
font-size: inherit;
@ -351,7 +349,6 @@
.zammad-chat-button:disabled,
.zammad-chat-input:disabled {
cursor: not-allowed;
opacity: 0.3; }
.zammad-chat-is-hidden {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more