ticket.rb 63 KB

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