ticket.rb 63 KB

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