diff --git a/.circleci/docker-image-build.sh b/.circleci/docker-image-build.sh
index b744ad6ff..8d864ad94 100755
--- a/.circleci/docker-image-build.sh
+++ b/.circleci/docker-image-build.sh
@@ -6,16 +6,20 @@ set -o errexit
set -o pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
-REPO_USER="zammad"
+GITHUB_REPO_USER="zammad"
ZAMMAD_VERSION="$(git describe --tags | sed -e 's/-[a-z0-9]\{8,\}.*//g')"
export ZAMMAD_VERSION
if [ "${CIRCLE_BRANCH}" == 'develop' ]; then
- DOCKER_REPOSITORY="zammad-docker"
BUILD_SCRIPT="scripts/build_image.sh"
+ DOCKER_REPOSITORY="zammad"
+ export DOCKER_REPOSITORY
+ GITHUB_REPOSITORY="zammad-docker"
elif [ "${CIRCLE_BRANCH}" == 'stable' ]; then
+ BUILD_SCRIPT="hooks/build"
DOCKER_REPOSITORY="zammad-docker-compose"
- BUILD_SCRIPT="hooks/build.sh"
+ export DOCKER_REPOSITORY
+ GITHUB_REPOSITORY="zammad-docker-compose"
else
echo "branch is ${CIRCLE_BRANCH}... no docker image build needed..."
exit 0
@@ -25,11 +29,11 @@ fi
echo "${DOCKER_PASSWORD}" | docker login --username="${DOCKER_USERNAME}" --password-stdin
# clone docker repo
-git clone https://github.com/"${REPO_USER}"/"${DOCKER_REPOSITORY}"
+git clone https://github.com/"${GITHUB_REPO_USER}"/"${GITHUB_REPOSITORY}"
# enter dockerfile dir
-cd "${REPO_ROOT}/${DOCKER_REPOSITORY}"
+cd "${REPO_ROOT}/${GITHUB_REPOSITORY}"
# build & push docker image
# shellcheck disable=SC1090
-source "${REPO_ROOT}/${DOCKER_REPOSITORY}/${BUILD_SCRIPT}"
+source "${REPO_ROOT}/${GITHUB_REPOSITORY}/${BUILD_SCRIPT}"
diff --git a/.circleci/install.sh b/.circleci/install.sh
index 4eccd04e4..047315c7f 100755
--- a/.circleci/install.sh
+++ b/.circleci/install.sh
@@ -10,7 +10,7 @@ DB_CONFIG="test:\n adapter: postgresql\n database: zammad_test\n host: 127.0.
# install build dependencies
sudo apt-get update
-sudo apt-get install -y --no-install-recommends autoconf automake autotools-dev bison build-essential curl git-core libffi-dev libgdbm-dev libgmp-dev libmariadbclient-dev-compat libncurses5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libxml2-dev libxslt1-dev libyaml-0-2 libyaml-dev patch pkg-config postfix sqlite3 zlib1g-dev
+sudo apt-get install -y --no-install-recommends autoconf automake autotools-dev bison build-essential curl git-core libffi-dev libgdbm-dev libgmp-dev libmariadbclient-dev-compat libncurses5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libxml2-dev libxslt1-dev libyaml-0-2 libyaml-dev patch pkg-config postfix sqlite3 zlib1g-dev libimlib2 libimlib2-dev
if [ "${CIRCLE_JOB}" == "install-mysql" ]; then
DB_ADAPTER="mysql2"
diff --git a/.pkgr.yml b/.pkgr.yml
index 6e32b62eb..b5a6647c1 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -10,36 +10,80 @@ targets:
- nginx
- postgresql-server
- which
+ - epel-release
+ - imlib2
+ - imlib2-devel
+ build_dependencies:
+ - http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-1.4.5-9.el7.x86_64.rpm
+ - http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-devel-1.4.5-9.el7.x86_64.rpm
debian-8:
dependencies:
- curl
- elasticsearch
- nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite
+ - libimlib2
+ - libimlib2-dev
+ build_dependencies:
+ - libimlib2
+ - libimlib2-dev
debian-9:
dependencies:
- curl
- elasticsearch
- nginx|apache2
- postgresql|mariadb-server|sqlite
+ - libimlib2
+ - libimlib2-dev
+ build_dependencies:
+ - libimlib2
+ - libimlib2-dev
ubuntu-16.04:
dependencies:
- curl
- elasticsearch
- nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite
+ - libimlib2
+ - libimlib2-dev
+ build_dependencies:
+ - libimlib2
+ - libimlib2-dev
ubuntu-18.04:
dependencies:
- curl
- elasticsearch
- nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite
+ - libimlib2
+ build_dependencies:
+ - libimlib2-dev
sles-12:
dependencies:
- curl
- elasticsearch
- nginx
- postgresql-server
+ - imlib2
+ - imlib2-devel
+ build_dependencies:
+ - imlib2
+ - imlib2-devel
+ sles-12:
+ dependencies:
+ - curl
+ - elasticsearch
+ - nginx
+ - postgresql-server
+ - imlib2
+ - libImlib2-1
+ - imlib2
+ - imlib2-devel
+ build_dependencies:
+ - https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-1.4.5-12.1.1.x86_64.rpm
+ - https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-devel-1.4.5-12.1.1.x86_64.rpm
+ - https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-filters-1.4.5-12.1.1.x86_64.rpm
+ - https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/libImlib2-1-1.4.5-12.1.1.x86_64.rpm
before:
- contrib/packager.io/before.sh
after:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da82b127a..954e06c0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# Change Log
-## [2.9.0](https://github.com/zammad/zammad/tree/2.9.0) (2018-xx-xx)
-[Full Changelog](https://github.com/zammad/zammad/compare/2.8.0...2.9.0)
+## [2.10.0](https://github.com/zammad/zammad/tree/2.10.0) (2019-xx-xx)
+[Full Changelog](https://github.com/zammad/zammad/compare/2.8.0...2.10.0)
**Implemented enhancements:**
diff --git a/Gemfile b/Gemfile
index e52605d67..cca8287d4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -116,6 +116,9 @@ gem 'autodiscover', git: 'https://github.com/zammad-deps/autodiscover'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm'
gem 'viewpoint'
+# image processing
+gem 'rszr'
+
# Gems used only for develop/test and not required
# in production environments by default.
group :development, :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index 338636dc6..e358c64c6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -408,6 +408,7 @@ GEM
rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.0)
+ rszr (0.3.2)
rubocop (0.64.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
@@ -584,6 +585,7 @@ DEPENDENCIES
rb-fsevent
rchardet (>= 1.8.0)
rspec-rails
+ rszr
rubocop
rubyntlm!
sassc-rails
diff --git a/VERSION b/VERSION
index f4622d320..e9f1e3cea 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.9.x
+2.10.x
diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee
index 73fc47fc1..6df84a8b0 100644
--- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee
+++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee
@@ -808,7 +808,8 @@ class App.TicketZoom extends App.Controller
@autosaveStop()
# validate ticket form using HTML5 validity check
- if !@$('.edit').parent().get(0).reportValidity()
+ element = @$('.edit').parent().get(0)
+ if element && element.reportValidity && !element.reportValidity()
@submitEnable(e)
@autosaveStart()
return
diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_image_view.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_image_view.coffee
index df8661ed2..de542b3fc 100644
--- a/app/assets/javascripts/app/controllers/ticket_zoom/article_image_view.coffee
+++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_image_view.coffee
@@ -11,9 +11,33 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
'click .js-cancel': 'cancel'
'click .js-close': 'cancel'
+ constructor: ->
+ super
+ @unbindAll()
+ $(document).bind('keydown.image_preview', 'right', (e) =>
+ nextElement = @parentElement.closest('.attachment').next('.attachment.attachment--preview')
+ return if nextElement.length is 0
+ @close()
+ nextElement.find('img').click()
+ )
+ $(document).bind('keydown.image_preview', 'left', (e) =>
+ prevElement = @parentElement.closest('.attachment').prev('.attachment.attachment--preview')
+ return if prevElement.length is 0
+ @close()
+ prevElement.find('img').click()
+ )
+
content: ->
+ @image = @image.replace(/view=preview/, 'view=inline')
"
<% if attachment.preferences && attachment.preferences['Content-Type'] && @ContentTypeIcon(attachment.preferences['Content-Type']): %>
<% if @canPreview(attachment.preferences['Content-Type']): %>
-
+
<% else: %>
<%- @Icon( @ContentTypeIcon(attachment.preferences['Content-Type']) ) %>
<% end %>
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4e4be2efb..ee2944184 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -2,7 +2,7 @@
class SessionsController < ApplicationController
prepend_before_action :authentication_check, only: %i[switch_to_user list delete]
- skip_before_action :verify_csrf_token, only: %i[create show destroy create_omniauth failure_omniauth create_sso]
+ skip_before_action :verify_csrf_token, only: %i[show destroy create_omniauth failure_omniauth create_sso]
# "Create" a login, aka "log the user in"
def create
diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb
index 681ffb17b..138a23fdd 100644
--- a/app/controllers/ticket_articles_controller.rb
+++ b/app/controllers/ticket_articles_controller.rb
@@ -255,8 +255,21 @@ class TicketArticlesController < ApplicationController
disposition = sanitized_disposition
+ content = nil
+ if params[:view].present? && file.preferences[:resizable] == true
+ if file.preferences[:content_inline] == true && params[:view] == 'inline'
+ content = file.content_inline
+ elsif file.preferences[:content_preview] == true && params[:view] == 'preview'
+ content = file.content_preview
+ end
+ end
+
+ if content.blank?
+ content = file.content
+ end
+
send_data(
- file.content,
+ content,
filename: file.filename,
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: disposition
diff --git a/app/models/organization.rb b/app/models/organization.rb
index d772fea1b..0b35e24af 100644
--- a/app/models/organization.rb
+++ b/app/models/organization.rb
@@ -7,6 +7,7 @@ class Organization < ApplicationModel
include HasHistory
include HasSearchIndexBackend
include CanCsvImport
+ include ChecksHtmlSanitized
include Organization::ChecksAccess
include Organization::Assets
@@ -22,6 +23,8 @@ class Organization < ApplicationModel
activity_stream_permission 'admin.role'
+ sanitized_html :note
+
private
def domain_cleanup
diff --git a/app/models/store.rb b/app/models/store.rb
index 5a79c1966..4827b2c5a 100644
--- a/app/models/store.rb
+++ b/app/models/store.rb
@@ -34,7 +34,7 @@ returns
=end
def self.add(data)
- data = data.stringify_keys
+ data.deep_stringify_keys!
# lookup store_object.id
store_object = Store::Object.create_if_not_exists(name: data['object'])
@@ -50,9 +50,34 @@ returns
data.delete('data')
data.delete('object')
+ data['preferences'] ||= {}
+ ['Mime-Type', 'Content-Type', 'mime_type', 'content_type'].each do |key|
+ next if data['preferences'][key].blank?
+ next if !data['preferences'][key].match(%r{image/(jpeg|jpg|png)}i)
+
+ data['preferences']['resizable'] = true
+ break
+ end
+
# store meta data
store = Store.create!(data)
+ begin
+ if store.preferences[:resizable] == true
+ if store.content_preview(silence: true)
+ store.preferences[:content_preview] = true
+ end
+ if store.content_inline(silence: true)
+ store.preferences[:content_inline] = true
+ end
+ store.save!
+ end
+ rescue => e
+ logger.error e
+ store.preferences[:resizable] = false
+ store.save!
+ end
+
store
end
@@ -165,6 +190,52 @@ returns
=begin
+get content of file in preview size
+
+ store = Store.find(store_id)
+ content_as_string = store.content_preview
+
+returns
+
+ content_as_string
+
+=end
+
+ def content_preview(options = {})
+ file = Store::File.find_by(id: store_file_id)
+ if !file
+ raise "No such file #{store_file_id}!"
+ end
+ raise 'Unable to generate preview' if options[:silence] != true && preferences[:content_preview] != true
+
+ image_resize(file.content, 200)
+ end
+
+=begin
+
+get content of file in inline size
+
+ store = Store.find(store_id)
+ content_as_string = store.content_inline
+
+returns
+
+ content_as_string
+
+=end
+
+ def content_inline(options = {})
+ file = Store::File.find_by(id: store_file_id)
+ if !file
+ raise "No such file #{store_file_id}!"
+ end
+ raise 'Unable to generate inline' if options[:silence] != true && preferences[:content_inline] != true
+
+ image_resize(file.content, 1800)
+ end
+
+=begin
+
get content of file
store = Store.find(store_id)
@@ -204,4 +275,32 @@ returns
file.provider
end
+
+ private
+
+ def image_resize(content, width)
+ local_sha = Digest::SHA256.hexdigest(content)
+
+ cache_key = "image-resize-#{local_sha}_#{width}"
+ all = nil
+ image = Cache.get(cache_key)
+ return image if image
+
+ temp_file = ::Tempfile.new
+ temp_file.binmode
+ temp_file.write(content)
+ temp_file.close
+ image = Rszr::Image.load(temp_file.path)
+ return if image.width < width
+
+ image.resize!(width, :auto)
+ temp_file_resize = ::Tempfile.new.path
+ image.save(temp_file_resize)
+ image_resized = ::File.binread(temp_file_resize)
+
+ Cache.write(cache_key, image_resized, { expires_in: 6.months })
+
+ image_resized
+ end
+
end
diff --git a/app/models/ticket.rb b/app/models/ticket.rb
index 740120b55..a5a81c1df 100644
--- a/app/models/ticket.rb
+++ b/app/models/ticket.rb
@@ -6,6 +6,7 @@ class Ticket < ApplicationModel
include ChecksClientNotification
include ChecksLatestChangeObserved
include CanCsvImport
+ include ChecksHtmlSanitized
include HasHistory
include HasTags
include HasSearchIndexBackend
@@ -56,6 +57,8 @@ class Ticket < ApplicationModel
history_relation_object 'Ticket::Article'
+ sanitized_html :note
+
belongs_to :group
belongs_to :organization
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb
index e5c1c56f7..d4f23bf83 100644
--- a/app/models/ticket/article.rb
+++ b/app/models/ticket/article.rb
@@ -82,7 +82,7 @@ returns
article['attachments'].each do |file|
next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" )
- replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}\"#{tag_end}>"
+ replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}?view=inline\"#{tag_end}>"
inline_attachments[file[:id]] = true
break
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5d564f088..280431476 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -6,6 +6,7 @@ class User < ApplicationModel
include HasHistory
include HasSearchIndexBackend
include CanCsvImport
+ include ChecksHtmlSanitized
include HasGroups
include HasRoles
@@ -66,6 +67,8 @@ class User < ApplicationModel
:groups,
:user_groups
+ sanitized_html :note
+
def ignore_search_indexing?(_action)
# ignore internal user
return true if id == 1
diff --git a/db/migrate/20190131000001_setting_change_ticket_zoom_attachment_preview.rb b/db/migrate/20190131000001_setting_change_ticket_zoom_attachment_preview.rb
new file mode 100644
index 000000000..296db7b3f
--- /dev/null
+++ b/db/migrate/20190131000001_setting_change_ticket_zoom_attachment_preview.rb
@@ -0,0 +1,34 @@
+class SettingChangeTicketZoomAttachmentPreview < ActiveRecord::Migration[5.1]
+ def up
+ # return if it's a new setup
+ return if !Setting.find_by(name: 'system_init_done')
+
+ Setting.create_or_update(
+ title: 'Sidebar Attachments',
+ name: 'ui_ticket_zoom_attachments_preview',
+ area: 'UI::TicketZoom::Preview',
+ description: 'Enables preview of attachments.',
+ options: {
+ form: [
+ {
+ display: '',
+ null: true,
+ name: 'ui_ticket_zoom_attachments_preview',
+ tag: 'boolean',
+ translate: true,
+ options: {
+ true => 'yes',
+ false => 'no',
+ },
+ },
+ ],
+ },
+ state: true,
+ preferences: {
+ prio: 400,
+ permission: ['admin.ui'],
+ },
+ frontend: true
+ )
+ end
+end
diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb
index 778538f0c..0d6ce04e1 100644
--- a/db/seeds/settings.rb
+++ b/db/seeds/settings.rb
@@ -793,7 +793,7 @@ Setting.create_if_not_exists(
},
],
},
- state: false,
+ state: true,
preferences: {
prio: 400,
permission: ['admin.ui'],
diff --git a/lib/notification_factory/template.rb b/lib/notification_factory/template.rb
index 98ce92347..945eba14d 100644
--- a/lib/notification_factory/template.rb
+++ b/lib/notification_factory/template.rb
@@ -17,35 +17,31 @@ examples how to use
end
def to_s
- strip_html
- end
+ @template.gsub(/\#{\s*(.*?)\s*}/m) do
+ # some browsers start adding HTML tags
+ # fixes https://github.com/zammad/zammad/issues/385
+ input_template = $1.gsub(/\A<.+?>\s*|\s*<.+?>\z/, '')
- def strip_html
- # some browsers start adding HTML tags
- # fixes https://github.com/zammad/zammad/issues/385
- @template.gsub(/\#\{\s*t\((.+?)\)\s*\}/m) do
- content = $1
- if content =~ /^'(.+?)'$/
- %(<%= t "#{strip_content($1)}", #{@escape} %>)
+ case input_template
+ when /\At\('(.+?)'\)\z/m
+ %(<%= t "#{sanitize_text($1)}", #{@escape} %>)
+ when /\At\((.+?)\)\z/m
+ %(<%= t d"#{sanitize_object_name($1)}", #{@escape} %>)
+ when /\Aconfig\.(.+?)\z/m
+ %(<%= c "#{sanitize_object_name($1)}", #{@escape} %>)
else
- %(<%= t d"#{strip_variable(content)}", #{@escape} %>)
+ %(<%= d "#{sanitize_object_name(input_template)}", #{@escape} %>)
end
- end.gsub(/\#\{\s*config\.(.+?)\s*\}/m) do
- %(<%= c "#{strip_variable($1)}", #{@escape} %>)
- end.gsub(/\#\{(.*?)\}/m) do
- %(<%= d "#{strip_variable($1)}", #{@escape} %>)
end
end
- def strip_content(string)
- string&.gsub(/\t|\r|\n/, '')
- &.gsub(/"/, '\"')
+ def sanitize_text(string)
+ string&.tr("\t\r\n", '')
+ &.gsub(/(?/, '')
+ def sanitize_object_name(string)
+ string&.tr("\t\r\n\f \"'§;", '')
end
end
diff --git a/public/assets/chat/chat-no-jquery.coffee b/public/assets/chat/chat-no-jquery.coffee
index f43d1285b..7b8fe5cc7 100644
--- a/public/assets/chat/chat-no-jquery.coffee
+++ b/public/assets/chat/chat-no-jquery.coffee
@@ -501,7 +501,7 @@ do(window) ->
stopPropagation: (event) ->
event.stopPropagation()
- onPaste: (e) =>
+ onDrop: (e) =>
e.stopPropagation()
e.preventDefault()
diff --git a/spec/lib/notification_factory/template_spec.rb b/spec/lib/notification_factory/template_spec.rb
new file mode 100644
index 000000000..094f5ca46
--- /dev/null
+++ b/spec/lib/notification_factory/template_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+RSpec.describe NotificationFactory::Template do
+ subject(:template) do
+ NotificationFactory::Template.new(template_string, escape)
+ end
+
+ describe '#to_s' do
+ context 'for empty input template (incl. whitespace-only)' do
+ let(:template_string) { "\#{ }" }
+
+ context 'with escape = true' do
+ let(:escape) { true }
+
+ it 'returns an ERB template with the #d helper, and passes escape arg as string' do
+ expect(template.to_s).to eq('<%= d "", true %>')
+ end
+ end
+
+ context 'with escape = false' do
+ let(:escape) { false }
+
+ it 'returns an ERB template with the #d helper, and passes escape arg as string' do
+ expect(template.to_s).to eq('<%= d "", false %>')
+ end
+ end
+ end
+
+ context 'for input template using #t helper' do
+ let(:template_string) { "\#{t('some text')}" }
+ let(:escape) { false }
+
+ it 'returns an ERB template with the #t helper, and passes escape arg as string' do
+ expect(template.to_s).to eq('<%= t "some text", false %>')
+ end
+
+ context 'with double-quotes in argument' do
+ let(:template_string) { "\#{t('some \"text\"')}" }
+
+ it 'adds backslash-escaping' do
+ expect(template.to_s).to eq('<%= t "some \"text\"", false %>')
+ end
+ end
+ end
+
+ # Regression test for https://github.com/zammad/zammad/issues/385
+ context 'with HTML auto-injected by browser' do
+ let(:escape) { true }
+
+ context 'for
tags wrapped around "ticket.id"' do
+ let(:template_string) { <<~'TEMPLATE'.chomp }
+ #{ticket.id}
+ TEMPLATE
+
+ it 'strips tag from resulting ERB template' do
+ expect(template.to_s).to eq('<%= d "ticket.id", true %>')
+ end
+ end
+
+ context 'for
tags wrapped around "config.fqdn"' do
+ let(:template_string) { <<~'TEMPLATE'.chomp }
+ #{config.fqdn}
+ TEMPLATE
+
+ it 'strips tag from resulting ERB template' do
+ expect(template.to_s).to eq('<%= c "fqdn", true %>')
+ end
+ end
+
+ context 'for
tags surrounded by whitespace' do
+ let(:template_string) { <<~'TEMPLATE'.chomp }
+ #{ ticket.id }
+ TEMPLATE
+
+ it 'strips tag and spaces from template' do
+ expect(template.to_s).to eq('<%= d "ticket.id", true %>')
+ end
+ end
+
+ context 'for unpaired
tag and trailing whitespace' do
+ let(:template_string) { <<~'TEMPLATE'.chomp }
+ #{ticket.id }
+ TEMPLATE
+
+ it 'strips tag and spaces from template' do
+ expect(template.to_s).to eq('<%= d "ticket.id", true %>')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_xss_sanitized_note_examples.rb b/spec/models/concerns/has_xss_sanitized_note_examples.rb
new file mode 100644
index 000000000..8af57b840
--- /dev/null
+++ b/spec/models/concerns/has_xss_sanitized_note_examples.rb
@@ -0,0 +1,10 @@
+RSpec.shared_examples 'HasXssSanitizedNote' do |model_factory:|
+ describe 'XSS prevention' do
+ context 'with injected JS' do
+ subject { create(model_factory, note: 'test 123 some text') }
+ it 'strips out