renderer.rb 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. class NotificationFactory::Renderer
  3. =begin
  4. examples how to use
  5. message_subject = NotificationFactory::Renderer.new(
  6. objects: {
  7. ticket: Ticket.first,
  8. },
  9. locale: 'de-de',
  10. timezone: 'America/Port-au-Prince',
  11. template: 'some template <b>#{ticket.title}</b> {config.fqdn}',
  12. escape: false,
  13. trusted: false, # Allow ERB tags in the template?
  14. ).render
  15. message_body = NotificationFactory::Renderer.new(
  16. objects: {
  17. ticket: Ticket.first,
  18. },
  19. locale: 'de-de',
  20. timezone: 'America/Port-au-Prince',
  21. template: 'some template <b>#{ticket.title}</b> #{config.fqdn}',
  22. ).render
  23. =end
  24. def initialize(objects:, template:, locale: nil, timezone: nil, escape: true, trusted: false) # rubocop:disable Metrics/ParameterLists
  25. @objects = objects
  26. @locale = locale || Locale.default
  27. @timezone = timezone || Setting.get('timezone_default')
  28. @template = NotificationFactory::Template.new(template, escape, trusted)
  29. @escape = escape
  30. end
  31. def render
  32. ERB.new(@template.to_s).result(binding)
  33. rescue Exception => e # rubocop:disable Lint/RescueException
  34. raise StandardError, e.message if e.is_a? SyntaxError
  35. raise
  36. end
  37. # d - data of object
  38. # d('user.firstname', htmlEscape)
  39. def d(key, escape = nil)
  40. # do validation, ignore some methods
  41. return "\#{#{key} / not allowed}" if !data_key_valid?(key)
  42. # aliases
  43. map = {
  44. 'article.body' => 'article.body_as_text_with_quote.text2html',
  45. 'ticket.tags' => 'ticket.tag_list',
  46. }
  47. if map[key]
  48. key = map[key]
  49. end
  50. # escape in html mode
  51. if escape
  52. no_escape = {
  53. 'article.body_as_html' => true,
  54. 'article.body_as_text_with_quote.text2html' => true,
  55. }
  56. if no_escape[key]
  57. escape = false
  58. end
  59. end
  60. value = nil
  61. object_methods = key.split('.')
  62. object_name = object_methods.shift
  63. # if no object is given, just return
  64. return '#{no such object}' if object_name.blank? # rubocop:disable Lint/InterpolationCheck
  65. object_refs = @objects[object_name] || @objects[object_name.to_sym]
  66. # if object is not in available objects, just return
  67. return "\#{#{object_name} / no such object}" if !object_refs
  68. # if content of method is a complex datatype, just return
  69. if object_methods.blank? && object_refs.class != String && object_refs.class != Float && object_refs.class != Integer
  70. return "\#{#{key} / no such method}"
  71. end
  72. previous_object_refs = ''
  73. object_methods_s = ''
  74. object_methods.each do |method_raw|
  75. method = method_raw.strip
  76. if method == 'value'
  77. temp = object_refs
  78. object_refs = display_value(previous_object_refs, method, object_methods_s, object_refs)
  79. previous_object_refs = temp
  80. end
  81. if object_methods_s != ''
  82. object_methods_s += '.'
  83. end
  84. object_methods_s += method
  85. next if method == 'value'
  86. if object_methods_s == ''
  87. value = "\#{#{object_name}.#{object_methods_s} / no such method}"
  88. break
  89. end
  90. arguments = nil
  91. if %r{\A(?<method_id>[^(]+)\((?<parameter>[^)]+)\)\z} =~ method
  92. if parameter != parameter.to_i.to_s
  93. value = "\#{#{object_name}.#{object_methods_s} / invalid parameter: #{parameter}}"
  94. break
  95. end
  96. begin
  97. arguments = Array(parameter.to_i)
  98. method = method_id
  99. rescue
  100. value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
  101. break
  102. end
  103. end
  104. # if method exists
  105. if !object_refs.respond_to?(method.to_sym)
  106. value = "\#{#{object_name}.#{object_methods_s} / no such method}"
  107. break
  108. end
  109. begin
  110. previous_object_refs = object_refs
  111. object_refs = object_refs.send(method.to_sym, *arguments)
  112. # body_as_html should trigger the cloning of all inline attachments from the parent article (issue #2399)
  113. if method.to_sym == :body_as_html && previous_object_refs.respond_to?(:should_clone_inline_attachments)
  114. previous_object_refs.should_clone_inline_attachments = true
  115. end
  116. rescue => e
  117. value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
  118. break
  119. end
  120. end
  121. placeholder = value || object_refs
  122. escaping(convert_to_timezone(placeholder), escape)
  123. end
  124. # c - config
  125. # c('fqdn', htmlEscape)
  126. def c(key, escape = nil)
  127. config = Setting.get(key)
  128. escaping(config, escape)
  129. end
  130. # t - translation
  131. # t('yes', htmlEscape)
  132. def t(key, escape = nil)
  133. translation = Translation.translate(@locale, key)
  134. escaping(translation, escape)
  135. end
  136. # h - htmlEscape
  137. # h(htmlEscape)
  138. def h(value)
  139. return value if !value
  140. CGI.escapeHTML(convert_to_timezone(value).to_s)
  141. end
  142. private
  143. def convert_to_timezone(value)
  144. return Translation.timestamp(@locale, @timezone, value) if value.instance_of?(ActiveSupport::TimeWithZone)
  145. return Translation.date(@locale, value) if value.instance_of?(Date)
  146. value
  147. end
  148. def escaping(key, escape)
  149. return escaping(key.join(', '), escape) if key.respond_to?(:join)
  150. return key if escape == false
  151. return key if escape.nil? && !@escape
  152. h key
  153. end
  154. def data_key_valid?(key)
  155. return false if key =~ %r{`|\.(|\s*)(save|destroy|delete|remove|drop|update|create|new|all|where|find|raise|dump|rollback|freeze)}i && key !~ %r{(update|create)d_(at|by)}i
  156. true
  157. end
  158. def display_value(object, method_name, previous_method_names, key)
  159. return key if method_name != 'value' ||
  160. (!key.instance_of?(String) && !key.instance_of?(Array))
  161. attributes = ObjectManager::Attribute
  162. .where(object_lookup_id: ObjectLookup.by_name(object.class.to_s))
  163. .where(name: previous_method_names.split('.').last)
  164. case attributes.first.data_type
  165. when 'select'
  166. attributes.first.data_option['options'][key] || key
  167. when 'multiselect'
  168. key.map { |k| attributes.first.data_option['options'][k] || k }
  169. else
  170. key
  171. end
  172. end
  173. end