From eb559755eb598b395a80884c4adb20e8625af206 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Sat, 12 Jan 2019 12:18:28 +0100 Subject: [PATCH 01/10] Stabilize tests: JS AJAX UnitTest is flanky in single process Capybara test. --- public/assets/tests/core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/assets/tests/core.js b/public/assets/tests/core.js index 3c9401487..1dc17249f 100644 --- a/public/assets/tests/core.js +++ b/public/assets/tests/core.js @@ -63,7 +63,7 @@ App.Ajax.request({ // ajax parallel App.Ajax.request({ type: 'GET', - url: '/tests/wait/2', + url: '/tests/wait/3', success: function (data) { test( "ajax - parallel - ajax get 200 1/2", function() { From f0caad9725e54617819ac0c913dc493911679ade Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Sat, 12 Jan 2019 12:19:00 +0100 Subject: [PATCH 02/10] Stabilize tests: Unstable tests require recurring examination. --- test/browser/admin_channel_email_test.rb | 1 + test/browser/agent_ticket_auto_assignment_test.rb | 1 + test/browser/integration_cti_test.rb | 1 + test/browser/integration_sipgate_test.rb | 1 + test/browser/keyboard_shortcuts_test.rb | 1 + 5 files changed, 5 insertions(+) diff --git a/test/browser/admin_channel_email_test.rb b/test/browser/admin_channel_email_test.rb index a153b663d..89bf13ad6 100644 --- a/test/browser/admin_channel_email_test.rb +++ b/test/browser/admin_channel_email_test.rb @@ -72,6 +72,7 @@ class AdminChannelEmailTest < TestCase click(css: '.content.active .js-channelDelete') sleep 2 + # flanky click(css: '.modal .js-submit') sleep 2 end diff --git a/test/browser/agent_ticket_auto_assignment_test.rb b/test/browser/agent_ticket_auto_assignment_test.rb index 693b0887b..56ae1e49d 100644 --- a/test/browser/agent_ticket_auto_assignment_test.rb +++ b/test/browser/agent_ticket_auto_assignment_test.rb @@ -113,6 +113,7 @@ class AgentTicketAutoAssignmentTest < TestCase # define auto assignment exception click(css: 'a[href="#manage"]') + # flanky click(css: '.content.active a[href="#settings/ticket"]') click(css: '.content.active a[href="#auto_assignment"]') click(css: '.content.active .js-select.js-option[title="master@example.com"]') diff --git a/test/browser/integration_cti_test.rb b/test/browser/integration_cti_test.rb index 67f26a584..6ce8ae43f 100644 --- a/test/browser/integration_cti_test.rb +++ b/test/browser/integration_cti_test.rb @@ -50,6 +50,7 @@ class IntegrationCtiTest < TestCase Net::HTTP.post_form(url, params.merge(event: 'newCall')) Net::HTTP.post_form(url, params.merge(event: 'hangup')) + # flanky watch_for( css: '.js-phoneMenuItem .counter', value: (call_counter + 1).to_s, diff --git a/test/browser/integration_sipgate_test.rb b/test/browser/integration_sipgate_test.rb index 3c776d403..7951f352c 100644 --- a/test/browser/integration_sipgate_test.rb +++ b/test/browser/integration_sipgate_test.rb @@ -43,6 +43,7 @@ class IntegrationSipgateTest < TestCase Net::HTTP.post_form(url, params.merge(event: 'newCall')) Net::HTTP.post_form(url, params.merge(event: 'hangup')) + # flanky watch_for( css: '.js-phoneMenuItem .counter', value: (call_counter + 1).to_s, diff --git a/test/browser/keyboard_shortcuts_test.rb b/test/browser/keyboard_shortcuts_test.rb index 21cac9db9..1a1088338 100644 --- a/test/browser/keyboard_shortcuts_test.rb +++ b/test/browser/keyboard_shortcuts_test.rb @@ -203,6 +203,7 @@ class KeyboardShortcutsTest < TestCase ) sleep 5 shortcut(key: 'a') + # flanky watch_for( css: '.js-notificationsContainer', value: 'Test Ticket for Shortcuts II', From 6870c11103df41b43ba8031adc387a68f925a3e5 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 1 Jan 2019 18:53:50 +0100 Subject: [PATCH 03/10] Updated omniauth-google-oauth2 gem - due to Google Plus (G+) sunsetting in Q1 2019 (https://github.com/zquestz/omniauth-google-oauth2/issues/340). --- Gemfile.lock | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f51d867be..c7337b845 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,7 +174,7 @@ GEM railties (>= 3.0.0) faker (1.9.1) i18n (>= 0.7) - faraday (0.12.2) + faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday-http-cache (2.0.0) faraday (~> 0.8) @@ -212,7 +212,7 @@ GEM guard guard-compat (~> 1.1) hashdiff (0.3.7) - hashie (3.5.6) + hashie (3.6.0) htmlentities (4.3.4) http (3.3.0) addressable (~> 2.3) @@ -235,7 +235,7 @@ GEM interception (0.5) jaro_winkler (1.5.1) json (2.1.0) - jwt (1.5.6) + jwt (2.1.0) kgio (2.11.0) koala (3.0.0) addressable @@ -265,7 +265,7 @@ GEM mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) - multi_json (1.12.2) + multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) mysql2 (0.4.10) @@ -282,16 +282,16 @@ GEM nenv (~> 0.1) shellany (~> 0.0) oauth (0.5.3) - oauth2 (1.4.0) - faraday (>= 0.8, < 0.13) - jwt (~> 1.0) + oauth2 (1.4.1) + faraday (>= 0.8, < 0.16.0) + jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) octokit (4.7.0) sawyer (~> 0.8.0, >= 0.5.3) - omniauth (1.7.1) - hashie (>= 3.4.6, < 3.6.0) + omniauth (1.9.0) + hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) @@ -301,11 +301,10 @@ GEM omniauth-gitlab (1.0.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.5.2) - jwt (~> 1.5) - multi_json (~> 1.3) + omniauth-google-oauth2 (0.6.0) + jwt (>= 2.0) omniauth (>= 1.1.1) - omniauth-oauth2 (>= 1.3.1) + omniauth-oauth2 (>= 1.5) omniauth-linkedin-oauth2 (0.2.5) omniauth (~> 1.0) omniauth-oauth2 @@ -315,9 +314,9 @@ GEM omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.4.0) - oauth2 (~> 1.0) - omniauth (~> 1.2) + omniauth-oauth2 (1.6.0) + oauth2 (~> 1.1) + omniauth (~> 1.9) omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack From 10110e03cb2de1f8cdfb192a8f46090e5b19ef08 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 15 Jan 2019 13:17:13 +0100 Subject: [PATCH 04/10] Refactoring: Rails/SkipsModelValidations cop is unnecessarily active for RSpec files. --- .rubocop.yml | 2 ++ spec/db/migrate/check_for_object_attributes_spec.rb | 5 +---- spec/requests/ticket_spec.rb | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index cc625b5b5..ae1736c76 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -175,6 +175,8 @@ Rails/SkipsModelValidations: Enabled: true Exclude: - test/**/* + - "**/*_spec.rb" + - "**/*_examples.rb" Style/ClassAndModuleChildren: Description: 'Checks style of children classes and modules.' diff --git a/spec/db/migrate/check_for_object_attributes_spec.rb b/spec/db/migrate/check_for_object_attributes_spec.rb index a4bb3c433..6dd8ba0be 100644 --- a/spec/db/migrate/check_for_object_attributes_spec.rb +++ b/spec/db/migrate/check_for_object_attributes_spec.rb @@ -102,11 +102,8 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do type: 'text', # to trigger a #save in the migration. maxlength: 255, } - - # rubocop:disable Rails/SkipsModelValidations create(:object_manager_attribute_text) .update_columns(data_option: wrong) - # rubocop:enable Rails/SkipsModelValidations expect { migrate }.not_to raise_error end @@ -117,7 +114,7 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do context 'for interger attributes' do it 'missing :min and :max' do attribute = create(:object_manager_attribute_integer) - attribute.update_columns(data_option: {}) # rubocop:disable Rails/SkipsModelValidations + attribute.update_columns(data_option: {}) expect { migrate }.not_to raise_error diff --git a/spec/requests/ticket_spec.rb b/spec/requests/ticket_spec.rb index 79eebf696..a9fecc5db 100644 --- a/spec/requests/ticket_spec.rb +++ b/spec/requests/ticket_spec.rb @@ -2120,7 +2120,7 @@ RSpec.describe 'Ticket', type: :request do travel 2.minutes ticket3 travel 2.minutes - ticket2.touch # rubocop:disable Rails/SkipsModelValidations + ticket2.touch end # https://github.com/zammad/zammad/issues/2296 From 3a5d597ab9b952e34fe9f0c8357a3c37f9189881 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 15 Jan 2019 13:21:04 +0100 Subject: [PATCH 05/10] Adding ActiveJob RSpec helper for using Test queue adapter. --- app/jobs/application_job.rb | 3 +++ spec/support/active_job.rb | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 spec/support/active_job.rb diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 9a41ff518..cab2f8af4 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -11,6 +11,9 @@ class ApplicationJob < ActiveJob::Base # until we resolve this dependency. around_enqueue do |job, block| block.call.tap do |delayed_job| + # skip test adapter + break if delayed_job.is_a?(Array) + delayed_job.update!(attempts: job.executions) end end diff --git a/spec/support/active_job.rb b/spec/support/active_job.rb new file mode 100644 index 000000000..af2647cb8 --- /dev/null +++ b/spec/support/active_job.rb @@ -0,0 +1,38 @@ +module ZammadActiveJobHelper + + delegate :enqueued_jobs, :performed_jobs, to: :queue_adapter + + def queue_adapter + ::ActiveJob::Base.queue_adapter + end + + def clear_jobs + enqueued_jobs.clear + performed_jobs.clear + end +end + +RSpec.configure do |config| + + activate_for = { + type: :job, # actual Job examples + performs_jobs: true, # examples performing Jobs + } + + activate_for.each do |key, value| + config.include ZammadActiveJobHelper, key => value + config.include RSpec::Rails::JobExampleGroup, key => value + + config.around(:each, key => value) do |example| + + default_queue_adapter = ::ActiveJob::Base.queue_adapter + ::ActiveJob::Base.queue_adapter = :test + + clear_jobs + + example.run + + ::ActiveJob::Base.queue_adapter = default_queue_adapter + end + end +end From a84edbe764f8e11aa94370c99fe9af9861dd465b Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 15 Jan 2019 13:23:16 +0100 Subject: [PATCH 06/10] Refactoring: Infered spec type from file location applies unwanted support files which cause tests (ApplicationJob) to fail. --- spec/jobs/application_job_spec.rb | 2 +- spec/rails_helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/jobs/application_job_spec.rb b/spec/jobs/application_job_spec.rb index 27b0a5f9d..65f5bb76a 100644 --- a/spec/jobs/application_job_spec.rb +++ b/spec/jobs/application_job_spec.rb @@ -9,7 +9,7 @@ class FailingTestJob < ApplicationJob end end -RSpec.describe ApplicationJob, type: :job do +RSpec.describe ApplicationJob do it 'syncs ActiveJob#executions to Delayed::Job#attempts' do FailingTestJob.perform_later diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 81792f90d..6f47b5b4f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -64,7 +64,7 @@ RSpec.configure do |config| # # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs - config.infer_spec_type_from_file_location! + # config.infer_spec_type_from_file_location! # Filter lines from Rails gems in backtraces. config.filter_rails_from_backtrace! From a211d20db988eff21784f752fd615e3a7a789fad Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 15 Jan 2019 13:32:14 +0100 Subject: [PATCH 07/10] Refactoring: Migrated BackgroundJobSearchIndex (Delayed::Job) to SearchIndexJob (ActiveJob). Refactoring: Enable MonitoringController to handle ActiveJob background jobs. --- app/controllers/monitoring_controller.rb | 25 +++++++++++--- .../jobs/search_index_job.rb | 15 +++------ .../concerns/has_search_index_backend.rb | 2 +- spec/jobs/search_index_job_spec.rb | 24 ++++++++++++++ .../has_search_index_backend_examples.rb | 33 +++++++++++++++++++ spec/models/organization_spec.rb | 2 ++ spec/requests/integration/monitoring_spec.rb | 16 ++++----- 7 files changed, 92 insertions(+), 25 deletions(-) rename lib/background_job_search_index.rb => app/jobs/search_index_job.rb (64%) create mode 100644 spec/jobs/search_index_job_spec.rb create mode 100644 spec/models/concerns/has_search_index_backend_examples.rb diff --git a/app/controllers/monitoring_controller.rb b/app/controllers/monitoring_controller.rb index 7d4aa3172..8f2ee12b4 100644 --- a/app/controllers/monitoring_controller.rb +++ b/app/controllers/monitoring_controller.rb @@ -99,11 +99,26 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX issues.push "#{count_failed_jobs} failing background jobs" end - listed_failed_jobs = failed_jobs.select(:handler, :attempts).limit(10) - sorted_failed_jobs = listed_failed_jobs.group_by(&:name).sort_by { |_handler, entries| entries.length }.reverse.to_h - sorted_failed_jobs.each_with_index do |(name, jobs), index| - attempts = jobs.map(&:attempts).sum - issues.push "Failed to run background job ##{index += 1} '#{name}' #{jobs.count} time(s) with #{attempts} attempt(s)." + handler_attempts_map = {} + failed_jobs.order(:created_at).limit(10).each do |job| + + job_name = if job.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'.freeze + job.payload_object.job_data['job_class'] + else + job.name + end + + handler_attempts_map[job_name] ||= { + count: 0, + attempts: 0, + } + + handler_attempts_map[job_name][:count] += 1 + handler_attempts_map[job_name][:attempts] += job.attempts + end + + Hash[handler_attempts_map.sort].each_with_index do |(job_name, job_data), index| + issues.push "Failed to run background job ##{index + 1} '#{job_name}' #{job_data[:count]} time(s) with #{job_data[:attempts]} attempt(s)." end # job count check diff --git a/lib/background_job_search_index.rb b/app/jobs/search_index_job.rb similarity index 64% rename from lib/background_job_search_index.rb rename to app/jobs/search_index_job.rb index 9fa062efb..96e3c2c64 100644 --- a/lib/background_job_search_index.rb +++ b/app/jobs/search_index_job.rb @@ -1,11 +1,11 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -class BackgroundJobSearchIndex - def initialize(object, o_id) +class SearchIndexJob < ApplicationJob + + retry_on StandardError, attempts: 20 + + def perform(object, o_id) @object = object @o_id = o_id - end - def perform record = @object.constantize.lookup(id: @o_id) return if !exists?(record) @@ -20,9 +20,4 @@ class BackgroundJobSearchIndex Rails.logger.info "Can't index #{@object}.lookup(id: #{@o_id}), no such record found" false end - - def max_attempts - 20 - end - end diff --git a/app/models/concerns/has_search_index_backend.rb b/app/models/concerns/has_search_index_backend.rb index 09622f472..1f09d77fd 100644 --- a/app/models/concerns/has_search_index_backend.rb +++ b/app/models/concerns/has_search_index_backend.rb @@ -24,7 +24,7 @@ update search index, if configured - will be executed automatically # start background job to transfer data to search index return true if !SearchIndexBackend.enabled? - Delayed::Job.enqueue(BackgroundJobSearchIndex.new(self.class.to_s, id)) + SearchIndexJob.perform_later(self.class.to_s, id) true end diff --git a/spec/jobs/search_index_job_spec.rb b/spec/jobs/search_index_job_spec.rb new file mode 100644 index 000000000..2d037fb64 --- /dev/null +++ b/spec/jobs/search_index_job_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe SearchIndexJob, type: :job do + + it 'calls search_index_update_backend on matching record' do + user = create(:user) + expect(::User).to receive(:lookup).with(id: user.id).and_return(user) + expect(user).to receive(:search_index_update_backend) + + described_class.perform_now('User', user.id) + end + + it "doesn't perform for non existing records" do + id = 9999 + expect(::User).to receive(:lookup).with(id: id).and_return(nil) + described_class.perform_now('User', id) + end + + it 'retries on exception' do + expect(::User).to receive(:lookup).and_raise(RuntimeError) + described_class.perform_now('User', 1) + expect(SearchIndexJob).to have_been_enqueued + end +end diff --git a/spec/models/concerns/has_search_index_backend_examples.rb b/spec/models/concerns/has_search_index_backend_examples.rb new file mode 100644 index 000000000..8bb1c6a9e --- /dev/null +++ b/spec/models/concerns/has_search_index_backend_examples.rb @@ -0,0 +1,33 @@ +RSpec.shared_examples 'HasSearchIndexBackend' do |indexed_factory:| + + context '#search_index_update', performs_jobs: true do + subject { create(indexed_factory) } + + before(:each) do + allow(SearchIndexBackend).to receive(:enabled?).and_return(true) + end + + context 'record indexing' do + + before(:each) do + expect(subject).to be_present + end + + it 'indexes on create' do + expect(SearchIndexJob).to have_been_enqueued + end + + it 'indexes on update' do + clear_jobs + subject.update(note: 'Updated') + expect(SearchIndexJob).to have_been_enqueued + end + + it 'indexes on touch' do + clear_jobs + subject.touch + expect(SearchIndexJob).to have_been_enqueued + end + end + end +end diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 1ffce6605..af3718b65 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' require 'models/concerns/can_lookup_examples' +require 'models/concerns/has_search_index_backend_examples' RSpec.describe Organization do include_examples 'CanLookup' + include_examples 'HasSearchIndexBackend', indexed_factory: :organization context '.where_or_cis' do diff --git a/spec/requests/integration/monitoring_spec.rb b/spec/requests/integration/monitoring_spec.rb index ef24bb0c5..513351901 100644 --- a/spec/requests/integration/monitoring_spec.rb +++ b/spec/requests/integration/monitoring_spec.rb @@ -372,7 +372,6 @@ RSpec.describe 'Monitoring', type: :request do end it 'does check health false' do - channel = Channel.find_by(active: true) channel.status_in = 'ok' channel.status_out = 'error' @@ -423,7 +422,7 @@ RSpec.describe 'Monitoring', type: :request do # health_check - scheduler job count travel 2.seconds 8001.times do - Delayed::Job.enqueue( BackgroundJobSearchIndex.new('Ticket', 1)) + SearchIndexJob.perform_later('Ticket', 1) end Scheduler.where(active: true).each do |local_scheduler| local_scheduler.last_run = Time.zone.now @@ -520,7 +519,6 @@ RSpec.describe 'Monitoring', type: :request do end it 'does check failed delayed job', db_strategy: :reset do - # disable elasticsearch prev_es_config = Setting.get('es_url') Setting.set('es_url', 'http://127.0.0.1:92001') @@ -598,11 +596,11 @@ RSpec.describe 'Monitoring', type: :request do expect(json_response['message']).to be_truthy expect(json_response['issues']).to be_truthy expect(json_response['healthy']).to eq(false) - expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 1 time(s) with 4 attempt(s).") + expect( json_response['message']).to eq("Failed to run background job #1 'SearchIndexJob' 4 time(s) with 4 attempt(s).") # add another job - manual_added = Delayed::Job.enqueue( BackgroundJobSearchIndex.new('Ticket', 1)) - manual_added.update!(attempts: 10) + manual_added = SearchIndexJob.perform_later('Ticket', 1) + Delayed::Job.find(manual_added.provider_job_id).update!(attempts: 10) # health_check get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json @@ -612,7 +610,7 @@ RSpec.describe 'Monitoring', type: :request do expect(json_response['message']).to be_truthy expect(json_response['issues']).to be_truthy expect(json_response['healthy']).to eq(false) - expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).") + expect( json_response['message']).to eq("Failed to run background job #1 'SearchIndexJob' 5 time(s) with 14 attempt(s).") # add another job dummy_class = Class.new do @@ -633,7 +631,7 @@ RSpec.describe 'Monitoring', type: :request do expect(json_response['message']).to be_truthy expect(json_response['issues']).to be_truthy expect(json_response['healthy']).to eq(false) - expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).;Failed to run background job #2 'Object' 1 time(s) with 5 attempt(s).") + expect( json_response['message']).to eq("Failed to run background job #1 'Object' 1 time(s) with 5 attempt(s).;Failed to run background job #2 'SearchIndexJob' 5 time(s) with 14 attempt(s).") # reset settings Setting.set('es_url', prev_es_config) @@ -652,7 +650,7 @@ RSpec.describe 'Monitoring', type: :request do expect(json_response['message']).to be_truthy expect(json_response['issues']).to be_truthy expect(json_response['healthy']).to eq(false) - expect( json_response['message']).to eq("13 failing background jobs;Failed to run background job #1 'Object' 8 time(s) with 40 attempt(s).;Failed to run background job #2 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).") + expect(json_response['message']).to eq("16 failing background jobs;Failed to run background job #1 'Object' 5 time(s) with 25 attempt(s).;Failed to run background job #2 'SearchIndexJob' 5 time(s) with 14 attempt(s).") # cleanup Delayed::Job.delete_all From cfc1bdbb4587b837e2f03a67bce7fae216982742 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 8 Jan 2019 09:17:18 +0100 Subject: [PATCH 08/10] Refactoring: NO_RESET_BEFORE_SUITE is not practical for the general use case of running RSpec multiple times and slows down/decreases DX. --- .gitlab-ci.yml | 2 -- spec/support/reset_system_before_suite.rb | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b54d0f2f3..510c28235 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -366,14 +366,12 @@ browser:build: <<: *test_capybara_definition variables: RAILS_ENV: "test" - NO_RESET_BEFORE_SUITE: "true" BROWSER: "chrome" .variables_capybara_ff_template: &variables_capybara_ff_definition <<: *test_capybara_definition variables: RAILS_ENV: "test" - NO_RESET_BEFORE_SUITE: "true" BROWSER: "firefox" test:browser:core:capybara_chrome_postgresql: diff --git a/spec/support/reset_system_before_suite.rb b/spec/support/reset_system_before_suite.rb index 15636f579..da435cb4c 100644 --- a/spec/support/reset_system_before_suite.rb +++ b/spec/support/reset_system_before_suite.rb @@ -1,6 +1,6 @@ RSpec.configure do |config| config.before(:suite) do - next if ENV['NO_RESET_BEFORE_SUITE'] + next if !ENV['RESET_BEFORE_SUITE'] Rake::Task['zammad:db:reset'].invoke end From 1c55fc0a56c182db82fe2f5e44e38b26f2125b85 Mon Sep 17 00:00:00 2001 From: Billy Zhou Date: Fri, 18 Jan 2019 16:33:07 +0100 Subject: [PATCH 09/10] Fixed #2416 - HTML sanitizer blocks email processing because of an endless loop. --- lib/html_sanitizer.rb | 366 ++++++++++++++++---------------- spec/lib/html_sanitizer_spec.rb | 23 ++ 2 files changed, 211 insertions(+), 178 deletions(-) diff --git a/lib/html_sanitizer.rb b/lib/html_sanitizer.rb index 3b5d8e588..9dfd11718 100644 --- a/lib/html_sanitizer.rb +++ b/lib/html_sanitizer.rb @@ -1,5 +1,7 @@ class HtmlSanitizer LINKABLE_URL_SCHEMES = URI.scheme_list.keys.map(&:downcase) - ['mailto'] + ['tel'] + PROCESSING_TIMEOUT = 10 + UNPROCESSABLE_HTML_MSG = 'This message cannot be displayed due to HTML processing issues. Download the raw message below and open it via an Email client if you still wish to view it.'.freeze =begin @@ -9,198 +11,202 @@ satinize html string based on whiltelist =end - def self.strict(string, external = false) - @fqdn = Setting.get('fqdn') + def self.strict(string, external = false, timeout: true) + Timeout.timeout(timeout ? PROCESSING_TIMEOUT : nil) do + @fqdn = Setting.get('fqdn') - # config - tags_remove_content = Rails.configuration.html_sanitizer_tags_remove_content - tags_quote_content = Rails.configuration.html_sanitizer_tags_quote_content - tags_whitelist = Rails.configuration.html_sanitizer_tags_whitelist - attributes_whitelist = Rails.configuration.html_sanitizer_attributes_whitelist - css_properties_whitelist = Rails.configuration.html_sanitizer_css_properties_whitelist - css_values_blacklist = Rails.application.config.html_sanitizer_css_values_backlist + # config + tags_remove_content = Rails.configuration.html_sanitizer_tags_remove_content + tags_quote_content = Rails.configuration.html_sanitizer_tags_quote_content + tags_whitelist = Rails.configuration.html_sanitizer_tags_whitelist + attributes_whitelist = Rails.configuration.html_sanitizer_attributes_whitelist + css_properties_whitelist = Rails.configuration.html_sanitizer_css_properties_whitelist + css_values_blacklist = Rails.application.config.html_sanitizer_css_values_backlist - # We whitelist yahoo_quoted because Yahoo Mail marks quoted email content using - #
and we rely on this class to identify quoted messages - classes_whitelist = ['js-signatureMarker', 'yahoo_quoted'] - attributes_2_css = %w[width height] + # We whitelist yahoo_quoted because Yahoo Mail marks quoted email content using + #
and we rely on this class to identify quoted messages + classes_whitelist = ['js-signatureMarker', 'yahoo_quoted'] + attributes_2_css = %w[width height] - # remove html comments - string.gsub!(//m, '') + # remove html comments + string.gsub!(//m, '') - scrubber_link = Loofah::Scrubber.new do |node| + scrubber_link = Loofah::Scrubber.new do |node| - # 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(/[,.]$/, '') } # URI::extract captures trailing dots/commas - .reject { |u| u.match?(/^[^:]+:$/) } # URI::extract will match, e.g., 'tel:' + # 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(/[,.]$/, '') } # URI::extract captures trailing dots/commas + .reject { |u| u.match?(/^[^:]+:$/) } # URI::extract will match, e.g., 'tel:' - next if urls.blank? + next if urls.blank? - add_link(node.content, urls, node) - end + add_link(node.content, urls, node) + end - # prepare links - if node['href'] - href = cleanup_target(node['href'], keep_spaces: true) - href_without_spaces = href.gsub(/[[:space:]]/, '') - if external && href_without_spaces.present? && !href_without_spaces.downcase.start_with?('//') && href_without_spaces.downcase !~ %r{^.{1,6}://.+?} - node['href'] = "http://#{node['href']}" - href = node['href'] + # prepare links + if node['href'] + href = cleanup_target(node['href'], keep_spaces: true) href_without_spaces = href.gsub(/[[:space:]]/, '') + if external && href_without_spaces.present? && !href_without_spaces.downcase.start_with?('//') && href_without_spaces.downcase !~ %r{^.{1,6}://.+?} + node['href'] = "http://#{node['href']}" + href = node['href'] + href_without_spaces = href.gsub(/[[:space:]]/, '') + end + + next if !href_without_spaces.downcase.start_with?('http', 'ftp', '//') + + node.set_attribute('href', href) + node.set_attribute('rel', 'nofollow noreferrer noopener') + node.set_attribute('target', '_blank') end - next if !href_without_spaces.downcase.start_with?('http', 'ftp', '//') + if node.name == 'a' && node['href'].blank? + node.replace node.children.to_s + Loofah::Scrubber::STOP + end - node.set_attribute('href', href) - node.set_attribute('rel', 'nofollow noreferrer noopener') - node.set_attribute('target', '_blank') - end - - if node.name == 'a' && node['href'].blank? - node.replace node.children.to_s - Loofah::Scrubber::STOP - end - - # check if href is different to text - if node.name == 'a' && !url_same?(node['href'], node.text) - if node['title'].blank? - node['title'] = node['href'] + # check if href is different to text + if node.name == 'a' && !url_same?(node['href'], node.text) + if node['title'].blank? + node['title'] = node['href'] + end end end - end - scrubber_wipe = Loofah::Scrubber.new do |node| + scrubber_wipe = Loofah::Scrubber.new do |node| - # remove tags with subtree - if tags_remove_content.include?(node.name) - node.remove - Loofah::Scrubber::STOP - end - - # remove tag, insert quoted content - if tags_quote_content.include?(node.name) - string = html_decode(node.content) - text = Nokogiri::XML::Text.new(string, node.document) - node.add_next_sibling(text) - node.remove - Loofah::Scrubber::STOP - end - - # replace tags, keep subtree - if !tags_whitelist.include?(node.name) - node.replace node.children.to_s - Loofah::Scrubber::STOP - end - - # prepare src attribute - if node['src'] - src = cleanup_target(node['src']) - if src =~ /(javascript|livescript|vbscript):/i || src.downcase.start_with?('http', 'ftp', '//') + # remove tags with subtree + if tags_remove_content.include?(node.name) node.remove Loofah::Scrubber::STOP end - end - # clean class / only use allowed classes - if node['class'] - classes = node['class'].gsub(/\t|\n|\r/, '').split(' ') - class_new = '' - classes.each do |local_class| - next if !classes_whitelist.include?(local_class.to_s.strip) - - if class_new != '' - class_new += ' ' - end - class_new += local_class - end - if class_new != '' - node['class'] = class_new - else - node.delete('class') - end - end - - # move style attributes to css attributes - attributes_2_css.each do |key| - next if !node[key] - - if node['style'].blank? - node['style'] = '' - else - node['style'] += ';' - end - value = node[key] - node.delete(key) - next if value.blank? - - value += 'px' if !value.match?(/%|px|em/i) - node['style'] += "#{key}:#{value}" - end - - # clean style / only use allowed style properties - if node['style'] - pears = node['style'].downcase.gsub(/\t|\n|\r/, '').split(';') - style = '' - pears.each do |local_pear| - prop = local_pear.split(':') - next if !prop[0] - - key = prop[0].strip - next if !css_properties_whitelist.include?(node.name) - next if !css_properties_whitelist[node.name].include?(key) - next if css_values_blacklist[node.name]&.include?(local_pear.gsub(/[[:space:]]/, '').strip) - - style += "#{local_pear};" - end - node['style'] = style - if style == '' - node.delete('style') - end - end - - # scan for invalid link content - %w[href style].each do |attribute_name| - next if !node[attribute_name] - - href = cleanup_target(node[attribute_name]) - next if href !~ /(javascript|livescript|vbscript):/i - - node.delete(attribute_name) - end - - # remove attributes if not whitelisted - node.each do |attribute, _value| - attribute_name = attribute.downcase - next if attributes_whitelist[:all].include?(attribute_name) || (attributes_whitelist[node.name]&.include?(attribute_name)) - - node.delete(attribute) - end - - # remove mailto links - if node['href'] - href = cleanup_target(node['href']) - if href =~ /mailto:(.*)$/i - text = Nokogiri::XML::Text.new($1, node.document) + # remove tag, insert quoted content + if tags_quote_content.include?(node.name) + string = html_decode(node.content) + text = Nokogiri::XML::Text.new(string, node.document) node.add_next_sibling(text) node.remove Loofah::Scrubber::STOP end - end - end - new_string = '' - done = true - while done - new_string = Loofah.fragment(string).scrub!(scrubber_wipe).to_s - if string == new_string - done = false - end - string = new_string - end + # replace tags, keep subtree + if !tags_whitelist.include?(node.name) + node.replace node.children.to_s + Loofah::Scrubber::STOP + end - Loofah.fragment(string).scrub!(scrubber_link).to_s + # prepare src attribute + if node['src'] + src = cleanup_target(node['src']) + if src =~ /(javascript|livescript|vbscript):/i || src.downcase.start_with?('http', 'ftp', '//') + node.remove + Loofah::Scrubber::STOP + end + end + + # clean class / only use allowed classes + if node['class'] + classes = node['class'].gsub(/\t|\n|\r/, '').split(' ') + class_new = '' + classes.each do |local_class| + next if !classes_whitelist.include?(local_class.to_s.strip) + + if class_new != '' + class_new += ' ' + end + class_new += local_class + end + if class_new != '' + node['class'] = class_new + else + node.delete('class') + end + end + + # move style attributes to css attributes + attributes_2_css.each do |key| + next if !node[key] + + if node['style'].blank? + node['style'] = '' + else + node['style'] += ';' + end + value = node[key] + node.delete(key) + next if value.blank? + + value += 'px' if !value.match?(/%|px|em/i) + node['style'] += "#{key}:#{value}" + end + + # clean style / only use allowed style properties + if node['style'] + pears = node['style'].downcase.gsub(/\t|\n|\r/, '').split(';') + style = '' + pears.each do |local_pear| + prop = local_pear.split(':') + next if !prop[0] + + key = prop[0].strip + next if !css_properties_whitelist.include?(node.name) + next if !css_properties_whitelist[node.name].include?(key) + next if css_values_blacklist[node.name]&.include?(local_pear.gsub(/[[:space:]]/, '').strip) + + style += "#{local_pear};" + end + node['style'] = style + if style == '' + node.delete('style') + end + end + + # scan for invalid link content + %w[href style].each do |attribute_name| + next if !node[attribute_name] + + href = cleanup_target(node[attribute_name]) + next if href !~ /(javascript|livescript|vbscript):/i + + node.delete(attribute_name) + end + + # remove attributes if not whitelisted + node.each do |attribute, _value| + attribute_name = attribute.downcase + next if attributes_whitelist[:all].include?(attribute_name) || (attributes_whitelist[node.name]&.include?(attribute_name)) + + node.delete(attribute) + end + + # remove mailto links + if node['href'] + href = cleanup_target(node['href']) + if href =~ /mailto:(.*)$/i + text = Nokogiri::XML::Text.new($1, node.document) + node.add_next_sibling(text) + node.remove + Loofah::Scrubber::STOP + end + end + end + + new_string = '' + done = true + while done + new_string = Loofah.fragment(string).scrub!(scrubber_wipe).to_s + if string == new_string + done = false + end + string = new_string + end + + Loofah.fragment(string).scrub!(scrubber_link).to_s + end + rescue Timeout::Error => e + UNPROCESSABLE_HTML_MSG end =begin @@ -214,21 +220,25 @@ cleanup html string: =end - def self.cleanup(string) - string.gsub!(/<[A-z]:[A-z]>/, '') - string.gsub!(%r{}, '') - string.delete!("\t") + def self.cleanup(string, timeout: true) + Timeout.timeout(timeout ? PROCESSING_TIMEOUT : nil) do + string.gsub!(/<[A-z]:[A-z]>/, '') + string.gsub!(%r{}, '') + string.delete!("\t") - # remove all new lines - string.gsub!(/(\n\r|\r\r\n|\r\n|\n)/, "\n") + # remove all new lines + string.gsub!(/(\n\r|\r\r\n|\r\n|\n)/, "\n") - # remove double multiple empty lines - string.gsub!(/\n\n\n+/, "\n\n") + # remove double multiple empty lines + string.gsub!(/\n\n\n+/, "\n\n") - string = cleanup_structure(string, 'pre') - string = cleanup_replace_tags(string) - string = cleanup_structure(string) - string + string = cleanup_structure(string, 'pre') + string = cleanup_replace_tags(string) + string = cleanup_structure(string) + string + end + rescue Timeout::Error => e + UNPROCESSABLE_HTML_MSG end def self.cleanup_replace_tags(string) diff --git a/spec/lib/html_sanitizer_spec.rb b/spec/lib/html_sanitizer_spec.rb index 0d1151b3e..970ea0d0c 100644 --- a/spec/lib/html_sanitizer_spec.rb +++ b/spec/lib/html_sanitizer_spec.rb @@ -183,4 +183,27 @@ RSpec.describe HtmlSanitizer do end end end + + # Issue #2416 - html_sanitizer goes into loop for specific content + describe '.strict' do + context 'with strings that take a long time (>10s) to parse' do + before { allow(Timeout).to receive(:timeout).and_raise(Timeout::Error) } + + it 'returns a timeout error message for the user' do + expect(HtmlSanitizer.strict(+'', true)) + .to match(HtmlSanitizer::UNPROCESSABLE_HTML_MSG) + end + end + end + + describe '.cleanup' do + context 'with strings that take a long time (>10s) to parse' do + before { allow(Timeout).to receive(:timeout).and_raise(Timeout::Error) } + + it 'returns a timeout error message for the user' do + expect(HtmlSanitizer.cleanup(+'')) + .to match(HtmlSanitizer::UNPROCESSABLE_HTML_MSG) + end + end + end end From 9300fd732cf781f1bb1f60e394883c94b396257c Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 19 Jan 2019 16:20:31 +0100 Subject: [PATCH 10/10] Unable to open customer chat widget via separate open button. Fixes #2435 --- public/assets/chat/chat.coffee | 9 +- public/assets/chat/chat.js | 130 ++++++------ public/assets/chat/chat.min.js | 4 +- public/assets/chat/znuny.html | 2 +- public/assets/chat/znuny_open_by_button.html | 202 +++++++++++++++++++ test/browser/chat_test.rb | 99 +++++++++ 6 files changed, 378 insertions(+), 68 deletions(-) create mode 100644 public/assets/chat/znuny_open_by_button.html diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee index 33a09d82f..5e47fae36 100644 --- a/public/assets/chat/chat.coffee +++ b/public/assets/chat/chat.coffee @@ -1,9 +1,13 @@ do($ = window.jQuery, window) -> scripts = document.getElementsByTagName('script') + + # search for script to get protocol and hostname for ws connection myScript = scripts[scripts.length - 1] - scriptHost = myScript.src.match('.*://([^:/]*).*')[1] - scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1] + scriptProtocol = window.location.protocol.replace(':', '') # set default protocol + if myScript && myScript.src + scriptHost = myScript.src.match('.*://([^:/]*).*')[1] + scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1] # Define the plugin class class Base @@ -875,6 +879,7 @@ do($ = window.jQuery, window) -> @isOpen = true @log.debug 'open widget' + @show() if !@sessionId @showLoader() diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index a7881a977..941e05b05 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1,64 +1,3 @@ -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["agent"] = 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() { - if (this.agent.avatar) { - __out.push('\n\n'); - } - - __out.push('\n\n '); - - __out.push(__sanitize(this.agent.name)); - - __out.push('\n'); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -68,8 +7,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); var Base, Io, Log, Timeout, ZammadChat, myScript, scriptHost, scriptProtocol, scripts; scripts = document.getElementsByTagName('script'); myScript = scripts[scripts.length - 1]; - scriptHost = myScript.src.match('.*://([^:/]*).*')[1]; - scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]; + scriptProtocol = window.location.protocol.replace(':', ''); + if (myScript && myScript.src) { + scriptHost = myScript.src.match('.*://([^:/]*).*')[1]; + scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]; + } Base = (function() { Base.prototype.defaults = { debug: false @@ -1167,6 +1109,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); } this.isOpen = true; this.log.debug('open widget'); + this.show(); if (!this.sessionId) { this.showLoader(); } @@ -1899,6 +1842,67 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return window.ZammadChat = ZammadChat; })(window.jQuery, window); +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["agent"] = 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() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index 941843be5..0e2059502 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1,2 +1,2 @@ -window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(n.push('\n\n')),n.push('\n\n '),n.push(s(this.agent.name)),n.push("\n")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function n(){this.constructor=t}for(var s in e)hasProp.call(e,s)&&(t[s]=e[s]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var n,s,i,o,a,r,l,c,d;return d=document.getElementsByTagName("script"),r=d[d.length-1],l=r.src.match(".*://([^:/]*).*")[1],c=r.src.match("(.*)://[^:/]*.*")[1],n=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,n){var s,i,o,a;if(n.unshift("||"),n.unshift(e),n.unshift(this.options.logPrefix),console.log.apply(console,n),this.options.debug){for(a="",i=0,o=n.length;i"+a+"
")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var n;if(n=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+n/1e3+" sec.)"),!(n<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(n),s=function(t){function n(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),n.__super__.constructor.call(this,t)}return extend(n,t),n.prototype.logPrefix="io",n.prototype.set=function(t){var e,n,s;n=[];for(e in t)s=t[e],n.push(this.options[e]=s);return n},n.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var n,s,i,o;for(o=JSON.parse(e.data),t.log.debug("onMessage",e.data),n=0,s=o.length;nChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},i.prototype.logPrefix="chat",i.prototype._messageCount=0,i.prototype.isOpen=!1,i.prototype.blinkOnlineInterval=null,i.prototype.stopBlinOnlineStateTimeout=null,i.prototype.showTimeEveryXMinutes=2,i.prototype.lastTimestamp=null,i.prototype.lastAddedType=null,i.prototype.inputTimeout=null,i.prototype.isTyping=!1,i.prototype.state="offline",i.prototype.initialQueueDelay=1e4,i.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorri verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento in corso","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Chat chiusa da %s","Compose your message...":"Componi il tuo messaggio...","All colleagues are busy.":"Tutti gli operatori sono occupati.","You are on waiting list position %s.":"Sei in posizione %s nella lista d'attesa.","Start new conversation":"Avvia una nuova chat","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat con %s è stata chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat è stata chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo del previsto per arrivare al tuo turno. Per favore riprova più tardi o inviaci un'email. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"}},i.prototype.sessionId=void 0,i.prototype.scrolledToBottom=!0,i.prototype.scrollSnapTolerance=10,i.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},i.prototype.T=function(){var t,e,n,s,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(n=0,s=e.length;nn?e:document.body)},i.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},i.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".js-chat-status").click(this.stopPropagation),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var n;if(n=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(n=!0):n=!0,n&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(n){return function(s){var i,o,a,r,l,c,d,h,u,p,m,g;if(s.stopPropagation(),s.preventDefault(),s.clipboardData)i=s.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!s.originalEvent.clipboardData)throw"No clipboardData support";i=s.originalEvent.clipboardData}if(c=!1,i&&i.items&&i.items[0]&&(d=i.items[0],"file"!==d.kind||"image/png"!==d.type&&"image/jpeg"!==d.type||(l=d.getAsFile(),u=new FileReader,u.onload=function(t){var e,s,i;return i=t.target.result,e=document.createElement("img"),e.src=i,s=function(t,s,o,a){return n.isRetina()&&(s/=2,o/=2),i=t,e='',document.execCommand("insertHTML",!1,e)},n.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",s)},u.readAsDataURL(l),c=!0)),!c){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(f){s=f,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
"+g.replace(/\n/g,"
")+"
",g=g.replace(/
<\/div>/g,"

")),console.log("p",o,g),"html"===o&&(a=t("
"+g+"
"),h=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(h=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(h=!0,r=r.replace(p,"")),h&&(a=n.wordFilter(a)),a=t(a),a.contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,n;return n=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=n.replace(p,"<"+m),p=new RegExp("'),s=s.get(0),document.caretPositionFromPoint?(d=document.caretPositionFromPoint(r,l),h=document.createRange(),h.setStart(d.offsetNode,d.offset),h.collapse(),h.insertNode(s)):document.caretRangeFromPoint?(h=document.caretRangeFromPoint(r,l),h.insertNode(s)):console.log("could not find carat")},n.resizeImage(s.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},i.prototype.stopPropagation=function(t){return t.stopPropagation()},i.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},i.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},i.prototype.onWebSocketMessage=function(t){var e,n,s;for(e=0,n=t.length;e0,t(e).scrollTop(0),n)return this.log.notice("virtual keyboard shown")},i.prototype.onFocusOut=function(){},i.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},i.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},i.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},i.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},i.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},i.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href})))},i.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},i.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},i.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},i.prototype.close=function(t){var e;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd)):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},i.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},i.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},i.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},i.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},i.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1); -},i.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},i.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},i.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},i.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},i.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},i.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},i.prototype.maybeAddTimestamp=function(){var t,e,n;if(n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=n):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=n,this.lastAddedType="timestamp",this.scrollToBottom())},i.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},i.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},i.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},i.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},i.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},i.prototype.scrollToBottom=function(e){var n;return n=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):n?this.showScrollHint():void 0},i.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},i.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},i.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},i.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},i.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},i.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},i.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},i.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},i.prototype.detectHost=function(){var t;return t="ws://","https"===c&&(t="wss://"),this.options.host=""+t+l+"/ws"},i.prototype.loadCss=function(){var t,e,n;if(this.options.cssAutoload)return n=this.options.cssUrl,n||(n=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),n+="/assets/chat/chat.css"),this.log.debug("load css from '"+n+"'"),e="@import url('"+n+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},i.prototype.onCssLoaded=function(){if(this.cssLoaded=!0,this.socketReady)return this.onReady()},i.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},i.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},i.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},i.prototype.isVisible=function(n,s,i,o){var a,r,l,c,d,h,u,p,m,g,f,y,v,b,w,T,C,z,S,k,I,A,x,_,O,E;if(!(n.length<1))if(r=t(e),a=n.length>1?n.eq(0):n,z=a.get(0),E=r.width(),O=r.height(),o=o?o:"both",p=i!==!0||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=O,b=C.left>=0&&C.left0&&C.right<=E,k=s?S||u:S&&u,v=s?b||T:b&&T,"both"===o)return p&&k&&v;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&v}else{if(_=r.scrollTop(),I=_+O,A=r.scrollLeft(),x=A+E,w=a.offset(),h=w.top,l=h+a.height(),c=w.left,d=c+a.width(),y=s===!0?l:h,m=s===!0?h:l,g=s===!0?d:c,f=s===!0?c:d,"both"===o)return!!p&&m<=I&&y>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&y>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},i.prototype.isRetina=function(){var t;return!!e.matchMedia&&(t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"),t&&t.matches||e.devicePixelRatio>1)},i.prototype.resizeImage=function(t,e,n,s,i,o,a,r){var l;return null==e&&(e="auto"),null==n&&(n="auto"),null==s&&(s=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,c,d,h,u,p;return h=l.width,d=l.height,console.log("ImageService","current size",h,d),"auto"===n&&"auto"===e&&(e=h,n=d),"auto"===n&&(c=h/e,n=d/c),"auto"===e&&(c=h/n,e=d/c),p=!1,e/gi,""),n=n.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),n=n.replace(/<(\/?)s>/gi,"<$1strike>"),n=n.replace(/ /gi," "),e.html(n),t("p",e).each(function(){var e,n;if(n=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(n))return t(this).data("_listLevel",parseInt(e[1],10))}),s=0,i=null,t("p",e).each(function(){var e,n,o,a,r,l,c,d,h,u;if(e=t(this).data("_listLevel"),void 0!==e){if(u=t(this).text(),a="
    ",/^\s*\w+\./.test(u)&&(r=/([0-9])\./.exec(u),r?(h=parseInt(r[1],10),a=null!=(l=h>1)?l:'
      ':"
        "}):a="
          "),e>s&&(0===s?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=d;n=c<=d?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
        1. "+t(this).html()+"
        2. "),t(this).remove(),s=e}return s=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},i.prototype.removeAttribute=function(e){var n,s,i,o,a;if(e){for(n=t(e),a=e.attributes,i=0,o=a.length;i/g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n
          \n
          \n \n \n \n \n \n
          \n
          \n
          \n
          \n \n '),n.push(this.T(this.title)),n.push('\n
          \n
          \n
          \n \n
          \n
          \n
          \n \n
          \n
          ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n '),this.agent?(n.push("\n "),n.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),n.push("\n ")):(n.push("\n "),n.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),n.push("\n ")),n.push('\n
          \n
          "),n.push(this.T("Start new conversation")),n.push("
          \n
          ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n\n'),n.push(this.T("Connecting")),n.push("")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n "),n.push(this.message),n.push("\n
          ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n
          \n '),n.push(this.status),n.push("\n
          \n
          ")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          '),n.push(s(this.label)),n.push(" "),n.push(s(this.time)),n.push("
          ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n \n \n \n \n \n \n \n
          ')}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n \n \n \n \n \n '),n.push(this.T("All colleagues are busy.")),n.push("
          \n "),n.push(this.T("You are on waiting list position %s.",this.position)),n.push("\n
          ")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
          \n '),n.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),n.push('\n
          \n
          "),n.push(this.T("Start new conversation")),n.push("
          \n
          ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")}; \ No newline at end of file +var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var n in e)hasProp.call(e,n)&&(t[n]=e[n]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,n,i,o,a,r,l,c,h;return h=document.getElementsByTagName("script"),r=h[h.length-1],c=e.location.protocol.replace(":",""),r&&r.src&&(l=r.src.match(".*://([^:/]*).*")[1],c=r.src.match("(.*)://[^:/]*.*")[1]),s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var n,i,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;i"+a+"
          ")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;if(s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),!(s<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(s),n=function(t){function s(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),s.__super__.constructor.call(this,t)}return extend(s,t),s.prototype.logPrefix="io",s.prototype.set=function(t){var e,s,n;s=[];for(e in t)n=t[e],s.push(this.options[e]=n);return s},s.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,n,i,o;for(o=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,n=o.length;sChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},i.prototype.logPrefix="chat",i.prototype._messageCount=0,i.prototype.isOpen=!1,i.prototype.blinkOnlineInterval=null,i.prototype.stopBlinOnlineStateTimeout=null,i.prototype.showTimeEveryXMinutes=2,i.prototype.lastTimestamp=null,i.prototype.lastAddedType=null,i.prototype.inputTimeout=null,i.prototype.isTyping=!1,i.prototype.state="offline",i.prototype.initialQueueDelay=1e4,i.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorri verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento in corso","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Chat chiusa da %s","Compose your message...":"Componi il tuo messaggio...","All colleagues are busy.":"Tutti gli operatori sono occupati.","You are on waiting list position %s.":"Sei in posizione %s nella lista d'attesa.","Start new conversation":"Avvia una nuova chat","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat con %s è stata chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat è stata chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo del previsto per arrivare al tuo turno. Per favore riprova più tardi o inviaci un'email. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"}},i.prototype.sessionId=void 0,i.prototype.scrolledToBottom=!0,i.prototype.scrollSnapTolerance=10,i.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},i.prototype.T=function(){var t,e,s,n,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(s=0,n=e.length;ss?e:document.body)},i.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},i.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".js-chat-status").click(this.stopPropagation),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var s;if(s=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(s=!0):s=!0,s&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(s){return function(n){var i,o,a,r,l,c,h,d,u,p,m,g;if(n.stopPropagation(),n.preventDefault(),n.clipboardData)i=n.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!n.originalEvent.clipboardData)throw"No clipboardData support";i=n.originalEvent.clipboardData}if(c=!1,i&&i.items&&i.items[0]&&(h=i.items[0],"file"!==h.kind||"image/png"!==h.type&&"image/jpeg"!==h.type||(l=h.getAsFile(),u=new FileReader,u.onload=function(t){var e,n,i;return i=t.target.result,e=document.createElement("img"),e.src=i,n=function(t,n,o,a){return s.isRetina()&&(n/=2,o/=2),i=t,e='',document.execCommand("insertHTML",!1,e)},s.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",n)},u.readAsDataURL(l),c=!0)),!c){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(f){n=f,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
          "+g.replace(/\n/g,"
          ")+"
          ",g=g.replace(/
          <\/div>/g,"

          ")),console.log("p",o,g),"html"===o&&(a=t("
          "+g+"
          "),d=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),d&&(a=s.wordFilter(a)),a=t(a),a.contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,s;return s=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=s.replace(p,"<"+m),p=new RegExp("'),n=n.get(0),document.caretPositionFromPoint?(h=document.caretPositionFromPoint(r,l),d=document.createRange(),d.setStart(h.offsetNode,h.offset),d.collapse(),d.insertNode(n)):document.caretRangeFromPoint?(d=document.caretRangeFromPoint(r,l),d.insertNode(n)):console.log("could not find carat")},s.resizeImage(n.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},i.prototype.stopPropagation=function(t){return t.stopPropagation()},i.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},i.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},i.prototype.onWebSocketMessage=function(t){var e,s,n;for(e=0,s=t.length;e0,t(e).scrollTop(0),s)return this.log.notice("virtual keyboard shown")},i.prototype.onFocusOut=function(){},i.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},i.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},i.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},i.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},i.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},i.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.show(),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href})))},i.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},i.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},i.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},i.prototype.close=function(t){var e;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd)):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},i.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},i.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},i.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},i.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},i.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},i.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},i.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},i.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},i.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId), +this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},i.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},i.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},i.prototype.maybeAddTimestamp=function(){var t,e,s;if(s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())},i.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},i.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},i.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},i.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},i.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},i.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},i.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},i.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},i.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},i.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},i.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},i.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},i.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},i.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},i.prototype.detectHost=function(){var t;return t="ws://","https"===c&&(t="wss://"),this.options.host=""+t+l+"/ws"},i.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},i.prototype.onCssLoaded=function(){if(this.cssLoaded=!0,this.socketReady)return this.onReady()},i.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},i.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},i.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},i.prototype.isVisible=function(s,n,i,o){var a,r,l,c,h,d,u,p,m,g,f,y,v,b,w,T,C,z,S,k,I,A,x,_,O,E;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,z=a.get(0),E=r.width(),O=r.height(),o=o?o:"both",p=i!==!0||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=O,b=C.left>=0&&C.left0&&C.right<=E,k=n?S||u:S&&u,v=n?b||T:b&&T,"both"===o)return p&&k&&v;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&v}else{if(_=r.scrollTop(),I=_+O,A=r.scrollLeft(),x=A+E,w=a.offset(),d=w.top,l=d+a.height(),c=w.left,h=c+a.width(),y=n===!0?l:d,m=n===!0?d:l,g=n===!0?h:c,f=n===!0?c:h,"both"===o)return!!p&&m<=I&&y>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&y>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},i.prototype.isRetina=function(){var t;return!!e.matchMedia&&(t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"),t&&t.matches||e.devicePixelRatio>1)},i.prototype.resizeImage=function(t,e,s,n,i,o,a,r){var l;return null==e&&(e="auto"),null==s&&(s="auto"),null==n&&(n=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,c,h,d,u,p;return d=l.width,h=l.height,console.log("ImageService","current size",d,h),"auto"===s&&"auto"===e&&(e=d,s=h),"auto"===s&&(c=d/e,s=h/c),"auto"===e&&(c=d/s,e=h/c),p=!1,e/gi,""),s=s.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),s=s.replace(/<(\/?)s>/gi,"<$1strike>"),s=s.replace(/ /gi," "),e.html(s),t("p",e).each(function(){var e,s;if(s=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(s))return t(this).data("_listLevel",parseInt(e[1],10))}),n=0,i=null,t("p",e).each(function(){var e,s,o,a,r,l,c,h,d,u;if(e=t(this).data("_listLevel"),void 0!==e){if(u=t(this).text(),a="
            ",/^\s*\w+\./.test(u)&&(r=/([0-9])\./.exec(u),r?(d=parseInt(r[1],10),a=null!=(l=d>1)?l:'
              ':"
                "}):a="
                  "),e>n&&(0===n?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=h;s=c<=h?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
                1. "+t(this).html()+"
                2. "),t(this).remove(),n=e}return n=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},i.prototype.removeAttribute=function(e){var s,n,i,o,a;if(e){for(s=t(e),a=e.attributes,i=0,o=a.length;i/g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(n(this.agent.name)),s.push("\n")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n
                  \n
                  \n \n \n \n \n \n
                  \n
                  \n
                  \n
                  \n \n '),s.push(this.T(this.title)),s.push('\n
                  \n
                  \n
                  \n \n
                  \n
                  \n
                  \n \n
                  \n
                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n '),this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
                  \n
                  "),s.push(this.T("Start new conversation")),s.push("
                  \n
                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,s=[],n=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('\n \n \n \n\n'),s.push(this.T("Connecting")),s.push("")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n "),s.push(this.message),s.push("\n
                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,s=[],n=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n
                  \n '),s.push(this.status),s.push("\n
                  \n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  '),s.push(n(this.label)),s.push(" "),s.push(n(this.time)),s.push("
                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,s=[],n=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n \n \n \n \n \n \n \n
                  ')}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,s=[],n=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n \n \n \n \n \n '),s.push(this.T("All colleagues are busy.")),s.push("
                  \n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                  \n '),s.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),s.push('\n
                  \n
                  "),s.push(this.T("Start new conversation")),s.push("
                  \n
                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")}; \ No newline at end of file diff --git a/public/assets/chat/znuny.html b/public/assets/chat/znuny.html index f9fa7cda2..51b69fb15 100644 --- a/public/assets/chat/znuny.html +++ b/public/assets/chat/znuny.html @@ -157,7 +157,7 @@ debug: true, background: '#494d52', flat: true, - shown: false, + show: true, idleTimeout: 1, idleTimeoutIntervallCheck: 0.5, inactiveTimeout: 2, diff --git a/public/assets/chat/znuny_open_by_button.html b/public/assets/chat/znuny_open_by_button.html new file mode 100644 index 000000000..e0b0aa86b --- /dev/null +++ b/public/assets/chat/znuny_open_by_button.html @@ -0,0 +1,202 @@ + + + + + Zammad Chat + + + + + + + + + + + + + + + + + + +
                  + + + + + + + + + +

                  Settings

                  +
                  +
                  + + + +
                  + + + +
                  + + px + + +
                  + + px + + +
                  + +

                  Log

                  +
                  +
                  +
                  +
                  + + + + + + \ No newline at end of file diff --git a/test/browser/chat_test.rb b/test/browser/chat_test.rb index d39d15d4d..dbd6e35fb 100644 --- a/test/browser/chat_test.rb +++ b/test/browser/chat_test.rb @@ -591,6 +591,78 @@ class ChatTest < TestCase ) end + def test_open_chat_by_button + chat_url = "#{browser_url}/assets/chat/znuny_open_by_button.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + exists_not( + browser: customer, + css: '.zammad-chat-is-shown', + ) + exists_not( + browser: customer, + css: '.zammad-chat-is-open', + ) + click( + browser: customer, + css: '.open-zammad-chat', + ) + watch_for( + browser: customer, + css: '.zammad-chat-is-shown', + timeout: 4, + ) + watch_for( + browser: customer, + css: '.zammad-chat-is-open', + timeout: 4, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: customer, + css: '.zammad-chat-header-icon-close', + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat-is-shown', + timeout: 4, + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat-is-open', + timeout: 4, + ) + end + def test_timeouts chat_url = "#{browser_url}/assets/chat/znuny.html?port=#{ENV['WS_PORT']}" agent = browser_instance @@ -762,4 +834,31 @@ class ChatTest < TestCase end + def disable_chat + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + + # disable chat + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: '.content.active a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '.content.active .js-chatSetting', + type: 'off', + ) + end + end