Merge branch 'develop' into private-te-refactoring-constantize

This commit is contained in:
Thorsten Eckel 2019-01-19 18:16:34 +01:00
commit ae5ccc11df
31 changed files with 750 additions and 298 deletions

View file

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

View file

@ -175,6 +175,8 @@ Rails/SkipsModelValidations:
Enabled: true
Exclude:
- test/**/*
- "**/*_spec.rb"
- "**/*_examples.rb"
Style/ClassAndModuleChildren:
Description: 'Checks style of children classes and modules.'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
# <div class='yahoo_quoted'> 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
# <div class='yahoo_quoted'> 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 <a> 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 <a> 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{</[A-z]:[A-z]>}, '')
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{</[A-z]:[A-z]>}, '')
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)

View file

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

View file

@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}

File diff suppressed because one or more lines are too long

View file

@ -157,7 +157,7 @@
debug: true,
background: '#494d52',
flat: true,
shown: false,
show: true,
idleTimeout: 1,
idleTimeoutIntervallCheck: 0.5,
inactiveTimeout: 2,

View file

@ -0,0 +1,202 @@
<!doctype html>
<html lang="de-de">
<head>
<meta charset="utf-8">
<title>Zammad Chat</title>
<link rel="stylesheet" href="znuny.css">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<style>
body {
margin: 0;
font-family: sans-serif;
}
.mockup {
vertical-align: bottom;
}
.settings {
position: fixed;
left: 20px;
top: 20px;
background: white;
font-size: 14px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 3px 10px rgba(0,0,0,.3);
width: 500px;
}
.settings input {
vertical-align: middle;
}
.settings input + input {
margin-right: 3px;
}
table td:first-child {
text-align: right;
padding-right: 0;
}
table td.log {
text-align: left;
padding-right: 0;
word-break: break-all;
}
td {
padding: 5px;
}
h2 {
font-size: 1em;
margin: 0;
}
@media only screen and (max-width: 768px) {
.settings {
display: none;
}
}
.Box {
background: hsl(0,0%,91%);
width: 26px;
height: 24px;
color: hsl(0,0%,47%);
float: left;
}
.Box.Active {
background: hsl(0,0%,36%);
color: white;
}
</style>
</head>
<body>
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<img class="mockup" width="100%" src="znuny.png">
<div class="settings">
<table>
<tr>
<td><h2>Settings</h2>
<td>
<tr>
<td>
<input id="flat" type="checkbox" data-option="flat">
<td>
<label for="flat">Flat Design</label>
<tr>
<td>
<input type="color" id="color" value="#AE99D6" data-option="color">
<td>
<label for="color">Color</label>
<tr>
<td>
<input type="range" id="borderRadius" value="5" min="0" max="20" data-option="borderRadius">
<input type="number" value="5" min="5" max="20" data-option="borderRadius">px
<td>
<label for="borderRadius">Border Radius</label>
<tr>
<td>
<input type="range" id="fontSize" value="12" min="11" max="18" data-option="fontSize">
<input type="number" value="12" min="11" max="18" data-option="fontSize">px
<td>
<label for="fontSize">Font Size</label>
<tr>
<td>
<td><button class="open-zammad-chat">Open Chat</button>
<tr>
<td class="log"><h2>Log</h2>
<td>
<tr>
<td colspan="2" class="log js-chatLogDisplay">
</table>
</div>
<script src="jquery-2.1.4.min.js"></script>
<script src="chat.js"></script>
<script>
function getSearchParameters() {
var prmstr = window.location.search.substr(1);
return prmstr != null && prmstr != '' ? transformToAssocArray(prmstr) : {};
}
function transformToAssocArray( prmstr ) {
var params = {};
var prmarr = prmstr.split('&');
for ( var i = 0; i < prmarr.length; i++) {
var tmparr = prmarr[i].split('=');
params[tmparr[0]] = tmparr[1];
}
return params;
}
var hostname = window.location.hostname;
var port = window.location.port;
var params = getSearchParameters();
var host = 'ws://'+ (location.host || 'localhost').split(':')[0] +':6042'
if (params['port']) {
host = 'ws://' + hostname + ':' + params['port']
}
cssUrl = 'http://' + hostname + ':' + port + '/assets/chat/chat.css'
var chat = new ZammadChat({
chatId: 1,
host: host,
cssUrl: cssUrl,
debug: true,
background: '#494d52',
flat: true,
show: false,
idleTimeout: 1,
idleTimeoutIntervallCheck: 0.5,
inactiveTimeout: 2,
inactiveTimeoutIntervallCheck: 0.5,
waitingListTimeout: 1.2,
waitingListTimeoutIntervallCheck: 0.5,
});
$('.settings :input').on({
change: function(){
switch($(this).attr('data-option')){
case "flat":
$('.zammad-chat').toggleClass('zammad-chat--flat', this.checked);
break;
case "color":
setScssVariable('themeColor', this.value);
updateStyle();
break;
case "borderRadius":
setScssVariable('borderRadius', this.value + "px");
updateStyle();
break;
}
},
input: function(){
switch($(this).attr('data-option')){
case "borderRadius":
$('[data-option="borderRadius"]').val(this.value);
setScssVariable('borderRadius', this.value + "px");
updateStyle();
break;
case "fontSize":
$('[data-option="fontSize"]').val(this.value);
setScssVariable('fontSize', this.value + "px");
updateStyle();
break;
}
}
});
</script>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

@ -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(+'<img src="/some_one.png">', 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(+'<img src="/some_one.png">'))
.to match(HtmlSanitizer::UNPROCESSABLE_HTML_MSG)
end
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -72,6 +72,7 @@ class AdminChannelEmailTest < TestCase
click(css: '.content.active .js-channelDelete')
sleep 2
# flanky
click(css: '.modal .js-submit')
sleep 2
end

View file

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

View file

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

View file

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

View file

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

View file

@ -203,6 +203,7 @@ class KeyboardShortcutsTest < TestCase
)
sleep 5
shortcut(key: 'a')
# flanky
watch_for(
css: '.js-notificationsContainer',
value: 'Test Ticket for Shortcuts II',