From b85e402807f70eb8b240547f0cf064c6e32a9e2c Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 28 Sep 2021 15:28:57 +0200 Subject: [PATCH 01/65] Maintenance: Pluralize admin navigation entry name for Core Workflows. --- app/assets/javascripts/app/controllers/core_workflow.coffee | 4 ++-- spec/system/system/core_workflow_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/app/controllers/core_workflow.coffee b/app/assets/javascripts/app/controllers/core_workflow.coffee index a537a9077..a39a2a61b 100644 --- a/app/assets/javascripts/app/controllers/core_workflow.coffee +++ b/app/assets/javascripts/app/controllers/core_workflow.coffee @@ -1,6 +1,6 @@ class CoreWorkflow extends App.ControllerSubContent requiredPermission: 'admin.core_workflow' - header: 'Core Workflow' + header: 'Core Workflows' constructor: -> super @@ -54,4 +54,4 @@ class CoreWorkflow extends App.ControllerSubContent } return mapping[screen] || screen -App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core 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') diff --git a/spec/system/system/core_workflow_spec.rb b/spec/system/system/core_workflow_spec.rb index ccbf5f6c2..0530b0289 100644 --- a/spec/system/system/core_workflow_spec.rb +++ b/spec/system/system/core_workflow_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'System > Core Workflow', type: :system do +RSpec.describe 'System > Core Workflows', type: :system do before do ensure_websocket do visit 'system/core_workflow' From 5f2181d8a3e8a86acdc6ce0576343e23242c407d Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Wed, 29 Sep 2021 10:03:04 +0200 Subject: [PATCH 02/65] Fixes #3757 - escaped 'Set fixed' workflows don't refresh set values on active ticket sessions. --- .../_ui_element/core_workflow_perform.coffee | 3 +- .../form_handler_core_workflow.coffee | 4 +- .../ticket_zoom/sidebar_ticket.coffee | 29 ++++++----- app/models/core_workflow/attributes.rb | 27 +++++++++- app/models/core_workflow/result/backend.rb | 22 ++++++++ .../core_workflow/result/remove_option.rb | 8 ++- .../core_workflow/result/set_fixed_to.rb | 14 ++--- .../system/examples/core_workflow_examples.rb | 10 ++-- spec/system/ticket/zoom_spec.rb | 51 +++++++++++++++++++ 9 files changed, 139 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee index 4d0e3110c..b20780c62 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee @@ -128,9 +128,10 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec @buildValueConfigMultiple: (config, meta) -> if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator) config.multiple = true + config.nulloption = true else config.multiple = false - config.nulloption = false + config.nulloption = false return config @HasPreCondition: -> diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee index e1548091c..d0ca39503 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee @@ -119,7 +119,9 @@ class App.FormHandlerCoreWorkflow valueFound = false 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() valueFound = true break diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index 5f033dd86..2cf626c38 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -1,12 +1,14 @@ -class Edit extends App.ControllerObserver - model: 'Ticket' - observeNot: - created_at: true - updated_at: true - globalRerender: false +# No usage of a ControllerObserver here because we want to use +# the data of the ticket zoom ajax request which is using the all=true parameter +# and contain the core workflow information as well. Without observer we also +# dont have double rendering because of the zoom (all=true) and observer (full=true) render callback +class Edit extends App.Controller + constructor: (params) -> + super + @render() - render: (ticket, diff) => - defaults = ticket.attributes() + render: => + defaults = @ticket.attributes() delete defaults.article # ignore article infos followUpPossible = App.Group.find(defaults.group_id).follow_up_possible ticketState = App.TicketState.find(defaults.state_id).name @@ -19,7 +21,7 @@ class Edit extends App.ControllerObserver if followUpPossible == 'new_ticket' && ticketState != 'closed' || followUpPossible != 'new_ticket' || - @permissionCheck('admin') || ticket.currentView() is 'agent' + @permissionCheck('admin') || @ticket.currentView() is 'agent' @controllerFormSidebarTicket = new App.ControllerForm( elReplace: @el model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } @@ -28,7 +30,7 @@ class Edit extends App.ControllerObserver filter: @formMeta.filter formMeta: @formMeta params: defaults - isDisabled: !ticket.editable() + isDisabled: !@ticket.editable() taskKey: @taskKey core_workflow: { callbacks: [@markForm] @@ -44,7 +46,7 @@ class Edit extends App.ControllerObserver filter: @formMeta.filter formMeta: @formMeta params: defaults - isDisabled: ticket.editable() + isDisabled: @ticket.editable() taskKey: @taskKey core_workflow: { callbacks: [@markForm] @@ -57,8 +59,8 @@ class Edit extends App.ControllerObserver return if @resetBind @resetBind = true @controllerBind('ui::ticket::taskReset', (data) => - return if data.ticket_id.toString() isnt ticket.id.toString() - @render(ticket) + return if data.ticket_id.toString() isnt @ticket.id.toString() + @render() ) class SidebarTicket extends App.Controller @@ -128,6 +130,7 @@ class SidebarTicket extends App.Controller @edit = new Edit( object_id: @ticket.id + ticket: @ticket el: localEl.find('.edit') taskGet: @taskGet formMeta: @formMeta diff --git a/app/models/core_workflow/attributes.rb b/app/models/core_workflow/attributes.rb index 0cb4da236..7005155af 100644 --- a/app/models/core_workflow/attributes.rb +++ b/app/models/core_workflow/attributes.rb @@ -30,10 +30,26 @@ class CoreWorkflow::Attributes end end + def selectable_field?(key) + return if key == 'id' + return if !@payload['params'].key?(key) + + # some objects have no attributes like "CoreWorkflow"-object as well. + # attributes only exists in the frontend so we skip this check + return true if object_elements.blank? + + object_elements_hash.key?(key) + end + def overwrite_selected(result) selected_attributes = selected_only.attributes selected_attributes.each_key do |key| - next if 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] end @@ -55,7 +71,10 @@ class CoreWorkflow::Attributes # dont use lookup here because the cache will not # know about new attributes and make crashes @saved_only ||= payload_class.find_by(id: @payload['params']['id']) - @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 def saved @@ -68,6 +87,10 @@ class CoreWorkflow::Attributes end end + def object_elements_hash + @object_elements_hash ||= object_elements.index_by { |x| x[:name] } + end + def screen_value(attribute, type) attribute[:screens].dig(@payload['screen'], type) end diff --git a/app/models/core_workflow/result/backend.rb b/app/models/core_workflow/result/backend.rb index 0a458f4b6..4c8452dbf 100644 --- a/app/models/core_workflow/result/backend.rb +++ b/app/models/core_workflow/result/backend.rb @@ -18,4 +18,26 @@ class CoreWorkflow::Result::Backend def result(backend, field, value = nil) @result_object.run_backend_value(backend, field, value) end + + def saved_value + + # make sure we have a saved object + return if @result_object.attributes.saved_only.blank? + + # we only want to have the saved value in the restrictions + # if no changes happend to the form. If the users does changes + # to the form then also the saved value should get removed + return if @result_object.attributes.selected.changed? + + # attribute can be blank e.g. in custom development + # or if attribute is only available in the frontend but not + # in the backend + return if attribute.blank? + + @result_object.attributes.saved_attribute_value(attribute).to_s + end + + def attribute + @attribute ||= @result_object.attributes.object_elements_hash[field] + end end diff --git a/app/models/core_workflow/result/remove_option.rb b/app/models/core_workflow/result/remove_option.rb index a75e7f06c..03b0bcb7e 100644 --- a/app/models/core_workflow/result/remove_option.rb +++ b/app/models/core_workflow/result/remove_option.rb @@ -3,8 +3,14 @@ class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption def run @result_object.result[:restrict_values][field] ||= Array(@result_object.payload['params'][field]) - @result_object.result[:restrict_values][field] -= Array(@perform_config['remove_option']) + @result_object.result[:restrict_values][field] -= Array(config_value) remove_excluded_param_values true end + + def config_value + result = Array(@perform_config['remove_option']) + result -= Array(saved_value) + result + end end diff --git a/app/models/core_workflow/result/set_fixed_to.rb b/app/models/core_workflow/result/set_fixed_to.rb index 45c481b10..9203f12e3 100644 --- a/app/models/core_workflow/result/set_fixed_to.rb +++ b/app/models/core_workflow/result/set_fixed_to.rb @@ -5,21 +5,23 @@ class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption @result_object.result[:restrict_values][field] = if restriction_set? restrict_values else - replace_values + config_value end remove_excluded_param_values true end + def config_value + result = Array(@perform_config['set_fixed_to']) + result |= Array(saved_value) + result + end + def restriction_set? @result_object.result[:restrict_values][field] end def restrict_values - @result_object.result[:restrict_values][field].reject { |v| Array(@perform_config['set_fixed_to']).exclude?(v) } - end - - def replace_values - Array(@perform_config['set_fixed_to']) + @result_object.result[:restrict_values][field].reject { |v| config_value.exclude?(v) } end end diff --git a/spec/system/examples/core_workflow_examples.rb b/spec/system/examples/core_workflow_examples.rb index b507bd2a8..5635bee28 100644 --- a/spec/system/examples/core_workflow_examples.rb +++ b/spec/system/examples/core_workflow_examples.rb @@ -525,15 +525,15 @@ RSpec.shared_examples 'core workflow' do perform: { "#{object_name.downcase}.#{field_name}": { operator: 'set_fixed_to', - set_fixed_to: %w[true] + set_fixed_to: %w[false] }, }) end it 'does perform' do before_it.call - expect(page).to have_selector("select[name='#{field_name}'] option[value='true']", wait: 10) - expect(page).to have_no_selector("select[name='#{field_name}'] option[value='false']", wait: 10) + expect(page).to have_selector("select[name='#{field_name}'] option[value='false']", wait: 10) + expect(page).to have_no_selector("select[name='#{field_name}'] option[value='true']", wait: 10) end end @@ -562,7 +562,7 @@ RSpec.shared_examples 'core workflow' do perform: { "#{object_name.downcase}.#{field_name}": { operator: 'set_fixed_to', - set_fixed_to: ['', 'true'], + set_fixed_to: ['', 'false'], }, }) create(:core_workflow, @@ -577,7 +577,7 @@ RSpec.shared_examples 'core workflow' do it 'does perform' do before_it.call - expect(page).to have_selector("select[name='#{field_name}'] option[value='true'][selected]", wait: 10) + expect(page).to have_selector("select[name='#{field_name}'] option[value='false'][selected]", wait: 10) end end end diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb index 588b9ef3e..ebcf07f0b 100644 --- a/spec/system/ticket/zoom_spec.rb +++ b/spec/system/ticket/zoom_spec.rb @@ -2083,4 +2083,55 @@ RSpec.describe 'Ticket zoom', type: :system do expect(ticket.reload.owner_id).to eq(admin.id) end end + + describe "escaped 'Set fixed' workflows don't refresh set values on active ticket sessions #3757", authenticated_as: :authenticate, db_strategy: :reset do + let(:field_name) { SecureRandom.uuid } + let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => false) } + + def authenticate + workflow + create :object_manager_attribute_boolean, name: field_name, display: field_name, screens: attributes_for(:required_screen) + ObjectManager::Attribute.migration_execute + ticket + true + end + + before do + visit "#ticket/zoom/#{ticket.id}" + end + + context 'when operator set_fixed_to' do + let(:workflow) do + create(:core_workflow, + object: 'Ticket', + perform: { "ticket.#{field_name}" => { 'operator' => 'set_fixed_to', 'set_fixed_to' => ['false'] } }) + end + + context 'when saved value is removed by set_fixed_to operator' do + it 'does show up the saved value if it would not be possible because of the restriction' do + expect(page.find("select[name='#{field_name}']").value).to eq('false') + ticket.update(field_name => true) + wait(10, interval: 0.5).until { page.find("select[name='#{field_name}']").value == 'true' } + expect(page.find("select[name='#{field_name}']").value).to eq('true') + end + end + end + + context 'when operator remove_option' do + let(:workflow) do + create(:core_workflow, + object: 'Ticket', + perform: { "ticket.#{field_name}" => { 'operator' => 'remove_option', 'remove_option' => ['true'] } }) + end + + context 'when saved value is removed by set_fixed_to operator' do + it 'does show up the saved value if it would not be possible because of the restriction' do + expect(page.find("select[name='#{field_name}']").value).to eq('false') + ticket.update(field_name => true) + wait(10, interval: 0.5).until { page.find("select[name='#{field_name}']").value == 'true' } + expect(page.find("select[name='#{field_name}']").value).to eq('true') + end + end + end + end end From 8d37feceb64bcb894fa0941e2a8243b391f59ddf Mon Sep 17 00:00:00 2001 From: Mantas Masalskis Date: Wed, 29 Sep 2021 10:09:08 +0200 Subject: [PATCH 03/65] Fixes #3772 - Existing tickets: New article modal with padding-left: 0; padding-right: 0; --- app/assets/stylesheets/zammad.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 55370dabd..5be9eecfd 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -7017,8 +7017,8 @@ footer { .article-new .textBubble { border-color: #b3b3b3; border-radius: 5px; - padding-left: 0; - padding-right: 0; + padding-left: 12px; + padding-right: 12px; cursor: text; } @@ -7057,7 +7057,12 @@ footer { .attachments:not(:empty) { padding: 9px 5px; 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; } .attachment.attachment--row { From 0cc5764ab35f073f1e8e82d10dee257fee700199 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 29 Sep 2021 10:13:40 +0200 Subject: [PATCH 04/65] Fixes #3365 - No script content (e. g. JavaScript) in emails --- config/initializers/html_sanitizer.rb | 2 +- spec/models/channel/email_parser_spec.rb | 2 +- .../concerns/has_xss_sanitized_note_examples.rb | 4 ++-- spec/models/ticket/article_spec.rb | 6 +++--- test/unit/html_sanitizer_test.rb | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/config/initializers/html_sanitizer.rb b/config/initializers/html_sanitizer.rb index 8183f9a85..80ae25b52 100644 --- a/config/initializers/html_sanitizer.rb +++ b/config/initializers/html_sanitizer.rb @@ -5,11 +5,11 @@ Rails.application.config.html_sanitizer_tags_remove_content = %w[ style comment meta + script ] # content of this tags will will be inserted html quoted Rails.application.config.html_sanitizer_tags_quote_content = %w[ - script ] # only this tags are allowed diff --git a/spec/models/channel/email_parser_spec.rb b/spec/models/channel/email_parser_spec.rb index b553db6f3..9a30a3102 100644 --- a/spec/models/channel/email_parser_spec.rb +++ b/spec/models/channel/email_parser_spec.rb @@ -1258,7 +1258,7 @@ RSpec.describe Channel::EmailParser, type: :model do let(:content_type) { 'text/html' } it 'removes injected some text') } - it 'strips out + some other text RAW it 'removes '), '<b>123</b>') - assert_equal(HtmlSanitizer.strict(''), '<style><b>123</b></style>') + assert_equal(HtmlSanitizer.strict(''), '') + assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict('123123'), '123123') assert_equal(HtmlSanitizer.strict('123123abc'), '123123abc') assert_equal(HtmlSanitizer.strict('123'), '123') - assert_equal(HtmlSanitizer.strict(''), 'alert("XSS!");') + assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') - assert_equal(HtmlSanitizer.strict('">'), 'alert("XSS")">') + assert_equal(HtmlSanitizer.strict('">'), '">') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') @@ -27,13 +27,13 @@ class HtmlSanitizerTest < ActiveSupport::TestCase assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') - assert_equal(HtmlSanitizer.strict('<'), '<alert("XSS");//<') + assert_equal(HtmlSanitizer.strict('<'), '<') assert_equal(HtmlSanitizer.strict(''), 'alert(\'XSS\');') + assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict('
  • XSS
    '), '
    • XSS
    ') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') @@ -73,7 +73,7 @@ tt p://6 6.000146.0x7.147/">XSS', true), ''), '') assert_equal(HtmlSanitizer.strict('XXX'), 'XXX') assert_equal(HtmlSanitizer.strict('XXX', true), 'XXX') - assert_equal(HtmlSanitizer.strict(''), 'alert(1)') + assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict('', true), '') assert_equal(HtmlSanitizer.strict('
    From 68adb3974e1c738d2074039e06763855480d6f3f Mon Sep 17 00:00:00 2001 From: Mantas Date: Mon, 27 Sep 2021 17:24:09 +0300 Subject: [PATCH 05/65] Fixes #3028 - Syntax errors break scheduler job for good --- lib/notification_factory/renderer.rb | 4 ++ .../lib/notification_factory/renderer_spec.rb | 52 ++++++------------- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/lib/notification_factory/renderer.rb b/lib/notification_factory/renderer.rb index b69fad924..669279bd9 100644 --- a/lib/notification_factory/renderer.rb +++ b/lib/notification_factory/renderer.rb @@ -37,6 +37,10 @@ examples how to use def render ERB.new(@template.to_s).result(binding) + rescue Exception => e # rubocop:disable Lint/RescueException + raise StandardError, e.message if e.is_a? SyntaxError + + raise end # d - data of object diff --git a/spec/lib/notification_factory/renderer_spec.rb b/spec/lib/notification_factory/renderer_spec.rb index 87635a90b..1785ba2a0 100644 --- a/spec/lib/notification_factory/renderer_spec.rb +++ b/spec/lib/notification_factory/renderer_spec.rb @@ -19,7 +19,6 @@ RSpec.describe NotificationFactory::Renderer do objects: { ticket: ticket }, template: '#{ticket.customer.firstname.downcase}' expect(renderer.render).to eq 'nicole' - ticket.destroy end it 'correctly renders multiple value calls' do @@ -28,7 +27,21 @@ RSpec.describe NotificationFactory::Renderer do objects: { ticket: ticket }, template: '#{ticket.created_at.value.value.value.value.to_s.first}' expect(renderer.render).to eq '2' - ticket.destroy + end + + it 'raises a StandardError when rendering a template with a broken syntax' do + renderer = build :notification_factory_renderer, template: 'test <% if %>', objects: {} + expect { renderer.render }.to raise_error(StandardError) + end + + it 'raises a StandardError when rendering a template calling a non existant method' do + renderer = build :notification_factory_renderer, template: 'test <% Ticket.non_existant_method %>', objects: {} + expect { renderer.render }.to raise_error(StandardError) + end + + it 'raises a StandardError when rendering a template referencing a non existant object' do + renderer = build :notification_factory_renderer, template: 'test <% NonExistantObject.first %>', objects: {} + expect { renderer.render }.to raise_error(StandardError) end context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do @@ -44,13 +57,6 @@ RSpec.describe NotificationFactory::Renderer do template: '#{ticket.select} _SEPERATOR_ #{ticket.select.value}' expect(renderer.render).to eq 'key_1 _SEPERATOR_ value_1' - ticket.destroy - - ObjectManager::Attribute.remove( - object: 'Ticket', - name: 'select', - ) - ObjectManager::Attribute.migration_execute end it 'correctly renders select attributes on chained user object' do @@ -69,13 +75,6 @@ RSpec.describe NotificationFactory::Renderer do template: '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}' expect(renderer.render).to eq 'key_2 _SEPERATOR_ value_2' - ticket.destroy - - ObjectManager::Attribute.remove( - object: 'User', - name: 'select', - ) - ObjectManager::Attribute.migration_execute end it 'correctly renders select attributes on chained group object' do @@ -94,13 +93,6 @@ RSpec.describe NotificationFactory::Renderer do template: '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' expect(renderer.render).to eq 'key_3 _SEPERATOR_ value_3' - ticket.destroy - - ObjectManager::Attribute.remove( - object: 'Group', - name: 'select', - ) - ObjectManager::Attribute.migration_execute end it 'correctly renders select attributes on chained organization object' do @@ -118,13 +110,6 @@ RSpec.describe NotificationFactory::Renderer do template: '#{ticket.customer.organization.select} _SEPERATOR_ #{ticket.customer.organization.select.value}' expect(renderer.render).to eq 'key_2 _SEPERATOR_ value_2' - ticket.destroy - - ObjectManager::Attribute.remove( - object: 'Organization', - name: 'select', - ) - ObjectManager::Attribute.migration_execute end it 'correctly renders tree select attributes' do @@ -138,13 +123,6 @@ RSpec.describe NotificationFactory::Renderer do template: '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}' expect(renderer.render).to eq 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop' - ticket.destroy - - ObjectManager::Attribute.remove( - object: 'Ticket', - name: 'tree_select', - ) - ObjectManager::Attribute.migration_execute end end end From 1404f7b2fcb39f150aad823d0499b35d35bbf690 Mon Sep 17 00:00:00 2001 From: Mantas Date: Mon, 27 Sep 2021 15:28:53 +0300 Subject: [PATCH 06/65] Fixes #2619 - KB header and footer link-color not changeable --- .../knowledge_base/public_menu_manager.coffee | 6 +- .../app/models/knowledge_base.coffee | 13 +++- .../app/views/knowledge_base/new_modal.coffee | 11 +-- .../public_menu_manager.jst.eco | 2 +- app/assets/stylesheets/zammad.scss | 2 +- app/models/knowledge_base.rb | 5 +- .../public/_inline_stylesheet.html.erb | 4 ++ ...0190531180304_initialize_knowledge_base.rb | 5 +- ...3172256_issue_2619_kb_header_link_color.rb | 11 +++ spec/factories/knowledge_base.rb | 1 + spec/models/knowledge_base_spec.rb | 8 +-- spec/support/custom_matchers.rb | 23 +++++++ .../admin/knowledge_base/public_menu_spec.rb | 34 ++++++++-- .../system/admin/knowledge_base/theme_spec.rb | 30 +++++++++ .../knowledge_base_public/menu_items_spec.rb | 67 +++++++++++++------ 15 files changed, 177 insertions(+), 45 deletions(-) create mode 100644 db/migrate/20210923172256_issue_2619_kb_header_link_color.rb create mode 100644 spec/system/admin/knowledge_base/theme_spec.rb diff --git a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee index 879497e6d..34941450c 100644 --- a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee +++ b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee @@ -27,11 +27,13 @@ class App.KnowledgeBasePublicMenuManager extends App.Controller { headline: 'Header menu', identifier: 'header', - color: kb.color_header + color: kb.color_header, + color_link: kb.color_header_link }, { headline: 'Footer menu', - identifier: 'footer' + identifier: 'footer', + color_link: 'hsl(207,12%,50%)' } ] diff --git a/app/assets/javascripts/app/models/knowledge_base.coffee b/app/assets/javascripts/app/models/knowledge_base.coffee index a48f55bef..8bf326ba7 100644 --- a/app/assets/javascripts/app/models/knowledge_base.coffee +++ b/app/assets/javascripts/app/models/knowledge_base.coffee @@ -1,5 +1,5 @@ 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 App.KnowledgeBaseActions @url: @apiPath + '/knowledge_bases' @@ -148,6 +148,17 @@ class App.KnowledgeBase extends App.Model display: false horizontal: true shown: true + }, { + name: 'color_header_link' + display: 'Header Link Color' + tag: 'color' + style: 'block' + null: false + screen: + admin_style_color_header_link: + display: false + horizontal: true + shown: true # Layout picker is disabled in V1 #}, { # name: 'homepage_layout' diff --git a/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee b/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee index 3dbb91fd8..bfd1905db 100644 --- a/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee +++ b/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee @@ -26,11 +26,12 @@ class App.KnowledgeBaseNewModal extends App.ControllerModal App.UiElement[attribute.tag].prepareParams?(attribute, dom, params) applyDefaults: (params) -> - params['iconset'] = 'FontAwesome' - params['color_highlight'] = '#38ae6a' - params['color_header'] = '#f9fafb' - params['homepage_layout'] = 'grid' - params['category_layout'] = 'grid' + params['iconset'] = 'FontAwesome' + params['color_highlight'] = '#38ae6a' + params['color_header'] = '#f9fafb' + params['color_header_link'] = 'hsl(206,8%,50%)' + params['homepage_layout'] = 'grid' + params['category_layout'] = 'grid' onSubmit: (e) -> params = @formParams(@el) diff --git a/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco index 3ccb6b454..7922cf902 100644 --- a/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco +++ b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco @@ -14,7 +14,7 @@
    <%= kb_locale.systemLocale().name %>
    -
    +
    <% menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, location.identifier) %> <% if menu_items.length == 0: %> diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 5be9eecfd..62d8ff644 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -2661,7 +2661,7 @@ input.has-error { } a { - color: hsl(206,8%,50%); + color: inherit; } .label { diff --git a/app/models/knowledge_base.rb b/app/models/knowledge_base.rb index fac0bfec9..4a254e95b 100644 --- a/app/models/knowledge_base.rb +++ b/app/models/knowledge_base.rb @@ -27,8 +27,9 @@ class KnowledgeBase < ApplicationModel validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS } validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS } - validates :color_highlight, presence: true, color: true - validates :color_header, presence: true, color: true + validates :color_highlight, 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 } diff --git a/app/views/knowledge_base/public/_inline_stylesheet.html.erb b/app/views/knowledge_base/public/_inline_stylesheet.html.erb index 0bcbfbeda..5cf8f590b 100644 --- a/app/views/knowledge_base/public/_inline_stylesheet.html.erb +++ b/app/views/knowledge_base/public/_inline_stylesheet.html.erb @@ -28,4 +28,8 @@ .header { background-color: <%= knowledge_base.color_header %>; } + + .header .menu-item { + color: <%= knowledge_base.color_header_link %>; + } diff --git a/db/migrate/20190531180304_initialize_knowledge_base.rb b/db/migrate/20190531180304_initialize_knowledge_base.rb index b4de691f5..d462e4374 100644 --- a/db/migrate/20190531180304_initialize_knowledge_base.rb +++ b/db/migrate/20190531180304_initialize_knowledge_base.rb @@ -8,8 +8,9 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0] create_table :knowledge_bases do |t| t.string :iconset, limit: 30, null: false - t.string :color_highlight, limit: 25, null: false - t.string :color_header, limit: 25, null: false + t.string :color_highlight, 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 :category_layout, null: false diff --git a/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb new file mode 100644 index 000000000..8c7098004 --- /dev/null +++ b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb @@ -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 + KnowledgeBasis.reset_column_information + end +end diff --git a/spec/factories/knowledge_base.rb b/spec/factories/knowledge_base.rb index 4ad0296a6..0e30049e2 100644 --- a/spec/factories/knowledge_base.rb +++ b/spec/factories/knowledge_base.rb @@ -8,6 +8,7 @@ FactoryBot.define do iconset { 'FontAwesome' } color_highlight { '#AAA' } color_header { '#EEE' } + color_header_link { '#FFF000' } homepage_layout { 'grid' } category_layout { 'list' } diff --git a/spec/models/knowledge_base_spec.rb b/spec/models/knowledge_base_spec.rb index 067d57ad1..65cfcbfa7 100644 --- a/spec/models/knowledge_base_spec.rb +++ b/spec/models/knowledge_base_spec.rb @@ -56,9 +56,9 @@ RSpec.describe KnowledgeBase, type: :model do let(:allowed_values) { ['#aaa', '#ff0000', 'rgb(0,100,100)', 'hsl(0,100%,50%)'] } let(:not_allowed_values) { ['aaa', '#aa', '#ff000', 'rgb(0,100,100', 'def(0,100%,0.5)', 'test'] } - it { is_expected.to allow_values(*allowed_values).for(:color_header) } - it { is_expected.to allow_values(*allowed_values).for(:color_highlight) } - it { is_expected.not_to allow_values(*not_allowed_values).for(:color_header) } - it { is_expected.not_to allow_values(*not_allowed_values).for(:color_highlight) } + %i[color_header color_header_link color_highlight].each do |attr| + it { is_expected.to allow_values(*allowed_values).for(attr) } + it { is_expected.not_to allow_values(*not_allowed_values).for(attr) } + end end end diff --git a/spec/support/custom_matchers.rb b/spec/support/custom_matchers.rb index d0b494ca3..ac8186455 100644 --- a/spec/support/custom_matchers.rb +++ b/spec/support/custom_matchers.rb @@ -7,3 +7,26 @@ RSpec::Matchers.define :exist_in_database do actual.class.exists?(actual.id) end end + +RSpec::Matchers.define :have_computed_style do + + match do + actual_value == expected_value + end + + failure_message do + "Expected element to have CSS property #{expected_key} with value #{expected_value}. But it was #{actual_value}." + end + + def expected_key + expected[0] + end + + def expected_value + expected[1] + end + + def actual_value + actual.evaluate_script "getComputedStyle(this).#{expected_key}" + end +end diff --git a/spec/system/admin/knowledge_base/public_menu_spec.rb b/spec/system/admin/knowledge_base/public_menu_spec.rb index e8829fb31..936518e62 100644 --- a/spec/system/admin/knowledge_base/public_menu_spec.rb +++ b/spec/system/admin/knowledge_base/public_menu_spec.rb @@ -7,20 +7,44 @@ RSpec.describe 'Admin Panel > Knowledge Base > Public Menu', type: :system do include_context 'basic Knowledge Base' include_context 'Knowledge Base menu items' - before do - visit '/#manage/knowledge_base' - find('a', text: 'Public Menu').click - end - context 'lists menu items' do + before do + visit '/#manage/knowledge_base' + find('a', text: 'Public Menu').click + end + it { expect(find_locale('Footer menu', alternative_locale).text).to include menu_item_4.title } it { expect(find_locale('Header menu', primary_locale).text).to include menu_item_1.title } it { expect(find_locale('Header menu', alternative_locale).text).not_to include menu_item_2.title } it { expect(find_locale('Header menu', primary_locale).text).to include menu_item_2.title } end + context 'menu items color' do + before do + knowledge_base.update! color_header_link: color + visit '/#manage/knowledge_base' + find('a', text: 'Public Menu').click + end + + let(:color) { 'rgb(255, 0, 255)' } + + it 'applies color for header preview' do + elem = all('.kb-menu-preview a')[0] + + expect(elem).to have_computed_style :color, color + end + + it 'does not apply color for footer preview' do + elem = all('.kb-menu-preview a')[3] + + expect(elem).not_to have_computed_style :color, color + end + end + context 'edit menu items' do before do + visit '/#manage/knowledge_base' + find('a', text: 'Public Menu').click find_location('Header menu').find('a', text: 'Edit').click modal_ready diff --git a/spec/system/admin/knowledge_base/theme_spec.rb b/spec/system/admin/knowledge_base/theme_spec.rb new file mode 100644 index 000000000..d73faa12b --- /dev/null +++ b/spec/system/admin/knowledge_base/theme_spec.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +# https://github.com/zammad/zammad/issues/266 +RSpec.describe 'Admin Panel > Knowledge Base > Theme', type: :system do + include_context 'basic Knowledge Base' + + context 'header link color' do + before do + knowledge_base + visit '/#manage/knowledge_base' + end + + it 'shows color' do + elem = find('#color_header_link input') + + expect(elem.value).to eq knowledge_base.color_header_link + end + + it 'saves new color' do + find('#color_header_link input').fill_in with: '#ccc' + find('#color_header_link button').click + + await_empty_ajax_queue + + expect(knowledge_base.reload.color_header_link).to eq '#ccc' + end + end +end diff --git a/spec/system/knowledge_base_public/menu_items_spec.rb b/spec/system/knowledge_base_public/menu_items_spec.rb index 53d803515..d36e8f320 100644 --- a/spec/system/knowledge_base_public/menu_items_spec.rb +++ b/spec/system/knowledge_base_public/menu_items_spec.rb @@ -6,35 +6,58 @@ RSpec.describe 'Public Knowledge Base menu items', type: :system, authenticated_ include_context 'basic Knowledge Base' include_context 'Knowledge Base menu items' - before do - published_answer + context 'menu items visibility' do + before do + published_answer - visit help_no_locale_path + visit help_no_locale_path + end + + it 'shows header public link' do + expect(page).to have_css('header .menu-item', text: menu_item_1.title) + end + + it 'shows another header public link' do + expect(page).to have_css('header .menu-item', text: menu_item_2.title) + end + + it "doesn't show footer link in header" do + expect(page).to have_no_css('header .menu-item', text: menu_item_3.title) + end + + it 'shows footer public link' do + expect(page).to have_css('footer .menu-item', text: menu_item_3.title) + end + + it "doesn't show footer link of another locale" do + expect(page).to have_no_css('footer .menu-item', text: menu_item_4.title) + end + + it 'shows public links in given order' do + index_1 = page.body.index menu_item_1.title + index_2 = page.body.index menu_item_2.title + expect(index_1).to be < index_2 + end end - it 'shows header public link' do - expect(page).to have_css('header .menu-item', text: menu_item_1.title) - end + context 'menu items color' do + before do + knowledge_base.update! color_header_link: color + visit help_no_locale_path + end - it 'shows another header public link' do - expect(page).to have_css('header .menu-item', text: menu_item_2.title) - end + let(:color) { 'rgb(255, 0, 255)' } - it "doesn't show footer link in header" do - expect(page).to have_no_css('header .menu-item', text: menu_item_3.title) - end + it 'applies color for header preview' do + elem = all('.menu-item')[0] - it 'shows footer public link' do - expect(page).to have_css('footer .menu-item', text: menu_item_3.title) - end + expect(elem).to have_computed_style :color, color + end - it "doesn't show footer link of another locale" do - expect(page).to have_no_css('footer .menu-item', text: menu_item_4.title) - end + it 'does not apply color for footer preview' do + elem = all('.menu-item')[2] - it 'shows public links in given order' do - index_1 = page.body.index menu_item_1.title - index_2 = page.body.index menu_item_2.title - expect(index_1).to be < index_2 + expect(elem).not_to have_computed_style :color, color + end end end From bf6da241d8190d29c7e0a2ad4c5f929b51faaa64 Mon Sep 17 00:00:00 2001 From: Romit Choudhary Date: Wed, 29 Sep 2021 11:24:50 +0200 Subject: [PATCH 07/65] Fixes #2351 - Unable to cancel attachment upload --- app/assets/stylesheets/zammad.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 62d8ff644..a4e80733d 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -7051,7 +7051,7 @@ footer { padding: 10px 0; color: #b3b3b3; overflow: hidden; - @extend .u-unclickable, .u-textTruncate; + @extend .u-textTruncate; } .attachments:not(:empty) { From 6d8f5b7d958bbfed431d78aed4461f0bc2c79dae Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Wed, 29 Sep 2021 12:16:16 +0200 Subject: [PATCH 08/65] Fixes #3777 - misspelled KnowledgeBase constant breaks update. --- db/migrate/20210923172256_issue_2619_kb_header_link_color.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb index 8c7098004..b2b39ea8b 100644 --- a/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb +++ b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb @@ -4,8 +4,8 @@ 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%)' + add_column :knowledge_bases, :color_header_link, :string, limit: 25, null: false, default: 'hsl(206,8%,50%)' # rubocop:disable Zammad/ExistsResetColumnInformation change_column_default :knowledge_bases, :color_header_link, nil - KnowledgeBasis.reset_column_information + KnowledgeBase.reset_column_information end end From 36aa35f765874b941b3df612a7448bd881a5a509 Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Wed, 29 Sep 2021 11:01:24 +0200 Subject: [PATCH 09/65] Maintenance: Port admin_calendar_sla_test.rb to Capybara. --- script/build/test_slice_tests.sh | 6 -- spec/system/manage/sla_spec.rb | 19 ++++++ test/browser/admin_calendar_sla_test.rb | 85 ------------------------- 3 files changed, 19 insertions(+), 91 deletions(-) delete mode 100644 test/browser/admin_calendar_sla_test.rb diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index cd4919851..8da4c701d 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -12,7 +12,6 @@ if [ "$LEVEL" == '1' ]; then cp test/integration/aaa_auto_wizard_base_setup_test.rb test/browser/aaa_auto_wizard_base_setup_test.rb rm test/browser/abb_one_group_test.rb rm test/browser/admin_channel_email_test.rb - rm test/browser/admin_calendar_sla_test.rb rm test/browser/admin_drag_drop_to_new_group_test.rb rm test/browser/admin_overview_test.rb rm test/browser/admin_permissions_granular_vs_full_test.rb @@ -72,7 +71,6 @@ elif [ "$LEVEL" == '2' ]; then # test/browser/aaa_getting_started_test.rb # test/browser/abb_one_group_test.rb rm test/browser/admin_channel_email_test.rb - rm test/browser/admin_calendar_sla_test.rb rm test/browser/admin_drag_drop_to_new_group_test.rb rm test/browser/admin_overview_test.rb rm test/browser/admin_permissions_granular_vs_full_test.rb @@ -132,7 +130,6 @@ elif [ "$LEVEL" == '3' ]; then # test/browser/aaa_getting_started_test.rb # test/browser/abb_one_group_test.rb rm test/browser/admin_channel_email_test.rb - rm test/browser/admin_calendar_sla_test.rb rm test/browser/admin_drag_drop_to_new_group_test.rb rm test/browser/admin_overview_test.rb rm test/browser/admin_permissions_granular_vs_full_test.rb @@ -192,7 +189,6 @@ elif [ "$LEVEL" == '4' ]; then # test/browser/aaa_getting_started_test.rb # test/browser/abb_one_group_test.rb rm test/browser/admin_channel_email_test.rb - rm test/browser/admin_calendar_sla_test.rb rm test/browser/admin_drag_drop_to_new_group_test.rb rm test/browser/admin_overview_test.rb rm test/browser/admin_permissions_granular_vs_full_test.rb @@ -251,7 +247,6 @@ elif [ "$LEVEL" == '5' ]; then # only profile action & admin # test/browser/abb_one_group_test.rb # test/browser/admin_channel_email_test.rb - # test/browser/admin_calendar_sla_test.rb # rm test/browser/admin_drag_drop_to_new_group_test.rb # test/browser/admin_overview_test.rb # rm test/browser/admin_permissions_granular_vs_full_test.rb @@ -313,7 +308,6 @@ elif [ "$LEVEL" == '6' ]; then cp test/integration/aaa_auto_wizard_base_setup_test.rb test/browser/aaa_auto_wizard_base_setup_test.rb rm test/browser/abb_one_group_test.rb rm test/browser/admin_channel_email_test.rb - rm test/browser/admin_calendar_sla_test.rb rm test/browser/admin_drag_drop_to_new_group_test.rb rm test/browser/admin_overview_test.rb rm test/browser/admin_permissions_granular_vs_full_test.rb diff --git a/spec/system/manage/sla_spec.rb b/spec/system/manage/sla_spec.rb index e667856ef..385109e81 100644 --- a/spec/system/manage/sla_spec.rb +++ b/spec/system/manage/sla_spec.rb @@ -57,4 +57,23 @@ RSpec.describe 'Manage > Sla', type: :system do expect(page.find('input[name=solution_time_in_text]')[:required]).not_to eq('true') end end + + context 'when using custom calendars' do + let(:calendar) { create(:calendar) } + + it 'allows to select custom calendars' do + calendar + page.refresh + click '.js-new' + + within '.modal-dialog' do + fill_in :name, with: 'SLA with custom calendar' + select calendar.name, from: :calendar_id + click '.js-submit' + end + + modal_disappear + expect(page).to have_text(calendar.name) + end + end end diff --git a/test/browser/admin_calendar_sla_test.rb b/test/browser/admin_calendar_sla_test.rb deleted file mode 100644 index 8f0580aa2..000000000 --- a/test/browser/admin_calendar_sla_test.rb +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -require 'browser_test_helper' - -class AdminCalendarSlaTest < TestCase - def test_calendar - @browser = browser_instance - login( - username: 'admin@example.com', - password: 'test', - url: browser_url, - ) - tasks_close_all - - calendar_name = "ZZZ some calendar #{SecureRandom.uuid}" - sla_name = "ZZZ some sla #{SecureRandom.uuid}" - timezone = 'Europe/Berlin' - timezone_verify = "Europe/Berlin\s\\(GMT\\+(2|1)\\)" - calendar_create( - data: { - name: calendar_name, - timezone: timezone, - } - ) - - # got to maintanance - click(css: '[href="#manage"]') - click(css: '[href="#system/maintenance"]') - watch_for( - css: '.content.active', - value: 'Enable or disable the maintenance mode', - timeout: 4, - ) - - # go back - click(css: '[href="#manage"]') - click(css: '[href="#manage/calendars"]') - watch_for( - css: '.content.active', - value: calendar_name, - timeout: 4, - ) - - logout - - login( - username: 'admin@example.com', - password: 'test', - ) - - # check if admin exists - click(css: '[href="#manage"]') - click(css: '[href="#manage/calendars"]') - watch_for( - css: '.content.active', - value: calendar_name, - timeout: 4, - ) - - # @browser.execute_script('$(\'.content.active table tr td:contains(" ' + data[:name] + '")\').first().click()') - @browser.execute_script('$(\'.content.active .main .js-edit\').last().click()') - - modal_ready(browser: @browser) - watch_for( - css: '.content.active .modal input[name=name]', - value: calendar_name, - timeout: 4, - ) - watch_for( - css: '.content.active .modal input.js-input', - value: timezone_verify, - timeout: 4, - ) - modal_close - - sla_create( - data: { - name: sla_name, - calendar: "#{calendar_name} - #{timezone}", - first_response_time_in_text: 61 - }, - ) - - end -end From c335147d625d78bcc25320cb0f783379a57b59d5 Mon Sep 17 00:00:00 2001 From: Dominik Klein Date: Wed, 29 Sep 2021 17:17:21 +0200 Subject: [PATCH 10/65] Maintenance: Port customer ticket create fields test to capybara. --- script/build/test_slice_tests.sh | 6 --- spec/system/ticket/create_spec.rb | 33 +++++++++++++ .../customer_ticket_create_fields_test.rb | 47 ------------------- 3 files changed, 33 insertions(+), 53 deletions(-) delete mode 100644 test/browser/customer_ticket_create_fields_test.rb diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index 8da4c701d..dd79296ab 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -46,7 +46,6 @@ if [ "$LEVEL" == '1' ]; then rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_profile_test.rb # test/browser/auth_test.rb - rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb rm test/browser/integration_test.rb @@ -105,7 +104,6 @@ elif [ "$LEVEL" == '2' ]; then rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb - rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb rm test/browser/integration_test.rb @@ -164,7 +162,6 @@ elif [ "$LEVEL" == '3' ]; then rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb - rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb rm test/browser/integration_test.rb @@ -223,7 +220,6 @@ elif [ "$LEVEL" == '4' ]; then rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb - # test/browser/customer_ticket_create_fields_test.rb # test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb rm test/browser/integration_test.rb @@ -281,7 +277,6 @@ elif [ "$LEVEL" == '5' ]; then # test/browser/agent_user_manage_test.rb # test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb - rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb rm test/browser/integration_test.rb @@ -342,7 +337,6 @@ elif [ "$LEVEL" == '6' ]; then rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb - rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb # test/browser/first_steps_test.rb # test/browser/integration_test.rb diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index 05a11b108..debac63c2 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -500,6 +500,39 @@ RSpec.describe 'Ticket Create', type: :system do end end + context 'when agent and customer user login after another' do + let(:agent) { create(:agent, password: 'test') } + let(:customer) { create(:customer, password: 'test') } + + it 'customer user should not have agent object attributes', authenticated_as: :agent do + visit 'ticket/create' + + logout + + # Re-create agent session and fetch object attributes. + login( + username: agent.login, + password: 'test' + ) + visit 'ticket/create' + + # Re-remove local object attributes bound to the session + # there was an issue (#1856) where the old attribute values + # persisted and were stored as the original attributes. + logout + + # Create customer session and fetch object attributes. + login( + username: customer.login, + password: 'test' + ) + + visit 'customer_ticket_new' + + expect(page).to have_no_css('.newTicket input[name="customer_id"]') + end + end + describe 'It should be possible to show attributes which are configured shown false #3726', authenticated_as: :authenticate, db_strategy: :reset do let(:field_name) { SecureRandom.uuid } let(:field) do diff --git a/test/browser/customer_ticket_create_fields_test.rb b/test/browser/customer_ticket_create_fields_test.rb deleted file mode 100644 index c7ee90222..000000000 --- a/test/browser/customer_ticket_create_fields_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -require 'browser_test_helper' - -class CustomerTicketCreateFieldsTest < TestCase - def test_customer_ticket_create_fields - @browser = browser_instance - - # create agent session and fetch object attributes - login( - username: 'admin@example.com', - password: 'test', - url: browser_url, - ) - # remove local object attributes bound to the session - logout - - # re-create agent session and fetch object attributes - login( - username: 'admin@example.com', - password: 'test', - url: browser_url, - ) - # re-remove local object attributes bound to the session - # there was an issue (#1856) where the old attribute values - # persisted and were stored as the original attributes - logout - - # create customer session and fetch object attributes - login( - username: 'nicole.braun@zammad.org', - password: 'test', - url: browser_url, - ) - - # customer ticket create - click(css: 'a[href="#new"]', only_if_exists: true) - click(css: 'a[href="#customer_ticket_new"]') - sleep 2 - - # ensure that the object attributes of the agent session - # were removed properly and won't get displayed in the form - exists_not( - css: '.newTicket input[name="customer_id"]', - ) - end -end From 4e4ba091d40484181df3dc503586dbf05feeccfa Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Wed, 29 Sep 2021 15:18:47 +0100 Subject: [PATCH 11/65] Fixes #3776 - Force users to reload after system migration. --- .../20210929161701_reload_after_core_workflow.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 db/migrate/20210929161701_reload_after_core_workflow.rb diff --git a/db/migrate/20210929161701_reload_after_core_workflow.rb b/db/migrate/20210929161701_reload_after_core_workflow.rb new file mode 100644 index 000000000..a936e3653 --- /dev/null +++ b/db/migrate/20210929161701_reload_after_core_workflow.rb @@ -0,0 +1,11 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class ReloadAfterCoreWorkflow < ActiveRecord::Migration[4.2] + def up + + # return if it's a new setup + return if !Setting.exists?(name: 'system_init_done') + + AppVersion.set(true, 'app_version') + end +end From 51933e3b76a6b7bfd716ae73cb0274adaf6126c8 Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Thu, 30 Sep 2021 09:18:16 +0200 Subject: [PATCH 12/65] Maintenance: Bump rubocop from 1.21.0 to 1.22.0 --- Gemfile.lock | 4 ++-- app/models/channel/email_parser.rb | 2 +- lib/html_sanitizer.rb | 4 ++-- test/integration/email_postmaster_to_sender.rb | 8 ++------ test/test_helper.rb | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f41562771..b9dbee417 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -498,13 +498,13 @@ GEM rspec-support (~> 3.10) rspec-support (3.10.2) rszr (0.5.2) - rubocop (1.21.0) + rubocop (1.22.0) parallel (~> 1.10) parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.9.1, < 2.0) + rubocop-ast (>= 1.12.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.12.0) diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 9c132dc48..57ed916ff 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -502,7 +502,7 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again path = Rails.root.join('tmp/unprocessable_mail') files = [] Dir.glob("#{path}/*.eml") do |entry| - ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, IO.binread(entry)) + ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, File.binread(entry)) next if ticket.blank? files.push entry diff --git a/lib/html_sanitizer.rb b/lib/html_sanitizer.rb index 2ca0819b9..440f3b94e 100644 --- a/lib/html_sanitizer.rb +++ b/lib/html_sanitizer.rb @@ -161,8 +161,8 @@ satinize html string based on whiltelist # wrap plain-text URLs in tags if node.is_a?(Nokogiri::XML::Text) && node.content.present? && node.content.include?(':') && node.ancestors.map(&:name).exclude?('a') urls = URI.extract(node.content, LINKABLE_URL_SCHEMES) - .map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas - .reject { |u| u.match?(%r{^[^:]+:$}) } # URI::extract will match, e.g., 'tel:' + .map { |u| u.sub(%r{[,.]$}, '') } # URI::extract captures trailing dots/commas + .grep_v(%r{^[^:]+:$}) # URI::extract will match, e.g., 'tel:' next if urls.blank? diff --git a/test/integration/email_postmaster_to_sender.rb b/test/integration/email_postmaster_to_sender.rb index b66625921..af10ad22d 100644 --- a/test/integration/email_postmaster_to_sender.rb +++ b/test/integration/email_postmaster_to_sender.rb @@ -104,9 +104,7 @@ Oversized Email Message Body #{'#' * 120_000} # 1. verify that the oversized email has been saved locally to: # /tmp/oversized_mail/yyyy-mm-ddThh:mm:ss-:md5.eml path = Rails.root.join('tmp/oversized_mail') - target_files = Dir.entries(path).select do |filename| - filename =~ %r{^#{large_message_md5}\.eml$} - end + target_files = Dir.entries(path).grep(%r{^#{large_message_md5}\.eml$}) assert(target_files.present?, 'Large message .eml log file must be present.') # pick the latest file that matches the criteria @@ -193,9 +191,7 @@ Oversized Email Message Body #{'#' * 120_000} # 1. verify that the oversized email has been saved locally to: # /tmp/oversized_mail/yyyy-mm-ddThh:mm:ss-:md5.eml path = Rails.root.join('tmp/oversized_mail') - target_files = Dir.entries(path).select do |filename| - filename =~ %r{^.+?\.eml$} - end + target_files = Dir.entries(path).grep(%r{^.+?\.eml$}) assert_not(target_files.blank?, 'Large message .eml log file must be blank.') # 2. verify that a postmaster response email has been sent to the sender diff --git a/test/test_helper.rb b/test/test_helper.rb index ad9295bfe..fe893a130 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -62,7 +62,7 @@ class ActiveSupport::TestCase # read config file and count type & recipients file = Rails.root.join('log', "#{Rails.env}.log") lines = [] - IO.foreach(file) do |line| + File.foreach(file) do |line| lines.push line end count = 0 From 4f3e7f4003f97ea75e1ff953e82e46369bd9ec4b Mon Sep 17 00:00:00 2001 From: Romit Choudhary Date: Thu, 30 Sep 2021 09:25:13 +0200 Subject: [PATCH 13/65] Fixes #2780 - Shared Organisation issue create your first ticket --- .../app/controllers/ticket_overview.coffee | 2 +- .../app/views/customer_not_ticket_exists.jst.eco | 12 ++++++++---- spec/system/overview_spec.rb | 13 +++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index d9231b347..aa254c6e3 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -1314,7 +1314,7 @@ class Table extends App.Controller return if ticketListShow[0] || @permissionCheck('ticket.agent') tickets_count = user.lifetimeCustomerTicketsCount() - @html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0) + @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 @listenTo user, 'refresh', => diff --git a/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco b/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco index dfd72889b..17aa5bfbe 100644 --- a/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco +++ b/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco @@ -6,11 +6,15 @@ <% if @has_any_tickets: %>

    <%- @T('You have no tickets to display in this overview.') %>

    <% else: %> -

    <%- @T('You have not created a ticket yet.') %>

    -

    <%- @T('The way to communicate with us is this thing called "ticket".') %>

    -

    <%- @T('Please click the button below to create your first one.') %>

    + <% if @is_allowed_to_create_ticket: %> +

    <%- @T('You have not created a ticket yet.') %>

    +

    <%- @T('The way to communicate with us is this thing called "ticket".') %>

    +

    <%- @T('Please click the button below to create your first one.') %>

    -

    <%- @T('Create your first ticket') %>

    +

    <%- @T('Create your first ticket') %>

    + <% else: %> +

    <%- @T('You currently don\'t have any tickets.') %>

    + <% end %> <% end %>
    diff --git a/spec/system/overview_spec.rb b/spec/system/overview_spec.rb index f984e1626..50402cfe8 100644 --- a/spec/system/overview_spec.rb +++ b/spec/system/overview_spec.rb @@ -23,6 +23,19 @@ RSpec.describe 'Overview', type: :system do end end + def authenticate + Setting.set('customer_ticket_create', false) + customer + end + + it 'does not show create button when ticket creation via web is disabled', authenticated_as: :authenticate do + visit "ticket/view/#{main_overview.link}" + + within :active_content do + expect(page).to have_text 'You currently don\'t have any tickets.' + end + end + it 'shows overview-specific message if customer has tickets in other overview', performs_jobs: true do perform_enqueued_jobs only: TicketUserTicketCounterJob do create(:ticket, customer: customer) From dbb93bf02ce46a7aed29a39619ea9dece66e7f28 Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Thu, 30 Sep 2021 10:15:34 +0200 Subject: [PATCH 14/65] Maintenance: Improve MR template. --- .gitlab/merge_request_templates/Default.md | 40 ++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md index 941bbfadc..477469157 100644 --- a/.gitlab/merge_request_templates/Default.md +++ b/.gitlab/merge_request_templates/Default.md @@ -1,8 +1,9 @@ ## What does this MR do? - + +[Issue Link]() -## Screenshots +## Screenshots ### Before @@ -12,7 +13,7 @@ ![alt text](https://example.com/after.png) -## Notes +## Code Changes * This MR **does** @@ -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!) --> -### Follow-up Required +### Documentation Follow-up Required? + + +This MR may require follow-up by the documentation team. +/label ~Documentation + + +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 +> @MrGeneration please check if this MR requires changes to the documentation. Thanks! From 489177cca18b87979ca437b82c201fa3ff43d7ab Mon Sep 17 00:00:00 2001 From: benrubson <6764151+benrubson@users.noreply.github.com> Date: Tue, 28 Sep 2021 21:03:29 +0200 Subject: [PATCH 15/65] Fixes #2674, closes #3775 - Zammad preflight check warning output causes Syntax-Error in postinstall.sh and failing installation. --- contrib/packager.io/functions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/packager.io/functions b/contrib/packager.io/functions index 2847672f5..362d065d9 100644 --- a/contrib/packager.io/functions +++ b/contrib/packager.io/functions @@ -228,7 +228,7 @@ function create_webserver_config () { function setup_elasticsearch () { echo "# Configuring Elasticsearch..." - ES_CONNECTION="$(zammad run rails r "puts Setting.get('es_url')"| tail -n 1 2>> /dev/null)" + ES_CONNECTION="$(zammad run rails r "puts '',Setting.get('es_url')"| tail -n 1 2>> /dev/null)" if [ -z "${ES_CONNECTION}" ]; then echo "-- Nevermind, no es_url is set, leaving Elasticsearch untouched ...!" From 9c5a11f36e603abe96a43d713ae41f8aae33483d Mon Sep 17 00:00:00 2001 From: Mantas Masalskis Date: Fri, 1 Oct 2021 11:56:09 +0200 Subject: [PATCH 16/65] Maintenance: Rubocop uses custom inflections --- .rubocop/default.yml | 2 ++ Gemfile | 1 + Gemfile.lock | 7 ++++++- config/initializers/inflections.rb | 2 +- .../20210923172256_issue_2619_kb_header_link_color.rb | 2 +- spec/integration/github_spec.rb | 2 +- spec/integration/gitlab_spec.rb | 2 +- 7 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.rubocop/default.yml b/.rubocop/default.yml index bf0656c06..f08267c10 100644 --- a/.rubocop/default.yml +++ b/.rubocop/default.yml @@ -6,6 +6,8 @@ require: - rubocop-performance - rubocop-rails - rubocop-rspec + - rubocop-inflector + - ../config/initializers/inflections.rb - ./rubocop_zammad.rb inherit_from: diff --git a/Gemfile b/Gemfile index 08c7be873..6ce41c930 100644 --- a/Gemfile +++ b/Gemfile @@ -198,6 +198,7 @@ group :development, :test do gem 'overcommit' gem 'rubocop' gem 'rubocop-faker' + gem 'rubocop-inflector' gem 'rubocop-performance' gem 'rubocop-rails' gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index b9dbee417..f37b75ee5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -512,6 +512,10 @@ GEM rubocop-faker (1.1.0) faker (>= 2.12.0) rubocop (>= 0.82.0) + rubocop-inflector (0.1.1) + activesupport + rubocop + rubocop-rspec rubocop-performance (1.11.5) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) @@ -729,6 +733,7 @@ DEPENDENCIES rszr (= 0.5.2) rubocop rubocop-faker + rubocop-inflector rubocop-performance rubocop-rails rubocop-rspec @@ -762,4 +767,4 @@ RUBY VERSION ruby 2.7.4p191 BUNDLED WITH - 2.2.20 + 2.2.27 diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index c73fe85db..8bfe2e324 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -21,7 +21,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| # Rails thinks the singularized version of knowledge_bases is knowledge_basis?! # see: KnowledgeBase.table_name.singularize - inflect.singular(%r{(knowledge_base)s$}i, '\1') + inflect.irregular 'base', 'bases' inflect.acronym 'SMIME' inflect.acronym 'GitLab' inflect.acronym 'GitHub' diff --git a/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb index b2b39ea8b..53e0e680d 100644 --- a/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb +++ b/db/migrate/20210923172256_issue_2619_kb_header_link_color.rb @@ -4,7 +4,7 @@ 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%)' # rubocop:disable Zammad/ExistsResetColumnInformation + 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 diff --git a/spec/integration/github_spec.rb b/spec/integration/github_spec.rb index 1f38c81de..060009657 100644 --- a/spec/integration/github_spec.rb +++ b/spec/integration/github_spec.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ require 'rails_helper' -RSpec.describe GitHub, type: :integration, required_envs: %w[GITHUB_ENDPOINT GITHUB_APITOKEN] do # rubocop:disable RSpec/FilePath +RSpec.describe GitHub, type: :integration, required_envs: %w[GITHUB_ENDPOINT GITHUB_APITOKEN] do let(:instance) { described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']) } let(:issue_data) do diff --git a/spec/integration/gitlab_spec.rb b/spec/integration/gitlab_spec.rb index 3810c78b3..788e376f6 100644 --- a/spec/integration/gitlab_spec.rb +++ b/spec/integration/gitlab_spec.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ require 'rails_helper' -RSpec.describe GitLab, type: :integration, required_envs: %w[GITLAB_ENDPOINT GITLAB_APITOKEN] do # rubocop:disable RSpec/FilePath +RSpec.describe GitLab, type: :integration, required_envs: %w[GITLAB_ENDPOINT GITLAB_APITOKEN] do let(:instance) { described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']) } let(:issue_data) do From 79bacb14aa1c1dc40e50909f5ee942cd3d6dbc84 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Thu, 30 Sep 2021 13:19:08 +0100 Subject: [PATCH 17/65] Follow up 4e4ba091d40484181df3dc503586dbf05feeccfa - Fixes #3776 - Force users to reload after system migration. --- db/migrate/20210929161701_reload_after_core_workflow.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20210929161701_reload_after_core_workflow.rb b/db/migrate/20210929161701_reload_after_core_workflow.rb index a936e3653..2a3004141 100644 --- a/db/migrate/20210929161701_reload_after_core_workflow.rb +++ b/db/migrate/20210929161701_reload_after_core_workflow.rb @@ -1,6 +1,6 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ -class ReloadAfterCoreWorkflow < ActiveRecord::Migration[4.2] +class ReloadAfterCoreWorkflow < ActiveRecord::Migration[6.0] def up # return if it's a new setup From 5bd714878aa93f0321eaf60820d3ed95d00e2f15 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Fri, 1 Oct 2021 12:25:06 +0200 Subject: [PATCH 18/65] Fixes #3781 - ObjectManager Attribute without screen attribute causes CoreWorkflows migration to fail --- db/migrate/20210128131507_init_core_workflow.rb | 6 ++++++ .../20210921112300_issue_3751_missing_workflow_screens.rb | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/db/migrate/20210128131507_init_core_workflow.rb b/db/migrate/20210128131507_init_core_workflow.rb index ad4782b78..1c29a9e13 100644 --- a/db/migrate/20210128131507_init_core_workflow.rb +++ b/db/migrate/20210128131507_init_core_workflow.rb @@ -75,6 +75,8 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2] def fix_pending_time pending_time = ObjectManager::Attribute.find_by(name: 'pending_time', object_lookup: ObjectLookup.find_by(name: 'Ticket')) + return if pending_time.blank? + pending_time.data_option.delete('required_if') pending_time.data_option.delete('shown_if') pending_time.save @@ -83,6 +85,8 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2] def fix_organization_screens %w[domain note].each do |name| field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization')) + next if field.blank? + field.screens['create'] ||= {} field.screens['create']['-all-'] ||= {} field.screens['create']['-all-']['null'] = true @@ -93,6 +97,8 @@ class InitCoreWorkflow < ActiveRecord::Migration[5.2] def fix_user_screens %w[email web phone mobile organization_id fax department street zip city country address password vip note role_ids].each do |name| field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User')) + next if field.blank? + field.screens['create'] ||= {} field.screens['create']['-all-'] ||= {} field.screens['create']['-all-']['null'] = true diff --git a/db/migrate/20210921112300_issue_3751_missing_workflow_screens.rb b/db/migrate/20210921112300_issue_3751_missing_workflow_screens.rb index f1eaade52..aae63c2b5 100644 --- a/db/migrate/20210921112300_issue_3751_missing_workflow_screens.rb +++ b/db/migrate/20210921112300_issue_3751_missing_workflow_screens.rb @@ -11,6 +11,8 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0] def fix_organization_screens_create %w[name shared domain_assignment active].each do |name| field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization')) + next if field.blank? + field.screens['create'] ||= {} field.screens['create']['-all-'] ||= {} field.screens['create']['-all-']['null'] = false @@ -21,6 +23,8 @@ class Issue3751MissingWorkflowScreens < ActiveRecord::Migration[6.0] def fix_user_screens_create %w[firstname lastname active].each do |name| field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User')) + next if field.blank? + field.screens['create'] ||= {} field.screens['create']['-all-'] ||= {} field.screens['create']['-all-']['null'] = false From 03bcf6612685e24e4be11399c976c2f8be4c7aa5 Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Mon, 4 Oct 2021 07:02:01 +0200 Subject: [PATCH 19/65] Maintenance: Updated argon2, async-pool, jwt and rubocop gems. --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f37b75ee5..f776bcd79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM activerecord (>= 4.2) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - argon2 (2.0.3) + argon2 (2.1.0) ffi (~> 1.14) ffi-compiler (~> 1.0) ast (2.4.2) @@ -110,7 +110,7 @@ GEM faraday async-io (1.32.2) async - async-pool (0.3.8) + async-pool (0.3.9) async (>= 1.25) autoprefixer-rails (10.3.3.0) execjs (~> 2) @@ -293,7 +293,7 @@ GEM iniparse (1.5.0) interception (0.5) json (2.5.1) - jwt (2.2.3) + jwt (2.3.0) kgio (2.11.4) koala (3.0.0) addressable @@ -498,7 +498,7 @@ GEM rspec-support (~> 3.10) rspec-support (3.10.2) rszr (0.5.2) - rubocop (1.22.0) + rubocop (1.22.1) parallel (~> 1.10) parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) From dd66c30d59d3d4d29d37e4ba0873fcba1f67316f Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Mon, 4 Oct 2021 10:50:14 +0200 Subject: [PATCH 20/65] Follow up 5f2181d8a3e8a86acdc6ce0576343e23242c407d - Fixes #3757 - escaped 'Set fixed' workflows don't refresh set values on active ticket sessions. --- .../app/controllers/ticket_zoom.coffee | 5 +- .../ticket_zoom/sidebar_ticket.coffee | 10 ++++ spec/system/ticket/zoom_spec.rb | 59 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 9a9bad539..6b560691b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller formMeta = data.form_meta # on the following states we want to rerender the ticket: - # - if the object attribute configuration has changed (attribute values, restrictions, filters) + # - if the object attribute configuration has changed (attribute values, dependecies, filters) # - if the user view has changed (agent/customer) # - 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 @view = view @@ -214,6 +214,7 @@ class App.TicketZoom extends App.Controller # render page @render(local) + App.Event.trigger('ui::ticket::load', data) meta: => diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index 2cf626c38..bdb18819b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -5,6 +5,13 @@ class Edit extends App.Controller constructor: (params) -> super + @controllerBind('ui::ticket::load', (data) => + return if data.ticket_id.toString() isnt @ticket.id.toString() + + @ticket = App.Ticket.find(@ticket.id) + @formMeta = data.form_meta + @render() + ) @render() render: => @@ -18,6 +25,9 @@ class Edit extends App.Controller if !_.isEmpty(taskState) defaults = _.extend(defaults, taskState) + # remove core workflow data because it should trigger a request to get data + # for the new ticket + eventually changed task state + @formMeta.core_workflow = undefined if followUpPossible == 'new_ticket' && ticketState != 'closed' || followUpPossible != 'new_ticket' || diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb index ebcf07f0b..8a63fba43 100644 --- a/spec/system/ticket/zoom_spec.rb +++ b/spec/system/ticket/zoom_spec.rb @@ -2134,4 +2134,63 @@ RSpec.describe 'Ticket zoom', type: :system do end end end + + context 'Basic sidebar handling because of regressions in #3757' do + let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) } + + before do + visit "#ticket/zoom/#{ticket.id}" + end + + it 'does show up the new priority' do + high_prio = Ticket::Priority.find_by(name: '3 high') + ticket.update(priority: high_prio) + wait(10, interval: 0.5).until { page.find("select[name='priority_id']").value == high_prio.id.to_s } + expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s) + end + + it 'does show up the new state and pending time' do + pending_state = Ticket::State.find_by(name: 'pending reminder') + ticket.update(state: pending_state, pending_time: 1.day.from_now) + wait(10, interval: 0.5).until { page.find("select[name='state_id']").value == pending_state.id.to_s } + expect(page.find("select[name='state_id']").value).to eq(pending_state.id.to_s) + expect(page).to have_selector("div[data-name='pending_time']") + end + + it 'does merge attributes with remote priority (ajax) and local state (user)' do + select 'closed', from: 'State' + high_prio = Ticket::Priority.find_by(name: '3 high') + closed_state = Ticket::State.find_by(name: 'closed') + ticket.update(priority: high_prio) + wait(10, interval: 0.5).until { page.find("select[name='priority_id']").value == high_prio.id.to_s } + expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s) + expect(page.find("select[name='state_id']").value).to eq(closed_state.id.to_s) + end + + context 'when 2 users are in 2 different tickets' do + let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users')) } + let(:agent2) { create(:agent, password: 'test', groups: [Group.find_by(name: 'Users')]) } + + before do + using_session(:second_browser) do + login( + username: agent2.login, + password: 'test', + ) + visit "#ticket/zoom/#{ticket.id}" + visit "#ticket/zoom/#{ticket2.id}" + end + end + + it 'does not make any changes to the second browser ticket' do + closed_state = Ticket::State.find_by(name: 'closed') + select 'closed', from: 'State' + find('.js-submit').click + using_session(:second_browser) do + sleep 3 + expect(page.find("select[name='state_id']").value).not_to eq(closed_state.id.to_s) + end + end + end + end end From ca6e510ed4859f2ff214083003d51dc9834ef03d Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Mon, 4 Oct 2021 11:57:32 +0200 Subject: [PATCH 21/65] Maintenance: Specify a certain PostgreSQL version for the build process that still works with older platforms. --- .pkgr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pkgr.yml b/.pkgr.yml index 4e4e94567..e4468d5b5 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -116,6 +116,6 @@ env: - ZAMMAD_RAILS_PORT=3000 - ZAMMAD_WEBSOCKET_PORT=6042 services: - - postgres + - postgres:13 before_install: contrib/packager.io/preinstall.sh after_install: contrib/packager.io/postinstall.sh From 798d45b299d7fd4cd99ccffc0fa6bec80eea0381 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Mon, 4 Oct 2021 10:16:46 +0100 Subject: [PATCH 22/65] Maintenance: Add another reload to setup new js for core workflow. --- ...20211004111501_reload_after_core_workflow_again.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 db/migrate/20211004111501_reload_after_core_workflow_again.rb diff --git a/db/migrate/20211004111501_reload_after_core_workflow_again.rb b/db/migrate/20211004111501_reload_after_core_workflow_again.rb new file mode 100644 index 000000000..751d6693e --- /dev/null +++ b/db/migrate/20211004111501_reload_after_core_workflow_again.rb @@ -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 From 32a23f9e8bc71219611b60ebad1c4ad8e1b95cb5 Mon Sep 17 00:00:00 2001 From: Mantas Masalskis Date: Mon, 4 Oct 2021 15:42:35 +0200 Subject: [PATCH 23/65] Fixes #3783 - Improve contrasts in answer search for articles --- app/assets/stylesheets/zammad.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index a4e80733d..c82d33bef 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -8447,6 +8447,10 @@ footer { .dropdown li.with-category, .dropdown.dropdown--actions li.with-category { line-height: 19.5px; + + small { + color: #fff !important; + } } .dropdown.dropdown--actions li.with-category { From ffa8814d0243e2365b982edcb19acbd10cb2f249 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Mon, 4 Oct 2021 15:47:30 +0200 Subject: [PATCH 24/65] Fixes #3779 - Core Workflow: Add organization condition attributes for object User. --- .../core_workflow_condition.coffee | 2 +- spec/models/core_workflow_spec.rb | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee index 10634e7a4..6cedd5b90 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee @@ -23,7 +23,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel organization: name: 'Organization' model: 'Organization' - model_show: ['Organization'] + model_show: ['User', 'Organization'] 'customer.organization': name: 'Organization' model: 'Organization' diff --git a/spec/models/core_workflow_spec.rb b/spec/models/core_workflow_spec.rb index 92c94f933..a98241f7a 100644 --- a/spec/models/core_workflow_spec.rb +++ b/spec/models/core_workflow_spec.rb @@ -1678,4 +1678,43 @@ RSpec.describe CoreWorkflow, type: :model do end end end + + describe 'Core Workflow: Add organization condition attributes for object User #3779' do + let(:organization) { create(:organization, note: 'hello') } + let!(:base_payload) do + { + 'event' => 'core_workflow', + 'request_id' => 'default', + 'class_name' => 'User', + 'screen' => 'create', + 'params' => {}, + } + end + let!(:workflow) do + create(:core_workflow, + object: 'User', + condition_selected: { + 'organization.note': { + operator: 'is', + value: 'hello', + }, + }) + end + + context 'when new user has no organization' do + it 'does not match the workflow' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + + context 'when new user is part of the organization' do + let(:payload) do + base_payload.merge('params' => { 'organization_id' => organization.id.to_s }) + end + + it 'does match the workflow' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + end end From 90eca0f1ebea230eb77a338204ca40cc14cbb67c Mon Sep 17 00:00:00 2001 From: Mantas Masalskis Date: Mon, 4 Oct 2021 21:05:32 +0200 Subject: [PATCH 25/65] Fixes #3773 - Inconstant alignment in the listing of attachments/submit button in new article area Fixes #3774 - Broken dialog whiling uploading oversized attachment --- .../controllers/_ui_element/richtext.coffee | 94 ++++++------------ .../controllers/agent_ticket_create.coffee | 10 +- .../ticket_zoom/article_new.coffee | 92 ++++------------- .../app/lib/app_post/html5_upload.coffee | 98 +++++++++++++++++++ .../javascripts/app/lib/base/html5Upload.js | 3 +- .../app/views/generic/attachment.jst.eco | 2 +- app/assets/stylesheets/zammad.scss | 5 + spec/system/ticket/create_spec.rb | 30 ++++++ 8 files changed, 195 insertions(+), 139 deletions(-) create mode 100644 app/assets/javascripts/app/lib/app_post/html5_upload.coffee diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee index e20b1479d..b7b027ae7 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee @@ -6,7 +6,7 @@ class App.UiElement.richtext attribute.value = attribute.value.text item = $( App.view('generic/richtext')(attribute: attribute, toolButtons: @toolButtons) ) - @contenteditable = item.find('[contenteditable]').ce( + item.find('[contenteditable]').ce( mode: attribute.type maxlength: attribute.maxlength buttons: attribute.buttons @@ -21,12 +21,12 @@ class App.UiElement.richtext new App[plugin.controller](params) if attribute.upload - @attachments = [] + attachments = [] item.append( $( App.view('generic/attachment')(attribute: attribute) ) ) - renderFile = (file) => + renderFile = (file) -> item.find('.attachments').append(App.view('generic/attachment_item')(file)) - @attachments.push file + attachments.push file if params && params.attachments for file in params.attachments @@ -46,10 +46,10 @@ class App.UiElement.richtext , form.form_id) # remove items - item.find('.attachments').on('click', '.js-delete', (e) => + item.find('.attachments').on('click', '.js-delete', (e) -> id = $(e.currentTarget).data('id') - @attachments = _.filter( - @attachments, + attachments = _.filter( + attachments, (item) -> return if item.id.toString() is id.toString() item @@ -71,67 +71,35 @@ class App.UiElement.richtext element.empty() ) - @progressBar = item.find('.attachmentUpload-progressBar') - @progressText = item.find('.js-percentage') - @attachmentPlaceholder = item.find('.attachmentPlaceholder') - @attachmentUpload = item.find('.attachmentUpload') - @attachmentsHolder = item.find('.attachments') - @cancelContainer = item.find('.js-cancel') + App.Delay.set( -> + uploader = new App.Html5Upload( + uploadUrl: "#{App.Config.get('api_path')}/attachments" + dropContainer: item.closest('form') + cancelContainer: item.find('.js-cancel') + inputField: item.find('input') + data: + form_id: item.closest('form').find('[name=form_id]').val() - u = => html5Upload.initialize( - uploadUrl: "#{App.Config.get('api_path')}/attachments" - dropContainer: item.closest('form').get(0) - cancelContainer: @cancelContainer - inputField: item.find('input').get(0) - maxSimultaneousUploads: 1, - key: 'File' - data: - form_id: item.closest('form').find('[name=form_id]').val() - onFileAdded: (file) => + onFileStartCallback: -> + item.find('[contenteditable]').trigger('fileUploadStart') - file.on( - onStart: => - @attachmentPlaceholder.addClass('hide') - @attachmentUpload.removeClass('hide') - @cancelContainer.removeClass('hide') - item.find('[contenteditable]').trigger('fileUploadStart') - App.Log.debug 'UiElement.richtext', 'upload start' + onFileCompletedCallback: (response) -> + renderFile(response.data) + item.find('input').val('') + item.find('[contenteditable]').trigger('fileUploadStop', ['completed']) - onAborted: => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - item.find('input').val('') - item.find('[contenteditable]').trigger('fileUploadStop', ['aborted']) + onFileAbortedCallback: -> + item.find('input').val('') + item.find('[contenteditable]').trigger('fileUploadStop', ['aborted']) - # Called after received response from the server - onCompleted: (response) => - response = JSON.parse(response) + attachmentPlaceholder: item.find('.attachmentPlaceholder') + attachmentUpload: item.find('.attachmentUpload') + progressBar: item.find('.attachmentUpload-progressBar') + progressText: item.find('.js-percentage') + ) - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - - # reset progress bar - @progressBar.width(parseInt(0) + '%') - @progressText.text('') - - renderFile(response.data) - item.find('input').val('') - item.find('[contenteditable]').trigger('fileUploadStop', ['completed']) - App.Log.debug 'UiElement.richtext', 'upload complete', response.data - - # Called during upload progress, first parameter - # is decimal value from 0 to 100. - onProgress: (progress, fileSize, uploadedBytes) => - @progressBar.width(parseInt(progress) + '%') - @progressText.text(parseInt(progress)) - # hide cancel on 90% - if parseInt(progress) >= 90 - @cancelContainer.addClass('hide') - App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress) - - ) - ) - App.Delay.set(u, 100, undefined, 'form_upload') + uploader.render() + , 100, undefined, 'form_upload') item diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 5e8a9299a..954aa8859 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller events: 'click .type-tabs .tab': 'changeFormType' 'submit form': 'submit' - 'click .js-cancel': 'cancel' + 'click .form-controls .js-cancel': 'cancel' 'click .js-active-toggle': 'toggleButton' types: { @@ -184,8 +184,11 @@ class App.TicketCreate extends App.Controller @controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template)) changed: => + return true if @hasAttachments() + formCurrent = @formParam( @$('.ticket-create') ) diff = difference(@formDefault, formCurrent) + return false if !diff || _.isEmpty(diff) return true @@ -461,6 +464,9 @@ class App.TicketCreate extends App.Controller params: => params = @formParam(@$('.main form')) + hasAttachments: => + @$('.richtext .attachments .attachment').length > 0 + submit: (e) => e.preventDefault() @@ -563,7 +569,7 @@ class App.TicketCreate extends App.Controller # save ticket, create article # check attachment if article['body'] - if @$('.richtext .attachments .attachment').length < 1 + if !@hasAttachments() matchingWord = App.Utils.checkAttachmentReference(article['body']) if matchingWord if !confirm(App.i18n.translateContent('You use %s in text but no attachment is attached. Do you want to continue?', matchingWord)) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index 53ef10e57..82411fe48 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller @tokanice(@type) - if @defaults.body or @isIE10() + if @defaults.body or @attachments or @isIE10() @openTextarea(null, true) tokanice: (type = 'email') -> @@ -191,82 +191,30 @@ class App.TicketZoomArticleNew extends App.Controller maxlength: 150000 }) - html5Upload.initialize( - uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}" - dropContainer: @$('.article-add').get(0) - cancelContainer: @cancelContainer - inputField: @$('.article-attachment input').get(0) - key: 'File' - maxSimultaneousUploads: 1 - onFileAdded: (file) => + new App.Html5Upload( + uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}" + dropContainer: @$('.article-add') + cancelContainer: @cancelContainer + inputField: @$('.article-attachment input') - file.on( + onFileStartCallback: => + @callbackFileUploadStart?() - onStart: => - @attachmentPlaceholder.addClass('hide') - @attachmentUpload.removeClass('hide') - @cancelContainer.removeClass('hide') + onFileCompletedCallback: (response) => + @attachments.push response.data + @renderAttachment(response.data) + @$('.article-attachment input').val('') - if @callbackFileUploadStart - @callbackFileUploadStart() + @callbackFileUploadStop?() - onAborted: => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - @$('.article-attachment input').val('') + onFileAbortedCallback: => + @callbackFileUploadStop?() - if @callbackFileUploadStop - @callbackFileUploadStop() - - # Called after received response from the server - onCompleted: (response) => - - response = JSON.parse(response) - @attachments.push response.data - - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - - # reset progress bar - @progressBar.width(parseInt(0) + '%') - @progressText.text('') - - @renderAttachment(response.data) - @$('.article-attachment input').val('') - - if @callbackFileUploadStop - @callbackFileUploadStop() - - # Called during upload progress, first parameter - # is decimal value from 0 to 100. - onProgress: (progress, fileSize, uploadedBytes) => - @progressBar.width(parseInt(progress) + '%') - @progressText.text(parseInt(progress)) - # hide cancel on 90% - if parseInt(progress) >= 90 - @cancelContainer.addClass('hide') - - # Called when upload failed - onError: (message) => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - @$('.article-attachment input').val('') - - if @callbackFileUploadStop - @callbackFileUploadStop() - - new App.ControllerModal( - head: 'Upload Failed' - buttonCancel: 'Cancel' - buttonCancelClass: 'btn--danger' - buttonSubmit: false - message: message - shown: true - small: true - container: @el.closest('.content') - ) - ) - ) + attachmentPlaceholder: @attachmentPlaceholder + attachmentUpload: @attachmentUpload + progressBar: @progressBar + progressText: @progressText + ).render() @bindAttachmentDelete() diff --git a/app/assets/javascripts/app/lib/app_post/html5_upload.coffee b/app/assets/javascripts/app/lib/app_post/html5_upload.coffee new file mode 100644 index 000000000..4daca6510 --- /dev/null +++ b/app/assets/javascripts/app/lib/app_post/html5_upload.coffee @@ -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') diff --git a/app/assets/javascripts/app/lib/base/html5Upload.js b/app/assets/javascripts/app/lib/base/html5Upload.js index 40287b23f..72a2e455e 100644 --- a/app/assets/javascripts/app/lib/base/html5Upload.js +++ b/app/assets/javascripts/app/lib/base/html5Upload.js @@ -255,7 +255,7 @@ manager.ajaxUpload(manager.uploadsQueue.shift()); } }; - xhr.abort = function (event) { + xhr.onabort = function (event) { console.log('Upload abort'); // Reduce number of active uploads: @@ -269,6 +269,7 @@ // Triggered when upload fails: xhr.onerror = function () { console.log('Upload failed: ', upload.fileName); + upload.events.onError('Upload failed: ' + upload.fileName); }; // Append additional data if provided: diff --git a/app/assets/javascripts/app/views/generic/attachment.jst.eco b/app/assets/javascripts/app/views/generic/attachment.jst.eco index 2ab26dae8..d2a5e2cc6 100644 --- a/app/assets/javascripts/app/views/generic/attachment.jst.eco +++ b/app/assets/javascripts/app/views/generic/attachment.jst.eco @@ -17,7 +17,7 @@ <%- @T('Uploading') %> (0%) ...
    - <%- @Icon('diagonal-cross') %>
    <%- @T('Cancel Upload') %> + <%- @Icon('diagonal-cross') %><%- @T('Cancel Upload') %>
    diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index c82d33bef..025d2a488 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -7063,6 +7063,11 @@ footer { .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 { diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index debac63c2..2988643d3 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -469,6 +469,36 @@ RSpec.describe 'Ticket Create', type: :system do expect(page).to have_no_selector(:task_with, task_key) end + + it 'asks for confirmation if attachment was added' do + visit 'ticket/create' + + within :active_content do + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + await_empty_ajax_queue + + find('.js-cancel').click + end + + in_modal disappears: false do + expect(page).to have_text 'Tab has changed' + end + end + end + + context 'when uploading attachment' do + it 'shows an error if server throws an error' do + allow(Store).to receive(:add) { raise 'Error' } + visit 'ticket/create' + + within :active_content do + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + end + + in_modal disappears: false do + expect(page).to have_text 'Error' + end + end end context 'when closing taskbar tab for new ticket creation' do From 4c72d5b9d9d9546f06f0440346b16aab70f5c53e Mon Sep 17 00:00:00 2001 From: Mantas Date: Fri, 1 Oct 2021 22:23:20 +0300 Subject: [PATCH 26/65] Fixes #3769 - Usage of inactive object attributes in SLAs will crash admin SLA interface --- app/models/object_manager/attribute.rb | 32 ++++++++++++++------ app/models/report/profile.rb | 1 + spec/models/object_manager/attribute_spec.rb | 8 +++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index 404b5b3d1..806b77055 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -678,7 +678,7 @@ to send no browser reload event, pass false =begin -where attributes are used by triggers, overviews or schedulers +where attributes are used in conditions result = ObjectManager::Attribute.attribute_to_references_hash @@ -696,22 +696,36 @@ where attributes are used by triggers, overviews or schedulers =end def self.attribute_to_references_hash - objects = Trigger.select(:name, :condition) + Overview.select(:name, :condition) + Job.select(:name, :condition) attribute_list = {} - objects.each do |item| - item.condition.each do |condition_key, _condition_attributes| - attribute_list[condition_key] ||= {} - attribute_list[condition_key][item.class.name] ||= [] - next if attribute_list[condition_key][item.class.name].include?(item.name) - attribute_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 + attribute_list end =begin +models that may reference attributes + +=end + + def self.attribute_to_references_hash_objects + Models.all.keys.select { |elem| elem.include? ChecksConditionValidation } + end + +=begin + is certain attribute used by triggers, overviews or schedulers ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name') diff --git a/app/models/report/profile.rb b/app/models/report/profile.rb index 9f8cbc3f5..fc1e8d07a 100644 --- a/app/models/report/profile.rb +++ b/app/models/report/profile.rb @@ -2,6 +2,7 @@ class Report::Profile < ApplicationModel self.table_name = 'report_profiles' + include ChecksConditionValidation validates :name, presence: true store :condition diff --git a/spec/models/object_manager/attribute_spec.rb b/spec/models/object_manager/attribute_spec.rb index 4e71f5b58..03d040947 100644 --- a/spec/models/object_manager/attribute_spec.rb +++ b/spec/models/object_manager/attribute_spec.rb @@ -148,4 +148,12 @@ RSpec.describe ObjectManager::Attribute, type: :model do it { is_expected.to be_valid } end end + + describe 'Class methods:' do + describe '.attribute_to_references_hash_objects' do + it 'returns classes with conditions' do + expect(described_class.attribute_to_references_hash_objects).to match_array [Trigger, Overview, Job, Sla, Report::Profile ] + end + end + end end From 5ddec48643c2f29a19ea2a557934bbcb3f9f26ee Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Mon, 16 Aug 2021 10:14:23 +0200 Subject: [PATCH 27/65] Maintenance: Improved updating of user records. --- app/policies/user_policy.rb | 5 ++++- spec/policies/user_policy_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index bed708ff2..16e286d21 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -13,11 +13,14 @@ class UserPolicy < ApplicationPolicy end def update? + # full access for admins return true if user.permissions?('admin.user') # forbid non-agents to change users return false if !user.permissions?('ticket.agent') - # allow agents to change customers + # allow agents to change customers only + return false if record.permissions?(['admin.user', 'ticket.agent']) + record.permissions?('ticket.customer') end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 1212bfbcb..339fb47ef 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -126,6 +126,21 @@ describe UserPolicy do it { is_expected.to permit_action(:show) } it { is_expected.to forbid_actions(%i[update destroy]) } end + + context 'when record is both admin and customer' do + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Admin').id)) } + + it { is_expected.to permit_action(:show) } + it { is_expected.to forbid_actions(%i[update destroy]) } + end + + context 'when record is both agent and customer' do + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Agent').id)) } + + it { is_expected.to permit_action(:show) } + it { is_expected.to forbid_actions(%i[update destroy]) } + end + end context 'when user is a customer' do @@ -169,5 +184,18 @@ describe UserPolicy do it { is_expected.to permit_action(:show) } it { is_expected.to forbid_actions(%i[update destroy]) } end + + context 'when record is both admin and customer' do + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Admin').id)) } + + it { is_expected.to forbid_actions(%i[show update destroy]) } + end + + context 'when record is both agent and customer' do + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Agent').id)) } + + it { is_expected.to forbid_actions(%i[show update destroy]) } + end + end end From 6602d19dbfe016589409db088126752194904921 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Mon, 23 Aug 2021 16:51:16 +0200 Subject: [PATCH 28/65] Maintenance: Enhanced GitHub and GitLab GraphQL endpoint check --- lib/github/http_client.rb | 6 ++++-- lib/gitlab/http_client.rb | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/github/http_client.rb b/lib/github/http_client.rb index dd582384d..99762c770 100644 --- a/lib/github/http_client.rb +++ b/lib/github/http_client.rb @@ -1,12 +1,14 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ +require 'uri' + class GitHub class HttpClient attr_reader :api_token, :endpoint def initialize(endpoint, api_token) raise 'api_token required' if api_token.blank? - raise 'endpoint required' if endpoint.blank? + raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank? @api_token = api_token @endpoint = endpoint @@ -30,7 +32,7 @@ class GitHub if !response.success? 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 response.data diff --git a/lib/gitlab/http_client.rb b/lib/gitlab/http_client.rb index f2a247537..76338e204 100644 --- a/lib/gitlab/http_client.rb +++ b/lib/gitlab/http_client.rb @@ -1,12 +1,14 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ +require 'uri' + class GitLab class HttpClient attr_reader :api_token, :endpoint def initialize(endpoint, api_token) raise 'api_token required' if api_token.blank? - raise 'endpoint required' if endpoint.blank? + raise 'endpoint required' if endpoint.blank? || endpoint.exclude?('/graphql') || endpoint.scan(URI::DEFAULT_PARSER.make_regexp).blank? @api_token = api_token @endpoint = endpoint @@ -30,7 +32,7 @@ class GitLab if !response.success? 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 response.data From 0f5807d6feb4fe67e4746df609ce9a312b388a67 Mon Sep 17 00:00:00 2001 From: Martin Gruner Date: Wed, 8 Sep 2021 14:31:42 +0200 Subject: [PATCH 29/65] Maintenance: Improved updating of user records in the front end. --- app/assets/javascripts/app/models/user.coffee | 5 +- spec/system/manage/users_spec.rb | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/models/user.coffee b/app/assets/javascripts/app/models/user.coffee index f1ed97298..423035030 100644 --- a/app/assets/javascripts/app/models/user.coffee +++ b/app/assets/javascripts/app/models/user.coffee @@ -344,9 +344,12 @@ class App.User extends App.Model @sameOrganization?(requester) isChangeableBy: (requester) -> + # full access for admins 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') + # allow agents to change customers only + return false if @permission(['admin.user', 'ticket.agent']) @permission('ticket.customer') isDeleteableBy: (requester) -> diff --git a/spec/system/manage/users_spec.rb b/spec/system/manage/users_spec.rb index 8072ba604..fe10293a9 100644 --- a/spec/system/manage/users_spec.rb +++ b/spec/system/manage/users_spec.rb @@ -110,4 +110,73 @@ RSpec.describe 'Manage > Users', type: :system do end end end + + describe 'check user edit permissions', authenticated_as: -> { user } do + + shared_examples 'user permission' do |allow| + it(allow ? 'allows editing' : 'forbids editing') do + visit "#user/profile/#{record.id}" + find('.js-action .icon-arrow-down').click + selector = '.js-action [data-type="edit"]' + expect(page).to(allow ? have_css(selector) : have_no_css(selector)) + end + end + + context 'when admin tries to change admin' do + let(:user) { create(:admin) } + let(:record) { create(:admin) } + + include_examples 'user permission', true + end + + context 'when admin tries to change agent' do + let(:user) { create(:admin) } + let(:record) { create(:agent) } + + include_examples 'user permission', true + end + + context 'when admin tries to change customer' do + let(:user) { create(:admin) } + let(:record) { create(:customer) } + + include_examples 'user permission', true + end + + context 'when agent tries to change admin' do + let(:user) { create(:agent) } + let(:record) { create(:admin) } + + include_examples 'user permission', false + end + + context 'when agent tries to change agent' do + let(:user) { create(:agent) } + let(:record) { create(:agent) } + + include_examples 'user permission', false + end + + context 'when agent tries to change customer' do + let(:user) { create(:agent) } + let(:record) { create(:customer) } + + include_examples 'user permission', true + end + + context 'when agent tries to change customer who is also admin' do + let(:user) { create(:agent) } + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Admin').id)) } + + include_examples 'user permission', false + end + + context 'when agent tries to change customer who is also agent' do + let(:user) { create(:agent) } + let(:record) { create(:customer, role_ids: Role.signup_role_ids.push(Role.find_by(name: 'Agent').id)) } + + include_examples 'user permission', false + end + + end end From ecbda834bca4b49f2df325e4162137929531dca2 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Fri, 3 Sep 2021 10:43:45 +0200 Subject: [PATCH 30/65] Maintenance: Improve clipboard handling of website chat --- public/assets/chat/Dockerfile | 16 + public/assets/chat/README.md | 5 + public/assets/chat/build.sh | 8 + public/assets/chat/chat-no-jquery.coffee | 6 +- public/assets/chat/chat-no-jquery.js | 352 +++++++++++----------- public/assets/chat/chat-no-jquery.min.js | 2 +- public/assets/chat/chat.coffee | 4 +- public/assets/chat/chat.css | 3 + public/assets/chat/chat.js | 354 ++++++++++++----------- public/assets/chat/chat.min.js | 2 +- public/assets/chat/docker-entrypoint.sh | 5 + public/assets/chat/gulpfile.js | 6 + public/assets/chat/purify.min.js | 3 + 13 files changed, 415 insertions(+), 351 deletions(-) create mode 100644 public/assets/chat/Dockerfile create mode 100644 public/assets/chat/README.md create mode 100755 public/assets/chat/build.sh create mode 100755 public/assets/chat/docker-entrypoint.sh create mode 100644 public/assets/chat/purify.min.js diff --git a/public/assets/chat/Dockerfile b/public/assets/chat/Dockerfile new file mode 100644 index 000000000..74310e6e9 --- /dev/null +++ b/public/assets/chat/Dockerfile @@ -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"] diff --git a/public/assets/chat/README.md b/public/assets/chat/README.md new file mode 100644 index 000000000..02a5afbb1 --- /dev/null +++ b/public/assets/chat/README.md @@ -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. diff --git a/public/assets/chat/build.sh b/public/assets/chat/build.sh new file mode 100755 index 000000000..70057025e --- /dev/null +++ b/public/assets/chat/build.sh @@ -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 diff --git a/public/assets/chat/chat-no-jquery.coffee b/public/assets/chat/chat-no-jquery.coffee index 2627e6100..616a442bd 100644 --- a/public/assets/chat/chat-no-jquery.coffee +++ b/public/assets/chat/chat-no-jquery.coffee @@ -762,7 +762,11 @@ do(window) -> console.log('p', docType, text) if docType is 'html' 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 htmlTmp = text regex = new RegExp('<(/w|w)\:[A-Za-z]') diff --git a/public/assets/chat/chat-no-jquery.js b/public/assets/chat/chat-no-jquery.js index eed63bd55..58c6e9764 100644 --- a/public/assets/chat/chat-no-jquery.js +++ b/public/assets/chat/chat-no-jquery.js @@ -1,7 +1,7 @@ if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } -window.zammadChatTemplates["agent"] = function(__obj) { +window.zammadChatTemplates["agent"] = function (__obj) { if (!__obj) __obj = {}; var __out = [], __capture = function(callback) { var out = __out, result; @@ -59,169 +59,9 @@ window.zammadChatTemplates["agent"] = function(__obj) { return __out.join(''); }; -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["chat"] = function(__obj) { - if (!__obj) __obj = {}; - var __out = [], __capture = function(callback) { - var out = __out, result; - __out = []; - callback.call(this); - result = __out.join(''); - __out = out; - return __safe(result); - }, __sanitize = function(value) { - if (value && value.ecoSafe) { - return value; - } else if (typeof value !== 'undefined' && value != null) { - return __escape(value); - } else { - return ''; - } - }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; - __safe = __obj.safe = function(value) { - if (value && value.ecoSafe) { - return value; - } else { - if (!(typeof value !== 'undefined' && value != null)) value = ''; - var result = new String(value); - result.ecoSafe = true; - return result; - } - }; - if (!__escape) { - __escape = __obj.escape = function(value) { - return ('' + value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - }; - } - (function() { - (function() { - __out.push('
    \n
    \n
    \n \n \n \n \n \n
    \n
    \n
    \n
    \n \n '); - - __out.push(this.T(this.title)); - - __out.push('\n
    \n
    \n
    \n \n
    \n
    \n
    \n \n
    \n
    '); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["customer_timeout"] = function(__obj) { - if (!__obj) __obj = {}; - var __out = [], __capture = function(callback) { - var out = __out, result; - __out = []; - callback.call(this); - result = __out.join(''); - __out = out; - return __safe(result); - }, __sanitize = function(value) { - if (value && value.ecoSafe) { - return value; - } else if (typeof value !== 'undefined' && value != null) { - return __escape(value); - } else { - return ''; - } - }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; - __safe = __obj.safe = function(value) { - if (value && value.ecoSafe) { - return value; - } else { - if (!(typeof value !== 'undefined' && value != null)) value = ''; - var result = new String(value); - result.ecoSafe = true; - return result; - } - }; - if (!__escape) { - __escape = __obj.escape = function(value) { - return ('' + value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - }; - } - (function() { - (function() { - __out.push('
    \n '); - - if (this.agent) { - __out.push('\n '); - __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation with %s got closed.', this.delay, this.agent)); - __out.push('\n '); - } else { - __out.push('\n '); - __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation got closed.', this.delay)); - __out.push('\n '); - } - - __out.push('\n
    \n
    '); - - __out.push(this.T('Start new conversation')); - - __out.push('
    \n
    '); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; +/*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),U=a(/^data-[\-\w.\u00B7-\uFFFF]/),j=a(/^aria-[\-\w]+$/),B=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),P=a(/^(?:\w+script|data):/i),W=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),G="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function q(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.1",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,f=t.NamedNodeMap,x=void 0===f?t.NamedNodeMap||t.MozNamedAttrMap:f,Y=t.Text,X=t.Comment,$=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=N(J,"cloneNode"),ee=N(J,"nextSibling"),te=N(J,"childNodes"),ne=N(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe&&ze?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,fe=r.importNode,me={};try{me=w(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==me;var pe=z,ge=H,he=U,ye=j,ve=P,be=W,Te=B,Ae=null,xe=S({},[].concat(q(k),q(E),q(D),q(R),q(M))),Se=null,we=S({},[].concat(q(L),q(F),q(I),q(C))),Ne=null,ke=null,Ee=!0,De=!0,Oe=!1,Re=!1,_e=!1,Me=!1,Le=!1,Fe=!1,Ie=!1,Ce=!0,ze=!1,He=!0,Ue=!0,je=!1,Be={},Pe=null,We=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ge=null,qe=S({},["audio","video","img","source","image","track"]),Ke=null,Ve=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ye="http://www.w3.org/1998/Math/MathML",Xe="http://www.w3.org/2000/svg",$e="http://www.w3.org/1999/xhtml",Ze=$e,Je=!1,Qe=null,et=o.createElement("form"),tt=function(e){Qe&&Qe===e||(e&&"object"===(void 0===e?"undefined":G(e))||(e={}),e=w(e),Ae="ALLOWED_TAGS"in e?S({},e.ALLOWED_TAGS):xe,Se="ALLOWED_ATTR"in e?S({},e.ALLOWED_ATTR):we,Ke="ADD_URI_SAFE_ATTR"in e?S(w(Ve),e.ADD_URI_SAFE_ATTR):Ve,Ge="ADD_DATA_URI_TAGS"in e?S(w(qe),e.ADD_DATA_URI_TAGS):qe,Pe="FORBID_CONTENTS"in e?S({},e.FORBID_CONTENTS):We,Ne="FORBID_TAGS"in e?S({},e.FORBID_TAGS):{},ke="FORBID_ATTR"in e?S({},e.FORBID_ATTR):{},Be="USE_PROFILES"in e&&e.USE_PROFILES,Ee=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Re=e.SAFE_FOR_TEMPLATES||!1,_e=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Ie=e.RETURN_DOM_FRAGMENT||!1,Ce=!1!==e.RETURN_DOM_IMPORT,ze=e.RETURN_TRUSTED_TYPE||!1,Le=e.FORCE_BODY||!1,He=!1!==e.SANITIZE_DOM,Ue=!1!==e.KEEP_CONTENT,je=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||$e,Re&&(De=!1),Ie&&(Fe=!0),Be&&(Ae=S({},[].concat(q(M))),Se=[],!0===Be.html&&(S(Ae,k),S(Se,L)),!0===Be.svg&&(S(Ae,E),S(Se,F),S(Se,C)),!0===Be.svgFilters&&(S(Ae,D),S(Se,F),S(Se,C)),!0===Be.mathMl&&(S(Ae,R),S(Se,I),S(Se,C))),e.ADD_TAGS&&(Ae===xe&&(Ae=w(Ae)),S(Ae,e.ADD_TAGS)),e.ADD_ATTR&&(Se===we&&(Se=w(Se)),S(Se,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&S(Ke,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(Pe===We&&(Pe=w(Pe)),S(Pe,e.FORBID_CONTENTS)),Ue&&(Ae["#text"]=!0),_e&&S(Ae,["html","head","body"]),Ae.table&&(S(Ae,["tbody"]),delete Ne.tbody),i&&i(e),Qe=e)},nt=S({},["mi","mo","mn","ms","mtext"]),rt=S({},["foreignobject","desc","title","annotation-xml"]),ot=S({},E);S(ot,D),S(ot,O);var it=S({},R);S(it,_);var at=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:$e,tagName:"template"});var n=g(e.tagName),r=g(t.tagName);if(e.namespaceURI===Xe)return t.namespaceURI===$e?"svg"===n:t.namespaceURI===Ye?"svg"===n&&("annotation-xml"===r||nt[r]):Boolean(ot[n]);if(e.namespaceURI===Ye)return t.namespaceURI===$e?"math"===n:t.namespaceURI===Xe?"math"===n&&rt[r]:Boolean(it[n]);if(e.namespaceURI===$e){if(t.namespaceURI===Xe&&!rt[r])return!1;if(t.namespaceURI===Ye&&!nt[r])return!1;var o=S({},["title","style","font","a","script"]);return!it[n]&&(o[n]||!ot[n])}return!1},lt=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},ct=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||Ie)try{lt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},st=function(e){var t=void 0,n=void 0;if(Le)e=""+e;else{var r=h(e,/^[\r\n\t ]+/);n=r&&r[0]}var i=oe?oe.createHTML(e):e;if(Ze===$e)try{t=(new $).parseFromString(i,"text/html")}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===$e?ue.call(t,_e?"html":"body")[0]:_e?t.documentElement:a},ut=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},ft=function(e){return!(e instanceof Y||e instanceof X)&&!("string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof x&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute&&"string"==typeof e.namespaceURI&&"function"==typeof e.insertBefore)},mt=function(e){return"object"===(void 0===c?"undefined":G(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":G(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},dt=function(e,t,r){de[e]&&m(de[e],(function(e){e.call(n,t,r,Qe)}))},pt=function(e){var t=void 0;if(dt("beforeSanitizeElements",e,null),ft(e))return lt(e),!0;if(h(e.nodeName,/[\u0080-\uFFFF]/))return lt(e),!0;var r=g(e.nodeName);if(dt("uponSanitizeElement",e,{tagName:r,allowedTags:Ae}),!mt(e.firstElementChild)&&(!mt(e.content)||!mt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return lt(e),!0;if("select"===r&&T(/