utils.rb 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. require 'jsonpath'
  2. require 'cgi'
  3. require 'uri'
  4. require 'addressable/uri'
  5. module Utils
  6. def self.unindent(s)
  7. s = s.gsub(/\t/, ' ').chomp
  8. min = ((s.split("\n").find {|l| l !~ /^\s*$/ })[/^\s+/, 0] || "").length
  9. if min > 0
  10. s.gsub(/^#{" " * min}/, "")
  11. else
  12. s
  13. end
  14. end
  15. def self.pretty_print(struct, indent = true)
  16. output = JSON.pretty_generate(struct)
  17. if indent
  18. output.gsub(/\n/i, "\n ")
  19. else
  20. output
  21. end
  22. end
  23. class << self
  24. def normalize_uri(uri)
  25. URI.parse(uri)
  26. rescue URI::Error => e
  27. begin
  28. auri = Addressable::URI.parse(uri.to_s)
  29. rescue StandardError
  30. # Do not leak Addressable::URI::InvalidURIError which
  31. # callers might not expect.
  32. raise e
  33. else
  34. # Addressable::URI#normalize! modifies the query and
  35. # fragment components beyond escaping unsafe characters, so
  36. # avoid using it. Otherwise `?a[]=%2F` would be normalized
  37. # as `?a%5B%5D=/`, for example.
  38. auri.site = auri.normalized_site
  39. auri.path = auri.normalized_path
  40. auri.query &&= escape_uri_unsafe_characters(auri.query)
  41. auri.fragment &&= escape_uri_unsafe_characters(auri.fragment)
  42. URI.parse(auri.to_s)
  43. end
  44. end
  45. private
  46. def escape_uri_unsafe_characters(string)
  47. string.gsub(/(?!%[\h\H]{2})[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]+/) { |unsafe|
  48. unsafe.bytes.each_with_object(String.new) { |uc, s|
  49. s << '%%%02X' % uc
  50. }
  51. }.force_encoding(Encoding::US_ASCII)
  52. end
  53. end
  54. def self.interpolate_jsonpaths(value, data, options = {})
  55. if options[:leading_dollarsign_is_jsonpath] && value[0] == '$'
  56. Utils.values_at(data, value).first.to_s
  57. else
  58. value.gsub(/<[^>]+>/).each { |jsonpath|
  59. Utils.values_at(data, jsonpath[1..-2]).first.to_s
  60. }
  61. end
  62. end
  63. def self.recursively_interpolate_jsonpaths(struct, data, options = {})
  64. case struct
  65. when Hash
  66. struct.inject({}) {|memo, (key, value)| memo[key] = recursively_interpolate_jsonpaths(value, data, options); memo }
  67. when Array
  68. struct.map {|elem| recursively_interpolate_jsonpaths(elem, data, options) }
  69. when String
  70. interpolate_jsonpaths(struct, data, options)
  71. else
  72. struct
  73. end
  74. end
  75. def self.value_at(data, path)
  76. values_at(data, path).first
  77. end
  78. def self.values_at(data, path)
  79. if path =~ /\Aescape /
  80. path.gsub!(/\Aescape /, '')
  81. escape = true
  82. else
  83. escape = false
  84. end
  85. result = JsonPath.new(path).on(data.is_a?(String) ? data : data.to_json)
  86. if escape
  87. result.map {|r| CGI::escape r }
  88. else
  89. result
  90. end
  91. end
  92. # Output JSON that is ready for inclusion into HTML. If you simply use to_json on an object, the
  93. # presence of </script> in the valid JSON can break the page and allow XSS attacks.
  94. # Optionally, pass `:skip_safe => true` to not call html_safe on the output.
  95. def self.jsonify(thing, options = {})
  96. json = thing.to_json.gsub('</', '<\/')
  97. if !options[:skip_safe]
  98. json.html_safe
  99. else
  100. json
  101. end
  102. end
  103. def self.pretty_jsonify(thing)
  104. JSON.pretty_generate(thing).gsub('</', '<\/')
  105. end
  106. class TupleSorter
  107. class SortableTuple
  108. attr_reader :array
  109. # The <=> method will call orders[n] to determine if the nth element
  110. # should be compared in descending order.
  111. def initialize(array, orders = [])
  112. @array = array
  113. @orders = orders
  114. end
  115. def <=> other
  116. other = other.array
  117. @array.each_with_index do |e, i|
  118. o = other[i]
  119. case cmp = e <=> o || e.to_s <=> o.to_s
  120. when 0
  121. next
  122. else
  123. return @orders[i] ? -cmp : cmp
  124. end
  125. end
  126. 0
  127. end
  128. end
  129. class << self
  130. def sort!(array, orders = [])
  131. array.sort_by! do |e|
  132. SortableTuple.new(e, orders)
  133. end
  134. end
  135. end
  136. end
  137. def self.sort_tuples!(array, orders = [])
  138. TupleSorter.sort!(array, orders)
  139. end
  140. def self.parse_duration(string)
  141. return nil if string.blank?
  142. case string.strip
  143. when /\A(\d+)\.(\w+)\z/
  144. $1.to_i.send($2.to_s)
  145. when /\A(\d+)\z/
  146. $1.to_i
  147. else
  148. STDERR.puts "WARNING: Invalid duration format: '#{string.strip}'"
  149. nil
  150. end
  151. end
  152. def self.if_present(string, method)
  153. if string.present?
  154. string.send(method)
  155. else
  156. nil
  157. end
  158. end
  159. def self.rebase_hrefs(html, base_uri)
  160. base_uri = normalize_uri(base_uri)
  161. HtmlTransformer.replace_uris(html) { |url|
  162. base_uri.merge(normalize_uri(url)).to_s
  163. }
  164. end
  165. end