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/.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/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 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/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/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/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/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/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() { 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/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/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/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 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/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! 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 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 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 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 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/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 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',