ticket.rb 63 KB

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