ticket.rb 63 KB

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