sql.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class Ticket::Selector::Sql < Ticket::Selector::Base
  3. VALID_OPERATORS = [
  4. 'has changed',
  5. 'has reached',
  6. 'has reached warning',
  7. 'is',
  8. 'is not',
  9. 'contains',
  10. %r{contains (not|all|one|all not|one not)},
  11. 'today',
  12. %r{(after|before) \(absolute\)},
  13. %r{(within next|within last|after|before|till|from) \(relative\)},
  14. 'is in working time',
  15. 'is not in working time',
  16. 'starts with',
  17. 'ends with',
  18. 'matches regex',
  19. 'does not match regex',
  20. ].freeze
  21. attr_accessor :final_query, :final_bind_params, :final_tables, :changed_attributes
  22. def get
  23. @final_query = []
  24. @final_bind_params = []
  25. @final_tables = []
  26. @final_query = run(selector, 0)
  27. [query_sql, final_bind_params, tables_sql]
  28. rescue InvalidCondition => e
  29. Rails.logger.error "Ticket::Selector.get->InvalidCondition: #{e}"
  30. nil
  31. rescue => e
  32. Rails.logger.error "Ticket::Selector.get->default: #{e}"
  33. raise e
  34. end
  35. def query_sql
  36. Array(final_query).join(' AND ')
  37. end
  38. def tables_sql
  39. return '' if final_tables.blank?
  40. " #{final_tables.join(' ')}"
  41. end
  42. def run(block, level)
  43. if block.key?(:conditions)
  44. run_block(block, level)
  45. else
  46. query, bind_params, tables = condition_sql(block)
  47. @final_bind_params += bind_params
  48. @final_tables |= tables
  49. query
  50. end
  51. end
  52. def run_block(block, level)
  53. block_query = []
  54. block[:conditions].each do |sub_block|
  55. block_query << run(sub_block, level + 1)
  56. end
  57. block_query = block_query.compact
  58. return if block_query.blank?
  59. return "NOT(#{block_query.join(' AND ')})" if block[:operator] == 'NOT'
  60. "(#{block_query.join(" #{block[:operator]} ")})"
  61. end
  62. def condition_sql(block_condition)
  63. current_user = options[:current_user]
  64. current_user_id = UserInfo.current_user_id
  65. if current_user
  66. current_user_id = current_user.id
  67. end
  68. raise InvalidCondition, "No block condition #{block_condition.inspect}" if block_condition.blank?
  69. raise InvalidCondition, "No block condition name #{block_condition.inspect}" if block_condition[:name].blank?
  70. # remember query and bind params
  71. query = []
  72. tables = []
  73. bind_params = []
  74. like = Rails.application.config.db_like
  75. attribute_table, attribute_name = block_condition[:name].split('.')
  76. # get tables to join
  77. return if !attribute_name
  78. return if !attribute_table
  79. sql_helper = SqlHelper.new(object: Ticket)
  80. if attribute_table && attribute_table != 'execution_time' && 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')
  81. case attribute_table
  82. when 'customer'
  83. tables |= ['INNER JOIN users customers ON tickets.customer_id = customers.id']
  84. sql_helper = SqlHelper.new(object: User, table_name: 'customers')
  85. when 'organization'
  86. tables |= ['LEFT JOIN organizations ON tickets.organization_id = organizations.id']
  87. sql_helper = SqlHelper.new(object: Organization)
  88. when 'owner'
  89. tables |= ['INNER JOIN users owners ON tickets.owner_id = owners.id']
  90. sql_helper = SqlHelper.new(object: User, table_name: 'owners')
  91. when 'article'
  92. tables |= ['INNER JOIN ticket_articles articles ON tickets.id = articles.ticket_id']
  93. sql_helper = SqlHelper.new(object: Ticket::Article)
  94. when 'ticket_state'
  95. tables |= ['INNER JOIN ticket_states ON tickets.state_id = ticket_states.id']
  96. sql_helper = SqlHelper.new(object: Ticket::State)
  97. when 'ticket'
  98. if attribute_name == 'mention_user_ids'
  99. tables |= ["LEFT JOIN mentions ON tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"]
  100. end
  101. else
  102. raise "invalid selector #{attribute_table}, #{attribute_name}"
  103. end
  104. end
  105. validate_operator! block_condition
  106. validate_pre_condition_blank! block_condition
  107. validate_pre_condition_values! block_condition
  108. # get attributes
  109. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attribute_table}s")}.#{ActiveRecord::Base.connection.quote_column_name(attribute_name)}"
  110. # magic block_condition
  111. if attribute_table == 'ticket' && attribute_name == 'out_of_office_replacement_id'
  112. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attribute_table}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
  113. end
  114. if attribute_table == 'ticket' && attribute_name == 'tags'
  115. block_condition[:value] = block_condition[:value].split(',').collect(&:strip)
  116. end
  117. #
  118. # checks
  119. #
  120. #
  121. if attribute_table == 'article' && options.key?(:article_id) && options[:article_id].blank? && attribute_name != 'action'
  122. query << '1 = 0'
  123. elsif block_condition[:operator].include?('in working time')
  124. raise __('Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)') if !options[:execution_time]
  125. biz = Calendar.lookup(id: block_condition[:value])&.biz
  126. 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)))
  127. '1 = 0'
  128. else
  129. '1 = 1'
  130. end
  131. elsif block_condition[:operator] == 'has changed'
  132. query << if changed_attributes[ block_condition[:name] ]
  133. '1 = 1'
  134. else
  135. '1 = 0'
  136. end
  137. elsif block_condition[:operator] == 'has reached'
  138. query << if time_based_trigger?(block_condition, warning: false)
  139. '1 = 1'
  140. else
  141. '1 = 0'
  142. end
  143. elsif block_condition[:operator] == 'has reached warning'
  144. query << if time_based_trigger?(block_condition, warning: true)
  145. '1 = 1'
  146. else
  147. '1 = 0'
  148. end
  149. elsif attribute_table == 'ticket' && attribute_name == 'action'
  150. check = options[:ticket_action] == block_condition[:value] ? 1 : 0
  151. query << if update_action_requires_changed_attributes?(block_condition, check)
  152. '1 = 0'
  153. elsif block_condition[:operator] == 'is'
  154. "1 = #{check}"
  155. else
  156. "0 = #{check}" # is not
  157. end
  158. elsif attribute_table == 'article' && attribute_name == 'action'
  159. check = options[:article_id] ? 1 : 0
  160. query << if block_condition[:operator] == 'is'
  161. "1 = #{check}"
  162. else
  163. "0 = #{check}" # is not
  164. end
  165. # because of no grouping support we select not_set by sub select for mentions
  166. elsif attribute_table == 'ticket' && attribute_name == 'mention_user_ids'
  167. if block_condition[:pre_condition] == 'not_set'
  168. query << if block_condition[:operator] == 'is'
  169. "(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
  170. else
  171. "1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
  172. end
  173. else
  174. query << if block_condition[:operator] == 'is'
  175. 'mentions.user_id IN (?)'
  176. else
  177. 'mentions.user_id NOT IN (?)'
  178. end
  179. if block_condition[:pre_condition] == 'current_user.id'
  180. bind_params.push current_user_id
  181. else
  182. bind_params.push block_condition[:value]
  183. end
  184. end
  185. elsif block_condition[:operator] == 'starts with'
  186. query << "#{attribute} #{like} (?)"
  187. bind_params.push "#{block_condition[:value]}%"
  188. elsif block_condition[:operator] == 'ends with'
  189. query << "#{attribute} #{like} (?)"
  190. bind_params.push "%#{block_condition[:value]}"
  191. elsif block_condition[:operator] == 'is'
  192. if block_condition[:pre_condition] == 'not_set'
  193. if attribute_name.match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  194. query << "(#{attribute} IS NULL OR #{attribute} IN (?))"
  195. bind_params.push 1
  196. else
  197. query << "#{attribute} IS NULL"
  198. end
  199. elsif block_condition[:pre_condition] == 'current_user.id'
  200. raise "Use current_user.id in block_condition, but no current_user is set #{block_condition.inspect}" if !current_user_id
  201. query << "#{attribute} IN (?)"
  202. if attribute_name == 'out_of_office_replacement_id'
  203. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  204. else
  205. bind_params.push current_user_id
  206. end
  207. elsif block_condition[:pre_condition] == 'current_user.organization_id'
  208. raise "Use current_user.id in block_condition, but no current_user is set #{block_condition.inspect}" if !current_user_id
  209. query << "#{attribute} IN (?)"
  210. user = User.find_by(id: current_user_id)
  211. bind_params.push user.all_organization_ids
  212. else
  213. # rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
  214. if block_condition[:value].nil?
  215. query << "#{attribute} IS NULL"
  216. else
  217. if attribute_name == 'out_of_office_replacement_id'
  218. query << "#{attribute} IN (?)"
  219. bind_params.push User.find(block_condition[:value]).out_of_office_agent_of.pluck(:id)
  220. else
  221. if block_condition[:value].class != Array
  222. block_condition[:value] = [block_condition[:value]]
  223. end
  224. query << if block_condition[:value].include?('')
  225. "(#{attribute} IN (?) OR #{attribute} IS NULL)"
  226. else
  227. "#{attribute} IN (?)"
  228. end
  229. bind_params.push block_condition[:value]
  230. end
  231. end
  232. # rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
  233. end
  234. elsif block_condition[:operator] == 'is not'
  235. if block_condition[:pre_condition] == 'not_set'
  236. if attribute_name.match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  237. query << "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  238. bind_params.push 1
  239. else
  240. query << "#{attribute} IS NOT NULL"
  241. end
  242. elsif block_condition[:pre_condition] == 'current_user.id'
  243. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  244. if attribute_name == 'out_of_office_replacement_id'
  245. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  246. else
  247. bind_params.push current_user_id
  248. end
  249. elsif block_condition[:pre_condition] == 'current_user.organization_id'
  250. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  251. user = User.find_by(id: current_user_id)
  252. bind_params.push user.organization_id
  253. else
  254. # rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
  255. if block_condition[:value].nil?
  256. query << "#{attribute} IS NOT NULL"
  257. else
  258. if attribute_name == 'out_of_office_replacement_id'
  259. bind_params.push User.find(block_condition[:value]).out_of_office_agent_of.pluck(:id)
  260. query << "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  261. else
  262. if block_condition[:value].class != Array
  263. block_condition[:value] = [block_condition[:value]]
  264. end
  265. query << if block_condition[:value].include?('')
  266. "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  267. else
  268. "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  269. end
  270. bind_params.push block_condition[:value]
  271. end
  272. end
  273. # rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
  274. end
  275. elsif block_condition[:operator] == 'contains'
  276. query << "#{attribute} #{like} (?)"
  277. bind_params.push "%#{block_condition[:value]}%"
  278. elsif block_condition[:operator] == 'contains not'
  279. query << "#{attribute} NOT #{like} (?)"
  280. bind_params.push "%#{block_condition[:value]}%"
  281. elsif block_condition[:operator] == 'matches regex'
  282. query << sql_helper.regex_match(attribute, negated: false)
  283. bind_params.push block_condition[:value]
  284. elsif block_condition[:operator] == 'does not match regex'
  285. query << sql_helper.regex_match(attribute, negated: true)
  286. bind_params.push block_condition[:value]
  287. elsif block_condition[:operator] == 'contains all'
  288. if attribute_table == 'ticket' && attribute_name == 'tags'
  289. query << "? = (
  290. SELECT
  291. COUNT(*)
  292. FROM
  293. tag_objects,
  294. tag_items,
  295. tags
  296. WHERE
  297. tickets.id = tags.o_id AND
  298. tag_objects.id = tags.tag_object_id AND
  299. tag_objects.name = 'Ticket' AND
  300. tag_items.id = tags.tag_item_id AND
  301. tag_items.name IN (?)
  302. )"
  303. bind_params.push block_condition[:value].count
  304. bind_params.push block_condition[:value]
  305. elsif sql_helper.containable?(attribute_name)
  306. query << sql_helper.array_contains_all(attribute_name, block_condition[:value])
  307. end
  308. elsif block_condition[:operator] == 'contains one'
  309. if attribute_name == 'tags' && attribute_table == 'ticket'
  310. 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"]
  311. query << 'tag_items.name IN (?)'
  312. bind_params.push block_condition[:value]
  313. elsif sql_helper.containable?(attribute_name)
  314. query << sql_helper.array_contains_one(attribute_name, block_condition[:value])
  315. end
  316. elsif block_condition[:operator] == 'contains all not'
  317. if attribute_name == 'tags' && attribute_table == 'ticket'
  318. query << "0 = (
  319. SELECT
  320. COUNT(*)
  321. FROM
  322. tag_objects,
  323. tag_items,
  324. tags
  325. WHERE
  326. tickets.id = tags.o_id AND
  327. tag_objects.id = tags.tag_object_id AND
  328. tag_objects.name = 'Ticket' AND
  329. tag_items.id = tags.tag_item_id AND
  330. tag_items.name IN (?)
  331. )"
  332. bind_params.push block_condition[:value]
  333. elsif sql_helper.containable?(attribute_name)
  334. query << sql_helper.array_contains_all(attribute_name, block_condition[:value], negated: true)
  335. end
  336. elsif block_condition[:operator] == 'contains one not'
  337. if attribute_name == 'tags' && attribute_table == 'ticket'
  338. query << "(
  339. SELECT
  340. COUNT(*)
  341. FROM
  342. tag_objects,
  343. tag_items,
  344. tags
  345. WHERE
  346. tickets.id = tags.o_id AND
  347. tag_objects.id = tags.tag_object_id AND
  348. tag_objects.name = 'Ticket' AND
  349. tag_items.id = tags.tag_item_id AND
  350. tag_items.name IN (?)
  351. ) BETWEEN 0 AND 0"
  352. bind_params.push block_condition[:value]
  353. elsif sql_helper.containable?(attribute_name)
  354. query << sql_helper.array_contains_one(attribute_name, block_condition[:value], negated: true)
  355. end
  356. elsif block_condition[:operator] == 'today'
  357. Time.use_zone(Setting.get('timezone_default_sanitized').presence) do
  358. day_start = Time.zone.now.beginning_of_day.utc
  359. day_end = Time.zone.now.end_of_day.utc
  360. query << "#{attribute} BETWEEN ? AND ?"
  361. bind_params.push day_start
  362. bind_params.push day_end
  363. end
  364. elsif block_condition[:operator] == 'before (absolute)'
  365. query << "#{attribute} <= ?"
  366. bind_params.push block_condition[:value]
  367. elsif block_condition[:operator] == 'after (absolute)'
  368. query << "#{attribute} >= ?"
  369. bind_params.push block_condition[:value]
  370. elsif block_condition[:operator] == 'within last (relative)'
  371. query << "#{attribute} BETWEEN ? AND ?"
  372. time = range(block_condition).ago
  373. bind_params.push time
  374. bind_params.push Time.zone.now
  375. elsif block_condition[:operator] == 'within next (relative)'
  376. query << "#{attribute} BETWEEN ? AND ?"
  377. time = range(block_condition).from_now
  378. bind_params.push Time.zone.now
  379. bind_params.push time
  380. elsif block_condition[:operator] == 'before (relative)'
  381. query << "#{attribute} <= ?"
  382. time = range(block_condition).ago
  383. bind_params.push time
  384. elsif block_condition[:operator] == 'after (relative)'
  385. query << "#{attribute} >= ?"
  386. time = range(block_condition).from_now
  387. bind_params.push time
  388. elsif block_condition[:operator] == 'till (relative)'
  389. query << "#{attribute} <= ?"
  390. time = range(block_condition).from_now
  391. bind_params.push time
  392. elsif block_condition[:operator] == 'from (relative)'
  393. query << "#{attribute} >= ?"
  394. time = range(block_condition).ago
  395. bind_params.push time
  396. else
  397. raise "Invalid operator '#{block_condition[:operator]}' for '#{block_condition[:value].inspect}'"
  398. end
  399. [query, bind_params, tables]
  400. end
  401. def range(selector)
  402. selector[:value].to_i.send(selector[:range].pluralize)
  403. rescue
  404. raise 'unknown selector'
  405. end
  406. def validate_operator!(condition)
  407. if condition[:operator].blank?
  408. raise "Invalid condition, operator missing #{condition.inspect}"
  409. end
  410. return true if self.class.valid_operator? condition[:operator]
  411. raise "Invalid condition, operator '#{condition[:operator]}' is invalid #{condition.inspect}"
  412. end
  413. def time_based_trigger?(condition, warning:)
  414. case [condition[:name], options[:ticket_action]]
  415. in 'ticket.pending_time', 'reminder_reached'
  416. true
  417. in 'ticket.escalation_at', 'escalation'
  418. !warning
  419. in 'ticket.escalation_at', 'escalation_warning'
  420. warning
  421. else
  422. false
  423. end
  424. end
  425. # validate pre_condition values
  426. def validate_pre_condition_values!(condition)
  427. return if ['has changed', 'has reached', 'has reached warning'].include? condition[:operator]
  428. return if condition[:pre_condition].blank?
  429. return if %w[not_set current_user. specific].any? { |elem| condition[:pre_condition].start_with? elem }
  430. raise InvalidCondition, "Invalid condition pre_condition not set #{condition}!"
  431. end
  432. # validate value / allow blank but only if pre_condition exists and is not specific
  433. def validate_pre_condition_blank!(condition)
  434. return if ['has changed', 'has reached', 'has reached warning'].include? condition[:operator]
  435. if (condition[:operator] != 'today' && !condition.key?(:value)) ||
  436. (condition[:value].instance_of?(Array) && condition[:value].respond_to?(:blank?) && condition[:value].blank?) ||
  437. (condition[:operator].start_with?('contains') && condition[:value].respond_to?(:blank?) && condition[:value].blank?)
  438. raise InvalidCondition, "Invalid condition pre_condition nil #{condition}!" if condition[:pre_condition].nil?
  439. raise InvalidCondition, "Invalid condition pre_condition blank #{condition}!" if condition[:pre_condition].respond_to?(:blank?) && condition[:pre_condition].blank?
  440. raise InvalidCondition, "Invalid condition pre_condition specific #{condition}!" if condition[:pre_condition] == 'specific'
  441. end
  442. end
  443. def update_action_requires_changed_attributes?(condition, check)
  444. condition[:value] == 'update' && check && options[:changes_required] && changed_attributes.blank?
  445. end
  446. def self.valid_operator?(operator)
  447. VALID_OPERATORS.any? { |elem| operator.match? elem }
  448. end
  449. def valid?
  450. ticket_count, _tickets = Ticket.selectors(selector, **options.merge(limit: 1, execution_time: true, ticket_id: 1, access: 'ignore'))
  451. !ticket_count.nil?
  452. rescue
  453. false
  454. end
  455. end