Compare commits
No commits in common. "antifascista" and "1bf468a262dfa03925ba50a242fb548d226b1623" have entirely different histories.
antifascis
...
1bf468a262
171 changed files with 2582 additions and 4080 deletions
|
@ -1,9 +1,8 @@
|
|||
## What does this MR do?
|
||||
|
||||
<!--Insert the link to a GitHub issue in (), or describe the changes if there is no issue -->
|
||||
[Issue Link]()
|
||||
<!-- Is there a lot to say? Consider creating an issue. -->
|
||||
|
||||
## Screenshots <!-- Optional, very helpful for the reviewer colleagues from other teams -->
|
||||
## Screenshots <!-- Optional -->
|
||||
|
||||
### Before
|
||||
|
||||
|
@ -13,7 +12,7 @@
|
|||
|
||||
![alt text](https://example.com/after.png)
|
||||
|
||||
## Code Changes
|
||||
## Notes
|
||||
|
||||
* This MR
|
||||
**does** <!-- KEEP ONLY ONE -->
|
||||
|
@ -59,36 +58,9 @@ How do your performance changes scale on a system of this size?
|
|||
they are really big customers, and we want to keep their business!)
|
||||
-->
|
||||
|
||||
### Documentation Follow-up Required?
|
||||
|
||||
<!-- Keep one of the two sections -->
|
||||
### Follow-up Required <!-- Optional -->
|
||||
|
||||
<!--
|
||||
If this MR does change:
|
||||
- How the user experiences or uses the application
|
||||
- Visual appearance
|
||||
- Screen flow
|
||||
- Texts
|
||||
- How the application is deployed an maintained
|
||||
- Deployment process
|
||||
- System requirements
|
||||
- Command line interfaces
|
||||
-->
|
||||
This MR may require follow-up by the documentation team.
|
||||
/label ~Documentation
|
||||
|
||||
<!--
|
||||
Otherwise
|
||||
Does your MR require coordination with the documentation/support teams?
|
||||
If so, apply the label and explain here.
|
||||
-->
|
||||
This MR does not require any follow-up.
|
||||
|
||||
## QA Checklist (to be filled by the reviewer)
|
||||
|
||||
- [ ] Implementation satisfies specification
|
||||
- [ ] Changes confirmed by manual testing
|
||||
- [ ] [Code style](https://git.znuny.com/zammad/zammad/-/wikis/Coding-style-guide) is appropriate
|
||||
- [ ] Performance will not degrade
|
||||
- [ ] Code is properly covered with tests
|
||||
- If follow-up by the documentation team is needed:
|
||||
- [ ] Add a comment with this text
|
||||
> @<!-- don't treat this as a mention until copied -->MrGeneration please check if this MR requires changes to the documentation. Thanks!
|
||||
|
|
|
@ -116,6 +116,6 @@ env:
|
|||
- ZAMMAD_RAILS_PORT=3000
|
||||
- ZAMMAD_WEBSOCKET_PORT=6042
|
||||
services:
|
||||
- postgres:13
|
||||
- postgres
|
||||
before_install: contrib/packager.io/preinstall.sh
|
||||
after_install: contrib/packager.io/postinstall.sh
|
||||
|
|
|
@ -6,8 +6,6 @@ require:
|
|||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-inflector
|
||||
- ../config/initializers/inflections.rb
|
||||
- ./rubocop_zammad.rb
|
||||
|
||||
inherit_from:
|
||||
|
|
|
@ -887,6 +887,10 @@ Metrics/PerceivedComplexity:
|
|||
- 'test/browser_test_helper.rb'
|
||||
- 'test/integration/slack_test.rb'
|
||||
|
||||
Rails/AssertNot:
|
||||
Exclude:
|
||||
- 'test/browser/admin_permissions_granular_vs_full_test.rb'
|
||||
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20120101000001_create_base.rb'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Change Log
|
||||
|
||||
## [5.1.0](https://github.com/zammad/zammad/tree/5.1.0) (2021-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/5.0.0...5.1.0)
|
||||
## [5.0.0](https://github.com/zammad/zammad/tree/5.0.0) (2021-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/4.1.0...5.0.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -198,7 +198,6 @@ group :development, :test do
|
|||
gem 'overcommit'
|
||||
gem 'rubocop'
|
||||
gem 'rubocop-faker'
|
||||
gem 'rubocop-inflector'
|
||||
gem 'rubocop-performance'
|
||||
gem 'rubocop-rails'
|
||||
gem 'rubocop-rspec'
|
||||
|
|
27
Gemfile.lock
27
Gemfile.lock
|
@ -90,7 +90,7 @@ GEM
|
|||
activerecord (>= 4.2)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
argon2 (2.1.1)
|
||||
argon2 (2.0.3)
|
||||
ffi (~> 1.14)
|
||||
ffi-compiler (~> 1.0)
|
||||
argon2 (2.0.3-x86_64-linux-musl)
|
||||
|
@ -113,7 +113,7 @@ GEM
|
|||
faraday
|
||||
async-io (1.32.2)
|
||||
async
|
||||
async-pool (0.3.9)
|
||||
async-pool (0.3.8)
|
||||
async (>= 1.25)
|
||||
autoprefixer-rails (10.3.3.0)
|
||||
execjs (~> 2)
|
||||
|
@ -188,7 +188,7 @@ GEM
|
|||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.5.4)
|
||||
doorkeeper (5.5.3)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.6)
|
||||
eco (1.0.0)
|
||||
|
@ -302,8 +302,9 @@ GEM
|
|||
inflection (1.0.0)
|
||||
iniparse (1.5.0)
|
||||
interception (0.5)
|
||||
json (2.5.1)
|
||||
json (2.5.1-x86_64-linux-musl)
|
||||
jwt (2.3.0)
|
||||
jwt (2.2.3)
|
||||
kgio (2.11.4)
|
||||
kgio (2.11.4-x86_64-linux-musl)
|
||||
koala (3.0.0)
|
||||
|
@ -443,7 +444,7 @@ GEM
|
|||
binding_of_caller (~> 1.0)
|
||||
pry (~> 0.13)
|
||||
public_suffix (4.0.6)
|
||||
puma (4.3.10)
|
||||
puma (4.3.8)
|
||||
nio4r (~> 2.0)
|
||||
puma (4.3.8-x86_64-linux-musl)
|
||||
nio4r (~> 2.0)
|
||||
|
@ -521,14 +522,15 @@ GEM
|
|||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rszr (0.5.2)
|
||||
rszr (0.5.2-x86_64-linux-musl)
|
||||
rubocop (1.22.1)
|
||||
rubocop (1.21.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
rubocop-ast (>= 1.9.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.12.0)
|
||||
|
@ -536,14 +538,10 @@ GEM
|
|||
rubocop-faker (1.1.0)
|
||||
faker (>= 2.12.0)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop-inflector (0.1.1)
|
||||
activesupport
|
||||
rubocop
|
||||
rubocop-rspec
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.12.3)
|
||||
rubocop-rails (2.12.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
|
@ -619,7 +617,7 @@ GEM
|
|||
timers (4.3.3)
|
||||
tins (1.29.1)
|
||||
sync
|
||||
twilio-ruby (5.59.0)
|
||||
twilio-ruby (5.58.3)
|
||||
faraday (>= 0.9, < 2.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
|
@ -766,7 +764,6 @@ DEPENDENCIES
|
|||
rszr (= 0.5.2)
|
||||
rubocop
|
||||
rubocop-faker
|
||||
rubocop-inflector
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
|
@ -800,4 +797,4 @@ RUBY VERSION
|
|||
ruby 2.7.3p183
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.27
|
||||
2.2.20
|
||||
|
|
149
Makefile
149
Makefile
|
@ -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
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
5.1.x
|
||||
5.0.x
|
||||
|
|
|
@ -493,25 +493,6 @@ class App.ControllerTable extends App.Controller
|
|||
sortable: @dndCallback
|
||||
))
|
||||
|
||||
getGroupByKeyName: (object, groupBy) ->
|
||||
reference_key = groupBy + '_id'
|
||||
|
||||
if reference_key of object
|
||||
attribute = _.findWhere(object.constructor.configure_attributes, { name: reference_key })
|
||||
|
||||
return App[attribute.relation]?.find(object[reference_key])?.displayName() || reference_key
|
||||
|
||||
groupBy
|
||||
|
||||
sortObjectKeys: (objects, direction) ->
|
||||
sorted = Object.keys(objects).sort()
|
||||
|
||||
switch direction
|
||||
when 'DESC'
|
||||
sorted.reverse()
|
||||
else
|
||||
sorted
|
||||
|
||||
renderTableRows: (sort = false) =>
|
||||
if sort is true
|
||||
@sortList()
|
||||
|
@ -525,11 +506,11 @@ class App.ControllerTable extends App.Controller
|
|||
objectsToShow = @objectsOfPage(@pagerShownPage)
|
||||
if @groupBy
|
||||
# group by raw (and not printable) value so dates work also
|
||||
objectsGrouped = _.groupBy(objectsToShow, (object) => object[@getGroupByKeyName(object, @groupBy)])
|
||||
objectsGrouped = _.groupBy(objectsToShow, (object) => object[@groupBy])
|
||||
else
|
||||
objectsGrouped = { '': objectsToShow }
|
||||
|
||||
for groupValue in @sortObjectKeys(objectsGrouped, @groupDirection)
|
||||
for groupValue in Object.keys(objectsGrouped).sort()
|
||||
groupObjects = objectsGrouped[groupValue]
|
||||
|
||||
for object in groupObjects
|
||||
|
|
|
@ -46,7 +46,7 @@ class App.UiElement.ApplicationUiElement
|
|||
result = []
|
||||
for row in selection
|
||||
if attribute.translate
|
||||
row.name = App.i18n.translatePlain(row.name)
|
||||
row.name = App.i18n.translateInline(row.name)
|
||||
if !_.isEmpty(row.children)
|
||||
row.children = @getConfigOptionListArray(attribute, row.children)
|
||||
result.push row
|
||||
|
@ -65,7 +65,7 @@ class App.UiElement.ApplicationUiElement
|
|||
for key in order
|
||||
name_new = selection[key]
|
||||
if attribute.translate
|
||||
name_new = App.i18n.translatePlain(name_new)
|
||||
name_new = App.i18n.translateInline(name_new)
|
||||
attribute.options.push {
|
||||
name: name_new
|
||||
value: key
|
||||
|
@ -162,7 +162,7 @@ class App.UiElement.ApplicationUiElement
|
|||
nameNew = item.name
|
||||
|
||||
if attribute.translate
|
||||
nameNew = App.i18n.translatePlain(nameNew)
|
||||
nameNew = App.i18n.translateInline(nameNew)
|
||||
|
||||
row =
|
||||
value: item.id,
|
||||
|
|
|
@ -23,7 +23,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
|||
organization:
|
||||
name: 'Organization'
|
||||
model: 'Organization'
|
||||
model_show: ['User', 'Organization']
|
||||
model_show: ['Organization']
|
||||
'customer.organization':
|
||||
name: 'Organization'
|
||||
model: 'Organization'
|
||||
|
|
|
@ -39,7 +39,6 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
|||
operatorsType =
|
||||
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to']
|
||||
'integer$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
||||
'^date': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
||||
'^select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
||||
'^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
||||
'^input$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty']
|
||||
|
@ -64,9 +63,8 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
|||
continue
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag)
|
||||
continue if _.contains(['created_at', 'updated_at'], row.name)
|
||||
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], row.name)
|
||||
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select'], row.tag)
|
||||
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title'], row.name)
|
||||
|
||||
# ignore passwords and relations
|
||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
|
||||
|
@ -130,10 +128,9 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
|||
@buildValueConfigMultiple: (config, meta) ->
|
||||
if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator)
|
||||
config.multiple = true
|
||||
config.nulloption = true
|
||||
else
|
||||
config.multiple = false
|
||||
config.nulloption = false
|
||||
config.nulloption = false
|
||||
return config
|
||||
|
||||
@HasPreCondition: ->
|
||||
|
|
|
@ -6,7 +6,7 @@ class App.UiElement.richtext
|
|||
attribute.value = attribute.value.text
|
||||
|
||||
item = $( App.view('generic/richtext')(attribute: attribute, toolButtons: @toolButtons) )
|
||||
item.find('[contenteditable]').ce(
|
||||
@contenteditable = item.find('[contenteditable]').ce(
|
||||
mode: attribute.type
|
||||
maxlength: attribute.maxlength
|
||||
buttons: attribute.buttons
|
||||
|
@ -21,12 +21,12 @@ class App.UiElement.richtext
|
|||
new App[plugin.controller](params)
|
||||
|
||||
if attribute.upload
|
||||
attachments = []
|
||||
@attachments = []
|
||||
item.append( $( App.view('generic/attachment')(attribute: attribute) ) )
|
||||
|
||||
renderFile = (file) ->
|
||||
renderFile = (file) =>
|
||||
item.find('.attachments').append(App.view('generic/attachment_item')(file))
|
||||
attachments.push file
|
||||
@attachments.push file
|
||||
|
||||
if params && params.attachments
|
||||
for file in params.attachments
|
||||
|
@ -46,10 +46,10 @@ class App.UiElement.richtext
|
|||
, form.form_id)
|
||||
|
||||
# remove items
|
||||
item.find('.attachments').on('click', '.js-delete', (e) ->
|
||||
item.find('.attachments').on('click', '.js-delete', (e) =>
|
||||
id = $(e.currentTarget).data('id')
|
||||
attachments = _.filter(
|
||||
attachments,
|
||||
@attachments = _.filter(
|
||||
@attachments,
|
||||
(item) ->
|
||||
return if item.id.toString() is id.toString()
|
||||
item
|
||||
|
@ -71,35 +71,67 @@ class App.UiElement.richtext
|
|||
element.empty()
|
||||
)
|
||||
|
||||
App.Delay.set( ->
|
||||
uploader = new App.Html5Upload(
|
||||
uploadUrl: "#{App.Config.get('api_path')}/attachments"
|
||||
dropContainer: item.closest('form')
|
||||
cancelContainer: item.find('.js-cancel')
|
||||
inputField: item.find('input')
|
||||
data:
|
||||
form_id: item.closest('form').find('[name=form_id]').val()
|
||||
@progressBar = item.find('.attachmentUpload-progressBar')
|
||||
@progressText = item.find('.js-percentage')
|
||||
@attachmentPlaceholder = item.find('.attachmentPlaceholder')
|
||||
@attachmentUpload = item.find('.attachmentUpload')
|
||||
@attachmentsHolder = item.find('.attachments')
|
||||
@cancelContainer = item.find('.js-cancel')
|
||||
|
||||
onFileStartCallback: ->
|
||||
item.find('[contenteditable]').trigger('fileUploadStart')
|
||||
u = => html5Upload.initialize(
|
||||
uploadUrl: "#{App.Config.get('api_path')}/attachments"
|
||||
dropContainer: item.closest('form').get(0)
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: item.find('input').get(0)
|
||||
maxSimultaneousUploads: 1,
|
||||
key: 'File'
|
||||
data:
|
||||
form_id: item.closest('form').find('[name=form_id]').val()
|
||||
onFileAdded: (file) =>
|
||||
|
||||
onFileCompletedCallback: (response) ->
|
||||
renderFile(response.data)
|
||||
item.find('input').val('')
|
||||
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
||||
file.on(
|
||||
onStart: =>
|
||||
@attachmentPlaceholder.addClass('hide')
|
||||
@attachmentUpload.removeClass('hide')
|
||||
@cancelContainer.removeClass('hide')
|
||||
item.find('[contenteditable]').trigger('fileUploadStart')
|
||||
App.Log.debug 'UiElement.richtext', 'upload start'
|
||||
|
||||
onFileAbortedCallback: ->
|
||||
item.find('input').val('')
|
||||
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
||||
onAborted: =>
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
item.find('input').val('')
|
||||
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
||||
|
||||
attachmentPlaceholder: item.find('.attachmentPlaceholder')
|
||||
attachmentUpload: item.find('.attachmentUpload')
|
||||
progressBar: item.find('.attachmentUpload-progressBar')
|
||||
progressText: item.find('.js-percentage')
|
||||
)
|
||||
# Called after received response from the server
|
||||
onCompleted: (response) =>
|
||||
response = JSON.parse(response)
|
||||
|
||||
uploader.render()
|
||||
, 100, undefined, 'form_upload')
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
|
||||
# reset progress bar
|
||||
@progressBar.width(parseInt(0) + '%')
|
||||
@progressText.text('')
|
||||
|
||||
renderFile(response.data)
|
||||
item.find('input').val('')
|
||||
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
||||
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
|
||||
|
||||
# Called during upload progress, first parameter
|
||||
# is decimal value from 0 to 100.
|
||||
onProgress: (progress, fileSize, uploadedBytes) =>
|
||||
@progressBar.width(parseInt(progress) + '%')
|
||||
@progressText.text(parseInt(progress))
|
||||
# hide cancel on 90%
|
||||
if parseInt(progress) >= 90
|
||||
@cancelContainer.addClass('hide')
|
||||
App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress)
|
||||
|
||||
)
|
||||
)
|
||||
App.Delay.set(u, 100, undefined, 'form_upload')
|
||||
|
||||
item
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller
|
|||
events:
|
||||
'click .type-tabs .tab': 'changeFormType'
|
||||
'submit form': 'submit'
|
||||
'click .form-controls .js-cancel': 'cancel'
|
||||
'click .js-cancel': 'cancel'
|
||||
'click .js-active-toggle': 'toggleButton'
|
||||
|
||||
types: {
|
||||
|
@ -184,11 +184,8 @@ class App.TicketCreate extends App.Controller
|
|||
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||
|
||||
changed: =>
|
||||
return true if @hasAttachments()
|
||||
|
||||
formCurrent = @formParam( @$('.ticket-create') )
|
||||
diff = difference(@formDefault, formCurrent)
|
||||
|
||||
return false if !diff || _.isEmpty(diff)
|
||||
return true
|
||||
|
||||
|
@ -464,9 +461,6 @@ class App.TicketCreate extends App.Controller
|
|||
params: =>
|
||||
params = @formParam(@$('.main form'))
|
||||
|
||||
hasAttachments: =>
|
||||
@$('.richtext .attachments .attachment').length > 0
|
||||
|
||||
submit: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
|
@ -569,7 +563,7 @@ class App.TicketCreate extends App.Controller
|
|||
# save ticket, create article
|
||||
# check attachment
|
||||
if article['body']
|
||||
if !@hasAttachments()
|
||||
if @$('.richtext .attachments .attachment').length < 1
|
||||
matchingWord = App.Utils.checkAttachmentReference(article['body'])
|
||||
if matchingWord
|
||||
if !confirm(App.i18n.translateContent('You use %s in text but no attachment is attached. Do you want to continue?', matchingWord))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class CoreWorkflow extends App.ControllerSubContent
|
||||
requiredPermission: 'admin.core_workflow'
|
||||
header: 'Core Workflows'
|
||||
header: 'Core Workflow'
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
|
@ -54,4 +54,4 @@ class CoreWorkflow extends App.ControllerSubContent
|
|||
}
|
||||
return mapping[screen] || screen
|
||||
|
||||
App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflows', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin')
|
||||
App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflow', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin')
|
||||
|
|
|
@ -27,13 +27,11 @@ class App.KnowledgeBasePublicMenuManager extends App.Controller
|
|||
{
|
||||
headline: 'Header menu',
|
||||
identifier: 'header',
|
||||
color: kb.color_header,
|
||||
color_link: kb.color_header_link
|
||||
color: kb.color_header
|
||||
},
|
||||
{
|
||||
headline: 'Footer menu',
|
||||
identifier: 'footer',
|
||||
color_link: 'hsl(207,12%,50%)'
|
||||
identifier: 'footer'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1314,7 +1314,7 @@ class Table extends App.Controller
|
|||
return if ticketListShow[0] || @permissionCheck('ticket.agent')
|
||||
|
||||
tickets_count = user.lifetimeCustomerTicketsCount()
|
||||
@html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0, is_allowed_to_create_ticket: @Config.get('customer_ticket_create'))
|
||||
@html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0)
|
||||
|
||||
if tickets_count == 0
|
||||
@listenTo user, 'refresh', =>
|
||||
|
|
|
@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller
|
|||
formMeta = data.form_meta
|
||||
|
||||
# on the following states we want to rerender the ticket:
|
||||
# - if the object attribute configuration has changed (attribute values, dependecies, filters)
|
||||
# - if the object attribute configuration has changed (attribute values, restrictions, filters)
|
||||
# - if the user view has changed (agent/customer)
|
||||
# - if the ticket permission has changed (read/write/full)
|
||||
if @view && ( !_.isEqual(@formMeta.configure_attributes, formMeta.configure_attributes) || !_.isEqual(@formMeta.dependencies, formMeta.dependencies) || !_.isEqual(@formMeta.filter, formMeta.filter) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable )
|
||||
if @view && ( !_.isEqual(@formMeta, formMeta) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable )
|
||||
@renderDone = false
|
||||
|
||||
@view = view
|
||||
|
@ -214,7 +214,6 @@ class App.TicketZoom extends App.Controller
|
|||
|
||||
# render page
|
||||
@render(local)
|
||||
App.Event.trigger('ui::ticket::load', data)
|
||||
|
||||
meta: =>
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@controllerBind('ui:rerender', =>
|
||||
@adjustedTextarea = false
|
||||
@defaults = @ui.taskGet('article')
|
||||
@attachments = @defaults.attachments || []
|
||||
@attachments = @defaults.attachments
|
||||
@render()
|
||||
)
|
||||
|
||||
|
@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
|
||||
@tokanice(@type)
|
||||
|
||||
if @defaults.body or @attachments.length > 0 or @isIE10()
|
||||
if @defaults.body or @isIE10()
|
||||
@openTextarea(null, true)
|
||||
|
||||
tokanice: (type = 'email') ->
|
||||
|
@ -191,30 +191,82 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
maxlength: 150000
|
||||
})
|
||||
|
||||
new App.Html5Upload(
|
||||
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
|
||||
dropContainer: @$('.article-add')
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: @$('.article-attachment input')
|
||||
html5Upload.initialize(
|
||||
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
|
||||
dropContainer: @$('.article-add').get(0)
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: @$('.article-attachment input').get(0)
|
||||
key: 'File'
|
||||
maxSimultaneousUploads: 1
|
||||
onFileAdded: (file) =>
|
||||
|
||||
onFileStartCallback: =>
|
||||
@callbackFileUploadStart?()
|
||||
file.on(
|
||||
|
||||
onFileCompletedCallback: (response) =>
|
||||
@attachments.push response.data
|
||||
@renderAttachment(response.data)
|
||||
@$('.article-attachment input').val('')
|
||||
onStart: =>
|
||||
@attachmentPlaceholder.addClass('hide')
|
||||
@attachmentUpload.removeClass('hide')
|
||||
@cancelContainer.removeClass('hide')
|
||||
|
||||
@callbackFileUploadStop?()
|
||||
if @callbackFileUploadStart
|
||||
@callbackFileUploadStart()
|
||||
|
||||
onFileAbortedCallback: =>
|
||||
@callbackFileUploadStop?()
|
||||
onAborted: =>
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
@$('.article-attachment input').val('')
|
||||
|
||||
attachmentPlaceholder: @attachmentPlaceholder
|
||||
attachmentUpload: @attachmentUpload
|
||||
progressBar: @progressBar
|
||||
progressText: @progressText
|
||||
).render()
|
||||
if @callbackFileUploadStop
|
||||
@callbackFileUploadStop()
|
||||
|
||||
# Called after received response from the server
|
||||
onCompleted: (response) =>
|
||||
|
||||
response = JSON.parse(response)
|
||||
@attachments.push response.data
|
||||
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
|
||||
# reset progress bar
|
||||
@progressBar.width(parseInt(0) + '%')
|
||||
@progressText.text('')
|
||||
|
||||
@renderAttachment(response.data)
|
||||
@$('.article-attachment input').val('')
|
||||
|
||||
if @callbackFileUploadStop
|
||||
@callbackFileUploadStop()
|
||||
|
||||
# Called during upload progress, first parameter
|
||||
# is decimal value from 0 to 100.
|
||||
onProgress: (progress, fileSize, uploadedBytes) =>
|
||||
@progressBar.width(parseInt(progress) + '%')
|
||||
@progressText.text(parseInt(progress))
|
||||
# hide cancel on 90%
|
||||
if parseInt(progress) >= 90
|
||||
@cancelContainer.addClass('hide')
|
||||
|
||||
# Called when upload failed
|
||||
onError: (message) =>
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
@$('.article-attachment input').val('')
|
||||
|
||||
if @callbackFileUploadStop
|
||||
@callbackFileUploadStop()
|
||||
|
||||
new App.ControllerModal(
|
||||
head: 'Upload Failed'
|
||||
buttonCancel: 'Cancel'
|
||||
buttonCancelClass: 'btn--danger'
|
||||
buttonSubmit: false
|
||||
message: message
|
||||
shown: true
|
||||
small: true
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@bindAttachmentDelete()
|
||||
|
||||
|
|
|
@ -119,9 +119,7 @@ class App.FormHandlerCoreWorkflow
|
|||
|
||||
valueFound = false
|
||||
for value in values
|
||||
|
||||
# false values are valid values e.g. for boolean fields (be careful)
|
||||
if value isnt undefined && paramValue isnt undefined
|
||||
if value && paramValue
|
||||
if value.toString() == paramValue.toString()
|
||||
valueFound = true
|
||||
break
|
||||
|
|
|
@ -1,21 +1,12 @@
|
|||
# No usage of a ControllerObserver here because we want to use
|
||||
# the data of the ticket zoom ajax request which is using the all=true parameter
|
||||
# and contain the core workflow information as well. Without observer we also
|
||||
# dont have double rendering because of the zoom (all=true) and observer (full=true) render callback
|
||||
class Edit extends App.Controller
|
||||
constructor: (params) ->
|
||||
super
|
||||
@controllerBind('ui::ticket::load', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||
class Edit extends App.ControllerObserver
|
||||
model: 'Ticket'
|
||||
observeNot:
|
||||
created_at: true
|
||||
updated_at: true
|
||||
globalRerender: false
|
||||
|
||||
@ticket = App.Ticket.find(@ticket.id)
|
||||
@formMeta = data.form_meta
|
||||
@render()
|
||||
)
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
defaults = @ticket.attributes()
|
||||
render: (ticket, diff) =>
|
||||
defaults = ticket.attributes()
|
||||
delete defaults.article # ignore article infos
|
||||
followUpPossible = App.Group.find(defaults.group_id).follow_up_possible
|
||||
ticketState = App.TicketState.find(defaults.state_id).name
|
||||
|
@ -25,13 +16,10 @@ class Edit extends App.Controller
|
|||
|
||||
if !_.isEmpty(taskState)
|
||||
defaults = _.extend(defaults, taskState)
|
||||
# remove core workflow data because it should trigger a request to get data
|
||||
# for the new ticket + eventually changed task state
|
||||
@formMeta.core_workflow = undefined
|
||||
|
||||
if followUpPossible == 'new_ticket' && ticketState != 'closed' ||
|
||||
followUpPossible != 'new_ticket' ||
|
||||
@permissionCheck('admin') || @ticket.currentView() is 'agent'
|
||||
@permissionCheck('admin') || ticket.currentView() is 'agent'
|
||||
@controllerFormSidebarTicket = new App.ControllerForm(
|
||||
elReplace: @el
|
||||
model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes }
|
||||
|
@ -40,7 +28,7 @@ class Edit extends App.Controller
|
|||
filter: @formMeta.filter
|
||||
formMeta: @formMeta
|
||||
params: defaults
|
||||
isDisabled: !@ticket.editable()
|
||||
isDisabled: !ticket.editable()
|
||||
taskKey: @taskKey
|
||||
core_workflow: {
|
||||
callbacks: [@markForm]
|
||||
|
@ -56,7 +44,7 @@ class Edit extends App.Controller
|
|||
filter: @formMeta.filter
|
||||
formMeta: @formMeta
|
||||
params: defaults
|
||||
isDisabled: @ticket.editable()
|
||||
isDisabled: ticket.editable()
|
||||
taskKey: @taskKey
|
||||
core_workflow: {
|
||||
callbacks: [@markForm]
|
||||
|
@ -69,8 +57,8 @@ class Edit extends App.Controller
|
|||
return if @resetBind
|
||||
@resetBind = true
|
||||
@controllerBind('ui::ticket::taskReset', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||
@render()
|
||||
return if data.ticket_id.toString() isnt ticket.id.toString()
|
||||
@render(ticket)
|
||||
)
|
||||
|
||||
class SidebarTicket extends App.Controller
|
||||
|
@ -140,7 +128,6 @@ class SidebarTicket extends App.Controller
|
|||
|
||||
@edit = new Edit(
|
||||
object_id: @ticket.id
|
||||
ticket: @ticket
|
||||
el: localEl.find('.edit')
|
||||
taskGet: @taskGet
|
||||
formMeta: @formMeta
|
||||
|
|
|
@ -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')
|
|
@ -255,7 +255,7 @@
|
|||
manager.ajaxUpload(manager.uploadsQueue.shift());
|
||||
}
|
||||
};
|
||||
xhr.onabort = function (event) {
|
||||
xhr.abort = function (event) {
|
||||
console.log('Upload abort');
|
||||
|
||||
// Reduce number of active uploads:
|
||||
|
@ -269,7 +269,6 @@
|
|||
// Triggered when upload fails:
|
||||
xhr.onerror = function () {
|
||||
console.log('Upload failed: ', upload.fileName);
|
||||
upload.events.onError('Upload failed: ' + upload.fileName);
|
||||
};
|
||||
|
||||
// Append additional data if provided:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class App.KnowledgeBase extends App.Model
|
||||
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'color_header_link', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
|
||||
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
|
||||
@extend Spine.Model.Ajax
|
||||
@extend App.KnowledgeBaseActions
|
||||
@url: @apiPath + '/knowledge_bases'
|
||||
|
@ -148,17 +148,6 @@ class App.KnowledgeBase extends App.Model
|
|||
display: false
|
||||
horizontal: true
|
||||
shown: true
|
||||
}, {
|
||||
name: 'color_header_link'
|
||||
display: 'Header Link Color'
|
||||
tag: 'color'
|
||||
style: 'block'
|
||||
null: false
|
||||
screen:
|
||||
admin_style_color_header_link:
|
||||
display: false
|
||||
horizontal: true
|
||||
shown: true
|
||||
# Layout picker is disabled in V1
|
||||
#}, {
|
||||
# name: 'homepage_layout'
|
||||
|
|
|
@ -344,12 +344,9 @@ class App.User extends App.Model
|
|||
@sameOrganization?(requester)
|
||||
|
||||
isChangeableBy: (requester) ->
|
||||
# full access for admins
|
||||
return true if requester.permission('admin.user')
|
||||
# forbid non-agents to change users
|
||||
# allow agents to change customers
|
||||
return false if !requester.permission('ticket.agent')
|
||||
# allow agents to change customers only
|
||||
return false if @permission(['admin.user', 'ticket.agent'])
|
||||
@permission('ticket.customer')
|
||||
|
||||
isDeleteableBy: (requester) ->
|
||||
|
|
|
@ -6,15 +6,11 @@
|
|||
<% if @has_any_tickets: %>
|
||||
<p><%- @T('You have no tickets to display in this overview.') %></p>
|
||||
<% else: %>
|
||||
<% if @is_allowed_to_create_ticket: %>
|
||||
<p><%- @T('You have not created a ticket yet.') %></p>
|
||||
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
|
||||
<p><%- @T('Please click the button below to create your first one.') %></p>
|
||||
<p><%- @T('You have not created a ticket yet.') %></p>
|
||||
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
|
||||
<p><%- @T('Please click the button below to create your first one.') %></p>
|
||||
|
||||
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
|
||||
<% else: %>
|
||||
<p><%- @T('You currently don\'t have any tickets.') %></p>
|
||||
<% end %>
|
||||
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div class="form-group js-sure">
|
||||
<h3 class="danger-color"><%- @T('Warning') %></h3>
|
||||
<p class="danger-color"><%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translatePlain('delete').toUpperCase()) %></p>
|
||||
<p class="danger-color"><%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translateInline('delete').toUpperCase()) %></p>
|
||||
<%- @sure_html %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ...
|
||||
</div>
|
||||
<div class="attachmentUpload-cancel js-cancel">
|
||||
<%- @Icon('diagonal-cross') %><%- @T('Cancel Upload') %>
|
||||
<%- @Icon('diagonal-cross') %></div><%- @T('Cancel Upload') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachmentUpload-progressBar" style="width: 0%"></div>
|
||||
|
|
|
@ -26,12 +26,11 @@ class App.KnowledgeBaseNewModal extends App.ControllerModal
|
|||
App.UiElement[attribute.tag].prepareParams?(attribute, dom, params)
|
||||
|
||||
applyDefaults: (params) ->
|
||||
params['iconset'] = 'FontAwesome'
|
||||
params['color_highlight'] = '#38ae6a'
|
||||
params['color_header'] = '#f9fafb'
|
||||
params['color_header_link'] = 'hsl(206,8%,50%)'
|
||||
params['homepage_layout'] = 'grid'
|
||||
params['category_layout'] = 'grid'
|
||||
params['iconset'] = 'FontAwesome'
|
||||
params['color_highlight'] = '#38ae6a'
|
||||
params['color_header'] = '#f9fafb'
|
||||
params['homepage_layout'] = 'grid'
|
||||
params['category_layout'] = 'grid'
|
||||
|
||||
onSubmit: (e) ->
|
||||
params = @formParams(@el)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="kb-menu-preview">
|
||||
<div class="label"><%= kb_locale.systemLocale().name %></div>
|
||||
|
||||
<div class="kb-menu-preview-container kb-menu-preview-container--<%= location.identifier %>" style="background-color: <%= location.color %>; color: <%= location.color_link %>;">
|
||||
<div class="kb-menu-preview-container kb-menu-preview-container--<%= location.identifier %>" style="background-color: <%= location.color %>">
|
||||
<% menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, location.identifier) %>
|
||||
|
||||
<% if menu_items.length == 0: %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="login fullscreen">
|
||||
<div class="fullscreen-center">
|
||||
<div class="fullscreen-body">
|
||||
<p><%- @T('Log in to %s', @C('fqdn')) %></p>
|
||||
<p><%- @T('Login with %s', @C('fqdn')) %></p>
|
||||
|
||||
<% if @C('maintenance_mode'): %>
|
||||
<div class="hero-unit alert alert--danger js-maintenanceMode"><%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %></div>
|
||||
|
|
|
@ -3,11 +3,6 @@
|
|||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<p>
|
||||
<%- @T('The installation of packages comes with security implications, because arbitrary code will be executed in the context of the Zammad application.') %>
|
||||
<br>
|
||||
<%- @T('Only packages from known, trusted and verfied sources should be installed.') %>
|
||||
</p>
|
||||
<p>
|
||||
<%- @T('After installing, updating or uninstalling packages the following commands need to be executed on the server:') %>
|
||||
<ul>
|
||||
|
@ -53,4 +48,4 @@
|
|||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
|
@ -2661,7 +2661,7 @@ input.has-error {
|
|||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
color: hsl(206,8%,50%);
|
||||
}
|
||||
|
||||
.label {
|
||||
|
@ -7017,8 +7017,8 @@ footer {
|
|||
.article-new .textBubble {
|
||||
border-color: #b3b3b3;
|
||||
border-radius: 5px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
|
@ -7051,23 +7051,13 @@ footer {
|
|||
padding: 10px 0;
|
||||
color: #b3b3b3;
|
||||
overflow: hidden;
|
||||
@extend .u-textTruncate;
|
||||
@extend .u-unclickable, .u-textTruncate;
|
||||
}
|
||||
|
||||
.attachments:not(:empty) {
|
||||
padding: 9px 5px;
|
||||
border-top: 1px solid hsl(0,0%,93%);
|
||||
margin: 6px -12px 30px;
|
||||
}
|
||||
|
||||
.ticket-create .attachments:not(:empty) {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.ticket-create .attachment--row {
|
||||
line-height: 1.45;
|
||||
margin: 6px 0 30px;
|
||||
}
|
||||
|
||||
.attachment.attachment--row {
|
||||
|
@ -8452,10 +8442,6 @@ footer {
|
|||
|
||||
.dropdown li.with-category, .dropdown.dropdown--actions li.with-category {
|
||||
line-height: 19.5px;
|
||||
|
||||
small {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.dropdown--actions li.with-category {
|
||||
|
|
|
@ -10,8 +10,8 @@ class ApplicationController < ActionController::Base
|
|||
include ApplicationController::RendersModels
|
||||
include ApplicationController::HasUser
|
||||
include ApplicationController::HasResponseExtentions
|
||||
include ApplicationController::HasDownload
|
||||
include ApplicationController::PreventsCsrf
|
||||
include ApplicationController::HasSecureContentSecurityPolicyForDownloads
|
||||
include ApplicationController::LogsHttpAccess
|
||||
include ApplicationController::Authorizes
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -6,13 +6,14 @@ class AttachmentsController < ApplicationController
|
|||
prepend_before_action :authentication_check_only, only: %i[show destroy]
|
||||
|
||||
def show
|
||||
view_type = params[:preview] ? 'preview' : nil
|
||||
content = @file.content_preview if params[:preview] && @file.preferences[:content_preview]
|
||||
content ||= @file.content
|
||||
|
||||
send_data(
|
||||
download_file.content(view_type),
|
||||
filename: download_file.filename,
|
||||
type: download_file.content_type,
|
||||
disposition: download_file.disposition
|
||||
content,
|
||||
filename: @file.filename,
|
||||
type: @file.preferences['Content-Type'] || @file.preferences['Mime-Type'] || 'application/octet-stream',
|
||||
disposition: sanitized_disposition
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -51,7 +52,7 @@ class AttachmentsController < ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
Store.remove_item(download_file.id)
|
||||
Store.remove_item(@file.id)
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
|
@ -71,8 +72,18 @@ class AttachmentsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def sanitized_disposition
|
||||
disposition = params.fetch(:disposition, 'inline')
|
||||
valid_disposition = %w[inline attachment]
|
||||
return disposition if valid_disposition.include?(disposition)
|
||||
|
||||
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
|
||||
end
|
||||
|
||||
def authorize!
|
||||
record = download_file&.store_object&.name&.safe_constantize&.find(download_file.o_id)
|
||||
@file = Store.find(params[:id])
|
||||
|
||||
record = @file&.store_object&.name&.safe_constantize&.find(@file.o_id)
|
||||
authorize(record) if record
|
||||
rescue Pundit::NotAuthorizedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
|
|
|
@ -156,7 +156,7 @@ class FormController < ApplicationController
|
|||
end
|
||||
|
||||
def token_gen(fingerprint)
|
||||
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32], serializer: JSON)
|
||||
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32])
|
||||
fingerprint = "#{Base64.strict_encode64(Setting.get('fqdn'))}:#{Time.zone.now.to_i}:#{Base64.strict_encode64(fingerprint)}"
|
||||
Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint))
|
||||
end
|
||||
|
@ -167,7 +167,7 @@ class FormController < ApplicationController
|
|||
raise Exceptions::Forbidden
|
||||
end
|
||||
begin
|
||||
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32], serializer: JSON)
|
||||
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32])
|
||||
result = crypt.decrypt_and_verify(Base64.decode64(token))
|
||||
rescue
|
||||
Rails.logger.info 'Invalid token for form!'
|
||||
|
|
|
@ -175,11 +175,29 @@ class TicketArticlesController < ApplicationController
|
|||
end
|
||||
raise Exceptions::Forbidden, 'Requested file id is not linked with article_id.' if !access
|
||||
|
||||
# find file
|
||||
file = Store.find(params[:id])
|
||||
|
||||
disposition = sanitized_disposition
|
||||
|
||||
content = nil
|
||||
if params[:view].present? && file.preferences[:resizable] == true
|
||||
if file.preferences[:content_inline] == true && params[:view] == 'inline'
|
||||
content = file.content_inline
|
||||
elsif file.preferences[:content_preview] == true && params[:view] == 'preview'
|
||||
content = file.content_preview
|
||||
end
|
||||
end
|
||||
|
||||
if content.blank?
|
||||
content = file.content
|
||||
end
|
||||
|
||||
send_data(
|
||||
download_file.content(params[:view]),
|
||||
filename: download_file.filename,
|
||||
type: download_file.content_type,
|
||||
disposition: download_file.disposition
|
||||
content,
|
||||
filename: file.filename,
|
||||
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
|
||||
disposition: disposition
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -260,4 +278,14 @@ class TicketArticlesController < ApplicationController
|
|||
|
||||
render json: result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sanitized_disposition
|
||||
disposition = params.fetch(:disposition, 'inline')
|
||||
valid_disposition = %w[inline attachment]
|
||||
return disposition if valid_disposition.include?(disposition)
|
||||
|
||||
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
|
||||
end
|
||||
end
|
||||
|
|
|
@ -722,28 +722,31 @@ curl http://localhost/api/v1/users/image/8d6cca1c6bdc226cf2ba131e264ca2c7 -v -u
|
|||
=end
|
||||
|
||||
def image
|
||||
|
||||
# cache image
|
||||
response.headers['Expires'] = 1.year.from_now.httpdate
|
||||
response.headers['Cache-Control'] = 'cache, store, max-age=31536000, must-revalidate'
|
||||
response.headers['Pragma'] = 'cache'
|
||||
|
||||
file = Avatar.get_by_hash(params[:hash])
|
||||
|
||||
if file
|
||||
file_content_type = file.preferences['Content-Type'] || file.preferences['Mime-Type']
|
||||
|
||||
return serve_default_image if ActiveStorage.content_types_allowed_inline.exclude?(file_content_type)
|
||||
|
||||
send_data(
|
||||
file.content,
|
||||
filename: file.filename,
|
||||
type: file_content_type,
|
||||
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'],
|
||||
disposition: 'inline'
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
serve_default_image
|
||||
# serve default image
|
||||
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
|
||||
send_data(
|
||||
Base64.decode64(image),
|
||||
filename: 'image.gif',
|
||||
type: 'image/gif',
|
||||
disposition: 'inline'
|
||||
)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
@ -775,11 +778,6 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
|
|||
return
|
||||
end
|
||||
|
||||
if ActiveStorage::Variant::WEB_IMAGE_CONTENT_TYPES.exclude?(file_full[:mime_type])
|
||||
render json: { error: 'Mime type is invalid' }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
file_resize = StaticAssets.data_url_attributes(params[:avatar_resize])
|
||||
rescue
|
||||
|
@ -1063,15 +1061,4 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
|
|||
|
||||
render json: { message: 'ok' }, status: :created
|
||||
end
|
||||
|
||||
def serve_default_image
|
||||
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
|
||||
|
||||
send_data(
|
||||
Base64.decode64(image),
|
||||
filename: 'image.gif',
|
||||
type: 'image/gif',
|
||||
disposition: 'inline'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,9 @@ class WebhooksController < ApplicationController
|
|||
prepend_before_action { authentication_check && authorize! }
|
||||
|
||||
def preview
|
||||
ticket = TicketPolicy::ReadScope.new(current_user).resolve.last
|
||||
access_condition = Ticket.access_condition(current_user, 'read')
|
||||
|
||||
ticket = Ticket.where(access_condition).last
|
||||
|
||||
render json: JSON.pretty_generate({
|
||||
ticket: TriggerWebhookJob::RecordPayload.generate(ticket),
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
class UploadCacheCleanupJob < ApplicationJob
|
||||
def perform
|
||||
taskbar_form_ids = Taskbar.with_form_id.filter_map(&:persisted_form_id)
|
||||
return if store_object_id.blank?
|
||||
|
||||
Store.where(store_object_id: store_object_id).where('created_at < ?', 1.month.ago).where.not(o_id: taskbar_form_ids).find_each do |store|
|
||||
Store.remove_item(store.id)
|
||||
|
@ -13,6 +12,6 @@ class UploadCacheCleanupJob < ApplicationJob
|
|||
private
|
||||
|
||||
def store_object_id
|
||||
Store::Object.lookup(name: 'UploadCache')&.id
|
||||
Store::Object.lookup(name: 'UploadCache').id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,7 +121,7 @@ returns
|
|||
|
||||
key = "#{self.class}::aws::#{id}"
|
||||
cache = Cache.read(key)
|
||||
return filter_unauthorized_attributes(cache) if cache
|
||||
return cache if cache
|
||||
|
||||
attributes = self.attributes
|
||||
relevant = %i[has_and_belongs_to_many has_many]
|
||||
|
@ -160,7 +160,7 @@ returns
|
|||
filter_attributes(attributes)
|
||||
|
||||
Cache.write(key, attributes)
|
||||
filter_unauthorized_attributes(attributes)
|
||||
attributes
|
||||
end
|
||||
|
||||
=begin
|
||||
|
@ -234,7 +234,8 @@ returns
|
|||
end
|
||||
|
||||
filter_attributes(attributes)
|
||||
filter_unauthorized_attributes(attributes)
|
||||
|
||||
attributes
|
||||
end
|
||||
|
||||
def filter_attributes(attributes)
|
||||
|
@ -242,10 +243,6 @@ returns
|
|||
attributes.except!('password', 'token', 'tokens', 'token_ids')
|
||||
end
|
||||
|
||||
def filter_unauthorized_attributes(attributes)
|
||||
attributes
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
reference if association id check
|
||||
|
|
|
@ -72,6 +72,7 @@ add avatar by url
|
|||
=end
|
||||
|
||||
def self.add(data)
|
||||
|
||||
# lookups
|
||||
if data[:object]
|
||||
object_id = ObjectLookup.by_name(data[:object])
|
||||
|
|
|
@ -7,8 +7,6 @@ class Channel::Driver::Imap < Channel::EmailParser
|
|||
FETCH_METADATA_TIMEOUT = 2.minutes
|
||||
FETCH_MSG_TIMEOUT = 4.minutes
|
||||
EXPUNGE_TIMEOUT = 16.minutes
|
||||
DEFAULT_TIMEOUT = 45.seconds
|
||||
CHECK_ONLY_TIMEOUT = 6.seconds
|
||||
|
||||
def fetchable?(_channel)
|
||||
true
|
||||
|
@ -112,7 +110,10 @@ example
|
|||
Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},starttls=#{starttls},folder=#{folder},keep_on_server=#{keep_on_server},auth_type=#{options.fetch(:auth_type, 'LOGIN')})"
|
||||
|
||||
# on check, reduce open_timeout to have faster probing
|
||||
check_type_timeout = check_type == 'check' ? CHECK_ONLY_TIMEOUT : DEFAULT_TIMEOUT
|
||||
check_type_timeout = 45
|
||||
if check_type == 'check'
|
||||
check_type_timeout = 6
|
||||
end
|
||||
|
||||
timeout(check_type_timeout) do
|
||||
@imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false)
|
||||
|
|
|
@ -502,7 +502,7 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
|||
path = Rails.root.join('tmp/unprocessable_mail')
|
||||
files = []
|
||||
Dir.glob("#{path}/*.eml") do |entry|
|
||||
ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, File.binread(entry))
|
||||
ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, IO.binread(entry))
|
||||
next if ticket.blank?
|
||||
|
||||
files.push entry
|
||||
|
|
|
@ -30,26 +30,10 @@ class CoreWorkflow::Attributes
|
|||
end
|
||||
end
|
||||
|
||||
def selectable_field?(key)
|
||||
return if key == 'id'
|
||||
return if !@payload['params'].key?(key)
|
||||
|
||||
# some objects have no attributes like "CoreWorkflow"-object as well.
|
||||
# attributes only exists in the frontend so we skip this check
|
||||
return true if object_elements.blank?
|
||||
|
||||
object_elements_hash.key?(key)
|
||||
end
|
||||
|
||||
def overwrite_selected(result)
|
||||
selected_attributes = selected_only.attributes
|
||||
selected_attributes.each_key do |key|
|
||||
next if !selectable_field?(key)
|
||||
|
||||
# special behaviour for owner id
|
||||
if key == 'owner_id' && selected_attributes[key].nil?
|
||||
selected_attributes[key] = 1
|
||||
end
|
||||
next if selected_attributes[key].nil?
|
||||
|
||||
result[key.to_sym] = selected_attributes[key]
|
||||
end
|
||||
|
@ -71,10 +55,7 @@ class CoreWorkflow::Attributes
|
|||
# dont use lookup here because the cache will not
|
||||
# know about new attributes and make crashes
|
||||
@saved_only ||= payload_class.find_by(id: @payload['params']['id'])
|
||||
|
||||
# we use marshal here because clone still uses references and dup can't
|
||||
# detect changes for the rails object
|
||||
Marshal.load(Marshal.dump(@saved_only))
|
||||
@saved_only.dup
|
||||
end
|
||||
|
||||
def saved
|
||||
|
@ -87,10 +68,6 @@ class CoreWorkflow::Attributes
|
|||
end
|
||||
end
|
||||
|
||||
def object_elements_hash
|
||||
@object_elements_hash ||= object_elements.index_by { |x| x[:name] }
|
||||
end
|
||||
|
||||
def screen_value(attribute, type)
|
||||
attribute[:screens].dig(@payload['screen'], type)
|
||||
end
|
||||
|
|
|
@ -18,26 +18,4 @@ class CoreWorkflow::Result::Backend
|
|||
def result(backend, field, value = nil)
|
||||
@result_object.run_backend_value(backend, field, value)
|
||||
end
|
||||
|
||||
def saved_value
|
||||
|
||||
# make sure we have a saved object
|
||||
return if @result_object.attributes.saved_only.blank?
|
||||
|
||||
# we only want to have the saved value in the restrictions
|
||||
# if no changes happend to the form. If the users does changes
|
||||
# to the form then also the saved value should get removed
|
||||
return if @result_object.attributes.selected.changed?
|
||||
|
||||
# attribute can be blank e.g. in custom development
|
||||
# or if attribute is only available in the frontend but not
|
||||
# in the backend
|
||||
return if attribute.blank?
|
||||
|
||||
@result_object.attributes.saved_attribute_value(attribute).to_s
|
||||
end
|
||||
|
||||
def attribute
|
||||
@attribute ||= @result_object.attributes.object_elements_hash[field]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,14 +3,8 @@
|
|||
class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption
|
||||
def run
|
||||
@result_object.result[:restrict_values][field] ||= Array(@result_object.payload['params'][field])
|
||||
@result_object.result[:restrict_values][field] -= Array(config_value)
|
||||
@result_object.result[:restrict_values][field] -= Array(@perform_config['remove_option'])
|
||||
remove_excluded_param_values
|
||||
true
|
||||
end
|
||||
|
||||
def config_value
|
||||
result = Array(@perform_config['remove_option'])
|
||||
result -= Array(saved_value)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,23 +5,21 @@ class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption
|
|||
@result_object.result[:restrict_values][field] = if restriction_set?
|
||||
restrict_values
|
||||
else
|
||||
config_value
|
||||
replace_values
|
||||
end
|
||||
remove_excluded_param_values
|
||||
true
|
||||
end
|
||||
|
||||
def config_value
|
||||
result = Array(@perform_config['set_fixed_to'])
|
||||
result |= Array(saved_value)
|
||||
result
|
||||
end
|
||||
|
||||
def restriction_set?
|
||||
@result_object.result[:restrict_values][field]
|
||||
end
|
||||
|
||||
def restrict_values
|
||||
@result_object.result[:restrict_values][field].reject { |v| config_value.exclude?(v) }
|
||||
@result_object.result[:restrict_values][field].reject { |v| Array(@perform_config['set_fixed_to']).exclude?(v) }
|
||||
end
|
||||
|
||||
def replace_values
|
||||
Array(@perform_config['set_fixed_to'])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,8 +12,6 @@ class Group < ApplicationModel
|
|||
include HasTicketCreateScreenImpact
|
||||
include HasSearchIndexBackend
|
||||
|
||||
include Group::Assets
|
||||
|
||||
belongs_to :email_address, optional: true
|
||||
belongs_to :signature, optional: true
|
||||
|
||||
|
|
|
@ -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
|
|
@ -27,9 +27,8 @@ class KnowledgeBase < ApplicationModel
|
|||
validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||
|
||||
validates :color_highlight, presence: true, color: true
|
||||
validates :color_header, presence: true, color: true
|
||||
validates :color_header_link, presence: true, color: true
|
||||
validates :color_highlight, presence: true, color: true
|
||||
validates :color_header, presence: true, color: true
|
||||
|
||||
validates :iconset, inclusion: { in: KnowledgeBase::ICONSETS }
|
||||
|
||||
|
|
|
@ -678,7 +678,7 @@ to send no browser reload event, pass false
|
|||
|
||||
=begin
|
||||
|
||||
where attributes are used in conditions
|
||||
where attributes are used by triggers, overviews or schedulers
|
||||
|
||||
result = ObjectManager::Attribute.attribute_to_references_hash
|
||||
|
||||
|
@ -696,36 +696,22 @@ where attributes are used in conditions
|
|||
=end
|
||||
|
||||
def self.attribute_to_references_hash
|
||||
objects = Trigger.select(:name, :condition) + Overview.select(:name, :condition) + Job.select(:name, :condition)
|
||||
attribute_list = {}
|
||||
objects.each do |item|
|
||||
item.condition.each do |condition_key, _condition_attributes|
|
||||
attribute_list[condition_key] ||= {}
|
||||
attribute_list[condition_key][item.class.name] ||= []
|
||||
next if attribute_list[condition_key][item.class.name].include?(item.name)
|
||||
|
||||
attribute_to_references_hash_objects
|
||||
.map { |elem| elem.select(:name, :condition) }
|
||||
.flatten
|
||||
.each do |item|
|
||||
item.condition.each do |condition_key, _condition_attributes|
|
||||
attribute_list[condition_key] ||= {}
|
||||
attribute_list[condition_key][item.class.name] ||= []
|
||||
next if attribute_list[condition_key][item.class.name].include?(item.name)
|
||||
|
||||
attribute_list[condition_key][item.class.name].push item.name
|
||||
end
|
||||
attribute_list[condition_key][item.class.name].push item.name
|
||||
end
|
||||
|
||||
end
|
||||
attribute_list
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
models that may reference attributes
|
||||
|
||||
=end
|
||||
|
||||
def self.attribute_to_references_hash_objects
|
||||
Models.all.keys.select { |elem| elem.include? ChecksConditionValidation }
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
is certain attribute used by triggers, overviews or schedulers
|
||||
|
||||
ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')
|
||||
|
|
|
@ -43,7 +43,7 @@ class ObjectManager::Element::Backend
|
|||
end
|
||||
|
||||
def screens
|
||||
@screens ||= attribute.screens.transform_values do |permission_options|
|
||||
attribute.screens.transform_values do |permission_options|
|
||||
screen_value(permission_options)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,12 +70,5 @@ returns
|
|||
end
|
||||
data
|
||||
end
|
||||
|
||||
def filter_unauthorized_attributes(attributes)
|
||||
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
|
||||
|
||||
attributes = super
|
||||
attributes.slice('id', 'name', 'active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -263,37 +263,32 @@ subsequently in a separate step.
|
|||
)
|
||||
end
|
||||
|
||||
Transaction.execute do
|
||||
# store package
|
||||
if !data[:reinstall]
|
||||
package_db = Package.create(meta)
|
||||
Store.add(
|
||||
object: 'Package',
|
||||
o_id: package_db.id,
|
||||
data: package.to_json,
|
||||
filename: "#{meta[:name]}-#{meta[:version]}.zpm",
|
||||
preferences: {},
|
||||
created_by_id: UserInfo.current_user_id || 1,
|
||||
)
|
||||
end
|
||||
|
||||
# write files
|
||||
package['files'].each do |file|
|
||||
if !allowed_file_path?(file['location'])
|
||||
raise "Can't create file, because of not allowed file location: #{file['location']}!"
|
||||
end
|
||||
|
||||
permission = file['permission'] || '644'
|
||||
content = Base64.decode64(file['content'])
|
||||
_write_file(file['location'], permission, content)
|
||||
end
|
||||
|
||||
# update package state
|
||||
package_db.state = 'installed'
|
||||
package_db.save
|
||||
# store package
|
||||
if !data[:reinstall]
|
||||
package_db = Package.create(meta)
|
||||
Store.add(
|
||||
object: 'Package',
|
||||
o_id: package_db.id,
|
||||
data: package.to_json,
|
||||
filename: "#{meta[:name]}-#{meta[:version]}.zpm",
|
||||
preferences: {},
|
||||
created_by_id: UserInfo.current_user_id || 1,
|
||||
)
|
||||
end
|
||||
|
||||
# write files
|
||||
package['files'].each do |file|
|
||||
permission = file['permission'] || '644'
|
||||
content = Base64.decode64(file['content'])
|
||||
_write_file(file['location'], permission, content)
|
||||
end
|
||||
|
||||
# update package state
|
||||
package_db.state = 'installed'
|
||||
package_db.save
|
||||
|
||||
# prebuild assets
|
||||
|
||||
package_db
|
||||
end
|
||||
|
||||
|
@ -488,9 +483,4 @@ execute all pending package migrations at once
|
|||
|
||||
true
|
||||
end
|
||||
|
||||
def self.allowed_file_path?(file)
|
||||
file.exclude?('..') && file.exclude?('%2e%2e')
|
||||
end
|
||||
private_class_method :allowed_file_path?
|
||||
end
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
class Report::Profile < ApplicationModel
|
||||
self.table_name = 'report_profiles'
|
||||
include ChecksConditionValidation
|
||||
validates :name, presence: true
|
||||
store :condition
|
||||
|
||||
|
|
|
@ -60,13 +60,5 @@ returns
|
|||
end
|
||||
data
|
||||
end
|
||||
|
||||
def filter_unauthorized_attributes(attributes)
|
||||
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
|
||||
|
||||
attributes = super
|
||||
attributes['name'] = "Role_#{id}"
|
||||
attributes.slice('id', 'name', 'group_ids', 'permission_ids', 'active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -926,16 +926,17 @@ try to find correct name
|
|||
end
|
||||
|
||||
# check if login already exists
|
||||
base_login = login.downcase.strip
|
||||
|
||||
alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ]
|
||||
alternatives.each do |suffix|
|
||||
self.login = "#{base_login}#{suffix}"
|
||||
self.login = login.downcase.strip
|
||||
check = true
|
||||
while check
|
||||
exists = User.find_by(login: login)
|
||||
return true if !exists || exists.id == id
|
||||
if exists && exists.id != id
|
||||
self.login = "#{login}#{rand(999)}" # rubocop:disable Zammad/ForbidRand
|
||||
else
|
||||
check = false
|
||||
end
|
||||
end
|
||||
|
||||
raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
|
||||
true
|
||||
end
|
||||
|
||||
def check_mail_delivery_failed
|
||||
|
|
|
@ -110,20 +110,5 @@ returns
|
|||
end
|
||||
data
|
||||
end
|
||||
|
||||
def filter_unauthorized_attributes(attributes)
|
||||
return super if UserInfo.assets.blank? || UserInfo.assets.agent?
|
||||
|
||||
# customer assets for the user session
|
||||
if UserInfo.current_user_id == id
|
||||
attributes = super
|
||||
attributes.except!('web', 'phone', 'mobile', 'fax', 'department', 'street', 'zip', 'city', 'country', 'address', 'note')
|
||||
return attributes
|
||||
end
|
||||
|
||||
# customer assets for other user
|
||||
attributes = super
|
||||
attributes.slice('id', 'firstname', 'lastname', 'image', 'image_source', 'active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,6 @@ class SettingPolicy < ApplicationPolicy
|
|||
private
|
||||
|
||||
def permitted?
|
||||
return false if record.preferences[:protected]
|
||||
return true if !record.preferences[:permission]
|
||||
|
||||
user.permissions?(record.preferences[:permission])
|
||||
|
|
|
@ -13,7 +13,7 @@ class TicketPolicy < ApplicationPolicy
|
|||
super
|
||||
end
|
||||
|
||||
def resolve # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def resolve # rubocop:disable Metrics/AbcSize
|
||||
raise NoMethodError, <<~ERR.chomp if instance_of?(TicketPolicy::BaseScope)
|
||||
specify an access type using a subclass of TicketPolicy::BaseScope
|
||||
ERR
|
||||
|
@ -26,19 +26,12 @@ class TicketPolicy < ApplicationPolicy
|
|||
bind.push(user.group_ids_access(self.class::ACCESS_TYPE))
|
||||
end
|
||||
|
||||
if user.permissions?('ticket.customer')
|
||||
if user.organization&.shared
|
||||
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
|
||||
bind.push(user.id, user.organization.id)
|
||||
else
|
||||
sql.push('tickets.customer_id = ?')
|
||||
bind.push(user.id)
|
||||
end
|
||||
end
|
||||
|
||||
# The report permission can access all tickets.
|
||||
if sql.empty? && !user.permissions?('report')
|
||||
sql.push '0 = 1' # Forbid unlimited access for all other permissions.
|
||||
if user.organization&.shared
|
||||
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
|
||||
bind.push(user.id, user.organization.id)
|
||||
else
|
||||
sql.push('tickets.customer_id = ?')
|
||||
bind.push(user.id)
|
||||
end
|
||||
|
||||
scope.where sql.join(' OR '), *bind
|
||||
|
|
|
@ -13,14 +13,11 @@ class UserPolicy < ApplicationPolicy
|
|||
end
|
||||
|
||||
def update?
|
||||
# full access for admins
|
||||
return true if user.permissions?('admin.user')
|
||||
# forbid non-agents to change users
|
||||
return false if !user.permissions?('ticket.agent')
|
||||
|
||||
# allow agents to change customers only
|
||||
return false if record.permissions?(['admin.user', 'ticket.agent'])
|
||||
|
||||
# allow agents to change customers
|
||||
record.permissions?('ticket.customer')
|
||||
end
|
||||
|
||||
|
|
|
@ -28,8 +28,4 @@
|
|||
.header {
|
||||
background-color: <%= knowledge_base.color_header %>;
|
||||
}
|
||||
|
||||
.header .menu-item {
|
||||
color: <%= knowledge_base.color_header_link %>;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,11 +5,11 @@ Rails.application.config.html_sanitizer_tags_remove_content = %w[
|
|||
style
|
||||
comment
|
||||
meta
|
||||
script
|
||||
]
|
||||
|
||||
# content of this tags will will be inserted html quoted
|
||||
Rails.application.config.html_sanitizer_tags_quote_content = %w[
|
||||
script
|
||||
]
|
||||
|
||||
# only this tags are allowed
|
||||
|
|
|
@ -21,7 +21,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
|
||||
# Rails thinks the singularized version of knowledge_bases is knowledge_basis?!
|
||||
# see: KnowledgeBase.table_name.singularize
|
||||
inflect.irregular 'base', 'bases'
|
||||
inflect.singular(%r{(knowledge_base)s$}i, '\1')
|
||||
inflect.acronym 'SMIME'
|
||||
inflect.acronym 'GitLab'
|
||||
inflect.acronym 'GitHub'
|
||||
|
|
|
@ -228,7 +228,7 @@ function create_webserver_config () {
|
|||
function setup_elasticsearch () {
|
||||
echo "# Configuring Elasticsearch..."
|
||||
|
||||
ES_CONNECTION="$(zammad run rails r "puts '',Setting.get('es_url')"| tail -n 1 2>> /dev/null)"
|
||||
ES_CONNECTION="$(zammad run rails r "puts Setting.get('es_url')"| tail -n 1 2>> /dev/null)"
|
||||
|
||||
if [ -z "${ES_CONNECTION}" ]; then
|
||||
echo "-- Nevermind, no es_url is set, leaving Elasticsearch untouched ...!"
|
||||
|
@ -274,10 +274,6 @@ function elasticsearch_searchindex_rebuild () {
|
|||
function update_or_install () {
|
||||
|
||||
if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then
|
||||
|
||||
echo "# Clear cache..."
|
||||
zammad run rails r Cache.clear
|
||||
|
||||
update_database
|
||||
|
||||
update_translations
|
||||
|
|
|
@ -8,9 +8,8 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
|||
create_table :knowledge_bases do |t|
|
||||
t.string :iconset, limit: 30, null: false
|
||||
|
||||
t.string :color_highlight, limit: 25, null: false
|
||||
t.string :color_header, limit: 25, null: false
|
||||
t.string :color_header_link, limit: 25, null: false
|
||||
t.string :color_highlight, limit: 25, null: false
|
||||
t.string :color_header, limit: 25, null: false
|
||||
|
||||
t.string :homepage_layout, null: false
|
||||
t.string :category_layout, null: false
|
||||
|
|
|
@ -75,8 +75,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
|
|||
|
||||
def fix_pending_time
|
||||
pending_time = ObjectManager::Attribute.find_by(name: 'pending_time', object_lookup: ObjectLookup.find_by(name: 'Ticket'))
|
||||
return if pending_time.blank?
|
||||
|
||||
pending_time.data_option.delete('required_if')
|
||||
pending_time.data_option.delete('shown_if')
|
||||
pending_time.save
|
||||
|
@ -85,8 +83,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
|
|||
def fix_organization_screens
|
||||
%w[domain note].each do |name|
|
||||
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
|
||||
next if field.blank?
|
||||
|
||||
field.screens['create'] ||= {}
|
||||
field.screens['create']['-all-'] ||= {}
|
||||
field.screens['create']['-all-']['null'] = true
|
||||
|
@ -97,8 +93,6 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2]
|
|||
def fix_user_screens
|
||||
%w[email web phone mobile organization_id fax department street zip city country address password vip note role_ids].each do |name|
|
||||
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
|
||||
next if field.blank?
|
||||
|
||||
field.screens['create'] ||= {}
|
||||
field.screens['create']['-all-'] ||= {}
|
||||
field.screens['create']['-all-']['null'] = true
|
||||
|
|
|
@ -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
|
|
@ -11,8 +11,6 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0]
|
|||
def fix_organization_screens_create
|
||||
%w[name shared domain_assignment active].each do |name|
|
||||
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization'))
|
||||
next if field.blank?
|
||||
|
||||
field.screens['create'] ||= {}
|
||||
field.screens['create']['-all-'] ||= {}
|
||||
field.screens['create']['-all-']['null'] = false
|
||||
|
@ -23,8 +21,6 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0]
|
|||
def fix_user_screens_create
|
||||
%w[firstname lastname active].each do |name|
|
||||
field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User'))
|
||||
next if field.blank?
|
||||
|
||||
field.screens['create'] ||= {}
|
||||
field.screens['create']['-all-'] ||= {}
|
||||
field.screens['create']['-all-']['null'] = false
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -9,7 +9,6 @@ Setting.create_if_not_exists(
|
|||
state: SecureRandom.hex(128),
|
||||
preferences: {
|
||||
permission: ['admin'],
|
||||
protected: true,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'faker'
|
||||
|
||||
# rubocop:disable Rails/Output
|
||||
module FillDb
|
||||
|
||||
|
@ -53,7 +55,7 @@ or if you only want to create 100 tickets
|
|||
else
|
||||
(1..organizations).each do
|
||||
ActiveRecord::Base.transaction do
|
||||
organization = Organization.create!(name: "FillOrganization::#{counter}", active: true)
|
||||
organization = Organization.create!(name: "FillOrganization::#{Faker::Number.number(digits: 6)}", active: true)
|
||||
organization_pool.push organization
|
||||
end
|
||||
end
|
||||
|
@ -70,7 +72,7 @@ or if you only want to create 100 tickets
|
|||
|
||||
(1..agents).each do
|
||||
ActiveRecord::Base.transaction do
|
||||
suffix = counter.to_s
|
||||
suffix = Faker::Number.number(digits: 5).to_s
|
||||
user = User.create_or_update(
|
||||
login: "filldb-agent-#{suffix}",
|
||||
firstname: "agent #{suffix}",
|
||||
|
@ -100,7 +102,7 @@ or if you only want to create 100 tickets
|
|||
|
||||
(1..customers).each do
|
||||
ActiveRecord::Base.transaction do
|
||||
suffix = counter.to_s
|
||||
suffix = Faker::Number.number(digits: 5).to_s
|
||||
organization = nil
|
||||
if organization_pool.present? && true_or_false.sample
|
||||
organization = organization_pool.sample
|
||||
|
@ -130,7 +132,7 @@ or if you only want to create 100 tickets
|
|||
else
|
||||
(1..groups).each do
|
||||
ActiveRecord::Base.transaction do
|
||||
group = Group.create!(name: "FillGroup::#{counter}", active: true)
|
||||
group = Group.create!(name: "FillGroup::#{Faker::Number.number(digits: 6)}", active: true)
|
||||
group_pool.push group
|
||||
Role.where(name: 'Agent').first.users.where(active: true).each do |user|
|
||||
user_groups = user.groups
|
||||
|
@ -148,7 +150,7 @@ or if you only want to create 100 tickets
|
|||
(1..overviews).each do
|
||||
ActiveRecord::Base.transaction do
|
||||
Overview.create!(
|
||||
name: "Filloverview::#{counter}",
|
||||
name: "Filloverview::#{Faker::Number.number(digits: 6)}",
|
||||
role_ids: [Role.find_by(name: 'Agent').id],
|
||||
condition: {
|
||||
'ticket.state_id' => {
|
||||
|
@ -183,7 +185,7 @@ or if you only want to create 100 tickets
|
|||
customer = customer_pool.sample
|
||||
agent = agent_pool.sample
|
||||
ticket = Ticket.create!(
|
||||
title: "some title äöüß#{counter}",
|
||||
title: "some title äöüß#{Faker::Number.number(digits: 6)}",
|
||||
group: group_pool.sample,
|
||||
customer: customer,
|
||||
owner: agent,
|
||||
|
@ -198,8 +200,8 @@ or if you only want to create 100 tickets
|
|||
ticket_id: ticket.id,
|
||||
from: customer.email,
|
||||
to: 'some_recipient@example.com',
|
||||
subject: "some subject#{counter}",
|
||||
message_id: "some@id-#{counter}",
|
||||
subject: "some subject#{Faker::Number.number(digits: 6)}",
|
||||
message_id: "some@id-#{Faker::Number.number(digits: 6)}",
|
||||
body: 'some message ...',
|
||||
internal: false,
|
||||
sender: Ticket::Article::Sender.where(name: 'Customer').first,
|
||||
|
@ -212,10 +214,5 @@ or if you only want to create 100 tickets
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.counter
|
||||
@counter ||= SecureRandom.random_number(1_000_000)
|
||||
@counter += 1
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/Output
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'uri'
|
||||
|
||||
class GitHub
|
||||
class HttpClient
|
||||
attr_reader :api_token, :endpoint
|
||||
|
||||
def initialize(endpoint, api_token)
|
||||
raise 'api_token required' if api_token.blank?
|
||||
raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank?
|
||||
raise 'endpoint required' if endpoint.blank?
|
||||
|
||||
@api_token = api_token
|
||||
@endpoint = endpoint
|
||||
|
@ -32,7 +30,7 @@ class GitHub
|
|||
|
||||
if !response.success?
|
||||
Rails.logger.error response.error
|
||||
raise 'GitHub request failed! Please have a look at the log file for details'
|
||||
raise "Error while requesting GitHub GraphQL API: #{response.error}"
|
||||
end
|
||||
|
||||
response.data
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'uri'
|
||||
|
||||
class GitLab
|
||||
class HttpClient
|
||||
attr_reader :api_token, :endpoint
|
||||
|
||||
def initialize(endpoint, api_token)
|
||||
raise 'api_token required' if api_token.blank?
|
||||
raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank?
|
||||
raise 'endpoint required' if endpoint.blank?
|
||||
|
||||
@api_token = api_token
|
||||
@endpoint = endpoint
|
||||
|
@ -32,7 +30,7 @@ class GitLab
|
|||
|
||||
if !response.success?
|
||||
Rails.logger.error response.error
|
||||
raise 'GitLab request failed! Please have a look at the log file for details'
|
||||
raise "Error while requesting GitLab GraphQL API: #{response.error}"
|
||||
end
|
||||
|
||||
response.data
|
||||
|
|
|
@ -161,8 +161,8 @@ satinize html string based on whiltelist
|
|||
# wrap plain-text URLs in <a> tags
|
||||
if node.is_a?(Nokogiri::XML::Text) && node.content.present? && node.content.include?(':') && node.ancestors.map(&:name).exclude?('a')
|
||||
urls = URI.extract(node.content, LINKABLE_URL_SCHEMES)
|
||||
.map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas
|
||||
.grep_v(%r{^[^:]+:$}) # URI::extract will match, e.g., 'tel:'
|
||||
.map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas
|
||||
.reject { |u| u.match?(%r{^[^:]+:$}) } # URI::extract will match, e.g., 'tel:'
|
||||
|
||||
next if urls.blank?
|
||||
|
||||
|
|
|
@ -324,8 +324,7 @@ returns
|
|||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: template[:subject],
|
||||
escape: false,
|
||||
trusted: true,
|
||||
escape: false
|
||||
).render
|
||||
|
||||
# strip off the extra newline at the end of the subject to avoid =0A suffixes (see #2726)
|
||||
|
@ -335,8 +334,7 @@ returns
|
|||
objects: data[:objects],
|
||||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: template[:body],
|
||||
trusted: true,
|
||||
template: template[:body]
|
||||
).render
|
||||
|
||||
if !data[:raw]
|
||||
|
@ -350,8 +348,7 @@ returns
|
|||
objects: data[:objects],
|
||||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: application_template,
|
||||
trusted: true,
|
||||
template: application_template
|
||||
).render
|
||||
end
|
||||
{
|
||||
|
|
|
@ -13,8 +13,7 @@ examples how to use
|
|||
locale: 'de-de',
|
||||
timezone: 'America/Port-au-Prince',
|
||||
template: 'some template <b>#{ticket.title}</b> {config.fqdn}',
|
||||
escape: false,
|
||||
trusted: false, # Allow ERB tags in the template?
|
||||
escape: false
|
||||
).render
|
||||
|
||||
message_body = NotificationFactory::Renderer.new(
|
||||
|
@ -28,20 +27,16 @@ examples how to use
|
|||
|
||||
=end
|
||||
|
||||
def initialize(objects:, template:, locale: nil, timezone: nil, escape: true, trusted: false) # rubocop:disable Metrics/ParameterLists
|
||||
def initialize(objects:, template:, locale: nil, timezone: nil, escape: true)
|
||||
@objects = objects
|
||||
@locale = locale || Locale.default
|
||||
@timezone = timezone || Setting.get('timezone_default')
|
||||
@template = NotificationFactory::Template.new(template, escape, trusted)
|
||||
@template = NotificationFactory::Template.new(template, escape)
|
||||
@escape = escape
|
||||
end
|
||||
|
||||
def render
|
||||
ERB.new(@template.to_s).result(binding)
|
||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||
raise StandardError, e.message if e.is_a? SyntaxError
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
# d - data of object
|
||||
|
|
|
@ -46,16 +46,14 @@ returns
|
|||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: template[:subject],
|
||||
escape: false,
|
||||
trusted: true
|
||||
escape: false
|
||||
).render
|
||||
message_body = NotificationFactory::Renderer.new(
|
||||
objects: data[:objects],
|
||||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: template[:body],
|
||||
escape: false,
|
||||
trusted: true
|
||||
escape: false
|
||||
).render
|
||||
|
||||
if !data[:raw]
|
||||
|
@ -70,8 +68,7 @@ returns
|
|||
locale: data[:locale],
|
||||
timezone: data[:timezone],
|
||||
template: application_template,
|
||||
escape: false,
|
||||
trusted: true
|
||||
escape: false
|
||||
).render
|
||||
end
|
||||
{
|
||||
|
|
|
@ -9,21 +9,17 @@ examples how to use
|
|||
cleaned_template = NotificationFactory::Template.new(
|
||||
'some template <b>#{ticket.title}</b> #{config.fqdn}',
|
||||
true,
|
||||
false, # Allow ERB tags in the template?
|
||||
).to_s
|
||||
|
||||
=end
|
||||
|
||||
def initialize(template, escape, trusted)
|
||||
def initialize(template, escape)
|
||||
@template = template
|
||||
@escape = escape
|
||||
@trusted = trusted
|
||||
@escape = escape
|
||||
end
|
||||
|
||||
def to_s
|
||||
result = @template
|
||||
result.gsub!(%r{<%(?!%)}, '<%%') if !@trusted
|
||||
result.gsub(%r{\#{\s*(.*?)\s*}}m) do
|
||||
@template.gsub(%r{\#{\s*(.*?)\s*}}m) do
|
||||
# some browsers start adding HTML tags
|
||||
# fixes https://github.com/zammad/zammad/issues/385
|
||||
input_template = $1.gsub(%r{\A<.+?>\s*|\s*<.+?>\z}, '')
|
||||
|
|
|
@ -4,7 +4,7 @@ module SessionHelper
|
|||
def self.json_hash(user)
|
||||
collections, assets = default_collections(user)
|
||||
{
|
||||
session: user.filter_unauthorized_attributes(user.filter_attributes(user.attributes)),
|
||||
session: user.filter_attributes(user.attributes),
|
||||
models: models(user),
|
||||
collections: collections,
|
||||
assets: assets,
|
||||
|
|
|
@ -7,11 +7,6 @@ module UserInfo
|
|||
|
||||
def self.current_user_id=(user_id)
|
||||
Thread.current[:user_id] = user_id
|
||||
Thread.current[:assets] = UserInfo::Assets.new(user_id)
|
||||
end
|
||||
|
||||
def self.assets
|
||||
Thread.current[:assets]
|
||||
end
|
||||
|
||||
def self.ensure_current_user_id
|
||||
|
|
|
@ -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
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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
|
|
@ -762,11 +762,7 @@ do(window) ->
|
|||
console.log('p', docType, text)
|
||||
if docType is 'html'
|
||||
html = document.createElement('div')
|
||||
# can't log because might contain malicious content
|
||||
# @log.debug 'HTML clipboard', text
|
||||
sanitized = DOMPurify.sanitize(text)
|
||||
@log.debug 'sanitized HTML clipboard', sanitized
|
||||
html.innerHTML = sanitized
|
||||
html.innerHTML = text
|
||||
match = false
|
||||
htmlTmp = text
|
||||
regex = new RegExp('<(/w|w)\:[A-Za-z]')
|
||||
|
|
File diff suppressed because one or more lines are too long
2
public/assets/chat/chat-no-jquery.min.js
vendored
2
public/assets/chat/chat-no-jquery.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -718,9 +718,7 @@ do($ = window.jQuery, window) ->
|
|||
text = text.replace(/<div><\/div>/g, '<div><br></div>')
|
||||
console.log('p', docType, text)
|
||||
if docType is 'html'
|
||||
sanitized = DOMPurify.sanitize(text)
|
||||
@log.debug 'sanitized HTML clipboard', sanitized
|
||||
html = $("<div>#{sanitized}</div>")
|
||||
html = $("<div>#{text}</div>")
|
||||
match = false
|
||||
htmlTmp = text
|
||||
regex = new RegExp('<(/w|w)\:[A-Za-z]')
|
||||
|
|
|
@ -314,7 +314,6 @@
|
|||
line-height: 1.4em;
|
||||
font-size: inherit;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
@ -330,7 +329,6 @@
|
|||
|
||||
.zammad-chat-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
@ -351,7 +349,6 @@
|
|||
|
||||
.zammad-chat-button:disabled,
|
||||
.zammad-chat-input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3; }
|
||||
|
||||
.zammad-chat-is-hidden {
|
||||
|
|
File diff suppressed because one or more lines are too long
2
public/assets/chat/chat.min.js
vendored
2
public/assets/chat/chat.min.js
vendored
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
Loading…
Reference in a new issue