ticket.rb 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include CanCsvImport
  7. include ChecksHtmlSanitized
  8. include HasHistory
  9. include HasTags
  10. include HasSearchIndexBackend
  11. include HasOnlineNotifications
  12. include HasLinks
  13. include HasObjectManagerAttributes
  14. include HasTaskbars
  15. include Ticket::CallsStatsTicketReopenLog
  16. include Ticket::EnqueuesUserTicketCounterJob
  17. include Ticket::ResetsPendingTimeSeconds
  18. include Ticket::SetsCloseTime
  19. include Ticket::SetsOnlineNotificationSeen
  20. include Ticket::TouchesAssociations
  21. include Ticket::TriggersSubscriptions
  22. include Ticket::ChecksReopenAfterCertainTime
  23. include ::Ticket::Escalation
  24. include ::Ticket::Subject
  25. include ::Ticket::Assets
  26. include ::Ticket::SearchIndex
  27. include ::Ticket::Search
  28. include ::Ticket::MergeHistory
  29. store :preferences
  30. before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  31. before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
  32. # This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
  33. include Ticket::SetsLastOwnerUpdateTime
  34. include HasTransactionDispatcher
  35. # workflow checks should run after before_create and before_update callbacks
  36. include ChecksCoreWorkflow
  37. validates :group_id, presence: true
  38. activity_stream_permission 'ticket.agent'
  39. core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
  40. activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
  41. :create_article_type_id,
  42. :create_article_sender_id,
  43. :article_count,
  44. :first_response_at,
  45. :first_response_escalation_at,
  46. :first_response_in_min,
  47. :first_response_diff_in_min,
  48. :close_at,
  49. :close_escalation_at,
  50. :close_in_min,
  51. :close_diff_in_min,
  52. :update_escalation_at,
  53. :update_in_min,
  54. :update_diff_in_min,
  55. :last_close_at,
  56. :last_contact_at,
  57. :last_contact_agent_at,
  58. :last_contact_customer_at,
  59. :last_owner_update_at,
  60. :preferences
  61. search_index_attributes_relevant :organization_id,
  62. :group_id,
  63. :state_id,
  64. :priority_id
  65. history_attributes_ignored :create_article_type_id,
  66. :create_article_sender_id,
  67. :article_count,
  68. :preferences
  69. history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
  70. validates :note, length: { maximum: 250 }
  71. sanitized_html :note
  72. belongs_to :group, optional: true
  73. belongs_to :organization, optional: true
  74. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  75. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  76. has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
  77. has_many :mentions, as: :mentionable, dependent: :destroy
  78. has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  79. belongs_to :state, class_name: 'Ticket::State', optional: true
  80. belongs_to :priority, class_name: 'Ticket::Priority', optional: true
  81. belongs_to :owner, class_name: 'User', optional: true
  82. belongs_to :customer, class_name: 'User', optional: true
  83. belongs_to :created_by, class_name: 'User', optional: true
  84. belongs_to :updated_by, class_name: 'User', optional: true
  85. belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
  86. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
  87. association_attributes_ignored :flags, :mentions
  88. attr_accessor :callback_loop
  89. =begin
  90. processes tickets which have reached their pending time and sets next state_id
  91. processed_tickets = Ticket.process_pending
  92. returns
  93. processed_tickets = [<Ticket>, ...]
  94. =end
  95. def self.process_pending
  96. result = []
  97. # process pending action tickets
  98. pending_action = Ticket::StateType.find_by(name: 'pending action')
  99. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  100. .where.not(next_state_id: nil)
  101. if ticket_states_pending_action.present?
  102. next_state_map = {}
  103. ticket_states_pending_action.each do |state|
  104. next_state_map[state.id] = state.next_state_id
  105. end
  106. tickets = where(state_id: next_state_map.keys)
  107. .where('pending_time <= ?', Time.zone.now)
  108. tickets.find_each(batch_size: 500) do |ticket|
  109. Transaction.execute do
  110. ticket.state_id = next_state_map[ticket.state_id]
  111. ticket.updated_at = Time.zone.now
  112. ticket.updated_by_id = 1
  113. ticket.save!
  114. end
  115. result.push ticket
  116. end
  117. end
  118. # process pending reminder tickets
  119. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  120. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  121. if ticket_states_pending_reminder.present?
  122. reminder_state_map = {}
  123. ticket_states_pending_reminder.each do |state|
  124. reminder_state_map[state.id] = state.next_state_id
  125. end
  126. tickets = where(state_id: reminder_state_map.keys)
  127. .where('pending_time <= ?', Time.zone.now)
  128. tickets.find_each(batch_size: 500) do |ticket|
  129. article_id = nil
  130. article = Ticket::Article.last_customer_agent_article(ticket.id)
  131. if article
  132. article_id = article.id
  133. end
  134. # send notification
  135. TransactionJob.perform_now(
  136. object: 'Ticket',
  137. type: 'reminder_reached',
  138. object_id: ticket.id,
  139. article_id: article_id,
  140. user_id: 1,
  141. )
  142. result.push ticket
  143. end
  144. end
  145. result
  146. end
  147. def auto_assign(user)
  148. return if Setting.get('ticket_auto_assignment').blank?
  149. return if owner_id != 1
  150. return if !TicketPolicy.new(user, self).full?
  151. user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
  152. return if user_ids_ignore.include?(user.id)
  153. ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
  154. return if ticket_auto_assignment_selector.blank?
  155. ticket_count, = Ticket.selectors(ticket_auto_assignment_selector[:condition], limit: 1, current_user: user, access: 'full')
  156. return if ticket_count.to_i.zero?
  157. update!(owner: user)
  158. end
  159. =begin
  160. processes escalated tickets
  161. processed_tickets = Ticket.process_escalation
  162. returns
  163. processed_tickets = [<Ticket>, ...]
  164. =end
  165. def self.process_escalation
  166. result = []
  167. # fetch all escalated and soon to be escalating tickets
  168. where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
  169. article_id = nil
  170. article = Ticket::Article.last_customer_agent_article(ticket.id)
  171. if article
  172. article_id = article.id
  173. end
  174. # send escalation
  175. if ticket.escalation_at < Time.zone.now
  176. TransactionJob.perform_now(
  177. object: 'Ticket',
  178. type: 'escalation',
  179. object_id: ticket.id,
  180. article_id: article_id,
  181. user_id: 1,
  182. )
  183. result.push ticket
  184. next
  185. end
  186. # check if warning needs to be sent
  187. TransactionJob.perform_now(
  188. object: 'Ticket',
  189. type: 'escalation_warning',
  190. object_id: ticket.id,
  191. article_id: article_id,
  192. user_id: 1,
  193. )
  194. result.push ticket
  195. end
  196. result
  197. end
  198. =begin
  199. processes tickets which auto unassign time has reached
  200. processed_tickets = Ticket.process_auto_unassign
  201. returns
  202. processed_tickets = [<Ticket>, ...]
  203. =end
  204. def self.process_auto_unassign
  205. # process pending action tickets
  206. state_ids = Ticket::State.by_category(:work_on).pluck(:id)
  207. return [] if state_ids.blank?
  208. result = []
  209. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  210. return [] if groups.blank?
  211. groups.each do |group|
  212. next if group.assignment_timeout.blank?
  213. ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
  214. ticket_ids.each do |ticket_id|
  215. ticket = Ticket.find_by(id: ticket_id)
  216. next if !ticket
  217. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  218. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  219. Transaction.execute do
  220. ticket.owner_id = 1
  221. ticket.updated_at = Time.zone.now
  222. ticket.updated_by_id = 1
  223. ticket.save!
  224. end
  225. result.push ticket
  226. end
  227. end
  228. result
  229. end
  230. =begin
  231. merge tickets
  232. ticket = Ticket.find(123)
  233. result = ticket.merge_to(
  234. ticket_id: 123,
  235. user_id: 123,
  236. )
  237. returns
  238. result = true|false
  239. =end
  240. def merge_to(data)
  241. # prevent cross merging tickets
  242. target_ticket = Ticket.find_by(id: data[:ticket_id])
  243. raise 'no target ticket given' if !target_ticket
  244. raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
  245. # check different ticket ids
  246. raise Exceptions::UnprocessableEntity, __('Can\'t merge ticket with itself!') if id == target_ticket.id
  247. # update articles
  248. Transaction.execute context: 'merge' do
  249. Ticket::Article.where(ticket_id: id).each(&:touch)
  250. # quiet update of reassign of articles
  251. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
  252. # mark target ticket as updated
  253. # otherwise the "received_merge" history entry
  254. # will be the same as the last updated_at
  255. # which might be a long time ago
  256. target_ticket.updated_at = Time.zone.now
  257. # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
  258. target_ticket.history_log(
  259. 'received_merge',
  260. data[:user_id],
  261. id_to: target_ticket.id,
  262. id_from: id,
  263. )
  264. history_log(
  265. 'merged_into',
  266. data[:user_id],
  267. id_to: target_ticket.id,
  268. id_from: id,
  269. )
  270. # create new merge article
  271. Ticket::Article.create(
  272. ticket_id: id,
  273. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  274. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  275. body: 'merged',
  276. internal: false,
  277. created_by_id: data[:user_id],
  278. updated_by_id: data[:user_id],
  279. )
  280. # search for mention duplicates and destroy them before moving mentions
  281. Mention.duplicates(self, target_ticket).destroy_all
  282. Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
  283. # reassign links to the new ticket
  284. # rubocop:disable Rails/SkipsModelValidations
  285. ticket_source_id = Link::Object.find_by(name: 'Ticket').id
  286. # search for all duplicate source and target links and destroy them
  287. # before link merging
  288. Link.duplicates(
  289. object1_id: ticket_source_id,
  290. object1_value: id,
  291. object2_value: data[:ticket_id]
  292. ).destroy_all
  293. Link.where(
  294. link_object_source_id: ticket_source_id,
  295. link_object_source_value: id,
  296. ).update_all(link_object_source_value: data[:ticket_id])
  297. Link.where(
  298. link_object_target_id: ticket_source_id,
  299. link_object_target_value: id,
  300. ).update_all(link_object_target_value: data[:ticket_id])
  301. # rubocop:enable Rails/SkipsModelValidations
  302. # link tickets
  303. Link.add(
  304. link_type: 'parent',
  305. link_object_source: 'Ticket',
  306. link_object_source_value: data[:ticket_id],
  307. link_object_target: 'Ticket',
  308. link_object_target_value: id
  309. )
  310. # external sync references
  311. ExternalSync.migrate('Ticket', id, target_ticket.id)
  312. # set state to 'merged'
  313. self.state_id = Ticket::State.lookup(name: 'merged').id
  314. # rest owner
  315. self.owner_id = 1
  316. # save ticket
  317. save!
  318. # touch new ticket (to broadcast change)
  319. target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
  320. EventBuffer.add('transaction', {
  321. object: target_ticket.class.name,
  322. type: 'update.received_merge',
  323. data: target_ticket,
  324. changes: {},
  325. id: target_ticket.id,
  326. user_id: UserInfo.current_user_id,
  327. created_at: Time.zone.now,
  328. })
  329. EventBuffer.add('transaction', {
  330. object: self.class.name,
  331. type: 'update.merged_into',
  332. data: self,
  333. changes: {},
  334. id: id,
  335. user_id: UserInfo.current_user_id,
  336. created_at: Time.zone.now,
  337. })
  338. end
  339. true
  340. end
  341. =begin
  342. check if online notification should be shown in general as already seen with current state
  343. ticket = Ticket.find(1)
  344. seen = ticket.online_notification_seen_state(user_id_check)
  345. returns
  346. result = true # or false
  347. =end
  348. def online_notification_seen_state(user_id_check = nil)
  349. state = Ticket::State.lookup(id: state_id)
  350. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  351. # always to set unseen for ticket owner and users which did not the update
  352. return false if state_type.name != 'merged' && user_id_check && user_id_check == owner_id && user_id_check != updated_by_id
  353. # set all to seen if pending action state is a closed or merged state
  354. if state_type.name == 'pending action' && state.next_state_id
  355. state = Ticket::State.lookup(id: state.next_state_id)
  356. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  357. end
  358. # set all to seen if new state is pending reminder state
  359. if state_type.name == 'pending reminder'
  360. if user_id_check
  361. return false if owner_id == 1
  362. return false if updated_by_id != owner_id && user_id_check == owner_id
  363. return true
  364. end
  365. return true
  366. end
  367. # set all to seen if new state is a closed or merged state
  368. return true if state_type.name == 'closed'
  369. return true if state_type.name == 'merged'
  370. false
  371. end
  372. =begin
  373. get count of tickets and tickets which match on selector
  374. @param [Hash] selectors hash with conditions
  375. @oparam [Hash] options
  376. @option options [String] :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
  377. @option options [Integer] :limit of tickets to return
  378. @option options [User] :user is a current user
  379. @option options [Integer] :execution_time is a current user
  380. @return [Integer, [<Ticket>]]
  381. @example
  382. ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')
  383. ticket_count # count of found tickets
  384. tickets # tickets
  385. =end
  386. def self.selectors(selectors, options)
  387. limit = options[:limit] || 10
  388. current_user = options[:current_user]
  389. access = options[:access] || 'full'
  390. raise 'no selectors given' if !selectors
  391. query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
  392. return [] if !query
  393. ActiveRecord::Base.transaction(requires_new: true) do
  394. if !current_user || access == 'ignore'
  395. ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
  396. tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
  397. next [ticket_count, tickets]
  398. end
  399. tickets = "TicketPolicy::#{access.camelize}Scope".constantize
  400. .new(current_user).resolve
  401. .distinct
  402. .where(query, *bind_params)
  403. .joins(tables)
  404. next [tickets.count, tickets.limit(limit)]
  405. rescue ActiveRecord::StatementInvalid => e
  406. Rails.logger.error e
  407. raise ActiveRecord::Rollback
  408. end
  409. end
  410. =begin
  411. generate condition query to search for tickets based on condition
  412. query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)
  413. condition example
  414. {
  415. 'ticket.title' => {
  416. operator: 'contains', # contains not
  417. value: 'some value',
  418. },
  419. 'ticket.state_id' => {
  420. operator: 'is',
  421. value: [1,2,5]
  422. },
  423. 'ticket.created_at' => {
  424. operator: 'after (absolute)', # after,before
  425. value: '2015-10-17T06:00:00.000Z',
  426. },
  427. 'ticket.created_at' => {
  428. operator: 'within next (relative)', # within next, within last, after, before
  429. range: 'day', # minute|hour|day|month|year
  430. value: '25',
  431. },
  432. 'ticket.owner_id' => {
  433. operator: 'is', # is not
  434. pre_condition: 'current_user.id',
  435. },
  436. 'ticket.owner_id' => {
  437. operator: 'is', # is not
  438. pre_condition: 'specific',
  439. value: 4711,
  440. },
  441. 'ticket.escalation_at' => {
  442. operator: 'is not', # not
  443. value: nil,
  444. },
  445. 'ticket.tags' => {
  446. operator: 'contains all', # contains all|contains one|contains all not|contains one not
  447. value: 'tag1, tag2',
  448. },
  449. }
  450. =end
  451. def self.selector2sql(selectors, options = {})
  452. current_user = options[:current_user]
  453. current_user_id = UserInfo.current_user_id
  454. if current_user
  455. current_user_id = current_user.id
  456. end
  457. return if !selectors
  458. # remember query and bind params
  459. query = ''
  460. bind_params = []
  461. like = Rails.application.config.db_like
  462. if selectors.respond_to?(:permit!)
  463. selectors = selectors.permit!.to_h
  464. end
  465. # get tables to join
  466. tables = ''
  467. selectors.each do |attribute, selector_raw|
  468. attributes = attribute.split('.')
  469. selector = selector_raw.stringify_keys
  470. next if !attributes[1]
  471. next if attributes[0] == 'execution_time'
  472. next if tables.include?(attributes[0])
  473. next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
  474. next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
  475. if query != ''
  476. query += ' AND '
  477. end
  478. case attributes[0]
  479. when 'customer'
  480. tables += ', users customers'
  481. query += 'tickets.customer_id = customers.id'
  482. when 'organization'
  483. tables += ', organizations'
  484. query += 'tickets.organization_id = organizations.id'
  485. when 'owner'
  486. tables += ', users owners'
  487. query += 'tickets.owner_id = owners.id'
  488. when 'article'
  489. tables += ', ticket_articles articles'
  490. query += 'tickets.id = articles.ticket_id'
  491. when 'ticket_state'
  492. tables += ', ticket_states'
  493. query += 'tickets.state_id = ticket_states.id'
  494. when 'ticket'
  495. if attributes[1] == 'mention_user_ids'
  496. tables += ', mentions'
  497. query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
  498. end
  499. else
  500. raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
  501. end
  502. end
  503. # add conditions
  504. no_result = false
  505. selectors.each do |attribute, selector_raw|
  506. # validation
  507. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
  508. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
  509. selector = selector_raw.stringify_keys
  510. raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
  511. 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)$})
  512. # validate value / allow blank but only if pre_condition exists and is not specific
  513. if !selector.key?('value') ||
  514. (selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
  515. (selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
  516. return nil if selector['pre_condition'].nil?
  517. return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
  518. return nil if selector['pre_condition'] == 'specific'
  519. end
  520. # validate pre_condition values
  521. return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)}
  522. # get attributes
  523. attributes = attribute.split('.')
  524. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}"
  525. # magic selectors
  526. if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id'
  527. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
  528. end
  529. if attributes[0] == 'ticket' && attributes[1] == 'tags'
  530. selector['value'] = selector['value'].split(',').collect(&:strip)
  531. end
  532. if selector['operator'].include?('in working time')
  533. next if attributes[1] != 'calendar_id'
  534. raise __('Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)') if !options[:execution_time]
  535. biz = Calendar.lookup(id: selector['value'])&.biz
  536. next if biz.blank?
  537. 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))
  538. no_result = true
  539. break
  540. end
  541. # skip to next condition
  542. next
  543. end
  544. if query != ''
  545. query += ' AND '
  546. end
  547. # because of no grouping support we select not_set by sub select for mentions
  548. if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
  549. if selector['pre_condition'] == 'not_set'
  550. query += if selector['operator'] == 'is'
  551. "(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
  552. else
  553. "1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
  554. end
  555. else
  556. query += if selector['operator'] == 'is'
  557. 'mentions.user_id IN (?)'
  558. else
  559. 'mentions.user_id NOT IN (?)'
  560. end
  561. if selector['pre_condition'] == 'current_user.id'
  562. bind_params.push current_user_id
  563. else
  564. bind_params.push selector['value']
  565. end
  566. end
  567. next
  568. end
  569. if selector['operator'] == 'is'
  570. if selector['pre_condition'] == 'not_set'
  571. if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  572. query += "(#{attribute} IS NULL OR #{attribute} IN (?))"
  573. bind_params.push 1
  574. else
  575. query += "#{attribute} IS NULL"
  576. end
  577. elsif selector['pre_condition'] == 'current_user.id'
  578. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  579. query += "#{attribute} IN (?)"
  580. if attributes[1] == 'out_of_office_replacement_id'
  581. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  582. else
  583. bind_params.push current_user_id
  584. end
  585. elsif selector['pre_condition'] == 'current_user.organization_id'
  586. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  587. query += "#{attribute} IN (?)"
  588. user = User.find_by(id: current_user_id)
  589. bind_params.push user.all_organization_ids
  590. else
  591. # rubocop:disable Style/IfInsideElse
  592. if selector['value'].nil?
  593. query += "#{attribute} IS NULL"
  594. else
  595. if attributes[1] == 'out_of_office_replacement_id'
  596. query += "#{attribute} IN (?)"
  597. bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
  598. else
  599. if selector['value'].class != Array
  600. selector['value'] = [selector['value']]
  601. end
  602. query += if selector['value'].include?('')
  603. "(#{attribute} IN (?) OR #{attribute} IS NULL)"
  604. else
  605. "#{attribute} IN (?)"
  606. end
  607. bind_params.push selector['value']
  608. end
  609. end
  610. # rubocop:enable Style/IfInsideElse
  611. end
  612. elsif selector['operator'] == 'is not'
  613. if selector['pre_condition'] == 'not_set'
  614. if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  615. query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  616. bind_params.push 1
  617. else
  618. query += "#{attribute} IS NOT NULL"
  619. end
  620. elsif selector['pre_condition'] == 'current_user.id'
  621. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  622. if attributes[1] == 'out_of_office_replacement_id'
  623. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  624. else
  625. bind_params.push current_user_id
  626. end
  627. elsif selector['pre_condition'] == 'current_user.organization_id'
  628. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  629. user = User.find_by(id: current_user_id)
  630. bind_params.push user.organization_id
  631. else
  632. # rubocop:disable Style/IfInsideElse
  633. if selector['value'].nil?
  634. query += "#{attribute} IS NOT NULL"
  635. else
  636. if attributes[1] == 'out_of_office_replacement_id'
  637. bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
  638. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  639. else
  640. if selector['value'].class != Array
  641. selector['value'] = [selector['value']]
  642. end
  643. query += if selector['value'].include?('')
  644. "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  645. else
  646. "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  647. end
  648. bind_params.push selector['value']
  649. end
  650. end
  651. # rubocop:enable Style/IfInsideElse
  652. end
  653. elsif selector['operator'] == 'contains'
  654. query += "#{attribute} #{like} (?)"
  655. value = "%#{selector['value']}%"
  656. bind_params.push value
  657. elsif selector['operator'] == 'contains not'
  658. query += "#{attribute} NOT #{like} (?)"
  659. value = "%#{selector['value']}%"
  660. bind_params.push value
  661. elsif selector['operator'] == 'contains all'
  662. if attributes[0] == 'ticket' && attributes[1] == 'tags'
  663. query += "? = (
  664. SELECT
  665. COUNT(*)
  666. FROM
  667. tag_objects,
  668. tag_items,
  669. tags
  670. WHERE
  671. tickets.id = tags.o_id AND
  672. tag_objects.id = tags.tag_object_id AND
  673. tag_objects.name = 'Ticket' AND
  674. tag_items.id = tags.tag_item_id AND
  675. tag_items.name IN (?)
  676. )"
  677. bind_params.push selector['value'].count
  678. bind_params.push selector['value']
  679. elsif Ticket.column_names.include?(attributes[1])
  680. query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'])
  681. end
  682. elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket'
  683. if attributes[1] == 'tags'
  684. tables += ', tag_objects, tag_items, tags'
  685. query += "
  686. tickets.id = tags.o_id AND
  687. tag_objects.id = tags.tag_object_id AND
  688. tag_objects.name = 'Ticket' AND
  689. tag_items.id = tags.tag_item_id AND
  690. tag_items.name IN (?)"
  691. bind_params.push selector['value']
  692. elsif Ticket.column_names.include?(attributes[1])
  693. query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'])
  694. end
  695. elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket'
  696. if attributes[1] == 'tags'
  697. query += "0 = (
  698. SELECT
  699. COUNT(*)
  700. FROM
  701. tag_objects,
  702. tag_items,
  703. tags
  704. WHERE
  705. tickets.id = tags.o_id AND
  706. tag_objects.id = tags.tag_object_id AND
  707. tag_objects.name = 'Ticket' AND
  708. tag_items.id = tags.tag_item_id AND
  709. tag_items.name IN (?)
  710. )"
  711. bind_params.push selector['value']
  712. elsif Ticket.column_names.include?(attributes[1])
  713. query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'], negated: true)
  714. end
  715. elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket'
  716. if attributes[1] == 'tags'
  717. query += "(
  718. SELECT
  719. COUNT(*)
  720. FROM
  721. tag_objects,
  722. tag_items,
  723. tags
  724. WHERE
  725. tickets.id = tags.o_id AND
  726. tag_objects.id = tags.tag_object_id AND
  727. tag_objects.name = 'Ticket' AND
  728. tag_items.id = tags.tag_item_id AND
  729. tag_items.name IN (?)
  730. ) BETWEEN 0 AND 0"
  731. bind_params.push selector['value']
  732. elsif Ticket.column_names.include?(attributes[1])
  733. query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'], negated: true)
  734. end
  735. elsif selector['operator'] == 'before (absolute)'
  736. query += "#{attribute} <= ?"
  737. bind_params.push selector['value']
  738. elsif selector['operator'] == 'after (absolute)'
  739. query += "#{attribute} >= ?"
  740. bind_params.push selector['value']
  741. elsif selector['operator'] == 'within last (relative)'
  742. query += "#{attribute} BETWEEN ? AND ?"
  743. time = range(selector).ago
  744. bind_params.push time
  745. bind_params.push Time.zone.now
  746. elsif selector['operator'] == 'within next (relative)'
  747. query += "#{attribute} BETWEEN ? AND ?"
  748. time = range(selector).from_now
  749. bind_params.push Time.zone.now
  750. bind_params.push time
  751. elsif selector['operator'] == 'before (relative)'
  752. query += "#{attribute} <= ?"
  753. time = range(selector).ago
  754. bind_params.push time
  755. elsif selector['operator'] == 'after (relative)'
  756. query += "#{attribute} >= ?"
  757. time = range(selector).from_now
  758. bind_params.push time
  759. elsif selector['operator'] == 'till (relative)'
  760. query += "#{attribute} <= ?"
  761. time = range(selector).from_now
  762. bind_params.push time
  763. elsif selector['operator'] == 'from (relative)'
  764. query += "#{attribute} >= ?"
  765. time = range(selector).ago
  766. bind_params.push time
  767. else
  768. raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
  769. end
  770. end
  771. return if no_result
  772. [query, bind_params, tables]
  773. end
  774. =begin
  775. perform changes on ticket
  776. ticket.perform_changes(trigger, 'trigger', item, current_user_id)
  777. # or
  778. ticket.perform_changes(job, 'job', item, current_user_id)
  779. =end
  780. def perform_changes(performable, perform_origin, item = nil, current_user_id = nil)
  781. perform = performable.perform
  782. logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }
  783. article = begin
  784. Ticket::Article.find_by(id: item.try(:dig, :article_id))
  785. rescue ArgumentError
  786. nil
  787. end
  788. # if the configuration contains the deletion of the ticket then
  789. # we skip all other ticket changes because they does not matter
  790. if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
  791. perform.each_key do |key|
  792. (object_name, attribute) = key.split('.', 2)
  793. next if object_name != 'ticket'
  794. next if attribute == 'action'
  795. perform.delete(key)
  796. end
  797. end
  798. objects = build_notification_template_objects(article)
  799. perform_notification = {}
  800. perform_article = {}
  801. changed = false
  802. perform.each do |key, value|
  803. (object_name, attribute) = key.split('.', 2)
  804. 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'
  805. # send notification/create article (after changes are done)
  806. if object_name == 'article'
  807. perform_article[key] = value
  808. next
  809. end
  810. if object_name == 'notification'
  811. perform_notification[key] = value
  812. next
  813. end
  814. # Apply pending_time changes
  815. if key == 'ticket.pending_time'
  816. new_value = case value['operator']
  817. when 'static'
  818. value['value']
  819. when 'relative'
  820. TimeRangeHelper.relative(range: value['range'], value: value['value'])
  821. end
  822. if new_value
  823. self[attribute] = new_value
  824. changed = true
  825. next
  826. end
  827. end
  828. # update tags
  829. if key == 'ticket.tags'
  830. next if value['value'].blank?
  831. tags = value['value'].split(',')
  832. case value['operator']
  833. when 'add'
  834. tags.each do |tag|
  835. tag_add(tag, current_user_id || 1)
  836. end
  837. when 'remove'
  838. tags.each do |tag|
  839. tag_remove(tag, current_user_id || 1)
  840. end
  841. else
  842. logger.error "Unknown #{attribute} operator #{value['operator']}"
  843. end
  844. next
  845. end
  846. # delete ticket
  847. if key == 'ticket.action'
  848. next if value['value'].blank?
  849. next if value['value'] != 'delete'
  850. logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
  851. destroy!
  852. next
  853. end
  854. # lookup pre_condition
  855. if value['pre_condition']
  856. if value['pre_condition'].start_with?('not_set')
  857. value['value'] = 1
  858. elsif value['pre_condition'].start_with?('current_user.')
  859. raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
  860. value['value'] = current_user_id
  861. end
  862. end
  863. # update ticket
  864. next if self[attribute].to_s == value['value'].to_s
  865. changed = true
  866. if value['value'].is_a?(String)
  867. value['value'] = NotificationFactory::Mailer.template(
  868. templateInline: value['value'],
  869. objects: objects,
  870. quote: true,
  871. )
  872. end
  873. self[attribute] = value['value']
  874. logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
  875. end
  876. if changed
  877. save!
  878. end
  879. perform_article.each do |key, value|
  880. raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
  881. add_trigger_note(id, value, objects, perform_origin)
  882. end
  883. perform_notification.each do |key, value|
  884. # send notification
  885. case key
  886. when 'notification.sms'
  887. send_sms_notification(value, article, perform_origin)
  888. next
  889. when 'notification.email'
  890. send_email_notification(value, article, perform_origin)
  891. when 'notification.webhook'
  892. TriggerWebhookJob.perform_later(performable, self, article)
  893. end
  894. end
  895. true
  896. end
  897. =begin
  898. perform changes on ticket
  899. ticket.add_trigger_note(ticket_id, note, objects, perform_origin)
  900. =end
  901. def add_trigger_note(ticket_id, note, objects, perform_origin)
  902. rendered_subject = NotificationFactory::Mailer.template(
  903. templateInline: note[:subject],
  904. objects: objects,
  905. quote: true,
  906. )
  907. rendered_body = NotificationFactory::Mailer.template(
  908. templateInline: note[:body],
  909. objects: objects,
  910. quote: true,
  911. )
  912. Ticket::Article.create!(
  913. ticket_id: ticket_id,
  914. subject: rendered_subject,
  915. content_type: 'text/html',
  916. body: rendered_body,
  917. internal: note[:internal],
  918. sender: Ticket::Article::Sender.find_by(name: 'System'),
  919. type: Ticket::Article::Type.find_by(name: 'note'),
  920. preferences: {
  921. perform_origin: perform_origin,
  922. notification: true,
  923. },
  924. updated_by_id: 1,
  925. created_by_id: 1,
  926. )
  927. end
  928. =begin
  929. perform active triggers on ticket
  930. Ticket.perform_triggers(ticket, article, item, options)
  931. =end
  932. def self.perform_triggers(ticket, article, item, options = {})
  933. recursive = Setting.get('ticket_trigger_recursive')
  934. type = options[:type] || item[:type]
  935. local_options = options.clone
  936. local_options[:type] = type
  937. local_options[:reset_user_id] = true
  938. local_options[:disable] = ['Transaction::Notification']
  939. local_options[:trigger_ids] ||= {}
  940. local_options[:trigger_ids][ticket.id.to_s] ||= []
  941. local_options[:loop_count] ||= 0
  942. local_options[:loop_count] += 1
  943. ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
  944. if local_options[:loop_count] > ticket_trigger_recursive_max_loop
  945. message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
  946. logger.info { message }
  947. return [false, message]
  948. end
  949. triggers = if Rails.configuration.db_case_sensitive
  950. ::Trigger.where(active: true).order(Arel.sql('LOWER(name)'))
  951. else
  952. ::Trigger.where(active: true).order(:name)
  953. end
  954. return [true, __('No triggers active')] if triggers.blank?
  955. # check if notification should be send because of customer emails
  956. send_notification = true
  957. if local_options[:send_notification] == false
  958. send_notification = false
  959. elsif item[:article_id]
  960. article = Ticket::Article.lookup(id: item[:article_id])
  961. if article&.preferences && article.preferences['send-auto-response'] == false
  962. send_notification = false
  963. end
  964. end
  965. Transaction.execute(local_options) do
  966. triggers.each do |trigger|
  967. logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  968. condition = trigger.condition
  969. # check if one article attribute is used
  970. one_has_changed_done = false
  971. article_selector = false
  972. trigger.condition.each_key do |key|
  973. (object_name, attribute) = key.split('.', 2)
  974. next if object_name != 'article'
  975. next if attribute == 'id'
  976. article_selector = true
  977. end
  978. if article && article_selector
  979. one_has_changed_done = true
  980. end
  981. if article && type == 'update'
  982. one_has_changed_done = true
  983. end
  984. # check ticket "has changed" options
  985. has_changed_done = true
  986. condition.each do |key, value|
  987. next if value.blank?
  988. next if value['operator'].blank?
  989. next if !value['operator']['has changed']
  990. # remove condition item, because it has changed
  991. (object_name, attribute) = key.split('.', 2)
  992. next if object_name != 'ticket'
  993. next if item[:changes].blank?
  994. next if !item[:changes].key?(attribute)
  995. condition.delete(key)
  996. one_has_changed_done = true
  997. end
  998. # check if we have not matching "has changed" attributes
  999. condition.each_value do |value|
  1000. next if value.blank?
  1001. next if value['operator'].blank?
  1002. next if !value['operator']['has changed']
  1003. has_changed_done = false
  1004. break
  1005. end
  1006. # check ticket action
  1007. if condition['ticket.action']
  1008. next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type
  1009. next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type
  1010. condition.delete('ticket.action')
  1011. end
  1012. next if !has_changed_done
  1013. # check in min one attribute of condition has changed on update
  1014. one_has_changed_condition = false
  1015. if type == 'update'
  1016. # verify if ticket condition exists
  1017. condition.each_key do |key|
  1018. (object_name, attribute) = key.split('.', 2)
  1019. next if object_name != 'ticket'
  1020. one_has_changed_condition = true
  1021. next if item[:changes].blank?
  1022. next if !item[:changes].key?(attribute)
  1023. one_has_changed_done = true
  1024. break
  1025. end
  1026. next if one_has_changed_condition && !one_has_changed_done
  1027. end
  1028. # check if ticket selector is matching
  1029. condition['ticket.id'] = {
  1030. operator: 'is',
  1031. value: ticket.id,
  1032. }
  1033. next if article_selector && !article
  1034. # check if article selector is matching
  1035. if article_selector
  1036. condition['article.id'] = {
  1037. operator: 'is',
  1038. value: article.id,
  1039. }
  1040. end
  1041. user_id = ticket.updated_by_id
  1042. if article
  1043. user_id = article.updated_by_id
  1044. end
  1045. user = User.lookup(id: user_id)
  1046. # verify is condition is matching
  1047. ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore')
  1048. next if ticket_count.blank?
  1049. next if ticket_count.zero?
  1050. next if tickets.first.id != ticket.id
  1051. if recursive == false && local_options[:loop_count] > 1
  1052. message = "Do not execute recursive triggers per default until Zammad 3.0. With Zammad 3.0 and higher the following trigger is executed '#{trigger.name}' on Ticket:#{ticket.id}. Please review your current triggers and change them if needed."
  1053. logger.info { message }
  1054. return [true, message]
  1055. end
  1056. if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
  1057. recipient = trigger.perform['notification.email']['recipient']
  1058. local_options[:send_notification] = false
  1059. if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
  1060. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
  1061. next
  1062. end
  1063. end
  1064. if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
  1065. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  1066. next
  1067. end
  1068. local_options[:trigger_ids][ticket.id.to_s].push trigger.id
  1069. logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  1070. ticket.perform_changes(trigger, 'trigger', item, user_id)
  1071. if recursive == true
  1072. TransactionDispatcher.commit(local_options)
  1073. end
  1074. end
  1075. end
  1076. [true, ticket, local_options]
  1077. end
  1078. def self.range(selector)
  1079. selector['value'].to_i.send(selector['range'].pluralize)
  1080. rescue
  1081. raise 'unknown selector'
  1082. end
  1083. =begin
  1084. get all email references headers of a ticket, to exclude some, parse it as array into method
  1085. references = ticket.get_references
  1086. result
  1087. ['message-id-1234', 'message-id-5678']
  1088. ignore references header(s)
  1089. references = ticket.get_references(['message-id-5678'])
  1090. result
  1091. ['message-id-1234']
  1092. =end
  1093. def get_references(ignore = [])
  1094. references = []
  1095. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
  1096. if article.in_reply_to.present?
  1097. references.push article.in_reply_to
  1098. end
  1099. next if article.message_id.blank?
  1100. references.push article.message_id
  1101. end
  1102. ignore.each do |item|
  1103. references.delete(item)
  1104. end
  1105. references
  1106. end
  1107. =begin
  1108. get all articles of a ticket in correct order (overwrite active record default method)
  1109. articles = ticket.articles
  1110. result
  1111. [article1, article2]
  1112. =end
  1113. def articles
  1114. Ticket::Article.where(ticket_id: id).order(:created_at, :id)
  1115. end
  1116. # Get whichever #last_contact_* was later
  1117. # This is not identical to #last_contact_at
  1118. # It returns time to last original (versus follow up) contact
  1119. # @return [Time, nil]
  1120. def last_original_update_at
  1121. [last_contact_agent_at, last_contact_customer_at].compact.max
  1122. end
  1123. # true if conversation did happen and agent responded
  1124. # false if customer is waiting for response or agent reached out and customer did not respond yet
  1125. # @return [Bool]
  1126. def agent_responded?
  1127. return false if last_contact_customer_at.blank?
  1128. return false if last_contact_agent_at.blank?
  1129. last_contact_customer_at < last_contact_agent_at
  1130. end
  1131. private
  1132. def check_generate
  1133. return true if number
  1134. self.number = Ticket::Number.generate
  1135. true
  1136. end
  1137. def check_title
  1138. return true if !title
  1139. title.gsub!(%r{\s|\t|\r}, ' ')
  1140. true
  1141. end
  1142. def check_defaults
  1143. if !owner_id
  1144. self.owner_id = 1
  1145. end
  1146. return true if !customer_id
  1147. customer = User.find_by(id: customer_id)
  1148. return true if !customer
  1149. return true if organization_id.present? && customer.organization_id?(organization_id)
  1150. self.organization_id = customer.organization_id
  1151. true
  1152. end
  1153. def reset_pending_time
  1154. # ignore if no state has changed
  1155. return true if !changes_to_save['state_id']
  1156. # ignore if new state is blank and
  1157. # let handle ActiveRecord the error
  1158. return if state_id.blank?
  1159. # check if new state isn't pending*
  1160. current_state = Ticket::State.lookup(id: state_id)
  1161. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  1162. # in case, set pending_time to nil
  1163. return true if current_state_type.name.match?(%r{^pending}i)
  1164. self.pending_time = nil
  1165. true
  1166. end
  1167. def set_default_state
  1168. return true if state_id
  1169. default_ticket_state = Ticket::State.find_by(default_create: true)
  1170. return true if !default_ticket_state
  1171. self.state_id = default_ticket_state.id
  1172. true
  1173. end
  1174. def set_default_priority
  1175. return true if priority_id
  1176. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  1177. return true if !default_ticket_priority
  1178. self.priority_id = default_ticket_priority.id
  1179. true
  1180. end
  1181. def check_owner_active
  1182. return true if Setting.get('import_mode')
  1183. # only change the owner for non closed Tickets for historical/reporting reasons
  1184. return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
  1185. # return when ticket is unassigned
  1186. return true if owner_id.blank?
  1187. return true if owner_id == 1
  1188. # return if owner is active, is agent and has access to group of ticket
  1189. return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
  1190. # else set the owner of the ticket to the default user as unassigned
  1191. self.owner_id = 1
  1192. true
  1193. end
  1194. # articles.last breaks (returns the wrong article)
  1195. # if another email notification trigger preceded this one
  1196. # (see https://github.com/zammad/zammad/issues/1543)
  1197. def build_notification_template_objects(article)
  1198. {
  1199. ticket: self,
  1200. article: article || articles.last
  1201. }
  1202. end
  1203. def send_email_notification(value, article, perform_origin)
  1204. # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
  1205. value_recipient = Array(value['recipient'])
  1206. recipients_raw = []
  1207. value_recipient.each do |recipient|
  1208. case recipient
  1209. when 'article_last_sender'
  1210. if article.present?
  1211. if article.reply_to.present?
  1212. recipients_raw.push(article.reply_to)
  1213. elsif article.from.present?
  1214. recipients_raw.push(article.from)
  1215. elsif article.origin_by_id
  1216. email = User.find_by(id: article.origin_by_id).email
  1217. recipients_raw.push(email)
  1218. elsif article.created_by_id
  1219. email = User.find_by(id: article.created_by_id).email
  1220. recipients_raw.push(email)
  1221. end
  1222. end
  1223. when 'ticket_customer'
  1224. email = User.find_by(id: customer_id).email
  1225. recipients_raw.push(email)
  1226. when 'ticket_owner'
  1227. email = User.find_by(id: owner_id).email
  1228. recipients_raw.push(email)
  1229. when 'ticket_agents'
  1230. User.group_access(group_id, 'full').sort_by(&:login).each do |user|
  1231. recipients_raw.push(user.email)
  1232. end
  1233. when %r{\Auserid_(\d+)\z}
  1234. user = User.lookup(id: $1)
  1235. if !user
  1236. logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
  1237. next
  1238. end
  1239. recipients_raw.push(user.email)
  1240. else
  1241. logger.error "Unknown email notification recipient '#{recipient}'"
  1242. next
  1243. end
  1244. end
  1245. recipients_checked = []
  1246. recipients_raw.each do |recipient_email|
  1247. users = User.where(email: recipient_email)
  1248. next if users.any? { |user| !trigger_based_notification?(user) }
  1249. # send notifications only to email addresses
  1250. next if recipient_email.blank?
  1251. # check if address is valid
  1252. begin
  1253. Mail::AddressList.new(recipient_email).addresses.each do |address|
  1254. recipient_email = address.address
  1255. email_address_validation = EmailAddressValidation.new(recipient_email)
  1256. break if recipient_email.present? && email_address_validation.valid?
  1257. end
  1258. rescue
  1259. if recipient_email.present?
  1260. if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
  1261. next # no usable format found
  1262. end
  1263. recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
  1264. end
  1265. end
  1266. email_address_validation = EmailAddressValidation.new(recipient_email)
  1267. next if !email_address_validation.valid?
  1268. # do not send notification if system address
  1269. next if EmailAddress.exists?(email: recipient_email.downcase)
  1270. # do not sent notifications to this recipients
  1271. send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
  1272. begin
  1273. next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
  1274. rescue => e
  1275. logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
  1276. logger.error e
  1277. next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
  1278. end
  1279. # check if notification should be send because of customer emails
  1280. if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
  1281. logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
  1282. next
  1283. end
  1284. # loop protection / check if maximal count of trigger mail has reached
  1285. map = {
  1286. 10 => 10,
  1287. 30 => 15,
  1288. 60 => 25,
  1289. 180 => 50,
  1290. 600 => 100,
  1291. }
  1292. skip = false
  1293. map.each do |minutes, count|
  1294. already_sent = Ticket::Article.where(
  1295. ticket_id: id,
  1296. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1297. type: Ticket::Article::Type.find_by(name: 'email'),
  1298. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  1299. next if already_sent < count
  1300. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
  1301. skip = true
  1302. break
  1303. end
  1304. next if skip
  1305. map = {
  1306. 10 => 30,
  1307. 30 => 60,
  1308. 60 => 120,
  1309. 180 => 240,
  1310. 600 => 360,
  1311. }
  1312. skip = false
  1313. map.each do |minutes, count|
  1314. already_sent = Ticket::Article.where(
  1315. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1316. type: Ticket::Article::Type.find_by(name: 'email'),
  1317. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  1318. next if already_sent < count
  1319. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
  1320. skip = true
  1321. break
  1322. end
  1323. next if skip
  1324. email = recipient_email.downcase.strip
  1325. next if recipients_checked.include?(email)
  1326. recipients_checked.push(email)
  1327. end
  1328. return if recipients_checked.blank?
  1329. recipient_string = recipients_checked.join(', ')
  1330. group_id = self.group_id
  1331. return if !group_id
  1332. email_address = Group.find(group_id).email_address
  1333. if !email_address
  1334. logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
  1335. return
  1336. end
  1337. if !email_address.channel_id
  1338. logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
  1339. return
  1340. end
  1341. security = nil
  1342. if Setting.get('smime_integration')
  1343. sign = value['sign'].present? && value['sign'] != 'no'
  1344. encryption = value['encryption'].present? && value['encryption'] != 'no'
  1345. security = {
  1346. type: 'S/MIME',
  1347. sign: {
  1348. success: false,
  1349. },
  1350. encryption: {
  1351. success: false,
  1352. },
  1353. }
  1354. if sign
  1355. sign_found = false
  1356. begin
  1357. list = Mail::AddressList.new(email_address.email)
  1358. from = list.addresses.first.to_s
  1359. cert = SMIMECertificate.for_sender_email_address(from)
  1360. if cert && !cert.expired?
  1361. sign_found = true
  1362. security[:sign][:success] = true
  1363. security[:sign][:comment] = "certificate for #{email_address.email} found"
  1364. end
  1365. rescue # rubocop:disable Lint/SuppressedException
  1366. end
  1367. if value['sign'] == 'discard' && !sign_found
  1368. logger.info "Unable to send trigger based notification to #{recipient_string} because of missing group #{group.name} email #{email_address.email} certificate for signing (discarding notification)."
  1369. return
  1370. end
  1371. end
  1372. if encryption
  1373. certs_found = false
  1374. begin
  1375. SMIMECertificate.for_recipipent_email_addresses!(recipients_checked)
  1376. certs_found = true
  1377. security[:encryption][:success] = true
  1378. security[:encryption][:comment] = "certificates found for #{recipient_string}"
  1379. rescue # rubocop:disable Lint/SuppressedException
  1380. end
  1381. if value['encryption'] == 'discard' && !certs_found
  1382. logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
  1383. return
  1384. end
  1385. end
  1386. end
  1387. objects = build_notification_template_objects(article)
  1388. # get subject
  1389. subject = NotificationFactory::Mailer.template(
  1390. templateInline: value['subject'],
  1391. objects: objects,
  1392. quote: false,
  1393. )
  1394. subject = subject_build(subject)
  1395. body = NotificationFactory::Mailer.template(
  1396. templateInline: value['body'],
  1397. objects: objects,
  1398. quote: true,
  1399. )
  1400. (body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
  1401. preferences = {}
  1402. preferences[:perform_origin] = perform_origin
  1403. if security.present?
  1404. preferences[:security] = security
  1405. end
  1406. message = Ticket::Article.create(
  1407. ticket_id: id,
  1408. to: recipient_string,
  1409. subject: subject,
  1410. content_type: 'text/html',
  1411. body: body,
  1412. internal: value['internal'] || false, # default to public if value was not set
  1413. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1414. type: Ticket::Article::Type.find_by(name: 'email'),
  1415. preferences: preferences,
  1416. updated_by_id: 1,
  1417. created_by_id: 1,
  1418. )
  1419. attachments_inline.each do |attachment|
  1420. Store.create!(
  1421. object: 'Ticket::Article',
  1422. o_id: message.id,
  1423. data: attachment[:data],
  1424. filename: attachment[:filename],
  1425. preferences: attachment[:preferences],
  1426. )
  1427. end
  1428. original_article = objects[:article]
  1429. if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
  1430. original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
  1431. end
  1432. if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
  1433. original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
  1434. original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
  1435. end
  1436. end
  1437. def sms_recipients_by_type(recipient_type, article)
  1438. case recipient_type
  1439. when 'article_last_sender'
  1440. return nil if article.blank?
  1441. if article.origin_by_id
  1442. article.origin_by_id
  1443. elsif article.created_by_id
  1444. article.created_by_id
  1445. end
  1446. when 'ticket_customer'
  1447. customer_id
  1448. when 'ticket_owner'
  1449. owner_id
  1450. when 'ticket_agents'
  1451. User.group_access(group_id, 'full').sort_by(&:login)
  1452. when %r{\Auserid_(\d+)\z}
  1453. return $1 if User.exists?($1)
  1454. logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
  1455. nil
  1456. else
  1457. logger.error "Unknown sms notification recipient '#{recipient}'"
  1458. nil
  1459. end
  1460. end
  1461. def build_sms_recipients_list(value, article)
  1462. Array(value['recipient'])
  1463. .each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
  1464. .map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
  1465. .uniq(&:id)
  1466. .select { |user| user.mobile.present? }
  1467. end
  1468. def send_sms_notification(value, article, perform_origin)
  1469. sms_recipients = build_sms_recipients_list(value, article)
  1470. if sms_recipients.blank?
  1471. logger.debug "No SMS recipients found for Ticket# #{number}"
  1472. return
  1473. end
  1474. sms_recipients_to = sms_recipients
  1475. .map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
  1476. .join(', ')
  1477. channel = Channel.find_by(area: 'Sms::Notification')
  1478. if !channel.active?
  1479. # write info message since we have an active trigger
  1480. logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
  1481. return
  1482. end
  1483. objects = build_notification_template_objects(article)
  1484. body = NotificationFactory::Renderer.new(
  1485. objects: objects,
  1486. template: value['body'],
  1487. escape: false
  1488. ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space
  1489. # attributes content_type is not needed for SMS
  1490. Ticket::Article.create(
  1491. ticket_id: id,
  1492. subject: 'SMS notification',
  1493. to: sms_recipients_to,
  1494. body: body,
  1495. internal: value['internal'] || false, # default to public if value was not set
  1496. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1497. type: Ticket::Article::Type.find_by(name: 'sms'),
  1498. preferences: {
  1499. perform_origin: perform_origin,
  1500. sms_recipients: sms_recipients.map(&:mobile),
  1501. channel_id: channel.id,
  1502. },
  1503. updated_by_id: 1,
  1504. created_by_id: 1,
  1505. )
  1506. end
  1507. def trigger_based_notification?(user)
  1508. blocked_in_days = trigger_based_notification_blocked_in_days(user)
  1509. return true if blocked_in_days.zero?
  1510. logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
  1511. false
  1512. end
  1513. def trigger_based_notification_blocked_in_days(user)
  1514. return 0 if !user.preferences[:mail_delivery_failed]
  1515. return 0 if user.preferences[:mail_delivery_failed_data].blank?
  1516. # blocked for 60 full days
  1517. (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  1518. end
  1519. end