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

This commit is contained in:
Martin Edenhofer 2016-11-14 00:29:14 +01:00
commit 80676678fc
57 changed files with 1361 additions and 346 deletions

View file

@ -28,6 +28,7 @@ end
gem 'autoprefixer-rails'
gem 'doorkeeper'
gem 'oauth2'
gem 'omniauth'

View file

@ -82,6 +82,8 @@ GEM
docile (1.1.5)
domain_name (0.5.20160826)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
eco (1.0.0)
coffee-script
eco-source
@ -366,6 +368,7 @@ DEPENDENCIES
daemons
delayed_job_active_record
diffy
doorkeeper
eco
em-websocket
email_verifier

View file

@ -78,14 +78,15 @@ class App.ControllerTable extends App.Controller
e.preventDefault()
console.log('checkboxClick', e.target)
callbackHeader = (header) ->
console.log('current header is', header)
callbackHeader = (headers) ->
console.log('current header is', headers)
# add new header item
attribute =
name: 'some name'
display: 'Some Name'
header.push attribute
console.log('new header is', header)
headers.push attribute
console.log('new header is', headers)
headers
callbackAttributes = (value, object, attribute, header, refObject) ->
console.log('data of item col', value, object, attribute, header, refObject)

View file

@ -34,20 +34,47 @@ class Index extends App.ControllerSubContent
App.Setting.unsubscribe(@subscribeApplicationId)
table = =>
callbackHeader = (headers) ->
attribute =
name: 'view'
display: 'View'
headers.splice(3, 0, attribute)
attribute =
name: 'token'
display: 'Token'
headers.splice(4, 0, attribute)
headers
callbackViewAttributes = (value, object, attribute, header, refObject) ->
value = 'X'
value
callbackTokenAttributes = (value, object, attribute, header, refObject) ->
value = 'X'
value
new App.ControllerTable(
el: @$('.js-appList')
model: App.Application
tableId: 'applications'
objects: App.Application.all()
el: @$('.js-appList')
model: App.Application
tableId: 'applications'
objects: App.Application.all()
bindRow:
events:
'click': @appEdit
bindCol:
view:
events:
'click': @appView
token:
events:
'click': @appToken
callbackHeader: [callbackHeader]
callbackAttributes:
view: [callbackViewAttributes]
token: [callbackTokenAttributes]
)
table()
#App.Application.fetchFull(
# table
# clear: true
#)
@subscribeApplicationId = App.Application.subscribe(table, initFetch: true, clear: true)
@ -82,6 +109,18 @@ class Index extends App.ControllerSubContent
value = @PasswordAccess.prop('checked')
App.Setting.set('api_password_access', value)
appToken: (id, e) ->
e.preventDefault()
new ViewAppTokenModal(
app: App.Application.find(id)
)
appView: (id, e) ->
e.preventDefault()
new ViewAppModal(
app: App.Application.find(id)
)
appNew: (e) ->
e.preventDefault()
new App.ControllerGenericNew(
@ -107,4 +146,51 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content')
)
class ViewAppModal extends App.ControllerModal
headPrefix: 'App'
buttonSubmit: false
buttonCancel: true
shown: true
small: true
events:
'click .js-select': 'selectAll'
constructor: (params) ->
@head = params.app.name
super
content: ->
"AppID: <input class=\"js-select\" type=\"text\" value=\"#{@app.uid}\">
<br>
Secret: <input class=\"js-select\" type=\"text\" value=\"#{@app.secret}\">"
class ViewAppTokenModal extends App.ControllerModal
headPrefix: 'Generate Token'
buttonSubmit: 'Generate Token'
buttonCancel: true
shown: true
small: true
events:
'click .js-select': 'selectAll'
constructor: (params) ->
@head = params.app.name
super
content: ->
"#{App.i18n.translateContent('Generate Access Token for |%s|', App.Session.get().displayNameLong())}"
onSubmit: =>
@ajax(
id: 'application_token'
type: 'POST'
url: "#{@apiPath}/applications/token"
processData: true
data: JSON.stringify(id: @app.id)
success: (data, status, xhr) =>
@contentInline = "#{App.i18n.translateContent('New Access Token is')}: <input class=\"js-select\" type=\"text\" value=\"#{data.token}\">"
@update()
@$('.js-submit').remove()
)
App.Config.set('API', { prio: 1200, name: 'API', parent: '#system', target: '#system/api', controller: Index, permission: ['admin.api'] }, 'NavBarAdmin')

View file

@ -1,17 +1,16 @@
class App.Application extends App.Model
@configure 'Application', 'name', 'scopes', 'redirect_uri'
@configure 'Application', 'name', 'redirect_uri'
@extend Spine.Model.Ajax
@url: @apiPath + '/applications'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'redirect_uri', display: 'Redirect URI', tag: 'textarea', limit: 250, null: false, note: 'Use one line per URI' },
{ name: 'scopes', display: 'Scopes', tag: 'input', note: 'Scopes define the access for' },
{ name: 'clients', display: 'Clients', tag: 'input', readonly: 1 },
{ name: 'redirect_uri', display: 'Callback URL', tag: 'textarea', limit: 250, null: false, note: 'Use one line per URI' },
{ name: 'clients', display: 'Clients', tag: 'input', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
'name', 'scopes', 'clients'
'name', 'redirect_uri', 'clients'
]
@configure_delete = true

View file

@ -51,6 +51,7 @@ curl -u <%= @S('email') %>:some_password <%= @C('http_type') %>://<%= @C('fqdn')
<button class="btn js-appNew"><%- @T('New Application') %></button>
<br>
<br>
<div>
@ -59,7 +60,7 @@ OAuth URLs are:
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="40%"><%- @T('Title') %>
<th width="40%"><%- @T('Action') %>
<th width="60%"><%- @T('URL') %>
</thead>
<tbody>
@ -69,9 +70,6 @@ OAuth URLs are:
<tr>
<td><%- @T('Getting an Access Token') %>
<td><%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/token
<tr>
<td><%- @T('Revoking Access') %>
<td><%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/applications
</tbody>
</table>
</div>

View file

@ -301,7 +301,6 @@ class ApplicationController < ActionController::Base
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user
end
=begin
# check oauth2 token based authentication
token = Doorkeeper::OAuth::Token.from_bearer_authorization(request)
if token
@ -309,19 +308,23 @@ class ApplicationController < ActionController::Base
logger.debug "oauth2 token auth check '#{token}'"
access_token = Doorkeeper::AccessToken.by_token(token)
if !access_token
raise Exceptions::NotAuthorized, 'Invalid token!'
end
# check expire
if access_token.expires_in && (access_token.created_at + access_token.expires_in) < Time.zone.now
raise Exceptions::NotAuthorized, 'OAuth2 token is expired!'
end
if access_token.scopes.empty?
raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!'
end
# if access_token.scopes.empty?
# raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!'
# end
user = User.find(access_token.resource_owner_id)
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user
end
=end
false
end

View file

@ -0,0 +1,72 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class ApplicationsController < ApplicationController
before_action { authentication_check(permission: 'admin.api') }
def index
all = Doorkeeper::Application.all
if params[:full]
assets = {}
item_ids = []
all.each { |item|
item_ids.push item.id
if !assets[:Application]
assets[:Application] = {}
end
application = item.attributes
application[:clients] = Doorkeeper::AccessToken.where(application_id: item.id).count
assets[:Application][item.id] = application
}
render json: {
record_ids: item_ids,
assets: assets,
}, status: :ok
return
end
render json: all, status: :ok
end
def token
access_token = Doorkeeper::AccessToken.create!(application_id: params[:id], resource_owner_id: current_user.id)
render json: { token: access_token.token }, status: :ok
end
def show
application = Doorkeeper::Application.find(params[:id])
render json: application, status: :ok
end
def create
application = Doorkeeper::Application.new(clean_params)
application.save!
render json: application, status: :ok
end
def update
application = Doorkeeper::Application.find(params[:id])
application.update_attributes!(clean_params)
render json: application, status: :ok
end
def destroy
application = Doorkeeper::Application.find(params[:id])
application.destroy!
render json: {}, status: :ok
end
private
def clean_params
params_data = params.permit! #.to_h
params_data.delete('application')
params_data.delete('action')
params_data.delete('controller')
params_data.delete('id')
params_data.delete('uid')
params_data.delete('secret')
params_data.delete('created_at')
params_data.delete('updated_at')
params_data
end
end

View file

@ -13,6 +13,9 @@ class Organization < ApplicationModel
has_many :members, class_name: 'User'
validates :name, presence: true
before_create :domain_cleanup
before_update :domain_cleanup
activity_stream_support permission: 'admin.role'
history_support
search_index_support
@ -21,6 +24,15 @@ class Organization < ApplicationModel
private
def domain_cleanup
return if !domain
return if domain.empty?
domain.gsub!(/@/, '')
domain.gsub!(/\s*/, '')
domain.strip!
domain.downcase!
end
def cache_delete
super

View file

@ -738,22 +738,21 @@ perform changes on ticket
}
# get subject
value['subject'].gsub!(/\#\{config\.(.+?)\}/, '<%= c "\\1", false %>')
value['subject'].gsub!(/\#\{(.+?)\}/, '<%= d "\\1", false %>')
subject = NotificationFactory::Mailer.template(
templateInline: value['subject'],
locale: 'en-en',
objects: objects,
quote: false,
)
subject = subject_build(subject)
value['body'].gsub!(/\#\{config\.(.+?)\}/, '<%= c "\\1", true %>')
value['body'].gsub!(/\#\{(.+?)\}/, '<%= d "\\1", true %>')
body = NotificationFactory::Mailer.template(
templateInline: value['body'],
locale: 'en-en',
objects: objects,
quote: true,
)
Ticket::Article.create(
ticket_id: id,
to: recipient_string,

View file

@ -121,6 +121,49 @@ returns
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order('created_at DESC').first
end
=begin
get body as html
article = Ticket::Article.find(123)
article.body_as_html
=end
def body_as_html
return '' if !body
return body if content_type && content_type =~ %r{text/html}i
body.text2html
end
=begin
get body as text
article = Ticket::Article.find(123)
article.body_as_text
=end
def body_as_text
return '' if !body
return body if !content_type || content_type.empty? || content_type =~ %r{text/plain}i
body.html2text
end
=begin
get body as text with quote sign "> " at the beginning of each line
article = Ticket::Article.find(123)
article.body_as_text
=end
def body_as_text_with_quote
body_as_text.word_wrap.message_quote
end
private
# strip not wanted chars

View file

@ -33,7 +33,7 @@ class User < ApplicationModel
include User::SearchIndex
before_validation :check_name, :check_email, :check_login, :check_password
before_create :check_preferences_default, :validate_roles
before_create :check_preferences_default, :validate_roles, :domain_based_assignment
before_update :check_preferences_default, :validate_roles
after_create :avatar_for_email_check
after_update :avatar_for_email_check
@ -856,6 +856,20 @@ returns
}
end
def domain_based_assignment
return if !email
return if organization_id
begin
domain = Mail::Address.new(email).domain
return if !domain
organization = Organization.find_by(domain: domain.downcase, domain_assignment: true)
return if !organization
self.organization_id = organization.id
rescue
return
end
end
def avatar_for_email_check
return if !email
return if email.empty?

View file

@ -1,9 +1,9 @@
Dein <%= c 'product_name' %> Passwort wurde geändert
Dein #{config.product_name} Passwort wurde geändert
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>das Passwort für Dein <%= c 'product_name' %> Account <b><%= d 'user.login' %></b> wurde kürzlich geändert.</div>
<div>das Passwort für Dein #{config.product_name} Account <b>#{user.login}</b> wurde kürzlich geändert.</div>
<br>
<div>Diese Aktivität ist Dir nicht bekannt? In diesen Fall kontaktiere Deinen System-Administrator.</div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,9 +1,9 @@
Your <%= c 'product_name' %> password has been changed
Your #{product_name} password has been changed
<p>Hi <%= d 'user.firstname' %>,</p>
<p>Hi #{user.firstname},</p>
<br>
<p>The password for your <%= c 'product_name' %> account <b><%= d 'user.login' %></b> has been changed recently.</p>
<p>The password for your #{product_name} account <b>#{user.login}</b> has been changed recently.</p>
<br>
<p>This activity is not known to you? If not, contact your system administrator.</p>
<br>
<p>Your <%= c 'product_name' %> Team</p>
<p>Your #{product_name} Team</p>

View file

@ -1,15 +1,15 @@
Zurücksetzen Deines <%= c 'product_name' %> Passworts
Zurücksetzen Deines #{config.product_name} Passworts
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>wir haben eine Anfrage zum Zurücksetzen des Passworts für <%= c 'product_name' %> Account <b><%= d 'user.login' %></b> erhalten.</div>
<div>wir haben eine Anfrage zum Zurücksetzen des Passworts für #{config.product_name} Account <b>#{user.login}</b> erhalten.</div>
<br>
<div>Wenn Sie Ihr Passwort zurückzusetzen wollen, klicken Sie auf den unten stehenden Link (oder kopieren Sie die URL in den Browser einfügen):</div>
<br>
<div><a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %>"><%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %></a></div>
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}</a></div>
<br>
<div>Dieser Link führt Sie zu einer Seite, auf der Sie Ihr Passwort ändern können.</div>
<br>
<div>Wenn Sie Ihr Passwort nicht zurücksetzen wollen, ignorieren Sie diese Meldung. Das Passwort bleibt unverändert.</div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,15 +1,15 @@
Reset your <%= c 'product_name' %> password
Reset your #{config.product_name} password
<div>Hi <%= d 'user.firstname' %>,</div>
<div>Hi #{user.firstname},</div>
<br>
<div>We received a request to reset the password for your <%= c 'product_name' %> account <b><%= d 'user.login' %></b>.</div>
<div>We received a request to reset the password for your #{config.product_name} account <b>#{user.login}</b>.</div>
<br>
<div>If you want to reset your password, click on the link below (or copy and paste the URL into your browser):</div>
<br>
<div><a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %>"><%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %></a></div>
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}</a></div>
<br>
<div>This link takes you to a page where you can change your password.</div>
<br>
<div>If you don't want to reset your password, please ignore this message. Your password will not be reseted.</div>
<br>
<div>Your <%= c 'product_name' %> Team</div>
<div>Your #{config.product_name} Team</div>

View file

@ -1,9 +1,9 @@
Bestätigung des <%= c 'product_name' %> Accounts, <%= d 'user.firstname' %> <%= d 'user.lastname' %>
Bestätigung des #{config.product_name} Accounts, #{user.firstname} #{user.lastname}
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>bestätige Deine E-Mail-Adresse um Deine Registrierung bei <%= c 'product_name' %> abzuschließen. Es ist einfach - klick einfach auf den Link unten.</div>
<div>bestätige Deine E-Mail-Adresse um Deine Registrierung bei #{config.product_name} abzuschließen. Es ist einfach - klick einfach auf den Link unten.</div>
<br>
<div><a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#email_verify/<%= d 'token.name' %>"><%= c 'http_type' %>://<%= c 'fqdn' %>/#email_verify/<%= d 'token.name' %></a></div>
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}</a></div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,9 +1,9 @@
Confirm your <%= c 'product_name' %> account, <%= d 'user.firstname' %> <%= d 'user.lastname' %>
Confirm your #{config.product_name} account, #{user.firstname} #{user.lastname}
<div>Hi <%= d 'user.firstname' %>,</div>
<div>Hi #{user.firstname},</div>
<br>
<div>Confirm your email address to complete your <%= c 'product_name' %> account. It's easy, just click the link below.</div>
<div>Confirm your email address to complete your #{config.product_name} account. It's easy, just click the link below.</div>
<br>
<div><a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#email_verify/<%= d 'token.name' %>"><%= c 'http_type' %>://<%= c 'fqdn' %>/#email_verify/<%= d 'token.name' %></a></div>
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}</a></div>
<br>
<div>Your <%= c 'product_name' %> Team</div>
<div>Your #{config.product_name} Team</div>

View file

@ -1,9 +1,9 @@
Test Ticket!
<div>Hallo <%= d 'agent.firstname' %>,</div>
<div>Hallo #{agent.firstname},</div>
<br>
<div>dies ist ein <b>Test Ticket</b>. Ich bin ein Kunde und benötige Hilfe! :)</div>
<br>
<div><%= d 'customer.fullname' %></div>
<div>#{customer.fullname}</div>
<br>
<div>Das Zammad Projekt</div>

View file

@ -1,9 +1,9 @@
Test Ticket!
<div>Dear <%= d 'agent.firstname' %>,</div>
<div>Dear #{agent.firstname},</div>
<br>
<div>This is a <b>test ticket</b>. I'm a customer and I need some help! :)</div>
<br>
<div><%= d 'customer.fullname' %></div>
<div>#{customer.fullname}</div>
<br>
<div>The Zammad Project</div>

View file

@ -1,24 +1,24 @@
Neues Ticket (<%= d 'ticket.title' %>)
Neues Ticket (#{ticket.title})
<div>Hallo <%= d 'recipient.firstname' %>,</div>
<div>Hallo #{recipient.firstname},</div>
<br>
<div>es wurde ein neues Ticket (<%= d 'ticket.title' %>) von "<b><%= d 'current_user.longname' %></b>" erstellt.</div>
<div>es wurde ein neues Ticket (#{ticket.title}) von "<b>#{current_user.longname}</b>" erstellt.</div>
<br>
<div>
<%= t 'Group' %>: <%= d 'ticket.group.name' %><br>
<%= t 'Owner' %>: <%= d 'ticket.owner.fullname' %><br>
<%= t 'State' %>: <%= t d 'ticket.state.name' %><br>
#{t('Group')}: #{ticket.group.name}<br>
#{t('Owner')}: #{ticket.owner.fullname}<br>
#{t('State')}: #{t(ticket.state.name)}<br>
</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,24 +1,24 @@
New Ticket (<%= d 'ticket.title' %>)
New Ticket (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>A new ticket (<%= d 'ticket.title' %>) has been created by "<b><%= d 'current_user.longname' %></b>".</div>
<div>A new ticket (#{ticket.title}) has been created by "<b>#{current_user.longname}</b>".</div>
<br>
<div>
<%= t 'Group' %>: <%= d 'ticket.group.name' %><br>
<%= t 'Owner' %>: <%= d 'ticket.owner.fullname' %><br>
<%= t 'State' %>: <%= t d 'ticket.state.name' %><br>
#{t('Group')}: #{ticket.group.name}<br>
#{t('Owner')}: #{ticket.owner.fullname}<br>
#{t('State')}: #{t(ticket.state.name)}<br>
</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Ticket ist eskaliert (<%= d 'ticket.title' %>)
Ticket ist eskaliert (#{ticket.title})
<div>Hallo <%= d 'recipient.firstname' %>,</div>
<div>Hallo #{recipient.firstname},</div>
<br>
<div>Ticket (<%= d 'ticket.title' %>) von "<b><%= d 'ticket.customer.longname' %></b>" ist seit "<%= d 'ticket.escalation_at' %>" eskaliert!</div>
<div>Ticket (#{ticket.title}) von "<b>#{ticket.customer.longname}</b>" ist seit "#{ticket.escalation_at}" eskaliert!</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Ticket is escalated (<%= d 'ticket.title' %>)
Ticket is escalated (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>A ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" is escalated since "<%= d 'ticket.escalation_at' %>"!</div>
<div>A ticket (#{ticket.title}) from "<b>#{ticket.customer.longname}</b>" is escalated since "#{ticket.escalation_at}"!</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Ticket wird eskalieren (<%= d 'ticket.title' %>)
Ticket wird eskalieren (#{ticket.title})
<div>Hallo <%= d 'recipient.firstname' %>,</div>
<div>Hallo #{recipient.firstname},</div>
<br>
<div>Ticket (<%= d 'ticket.title' %>) von "<b><%= d 'ticket.customer.longname' %></b>" wird um "<%= d 'ticket.escalation_at' %>" eskalieren!</div>
<div>Ticket (#{ticket.title}) von "<b>#{ticket.customer.longname}</b>" wird um "#{ticket.escalation_at}" eskalieren!</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Ticket will escalate (<%= d 'ticket.title' %>)
Ticket will escalate (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>A ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" will escalate at "<%= d 'ticket.escalation_at' %>"!</div>
<div>A ticket (#{ticket.title}) from "<b>#{ticket.customer.longname}</b>" will escalate at "#{ticket.escalation_at}"!</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Warten auf Erinnerung erreicht! (<%= d 'ticket.title' %>)
Warten auf Erinnerung erreicht! (#{ticket.title})
<div>Hallo <%= d 'recipient.firstname' %>,</div>
<div>Hallo #{recipient.firstname},</div>
<br>
<div>dieses Ticket benötigt Deine Aufmerksamkeit, warten auf Erinnerung für (<%= d 'ticket.title' %>) mit dem Kunden "<b><%= d 'ticket.customer.longname' %></b>" ist erreicht.</div>
<div>dieses Ticket benötigt Deine Aufmerksamkeit, warten auf Erinnerung für (#{ticket.title}) mit dem Kunden "<b>#{ticket.customer.longname}</b>" ist erreicht.</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,18 +1,18 @@
Reminder reached (<%= d 'ticket.title' %>)
Reminder reached (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>A ticket needs attention, reminder reached for (<%= d 'ticket.title' %>) with customer "<b><%= d 'ticket.customer.longname' %></b>".</div>
<div>A ticket needs attention, reminder reached for (#{ticket.title}) with customer "<b>#{ticket.customer.longname}</b>".</div>
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,14 +1,14 @@
Ticket aktualisiert (<%= d 'ticket.title' %>)
Ticket aktualisiert (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>
Ticket (<%= d 'ticket.title' %>) wurde von "<b><%= d 'current_user.longname' %></b>" aktualisiert.
Ticket (#{ticket.title}) wurde von "<b>#{current_user.longname}</b>" aktualisiert.
</div>
<br>
<% if @objects[:changes] && !@objects[:changes].empty? %>
<div>
<%= t 'Changes' %>:<br>
#{t('Changes')}:<br>
<% @objects[:changes].each do |key, value| %>
<%= t key %>: <%= h value[0] %> -&gt; <%= h value[1] %><br>
<% end %>
@ -17,13 +17,13 @@ Ticket (<%= d 'ticket.title' %>) wurde von "<b><%= d 'current_user.longname' %><
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,14 +1,14 @@
Updated Ticket (<%= d 'ticket.title' %>)
Updated Ticket (#{ticket.title})
<div>Hi <%= d 'recipient.firstname' %>,</div>
<div>Hi #{recipient.firstname},</div>
<br>
<div>
Ticket (<%= d 'ticket.title' %>) has been updated by "<b><%= d 'current_user.longname' %></b>".
Ticket (#{ticket.title}) has been updated by "<b>#{current_user.longname}</b>".
</div>
<br>
<% if @objects[:changes] && !@objects[:changes].empty? %>
<div>
<%= t 'Changes' %>:<br>
#{t('Changes')}:<br>
<% @objects[:changes].each do |key, value| %>
<%= t key %>: <%= h value[0] %> -&gt; <%= h value[1] %><br>
<% end %>
@ -17,13 +17,13 @@ Ticket (<%= d 'ticket.title' %>) has been updated by "<b><%= d 'current_user.lon
<br>
<% if @objects[:article] %>
<div>
<%= t 'Information' %>:
#{t('Information')}:
<blockquote type="cite">
<%= a_html 'article' %>
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>" target="zammad_app"><%= t 'View this in Zammad' %></a>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
</div>

View file

@ -1,19 +1,19 @@
<%= c 'product_name' %>-Anmeldung erfasst von einem neuen Gerät
#{config.product_name}-Anmeldung erfasst von einem neuen Gerät
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>es sieht aus, als ob Du Dich mit <b>einem neuen Gerät</b> um "<%= d 'user_device.created_at' %>" angemeldet hast:</div>
<div>es sieht aus, als ob Du Dich mit <b>einem neuen Gerät</b> um "#{user_device.created_at}" angemeldet hast:</div>
<br>
<div>
Dein Gerät: <%= d 'user_device.name' %><br>
Deine Lokation (relativ): <%= d 'user_device.location' %><br>
Deine IP: <%= d 'user_device.ip' %><br>
Dein Gerät: #{user_device.name}<br>
Deine Lokation (relativ): #{user_device.location}<br>
Deine IP: #{user_device.ip}<br>
</div>
<br>
<div>Das Gerät wurde in die Liste der bekannten Geräte hinzugefügt, diese Liste kannst Du hier einsehen:</div>
<br>
<div><%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/devices</div>
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
<br>
<div>Wenn dies nicht Du warst, entferne das Gerät aus der Liste, ändere Dein Account-Passwort und kontaktieren Deinen Administrator. Jemand könnte unberechtigten Zugriff auf Dein Konto bekommen haben.</div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,19 +1,19 @@
<%= c 'product_name' %> signin detected from a new device
#{config.product_name} signin detected from a new device
<div>Hi <%= d 'user.firstname' %>,</div>
<div>Hi #{user.firstname},</div>
<br>
<div>It looks like you signed into your account <b>using a new device</b> on "<%= d 'user_device.created_at' %>":</div>
<div>It looks like you signed into your account <b>using a new device</b> on "#{user_device.created_at}":</div>
<br>
<div>
Your device: <%= d 'user_device.name' %><br>
Your location (relative): <%= d 'user_device.location' %><br>
Your IP: <%= d 'user_device.ip' %><br>
Your device: #{user_device.name}<br>
Your location (relative): #{user_device.location}<br>
Your IP: #{user_device.ip}<br>
</div>
<br>
<div>Your device has been added to your list of known devices, which you can view here:</div>
<br>
<div><%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/devices</div>
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
<br>
<div>If this wasn't you, remove the device, changing your account password, and contacting your administrator. Somebody might have gained unauthorized access to your account.</div>
<br>
<div>Your <%= c 'product_name' %> Team</div>
<div>Your #{config.product_name} Team</div>

View file

@ -1,19 +1,19 @@
<%= c 'product_name' %>-Anmeldung von einem anderen Land erfasst
#{config.product_name}-Anmeldung von einem anderen Land erfasst
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>es sieht aus, als ob Du Dich um "<%= d 'user_device.created_at' %>" von einem <b>bekannten Gerät aus einem anderen Land angemeldet hast</b>:</div>
<div>es sieht aus, als ob Du Dich um "#{user_device.created_at}" von einem <b>bekannten Gerät aus einem anderen Land angemeldet hast</b>:</div>
<br>
<div>
Dein Gerät: <%= d 'user_device.name' %><br>
Deine Lokation (relativ): <%= d 'user_device.location' %><br>
Deine IP: <%= d 'user_device.ip' %><br>
Dein Gerät: #{user_device.name}<br>
Deine Lokation (relativ): #{user_device.location}<br>
Deine IP: #{user_device.ip}<br>
</div>
<br>
<div>Das neue Land wurde in die Liste der bekannten Geräte hinzugefügt, diese Liste kannst Du hier einsehen:</div>
<br>
<div><%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/devices</div>
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
<br>
<div>Wenn dies nicht Du warst, entferne die neue Lokation aus der Liste, ändere Dein Account-Passwort und kontaktieren Deinen Administrator. Jemand könnte unberechtigten Zugriff auf Dein Konto bekommen haben.</div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,19 +1,19 @@
<%= c 'product_name' %> signin detected from a new country
#{config.product_name} signin detected from a new country
<div>Hi <%= d 'user.firstname' %>,</div>
<div>Hi #{user.firstname},</div>
<br>
<div>It looks like you used your account with an <b>known device but from a new country</b> on "<%= d 'user_device.created_at' %>":</div>
<div>It looks like you used your account with an <b>known device but from a new country</b> on "#{user_device.created_at}":</div>
<br>
<div>
Your device: <%= d 'user_device.name' %><br>
Your location (relative): <%= d 'user_device.location' %><br>
Your IP: <%= d 'user_device.ip' %><br>
Your device: #{user_device.name}<br>
Your location (relative): #{user_device.location}<br>
Your IP: #{user_device.ip}<br>
</div>
<br>
<div>The country has been added to your list of known devices, which you can view here:</div>
<br>
<div><%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/devices</div>
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
<br>
<div>If this wasn't you, remove the device, changing your account password, and contacting your administrator. Somebody might have gained unauthorized access to your account.</div>
<br>
<div>Your <%= c 'product_name' %> Team</div>
<div>Your #{config.product_name} Team</div>

View file

@ -1,13 +1,13 @@
Einladung zu <%= c 'product_name' %> über <%= c 'fqdn' %>
Einladung zu #{config.product_name} über #{config.fqdn}
<div>Hallo <%= d 'user.firstname' %>,</div>
<div>Hallo #{user.firstname},</div>
<br>
<div>Ich (<%= d 'current_user.firstname' %> <%= d 'current_user.lastname' %>) möchte Dich zu <%= c 'product_name' %> einladen - unsere Kundensupport / Ticket System Platform.</div>
<div>Ich (#{current_user.firstname} #{current_user.lastname}) möchte Dich zu #{config.product_name} einladen - unsere Kundensupport / Ticket System Platform.</div>
<br>
<div>Um sich anzumelden kann <a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %>">hier</a> das Password gesetzt werden.</div>
<div>Um sich anzumelden kann <a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">hier</a> das Password gesetzt werden.</div>
<br>
<div>Enjoy,</div>
<br>
<div><%= d 'current_user.firstname' %> <%= d 'current_user.lastname' %></div>
<div>#{current_user.firstname} #{current_user.lastname}</div>
<br>
<div>Dein <%= c 'product_name' %> Team</div>
<div>Dein #{config.product_name} Team</div>

View file

@ -1,13 +1,13 @@
Invitation to <%= c 'product_name' %> at <%= c 'fqdn' %>
Invitation to #{config.product_name} at #{config.fqdn}
<div>Hi <%= d 'user.firstname' %>,</div>
<div>Hi #{user.firstname},</div>
<br>
<div>I (<%= d 'current_user.firstname' %> <%= d 'current_user.lastname' %>) invite you to <%= c 'product_name' %> - our customer support / ticket system platform.</div>
<div>I (#{current_user.firstname} #{current_user.lastname}) invite you to #{config.product_name} - our customer support / ticket system platform.</div>
<br>
<div>Click <a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#password_reset_verify/<%= d 'token.name' %>">here</a> and set your password.</div>
<div>Click <a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">here</a> and set your password.</div>
<br>
<div>Enjoy,</div>
<br>
<div><%= d 'current_user.firstname' %> <%= d 'current_user.lastname' %></div>
<div>#{current_user.firstname} #{current_user.lastname}</div>
<br>
<div>Your <%= c 'product_name' %> Team</div>
<div>Your #{config.product_name} Team</div>

View file

@ -1,9 +1,9 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Created by <%= d 'current_user.longname' %> at <%= d 'ticket.updated_at' %>_
* <%= t 'Group' %>: <%= d 'ticket.group.name' %>
* <%= t 'Owner' %>: <%= d 'ticket.owner.fullname' %>
* <%= t 'State' %>: <%= t d 'ticket.state.name' %>
# #{ticket.title}
_<#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}|Ticket##{ticket.number}>: Created by #{current_user.longname} at #{ticket.updated_at}_
* #{t('Group')}: #{ticket.group.name}
* #{t('Owner')}: #{ticket.owner.fullname}
* #{t('State')}: #{t(ticket.state.name)}
<% if @objects[:article] %>
<%= a_text 'article' %>
#{article.body_as_text}
<% end %>

View file

@ -1,7 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Escalated at <%= d 'ticket.escalation_at' %>_
A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" is escalated since "<%= d 'ticket.escalation_at' %>"!
# #{ticket.title}
_<#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}|Ticket##{ticket.number}>: Escalated at #{ticket.escalation_at}_
A ticket (#{ticket.title}) from "#{ticket.customer.longname}" is escalated since "#{ticket.escalation_at}"!
<% if @objects[:article] %>
<%= a_text 'article' %>
#{article.body_as_text}
<% end %>

View file

@ -1,7 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Will escalate at <%= d 'ticket.escalation_at' %>_
A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_at' %>"!
# #{ticket.title}
_<#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}|Ticket##{ticket.number}>: Will escalate at #{ticket.escalation_at}_
A ticket (#{ticket.title}) from "#{ticket.customer.longname}" will escalate at "#{ticket.escalation_at}"!
<% if @objects[:article] %>
<%= a_text 'article' %>
#{article.body_as_text}
<% end %>

View file

@ -1,7 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Reminder reached!_
A ticket needs attention, reminder reached for (<%= d 'ticket.title' %>) with customer "*<%= d 'ticket.customer.longname' %>*".
# #{ticket.title}
_<#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}|Ticket##{ticket.number}>: Reminder reached!_
A ticket needs attention, reminder reached for (#{ticket.title}) with customer "*#{ticket.customer.longname}*".
<% if @objects[:article] %>
<%= a_text 'article' %>
#{article.body_as_text}
<% end %>

View file

@ -1,5 +1,5 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Updated by <%= d 'current_user.longname' %> at <%= d 'ticket.updated_at' %>_
# #{ticket.title}
_<#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}|Ticket##{ticket.number}>: Updated by #{current_user.longname} at #{ticket.updated_at}_
<% if @objects[:changes] && !@objects[:changes].empty? %>
<% @objects[:changes].each do |key, value| %>
* <%= t key %>: <%= h value[0] %> -> <%= h value[1] %>
@ -7,5 +7,5 @@ _<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticke
<% end %>
<% if @objects[:article] %>
<%= a_text 'article' %>
#{article.body_as_text}
<% end %>

View file

@ -0,0 +1,112 @@
Doorkeeper.configure do
# Change the ORM that doorkeeper will use (needs plugins)
orm :active_record
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
# fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"
# Put your resource owner authentication logic here.
# Example implementation:
User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url)
end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
# admin_authenticator do
# # Put your admin authentication logic here.
# # Example implementation:
# Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url)
# end
# Authorization Code expiration time (default 10 minutes).
# authorization_code_expires_in 10.minutes
# Access token expiration time (default 2 hours).
# If you want to disable expiration, set this to nil.
# access_token_expires_in 2.hours
# Assign a custom TTL for implicit grants.
# custom_access_token_expires_in do |oauth_client|
# oauth_client.application.additional_settings.implicit_oauth_expiration
# end
# Use a custom class for generating the access token.
# https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator
# access_token_generator '::Doorkeeper::JWT'
# The controller Doorkeeper::ApplicationController inherits from.
# Defaults to ActionController::Base.
# https://github.com/doorkeeper-gem/doorkeeper#custom-base-controller
# base_controller 'ApplicationController'
# Reuse access token for the same resource owner within an application (disabled by default)
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
# reuse_access_token
# Issue access tokens with refresh token (disabled by default)
# use_refresh_token
# Provide support for an owner to be assigned to each registered application (disabled by default)
# Optional parameter confirmation: true (default false) if you want to enforce ownership of
# a registered application
# Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support
# enable_application_owner confirmation: false
# Define access token scopes for your provider
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
# default_scopes :public
# optional_scopes :write, :update
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
# falls back to the `:client_id` and `:client_secret` params from the `params` object.
# Check out the wiki for more information on customization
# client_credentials :from_basic, :from_params
# Change the way access token is authenticated from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
# falls back to the `:access_token` or `:bearer_token` params from the `params` object.
# Check out the wiki for more information on customization
# access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
# Change the native redirect uri for client apps
# When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider
# The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
# (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
#
# native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
# Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
# by default in non-development environments). OAuth2 delegates security in
# communication to the HTTPS protocol so it is wise to keep this enabled.
#
# force_ssl_in_redirect_uri !Rails.env.development?
# Specify what grant flows are enabled in array of Strings. The valid
# strings and the flows they enable are:
#
# "authorization_code" => Authorization Code Grant Flow
# "implicit" => Implicit Grant Flow
# "password" => Resource Owner Password Credentials Grant Flow
# "client_credentials" => Client Credentials Grant Flow
#
# If not specified, Doorkeeper enables authorization_code and
# client_credentials.
#
# implicit and password grant flows have risks that you should understand
# before enabling:
# http://tools.ietf.org/html/rfc6819#section-4.4.2
# http://tools.ietf.org/html/rfc6819#section-4.4.3
#
# grant_flows %w(authorization_code client_credentials)
# Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step.
# For example if dealing with a trusted application.
# skip_authorization do |resource_owner, client|
# client.superapp? or resource_owner.admin?
# end
# WWW-Authenticate Realm (default "Doorkeeper").
# realm "Doorkeeper"
end

View file

@ -0,0 +1,124 @@
en:
activerecord:
attributes:
doorkeeper/application:
name: 'Name'
redirect_uri: 'Redirect URI'
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: 'cannot contain a fragment.'
invalid_uri: 'must be a valid URI.'
relative_uri: 'must be an absolute URI.'
secured_uri: 'must be an HTTPS/SSL URI.'
doorkeeper:
applications:
confirmations:
destroy: 'Are you sure?'
buttons:
edit: 'Edit'
destroy: 'Destroy'
submit: 'Submit'
cancel: 'Cancel'
authorize: 'Authorize'
form:
error: 'Whoops! Check your form for possible errors'
help:
redirect_uri: 'Use one line per URI'
native_redirect_uri: 'Use %{native_redirect_uri} for local tests'
scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
edit:
title: 'Edit application'
index:
title: 'Your applications'
new: 'New Application'
name: 'Name'
callback_url: 'Callback URL'
new:
title: 'New Application'
show:
title: 'Application: %{name}'
application_id: 'Application Id'
secret: 'Secret'
scopes: 'Scopes'
callback_urls: 'Callback urls'
actions: 'Actions'
authorizations:
buttons:
authorize: 'Authorize'
deny: 'Deny'
error:
title: 'An error has occurred'
new:
title: 'Authorization required'
prompt: 'Authorize %{client_name} to use your account?'
able_to: 'This application will be able to'
show:
title: 'Authorization code'
authorized_applications:
confirmations:
revoke: 'Are you sure?'
buttons:
revoke: 'Revoke'
index:
title: 'Your authorized applications'
application: 'Application'
created_at: 'Created At'
date_format: '%Y-%m-%d %H:%M:%S'
errors:
messages:
# Common error messages
invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
invalid_redirect_uri: 'The redirect uri included is not valid.'
unauthorized_client: 'The client is not authorized to perform this request using this method.'
access_denied: 'The resource owner or authorization server denied the request.'
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
#configuration error messages
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.'
# Access grant errors
unsupported_response_type: 'The authorization server does not support this response type.'
# Access token errors
invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
# Password Access token errors
invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found'
invalid_token:
revoked: "The access token was revoked"
expired: "The access token expired"
unknown: "The access token is invalid"
flash:
applications:
create:
notice: 'Application created.'
destroy:
notice: 'Application deleted.'
update:
notice: 'Application updated.'
authorized_applications:
destroy:
notice: 'Application revoked.'
layouts:
admin:
nav:
oauth2_provider: 'OAuth2 Provider'
applications: 'Applications'
home: 'Home'
application:
title: 'OAuth authorization required'

View file

@ -0,0 +1,14 @@
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match api_path + '/applications', to: 'applications#index', via: :get
match api_path + '/applications/:id', to: 'applications#show', via: :get
match api_path + '/applications', to: 'applications#create', via: :post
match api_path + '/applications/:id', to: 'applications#update', via: :put
match api_path + '/applications/token', to: 'applications#token', via: :post
# oauth2 provider routes
use_doorkeeper do
skip_controllers :applications, :authorized_applications
end
end

View file

@ -125,6 +125,8 @@ class CreateBase < ActiveRecord::Migration
create_table :organizations do |t|
t.string :name, limit: 100, null: false
t.boolean :shared, null: false, default: true
t.string :domain, limit: 250, null: true, default: ''
t.boolean :domain_assignment, null: false, default: false
t.boolean :active, null: false, default: true
t.string :note, limit: 250, null: true, default: ''
t.integer :updated_by_id, null: false
@ -132,6 +134,7 @@ class CreateBase < ActiveRecord::Migration
t.timestamps limit: 3, null: false
end
add_index :organizations, [:name], unique: true
add_index :organizations, [:domain]
create_table :roles_users, id: false do |t|
t.integer :user_id

View file

@ -0,0 +1,68 @@
class CreateDoorkeeperTables < ActiveRecord::Migration
def change
create_table :oauth_applications do |t|
t.string :name, null: false
t.string :uid, null: false
t.string :secret, null: false
t.text :redirect_uri, null: false
t.string :scopes, null: false, default: ''
t.timestamps null: false
end
add_index :oauth_applications, :uid, unique: true
create_table :oauth_access_grants do |t|
t.integer :resource_owner_id, null: false
t.references :application, null: false
t.string :token, null: false
t.integer :expires_in, null: false
t.text :redirect_uri, null: false
t.datetime :created_at, null: false
t.datetime :revoked_at
t.string :scopes
end
add_index :oauth_access_grants, :token, unique: true
add_foreign_key(
:oauth_access_grants,
:oauth_applications,
column: :application_id
)
create_table :oauth_access_tokens do |t|
t.integer :resource_owner_id
t.references :application
# If you use a custom token generator you may need to change this column
# from string to text, so that it accepts tokens larger than 255
# characters. More info on custom token generators in:
# https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator
#
# t.text :token, null: false
t.string :token, null: false
t.string :refresh_token
t.integer :expires_in
t.datetime :revoked_at
t.datetime :created_at, null: false
t.string :scopes
# If there is a previous_refresh_token column,
# refresh tokens will be revoked after a related access token is used.
# If there is no previous_refresh_token column,
# previous tokens are revoked as soon as a new access token is created.
# Comment out this line if you'd rather have refresh tokens
# instantly revoked.
t.string :previous_refresh_token, null: false, default: ''
end
add_index :oauth_access_tokens, :token, unique: true
add_index :oauth_access_tokens, :resource_owner_id
add_index :oauth_access_tokens, :refresh_token, unique: true
add_foreign_key(
:oauth_access_tokens,
:oauth_applications,
column: :application_id
)
end
end

View file

@ -0,0 +1,85 @@
class OrganizationDomainBasedAssignment < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :organizations, :domain, :string, limit: 250, null: true, default: ''
add_column :organizations, :domain_assignment, :boolean, null: false, default: false
add_index :organizations, [:domain]
ObjectManager::Attribute.add(
force: true,
object: 'Organization',
name: 'domain_assignment',
display: 'Domain based assignment',
data_type: 'boolean',
data_option: {
null: true,
default: false,
note: 'Assign Users based on users domain.',
item_class: 'formGroup--halfSize',
options: {
true: 'yes',
false: 'no',
},
translate: true,
},
editable: false,
active: true,
screens: {
edit: {
Admin: {
null: false,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 1410,
updated_by_id: 1,
created_by_id: 1,
)
ObjectManager::Attribute.add(
force: true,
object: 'Organization',
name: 'domain',
display: 'Domain',
data_type: 'input',
data_option: {
type: 'text',
maxlength: 150,
null: true,
item_class: 'formGroup--halfSize',
},
editable: false,
active: true,
screens: {
edit: {
'-all-' => {
null: true,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 1420,
updated_by_id: 1,
created_by_id: 1,
)
Cache.clear
end
end

View file

@ -4660,6 +4660,75 @@ ObjectManager::Attribute.add(
position: 1400,
)
ObjectManager::Attribute.add(
force: true,
object: 'Organization',
name: 'domain_assignment',
display: 'Domain based assignment',
data_type: 'boolean',
data_option: {
null: true,
default: false,
note: 'Assign Users based on users domain.',
item_class: 'formGroup--halfSize',
options: {
true: 'yes',
false: 'no',
},
translate: true,
},
editable: false,
active: true,
screens: {
edit: {
Admin: {
null: false,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 1410,
)
ObjectManager::Attribute.add(
force: true,
object: 'Organization',
name: 'domain',
display: 'Domain',
data_type: 'input',
data_option: {
type: 'text',
maxlength: 150,
null: true,
item_class: 'formGroup--halfSize',
},
editable: false,
active: true,
screens: {
edit: {
'-all-' => {
null: true,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 1420,
)
ObjectManager::Attribute.add(
force: true,
object: 'Organization',

View file

@ -6,7 +6,7 @@ get notification settings for user and notification type
result = NotificationFactory::Mailer.notification_settings(user, ticket, type)
type: create | update | reminder_reached | pending
type: create | update | reminder_reached | escalation (escalation_warning)
returns
@ -21,6 +21,15 @@ returns
=end
def self.notification_settings(user, ticket, type)
# map types if needed
map = {
'escalation_warning' => 'escalation'
}
if map[type]
type = map[type]
end
return if !user.preferences
return if !user.preferences['notification_config']
matrix = user.preferences['notification_config']['matrix']
@ -190,11 +199,12 @@ retunes
)
result = NotificationFactory::Mailer.template(
templateInline: "Invitation to <%= c 'product_name' %> at <%= c 'fqdn' %>",
templateInline: "Invitation to \#{config.product_name} at \#{config.fqdn}",
locale: 'en-us',
objects: {
recipient: User.find(2),
},
quote: true, # html quoting
)
only raw subject/body
@ -221,7 +231,7 @@ returns
def self.template(data)
if data[:templateInline]
return NotificationFactory::Renderer.new(data[:objects], data[:locale], data[:templateInline], false).render
return NotificationFactory::Renderer.new(data[:objects], data[:locale], data[:templateInline], data[:quote]).render
end
template = NotificationFactory.template_read(

View file

@ -9,7 +9,7 @@ examples how to use
ticket: Ticket.first,
},
'de-de',
'some template <b><%= d "ticket.title", false %></b> <%= c "fqdn", false %>',
'some template <b>#{ticket.title}</b> {config.fqdn}',
false
).render
@ -18,7 +18,7 @@ examples how to use
ticket: Ticket.first,
},
'de-de',
'some template <b><%= d "ticket.title", true %></b> <%= c "fqdn", true %>',
'some template <b>#{ticket.title}</b> #{config.fqdn}',
).render
=end
@ -26,7 +26,7 @@ examples how to use
def initialize(objects, locale, template, escape = true)
@objects = objects
@locale = locale || 'en-us'
@template = NotificationFactory::Template.new(template)
@template = NotificationFactory::Template.new(template, escape)
@escape = escape
end
@ -41,6 +41,25 @@ examples how to use
# do validaton, ignore some methodes
return "\#{#{key} / not allowed}" if !data_key_valid?(key)
# aliases
map = {
'article.body' => 'article.body_as_text_with_quote.text2html',
}
if map[key]
key = map[key]
end
# escape in html mode
if escape
no_escape = {
'article.body_as_html' => true,
'article.body_as_text_with_quote.text2html' => true,
}
if no_escape[key]
escape = false
end
end
value = nil
object_methods = key.split('.')
object_name = object_methods.shift
@ -76,7 +95,11 @@ examples how to use
value = "\#{#{object_name}.#{object_methods_s} / no such method}"
break
end
object_refs = object_refs.send(method.to_sym)
begin
object_refs = object_refs.send(method.to_sym)
rescue => e
object_refs = "\#{#{object_name}.#{object_methods_s} / e.message}"
end
}
placeholder = if !value
object_refs
@ -100,27 +123,6 @@ examples how to use
escaping(translation, escape)
end
# a_html - article body in html
# a_html(article)
def a_html(article)
content_type = d "#{article}.content_type", false
if content_type =~ /html/
return d "#{article}.body", false
end
d("#{article}.body", false).text2html
end
# a_text - article body in text
# a_text(article)
def a_text(article)
content_type = d "#{article}.content_type", false
body = d "#{article}.body", false
if content_type =~ /html/
body = body.html2text
end
(body.strip + "\n").gsub(/^(.*?)$/, '> \\1')
end
# h - htmlEscape
# h('fqdn', htmlEscape)
def h(key)
@ -137,7 +139,7 @@ examples how to use
end
def data_key_valid?(key)
return false if key =~ /`|\.(|\s*)(save|destroy|delete|remove|drop|update|create|new|all|where|find)/i
return false if key =~ /`|\.(|\s*)(save|destroy|delete|remove|drop|update|create|new|all|where|find)/i && key !~ /(update|create)d_at/i
true
end

View file

@ -33,8 +33,8 @@ returns
type: 'slack',
)
message_subject = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:subject]).render
message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:body]).render
message_subject = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:subject], false).render
message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:body], false).render
if !data[:raw]
application_template = NotificationFactory.application_template_read(
@ -43,7 +43,7 @@ returns
)
data[:objects][:message] = message_body
data[:objects][:standalone] = data[:standalone]
message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], application_template).render
message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], application_template, false).render
end
{
subject: message_subject.strip!,

View file

@ -5,13 +5,15 @@ class NotificationFactory::Template
examples how to use
cleaned_template = NotificationFactory::Template.new(
'some template <b><%= d "ticket.title", false %></b> <%= c "fqdn", false %>',
'some template <b>#{ticket.title}</b> #{config.fqdn}',
true,
).to_s
=end
def initialize(template)
def initialize(template, escape)
@template = template
@escape = escape
end
def to_s
@ -23,7 +25,35 @@ examples how to use
def strip_html
# some browsers start adding HTML tags
# fixes https://github.com/zammad/zammad/issues/385
@template.gsub!(%r{#\{\s*<[^>]+>([^<]+)</[^>]+>\s*\}}, '\1')
@template.gsub!(/#\{\s*<[^>]+>([^<]+)\s*\}/, '\1')
@template.gsub!(/\#\{\s*t\((.+?)\)\s*\}/m) do
content = $1
if content =~ /^'(.+?)'$/
"<%= t \"#{strip_content($1)}\", #{@escape} %>"
else
"<%= t d\"#{strip_variable(content)}\", #{@escape} %>"
end
end
@template.gsub!(/\#\{\s*config\.(.+?)\s*\}/m) do
"<%= c \"#{strip_variable($1)}\", #{@escape} %>"
end
@template.gsub!(/\#\{(.*?)\}/m) do
"<%= d \"#{strip_variable($1)}\", #{@escape} %>"
end
end
def strip_content(string)
return string if !string
string.gsub!(/\t|\r|\n/, '')
string.gsub!(/"/, '\"')
string
end
def strip_variable(string)
return string if !string
string.gsub!(/\t|\r|\n|"|'|§|;/, '')
string.gsub!(/\s*/, '')
string.gsub!(/<.+?>/, '')
string
end
end

View file

@ -3,29 +3,40 @@ require 'test_helper'
class NotificationFactoryRendererTest < ActiveSupport::TestCase
# TODO: should be mocked somehow
Translation.load('de-de')
# RSpec incoming!
def described_class
NotificationFactory::Renderer
end
Group = Struct.new(:name)
State = Struct.new(:name)
User = Struct.new(:firstname, :lastname, :longname, :fullname)
Ticket = Struct.new(:id, :title, :group, :owner, :state)
group = Group.new('Users')
state = State.new('new')
owner = User.new('Notification<b>xxx</b>', 'Agent1<b>yyy</b>', 'Notification<b>xxx</b> Agent1<b>yyy</b>', 'Notification<b>xxx</b> Agent1<b>yyy</b> (Zammad)')
current_user = User.new('CurrentUser<b>xxx</b>', 'Agent2<b>yyy</b>', 'CurrentUser<b>xxx</b> Agent2<b>yyy</b>', 'CurrentUser<b>xxx</b> Agent2<b>yyy</b> (Zammad)')
recipient = User.new('Recipient<b>xxx</b>', 'Customer1<b>yyy</b>', 'Recipient<b>xxx</b> Customer1<b>yyy</b>', 'Recipient<b>xxx</b> Customer1<b>yyy</b> (Zammad)')
ticket = Ticket.new(1, '<b>Welcome to Zammad!</b>', group, owner, state)
group = Group.new(name: 'Users')
owner = User.new(firstname: 'Notification<b>xxx</b>', lastname: 'Agent1<b>yyy</b>')
current_user = User.new(firstname: 'CurrentUser<b>xxx</b>', lastname: 'Agent2<b>yyy</b>')
recipient = User.new(firstname: 'Recipient<b>xxx</b>', lastname: 'Customer1<b>yyy</b>')
state = Ticket::State.new(name: 'new')
ticket = Ticket.new(
id: 1,
title: '<b>Welcome to Zammad!</b>',
group: group,
owner: owner,
state: state,
created_at: Time.zone.parse('2016-11-12 12:00:00 UTC'),
updated_at: Time.zone.parse('2016-11-12 14:00:00 UTC'),
)
article_html1 = Ticket::Article.new(
body: 'test <b>hello</b><br>some new line',
content_type: 'text/html',
)
article_plain1 = Ticket::Article.new(
body: "test <b>hello</b>\nsome new line",
content_type: 'text/plain',
)
article_plain2 = Ticket::Article.new(
body: "test <b>hello</b>\nsome new line",
)
test 'replace object attribute' do
template = "<%= d 'ticket.title' %>"
template = "\#{ticket.title}"
result = described_class.new(
{
ticket: ticket,
@ -35,7 +46,27 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "<%= d 'ticket. title' %>"
template = "\#{ticket.created_at}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(ticket.created_at.to_s, result)
template = "\#{ticket.updated_at}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(ticket.updated_at.to_s, result)
template = "\#{ticket. title}"
result = described_class.new(
{
ticket: ticket,
@ -45,7 +76,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "<%= d 'ticket.\n title' %>"
template = "\#{ticket.\n title}"
result = described_class.new(
{
ticket: ticket,
@ -55,7 +86,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "<%= d 'ticket.\t title' %>"
template = "\#{ticket.\t title}"
result = described_class.new(
{
ticket: ticket,
@ -65,7 +96,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "<%= d 'ticket.\t\n title\t' %>"
template = "\#{ticket.\t\n title\t}"
result = described_class.new(
{
ticket: ticket,
@ -75,13 +106,50 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "\#{ticket.\" title\t}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML(ticket.title), result)
template = "some test<br>\#{article.body}"
result = described_class.new(
{
article: article_html1,
},
'en-us',
template,
).render
assert_equal('some test<br>&gt; test hello<br>&gt; some new line<br>', result)
result = described_class.new(
{
article: article_plain1,
},
'en-us',
template,
).render
assert_equal('some test<br>&gt; test &lt;b&gt;hello&lt;/b&gt;<br>&gt; some new line<br>', result)
result = described_class.new(
{
article: article_plain2,
},
'en-us',
template,
).render
assert_equal('some test<br>&gt; test &lt;b&gt;hello&lt;/b&gt;<br>&gt; some new line<br>', result)
end
test 'config' do
setting = 'fqdn'
template = "<%= c '#{setting}' %>"
setting = 'fqdn'
template = "\#{config.#{setting}}"
result = described_class.new(
{
ticket: ticket,
@ -89,13 +157,11 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(Setting.get(setting), result)
setting1 = 'fqdn'
setting2 = 'product_name'
template = "some <%= c '#{setting1}' %> and <%= c '#{setting2}' %>"
template = "some \#{config.#{setting1}} and \#{config.#{setting2}}"
result = described_class.new(
{
ticket: ticket,
@ -103,14 +169,25 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal("some #{Setting.get(setting1)} and #{Setting.get(setting2)}", result)
setting1 = 'fqdn'
setting2 = 'product_name'
template = "some \#{ config.#{setting1}} and \#{\tconfig.#{setting2}}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal("some #{Setting.get(setting1)} and #{Setting.get(setting2)}", result)
end
test 'translation' do
template = "<%= t 'new' %>"
#template = "<%= t 'new' %>"
template = "\#{t('new')}"
result = described_class.new(
{
ticket: ticket,
@ -118,11 +195,9 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'de-de',
template,
).render
assert_equal('neu', result)
template = "some text <%= t 'new' %> and <%= t 'open' %>"
template = "some text \#{t('new')} and \#{t('open')}"
result = described_class.new(
{
ticket: ticket,
@ -130,14 +205,33 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'de-de',
template,
).render
assert_equal('some text neu and offen', result)
template = "some text \#{t('new') } and \#{ t('open')}"
result = described_class.new(
{
ticket: ticket,
},
'de-de',
template,
).render
assert_equal('some text neu and offen', result)
template = "some text \#{\nt('new') } and \#{ t('open')\t}"
result = described_class.new(
{
ticket: ticket,
},
'de-de',
template,
).render
assert_equal('some text neu and offen', result)
end
test 'chained function calls' do
template = "<%= t d 'ticket.state.name' %>"
template = "\#{t(ticket.state.name)}"
result = described_class.new(
{
@ -152,7 +246,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
test 'not existing object and attribute' do
template = "<%= d '' %>"
template = "\#{}"
result = described_class.new(
{
ticket: ticket,
@ -162,7 +256,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{no such object}'), result)
template = "<%= d 'notexsiting.notexsiting' %>"
template = "\#{notexsiting.notexsiting}"
result = described_class.new(
{
ticket: ticket,
@ -172,7 +266,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result)
template = "<%= d 'ticket.notexsiting' %>"
template = "\#{ticket.notexsiting}"
result = described_class.new(
{
ticket: ticket,
@ -182,7 +276,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.notexsiting / no such method}'), result)
template = "<%= d 'ticket.' %>"
template = "\#{ticket.}"
result = described_class.new(
{
ticket: ticket,
@ -192,7 +286,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket. / no such method}'), result)
template = "<%= d 'ticket.title.notexsiting' %>"
template = "\#{ticket.title.notexsiting}"
result = described_class.new(
{
ticket: ticket,
@ -202,7 +296,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.title.notexsiting / no such method}'), result)
template = "<%= d 'ticket.notexsiting.notexsiting' %>"
template = "\#{ticket.notexsiting.notexsiting}"
result = described_class.new(
{
ticket: ticket,
@ -212,7 +306,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.notexsiting / no such method}'), result)
template = "<%= d 'notexsiting' %>"
template = "\#{notexsiting}"
result = described_class.new(
{
ticket: ticket,
@ -222,7 +316,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result)
template = "<%= d 'notexsiting.' %>"
template = "\#{notexsiting.}"
result = described_class.new(
{
ticket: ticket,
@ -232,7 +326,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result)
template = "<%= d 'string' %>"
template = "\#{string}"
result = described_class.new(
{
string: 'some string',
@ -242,7 +336,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('some string'), result)
template = "<%= d 'fixum' %>"
template = "\#{fixum}"
result = described_class.new(
{
fixum: 123,
@ -252,7 +346,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('123'), result)
template = "<%= d 'float' %>"
template = "\#{float}"
result = described_class.new(
{
float: 123.99,
@ -266,7 +360,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
test 'data key validation' do
template = "<%= d 'ticket.title `echo 1`' %>"
template = "\#{ticket.title `echo 1`}"
result = described_class.new(
{
ticket: ticket,
@ -274,9 +368,9 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.title `echo 1` / not allowed}'), result)
assert_equal(CGI.escapeHTML('#{ticket.title`echo1` / not allowed}'), result)
template = "<%= d 'ticket.destroy' %>"
template = "\#{ticket.destroy}"
result = described_class.new(
{
ticket: ticket,
@ -286,7 +380,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.destroy / not allowed}'), result)
template = "<%= d 'ticket.save' %>"
template = "\#{ticket.save}"
result = described_class.new(
{
ticket: ticket,
@ -296,7 +390,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.save / not allowed}'), result)
template = "<%= d 'ticket.update' %>"
template = "\#{ticket.update}"
result = described_class.new(
{
ticket: ticket,
@ -306,37 +400,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.update / not allowed}'), result)
template = "<%= d 'ticket.delete' %>"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.delete / not allowed}'), result)
template = "<%= d 'ticket.remove' %>"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.remove / not allowed}'), result)
template = "<%= d 'ticket.drop' %>"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.drop / not allowed}'), result)
template = "<%= d 'ticket.create' %>"
template = "\#{ticket.create}"
result = described_class.new(
{
ticket: ticket,
@ -346,7 +410,47 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.create / not allowed}'), result)
template = "<%= d 'ticket.new' %>"
template = "\#{ticket.delete}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.delete / not allowed}'), result)
template = "\#{ticket.remove}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.remove / not allowed}'), result)
template = "\#{ticket.drop}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.drop / not allowed}'), result)
template = "\#{ticket.create}"
result = described_class.new(
{
ticket: ticket,
},
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket.create / not allowed}'), result)
template = "\#{ticket.new}"
result = described_class.new(
{
ticket: ticket,
@ -356,7 +460,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.new / not allowed}'), result)
template = "<%= d 'ticket.update_att' %>"
template = "\#{ticket.update_att}"
result = described_class.new(
{
ticket: ticket,
@ -366,7 +470,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.update_att / not allowed}'), result)
template = "<%= d 'ticket.all' %>"
template = "\#{ticket.all}"
result = described_class.new(
{
ticket: ticket,
@ -376,7 +480,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.all / not allowed}'), result)
template = "<%= d 'ticket.find' %>"
template = "\#{ticket.find}"
result = described_class.new(
{
ticket: ticket,
@ -386,7 +490,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.find / not allowed}'), result)
template = "<%= d 'ticket.where' %>"
template = "\#{ticket.where}"
result = described_class.new(
{
ticket: ticket,
@ -396,7 +500,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
).render
assert_equal(CGI.escapeHTML('#{ticket.where / not allowed}'), result)
template = "<%= d 'ticket. destroy' %>"
template = "\#{ticket. destroy}"
result = described_class.new(
{
ticket: ticket,
@ -404,9 +508,9 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(CGI.escapeHTML('#{ticket. destroy / not allowed}'), result)
assert_equal(CGI.escapeHTML('#{ticket.destroy / not allowed}'), result)
template = "<%= d 'ticket.\n destroy' %>"
template = "\#{ticket.\n destroy}"
result = described_class.new(
{
ticket: ticket,
@ -414,9 +518,9 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(CGI.escapeHTML("\#{ticket.\n destroy / not allowed}"), result)
assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result)
template = "<%= d 'ticket.\t destroy' %>"
template = "\#{ticket.\t destroy}"
result = described_class.new(
{
ticket: ticket,
@ -424,9 +528,9 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(CGI.escapeHTML("\#{ticket.\t destroy / not allowed}"), result)
assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result)
template = "<%= d 'ticket.\r destroy' %>"
template = "\#{ticket.\r destroy}"
result = described_class.new(
{
ticket: ticket,
@ -434,7 +538,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
'en-us',
template,
).render
assert_equal(CGI.escapeHTML("\#{ticket.\r destroy / not allowed}"), result)
assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result)
end

View file

@ -78,7 +78,7 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase
)
assert_match('# Welcome to Zammad!', result[:subject])
assert_match('User&lt;b&gt;xxx&lt;/b&gt', result[:body])
assert_match('User<b>xxx</b>', result[:body])
assert_match('Created by', result[:body])
assert_match('<b>test123</b>', result[:body])
assert_no_match('Dein', result[:body])
@ -113,7 +113,7 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase
},
)
assert_match('# Welcome to Zammad!', result[:subject])
assert_match('User&lt;b&gt;xxx&lt;/b&gt', result[:body])
assert_match('User<b>xxx</b>', result[:body])
assert_match('state: aaa -> bbb', result[:body])
assert_match('group: xxx -> yyy', result[:body])
assert_no_match('Dein', result[:body])

View file

@ -11,48 +11,69 @@ class NotificationFactoryTemplateTest < ActiveSupport::TestCase
test 'regular browser html' do
# ensures https://github.com/zammad/zammad/issues/385
template_before = '<%= d "#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id</a>}" %>'
template_after = '<%= d "ticket.id" %>'
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).to_s
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 = '<%= d "#{ <a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id </a>} " %>'
template_after = '<%= d "ticket.id " %>'
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).to_s
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 = '<%= d "#{<a href="http://ticket.id" title="http://ticket.id" target="_blank">ticket.id }" %>'
template_after = '<%= d "ticket.id " %>'
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).to_s
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'empty tag' do
template_before = '<%= d "#{}" %>'
template_after = '<%= d "#{}" %>'
template_before = '#{}'
template_after = '<%= d "", true %>'
result = described_class.new(template_before).to_s
result = described_class.new(template_before, true).to_s
assert_equal(template_after, result)
end
test 'empty tag with space' do
template_before = '<%= d "#{ }" %>'
template_after = '<%= d "#{ }" %>'
template_before = '#{ }'
template_after = '<%= d "", false %>'
result = described_class.new(template_before).to_s
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

View file

@ -0,0 +1,78 @@
# encoding: utf-8
require 'test_helper'
class OrganizationDomainBasedAssignmentTest < ActiveSupport::TestCase
test 'organization based assignment' do
organization1 = Organization.create_if_not_exists(
name: 'organization based assignment 1',
domain: '@examPle1.com ',
domain_assignment: true,
updated_by_id: 1,
created_by_id: 1,
)
organization2 = Organization.create_if_not_exists(
name: 'organization based assignment 2',
domain: 'example2.com',
domain_assignment: false,
updated_by_id: 1,
created_by_id: 1,
)
roles = Role.where(name: 'Customer')
customer1 = User.create_or_update(
login: 'organization-based_assignment-customer1@example1.com',
firstname: 'Domain',
lastname: 'Agent1',
email: 'organization-based_assignment-customer1@example1.com',
password: 'customerpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(organization1.id, customer1.organization_id)
customer2 = User.create_or_update(
login: 'organization-based_assignment-customer2@example1.com',
firstname: 'Domain',
lastname: 'Agent2',
email: 'organization-based_assignment-customer2@example1.com',
password: 'customerpw',
active: true,
organization_id: organization2.id,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(organization2.id, customer2.organization_id)
customer3 = User.create_or_update(
login: 'organization-based_assignment-customer3@example2.com',
firstname: 'Domain',
lastname: 'Agent2',
email: 'organization-based_assignment-customer3@example2.com',
password: 'customerpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(nil, customer3.organization_id)
customer4 = User.create_or_update(
login: 'organization-based_assignment-customer4',
firstname: 'Domain',
lastname: 'Agent2',
email: '@',
password: 'customerpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(nil, customer4.organization_id)
end
end

View file

@ -13,7 +13,7 @@ class TicketTriggerTest < ActiveSupport::TestCase
},
perform: {
'notification.email' => {
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}',
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
'recipient' => 'ticket_customer',
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
},
@ -60,12 +60,25 @@ class TicketTriggerTest < ActiveSupport::TestCase
created_by_id: 1,
)
assert(ticket1, 'ticket1 created')
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: 'some@id',
body: "some message <b>note</b>\nnew line",
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
assert_equal('some <b>title</b> äöüß', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(0, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], Tag.tag_list(object: 'Ticket', o_id: ticket1.id))
Observer::Transaction.commit
@ -75,13 +88,14 @@ class TicketTriggerTest < ActiveSupport::TestCase
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('3 high', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(%w(aa kk), Tag.tag_list(object: 'Ticket', o_id: ticket1.id))
article1 = ticket1.articles.last
assert_match('Zammad <zammad@localhost>', article1.from)
assert_match('nicole.braun@zammad.org', article1.to)
assert_match('Thanks for your inquiry (some <b>title</b> äöüß)!', article1.subject)
assert_match('Braun<br>some &lt;b&gt;title&lt;/b&gt;', article1.body)
assert_match('&gt; some message &lt;b&gt;note&lt;/b&gt;<br>&gt; new line', article1.body)
assert_equal('text/html', article1.content_type)
ticket1.priority = Ticket::Priority.lookup(name: '2 normal')
@ -93,7 +107,7 @@ class TicketTriggerTest < ActiveSupport::TestCase
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(%w(aa kk), Tag.tag_list(object: 'Ticket', o_id: ticket1.id))
ticket1.state = Ticket::State.lookup(name: 'open')
@ -105,7 +119,7 @@ class TicketTriggerTest < ActiveSupport::TestCase
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal('open', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(%w(aa kk), Tag.tag_list(object: 'Ticket', o_id: ticket1.id))
ticket1.state = Ticket::State.lookup(name: 'new')
@ -118,7 +132,7 @@ class TicketTriggerTest < ActiveSupport::TestCase
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('3 high', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(3, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(%w(aa kk), Tag.tag_list(object: 'Ticket', o_id: ticket1.id))
article1 = ticket1.articles.last
assert_match('Zammad <zammad@localhost>', article1.from)
@ -155,6 +169,56 @@ class TicketTriggerTest < ActiveSupport::TestCase
assert_equal(0, ticket2.articles.count, 'ticket2.articles verify')
assert_equal([], Tag.tag_list(object: 'Ticket', o_id: ticket2.id))
ticket3 = Ticket.create(
title: "some <b>title</b>\n äöüß3",
group: Group.lookup(name: 'Users'),
customer: User.lookup(email: 'nicole.braun@zammad.org'),
state: Ticket::State.lookup(name: 'new'),
priority: Ticket::Priority.lookup(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
assert(ticket3, 'ticket3 created')
Ticket::Article.create(
ticket_id: ticket3.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
assert_equal('some <b>title</b> äöüß3', ticket3.title, 'ticket3.title verify')
assert_equal('Users', ticket3.group.name, 'ticket3.group verify')
assert_equal('new', ticket3.state.name, 'ticket3.state verify')
assert_equal('2 normal', ticket3.priority.name, 'ticket3.priority verify')
assert_equal(1, ticket3.articles.count, 'ticket3.articles verify')
assert_equal([], Tag.tag_list(object: 'Ticket', o_id: ticket3.id))
Observer::Transaction.commit
ticket3 = Ticket.lookup(id: ticket3.id)
assert_equal('some <b>title</b> äöüß3', ticket3.title, 'ticket3.title verify')
assert_equal('Users', ticket3.group.name, 'ticket3.group verify')
assert_equal('new', ticket3.state.name, 'ticket3.state verify')
assert_equal('3 high', ticket3.priority.name, 'ticket3.priority verify')
assert_equal(2, ticket3.articles.count, 'ticket3.articles verify')
assert_equal(%w(aa kk), Tag.tag_list(object: 'Ticket', o_id: ticket3.id))
article3 = ticket3.articles.last
assert_match('Zammad <zammad@localhost>', article3.from)
assert_match('nicole.braun@zammad.org', article3.to)
assert_match('Thanks for your inquiry (some <b>title</b> äöüß3)!', article3.subject)
assert_match('Braun<br>some &lt;b&gt;title&lt;/b&gt;', article3.body)
assert_match('&gt; some message note<br>&gt; new line', article3.body)
assert_no_match('&gt; some message &lt;b&gt;note&lt;/b&gt;<br>&gt; new line', article3.body)
assert_equal('text/html', article3.content_type)
Trigger.destroy_all
end