Compare commits
86 commits
ce1527f259
...
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 | ||
|
1bf468a262 | ||
|
5bd714878a | ||
|
79bacb14aa | ||
|
9c5a11f36e | ||
|
489177cca1 | ||
|
dbb93bf02c | ||
|
4f3e7f4003 | ||
|
51933e3b76 | ||
|
4e4ba091d4 | ||
|
c335147d62 | ||
|
36aa35f765 | ||
|
6d8f5b7d95 | ||
|
bf6da241d8 | ||
|
1404f7b2fc | ||
|
68adb3974e | ||
|
0cc5764ab3 | ||
|
8d37feceb6 | ||
|
5f2181d8a3 | ||
|
b85e402807 | ||
|
b17f1be9a0 | ||
|
8d77ac4579 | ||
|
98065e1df8 | ||
|
63335652cb | ||
|
1b3530f2ae | ||
|
b82e8544f0 | ||
|
24830cf814 | ||
|
4f2e7b9e52 | ||
|
59f1824c3a | ||
|
e88fec5bb3 | ||
|
39179c83f8 | ||
|
c099698c71 | ||
|
1fe7611bab | ||
|
0b659776e9 |
187 changed files with 4674 additions and 3917 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:**
|
||||||
|
|
||||||
|
|
11
Gemfile
11
Gemfile
|
@ -1,10 +1,10 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://gems.sutty.nl'
|
||||||
|
|
||||||
# core - base
|
# core - base
|
||||||
ruby '2.7.4'
|
ruby '2.7.3'
|
||||||
gem 'rails', '~> 6.0'
|
gem 'rails', '~> 6.0.0'
|
||||||
|
|
||||||
# core - rails additions
|
# core - rails additions
|
||||||
gem 'activerecord-import'
|
gem 'activerecord-import'
|
||||||
|
@ -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'
|
||||||
|
@ -227,10 +228,6 @@ group :development, :test do
|
||||||
|
|
||||||
# Slack helper for testing
|
# Slack helper for testing
|
||||||
gem 'slack-ruby-client', require: false
|
gem 'slack-ruby-client', require: false
|
||||||
|
|
||||||
# Can be used to detect for example the current
|
|
||||||
# operating system in tests, to handle things differently.
|
|
||||||
gem 'os'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Want to extend Zammad with additional gems?
|
# Want to extend Zammad with additional gems?
|
||||||
|
|
114
Gemfile.lock
114
Gemfile.lock
|
@ -17,7 +17,7 @@ GIT
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://gems.sutty.nl/
|
||||||
specs:
|
specs:
|
||||||
aasm (5.2.0)
|
aasm (5.2.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
@ -90,28 +90,31 @@ 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-compiler (~> 1.0)
|
||||||
|
argon2 (2.0.3-x86_64-linux-musl)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
async (1.29.1)
|
async (1.30.1)
|
||||||
console (~> 1.10)
|
console (~> 1.10)
|
||||||
nio4r (~> 2.3)
|
nio4r (~> 2.3)
|
||||||
timers (~> 4.1)
|
timers (~> 4.1)
|
||||||
async-http (0.56.3)
|
async-http (0.56.5)
|
||||||
async (~> 1.25)
|
async (>= 1.25)
|
||||||
async-io (~> 1.28)
|
async-io (>= 1.28)
|
||||||
async-pool (~> 0.2)
|
async-pool (>= 0.2)
|
||||||
protocol-http (~> 0.22.0)
|
protocol-http (~> 0.22.0)
|
||||||
protocol-http1 (~> 0.14.0)
|
protocol-http1 (~> 0.14.0)
|
||||||
protocol-http2 (~> 0.14.0)
|
protocol-http2 (~> 0.14.0)
|
||||||
async-http-faraday (0.11.0)
|
async-http-faraday (0.11.0)
|
||||||
async-http (~> 0.42)
|
async-http (~> 0.42)
|
||||||
faraday
|
faraday
|
||||||
async-io (1.32.1)
|
async-io (1.32.2)
|
||||||
async (~> 1.14)
|
async
|
||||||
async-pool (0.3.7)
|
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)
|
||||||
binding_of_caller (1.0.0)
|
binding_of_caller (1.0.0)
|
||||||
|
@ -121,11 +124,14 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
bootsnap (1.9.1)
|
bootsnap (1.9.1)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
|
bootsnap (1.9.1-x86_64-linux-musl)
|
||||||
|
msgpack (~> 1.0)
|
||||||
brakeman (5.1.1)
|
brakeman (5.1.1)
|
||||||
browser (5.3.1)
|
browser (5.3.1)
|
||||||
buftok (0.2.0)
|
buftok (0.2.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
|
byebug (11.1.3-x86_64-linux-musl)
|
||||||
capybara (3.35.3)
|
capybara (3.35.3)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
|
@ -169,6 +175,7 @@ GEM
|
||||||
daemons (1.4.1)
|
daemons (1.4.1)
|
||||||
dalli (2.7.11)
|
dalli (2.7.11)
|
||||||
debug_inspector (1.1.0)
|
debug_inspector (1.1.0)
|
||||||
|
debug_inspector (1.1.0-x86_64-linux-musl)
|
||||||
delayed_job (4.1.9)
|
delayed_job (4.1.9)
|
||||||
activesupport (>= 3.0, < 6.2)
|
activesupport (>= 3.0, < 6.2)
|
||||||
delayed_job_active_record (4.1.6)
|
delayed_job_active_record (4.1.6)
|
||||||
|
@ -181,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.2)
|
doorkeeper (5.5.4)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
eco (1.0.0)
|
eco (1.0.0)
|
||||||
|
@ -195,6 +202,7 @@ GEM
|
||||||
equalizer (0.0.11)
|
equalizer (0.0.11)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
eventmachine (1.2.7)
|
eventmachine (1.2.7)
|
||||||
|
eventmachine (1.2.7-x86_64-linux-musl)
|
||||||
execjs (2.8.1)
|
execjs (2.8.1)
|
||||||
factory_bot (6.2.0)
|
factory_bot (6.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
|
@ -224,9 +232,9 @@ GEM
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday_middleware (1.0.0)
|
faraday_middleware (1.1.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
ffi (1.15.3)
|
ffi (1.15.4-x86_64-linux-musl)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
|
@ -267,6 +275,7 @@ GEM
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
|
hiredis (0.6.3-x86_64-linux-musl)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (4.4.1)
|
http (4.4.1)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
|
@ -277,9 +286,10 @@ GEM
|
||||||
http-cookie (1.0.4)
|
http-cookie (1.0.4)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
http-parser (1.2.3)
|
http-parser (1.2.3-x86_64-linux-musl)
|
||||||
ffi-compiler (>= 1.0, < 2.0)
|
ffi-compiler (>= 1.0, < 2.0)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
|
http_parser.rb (0.6.0-x86_64-linux-musl)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.8.10)
|
i18n (1.8.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
@ -292,15 +302,17 @@ 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)
|
||||||
jwt (2.2.3)
|
jwt (2.3.0)
|
||||||
kgio (2.11.4)
|
kgio (2.11.4)
|
||||||
|
kgio (2.11.4-x86_64-linux-musl)
|
||||||
koala (3.0.0)
|
koala (3.0.0)
|
||||||
addressable
|
addressable
|
||||||
faraday
|
faraday
|
||||||
json (>= 1.8)
|
json (>= 1.8)
|
||||||
libv8 (8.4.255.0)
|
libv8 (8.4.255.0)
|
||||||
listen (3.5.1)
|
libv8 (8.4.255.0-x86_64-linux)
|
||||||
|
listen (3.7.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
little-plugger (1.1.4)
|
little-plugger (1.1.4)
|
||||||
|
@ -311,31 +323,37 @@ GEM
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
lumberjack (1.2.8)
|
lumberjack (1.2.8)
|
||||||
marcel (1.0.1)
|
marcel (1.0.2)
|
||||||
memoizable (0.4.2)
|
memoizable (0.4.2)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
messagebird-rest (3.0.0)
|
messagebird-rest (3.0.0)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2021.0225)
|
mime-types-data (3.2021.0901)
|
||||||
mini_mime (1.1.1)
|
mini_mime (1.1.1)
|
||||||
mini_portile2 (2.6.1)
|
mini_portile2 (2.6.1)
|
||||||
mini_racer (0.2.9)
|
mini_racer (0.2.9)
|
||||||
libv8 (>= 6.9.411)
|
libv8 (>= 6.9.411)
|
||||||
minitest (5.14.4)
|
minitest (5.14.4)
|
||||||
msgpack (1.4.2)
|
msgpack (1.4.2)
|
||||||
|
msgpack (1.4.2-x86_64-linux-musl)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
mysql2 (0.5.3)
|
mysql2 (0.5.3)
|
||||||
|
mysql2 (0.5.3-x86_64-linux-musl)
|
||||||
naught (1.1.0)
|
naught (1.1.0)
|
||||||
nenv (0.3.0)
|
nenv (0.3.0)
|
||||||
nestful (1.1.4)
|
nestful (1.1.4)
|
||||||
net-ldap (0.17.0)
|
net-ldap (0.17.0)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.12.4)
|
nio4r (2.5.8-x86_64-linux-musl)
|
||||||
|
nokogiri (1.12.5)
|
||||||
|
mini_portile2 (~> 2.6.1)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.12.5-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.6.1)
|
mini_portile2 (~> 2.6.1)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nori (2.6.0)
|
nori (2.6.0)
|
||||||
|
@ -392,7 +410,7 @@ GEM
|
||||||
omniauth (~> 1.5)
|
omniauth (~> 1.5)
|
||||||
omniauth-oauth2 (>= 1.4.0)
|
omniauth-oauth2 (>= 1.4.0)
|
||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
os (1.1.1)
|
openssl (2.2.0-x86_64-linux-musl)
|
||||||
overcommit (0.58.0)
|
overcommit (0.58.0)
|
||||||
childprocess (>= 0.6.3, < 5)
|
childprocess (>= 0.6.3, < 5)
|
||||||
iniparse (~> 1.4)
|
iniparse (~> 1.4)
|
||||||
|
@ -401,10 +419,11 @@ GEM
|
||||||
parser (3.0.2.0)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pg (0.21.0)
|
pg (0.21.0)
|
||||||
|
pg (0.21.0-x86_64-linux-musl)
|
||||||
power_assert (2.0.1)
|
power_assert (2.0.1)
|
||||||
protocol-hpack (1.4.2)
|
protocol-hpack (1.4.2)
|
||||||
protocol-http (0.22.4)
|
protocol-http (0.22.5)
|
||||||
protocol-http1 (0.14.1)
|
protocol-http1 (0.14.2)
|
||||||
protocol-http (~> 0.22)
|
protocol-http (~> 0.22)
|
||||||
protocol-http2 (0.14.2)
|
protocol-http2 (0.14.2)
|
||||||
protocol-hpack (~> 1.4)
|
protocol-hpack (~> 1.4)
|
||||||
|
@ -424,13 +443,16 @@ 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)
|
||||||
|
puma (4.3.8-x86_64-linux-musl)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.1)
|
pundit (2.1.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
pundit-matchers (1.7.0)
|
pundit-matchers (1.7.0)
|
||||||
rspec-rails (>= 3.0.0)
|
rspec-rails (>= 3.0.0)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
|
racc (1.5.2-x86_64-linux-musl)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-livereload (0.3.17)
|
rack-livereload (0.3.17)
|
||||||
rack
|
rack
|
||||||
|
@ -458,7 +480,7 @@ GEM
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.1)
|
rails-html-sanitizer (1.4.2)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.4.1)
|
railties (6.0.4.1)
|
||||||
actionpack (= 6.0.4.1)
|
actionpack (= 6.0.4.1)
|
||||||
|
@ -468,6 +490,7 @@ GEM
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (>= 0.20.3, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
raindrops (0.19.2)
|
raindrops (0.19.2)
|
||||||
|
raindrops (0.19.2-x86_64-linux-musl)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rb-fsevent (0.11.0)
|
rb-fsevent (0.11.0)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
|
@ -498,39 +521,45 @@ 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)
|
||||||
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.11.0)
|
rubocop-ast (1.12.0)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.0.1.1)
|
||||||
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)
|
||||||
rubocop-rspec (2.5.0)
|
rubocop-rspec (2.5.0)
|
||||||
rubocop (~> 1.19)
|
rubocop (~> 1.19)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-saml (1.12.2)
|
ruby-saml (1.13.0)
|
||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyntlm (0.6.3)
|
rubyntlm (0.6.3)
|
||||||
rubyzip (2.3.0)
|
rubyzip (2.3.2)
|
||||||
sassc (2.4.0)
|
sassc (2.4.0)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
|
sassc (2.4.0-x86_64-linux-musl)
|
||||||
|
ffi (~> 1.9)
|
||||||
sassc-rails (2.1.2)
|
sassc-rails (2.1.2)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
sassc (>= 2.0)
|
sassc (>= 2.0)
|
||||||
|
@ -590,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)
|
||||||
|
@ -611,11 +640,15 @@ GEM
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.8)
|
||||||
|
unf_ext (0.0.8-x86_64-linux-musl)
|
||||||
unicode-display_width (2.1.0)
|
unicode-display_width (2.1.0)
|
||||||
unicorn (6.0.0)
|
unicorn (6.0.0)
|
||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
|
unicorn (6.0.0-x86_64-linux-musl)
|
||||||
|
kgio (~> 2.6)
|
||||||
|
raindrops (~> 0.7)
|
||||||
valid_email2 (4.0.0)
|
valid_email2 (4.0.0)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
|
@ -631,6 +664,8 @@ GEM
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
websocket-driver (0.7.5)
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
|
websocket-driver (0.7.5-x86_64-linux-musl)
|
||||||
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
writeexcel (1.0.5)
|
writeexcel (1.0.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
|
@ -645,6 +680,7 @@ GEM
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
aasm
|
aasm
|
||||||
|
@ -711,7 +747,6 @@ DEPENDENCIES
|
||||||
omniauth-twitter
|
omniauth-twitter
|
||||||
omniauth-weibo-oauth2
|
omniauth-weibo-oauth2
|
||||||
openssl
|
openssl
|
||||||
os
|
|
||||||
overcommit
|
overcommit
|
||||||
pg (= 0.21.0)
|
pg (= 0.21.0)
|
||||||
pry-rails
|
pry-rails
|
||||||
|
@ -722,7 +757,7 @@ DEPENDENCIES
|
||||||
pundit
|
pundit
|
||||||
pundit-matchers
|
pundit-matchers
|
||||||
rack-livereload
|
rack-livereload
|
||||||
rails (~> 6.0)
|
rails (~> 6.0.0)
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
rb-fsevent
|
rb-fsevent
|
||||||
rchardet (>= 1.8.0)
|
rchardet (>= 1.8.0)
|
||||||
|
@ -731,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
|
||||||
|
@ -761,7 +797,7 @@ DEPENDENCIES
|
||||||
zendesk_api
|
zendesk_api
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.7.4p191
|
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,6 +130,7 @@ 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
|
||||||
|
|
|
@ -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')
|
|
||||||
@attachmentUpload = item.find('.attachmentUpload')
|
|
||||||
@attachmentsHolder = item.find('.attachments')
|
|
||||||
@cancelContainer = item.find('.js-cancel')
|
|
||||||
|
|
||||||
u = => html5Upload.initialize(
|
|
||||||
uploadUrl: "#{App.Config.get('api_path')}/attachments"
|
uploadUrl: "#{App.Config.get('api_path')}/attachments"
|
||||||
dropContainer: item.closest('form').get(0)
|
dropContainer: item.closest('form')
|
||||||
cancelContainer: @cancelContainer
|
cancelContainer: item.find('.js-cancel')
|
||||||
inputField: item.find('input').get(0)
|
inputField: item.find('input')
|
||||||
maxSimultaneousUploads: 1,
|
|
||||||
key: 'File'
|
|
||||||
data:
|
data:
|
||||||
form_id: item.closest('form').find('[name=form_id]').val()
|
form_id: item.closest('form').find('[name=form_id]').val()
|
||||||
onFileAdded: (file) =>
|
|
||||||
|
|
||||||
file.on(
|
onFileStartCallback: ->
|
||||||
onStart: =>
|
|
||||||
@attachmentPlaceholder.addClass('hide')
|
|
||||||
@attachmentUpload.removeClass('hide')
|
|
||||||
@cancelContainer.removeClass('hide')
|
|
||||||
item.find('[contenteditable]').trigger('fileUploadStart')
|
item.find('[contenteditable]').trigger('fileUploadStart')
|
||||||
App.Log.debug 'UiElement.richtext', 'upload start'
|
|
||||||
|
|
||||||
onAborted: =>
|
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
|
||||||
@attachmentUpload.addClass('hide')
|
|
||||||
item.find('input').val('')
|
|
||||||
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
|
||||||
|
|
||||||
# Called after received response from the server
|
|
||||||
onCompleted: (response) =>
|
|
||||||
response = JSON.parse(response)
|
|
||||||
|
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
|
||||||
@attachmentUpload.addClass('hide')
|
|
||||||
|
|
||||||
# reset progress bar
|
|
||||||
@progressBar.width(parseInt(0) + '%')
|
|
||||||
@progressText.text('')
|
|
||||||
|
|
||||||
|
onFileCompletedCallback: (response) ->
|
||||||
renderFile(response.data)
|
renderFile(response.data)
|
||||||
item.find('input').val('')
|
item.find('input').val('')
|
||||||
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
||||||
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
|
|
||||||
|
|
||||||
# Called during upload progress, first parameter
|
onFileAbortedCallback: ->
|
||||||
# is decimal value from 0 to 100.
|
item.find('input').val('')
|
||||||
onProgress: (progress, fileSize, uploadedBytes) =>
|
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
||||||
@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)
|
|
||||||
|
|
||||||
|
attachmentPlaceholder: item.find('.attachmentPlaceholder')
|
||||||
|
attachmentUpload: item.find('.attachmentUpload')
|
||||||
|
progressBar: item.find('.attachmentUpload-progressBar')
|
||||||
|
progressText: item.find('.js-percentage')
|
||||||
)
|
)
|
||||||
)
|
|
||||||
App.Delay.set(u, 100, undefined, 'form_upload')
|
uploader.render()
|
||||||
|
, 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')
|
|
||||||
@attachmentUpload.removeClass('hide')
|
|
||||||
@cancelContainer.removeClass('hide')
|
|
||||||
|
|
||||||
if @callbackFileUploadStart
|
|
||||||
@callbackFileUploadStart()
|
|
||||||
|
|
||||||
onAborted: =>
|
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
|
||||||
@attachmentUpload.addClass('hide')
|
|
||||||
@$('.article-attachment input').val('')
|
|
||||||
|
|
||||||
if @callbackFileUploadStop
|
|
||||||
@callbackFileUploadStop()
|
|
||||||
|
|
||||||
# Called after received response from the server
|
|
||||||
onCompleted: (response) =>
|
|
||||||
|
|
||||||
response = JSON.parse(response)
|
|
||||||
@attachments.push response.data
|
@attachments.push response.data
|
||||||
|
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
|
||||||
@attachmentUpload.addClass('hide')
|
|
||||||
|
|
||||||
# reset progress bar
|
|
||||||
@progressBar.width(parseInt(0) + '%')
|
|
||||||
@progressText.text('')
|
|
||||||
|
|
||||||
@renderAttachment(response.data)
|
@renderAttachment(response.data)
|
||||||
@$('.article-attachment input').val('')
|
@$('.article-attachment input').val('')
|
||||||
|
|
||||||
if @callbackFileUploadStop
|
@callbackFileUploadStop?()
|
||||||
@callbackFileUploadStop()
|
|
||||||
|
|
||||||
# Called during upload progress, first parameter
|
onFileAbortedCallback: =>
|
||||||
# is decimal value from 0 to 100.
|
@callbackFileUploadStop?()
|
||||||
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
|
attachmentPlaceholder: @attachmentPlaceholder
|
||||||
onError: (message) =>
|
attachmentUpload: @attachmentUpload
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
progressBar: @progressBar
|
||||||
@attachmentUpload.addClass('hide')
|
progressText: @progressText
|
||||||
@$('.article-attachment input').val('')
|
).render()
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -37,11 +37,11 @@ class App.Browser
|
||||||
|
|
||||||
# define min. required browser version
|
# define min. required browser version
|
||||||
map =
|
map =
|
||||||
Chrome: 37
|
Firefox: 78 # ESR
|
||||||
Firefox: 36
|
Chrome: 83 # parallel to FF ESR - Edge is also detected as Chrome
|
||||||
Explorer: 10
|
Opera: 69 # based on Chrome 83
|
||||||
Safari: 6
|
Explorer: 11 # 10 is EOL
|
||||||
Opera: 22
|
Safari: 11 # released 2018
|
||||||
|
|
||||||
# disable id older
|
# disable id older
|
||||||
if data.browser
|
if data.browser
|
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: %>
|
||||||
|
<% if @is_allowed_to_create_ticket: %>
|
||||||
<p><%- @T('You have not created a ticket yet.') %></p>
|
<p><%- @T('You have not created a ticket yet.') %></p>
|
||||||
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
|
<p><%- @T('The way to communicate with us is this thing called "ticket".') %></p>
|
||||||
<p><%- @T('Please click the button below to create your first one.') %></p>
|
<p><%- @T('Please click the button below to create your first one.') %></p>
|
||||||
|
|
||||||
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
|
<p><a class="btn btn--primary" href="#customer_ticket_new"><%- @T('Create your first ticket') %></a></p>
|
||||||
|
<% else: %>
|
||||||
|
<p><%- @T('You currently don\'t have any tickets.') %></p>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -29,6 +29,7 @@ class App.KnowledgeBaseNewModal extends App.ControllerModal
|
||||||
params['iconset'] = 'FontAwesome'
|
params['iconset'] = 'FontAwesome'
|
||||||
params['color_highlight'] = '#38ae6a'
|
params['color_highlight'] = '#38ae6a'
|
||||||
params['color_header'] = '#f9fafb'
|
params['color_header'] = '#f9fafb'
|
||||||
|
params['color_header_link'] = 'hsl(206,8%,50%)'
|
||||||
params['homepage_layout'] = 'grid'
|
params['homepage_layout'] = 'grid'
|
||||||
params['category_layout'] = 'grid'
|
params['category_layout'] = 'grid'
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -4997,6 +4997,11 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--vacation {
|
||||||
|
filter: grayscale(70%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--idle {
|
&--idle {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -5007,11 +5012,6 @@ footer {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--vacation {
|
|
||||||
filter: grayscale(70%);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--unique {
|
&--unique {
|
||||||
background-image: image_url("/assets/images/avatar-bg.png");
|
background-image: image_url("/assets/images/avatar-bg.png");
|
||||||
background-size: 300px 226px;
|
background-size: 300px 226px;
|
||||||
|
@ -7051,7 +7051,7 @@ 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) {
|
||||||
|
@ -7060,6 +7060,16 @@ footer {
|
||||||
margin: 6px -12px 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 {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 1px 10px 1px 7px;
|
padding: 1px 10px 1px 7px;
|
||||||
|
@ -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,6 +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'])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
@ -67,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
|
|
@ -29,6 +29,7 @@ class KnowledgeBase < ApplicationModel
|
||||||
|
|
||||||
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,9 +696,12 @@ 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|
|
|
||||||
|
attribute_to_references_hash_objects
|
||||||
|
.map { |elem| elem.select(:name, :condition) }
|
||||||
|
.flatten
|
||||||
|
.each do |item|
|
||||||
item.condition.each do |condition_key, _condition_attributes|
|
item.condition.each do |condition_key, _condition_attributes|
|
||||||
attribute_list[condition_key] ||= {}
|
attribute_list[condition_key] ||= {}
|
||||||
attribute_list[condition_key][item.class.name] ||= []
|
attribute_list[condition_key][item.class.name] ||= []
|
||||||
|
@ -707,11 +710,22 @@ where attributes are used by triggers, overviews or schedulers
|
||||||
attribute_list[condition_key][item.class.name].push 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,6 +263,7 @@ subsequently in a separate step.
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Transaction.execute do
|
||||||
# store package
|
# store package
|
||||||
if !data[:reinstall]
|
if !data[:reinstall]
|
||||||
package_db = Package.create(meta)
|
package_db = Package.create(meta)
|
||||||
|
@ -278,6 +279,10 @@ subsequently in a separate step.
|
||||||
|
|
||||||
# write files
|
# write files
|
||||||
package['files'].each do |file|
|
package['files'].each do |file|
|
||||||
|
if !allowed_file_path?(file['location'])
|
||||||
|
raise "Can't create file, because of not allowed file location: #{file['location']}!"
|
||||||
|
end
|
||||||
|
|
||||||
permission = file['permission'] || '644'
|
permission = file['permission'] || '644'
|
||||||
content = Base64.decode64(file['content'])
|
content = Base64.decode64(file['content'])
|
||||||
_write_file(file['location'], permission, content)
|
_write_file(file['location'], permission, content)
|
||||||
|
@ -286,9 +291,9 @@ subsequently in a separate step.
|
||||||
# update package state
|
# update package state
|
||||||
package_db.state = 'installed'
|
package_db.state = 'installed'
|
||||||
package_db.save
|
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,6 +26,7 @@ class TicketPolicy < ApplicationPolicy
|
||||||
bind.push(user.group_ids_access(self.class::ACCESS_TYPE))
|
bind.push(user.group_ids_access(self.class::ACCESS_TYPE))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if user.permissions?('ticket.customer')
|
||||||
if user.organization&.shared
|
if user.organization&.shared
|
||||||
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
|
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
|
||||||
bind.push(user.id, user.organization.id)
|
bind.push(user.id, user.organization.id)
|
||||||
|
@ -33,6 +34,12 @@ class TicketPolicy < ApplicationPolicy
|
||||||
sql.push('tickets.customer_id = ?')
|
sql.push('tickets.customer_id = ?')
|
||||||
bind.push(user.id)
|
bind.push(user.id)
|
||||||
end
|
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
|
||||||
|
|
||||||
scope.where sql.join(' OR '), *bind
|
scope.where sql.join(' OR '), *bind
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -10,6 +10,7 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class RemoveOtrsDiffWorkerSchedulerEntry < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
Scheduler.find_by(method: 'Import::OTRS.diff_worker')&.destroy
|
||||||
|
end
|
||||||
|
end
|
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
|
|
@ -21,15 +21,6 @@ Scheduler.create_if_not_exists(
|
||||||
prio: 1,
|
prio: 1,
|
||||||
active: true,
|
active: true,
|
||||||
)
|
)
|
||||||
Scheduler.create_if_not_exists(
|
|
||||||
name: 'Import OTRS diff load',
|
|
||||||
method: 'Import::OTRS.diff_worker',
|
|
||||||
period: 3.minutes,
|
|
||||||
prio: 1,
|
|
||||||
active: true,
|
|
||||||
updated_by_id: 1,
|
|
||||||
created_by_id: 1,
|
|
||||||
)
|
|
||||||
Scheduler.create_if_not_exists(
|
Scheduler.create_if_not_exists(
|
||||||
name: 'Check Channels',
|
name: 'Check Channels',
|
||||||
method: 'Channel.fetch',
|
method: 'Channel.fetch',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -162,7 +162,7 @@ satinize html string based on whiltelist
|
||||||
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
|
|
@ -360,7 +360,7 @@ do(window) ->
|
||||||
'Send': 'Wyślij'
|
'Send': 'Wyślij'
|
||||||
'Chat closed by %s': 'Czat zamknięty przez %s'
|
'Chat closed by %s': 'Czat zamknięty przez %s'
|
||||||
'Compose your message...': 'Utwórz swoją wiadomość...'
|
'Compose your message...': 'Utwórz swoją wiadomość...'
|
||||||
'All colleagues are busy.': 'Wszyscy koledzy są zajęci.'
|
'All colleagues are busy.': 'Wszyscy konsultanci są zajęci.'
|
||||||
'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
|
'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
|
||||||
'Start new conversation': 'Rozpoczęcie nowej konwersacji'
|
'Start new conversation': 'Rozpoczęcie nowej konwersacji'
|
||||||
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
|
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
|
||||||
|
@ -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
|
@ -358,7 +358,7 @@ do($ = window.jQuery, window) ->
|
||||||
'Send': 'Wyślij'
|
'Send': 'Wyślij'
|
||||||
'Chat closed by %s': 'Czat zamknięty przez %s'
|
'Chat closed by %s': 'Czat zamknięty przez %s'
|
||||||
'Compose your message...': 'Utwórz swoją wiadomość...'
|
'Compose your message...': 'Utwórz swoją wiadomość...'
|
||||||
'All colleagues are busy.': 'Wszyscy koledzy są zajęci.'
|
'All colleagues are busy.': 'Wszyscy konsultanci są zajęci.'
|
||||||
'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
|
'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
|
||||||
'Start new conversation': 'Rozpoczęcie nowej konwersacji'
|
'Start new conversation': 'Rozpoczęcie nowej konwersacji'
|
||||||
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
|
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
|
||||||
|
@ -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]')
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue