Compare commits

..

66 commits

Author SHA1 Message Date
f
6c1a6b48a4 Merge branch 'develop' into antifascista 2021-10-13 12:39:26 -03:00
Dominik Klein
2bd4ceeedf Fixes #3733 - Simple quote characters (') not properly displayed. 2021-10-13 11:27:28 +00:00
Thorsten Eckel
c435fa4b23 Maintenance: Bumped Puma to 4.3.10 to resolve CVE-2021-41136. 2021-10-13 08:44:52 +02:00
Thorsten Eckel
16870853e1 Follow up - d7cdb308dd - Maintenance: Remove wrongly re-added switch_to_user_test.rb in browser slicing. 2021-10-12 17:18:07 +02:00
Martin Gruner
54417cd416 Fixes #3794 - Example payload in webhook view leads to 500 error 2021-10-12 14:34:42 +00:00
Bola Ahmed Buari
e6c3b07b0d Maintenance: Port old cti integration tests to capybara. 2021-10-12 16:16:59 +02:00
Bola Ahmed Buari
d7cdb308dd Maintenance: Port old preferences permission language tests to capybara. 2021-10-12 16:11:47 +02:00
Dominik Klein
3036453a0f Maintenance: Ported auth test to capybara. 2021-10-12 16:02:34 +02:00
Mantas Masalskis
55e99d7aa8 Fixes #3800 - Sort order group_by broken (alphabetical) 2021-10-12 15:46:46 +02:00
Dominik Klein
61996989b0 Maintenance: Deleted already ported switch to user test. 2021-10-11 10:58:01 +02:00
Dominik Klein
587f36d220 Maintenance: Try to improve the stabilzation of the maintenance app version selenium test. 2021-10-08 18:48:08 +02:00
Martin Gruner
52587ca9e2 Maintenance: Port System > Monitoring test to capybara. 2021-10-08 11:47:47 +00:00
Martin Gruner
1de9c8d803 Maintenance: Ported admin_permissions_granular_vs_full_test.rb to Capybara. 2021-10-08 11:42:56 +00:00
Dominik Klein
22cd8f3376 Maintenance: Port maintenance session message test to capybara. 2021-10-08 12:34:31 +02:00
Martin Edenhofer
df6fe54b66 Fixes #3797 - OS package upgrade fails (activity_stream_object_id) 2021-10-08 09:30:34 +02:00
Martin Gruner
da22f4b1bd Maintenance: Ported Manage > Channels > Email test to capybara. 2021-10-08 08:41:05 +02:00
Martin Gruner
445700d0aa Follow-up: 276c45b2e9 - Improved ticket policy scope handling. 2021-10-07 16:37:57 +02:00
Romit Choudhary
c5ba0563a5 Fixes #3737 - Bug Report 4.1.x Overview Sort - Grouped by user 2021-10-07 15:43:44 +02:00
Dominik Klein
4a2c8dd234 Maintenance: Try to improve the stabilzation of the ticket create selenium test. 2021-10-07 13:20:51 +02:00
Martin Gruner
73cb552846 Maintenance: Bump rubocop-rails from 2.12.2 to 2.12.3
Bumps [rubocop-rails](https://github.com/rubocop/rubocop-rails) from 2.12.2 to 2.12.3.
- [Release notes](https://github.com/rubocop/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-rails/compare/v2.12.2...v2.12.3)
2021-10-07 08:12:42 +00:00
Rolf Schmidt
a7e0d460aa Fixes #3789 - Article box opening on tickets with no changes. 2021-10-07 09:01:07 +02:00
Martin Gruner
9f9ec58230 Maintenance: Bump twilio-ruby from 5.58.3 to 5.59.0
Bumps [twilio-ruby](https://github.com/twilio/twilio-ruby) from 5.58.3 to 5.59.0.
- [Release notes](https://github.com/twilio/twilio-ruby/releases)
- [Changelog](https://github.com/twilio/twilio-ruby/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-ruby/compare/5.58.3...5.59.0)
2021-10-07 03:05:18 +02:00
Dominik Klein
3c2cea9a22 Fixes #3000 - Login site wording could be better. 2021-10-06 15:06:49 +02:00
Dominik Klein
c1cb4fdd43 Maintenance: Port maintenance app version test to capybara. 2021-10-06 08:35:18 +00:00
Martin Gruner
45cbbde2be Maintenance: Updated argon2 and doorkeeper gems. 2021-10-06 07:44:50 +02:00
Rolf Schmidt
3df384c758 Fixes #3787 - UploadCacheCleanupJob does not execute. 2021-10-05 17:18:31 +02:00
Martin Gruner
b33bca9910 Fixes: #3788 - lib/fill_db.rb fails to work in production environments. 2021-10-05 14:00:33 +02:00
Martin Edenhofer
9df0aaed7c Prepared 5.1. 2021-10-05 11:10:44 +02:00
Martin Gruner
de30a5c1b1 Maintenance: Improve template rendering. 2021-10-05 08:52:23 +02:00
Dominik Klein
7f178484c7 Maintenance: Refactoring of Avatar storage logic. 2021-10-05 06:42:26 +00:00
Rolf Schmidt
867b36baa8 Maintenance: Add assets level to have different data sets based on permissions 2021-10-05 06:42:26 +00:00
Thorsten Eckel
acc93a23fb Maintenance: Enhance attachment preview capabilities 2021-10-05 06:42:26 +00:00
Rolf Schmidt
7dbd1c1b15 Maintenance: Remove while loop user check login. 2021-10-05 06:42:26 +00:00
Dominik Klein
57b9ac91f3 Maintenance: Improve package installation. 2021-10-05 06:42:26 +00:00
Thorsten Eckel
f86576c1e4 Maintenance: Improve application boot time by reducing initial asset payload 2021-10-05 06:42:26 +00:00
Dominik Klein
1285c88ca3 Maintenance: Increase performance of ticket creation via form. 2021-10-05 06:42:26 +00:00
Thorsten Eckel
ecbda834bc Maintenance: Improve clipboard handling of website chat 2021-10-05 06:42:26 +00:00
Martin Gruner
0f5807d6fe Maintenance: Improved updating of user records in the front end. 2021-10-05 06:42:26 +00:00
Rolf Schmidt
6602d19dbf Maintenance: Enhanced GitHub and GitLab GraphQL endpoint check 2021-10-05 06:42:26 +00:00
Martin Gruner
5ddec48643 Maintenance: Improved updating of user records. 2021-10-05 06:42:26 +00:00
Mantas
4c72d5b9d9 Fixes #3769 - Usage of inactive object attributes in SLAs will crash admin SLA interface 2021-10-04 19:09:10 +00:00
Mantas Masalskis
90eca0f1eb Fixes #3773 - Inconstant alignment in the listing of attachments/submit button in new article area
Fixes #3774 - Broken dialog whiling uploading oversized attachment
2021-10-04 21:05:32 +02:00
Rolf Schmidt
ffa8814d02 Fixes #3779 - Core Workflow: Add organization condition attributes for object User. 2021-10-04 15:47:30 +02:00
Mantas Masalskis
32a23f9e8b Fixes #3783 - Improve contrasts in answer search for articles 2021-10-04 15:42:35 +02:00
Rolf Schmidt
798d45b299 Maintenance: Add another reload to setup new js for core workflow. 2021-10-04 13:24:50 +00:00
Martin Gruner
ca6e510ed4 Maintenance: Specify a certain PostgreSQL version for the build process that still works with older platforms. 2021-10-04 11:57:32 +02:00
Rolf Schmidt
dd66c30d59 Follow up 5f2181d8a3 - Fixes #3757 - escaped 'Set fixed' workflows don't refresh set values on active ticket sessions. 2021-10-04 10:50:14 +02:00
Martin Gruner
03bcf66126 Maintenance: Updated argon2, async-pool, jwt and rubocop gems. 2021-10-04 07:02:01 +02:00
Rolf Schmidt
5bd714878a Fixes #3781 - ObjectManager Attribute without screen attribute causes CoreWorkflows migration to fail 2021-10-01 12:25:06 +02:00
Rolf Schmidt
79bacb14aa Follow up 4e4ba091d4 - Fixes #3776 - Force users to reload after system migration. 2021-10-01 09:56:43 +00:00
Mantas Masalskis
9c5a11f36e Maintenance: Rubocop uses custom inflections 2021-10-01 11:56:09 +02:00
benrubson
489177cca1 Fixes #2674, closes #3775 - Zammad preflight check warning output causes Syntax-Error in postinstall.sh and failing installation. 2021-09-30 11:09:15 +02:00
Martin Gruner
dbb93bf02c Maintenance: Improve MR template. 2021-09-30 10:15:34 +02:00
Romit Choudhary
4f3e7f4003 Fixes #2780 - Shared Organisation issue create your first ticket 2021-09-30 09:25:13 +02:00
Martin Gruner
51933e3b76 Maintenance: Bump rubocop from 1.21.0 to 1.22.0 2021-09-30 09:18:16 +02:00
Rolf Schmidt
4e4ba091d4 Fixes #3776 - Force users to reload after system migration. 2021-09-30 06:31:13 +00:00
Dominik Klein
c335147d62 Maintenance: Port customer ticket create fields test to capybara. 2021-09-29 17:17:21 +02:00
Martin Gruner
36aa35f765 Maintenance: Port admin_calendar_sla_test.rb to Capybara. 2021-09-29 12:43:58 +00:00
Thorsten Eckel
6d8f5b7d95 Fixes #3777 - misspelled KnowledgeBase constant breaks update. 2021-09-29 12:16:16 +02:00
Romit Choudhary
bf6da241d8 Fixes #2351 - Unable to cancel attachment upload 2021-09-29 11:24:50 +02:00
Mantas
1404f7b2fc Fixes #2619 - KB header and footer link-color not changeable 2021-09-29 08:29:54 +00:00
Mantas
68adb3974e Fixes #3028 - Syntax errors break scheduler job for good 2021-09-29 08:19:39 +00:00
Martin Edenhofer
0cc5764ab3 Fixes #3365 - No script content (e. g. JavaScript) in emails 2021-09-29 10:13:40 +02:00
Mantas Masalskis
8d37feceb6 Fixes #3772 - Existing tickets: New article modal with padding-left: 0; padding-right: 0; 2021-09-29 10:09:08 +02:00
Rolf Schmidt
5f2181d8a3 Fixes #3757 - escaped 'Set fixed' workflows don't refresh set values on active ticket sessions. 2021-09-29 10:03:04 +02:00
Thorsten Eckel
b85e402807 Maintenance: Pluralize admin navigation entry name for Core Workflows. 2021-09-28 15:29:44 +02:00
169 changed files with 3600 additions and 2579 deletions

View file

@ -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!

View file

@ -116,6 +116,6 @@ env:
- ZAMMAD_RAILS_PORT=3000 - ZAMMAD_RAILS_PORT=3000
- ZAMMAD_WEBSOCKET_PORT=6042 - ZAMMAD_WEBSOCKET_PORT=6042
services: services:
- postgres - 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

View file

@ -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:

View file

@ -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'

View file

@ -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:**

View file

@ -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'

View file

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

View file

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

View file

@ -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

View file

@ -46,7 +46,7 @@ class App.UiElement.ApplicationUiElement
result = [] result = []
for row in selection for row in selection
if attribute.translate if attribute.translate
row.name = App.i18n.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,

View file

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

View file

@ -128,9 +128,10 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
@buildValueConfigMultiple: (config, meta) -> @buildValueConfigMultiple: (config, meta) ->
if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator) if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator)
config.multiple = true config.multiple = true
config.nulloption = true
else else
config.multiple = false config.multiple = false
config.nulloption = false config.nulloption = false
return config return config
@HasPreCondition: -> @HasPreCondition: ->

View file

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

View file

@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller
events: events:
'click .type-tabs .tab': 'changeFormType' 'click .type-tabs .tab': 'changeFormType'
'submit form': 'submit' 'submit form': 'submit'
'click .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))

View file

@ -1,6 +1,6 @@
class CoreWorkflow extends App.ControllerSubContent class CoreWorkflow extends App.ControllerSubContent
requiredPermission: 'admin.core_workflow' requiredPermission: 'admin.core_workflow'
header: 'Core 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')

View file

@ -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%)'
} }
] ]

View file

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

View file

@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller
formMeta = data.form_meta formMeta = data.form_meta
# on the following states we want to rerender the ticket: # on the following states we want to rerender the ticket:
# - if the object attribute configuration has changed (attribute values, 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: =>

View file

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

View file

@ -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

View file

@ -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

View 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')

View file

@ -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:

View file

@ -1,5 +1,5 @@
class App.KnowledgeBase extends App.Model class App.KnowledgeBase extends App.Model
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', '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'

View file

@ -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) ->

View file

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

View file

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

View file

@ -17,7 +17,7 @@
<%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ... <%- @T('Uploading') %> (<span class="js-percentage">0</span>%) ...
</div> </div>
<div class="attachmentUpload-cancel js-cancel"> <div class="attachmentUpload-cancel js-cancel">
<%- @Icon('diagonal-cross') %></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>

View file

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

View file

@ -14,7 +14,7 @@
<div class="kb-menu-preview"> <div class="kb-menu-preview">
<div class="label"><%= kb_locale.systemLocale().name %></div> <div class="label"><%= kb_locale.systemLocale().name %></div>
<div class="kb-menu-preview-container kb-menu-preview-container--<%= location.identifier %>" style="background-color: <%= location.color %>"> <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: %>

View file

@ -1,7 +1,7 @@
<div class="login fullscreen"> <div class="login fullscreen">
<div class="fullscreen-center"> <div class="fullscreen-center">
<div class="fullscreen-body"> <div class="fullscreen-body">
<p><%- @T('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>

View file

@ -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>

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -156,7 +156,7 @@ class FormController < ApplicationController
end end
def token_gen(fingerprint) def token_gen(fingerprint)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32]) 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!'

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -121,7 +121,7 @@ returns
key = "#{self.class}::aws::#{id}" key = "#{self.class}::aws::#{id}"
cache = Cache.read(key) cache = Cache.read(key)
return 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

View file

@ -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])

View file

@ -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)

View file

@ -502,7 +502,7 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
path = Rails.root.join('tmp/unprocessable_mail') path = Rails.root.join('tmp/unprocessable_mail')
files = [] files = []
Dir.glob("#{path}/*.eml") do |entry| Dir.glob("#{path}/*.eml") do |entry|
ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, 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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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])

View file

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

View file

@ -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

View file

@ -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>

View file

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

View file

@ -21,7 +21,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
# Rails thinks the singularized version of knowledge_bases is knowledge_basis?! # Rails thinks the singularized version of knowledge_bases is knowledge_basis?!
# see: KnowledgeBase.table_name.singularize # see: KnowledgeBase.table_name.singularize
inflect.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'

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View file

@ -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
) )

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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
{ {

View file

@ -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

View file

@ -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
{ {

View file

@ -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}, '')

View file

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

View file

@ -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
View 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

View 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"]

View 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
View 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

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -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]')

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
#!/bin/bash
cd "${GULP_DIR}" || exit
gulp js css no-jquery

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