Merge branch 'develop' of git.znuny.com:zammad/zammad into develop

This commit is contained in:
André Bauer 2019-02-15 17:34:51 +01:00
commit 6aa189ee09
32 changed files with 514 additions and 120 deletions

View file

@ -6,16 +6,20 @@ set -o errexit
set -o pipefail set -o pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)" 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')" ZAMMAD_VERSION="$(git describe --tags | sed -e 's/-[a-z0-9]\{8,\}.*//g')"
export ZAMMAD_VERSION export ZAMMAD_VERSION
if [ "${CIRCLE_BRANCH}" == 'develop' ]; then if [ "${CIRCLE_BRANCH}" == 'develop' ]; then
DOCKER_REPOSITORY="zammad-docker"
BUILD_SCRIPT="scripts/build_image.sh" BUILD_SCRIPT="scripts/build_image.sh"
DOCKER_REPOSITORY="zammad"
export DOCKER_REPOSITORY
GITHUB_REPOSITORY="zammad-docker"
elif [ "${CIRCLE_BRANCH}" == 'stable' ]; then elif [ "${CIRCLE_BRANCH}" == 'stable' ]; then
BUILD_SCRIPT="hooks/build"
DOCKER_REPOSITORY="zammad-docker-compose" DOCKER_REPOSITORY="zammad-docker-compose"
BUILD_SCRIPT="hooks/build.sh" export DOCKER_REPOSITORY
GITHUB_REPOSITORY="zammad-docker-compose"
else else
echo "branch is ${CIRCLE_BRANCH}... no docker image build needed..." echo "branch is ${CIRCLE_BRANCH}... no docker image build needed..."
exit 0 exit 0
@ -25,11 +29,11 @@ fi
echo "${DOCKER_PASSWORD}" | docker login --username="${DOCKER_USERNAME}" --password-stdin echo "${DOCKER_PASSWORD}" | docker login --username="${DOCKER_USERNAME}" --password-stdin
# clone docker repo # 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 # enter dockerfile dir
cd "${REPO_ROOT}/${DOCKER_REPOSITORY}" cd "${REPO_ROOT}/${GITHUB_REPOSITORY}"
# build & push docker image # build & push docker image
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "${REPO_ROOT}/${DOCKER_REPOSITORY}/${BUILD_SCRIPT}" source "${REPO_ROOT}/${GITHUB_REPOSITORY}/${BUILD_SCRIPT}"

View file

@ -10,7 +10,7 @@ DB_CONFIG="test:\n adapter: postgresql\n database: zammad_test\n host: 127.0.
# install build dependencies # install build dependencies
sudo apt-get update 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 if [ "${CIRCLE_JOB}" == "install-mysql" ]; then
DB_ADAPTER="mysql2" DB_ADAPTER="mysql2"

View file

@ -10,36 +10,80 @@ targets:
- nginx - nginx
- postgresql-server - postgresql-server
- which - 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: debian-8:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
- libimlib2
- libimlib2-dev
build_dependencies:
- libimlib2
- libimlib2-dev
debian-9: debian-9:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mariadb-server|sqlite - postgresql|mariadb-server|sqlite
- libimlib2
- libimlib2-dev
build_dependencies:
- libimlib2
- libimlib2-dev
ubuntu-16.04: ubuntu-16.04:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
- libimlib2
- libimlib2-dev
build_dependencies:
- libimlib2
- libimlib2-dev
ubuntu-18.04: ubuntu-18.04:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
- libimlib2
build_dependencies:
- libimlib2-dev
sles-12: sles-12:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx - nginx
- postgresql-server - 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: before:
- contrib/packager.io/before.sh - contrib/packager.io/before.sh
after: after:

View file

@ -1,7 +1,7 @@
# Change Log # Change Log
## [2.9.0](https://github.com/zammad/zammad/tree/2.9.0) (2018-xx-xx) ## [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.9.0) [Full Changelog](https://github.com/zammad/zammad/compare/2.8.0...2.10.0)
**Implemented enhancements:** **Implemented enhancements:**

View file

@ -116,6 +116,9 @@ gem 'autodiscover', git: 'https://github.com/zammad-deps/autodiscover'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm' gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm'
gem 'viewpoint' gem 'viewpoint'
# image processing
gem 'rszr'
# Gems used only for develop/test and not required # Gems used only for develop/test and not required
# in production environments by default. # in production environments by default.
group :development, :test do group :development, :test do

View file

@ -408,6 +408,7 @@ GEM
rspec-mocks (~> 3.8.0) rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rszr (0.3.2)
rubocop (0.64.0) rubocop (0.64.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
@ -584,6 +585,7 @@ DEPENDENCIES
rb-fsevent rb-fsevent
rchardet (>= 1.8.0) rchardet (>= 1.8.0)
rspec-rails rspec-rails
rszr
rubocop rubocop
rubyntlm! rubyntlm!
sassc-rails sassc-rails

View file

@ -1 +1 @@
2.9.x 2.10.x

View file

@ -808,7 +808,8 @@ class App.TicketZoom extends App.Controller
@autosaveStop() @autosaveStop()
# validate ticket form using HTML5 validity check # 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) @submitEnable(e)
@autosaveStart() @autosaveStart()
return return

View file

@ -11,9 +11,33 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
'click .js-cancel': 'cancel' 'click .js-cancel': 'cancel'
'click .js-close': '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: -> content: ->
@image = @image.replace(/view=preview/, 'view=inline')
"<div class=\"centered imagePreview\">#{@image}</div>" "<div class=\"centered imagePreview\">#{@image}</div>"
onSubmit: => onSubmit: =>
@image = @image.replace(/(\?|)view=(preview|inline)/, '')
url = "#{$(@image).attr('src')}?disposition=attachment" url = "#{$(@image).attr('src')}?disposition=attachment"
window.open(url, '_blank') window.open(url, '_blank')
onClose: =>
@unbindAll()
unbindAll: ->
$(document).unbind('keydown.image_preview')

View file

@ -421,4 +421,4 @@ class ArticleViewItem extends App.ObserverController
imageView: (e) -> imageView: (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML) new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))

View file

@ -66,7 +66,7 @@
<div class="attachment-icon"> <div class="attachment-icon">
<% if attachment.preferences && attachment.preferences['Content-Type'] && @ContentTypeIcon(attachment.preferences['Content-Type']): %> <% if attachment.preferences && attachment.preferences['Content-Type'] && @ContentTypeIcon(attachment.preferences['Content-Type']): %>
<% if @canPreview(attachment.preferences['Content-Type']): %> <% if @canPreview(attachment.preferences['Content-Type']): %>
<img src="<%= App.Config.get('api_path') %>/ticket_attachment/<%= @article.ticket_id %>/<%= @article.id %>/<%= attachment.id %>"> <img src="<%= App.Config.get('api_path') %>/ticket_attachment/<%= @article.ticket_id %>/<%= @article.id %>/<%= attachment.id %>?view=preview">
<% else: %> <% else: %>
<%- @Icon( @ContentTypeIcon(attachment.preferences['Content-Type']) ) %> <%- @Icon( @ContentTypeIcon(attachment.preferences['Content-Type']) ) %>
<% end %> <% end %>

View file

@ -2,7 +2,7 @@
class SessionsController < ApplicationController class SessionsController < ApplicationController
prepend_before_action :authentication_check, only: %i[switch_to_user list delete] 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" # "Create" a login, aka "log the user in"
def create def create

View file

@ -255,8 +255,21 @@ class TicketArticlesController < ApplicationController
disposition = sanitized_disposition 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( send_data(
file.content, content,
filename: file.filename, filename: file.filename,
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream', type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: disposition disposition: disposition

View file

@ -7,6 +7,7 @@ class Organization < ApplicationModel
include HasHistory include HasHistory
include HasSearchIndexBackend include HasSearchIndexBackend
include CanCsvImport include CanCsvImport
include ChecksHtmlSanitized
include Organization::ChecksAccess include Organization::ChecksAccess
include Organization::Assets include Organization::Assets
@ -22,6 +23,8 @@ class Organization < ApplicationModel
activity_stream_permission 'admin.role' activity_stream_permission 'admin.role'
sanitized_html :note
private private
def domain_cleanup def domain_cleanup

View file

@ -34,7 +34,7 @@ returns
=end =end
def self.add(data) def self.add(data)
data = data.stringify_keys data.deep_stringify_keys!
# lookup store_object.id # lookup store_object.id
store_object = Store::Object.create_if_not_exists(name: data['object']) store_object = Store::Object.create_if_not_exists(name: data['object'])
@ -50,9 +50,34 @@ returns
data.delete('data') data.delete('data')
data.delete('object') 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 meta data
store = Store.create!(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 store
end end
@ -165,6 +190,52 @@ returns
=begin =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 get content of file
store = Store.find(store_id) store = Store.find(store_id)
@ -204,4 +275,32 @@ returns
file.provider file.provider
end 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 end

View file

@ -6,6 +6,7 @@ class Ticket < ApplicationModel
include ChecksClientNotification include ChecksClientNotification
include ChecksLatestChangeObserved include ChecksLatestChangeObserved
include CanCsvImport include CanCsvImport
include ChecksHtmlSanitized
include HasHistory include HasHistory
include HasTags include HasTags
include HasSearchIndexBackend include HasSearchIndexBackend
@ -56,6 +57,8 @@ class Ticket < ApplicationModel
history_relation_object 'Ticket::Article' history_relation_object 'Ticket::Article'
sanitized_html :note
belongs_to :group belongs_to :group
belongs_to :organization belongs_to :organization
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket

View file

@ -82,7 +82,7 @@ returns
article['attachments'].each do |file| article['attachments'].each do |file|
next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" ) 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 inline_attachments[file[:id]] = true
break break
end end

View file

@ -6,6 +6,7 @@ class User < ApplicationModel
include HasHistory include HasHistory
include HasSearchIndexBackend include HasSearchIndexBackend
include CanCsvImport include CanCsvImport
include ChecksHtmlSanitized
include HasGroups include HasGroups
include HasRoles include HasRoles
@ -66,6 +67,8 @@ class User < ApplicationModel
:groups, :groups,
:user_groups :user_groups
sanitized_html :note
def ignore_search_indexing?(_action) def ignore_search_indexing?(_action)
# ignore internal user # ignore internal user
return true if id == 1 return true if id == 1

View file

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

View file

@ -793,7 +793,7 @@ Setting.create_if_not_exists(
}, },
], ],
}, },
state: false, state: true,
preferences: { preferences: {
prio: 400, prio: 400,
permission: ['admin.ui'], permission: ['admin.ui'],

View file

@ -17,35 +17,31 @@ examples how to use
end end
def to_s def to_s
strip_html @template.gsub(/\#{\s*(.*?)\s*}/m) do
end # 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 case input_template
# some browsers start adding HTML tags when /\At\('(.+?)'\)\z/m
# fixes https://github.com/zammad/zammad/issues/385 %(<%= t "#{sanitize_text($1)}", #{@escape} %>)
@template.gsub(/\#\{\s*t\((.+?)\)\s*\}/m) do when /\At\((.+?)\)\z/m
content = $1 %(<%= t d"#{sanitize_object_name($1)}", #{@escape} %>)
if content =~ /^'(.+?)'$/ when /\Aconfig\.(.+?)\z/m
%(<%= t "#{strip_content($1)}", #{@escape} %>) %(<%= c "#{sanitize_object_name($1)}", #{@escape} %>)
else else
%(<%= t d"#{strip_variable(content)}", #{@escape} %>) %(<%= d "#{sanitize_object_name(input_template)}", #{@escape} %>)
end end
end.gsub(/\#\{\s*config\.(.+?)\s*\}/m) do
%(<%= c "#{strip_variable($1)}", #{@escape} %>)
end.gsub(/\#\{(.*?)\}/m) do
%(<%= d "#{strip_variable($1)}", #{@escape} %>)
end end
end end
def strip_content(string) def sanitize_text(string)
string&.gsub(/\t|\r|\n/, '') string&.tr("\t\r\n", '')
&.gsub(/"/, '\"') &.gsub(/(?<!\\)(?=")/, '\\')
end end
def strip_variable(string) def sanitize_object_name(string)
string&.gsub(/\t|\r|\n|"|'|§|;/, '') string&.tr("\t\r\n\f \"'§;", '')
&.gsub(/\s*/, '')
&.gsub(/<.+?>/, '')
end end
end end

View file

@ -501,7 +501,7 @@ do(window) ->
stopPropagation: (event) -> stopPropagation: (event) ->
event.stopPropagation() event.stopPropagation()
onPaste: (e) => onDrop: (e) =>
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()

View file

@ -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 <a> tags wrapped around "ticket.id"' do
let(:template_string) { <<~'TEMPLATE'.chomp }
#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id</a>}
TEMPLATE
it 'strips tag from resulting ERB template' do
expect(template.to_s).to eq('<%= d "ticket.id", true %>')
end
end
context 'for <a> tags wrapped around "config.fqdn"' do
let(:template_string) { <<~'TEMPLATE'.chomp }
#{<a href="http://config.fqdn" title="http://config.fqdn" target="_blank">config.fqdn</a>}
TEMPLATE
it 'strips tag from resulting ERB template' do
expect(template.to_s).to eq('<%= c "fqdn", true %>')
end
end
context 'for <a> tags surrounded by whitespace' do
let(:template_string) { <<~'TEMPLATE'.chomp }
#{ <a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id </a> }
TEMPLATE
it 'strips tag and spaces from template' do
expect(template.to_s).to eq('<%= d "ticket.id", true %>')
end
end
context 'for unpaired <a> tag and trailing whitespace' do
let(:template_string) { <<~'TEMPLATE'.chomp }
#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">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

View file

@ -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 <script type="text/javascript">alert("XSS!");</script> <b>some text</b>') }
it 'strips out <script> tag' do
expect(subject.note).to eq('test 123 alert("XSS!"); <b>some text</b>')
end
end
end
end

View file

@ -1,10 +1,12 @@
require 'rails_helper' require 'rails_helper'
require 'models/concerns/can_lookup_examples' require 'models/concerns/can_lookup_examples'
require 'models/concerns/has_search_index_backend_examples' require 'models/concerns/has_search_index_backend_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
RSpec.describe Organization, type: :model do RSpec.describe Organization, type: :model do
it_behaves_like 'CanLookup' it_behaves_like 'CanLookup'
it_behaves_like 'HasSearchIndexBackend', indexed_factory: :organization it_behaves_like 'HasSearchIndexBackend', indexed_factory: :organization
it_behaves_like 'HasXssSanitizedNote', model_factory: :organization
describe '.where_or_cis' do describe '.where_or_cis' do
it 'finds instance by querying multiple attributes case insensitive' do it 'finds instance by querying multiple attributes case insensitive' do
@ -13,4 +15,5 @@ RSpec.describe Organization, type: :model do
expect(organizations).not_to be_blank expect(organizations).not_to be_blank
end end
end end
end end

View file

@ -2,11 +2,13 @@ require 'rails_helper'
require 'models/application_model_examples' require 'models/application_model_examples'
require 'models/concerns/can_be_imported_examples' require 'models/concerns/can_be_imported_examples'
require 'models/concerns/can_lookup_examples' require 'models/concerns/can_lookup_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
RSpec.describe Ticket, type: :model do RSpec.describe Ticket, type: :model do
it_behaves_like 'ApplicationModel' it_behaves_like 'ApplicationModel'
it_behaves_like 'CanBeImported' it_behaves_like 'CanBeImported'
it_behaves_like 'CanLookup' it_behaves_like 'CanLookup'
it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
subject(:ticket) { create(:ticket) } subject(:ticket) { create(:ticket) }
@ -378,4 +380,5 @@ RSpec.describe Ticket, type: :model do
end end
end end
end end
end end

View file

@ -3,6 +3,7 @@ require 'models/application_model_examples'
require 'models/concerns/has_groups_examples' require 'models/concerns/has_groups_examples'
require 'models/concerns/has_roles_examples' require 'models/concerns/has_roles_examples'
require 'models/concerns/has_groups_permissions_examples' require 'models/concerns/has_groups_permissions_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
require 'models/concerns/can_be_imported_examples' require 'models/concerns/can_be_imported_examples'
require 'models/concerns/can_lookup_examples' require 'models/concerns/can_lookup_examples'
@ -10,6 +11,7 @@ RSpec.describe User do
it_behaves_like 'ApplicationModel' it_behaves_like 'ApplicationModel'
it_behaves_like 'HasGroups', group_access_factory: :agent_user it_behaves_like 'HasGroups', group_access_factory: :agent_user
it_behaves_like 'HasRoles', group_access_factory: :agent_user it_behaves_like 'HasRoles', group_access_factory: :agent_user
it_behaves_like 'HasXssSanitizedNote', model_factory: :user
it_behaves_like 'HasGroups and Permissions', group_access_no_permission_factory: :user it_behaves_like 'HasGroups and Permissions', group_access_no_permission_factory: :user
it_behaves_like 'CanBeImported' it_behaves_like 'CanBeImported'
it_behaves_like 'CanLookup' it_behaves_like 'CanLookup'
@ -831,4 +833,5 @@ RSpec.describe User do
end end
end end
end end
end end

View file

@ -2,6 +2,17 @@ require 'rails_helper'
RSpec.describe 'Api Auth', type: :request do RSpec.describe 'Api Auth', type: :request do
around(:each) do |example|
orig = ActionController::Base.allow_forgery_protection
begin
ActionController::Base.allow_forgery_protection = true
example.run
ensure
ActionController::Base.allow_forgery_protection = orig
end
end
let(:admin_user) do let(:admin_user) do
create(:admin_user) create(:admin_user)
end end
@ -369,7 +380,10 @@ RSpec.describe 'Api Auth', type: :request do
it 'does session auth - admin' do it 'does session auth - admin' do
create(:admin_user, login: 'api-admin@example.com', password: 'adminpw') create(:admin_user, login: 'api-admin@example.com', password: 'adminpw')
post '/api/v1/signin', params: { username: 'api-admin@example.com', password: 'adminpw', fingerprint: '123456789' } get '/'
token = response.headers['CSRF-TOKEN']
post '/api/v1/signin', params: { username: 'api-admin@example.com', password: 'adminpw', fingerprint: '123456789' }, headers: { 'X-CSRF-Token' => token }
expect(response.header.key?('Access-Control-Allow-Origin')).to be_falsey expect(response.header.key?('Access-Control-Allow-Origin')).to be_falsey
expect(response).to have_http_status(201) expect(response).to have_http_status(201)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
test/data/image/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

View file

@ -1,79 +0,0 @@
require 'test_helper'
class NotificationFactoryTemplateTest < ActiveSupport::TestCase
# RSpec incoming!
def described_class
NotificationFactory::Template
end
test 'regular browser html' do
# ensures https://github.com/zammad/zammad/issues/385
template_before = '#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id</a>}'
template_after = '<%= d "ticket.id", true %>'
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
template_before = '#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">config.fqdn</a>}'
template_after = '<%= d "config.fqdn", true %>'
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'spaced browser html' do
# ensures https://github.com/zammad/zammad/issues/385
template_before = '#{ <a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id </a> }'
template_after = '<%= d "ticket.id", true %>'
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'broken browser html' do
# ensures https://github.com/zammad/zammad/issues/385
template_before = '#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id }'
template_after = '<%= d "ticket.id", true %>'
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'empty tag' do
template_before = '#{}'
template_after = '<%= d "", true %>'
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'empty tag with space' do
template_before = '#{ }'
template_after = '<%= d "", false %>'
result = described_class.new(template_before, false).to_s
assert_equal(template_after, result)
end
test 'translation' do
template_before = "\#{t('some text')}"
template_after = '<%= t "some text", false %>'
result = described_class.new(template_before, false).to_s
assert_equal(template_after, result)
template_before = "\#{t('some \"text\"')}"
template_after = '<%= t "some \"text\"", false %>'
result = described_class.new(template_before, false).to_s
assert_equal(template_after, result)
end
end

View file

@ -151,4 +151,124 @@ class StoreTest < ActiveSupport::TestCase
assert_not(attachments[0]) assert_not(attachments[0])
end end
end end
test 'test resizable' do
# not possible
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'text/plain',
content_id: 234,
},
created_by_id: 1,
)
assert_not(store.preferences.key?(:resizable))
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# not possible
store = Store.add(
object: 'SomeObject2',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], false)
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# possible (preview and inline)
store = Store.add(
object: 'SomeObject3',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload2.jpg')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_equal(store.preferences[:content_inline], true)
assert_equal(store.preferences[:content_preview], true)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_inline)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 1800)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# possible (preview only)
store = Store.add(
object: 'SomeObject4',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'image', '1000x1000.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_nil(store.preferences[:content_inline])
assert_equal(store.preferences[:content_preview], true)
assert_raises(RuntimeError) do
store.content_inline
end
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# possible (now preview or inline needed)
store = Store.add(
object: 'SomeObject5',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'image', '1x1.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
end
end end