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