sql.rb 28 KB

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