liquid_interpolatable.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. # :markup: markdown
  2. module LiquidInterpolatable
  3. extend ActiveSupport::Concern
  4. included do
  5. validate :validate_interpolation
  6. end
  7. def valid?(context = nil)
  8. super
  9. rescue Liquid::Error
  10. errors.empty?
  11. end
  12. def validate_interpolation
  13. interpolated
  14. rescue Liquid::ZeroDivisionError => e
  15. # Ignore error (likely due to possibly missing variables on "divided_by")
  16. rescue Liquid::Error => e
  17. errors.add(:options, "has an error with Liquid templating: #{e.message}")
  18. rescue
  19. # Calling `interpolated` without an incoming may naturally fail
  20. # with various errors when an agent expects one.
  21. end
  22. # Return the current interpolation context. Use this in your Agent
  23. # class to manipulate interpolation context for user.
  24. #
  25. # For example, to provide local variables:
  26. #
  27. # # Create a new scope to define variables in:
  28. # interpolation_context.stack {
  29. # interpolation_context['_something_'] = 42
  30. # # And user can say "{{_something_}}" in their options.
  31. # value = interpolated['some_key']
  32. # }
  33. #
  34. def interpolation_context
  35. @interpolation_context ||= Context.new(self)
  36. end
  37. # Take the given object as "self" in the current interpolation
  38. # context while running a given block.
  39. #
  40. # The most typical use case for this is to evaluate options for each
  41. # received event like this:
  42. #
  43. # def receive(incoming_events)
  44. # incoming_events.each do |event|
  45. # interpolate_with(event) do
  46. # # Handle each event based on "interpolated" options.
  47. # end
  48. # end
  49. # end
  50. def interpolate_with(self_object)
  51. case self_object
  52. when nil
  53. yield
  54. else
  55. context = interpolation_context
  56. begin
  57. context.environments.unshift(self_object.to_liquid)
  58. yield
  59. ensure
  60. context.environments.shift
  61. end
  62. end
  63. end
  64. def interpolate_with_each(array)
  65. array.each do |object|
  66. interpolate_with(object) do
  67. self.current_event = object
  68. yield object
  69. end
  70. end
  71. end
  72. def interpolate_options(options, self_object = nil)
  73. interpolate_with(self_object) do
  74. case options
  75. when String
  76. interpolate_string(options)
  77. when ActiveSupport::HashWithIndifferentAccess, Hash
  78. options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
  79. memo[key] = interpolate_options(value)
  80. }
  81. when Array
  82. options.map { |value| interpolate_options(value) }
  83. else
  84. options
  85. end
  86. end
  87. end
  88. def interpolated(self_object = nil)
  89. interpolate_with(self_object) do
  90. (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
  91. interpolate_options(options)
  92. end
  93. end
  94. def interpolate_string(string, self_object = nil)
  95. interpolate_with(self_object) do
  96. catch :as_object do
  97. Liquid::Template.parse(string).render!(interpolation_context)
  98. end
  99. end
  100. end
  101. class Context < Liquid::Context
  102. def initialize(agent)
  103. super({}, {}, { agent: agent }, true)
  104. end
  105. def hash
  106. [@environments, @scopes, @registers].hash
  107. end
  108. def eql?(other)
  109. other.environments == @environments &&
  110. other.scopes == @scopes &&
  111. other.registers == @registers
  112. end
  113. end
  114. require 'uri'
  115. module Filters
  116. # Percent encoding for URI conforming to RFC 3986.
  117. # Ref: http://tools.ietf.org/html/rfc3986#page-12
  118. def uri_escape(string)
  119. CGI.escape(string) rescue string
  120. end
  121. # Parse an input into a URI object, optionally resolving it
  122. # against a base URI if given.
  123. #
  124. # A URI object will have the following properties: scheme,
  125. # userinfo, host, port, registry, path, opaque, query, and
  126. # fragment.
  127. def to_uri(uri, base_uri = nil)
  128. case base_uri
  129. when nil, ''
  130. Utils.normalize_uri(uri.to_s)
  131. else
  132. Utils.normalize_uri(base_uri) + Utils.normalize_uri(uri.to_s)
  133. end
  134. rescue URI::Error
  135. nil
  136. end
  137. # Get the destination URL of a given URL by recursively following
  138. # redirects, up to 5 times in a row. If a given string is not a
  139. # valid absolute HTTP URL or in case of too many redirects, the
  140. # original string is returned. If any network/protocol error
  141. # occurs while following redirects, the last URL followed is
  142. # returned.
  143. def uri_expand(url, limit = 5)
  144. case url
  145. when URI
  146. uri = url
  147. else
  148. url = url.to_s
  149. begin
  150. uri = Utils.normalize_uri(url)
  151. rescue URI::Error
  152. return url
  153. end
  154. end
  155. http = Faraday.new do |builder|
  156. builder.adapter :net_http
  157. # builder.use FaradayMiddleware::FollowRedirects, limit: limit
  158. # ...does not handle non-HTTP URLs.
  159. end
  160. limit.times do
  161. begin
  162. case uri
  163. when URI::HTTP
  164. return uri.to_s unless uri.host
  165. response = http.head(uri)
  166. case response.status
  167. when 301, 302, 303, 307
  168. if location = response['location']
  169. uri += Utils.normalize_uri(location)
  170. next
  171. end
  172. end
  173. end
  174. rescue URI::Error, Faraday::Error, SystemCallError => e
  175. logger.error "#{e.class} in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]: #{e.message}:\n#{e.backtrace.join("\n")}"
  176. end
  177. return uri.to_s
  178. end
  179. logger.error "Too many rediretions in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]"
  180. url
  181. end
  182. # Rebase URIs contained in attributes in a given HTML fragment
  183. def rebase_hrefs(input, base_uri)
  184. Utils.rebase_hrefs(input, base_uri) rescue input
  185. end
  186. # Unescape (basic) HTML entities in a string
  187. #
  188. # This currently decodes the following entities only: "&apos;",
  189. # "&quot;", "&lt;", "&gt;", "&amp;", "&#dd;" and "&#xhh;".
  190. def unescape(input)
  191. CGI.unescapeHTML(input) rescue input
  192. end
  193. # Escape a string for use in XPath expression
  194. def to_xpath(string)
  195. subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
  196. case x
  197. when /"/
  198. %Q{'#{x}'}
  199. else
  200. %Q{"#{x}"}
  201. end
  202. }
  203. if subs.size == 1
  204. subs.first
  205. else
  206. 'concat(' << subs.join(', ') << ')'
  207. end
  208. end
  209. def regex_replace(input, regex, replacement = nil)
  210. input.to_s.gsub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  211. end
  212. def regex_replace_first(input, regex, replacement = nil)
  213. input.to_s.sub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  214. end
  215. # Serializes data as JSON
  216. def json(input)
  217. JSON.dump(input)
  218. end
  219. def md5(input)
  220. Digest::MD5.hexdigest(input.to_s)
  221. end
  222. def sha1(input)
  223. Digest::SHA1.hexdigest(input.to_s)
  224. end
  225. def sha256(input)
  226. Digest::SHA256.hexdigest(input.to_s)
  227. end
  228. def hmac_sha1(input, key)
  229. OpenSSL::HMAC.hexdigest('sha1', key.to_s, input.to_s)
  230. end
  231. def hmac_sha256(input, key)
  232. OpenSSL::HMAC.hexdigest('sha256', key.to_s, input.to_s)
  233. end
  234. # Returns a Ruby object
  235. #
  236. # It can be used as a JSONPath replacement for Agents that only support Liquid:
  237. #
  238. # Event: {"something": {"nested": {"data": 1}}}
  239. # Liquid: {{something.nested | as_object}}
  240. # Returns: {"data": 1}
  241. #
  242. # Splitting up a string with Liquid filters and return the Array:
  243. #
  244. # Event: {"data": "A,B,C"}}
  245. # Liquid: {{data | split: ',' | as_object}}
  246. # Returns: ['A', 'B', 'C']
  247. #
  248. # as_object ALWAYS has be the last filter in a Liquid expression!
  249. def as_object(object)
  250. throw :as_object, object.as_json
  251. end
  252. # Group an array of items by a property
  253. #
  254. # Example usage:
  255. #
  256. # {% assign posts_by_author = site.posts | group_by: "author" %}
  257. # {% for author in posts_by_author %}
  258. # <dt>{{author.name}}</dt>
  259. # {% for post in author.items %}
  260. # <dd><a href="{{post.url}}">{{post.title}}</a></dd>
  261. # {% endfor %}
  262. # {% endfor %}
  263. def group_by(input, property)
  264. if input.respond_to?(:group_by)
  265. input.group_by { |item| item[property] }.map do |value, items|
  266. { 'name' => value, 'items' => items }
  267. end
  268. else
  269. input
  270. end
  271. end
  272. private
  273. def logger
  274. @@logger ||=
  275. if defined?(Rails)
  276. Rails.logger
  277. else
  278. require 'logger'
  279. Logger.new(STDERR)
  280. end
  281. end
  282. BACKSLASH = "\\".freeze
  283. UNESCAPE = {
  284. "a" => "\a",
  285. "b" => "\b",
  286. "e" => "\e",
  287. "f" => "\f",
  288. "n" => "\n",
  289. "r" => "\r",
  290. "s" => " ",
  291. "t" => "\t",
  292. "v" => "\v",
  293. }
  294. # Unescape a replacement text for use in the second argument of
  295. # gsub/sub. The following escape sequences are recognized:
  296. #
  297. # - "\\" (backslash itself)
  298. # - "\a" (alert)
  299. # - "\b" (backspace)
  300. # - "\e" (escape)
  301. # - "\f" (form feed)
  302. # - "\n" (new line)
  303. # - "\r" (carriage return)
  304. # - "\s" (space)
  305. # - "\t" (horizontal tab)
  306. # - "\u{XXXX}" (unicode codepoint)
  307. # - "\v" (vertical tab)
  308. # - "\xXX" (hexadecimal character)
  309. # - "\1".."\9" (numbered capture groups)
  310. # - "\+" (last capture group)
  311. # - "\k<name>" (named capture group)
  312. # - "\&" or "\0" (complete matched text)
  313. # - "\`" (string before match)
  314. # - "\'" (string after match)
  315. #
  316. # Octal escape sequences are deliberately unsupported to avoid
  317. # conflict with numbered capture groups. Rather obscure Emacs
  318. # style character codes ("\C-x", "\M-\C-x" etc.) are also omitted
  319. # from this implementation.
  320. def unescape_replacement(s)
  321. s.gsub(/\\(?:([\d+&`'\\]|k<\w+>)|u\{([[:xdigit:]]+)\}|x([[:xdigit:]]{2})|(.))/) {
  322. if c = $1
  323. BACKSLASH + c
  324. elsif c = ($2 && [$2.to_i(16)].pack('U')) ||
  325. ($3 && [$3.to_i(16)].pack('C'))
  326. if c == BACKSLASH
  327. BACKSLASH + c
  328. else
  329. c
  330. end
  331. else
  332. UNESCAPE[$4] || $4
  333. end
  334. }
  335. end
  336. end
  337. Liquid::Template.register_filter(LiquidInterpolatable::Filters)
  338. module Tags
  339. class Credential < Liquid::Tag
  340. def initialize(tag_name, name, tokens)
  341. super
  342. @credential_name = name.strip
  343. end
  344. def render(context)
  345. context.registers[:agent].credential(@credential_name)
  346. end
  347. end
  348. class LineBreak < Liquid::Tag
  349. def render(context)
  350. "\n"
  351. end
  352. end
  353. end
  354. Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
  355. Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
  356. module Blocks
  357. # Replace every occurrence of a given regex pattern in the first
  358. # "in" block with the result of the "with" block in which the
  359. # variable `match` is set for each iteration, which can be used as
  360. # follows:
  361. #
  362. # - `match[0]` or just `match`: the whole matching string
  363. # - `match[1]`..`match[n]`: strings matching the numbered capture groups
  364. # - `match.size`: total number of the elements above (n+1)
  365. # - `match.names`: array of names of named capture groups
  366. # - `match[name]`..: strings matching the named capture groups
  367. # - `match.pre_match`: string preceding the match
  368. # - `match.post_match`: string following the match
  369. # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
  370. #
  371. # If named captures (`(?<name>...)`) are used in the pattern, they
  372. # are also made accessible as variables. Note that if numbered
  373. # captures are used mixed with named captures, you could get
  374. # unexpected results.
  375. #
  376. # Example usage:
  377. #
  378. # {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
  379. # {% assign fullname = "Doe, John A." %}
  380. # {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
  381. #
  382. # Use Me Like This.
  383. #
  384. # John A. Doe
  385. #
  386. class RegexReplace < Liquid::Block
  387. Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
  388. def initialize(tag_name, markup, tokens)
  389. super
  390. case markup
  391. when Syntax
  392. @regexp = $1
  393. else
  394. raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
  395. end
  396. @in_block = Liquid::BlockBody.new
  397. @with_block = nil
  398. end
  399. def parse(tokens)
  400. if more = parse_body(@in_block, tokens)
  401. @with_block = Liquid::BlockBody.new
  402. parse_body(@with_block, tokens)
  403. end
  404. end
  405. def nodelist
  406. if @with_block
  407. [@in_block, @with_block]
  408. else
  409. [@in_block]
  410. end
  411. end
  412. def unknown_tag(tag, markup, tokens)
  413. return super unless tag == 'with'.freeze
  414. @with_block = Liquid::BlockBody.new
  415. end
  416. def render(context)
  417. begin
  418. regexp = Regexp.new(context[@regexp].to_s)
  419. rescue ::SyntaxError => e
  420. raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
  421. end
  422. subject = @in_block.render(context)
  423. subject.send(first? ? :sub : :gsub, regexp) {
  424. next '' unless @with_block
  425. m = Regexp.last_match
  426. context.stack do
  427. m.names.each do |name|
  428. context[name] = m[name]
  429. end
  430. context['match'.freeze] = m
  431. @with_block.render(context)
  432. end
  433. }
  434. end
  435. def first?
  436. @tag_name.end_with?('_first'.freeze)
  437. end
  438. end
  439. end
  440. Liquid::Template.register_tag('regex_replace', LiquidInterpolatable::Blocks::RegexReplace)
  441. Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
  442. end