Zammad Docker and addon updates

This commit is contained in:
Darren Clarke 2023-05-03 08:20:51 +00:00
parent dab5ce0521
commit aa18d3904e
16 changed files with 1972 additions and 2976 deletions

View file

@ -1,19 +1,17 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
class Ticket < ApplicationModel
include CanBeImported
include HasActivityStreamLog
include ChecksClientNotification
include ChecksLatestChangeObserved
include CanCsvImport
include ChecksHtmlSanitized
include HasHistory
include HasTags
include HasSearchIndexBackend
include HasOnlineNotifications
include HasKarmaActivityLog
include HasLinks
include HasObjectManagerAttributesValidation
include HasObjectManagerAttributes
include HasTaskbars
include Ticket::CallsStatsTicketReopenLog
include Ticket::EnqueuesUserTicketCounterJob
@ -21,6 +19,8 @@ class Ticket < ApplicationModel
include Ticket::SetsCloseTime
include Ticket::SetsOnlineNotificationSeen
include Ticket::TouchesAssociations
include Ticket::TriggersSubscriptions
include Ticket::ChecksReopenAfterCertainTime
include ::Ticket::Escalation
include ::Ticket::Subject
@ -38,10 +38,15 @@ class Ticket < ApplicationModel
include HasTransactionDispatcher
# workflow checks should run after before_create and before_update callbacks
include ChecksCoreWorkflow
validates :group_id, presence: true
activity_stream_permission 'ticket.agent'
core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
:create_article_type_id,
:create_article_sender_id,
@ -57,19 +62,26 @@ class Ticket < ApplicationModel
:update_escalation_at,
:update_in_min,
:update_diff_in_min,
:last_close_at,
:last_contact_at,
:last_contact_agent_at,
:last_contact_customer_at,
:last_owner_update_at,
:preferences
search_index_attributes_relevant :organization_id,
:group_id,
:state_id,
:priority_id
history_attributes_ignored :create_article_type_id,
:create_article_sender_id,
:article_count,
:preferences
history_relation_object 'Ticket::Article', 'Mention'
history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
validates :note, length: { maximum: 250 }
sanitized_html :note
belongs_to :group, optional: true
@ -78,6 +90,7 @@ class Ticket < ApplicationModel
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
has_many :mentions, as: :mentionable, dependent: :destroy
has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
belongs_to :state, class_name: 'Ticket::State', optional: true
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
belongs_to :owner, class_name: 'User', optional: true
@ -93,43 +106,6 @@ class Ticket < ApplicationModel
=begin
get user access conditions
conditions = Ticket.access_condition( User.find(1) , 'full')
returns
result = [user1, user2, ...]
=end
def self.access_condition(user, access)
sql = []
bind = []
if user.permissions?('ticket.agent')
sql.push('group_id IN (?)')
bind.push(user.group_ids_access(access))
end
if user.permissions?('ticket.customer')
if !user.organization || ( !user.organization.shared || user.organization.shared == false )
sql.push('tickets.customer_id = ?')
bind.push(user.id)
else
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
bind.push(user.id)
bind.push(user.organization.id)
end
end
return if sql.blank?
[ sql.join(' OR ') ].concat(bind)
end
=begin
processes tickets which have reached their pending time and sets next state_id
processed_tickets = Ticket.process_pending
@ -204,6 +180,31 @@ returns
result
end
def auto_assign(user)
return if !persisted?
return if Setting.get('ticket_auto_assignment').blank?
return if owner_id != 1
return if !TicketPolicy.new(user, self).full?
user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
return if user_ids_ignore.include?(user.id)
ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
return if ticket_auto_assignment_selector.blank?
condition = ticket_auto_assignment_selector[:condition].merge(
'ticket.id' => {
'operator' => 'is',
'value' => id,
}
)
ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
return if ticket_count.to_i.zero?
update!(owner: user)
end
=begin
processes escalated tickets
@ -220,7 +221,7 @@ returns
result = []
# fetch all escalated and soon to be escalating tickets
where('escalation_at <= ?', Time.zone.now + 15.minutes).find_each(batch_size: 500) do |ticket|
where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
article_id = nil
article = Ticket::Article.last_customer_agent_article(ticket.id)
@ -241,7 +242,7 @@ returns
next
end
# check if warning need to be sent
# check if warning needs to be sent
TransactionJob.perform_now(
object: 'Ticket',
type: 'escalation_warning',
@ -321,10 +322,10 @@ returns
# prevent cross merging tickets
target_ticket = Ticket.find_by(id: data[:ticket_id])
raise 'no target ticket given' if !target_ticket
raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
# check different ticket ids
raise Exceptions::UnprocessableEntity, 'Can\'t merge ticket with it self!' if id == target_ticket.id
raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id
# update articles
Transaction.execute context: 'merge' do
@ -413,6 +414,26 @@ returns
# touch new ticket (to broadcast change)
target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
EventBuffer.add('transaction', {
object: target_ticket.class.name,
type: 'update.received_merge',
data: target_ticket,
changes: {},
id: target_ticket.id,
user_id: UserInfo.current_user_id,
created_at: Time.zone.now,
})
EventBuffer.add('transaction', {
object: self.class.name,
type: 'update.merged_into',
data: self,
changes: {},
id: id,
user_id: UserInfo.current_user_id,
created_at: Time.zone.now,
})
end
true
end
@ -489,7 +510,7 @@ get count of tickets and tickets which match on selector
access = options[:access] || 'full'
raise 'no selectors given' if !selectors
query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
query, bind_params, tables = selector2sql(selectors, options)
return [] if !query
ActiveRecord::Base.transaction(requires_new: true) do
@ -497,20 +518,20 @@ get count of tickets and tickets which match on selector
if !current_user || access == 'ignore'
ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
return [ticket_count, tickets]
next [ticket_count, tickets]
end
access_condition = Ticket.access_condition(current_user, access)
ticket_count = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).count
tickets = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
tickets = "TicketPolicy::#{access.camelize}Scope".constantize
.new(current_user).resolve
.distinct
.where(query, *bind_params)
.joins(tables)
return [ticket_count, tickets]
next [tickets.count, tickets.limit(limit)]
rescue ActiveRecord::StatementInvalid => e
Rails.logger.error e
raise ActiveRecord::Rollback
end
[]
end
=begin
@ -561,419 +582,7 @@ condition example
=end
def self.selector2sql(selectors, options = {})
current_user = options[:current_user]
current_user_id = UserInfo.current_user_id
if current_user
current_user_id = current_user.id
end
return if !selectors
# remember query and bind params
query = ''
bind_params = []
like = Rails.application.config.db_like
if selectors.respond_to?(:permit!)
selectors = selectors.permit!.to_h
end
# get tables to join
tables = ''
selectors.each do |attribute, selector_raw|
attributes = attribute.split('.')
selector = selector_raw.stringify_keys
next if !attributes[1]
next if attributes[0] == 'execution_time'
next if tables.include?(attributes[0])
next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
if query != ''
query += ' AND '
end
case attributes[0]
when 'customer'
tables += ', users customers'
query += 'tickets.customer_id = customers.id'
when 'organization'
tables += ', organizations'
query += 'tickets.organization_id = organizations.id'
when 'owner'
tables += ', users owners'
query += 'tickets.owner_id = owners.id'
when 'article'
tables += ', ticket_articles articles'
query += 'tickets.id = articles.ticket_id'
when 'ticket_state'
tables += ', ticket_states'
query += 'tickets.state_id = ticket_states.id'
when 'ticket'
if attributes[1] == 'mention_user_ids'
tables += ', mentions'
query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
end
else
raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
end
end
# add conditions
no_result = false
selectors.each do |attribute, selector_raw|
# validation
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
selector = selector_raw.stringify_keys
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(%r{^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before|till|from)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$})
# validate value / allow blank but only if pre_condition exists and is not specific
if !selector.key?('value') ||
(selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
(selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
return nil if selector['pre_condition'].nil?
return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
return nil if selector['pre_condition'] == 'specific'
end
# validate pre_condition values
return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)}
# get attributes
attributes = attribute.split('.')
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}"
# magic selectors
if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id'
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
end
if attributes[0] == 'ticket' && attributes[1] == 'tags'
selector['value'] = selector['value'].split(',').collect(&:strip)
end
if selector['operator'].include?('in working time')
next if attributes[1] != 'calendar_id'
raise 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' if !options[:execution_time]
biz = Calendar.lookup(id: selector['value'])&.biz
next if biz.blank?
if ( selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now) ) || ( selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now) )
no_result = true
break
end
# skip to next condition
next
end
if query != ''
query += ' AND '
end
# because of no grouping support we select not_set by sub select for mentions
if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
if selector['pre_condition'] == 'not_set'
query += if selector['operator'] == 'is'
"(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
else
"1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
end
else
query += if selector['operator'] == 'is'
'mentions.user_id IN (?)'
else
'mentions.user_id NOT IN (?)'
end
if selector['pre_condition'] == 'current_user.id'
bind_params.push current_user_id
else
bind_params.push selector['value']
end
end
next
end
if selector['operator'] == 'is'
if selector['pre_condition'] == 'not_set'
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
query += "(#{attribute} IS NULL OR #{attribute} IN (?))"
bind_params.push 1
else
query += "#{attribute} IS NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
query += "#{attribute} IN (?)"
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
else
bind_params.push current_user_id
end
elsif selector['pre_condition'] == 'current_user.organization_id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
query += "#{attribute} IN (?)"
user = User.find_by(id: current_user_id)
bind_params.push user.organization_id
else
# rubocop:disable Style/IfInsideElse
if selector['value'].nil?
query += "#{attribute} IS NULL"
else
if attributes[1] == 'out_of_office_replacement_id'
query += "#{attribute} IN (?)"
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
else
if selector['value'].class != Array
selector['value'] = [selector['value']]
end
query += if selector['value'].include?('')
"(#{attribute} IN (?) OR #{attribute} IS NULL)"
else
"#{attribute} IN (?)"
end
bind_params.push selector['value']
end
end
# rubocop:enable Style/IfInsideElse
end
elsif selector['operator'] == 'is not'
if selector['pre_condition'] == 'not_set'
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
bind_params.push 1
else
query += "#{attribute} IS NOT NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
else
bind_params.push current_user_id
end
elsif selector['pre_condition'] == 'current_user.organization_id'
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
user = User.find_by(id: current_user_id)
bind_params.push user.organization_id
else
# rubocop:disable Style/IfInsideElse
if selector['value'].nil?
query += "#{attribute} IS NOT NULL"
else
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
else
if selector['value'].class != Array
selector['value'] = [selector['value']]
end
query += if selector['value'].include?('')
"(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
else
"(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
end
bind_params.push selector['value']
end
end
# rubocop:enable Style/IfInsideElse
end
elsif selector['operator'] == 'contains'
query += "#{attribute} #{like} (?)"
value = "%#{selector['value']}%"
bind_params.push value
elsif selector['operator'] == 'contains not'
query += "#{attribute} NOT #{like} (?)"
value = "%#{selector['value']}%"
bind_params.push value
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "? = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
)"
bind_params.push selector['value'].count
bind_params.push selector['value']
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
tables += ', tag_objects, tag_items, tags'
query += "
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)"
bind_params.push selector['value']
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "0 = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
)"
bind_params.push selector['value']
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "(
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
) BETWEEN 0 AND 0"
bind_params.push selector['value']
elsif selector['operator'] == 'before (absolute)'
query += "#{attribute} <= ?"
bind_params.push selector['value']
elsif selector['operator'] == 'after (absolute)'
query += "#{attribute} >= ?"
bind_params.push selector['value']
elsif selector['operator'] == 'within last (relative)'
query += "#{attribute} BETWEEN ? AND ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
bind_params.push Time.zone.now
elsif selector['operator'] == 'within next (relative)'
query += "#{attribute} BETWEEN ? AND ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push Time.zone.now
bind_params.push time
elsif selector['operator'] == 'before (relative)'
query += "#{attribute} <= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'after (relative)'
query += "#{attribute} >= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'till (relative)'
query += "#{attribute} <= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'from (relative)'
query += "#{attribute} >= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
else
raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
end
end
return if no_result
[query, bind_params, tables]
Ticket::Selector::Sql.new(selector: selectors, options: options).get
end
=begin
@ -1011,9 +620,10 @@ perform changes on ticket
end
end
objects = build_notification_template_objects(article)
perform_notification = {}
perform_article = {}
changed = false
perform_article = {}
changed = false
perform.each do |key, value|
(object_name, attribute) = key.split('.', 2)
raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification'
@ -1034,23 +644,7 @@ perform changes on ticket
when 'static'
value['value']
when 'relative'
pendtil = Time.zone.now
val = value['value'].to_i
case value['range']
when 'day'
pendtil += val.days
when 'minute'
pendtil += val.minutes
when 'hour'
pendtil += val.hours
when 'month'
pendtil += val.months
when 'year'
pendtil += val.years
end
pendtil
TimeRangeHelper.relative(range: value['range'], value: value['value'])
end
if new_value
@ -1095,7 +689,7 @@ perform changes on ticket
if value['pre_condition'].start_with?('not_set')
value['value'] = 1
elsif value['pre_condition'].start_with?('current_user.')
raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id
raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
value['value'] = current_user_id
end
@ -1106,6 +700,14 @@ perform changes on ticket
changed = true
if value['value'].is_a?(String)
value['value'] = NotificationFactory::Mailer.template(
templateInline: value['value'],
objects: objects,
quote: true,
)
end
self[attribute] = value['value']
logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
end
@ -1114,10 +716,8 @@ perform changes on ticket
save!
end
objects = build_notification_template_objects(article)
perform_article.each do |key, value|
raise 'Unable to create article, we only support article.note' if key != 'article.note'
raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
add_trigger_note(id, value, objects, perform_origin)
end
@ -1209,7 +809,7 @@ perform active triggers on ticket
else
::Trigger.where(active: true).order(:name)
end
return [true, 'No triggers active'] if triggers.blank?
return [true, __('No triggers active')] if triggers.blank?
# check if notification should be send because of customer emails
send_notification = true
@ -1226,95 +826,6 @@ perform active triggers on ticket
triggers.each do |trigger|
logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
condition = trigger.condition
# check if one article attribute is used
one_has_changed_done = false
article_selector = false
trigger.condition.each_key do |key|
(object_name, attribute) = key.split('.', 2)
next if object_name != 'article'
next if attribute == 'id'
article_selector = true
end
if article && article_selector
one_has_changed_done = true
end
if article && type == 'update'
one_has_changed_done = true
end
# check ticket "has changed" options
has_changed_done = true
condition.each do |key, value|
next if value.blank?
next if value['operator'].blank?
next if !value['operator']['has changed']
# remove condition item, because it has changed
(object_name, attribute) = key.split('.', 2)
next if object_name != 'ticket'
next if item[:changes].blank?
next if !item[:changes].key?(attribute)
condition.delete(key)
one_has_changed_done = true
end
# check if we have not matching "has changed" attributes
condition.each_value do |value|
next if value.blank?
next if value['operator'].blank?
next if !value['operator']['has changed']
has_changed_done = false
break
end
# check ticket action
if condition['ticket.action']
next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type
next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type
condition.delete('ticket.action')
end
next if !has_changed_done
# check in min one attribute of condition has changed on update
one_has_changed_condition = false
if type == 'update'
# verify if ticket condition exists
condition.each_key do |key|
(object_name, attribute) = key.split('.', 2)
next if object_name != 'ticket'
one_has_changed_condition = true
next if item[:changes].blank?
next if !item[:changes].key?(attribute)
one_has_changed_done = true
break
end
next if one_has_changed_condition && !one_has_changed_done
end
# check if ticket selector is matching
condition['ticket.id'] = {
operator: 'is',
value: ticket.id,
}
next if article_selector && !article
# check if article selector is matching
if article_selector
condition['article.id'] = {
operator: 'is',
value: article.id,
}
end
user_id = ticket.updated_by_id
if article
user_id = article.updated_by_id
@ -1323,7 +834,7 @@ perform active triggers on ticket
user = User.lookup(id: user_id)
# verify is condition is matching
ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore')
ticket_count, tickets = Ticket.selectors(trigger.condition, limit: 1, execution_time: true, current_user: user, access: 'ignore', ticket_action: type, ticket_id: ticket.id, article_id: article&.id, changes: item[:changes], changes_required: true)
next if ticket_count.blank?
next if ticket_count.zero?
@ -1455,7 +966,7 @@ result
customer = User.find_by(id: customer_id)
return true if !customer
return true if organization_id == customer.organization_id
return true if organization_id.present? && customer.organization_id?(organization_id)
self.organization_id = customer.organization_id
true
@ -1523,9 +1034,30 @@ result
# if another email notification trigger preceded this one
# (see https://github.com/zammad/zammad/issues/1543)
def build_notification_template_objects(article)
last_article = nil
last_internal_article = nil
last_external_article = nil
all_articles = articles
if article.nil?
last_article = all_articles.last
last_internal_article = all_articles.reverse.find(&:internal?)
last_external_article = all_articles.reverse.find { |a| !a.internal? }
else
last_article = article
last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
end
{
ticket: self,
article: article || articles.last
ticket: self,
article: last_article,
last_article: last_article,
last_internal_article: last_internal_article,
last_external_article: last_external_article,
created_article: article,
created_internal_article: article&.internal? ? article : nil,
created_external_article: article&.internal? ? nil : article,
}
end
@ -1587,7 +1119,7 @@ result
Mail::AddressList.new(recipient_email).addresses.each do |address|
recipient_email = address.address
email_address_validation = EmailAddressValidation.new(recipient_email)
break if recipient_email.present? && email_address_validation.valid_format?
break if recipient_email.present? && email_address_validation.valid?
end
rescue
if recipient_email.present?
@ -1600,7 +1132,7 @@ result
end
email_address_validation = EmailAddressValidation.new(recipient_email)
next if !email_address_validation.valid_format?
next if !email_address_validation.valid?
# do not send notification if system address
next if EmailAddress.exists?(email: recipient_email.downcase)
@ -1700,7 +1232,7 @@ result
sign = value['sign'].present? && value['sign'] != 'no'
encryption = value['encryption'].present? && value['encryption'] != 'no'
security = {
type: security_type,
type: security_type,
sign: {
success: false,
},
@ -1717,10 +1249,10 @@ result
else
cert = SMIMECertificate.for_sender_email_address(from)
end
begin
list = Mail::AddressList.new(email_address.email)
from = list.addresses.first.to_s
if cert && !cert.expired?
sign_found = true
security[:sign][:success] = true
@ -1795,7 +1327,7 @@ result
)
attachments_inline.each do |attachment|
Store.add(
Store.create!(
object: 'Ticket::Article',
o_id: message.id,
data: attachment[:data],
@ -1805,6 +1337,11 @@ result
end
original_article = objects[:article]
if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
end
if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
@ -1903,7 +1440,15 @@ result
return 0 if !user.preferences[:mail_delivery_failed]
return 0 if user.preferences[:mail_delivery_failed_data].blank?
# blocked for 60 full days
(user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
# blocked for 60 full days; see #4459
remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
return remaining_days if remaining_days.positive?
# cleanup user preferences
user.preferences[:mail_delivery_failed] = false
user.preferences[:mail_delivery_failed_data] = nil
user.save!
0
end
end