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? ## What does this MR do?
<!--Insert the link to a GitHub issue in (), or describe the changes if there is no issue --> <!-- Is there a lot to say? Consider creating an issue. -->
[Issue Link]()
## Screenshots <!-- Optional, very helpful for the reviewer colleagues from other teams --> ## Screenshots <!-- Optional -->
### Before ### Before
@ -13,7 +12,7 @@
![alt text](https://example.com/after.png) ![alt text](https://example.com/after.png)
## Code Changes ## Notes
* This MR * This MR
**does** <!-- KEEP ONLY ONE --> **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!) they are really big customers, and we want to keep their business!)
--> -->
### Documentation Follow-up Required? ### Follow-up Required <!-- Optional -->
<!-- Keep one of the two sections -->
<!-- <!--
If this MR does change: Does your MR require coordination with the documentation/support teams?
- How the user experiences or uses the application If so, apply the label and explain here.
- 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
--> -->
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_RAILS_PORT=3000
- ZAMMAD_WEBSOCKET_PORT=6042 - ZAMMAD_WEBSOCKET_PORT=6042
services: services:
- postgres:13 - postgres
before_install: contrib/packager.io/preinstall.sh before_install: contrib/packager.io/preinstall.sh
after_install: contrib/packager.io/postinstall.sh after_install: contrib/packager.io/postinstall.sh

View file

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

View file

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

View file

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

View file

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

View file

@ -90,7 +90,7 @@ GEM
activerecord (>= 4.2) activerecord (>= 4.2)
addressable (2.8.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
argon2 (2.1.1) argon2 (2.0.3)
ffi (~> 1.14) ffi (~> 1.14)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
argon2 (2.0.3-x86_64-linux-musl) argon2 (2.0.3-x86_64-linux-musl)
@ -113,7 +113,7 @@ GEM
faraday faraday
async-io (1.32.2) async-io (1.32.2)
async async
async-pool (0.3.9) async-pool (0.3.8)
async (>= 1.25) async (>= 1.25)
autoprefixer-rails (10.3.3.0) autoprefixer-rails (10.3.3.0)
execjs (~> 2) execjs (~> 2)
@ -188,7 +188,7 @@ GEM
docile (1.4.0) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.5.4) doorkeeper (5.5.3)
railties (>= 5) railties (>= 5)
dotenv (2.7.6) dotenv (2.7.6)
eco (1.0.0) eco (1.0.0)
@ -302,8 +302,9 @@ GEM
inflection (1.0.0) inflection (1.0.0)
iniparse (1.5.0) iniparse (1.5.0)
interception (0.5) interception (0.5)
json (2.5.1)
json (2.5.1-x86_64-linux-musl) json (2.5.1-x86_64-linux-musl)
jwt (2.3.0) jwt (2.2.3)
kgio (2.11.4) kgio (2.11.4)
kgio (2.11.4-x86_64-linux-musl) kgio (2.11.4-x86_64-linux-musl)
koala (3.0.0) koala (3.0.0)
@ -443,7 +444,7 @@ GEM
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
pry (~> 0.13) pry (~> 0.13)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (4.3.10) puma (4.3.8)
nio4r (~> 2.0) nio4r (~> 2.0)
puma (4.3.8-x86_64-linux-musl) puma (4.3.8-x86_64-linux-musl)
nio4r (~> 2.0) nio4r (~> 2.0)
@ -521,14 +522,15 @@ GEM
rspec-mocks (~> 3.10) rspec-mocks (~> 3.10)
rspec-support (~> 3.10) rspec-support (~> 3.10)
rspec-support (3.10.2) rspec-support (3.10.2)
rszr (0.5.2)
rszr (0.5.2-x86_64-linux-musl) rszr (0.5.2-x86_64-linux-musl)
rubocop (1.22.1) rubocop (1.21.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml rexml
rubocop-ast (>= 1.12.0, < 2.0) rubocop-ast (>= 1.9.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.12.0) rubocop-ast (1.12.0)
@ -536,14 +538,10 @@ GEM
rubocop-faker (1.1.0) rubocop-faker (1.1.0)
faker (>= 2.12.0) faker (>= 2.12.0)
rubocop (>= 0.82.0) rubocop (>= 0.82.0)
rubocop-inflector (0.1.1)
activesupport
rubocop
rubocop-rspec
rubocop-performance (1.11.5) rubocop-performance (1.11.5)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.12.3) rubocop-rails (2.12.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
@ -619,7 +617,7 @@ GEM
timers (4.3.3) timers (4.3.3)
tins (1.29.1) tins (1.29.1)
sync sync
twilio-ruby (5.59.0) twilio-ruby (5.58.3)
faraday (>= 0.9, < 2.0) faraday (>= 0.9, < 2.0)
jwt (>= 1.5, <= 2.5) jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
@ -766,7 +764,6 @@ DEPENDENCIES
rszr (= 0.5.2) rszr (= 0.5.2)
rubocop rubocop
rubocop-faker rubocop-faker
rubocop-inflector
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
@ -800,4 +797,4 @@ RUBY VERSION
ruby 2.7.3p183 ruby 2.7.3p183
BUNDLED WITH 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 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) => renderTableRows: (sort = false) =>
if sort is true if sort is true
@sortList() @sortList()
@ -525,11 +506,11 @@ class App.ControllerTable extends App.Controller
objectsToShow = @objectsOfPage(@pagerShownPage) objectsToShow = @objectsOfPage(@pagerShownPage)
if @groupBy if @groupBy
# group by raw (and not printable) value so dates work also # 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 else
objectsGrouped = { '': objectsToShow } objectsGrouped = { '': objectsToShow }
for groupValue in @sortObjectKeys(objectsGrouped, @groupDirection) for groupValue in Object.keys(objectsGrouped).sort()
groupObjects = objectsGrouped[groupValue] groupObjects = objectsGrouped[groupValue]
for object in groupObjects for object in groupObjects

View file

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

View file

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

View file

@ -39,7 +39,6 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
operatorsType = operatorsType =
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to'] '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'] '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'] '^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'] '^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'] '^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 continue
for row in App[groupMeta.model].configure_attributes for row in App[groupMeta.model].configure_attributes
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag) continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select'], row.tag)
continue if _.contains(['created_at', 'updated_at'], row.name) continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title'], 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)
# ignore passwords and relations # ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false 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) -> @buildValueConfigMultiple: (config, meta) ->
if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator) if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator)
config.multiple = true config.multiple = true
config.nulloption = true
else else
config.multiple = false config.multiple = false
config.nulloption = false config.nulloption = false
return config return config
@HasPreCondition: -> @HasPreCondition: ->

View file

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

View file

@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller
events: events:
'click .type-tabs .tab': 'changeFormType' 'click .type-tabs .tab': 'changeFormType'
'submit form': 'submit' 'submit form': 'submit'
'click .form-controls .js-cancel': 'cancel' 'click .js-cancel': 'cancel'
'click .js-active-toggle': 'toggleButton' 'click .js-active-toggle': 'toggleButton'
types: { types: {
@ -184,11 +184,8 @@ class App.TicketCreate extends App.Controller
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template)) @controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
changed: => changed: =>
return true if @hasAttachments()
formCurrent = @formParam( @$('.ticket-create') ) formCurrent = @formParam( @$('.ticket-create') )
diff = difference(@formDefault, formCurrent) diff = difference(@formDefault, formCurrent)
return false if !diff || _.isEmpty(diff) return false if !diff || _.isEmpty(diff)
return true return true
@ -464,9 +461,6 @@ class App.TicketCreate extends App.Controller
params: => params: =>
params = @formParam(@$('.main form')) params = @formParam(@$('.main form'))
hasAttachments: =>
@$('.richtext .attachments .attachment').length > 0
submit: (e) => submit: (e) =>
e.preventDefault() e.preventDefault()
@ -569,7 +563,7 @@ class App.TicketCreate extends App.Controller
# save ticket, create article # save ticket, create article
# check attachment # check attachment
if article['body'] if article['body']
if !@hasAttachments() if @$('.richtext .attachments .attachment').length < 1
matchingWord = App.Utils.checkAttachmentReference(article['body']) matchingWord = App.Utils.checkAttachmentReference(article['body'])
if matchingWord if matchingWord
if !confirm(App.i18n.translateContent('You use %s in text but no attachment is attached. Do you want to continue?', 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 class CoreWorkflow extends App.ControllerSubContent
requiredPermission: 'admin.core_workflow' requiredPermission: 'admin.core_workflow'
header: 'Core Workflows' header: 'Core Workflow'
constructor: -> constructor: ->
super super
@ -54,4 +54,4 @@ class CoreWorkflow extends App.ControllerSubContent
} }
return mapping[screen] || screen 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', headline: 'Header menu',
identifier: 'header', identifier: 'header',
color: kb.color_header, color: kb.color_header
color_link: kb.color_header_link
}, },
{ {
headline: 'Footer menu', headline: 'Footer menu',
identifier: 'footer', identifier: 'footer'
color_link: 'hsl(207,12%,50%)'
} }
] ]

View file

@ -1314,7 +1314,7 @@ class Table extends App.Controller
return if ticketListShow[0] || @permissionCheck('ticket.agent') return if ticketListShow[0] || @permissionCheck('ticket.agent')
tickets_count = user.lifetimeCustomerTicketsCount() 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 if tickets_count == 0
@listenTo user, 'refresh', => @listenTo user, 'refresh', =>

View file

@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller
formMeta = data.form_meta formMeta = data.form_meta
# on the following states we want to rerender the ticket: # 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 user view has changed (agent/customer)
# - if the ticket permission has changed (read/write/full) # - 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 @renderDone = false
@view = view @view = view
@ -214,7 +214,6 @@ class App.TicketZoom extends App.Controller
# render page # render page
@render(local) @render(local)
App.Event.trigger('ui::ticket::load', data)
meta: => meta: =>

View file

@ -98,7 +98,7 @@ class App.TicketZoomArticleNew extends App.Controller
@controllerBind('ui:rerender', => @controllerBind('ui:rerender', =>
@adjustedTextarea = false @adjustedTextarea = false
@defaults = @ui.taskGet('article') @defaults = @ui.taskGet('article')
@attachments = @defaults.attachments || [] @attachments = @defaults.attachments
@render() @render()
) )
@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller
@tokanice(@type) @tokanice(@type)
if @defaults.body or @attachments.length > 0 or @isIE10() if @defaults.body or @isIE10()
@openTextarea(null, true) @openTextarea(null, true)
tokanice: (type = 'email') -> tokanice: (type = 'email') ->
@ -191,30 +191,82 @@ class App.TicketZoomArticleNew extends App.Controller
maxlength: 150000 maxlength: 150000
}) })
new App.Html5Upload( html5Upload.initialize(
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}" uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
dropContainer: @$('.article-add') dropContainer: @$('.article-add').get(0)
cancelContainer: @cancelContainer cancelContainer: @cancelContainer
inputField: @$('.article-attachment input') inputField: @$('.article-attachment input').get(0)
key: 'File'
maxSimultaneousUploads: 1
onFileAdded: (file) =>
onFileStartCallback: => file.on(
@callbackFileUploadStart?()
onFileCompletedCallback: (response) => onStart: =>
@attachments.push response.data @attachmentPlaceholder.addClass('hide')
@renderAttachment(response.data) @attachmentUpload.removeClass('hide')
@$('.article-attachment input').val('') @cancelContainer.removeClass('hide')
@callbackFileUploadStop?() if @callbackFileUploadStart
@callbackFileUploadStart()
onFileAbortedCallback: => onAborted: =>
@callbackFileUploadStop?() @attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@$('.article-attachment input').val('')
attachmentPlaceholder: @attachmentPlaceholder if @callbackFileUploadStop
attachmentUpload: @attachmentUpload @callbackFileUploadStop()
progressBar: @progressBar
progressText: @progressText # Called after received response from the server
).render() 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() @bindAttachmentDelete()

View file

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

View file

@ -1,21 +1,12 @@
# No usage of a ControllerObserver here because we want to use class Edit extends App.ControllerObserver
# the data of the ticket zoom ajax request which is using the all=true parameter model: 'Ticket'
# and contain the core workflow information as well. Without observer we also observeNot:
# dont have double rendering because of the zoom (all=true) and observer (full=true) render callback created_at: true
class Edit extends App.Controller updated_at: true
constructor: (params) -> globalRerender: false
super
@controllerBind('ui::ticket::load', (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString()
@ticket = App.Ticket.find(@ticket.id) render: (ticket, diff) =>
@formMeta = data.form_meta defaults = ticket.attributes()
@render()
)
@render()
render: =>
defaults = @ticket.attributes()
delete defaults.article # ignore article infos delete defaults.article # ignore article infos
followUpPossible = App.Group.find(defaults.group_id).follow_up_possible followUpPossible = App.Group.find(defaults.group_id).follow_up_possible
ticketState = App.TicketState.find(defaults.state_id).name ticketState = App.TicketState.find(defaults.state_id).name
@ -25,13 +16,10 @@ class Edit extends App.Controller
if !_.isEmpty(taskState) if !_.isEmpty(taskState)
defaults = _.extend(defaults, 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' || if followUpPossible == 'new_ticket' && ticketState != 'closed' ||
followUpPossible != 'new_ticket' || followUpPossible != 'new_ticket' ||
@permissionCheck('admin') || @ticket.currentView() is 'agent' @permissionCheck('admin') || ticket.currentView() is 'agent'
@controllerFormSidebarTicket = new App.ControllerForm( @controllerFormSidebarTicket = new App.ControllerForm(
elReplace: @el elReplace: @el
model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes }
@ -40,7 +28,7 @@ class Edit extends App.Controller
filter: @formMeta.filter filter: @formMeta.filter
formMeta: @formMeta formMeta: @formMeta
params: defaults params: defaults
isDisabled: !@ticket.editable() isDisabled: !ticket.editable()
taskKey: @taskKey taskKey: @taskKey
core_workflow: { core_workflow: {
callbacks: [@markForm] callbacks: [@markForm]
@ -56,7 +44,7 @@ class Edit extends App.Controller
filter: @formMeta.filter filter: @formMeta.filter
formMeta: @formMeta formMeta: @formMeta
params: defaults params: defaults
isDisabled: @ticket.editable() isDisabled: ticket.editable()
taskKey: @taskKey taskKey: @taskKey
core_workflow: { core_workflow: {
callbacks: [@markForm] callbacks: [@markForm]
@ -69,8 +57,8 @@ class Edit extends App.Controller
return if @resetBind return if @resetBind
@resetBind = true @resetBind = true
@controllerBind('ui::ticket::taskReset', (data) => @controllerBind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString() return if data.ticket_id.toString() isnt ticket.id.toString()
@render() @render(ticket)
) )
class SidebarTicket extends App.Controller class SidebarTicket extends App.Controller
@ -140,7 +128,6 @@ class SidebarTicket extends App.Controller
@edit = new Edit( @edit = new Edit(
object_id: @ticket.id object_id: @ticket.id
ticket: @ticket
el: localEl.find('.edit') el: localEl.find('.edit')
taskGet: @taskGet taskGet: @taskGet
formMeta: @formMeta 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()); manager.ajaxUpload(manager.uploadsQueue.shift());
} }
}; };
xhr.onabort = function (event) { xhr.abort = function (event) {
console.log('Upload abort'); console.log('Upload abort');
// Reduce number of active uploads: // Reduce number of active uploads:
@ -269,7 +269,6 @@
// Triggered when upload fails: // Triggered when upload fails:
xhr.onerror = function () { xhr.onerror = function () {
console.log('Upload failed: ', upload.fileName); console.log('Upload failed: ', upload.fileName);
upload.events.onError('Upload failed: ' + upload.fileName);
}; };
// Append additional data if provided: // Append additional data if provided:

View file

@ -1,5 +1,5 @@
class App.KnowledgeBase extends App.Model 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 Spine.Model.Ajax
@extend App.KnowledgeBaseActions @extend App.KnowledgeBaseActions
@url: @apiPath + '/knowledge_bases' @url: @apiPath + '/knowledge_bases'
@ -148,17 +148,6 @@ class App.KnowledgeBase extends App.Model
display: false display: false
horizontal: true horizontal: true
shown: 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 # Layout picker is disabled in V1
#}, { #}, {
# name: 'homepage_layout' # name: 'homepage_layout'

View file

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

View file

@ -6,15 +6,11 @@
<% if @has_any_tickets: %> <% if @has_any_tickets: %>
<p><%- @T('You have no tickets to display in this overview.') %></p> <p><%- @T('You have no tickets to display in this overview.') %></p>
<% else: %> <% else: %>
<% if @is_allowed_to_create_ticket: %> <p><%- @T('You have not created a ticket yet.') %></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('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('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> <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 %>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -14,7 +14,7 @@
</div> </div>
<div class="form-group js-sure"> <div class="form-group js-sure">
<h3 class="danger-color"><%- @T('Warning') %></h3> <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 %> <%- @sure_html %>
</div> </div>
</div> </div>

View file

@ -17,7 +17,7 @@
<%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ... <%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ...
</div> </div>
<div class="attachmentUpload-cancel js-cancel"> <div class="attachmentUpload-cancel js-cancel">
<%- @Icon('diagonal-cross') %><%- @T('Cancel Upload') %> <%- @Icon('diagonal-cross') %></div><%- @T('Cancel Upload') %>
</div> </div>
</div> </div>
<div class="attachmentUpload-progressBar" style="width: 0%"></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) App.UiElement[attribute.tag].prepareParams?(attribute, dom, params)
applyDefaults: (params) -> applyDefaults: (params) ->
params['iconset'] = 'FontAwesome' params['iconset'] = 'FontAwesome'
params['color_highlight'] = '#38ae6a' params['color_highlight'] = '#38ae6a'
params['color_header'] = '#f9fafb' params['color_header'] = '#f9fafb'
params['color_header_link'] = 'hsl(206,8%,50%)' params['homepage_layout'] = 'grid'
params['homepage_layout'] = 'grid' params['category_layout'] = 'grid'
params['category_layout'] = 'grid'
onSubmit: (e) -> onSubmit: (e) ->
params = @formParams(@el) params = @formParams(@el)

View file

@ -14,7 +14,7 @@
<div class="kb-menu-preview"> <div class="kb-menu-preview">
<div class="label"><%= kb_locale.systemLocale().name %></div> <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) %> <% menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, location.identifier) %>
<% if menu_items.length == 0: %> <% if menu_items.length == 0: %>

View file

@ -1,7 +1,7 @@
<div class="login fullscreen"> <div class="login fullscreen">
<div class="fullscreen-center"> <div class="fullscreen-center">
<div class="fullscreen-body"> <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'): %> <% 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> <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>
<div class="page-content"> <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> <p>
<%- @T('After installing, updating or uninstalling packages the following commands need to be executed on the server:') %> <%- @T('After installing, updating or uninstalling packages the following commands need to be executed on the server:') %>
<ul> <ul>
@ -53,4 +48,4 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>

View file

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

View file

@ -10,8 +10,8 @@ class ApplicationController < ActionController::Base
include ApplicationController::RendersModels include ApplicationController::RendersModels
include ApplicationController::HasUser include ApplicationController::HasUser
include ApplicationController::HasResponseExtentions include ApplicationController::HasResponseExtentions
include ApplicationController::HasDownload
include ApplicationController::PreventsCsrf include ApplicationController::PreventsCsrf
include ApplicationController::HasSecureContentSecurityPolicyForDownloads
include ApplicationController::LogsHttpAccess include ApplicationController::LogsHttpAccess
include ApplicationController::Authorizes include ApplicationController::Authorizes
end 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] prepend_before_action :authentication_check_only, only: %i[show destroy]
def show def show
view_type = params[:preview] ? 'preview' : nil content = @file.content_preview if params[:preview] && @file.preferences[:content_preview]
content ||= @file.content
send_data( send_data(
download_file.content(view_type), content,
filename: download_file.filename, filename: @file.filename,
type: download_file.content_type, type: @file.preferences['Content-Type'] || @file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: download_file.disposition disposition: sanitized_disposition
) )
end end
@ -51,7 +52,7 @@ class AttachmentsController < ApplicationController
end end
def destroy def destroy
Store.remove_item(download_file.id) Store.remove_item(@file.id)
render json: { render json: {
success: true, success: true,
@ -71,8 +72,18 @@ class AttachmentsController < ApplicationController
private 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! 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 authorize(record) if record
rescue Pundit::NotAuthorizedError rescue Pundit::NotAuthorizedError
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound

View file

@ -156,7 +156,7 @@ class FormController < ApplicationController
end end
def token_gen(fingerprint) 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)}" fingerprint = "#{Base64.strict_encode64(Setting.get('fqdn'))}:#{Time.zone.now.to_i}:#{Base64.strict_encode64(fingerprint)}"
Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint)) Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint))
end end
@ -167,7 +167,7 @@ class FormController < ApplicationController
raise Exceptions::Forbidden raise Exceptions::Forbidden
end end
begin 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)) result = crypt.decrypt_and_verify(Base64.decode64(token))
rescue rescue
Rails.logger.info 'Invalid token for form!' Rails.logger.info 'Invalid token for form!'

View file

@ -175,11 +175,29 @@ class TicketArticlesController < ApplicationController
end end
raise Exceptions::Forbidden, 'Requested file id is not linked with article_id.' if !access 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( send_data(
download_file.content(params[:view]), content,
filename: download_file.filename, filename: file.filename,
type: download_file.content_type, type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: download_file.disposition disposition: disposition
) )
end end
@ -260,4 +278,14 @@ class TicketArticlesController < ApplicationController
render json: result render json: result
end 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 end

View file

@ -722,28 +722,31 @@ curl http://localhost/api/v1/users/image/8d6cca1c6bdc226cf2ba131e264ca2c7 -v -u
=end =end
def image def image
# cache image # cache image
response.headers['Expires'] = 1.year.from_now.httpdate response.headers['Expires'] = 1.year.from_now.httpdate
response.headers['Cache-Control'] = 'cache, store, max-age=31536000, must-revalidate' response.headers['Cache-Control'] = 'cache, store, max-age=31536000, must-revalidate'
response.headers['Pragma'] = 'cache' response.headers['Pragma'] = 'cache'
file = Avatar.get_by_hash(params[:hash]) file = Avatar.get_by_hash(params[:hash])
if file 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( send_data(
file.content, file.content,
filename: file.filename, filename: file.filename,
type: file_content_type, type: file.preferences['Content-Type'] || file.preferences['Mime-Type'],
disposition: 'inline' disposition: 'inline'
) )
return return
end 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 end
=begin =begin
@ -775,11 +778,6 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
return return
end 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 begin
file_resize = StaticAssets.data_url_attributes(params[:avatar_resize]) file_resize = StaticAssets.data_url_attributes(params[:avatar_resize])
rescue rescue
@ -1063,15 +1061,4 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
render json: { message: 'ok' }, status: :created render json: { message: 'ok' }, status: :created
end 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 end

View file

@ -4,7 +4,9 @@ class WebhooksController < ApplicationController
prepend_before_action { authentication_check && authorize! } prepend_before_action { authentication_check && authorize! }
def preview 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({ render json: JSON.pretty_generate({
ticket: TriggerWebhookJob::RecordPayload.generate(ticket), ticket: TriggerWebhookJob::RecordPayload.generate(ticket),

View file

@ -3,7 +3,6 @@
class UploadCacheCleanupJob < ApplicationJob class UploadCacheCleanupJob < ApplicationJob
def perform def perform
taskbar_form_ids = Taskbar.with_form_id.filter_map(&:persisted_form_id) 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.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) Store.remove_item(store.id)
@ -13,6 +12,6 @@ class UploadCacheCleanupJob < ApplicationJob
private private
def store_object_id def store_object_id
Store::Object.lookup(name: 'UploadCache')&.id Store::Object.lookup(name: 'UploadCache').id
end end
end end

View file

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

View file

@ -72,6 +72,7 @@ add avatar by url
=end =end
def self.add(data) def self.add(data)
# lookups # lookups
if data[:object] if data[:object]
object_id = ObjectLookup.by_name(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_METADATA_TIMEOUT = 2.minutes
FETCH_MSG_TIMEOUT = 4.minutes FETCH_MSG_TIMEOUT = 4.minutes
EXPUNGE_TIMEOUT = 16.minutes EXPUNGE_TIMEOUT = 16.minutes
DEFAULT_TIMEOUT = 45.seconds
CHECK_ONLY_TIMEOUT = 6.seconds
def fetchable?(_channel) def fetchable?(_channel)
true 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')})" 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 # 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 timeout(check_type_timeout) do
@imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false) @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') path = Rails.root.join('tmp/unprocessable_mail')
files = [] files = []
Dir.glob("#{path}/*.eml") do |entry| 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? next if ticket.blank?
files.push entry files.push entry

View file

@ -30,26 +30,10 @@ class CoreWorkflow::Attributes
end end
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) def overwrite_selected(result)
selected_attributes = selected_only.attributes selected_attributes = selected_only.attributes
selected_attributes.each_key do |key| selected_attributes.each_key do |key|
next if !selectable_field?(key) next if selected_attributes[key].nil?
# special behaviour for owner id
if key == 'owner_id' && selected_attributes[key].nil?
selected_attributes[key] = 1
end
result[key.to_sym] = selected_attributes[key] result[key.to_sym] = selected_attributes[key]
end end
@ -71,10 +55,7 @@ class CoreWorkflow::Attributes
# dont use lookup here because the cache will not # dont use lookup here because the cache will not
# know about new attributes and make crashes # know about new attributes and make crashes
@saved_only ||= payload_class.find_by(id: @payload['params']['id']) @saved_only ||= payload_class.find_by(id: @payload['params']['id'])
@saved_only.dup
# 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))
end end
def saved def saved
@ -87,10 +68,6 @@ class CoreWorkflow::Attributes
end end
end end
def object_elements_hash
@object_elements_hash ||= object_elements.index_by { |x| x[:name] }
end
def screen_value(attribute, type) def screen_value(attribute, type)
attribute[:screens].dig(@payload['screen'], type) attribute[:screens].dig(@payload['screen'], type)
end end

View file

@ -18,26 +18,4 @@ class CoreWorkflow::Result::Backend
def result(backend, field, value = nil) def result(backend, field, value = nil)
@result_object.run_backend_value(backend, field, value) @result_object.run_backend_value(backend, field, value)
end 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 end

View file

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

View file

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

View file

@ -12,8 +12,6 @@ class Group < ApplicationModel
include HasTicketCreateScreenImpact include HasTicketCreateScreenImpact
include HasSearchIndexBackend include HasSearchIndexBackend
include Group::Assets
belongs_to :email_address, optional: true belongs_to :email_address, optional: true
belongs_to :signature, 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 :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS } validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
validates :color_highlight, presence: true, color: true validates :color_highlight, presence: true, color: true
validates :color_header, presence: true, color: true validates :color_header, presence: true, color: true
validates :color_header_link, presence: true, color: true
validates :iconset, inclusion: { in: KnowledgeBase::ICONSETS } validates :iconset, inclusion: { in: KnowledgeBase::ICONSETS }

View file

@ -678,7 +678,7 @@ to send no browser reload event, pass false
=begin =begin
where attributes are used in conditions where attributes are used by triggers, overviews or schedulers
result = ObjectManager::Attribute.attribute_to_references_hash result = ObjectManager::Attribute.attribute_to_references_hash
@ -696,36 +696,22 @@ where attributes are used in conditions
=end =end
def self.attribute_to_references_hash def self.attribute_to_references_hash
objects = Trigger.select(:name, :condition) + Overview.select(:name, :condition) + Job.select(:name, :condition)
attribute_list = {} 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 attribute_list[condition_key][item.class.name].push item.name
.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
end end
end
attribute_list attribute_list
end end
=begin =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 is certain attribute used by triggers, overviews or schedulers
ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name') ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')

View file

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

View file

@ -70,12 +70,5 @@ returns
end end
data data
end 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
end end

View file

@ -263,37 +263,32 @@ subsequently in a separate step.
) )
end end
Transaction.execute do # store package
# store package if !data[:reinstall]
if !data[:reinstall] package_db = Package.create(meta)
package_db = Package.create(meta) Store.add(
Store.add( object: 'Package',
object: 'Package', o_id: package_db.id,
o_id: package_db.id, data: package.to_json,
data: package.to_json, filename: "#{meta[:name]}-#{meta[:version]}.zpm",
filename: "#{meta[:name]}-#{meta[:version]}.zpm", preferences: {},
preferences: {}, created_by_id: UserInfo.current_user_id || 1,
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
end 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 # prebuild assets
package_db package_db
end end
@ -488,9 +483,4 @@ execute all pending package migrations at once
true true
end end
def self.allowed_file_path?(file)
file.exclude?('..') && file.exclude?('%2e%2e')
end
private_class_method :allowed_file_path?
end end

View file

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

View file

@ -60,13 +60,5 @@ returns
end end
data data
end 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
end end

View file

@ -926,16 +926,17 @@ try to find correct name
end end
# check if login already exists # check if login already exists
base_login = login.downcase.strip self.login = login.downcase.strip
check = true
alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ] while check
alternatives.each do |suffix|
self.login = "#{base_login}#{suffix}"
exists = User.find_by(login: login) 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 end
true
raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
end end
def check_mail_delivery_failed def check_mail_delivery_failed

View file

@ -110,20 +110,5 @@ returns
end end
data data
end 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
end end

View file

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

View file

@ -13,7 +13,7 @@ class TicketPolicy < ApplicationPolicy
super super
end 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) raise NoMethodError, <<~ERR.chomp if instance_of?(TicketPolicy::BaseScope)
specify an access type using a subclass of TicketPolicy::BaseScope specify an access type using a subclass of TicketPolicy::BaseScope
ERR ERR
@ -26,19 +26,12 @@ class TicketPolicy < ApplicationPolicy
bind.push(user.group_ids_access(self.class::ACCESS_TYPE)) bind.push(user.group_ids_access(self.class::ACCESS_TYPE))
end end
if user.permissions?('ticket.customer') if user.organization&.shared
if user.organization&.shared sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)') bind.push(user.id, user.organization.id)
bind.push(user.id, user.organization.id) else
else sql.push('tickets.customer_id = ?')
sql.push('tickets.customer_id = ?') bind.push(user.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.
end end
scope.where sql.join(' OR '), *bind scope.where sql.join(' OR '), *bind

View file

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

View file

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

View file

@ -5,11 +5,11 @@ Rails.application.config.html_sanitizer_tags_remove_content = %w[
style style
comment comment
meta meta
script
] ]
# content of this tags will will be inserted html quoted # content of this tags will will be inserted html quoted
Rails.application.config.html_sanitizer_tags_quote_content = %w[ Rails.application.config.html_sanitizer_tags_quote_content = %w[
script
] ]
# only this tags are allowed # 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?! # Rails thinks the singularized version of knowledge_bases is knowledge_basis?!
# see: KnowledgeBase.table_name.singularize # see: KnowledgeBase.table_name.singularize
inflect.irregular 'base', 'bases' inflect.singular(%r{(knowledge_base)s$}i, '\1')
inflect.acronym 'SMIME' inflect.acronym 'SMIME'
inflect.acronym 'GitLab' inflect.acronym 'GitLab'
inflect.acronym 'GitHub' inflect.acronym 'GitHub'

View file

@ -228,7 +228,7 @@ function create_webserver_config () {
function setup_elasticsearch () { function setup_elasticsearch () {
echo "# Configuring 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 if [ -z "${ES_CONNECTION}" ]; then
echo "-- Nevermind, no es_url is set, leaving Elasticsearch untouched ...!" echo "-- Nevermind, no es_url is set, leaving Elasticsearch untouched ...!"
@ -274,10 +274,6 @@ function elasticsearch_searchindex_rebuild () {
function update_or_install () { function update_or_install () {
if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then
echo "# Clear cache..."
zammad run rails r Cache.clear
update_database update_database
update_translations update_translations

View file

@ -8,9 +8,8 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
create_table :knowledge_bases do |t| create_table :knowledge_bases do |t|
t.string :iconset, limit: 30, null: false t.string :iconset, limit: 30, null: false
t.string :color_highlight, limit: 25, null: false t.string :color_highlight, limit: 25, null: false
t.string :color_header, limit: 25, null: false t.string :color_header, limit: 25, null: false
t.string :color_header_link, limit: 25, null: false
t.string :homepage_layout, null: false t.string :homepage_layout, null: false
t.string :category_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 def fix_pending_time
pending_time = ObjectManager::Attribute.find_by(name: 'pending_time', object_lookup: ObjectLookup.find_by(name: 'Ticket')) 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('required_if')
pending_time.data_option.delete('shown_if') pending_time.data_option.delete('shown_if')
pending_time.save pending_time.save
@ -85,8 +83,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
def fix_organization_screens def fix_organization_screens
%w[domain note].each do |name| %w[domain note].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization')) field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
next if field.blank?
field.screens['create'] ||= {} field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {} field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = true field.screens['create']['-all-']['null'] = true
@ -97,8 +93,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
def fix_user_screens 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| %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')) field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
next if field.blank?
field.screens['create'] ||= {} field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {} field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = true 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 def fix_organization_screens_create
%w[name shared domain_assignment active].each do |name| %w[name shared domain_assignment active].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization')) field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
next if field.blank?
field.screens['create'] ||= {} field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {} field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = false field.screens['create']['-all-']['null'] = false
@ -23,8 +21,6 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0]
def fix_user_screens_create def fix_user_screens_create
%w[firstname lastname active].each do |name| %w[firstname lastname active].each do |name|
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User')) field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
next if field.blank?
field.screens['create'] ||= {} field.screens['create'] ||= {}
field.screens['create']['-all-'] ||= {} field.screens['create']['-all-'] ||= {}
field.screens['create']['-all-']['null'] = false 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), state: SecureRandom.hex(128),
preferences: { preferences: {
permission: ['admin'], permission: ['admin'],
protected: true,
}, },
frontend: false frontend: false
) )

View file

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

View file

@ -1,14 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'uri'
class GitHub class GitHub
class HttpClient class HttpClient
attr_reader :api_token, :endpoint attr_reader :api_token, :endpoint
def initialize(endpoint, api_token) def initialize(endpoint, api_token)
raise 'api_token required' if api_token.blank? 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 @api_token = api_token
@endpoint = endpoint @endpoint = endpoint
@ -32,7 +30,7 @@ class GitHub
if !response.success? if !response.success?
Rails.logger.error response.error 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 end
response.data response.data

View file

@ -1,14 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'uri'
class GitLab class GitLab
class HttpClient class HttpClient
attr_reader :api_token, :endpoint attr_reader :api_token, :endpoint
def initialize(endpoint, api_token) def initialize(endpoint, api_token)
raise 'api_token required' if api_token.blank? 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 @api_token = api_token
@endpoint = endpoint @endpoint = endpoint
@ -32,7 +30,7 @@ class GitLab
if !response.success? if !response.success?
Rails.logger.error response.error 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 end
response.data response.data

View file

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

View file

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

View file

@ -13,8 +13,7 @@ examples how to use
locale: 'de-de', locale: 'de-de',
timezone: 'America/Port-au-Prince', timezone: 'America/Port-au-Prince',
template: 'some template <b>#{ticket.title}</b> {config.fqdn}', template: 'some template <b>#{ticket.title}</b> {config.fqdn}',
escape: false, escape: false
trusted: false, # Allow ERB tags in the template?
).render ).render
message_body = NotificationFactory::Renderer.new( message_body = NotificationFactory::Renderer.new(
@ -28,20 +27,16 @@ examples how to use
=end =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 @objects = objects
@locale = locale || Locale.default @locale = locale || Locale.default
@timezone = timezone || Setting.get('timezone_default') @timezone = timezone || Setting.get('timezone_default')
@template = NotificationFactory::Template.new(template, escape, trusted) @template = NotificationFactory::Template.new(template, escape)
@escape = escape @escape = escape
end end
def render def render
ERB.new(@template.to_s).result(binding) 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 end
# d - data of object # d - data of object

View file

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

View file

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

View file

@ -4,7 +4,7 @@ module SessionHelper
def self.json_hash(user) def self.json_hash(user)
collections, assets = default_collections(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), models: models(user),
collections: collections, collections: collections,
assets: assets, assets: assets,

View file

@ -7,11 +7,6 @@ module UserInfo
def self.current_user_id=(user_id) def self.current_user_id=(user_id)
Thread.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 end
def self.ensure_current_user_id 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) console.log('p', docType, text)
if docType is 'html' if docType is 'html'
html = document.createElement('div') html = document.createElement('div')
# can't log because might contain malicious content html.innerHTML = text
# @log.debug 'HTML clipboard', text
sanitized = DOMPurify.sanitize(text)
@log.debug 'sanitized HTML clipboard', sanitized
html.innerHTML = sanitized
match = false match = false
htmlTmp = text htmlTmp = text
regex = new RegExp('<(/w|w)\:[A-Za-z]') 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>') text = text.replace(/<div><\/div>/g, '<div><br></div>')
console.log('p', docType, text) console.log('p', docType, text)
if docType is 'html' if docType is 'html'
sanitized = DOMPurify.sanitize(text) html = $("<div>#{text}</div>")
@log.debug 'sanitized HTML clipboard', sanitized
html = $("<div>#{sanitized}</div>")
match = false match = false
htmlTmp = text htmlTmp = text
regex = new RegExp('<(/w|w)\:[A-Za-z]') regex = new RegExp('<(/w|w)\:[A-Za-z]')

View file

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