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') "
#{@image}
" onSubmit: => + @image = @image.replace(/(\?|)view=(preview|inline)/, '') url = "#{$(@image).attr('src')}?disposition=attachment" window.open(url, '_blank') + + onClose: => + @unbindAll() + + unbindAll: -> + $(document).unbind('keydown.image_preview') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee index 33f1b2406..f5da7281e 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee @@ -421,4 +421,4 @@ class ArticleViewItem extends App.ObserverController imageView: (e) -> e.preventDefault() e.stopPropagation() - new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML) + new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget)) diff --git a/app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco index 348805aa2..aaa00e171 100644 --- a/app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco +++ b/app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco @@ -66,7 +66,7 @@
<% 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