sql.rb 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Selector::Sql < Selector::Base
  3. VALID_OPERATORS = [
  4. 'after (absolute)',
  5. 'after (relative)',
  6. 'before (absolute)',
  7. 'before (relative)',
  8. 'contains all not',
  9. 'contains all',
  10. 'contains not',
  11. 'contains one not',
  12. 'contains one',
  13. 'contains',
  14. 'does not match regex',
  15. 'ends with one of',
  16. 'ends with', # keep for compatibility with old conditions
  17. 'from (relative)',
  18. 'has changed',
  19. 'has reached warning',
  20. 'has reached',
  21. 'is any of',
  22. 'is in working time',
  23. 'is none of',
  24. 'is not in working time',
  25. 'is not',
  26. 'is set',
  27. 'is',
  28. 'matches regex',
  29. 'not set',
  30. 'starts with one of',
  31. 'starts with', # keep for compatibility with old conditions
  32. 'till (relative)',
  33. 'today',
  34. 'within last (relative)',
  35. 'within next (relative)',
  36. ].freeze
  37. attr_accessor :final_query, :final_bind_params, :final_tables, :changed_attributes
  38. def get
  39. @final_query = []
  40. @final_bind_params = []
  41. @final_tables = []
  42. @final_query = run(selector, 0)
  43. [query_sql, final_bind_params, tables_sql]
  44. rescue InvalidCondition => e
  45. Rails.logger.error "Selector::Sql.get->InvalidCondition: #{e}"
  46. nil
  47. rescue => e
  48. Rails.logger.error "Selector::Sql.get->default: #{e}"
  49. raise e
  50. end
  51. def query_sql
  52. Array(final_query).join(' AND ')
  53. end
  54. def tables_sql
  55. return '' if final_tables.blank?
  56. " #{final_tables.join(' ')}"
  57. end
  58. def run(block, level)
  59. if block.key?(:conditions)
  60. run_block(block, level)
  61. else
  62. query, bind_params, tables = condition_sql(block)
  63. @final_bind_params += bind_params
  64. @final_tables |= tables
  65. query
  66. end
  67. end
  68. def run_block(block, level)
  69. block_query = block[:conditions].map do |sub_block|
  70. run(sub_block, level + 1)
  71. end
  72. block_query = block_query.compact
  73. return if block_query.blank?
  74. return "NOT(#{block_query.join(' AND ')})" if block[:operator] == 'NOT'
  75. "(#{block_query.join(" #{block[:operator]} ")})"
  76. end
  77. def condition_sql(block_condition)
  78. current_user = options[:current_user]
  79. current_user_id = UserInfo.current_user_id
  80. if current_user
  81. current_user_id = current_user.id
  82. end
  83. raise InvalidCondition, "No block condition #{block_condition.inspect}" if block_condition.blank?
  84. raise InvalidCondition, "No block condition name #{block_condition.inspect}" if block_condition[:name].blank?
  85. # remember query and bind params
  86. query = []
  87. tables = []
  88. bind_params = []
  89. like = Rails.application.config.db_like
  90. attribute_table, attribute_name = block_condition[:name].split('.')
  91. # get tables to join
  92. return if !attribute_name
  93. return if !attribute_table
  94. sql_helper = SqlHelper.new(object: target_class)
  95. if attribute_table && %w[execution_time ticket_owner ticket_customer].exclude?(attribute_table) && attribute_table != target_name && tables.exclude?(attribute_table) && !(attribute_table == 'ticket' && attribute_name != 'mention_user_ids') && !(attribute_table == 'ticket' && attribute_name == 'mention_user_ids' && block_condition[:pre_condition] == 'not_set') && !(attribute_table == 'article' && attribute_name == 'action')
  96. case attribute_table
  97. when 'customer'
  98. tables |= ["INNER JOIN users customers ON #{target_table}.customer_id = customers.id"]
  99. sql_helper = SqlHelper.new(object: User, table_name: 'customers')
  100. when 'organization'
  101. tables |= ["LEFT JOIN organizations ON #{target_table}.organization_id = organizations.id"]
  102. sql_helper = SqlHelper.new(object: Organization)
  103. when 'owner'
  104. tables |= ['INNER JOIN users owners ON tickets.owner_id = owners.id']
  105. sql_helper = SqlHelper.new(object: User, table_name: 'owners')
  106. when 'article'
  107. tables |= ['INNER JOIN ticket_articles articles ON tickets.id = articles.ticket_id']
  108. sql_helper = SqlHelper.new(object: Ticket::Article, table_name: 'articles')
  109. when 'ticket_state'
  110. tables |= ['INNER JOIN ticket_states ON tickets.state_id = ticket_states.id']
  111. sql_helper = SqlHelper.new(object: Ticket::State)
  112. else
  113. raise "invalid selector #{attribute_table}, #{attribute_name}"
  114. end
  115. end
  116. validate_operator! block_condition
  117. validate_pre_condition_blank! block_condition
  118. validate_pre_condition_values! block_condition
  119. is_json_column = sql_helper.json_column?(attribute_name)
  120. # get attributes
  121. attribute = is_json_column ? sql_helper.json_db_column_with_key(attribute_name, 'value') : sql_helper.db_column(attribute_name)
  122. # magic block_condition
  123. if attribute_table == 'ticket' && attribute_name == 'out_of_office_replacement_id'
  124. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attribute_table}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
  125. end
  126. if attribute_table == 'ticket' && attribute_name == 'tags'
  127. block_condition[:value] = block_condition[:value].split(',').collect(&:strip)
  128. end
  129. # Performance: use left join instead of sub select if tags value is only one element and contains all is used
  130. if attribute_table == 'ticket' && attribute_name == 'tags' && block_condition[:operator] == 'contains all' && block_condition[:value].count == 1
  131. block_condition[:operator] = 'contains one'
  132. end
  133. # User customer tickets last_contact_at
  134. query_wrap = nil
  135. if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_at'
  136. attribute = 'last_contact_at'
  137. query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
  138. end
  139. # User customer tickets last_contact_agent_at
  140. if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_agent_at'
  141. attribute = 'last_contact_agent_at'
  142. query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
  143. end
  144. # User customer tickets last_contact_customer_at
  145. if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_customer_at'
  146. attribute = 'last_contact_customer_at'
  147. query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
  148. end
  149. # User customer tickets updated_at
  150. if attribute_table == 'ticket_customer' && attribute_name == 'updated_at'
  151. attribute = 'updated_at'
  152. query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
  153. end
  154. #
  155. # checks
  156. #
  157. #
  158. if attribute_table == 'article' && options.key?(:article_id) && options[:article_id].blank? && attribute_name != 'action'
  159. query << '1 = 0'
  160. elsif block_condition[:operator].include?('in working time')
  161. raise __('Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)') if !options[:execution_time]
  162. biz = Calendar.lookup(id: block_condition[:value])&.biz
  163. query << if biz.present? && attribute_name == 'calendar_id' && ((block_condition[:operator] == 'is in working time' && !biz.in_hours?(Time.zone.now)) || (block_condition[:operator] == 'is not in working time' && biz.in_hours?(Time.zone.now)))
  164. '1 = 0'
  165. else
  166. '1 = 1'
  167. end
  168. elsif block_condition[:operator] == 'has changed'
  169. query << if changed_attributes[ block_condition[:name] ]
  170. '1 = 1'
  171. else
  172. '1 = 0'
  173. end
  174. elsif block_condition[:operator] == 'has reached'
  175. query << if time_based_trigger?(block_condition, warning: false)
  176. '1 = 1'
  177. else
  178. '1 = 0'
  179. end
  180. elsif block_condition[:operator] == 'has reached warning'
  181. query << if time_based_trigger?(block_condition, warning: true)
  182. '1 = 1'
  183. else
  184. '1 = 0'
  185. end
  186. elsif attribute_table == 'ticket' && attribute_name == 'action'
  187. check = options[:ticket_action] == block_condition[:value] ? 1 : 0
  188. query << if update_action_requires_changed_attributes?(block_condition, check)
  189. '1 = 0'
  190. elsif block_condition[:operator] == 'is'
  191. "1 = #{check}"
  192. else
  193. "0 = #{check}" # is not
  194. end
  195. elsif attribute_table == 'article' && attribute_name == 'action'
  196. check = options[:article_id] ? 1 : 0
  197. query << if block_condition[:operator] == 'is'
  198. "1 = #{check}"
  199. else
  200. "0 = #{check}" # is not
  201. end
  202. elsif attribute_table == 'article' && attribute_name == 'time_accounting'
  203. tables |= ["LEFT JOIN ticket_time_accountings ON ticket_time_accountings.ticket_article_id = #{options[:article_id].to_i}"]
  204. query << if block_condition[:operator] == 'is set'
  205. 'ticket_time_accountings.id IS NOT NULL'
  206. else
  207. 'ticket_time_accountings.id IS NULL' # not set
  208. end
  209. # because of no grouping support we select not_set by sub select for mentions
  210. elsif attribute_table == 'ticket' && attribute_name == 'mention_user_ids'
  211. if block_condition[:pre_condition] == 'not_set'
  212. tables |= ["LEFT JOIN mentions ON tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"]
  213. query << if block_condition[:operator] == 'is'
  214. 'mentions.user_id IS NULL'
  215. else
  216. 'mentions.user_id IS NOT NULL'
  217. end
  218. else
  219. query << if block_condition[:operator] == 'is'
  220. tables |= ["LEFT JOIN mentions ON tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"]
  221. 'mentions.user_id IN (?)'
  222. else
  223. "tickets.id NOT IN (SELECT mentionable_id FROM mentions WHERE mentionable_type = 'Ticket' AND user_id IN (?))"
  224. end
  225. if block_condition[:pre_condition] == 'current_user.id'
  226. bind_params.push current_user_id
  227. else
  228. bind_params.push block_condition[:value]
  229. end
  230. end
  231. elsif attribute_table == 'user' && attribute_name == 'role_ids'
  232. query << if block_condition[:operator] == 'is'
  233. "users.id IN (
  234. SELECT
  235. roles_users.user_id
  236. FROM
  237. roles, roles_users
  238. WHERE
  239. roles.id = roles_users.role_id
  240. AND roles.id IN (?)
  241. GROUP BY
  242. roles_users.user_id
  243. )"
  244. else
  245. "users.id NOT IN (
  246. SELECT
  247. roles_users.user_id
  248. FROM
  249. roles, roles_users
  250. WHERE
  251. roles.id = roles_users.role_id
  252. AND roles.id IN (?)
  253. GROUP BY
  254. roles_users.user_id
  255. )"
  256. end
  257. bind_params.push block_condition[:value]
  258. elsif attribute_table == 'organization' && attribute_name == 'members_existing'
  259. query << if (block_condition[:operator] == 'is' && block_condition[:value].to_s == 'true') || (block_condition[:operator] == 'is not' && block_condition[:value].to_s != 'true')
  260. 'organizations.id IN (SELECT DISTINCT organizations.id FROM organizations, users WHERE organizations.id = users.organization_id)'
  261. else
  262. 'organizations.id NOT IN (SELECT DISTINCT organizations.id FROM organizations, users WHERE organizations.id = users.organization_id)'
  263. end
  264. elsif %w[ticket_customer ticket_owner].include?(attribute_table) && %w[existing open_existing].include?(attribute_name)
  265. distinct_column = if attribute_table == 'ticket_customer'
  266. 'customer_id'
  267. else
  268. 'owner_id'
  269. end
  270. query_where = ''
  271. if attribute_name == 'open_existing'
  272. query_where = ' WHERE state_id IN (?)'
  273. bind_params.push Ticket::State.by_category_ids(:open)
  274. end
  275. query << if (block_condition[:operator] == 'is' && block_condition[:value].to_s == 'true') || (block_condition[:operator] == 'is not' && block_condition[:value].to_s != 'true')
  276. "users.id IN (SELECT DISTINCT #{distinct_column} FROM tickets#{query_where})"
  277. else
  278. "users.id NOT IN (SELECT DISTINCT #{distinct_column} FROM tickets#{query_where})"
  279. end
  280. elsif block_condition[:operator] == 'starts with'
  281. query << "#{attribute} #{like} (?)"
  282. bind_params.push "#{SqlHelper.quote_like(block_condition[:value])}%"
  283. elsif block_condition[:operator] == 'starts with one of'
  284. block_condition[:value] = Array.wrap(block_condition[:value])
  285. sub_query = []
  286. block_condition[:value].each do |value|
  287. sub_query << "#{attribute} #{like} (?)"
  288. bind_params.push "#{SqlHelper.quote_like(value)}%"
  289. end
  290. query << "(#{sub_query.join(' OR ')})" if sub_query.present?
  291. elsif block_condition[:operator] == 'ends with'
  292. query << "#{attribute} #{like} (?)"
  293. bind_params.push "%#{SqlHelper.quote_like(block_condition[:value])}"
  294. elsif block_condition[:operator] == 'ends with one of'
  295. block_condition[:value] = Array.wrap(block_condition[:value])
  296. sub_query = []
  297. block_condition[:value].each do |value|
  298. sub_query << "#{attribute} #{like} (?)"
  299. bind_params.push "%#{SqlHelper.quote_like(value)}"
  300. end
  301. query << "(#{sub_query.join(' OR ')})" if sub_query.present?
  302. elsif block_condition[:operator] == 'is any of'
  303. block_condition[:value] = Array.wrap(block_condition[:value])
  304. block_condition[:value] = block_condition[:value].empty? ? [''] : block_condition[:value]
  305. sub_query = []
  306. block_condition[:value].each do |value|
  307. sub_query << "#{attribute} IN (?)"
  308. bind_params.push value
  309. end
  310. query << "(#{sub_query.join(' OR ')})" if sub_query.present?
  311. elsif block_condition[:operator] == 'is none of'
  312. block_condition[:value] = Array.wrap(block_condition[:value])
  313. block_condition[:value] = block_condition[:value].empty? ? [''] : block_condition[:value]
  314. sub_query = []
  315. block_condition[:value].each do |value|
  316. sub_query << "#{attribute} NOT IN (?)"
  317. bind_params.push value
  318. end
  319. query << "(#{sub_query.join(' AND ')})" if sub_query.present?
  320. elsif block_condition[:operator] == 'is'
  321. if block_condition[:pre_condition] == 'not_set'
  322. if attribute_name.match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  323. query << "(#{attribute} IS NULL OR #{attribute} IN (?))"
  324. bind_params.push 1
  325. else
  326. query << "#{attribute} IS NULL"
  327. end
  328. elsif block_condition[:pre_condition] == 'current_user.id'
  329. raise "Use current_user.id in block_condition, but no current_user is set #{block_condition.inspect}" if !current_user_id
  330. query << "#{attribute} IN (?)"
  331. if attribute_name == 'out_of_office_replacement_id'
  332. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  333. else
  334. bind_params.push current_user_id
  335. end
  336. elsif block_condition[:pre_condition] == 'current_user.organization_id'
  337. raise "Use current_user.id in block_condition, but no current_user is set #{block_condition.inspect}" if !current_user_id
  338. query << "#{attribute} IN (?)"
  339. user = User.find_by(id: current_user_id)
  340. bind_params.push user.all_organization_ids
  341. else
  342. # rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
  343. if block_condition[:value].nil?
  344. query << "#{attribute} IS NULL"
  345. else
  346. if is_json_column
  347. query << "#{attribute} IN (?)"
  348. bind_params.push(Array.wrap(block_condition[:value]).map { |item| item[:value].to_s })
  349. elsif attribute_name == 'out_of_office_replacement_id'
  350. query << "#{attribute} IN (?)"
  351. bind_params.push User.where(id: Array.wrap(block_condition[:value])).map(&:out_of_office_agent_of).flatten.map(&:id)
  352. else
  353. block_condition[:value] = Array.wrap(block_condition[:value])
  354. query << if block_condition[:value].include?('')
  355. "(#{attribute} IN (?) OR #{attribute} IS NULL)"
  356. else
  357. "#{attribute} IN (?)"
  358. end
  359. bind_params.push block_condition[:value]
  360. end
  361. end
  362. # rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
  363. end
  364. elsif block_condition[:operator] == 'is not'
  365. if block_condition[:pre_condition] == 'not_set'
  366. if attribute_name.match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  367. query << "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  368. bind_params.push 1
  369. else
  370. query << "#{attribute} IS NOT NULL"
  371. end
  372. elsif block_condition[:pre_condition] == 'current_user.id'
  373. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  374. if attribute_name == 'out_of_office_replacement_id'
  375. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  376. else
  377. bind_params.push current_user_id
  378. end
  379. elsif block_condition[:pre_condition] == 'current_user.organization_id'
  380. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  381. user = User.find_by(id: current_user_id)
  382. bind_params.push user.organization_id
  383. else
  384. # rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
  385. if block_condition[:value].nil?
  386. query << "#{attribute} IS NOT NULL"
  387. else
  388. if is_json_column
  389. query << "#{attribute} NOT IN (?)"
  390. bind_params.push(Array.wrap(block_condition[:value]).map { |item| item[:value].to_s })
  391. elsif attribute_name == 'out_of_office_replacement_id'
  392. bind_params.push User.find(block_condition[:value]).out_of_office_agent_of.pluck(:id)
  393. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  394. else
  395. block_condition[:value] = Array.wrap(block_condition[:value])
  396. query << if block_condition[:value].include?('')
  397. "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  398. else
  399. "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  400. end
  401. bind_params.push block_condition[:value]
  402. end
  403. end
  404. # rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
  405. end
  406. elsif block_condition[:operator] == 'contains'
  407. query << "#{attribute} #{like} (?)"
  408. bind_params.push "%#{SqlHelper.quote_like(block_condition[:value])}%"
  409. elsif block_condition[:operator] == 'contains not'
  410. # NOT LIKE is always false on NULL values
  411. # https://github.com/zammad/zammad/issues/4948
  412. query << "#{attribute} NOT #{like} (?) OR #{attribute} IS NULL"
  413. bind_params.push "%#{SqlHelper.quote_like(block_condition[:value])}%"
  414. elsif block_condition[:operator] == 'matches regex'
  415. query << sql_helper.regex_match(attribute, negated: false)
  416. bind_params.push block_condition[:value]
  417. elsif block_condition[:operator] == 'does not match regex'
  418. query << sql_helper.regex_match(attribute, negated: true)
  419. bind_params.push block_condition[:value]
  420. elsif block_condition[:operator] == 'contains all'
  421. if attribute_table == 'ticket' && attribute_name == 'tags'
  422. query << "tickets.id IN (
  423. SELECT
  424. tags.o_id
  425. FROM
  426. tag_objects, tag_items, tags
  427. WHERE
  428. tag_objects.id = tags.tag_object_id
  429. AND tag_objects.name = 'Ticket'
  430. AND tag_items.id = tags.tag_item_id
  431. AND tag_items.name IN (?)
  432. GROUP BY
  433. tags.o_id
  434. HAVING
  435. COUNT(*) = ?
  436. )"
  437. bind_params.push block_condition[:value]
  438. bind_params.push block_condition[:value].count
  439. elsif sql_helper.containable?(attribute_name)
  440. query << sql_helper.array_contains_all(attribute_name, block_condition[:value])
  441. end
  442. elsif block_condition[:operator] == 'contains one'
  443. if attribute_name == 'tags' && attribute_table == 'ticket'
  444. tables |= ["LEFT JOIN tags ON tickets.id = tags.o_id LEFT JOIN tag_objects ON tag_objects.id = tags.tag_object_id AND tag_objects.name = 'Ticket' LEFT JOIN tag_items ON tag_items.id = tags.tag_item_id"]
  445. query << 'tag_items.name IN (?)'
  446. bind_params.push block_condition[:value]
  447. elsif sql_helper.containable?(attribute_name)
  448. query << sql_helper.array_contains_one(attribute_name, block_condition[:value])
  449. end
  450. elsif block_condition[:operator] == 'contains all not'
  451. if attribute_name == 'tags' && attribute_table == 'ticket'
  452. query << "tickets.id NOT IN (
  453. SELECT
  454. DISTINCT tags.o_id
  455. FROM
  456. tag_objects, tag_items, tags
  457. WHERE
  458. tag_objects.id = tags.tag_object_id
  459. AND tag_objects.name = 'Ticket'
  460. AND tag_items.id = tags.tag_item_id
  461. AND tag_items.name IN (?)
  462. GROUP BY
  463. tags.o_id
  464. HAVING
  465. COUNT(*) = ?
  466. )"
  467. bind_params.push block_condition[:value]
  468. bind_params.push block_condition[:value].count
  469. elsif sql_helper.containable?(attribute_name)
  470. query << sql_helper.array_contains_all(attribute_name, block_condition[:value], negated: true)
  471. end
  472. elsif block_condition[:operator] == 'contains one not'
  473. if attribute_name == 'tags' && attribute_table == 'ticket'
  474. query << "tickets.id NOT IN (
  475. SELECT
  476. DISTINCT tags.o_id
  477. FROM
  478. tag_objects, tag_items, tags
  479. WHERE
  480. tag_objects.id = tags.tag_object_id
  481. AND tag_objects.name = 'Ticket'
  482. AND tag_items.id = tags.tag_item_id
  483. AND tag_items.name IN (?)
  484. )"
  485. bind_params.push block_condition[:value]
  486. elsif sql_helper.containable?(attribute_name)
  487. query << sql_helper.array_contains_one(attribute_name, block_condition[:value], negated: true)
  488. end
  489. elsif block_condition[:operator] == 'today'
  490. Time.use_zone(Setting.get('timezone_default')) do
  491. day_start = Time.zone.now.beginning_of_day.utc
  492. day_end = Time.zone.now.end_of_day.utc
  493. query << "#{attribute} BETWEEN ? AND ?"
  494. bind_params.push day_start
  495. bind_params.push day_end
  496. end
  497. elsif block_condition[:operator] == 'before (absolute)'
  498. query << "#{attribute} <= ?"
  499. bind_params.push block_condition[:value]
  500. elsif block_condition[:operator] == 'after (absolute)'
  501. query << "#{attribute} >= ?"
  502. bind_params.push block_condition[:value]
  503. elsif block_condition[:operator] == 'within last (relative)'
  504. query << "#{attribute} BETWEEN ? AND ?"
  505. time = range(block_condition).ago
  506. bind_params.push time
  507. bind_params.push Time.zone.now
  508. elsif block_condition[:operator] == 'within next (relative)'
  509. query << "#{attribute} BETWEEN ? AND ?"
  510. time = range(block_condition).from_now
  511. bind_params.push Time.zone.now
  512. bind_params.push time
  513. elsif block_condition[:operator] == 'before (relative)'
  514. query << "#{attribute} <= ?"
  515. time = range(block_condition).ago
  516. bind_params.push time
  517. elsif block_condition[:operator] == 'after (relative)'
  518. query << "#{attribute} >= ?"
  519. time = range(block_condition).from_now
  520. bind_params.push time
  521. elsif block_condition[:operator] == 'till (relative)'
  522. query << "#{attribute} <= ?"
  523. time = range(block_condition).from_now
  524. bind_params.push time
  525. elsif block_condition[:operator] == 'from (relative)'
  526. query << "#{attribute} >= ?"
  527. time = range(block_condition).ago
  528. bind_params.push time
  529. else
  530. raise "Invalid operator '#{block_condition[:operator]}' for '#{block_condition[:value].inspect}'"
  531. end
  532. if query_wrap.present?
  533. query << query_wrap.gsub('###QUERY###', query.pop)
  534. end
  535. query.map! { "(#{_1})" }
  536. [query, bind_params, tables]
  537. end
  538. def range(selector)
  539. selector[:value].to_i.send(selector[:range].pluralize)
  540. rescue
  541. raise 'unknown selector'
  542. end
  543. def validate_operator!(condition)
  544. if condition[:operator].blank?
  545. raise "Invalid condition, operator missing #{condition.inspect}"
  546. end
  547. return true if self.class.valid_operator?(condition[:operator])
  548. raise "Invalid condition, operator '#{condition[:operator]}' is invalid #{condition.inspect}"
  549. end
  550. def time_based_trigger?(condition, warning:)
  551. case [condition[:name], options[:ticket_action]]
  552. in 'ticket.pending_time', 'reminder_reached'
  553. true
  554. in 'ticket.escalation_at', 'escalation'
  555. !warning
  556. in 'ticket.escalation_at', 'escalation_warning'
  557. warning
  558. else
  559. false
  560. end
  561. end
  562. # validate pre_condition values
  563. def validate_pre_condition_values!(condition)
  564. return if ['has changed', 'has reached', 'has reached warning'].include? condition[:operator]
  565. return if condition[:pre_condition].blank?
  566. return if %w[not_set current_user. specific].any? { |elem| condition[:pre_condition].start_with? elem }
  567. raise InvalidCondition, "Invalid condition pre_condition not set #{condition}!"
  568. end
  569. # validate value / allow blank but only if pre_condition exists and is not specific
  570. def validate_pre_condition_blank!(condition)
  571. return if ['has changed', 'has reached', 'has reached warning', 'is any of', 'is none of', 'is set', 'not set'].include? condition[:operator]
  572. if (condition[:operator] != 'today' && !condition.key?(:value)) ||
  573. (condition[:value].instance_of?(Array) && condition[:value].respond_to?(:blank?) && condition[:value].blank?) ||
  574. (condition[:operator].start_with?('contains') && condition[:value].respond_to?(:blank?) && condition[:value].blank?)
  575. raise InvalidCondition, "Invalid condition pre_condition nil #{condition}!" if condition[:pre_condition].nil?
  576. raise InvalidCondition, "Invalid condition pre_condition blank #{condition}!" if condition[:pre_condition].respond_to?(:blank?) && condition[:pre_condition].blank?
  577. raise InvalidCondition, "Invalid condition pre_condition specific #{condition}!" if condition[:pre_condition] == 'specific'
  578. end
  579. end
  580. def update_action_requires_changed_attributes?(condition, check)
  581. condition[:value] == 'update' && check && options[:changes_required] && changed_attributes.blank?
  582. end
  583. def self.valid_operator?(operator)
  584. VALID_OPERATORS.include?(operator)
  585. end
  586. def valid?
  587. object_count, _objects = target_class.selectors(selector, **options.merge(limit: 1, execution_time: true, ticket_id: 1, access: 'ignore'))
  588. !object_count.nil?
  589. rescue
  590. false
  591. end
  592. end