Module: Gitlab::Utils

Extended by:
Utils
Included in:
API::Helpers, Utils
Defined in:
lib/gitlab/utils.rb,
lib/gitlab/utils/gzip.rb,
lib/gitlab/utils/markdown.rb,
lib/gitlab/utils/nokogiri.rb,
lib/gitlab/utils/override.rb,
lib/gitlab/utils/deep_size.rb,
lib/gitlab/utils/measuring.rb,
lib/gitlab/utils/mime_type.rb,
lib/gitlab/utils/merge_hash.rb,
lib/gitlab/utils/usage_data.rb,
lib/gitlab/utils/inline_hash.rb,
lib/gitlab/utils/strong_memoize.rb,
lib/gitlab/utils/lazy_attributes.rb,
lib/gitlab/utils/safe_inline_hash.rb,
lib/gitlab/utils/log_limited_array.rb,
lib/gitlab/utils/delegator_override.rb,
lib/gitlab/utils/sanitize_node_link.rb,
lib/gitlab/utils/json_size_estimator.rb,
lib/gitlab/utils/delegator_override/error.rb,
lib/gitlab/utils/delegator_override/validator.rb

Defined Under Namespace

Modules: DelegatorOverride, Gzip, InlineHash, LazyAttributes, LogLimitedArray, Markdown, MergeHash, Override, SanitizeNodeLink, StrongMemoize, UsageData Classes: DeepSize, JsonSizeEstimator, Measuring, MimeType, Nokogiri, SafeInlineHash

Instance Method Summary collapse

Instance Method Details

#add_url_parameters(url, params) ⇒ Object


214
215
216
217
218
219
# File 'lib/gitlab/utils.rb', line 214

def add_url_parameters(url, params)
  uri = parse_url(url.to_s)
  uri.query_values = uri.query_values.to_h.merge(params.to_h.stringify_keys)
  uri.query_values = nil if uri.query_values.empty?
  uri.to_s
end

#allowlisted?(absolute_path, allowlist) ⇒ Boolean

Returns:

  • (Boolean)

30
31
32
33
34
35
36
# File 'lib/gitlab/utils.rb', line 30

def allowlisted?(absolute_path, allowlist)
  path = absolute_path.downcase

  allowlist.map(&:downcase).any? do |allowed_path|
    path.start_with?(allowed_path)
  end
end

#append_path(host, path) ⇒ Object

Append path to host, making sure there's one single / in between


81
82
83
# File 'lib/gitlab/utils.rb', line 81

def append_path(host, path)
  "#{host.to_s.sub(%r{\/+$}, '')}/#{path.to_s.sub(%r{^\/+}, '')}"
end

#boolean_to_yes_no(bool) ⇒ Object


123
124
125
126
127
128
129
# File 'lib/gitlab/utils.rb', line 123

def boolean_to_yes_no(bool)
  if bool
    'Yes'
  else
    'No'
  end
end

#bytes_to_megabytes(bytes) ⇒ Object


150
151
152
# File 'lib/gitlab/utils.rb', line 150

def bytes_to_megabytes(bytes)
  bytes.to_f / Numeric::MEGABYTE
end

#check_allowed_absolute_path!(path, allowlist) ⇒ Object

Raises:

  • (StandardError)

38
39
40
41
42
43
# File 'lib/gitlab/utils.rb', line 38

def check_allowed_absolute_path!(path, allowlist)
  return unless Pathname.new(path).absolute?
  return if allowlisted?(path, allowlist)

  raise StandardError, "path #{path} is not allowed"
end

#check_allowed_absolute_path_and_path_traversal!(path, path_allowlist) ⇒ Object

Raises:

  • (StandardError)

45
46
47
48
49
50
# File 'lib/gitlab/utils.rb', line 45

def check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
  traversal_path = check_path_traversal!(path)
  raise StandardError, "path is not a string!" unless traversal_path.is_a?(String)

  check_allowed_absolute_path!(traversal_path, path_allowlist)
end

#check_path_traversal!(path) ⇒ Object

Ensure that the relative path will not traverse outside the base directory We url decode the path to avoid passing invalid paths forward in url encoded format. Also see gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580 It also checks for ALT_SEPARATOR aka '' (forward slash)


16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/gitlab/utils.rb', line 16

def check_path_traversal!(path)
  return unless path.is_a?(String)

  path = decode_path(path)
  path_regex = %r{(\A(\.{1,2})\z|\A\.\.[/\\]|[/\\]\.\.\z|[/\\]\.\.[/\\]|\n)}

  if path.match?(path_regex)
    logger.warn(message: "Potential path traversal attempt detected", path: "#{path}")
    raise PathTraversalAttackError, 'Invalid path'
  end

  path
end

#decode_path(encoded_path) ⇒ Object


52
53
54
55
56
57
58
59
# File 'lib/gitlab/utils.rb', line 52

def decode_path(encoded_path)
  decoded = CGI.unescape(encoded_path)
  if decoded != CGI.unescape(decoded)
    raise StandardError, "path #{encoded_path} is not allowed"
  end

  decoded
end

#deep_indifferent_access(data) ⇒ Object


166
167
168
169
170
171
172
173
174
# File 'lib/gitlab/utils.rb', line 166

def deep_indifferent_access(data)
  if data.is_a?(Array)
    data.map(&method(:deep_indifferent_access))
  elsif data.is_a?(Hash)
    data.with_indifferent_access
  else
    data
  end
end

#deep_symbolized_access(data) ⇒ Object


176
177
178
179
180
181
182
183
184
# File 'lib/gitlab/utils.rb', line 176

def deep_symbolized_access(data)
  if data.is_a?(Array)
    data.map(&method(:deep_symbolized_access))
  elsif data.is_a?(Hash)
    data.deep_symbolize_keys
  else
    data
  end
end

#ensure_array_from_string(string_or_array) ⇒ Object

Used in EE Accepts either an Array or a String and returns an array


160
161
162
163
164
# File 'lib/gitlab/utils.rb', line 160

def ensure_array_from_string(string_or_array)
  return string_or_array if string_or_array.is_a?(Array)

  string_or_array.split(',').map(&:strip)
end

#ensure_utf8_size(str, bytes:) ⇒ Object

Raises:

  • (ArgumentError)

65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/gitlab/utils.rb', line 65

def ensure_utf8_size(str, bytes:)
  raise ArgumentError, 'Empty string provided!' if str.empty?
  raise ArgumentError, 'Negative string size provided!' if bytes < 0

  truncated = str.each_char.each_with_object(+'') do |char, object|
    if object.bytesize + char.bytesize > bytes
      break object
    else
      object.concat(char)
    end
  end

  truncated + ('0' * (bytes - truncated.bytesize))
end

#force_utf8(str) ⇒ Object


61
62
63
# File 'lib/gitlab/utils.rb', line 61

def force_utf8(str)
  str.dup.force_encoding(Encoding::UTF_8)
end

#ms_to_round_sec(ms) ⇒ Object


154
155
156
# File 'lib/gitlab/utils.rb', line 154

def ms_to_round_sec(ms)
  (ms.to_f / 1000).round(6)
end

#multiple_key_invert(hash) ⇒ Object

Invert a hash, collecting all keys that map to a given value in an array.

Unlike `Hash#invert`, where the last encountered pair wins, and which has the type `Hash[k, v] => Hash[v, k]`, `multiple_key_invert` does not lose any information, has the type `Hash[k, v] => Hash[v, Array]`, and the original hash can always be reconstructed.

example:

multiple_key_invert({ a: 1, b: 2, c: 1 })
# => { 1 => [:a, :c], 2 => [:b] }

251
252
253
254
255
# File 'lib/gitlab/utils.rb', line 251

def multiple_key_invert(hash)
  hash.flat_map { |k, v| Array.wrap(v).zip([k].cycle) }
    .group_by(&:first)
    .transform_values { |kvs| kvs.map(&:last) }
end

#nlbr(str) ⇒ Object

Converts newlines into HTML line break elements


105
106
107
# File 'lib/gitlab/utils.rb', line 105

def nlbr(str)
  ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
end

#parse_url(uri_string) ⇒ Object

Converts a string to an Addressable::URI object. If the string is not a valid URI, it returns nil. Param uri_string should be a String object. This method returns an Addressable::URI object or nil.


209
210
211
212
# File 'lib/gitlab/utils.rb', line 209

def parse_url(uri_string)
  Addressable::URI.parse(uri_string)
rescue Addressable::URI::InvalidURIError, TypeError
end

#remove_line_breaks(str) ⇒ Object


109
110
111
# File 'lib/gitlab/utils.rb', line 109

def remove_line_breaks(str)
  str.gsub(/\r?\n/, '')
end

#removes_sensitive_data_from_url(uri_string) ⇒ Object


221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/gitlab/utils.rb', line 221

def removes_sensitive_data_from_url(uri_string)
  uri = parse_url(uri_string)

  return unless uri
  return uri_string unless uri.fragment

  stripped_params = CGI.parse(uri.fragment)
  if stripped_params['access_token']
    stripped_params['access_token'] = 'filtered'
    filtered_query = Addressable::URI.new
    filtered_query.query_values = stripped_params

    uri.fragment = filtered_query.query
  end

  uri.to_s
end

#safe_downcase!(str) ⇒ Object

A safe alternative to String#downcase!

This will make copies of frozen strings but downcase unfrozen strings in place, reducing allocations.


197
198
199
200
201
202
203
# File 'lib/gitlab/utils.rb', line 197

def safe_downcase!(str)
  if str.frozen?
    str.downcase
  else
    str.downcase! || str
  end
end

#slugify(str) ⇒ Object

A slugified version of the string, suitable for inclusion in URLs and domain names. Rules:

* Lowercased
* Anything not matching [a-z0-9-] is replaced with a -
* Maximum length is 63 bytes
* First/Last Character is not a hyphen

92
93
94
95
96
# File 'lib/gitlab/utils.rb', line 92

def slugify(str)
  str.downcase
    .gsub(/[^a-z0-9]/, '-')[0..62]
    .gsub(/(\A-+|-+\z)/, '')
end

#stable_sort_by(list) ⇒ Object

This sort is stable (see en.wikipedia.org/wiki/Sorting_algorithm#Stability) contrary to the bare Ruby sort_by method. Using just sort_by leads to instability across different platforms (e.g., x86_64-linux and x86_64-darwin18) which in turn leads to different sorting results for the equal elements across these platforms. This method uses a list item's original index position to break ties.


263
264
265
# File 'lib/gitlab/utils.rb', line 263

def stable_sort_by(list)
  list.sort_by.with_index { |x, idx| [yield(x), idx] }
end

#string_to_ip_object(str) ⇒ Object


186
187
188
189
190
191
# File 'lib/gitlab/utils.rb', line 186

def string_to_ip_object(str)
  return unless str

  IPAddr.new(str)
rescue IPAddr::InvalidAddressError
end

#to_boolean(value, default: nil) ⇒ Object


113
114
115
116
117
118
119
120
121
# File 'lib/gitlab/utils.rb', line 113

def to_boolean(value, default: nil)
  value = value.to_s if [0, 1].include?(value)

  return value if [true, false].include?(value)
  return true if value =~ /^(true|t|yes|y|1|on)$/i
  return false if value =~ /^(false|f|no|n|0|off)$/i

  default
end

#to_exclusive_sentence(array) ⇒ Object

Wraps ActiveSupport's Array#to_sentence to convert the given array to a comma-separated sentence joined with localized 'or' Strings instead of 'and'.


100
101
102
# File 'lib/gitlab/utils.rb', line 100

def to_exclusive_sentence(array)
  array.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
end

#try_megabytes_to_bytes(size) ⇒ Object


144
145
146
147
148
# File 'lib/gitlab/utils.rb', line 144

def try_megabytes_to_bytes(size)
  Integer(size).megabytes
rescue ArgumentError
  size
end

#valid_brackets?(string = '', allow_nested: true) ⇒ Boolean

Check for valid brackets (`[` and `]`) in a string using this aspects:

  • open brackets count == closed brackets count

  • (optionally) reject nested brackets via `allow_nested: false`

  • open / close brackets coherence, eg. ][[] -> invalid

Returns:

  • (Boolean)

271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/gitlab/utils.rb', line 271

def valid_brackets?(string = '', allow_nested: true)
  # remove everything except brackets
  brackets = string.remove(/[^\[\]]/)

  return true if brackets.empty?
  # balanced counts check
  return false if brackets.size.odd?

  unless allow_nested
    # nested brackets check
    return false if brackets.include?('[[') || brackets.include?(']]')
  end

  # open / close brackets coherence check
  untrimmed = brackets
  loop do
    trimmed = untrimmed.gsub('[]', '')
    return true if trimmed.empty?
    return false if trimmed == untrimmed

    untrimmed = trimmed
  end
end

#which(filename) ⇒ Object

Behaves like `which` on Linux machines: given PATH, try to resolve the given executable name to an absolute path, or return nil.

which('ruby') #=> /usr/bin/ruby

135
136
137
138
139
140
141
142
# File 'lib/gitlab/utils.rb', line 135

def which(filename)
  ENV['PATH']&.split(File::PATH_SEPARATOR)&.each do |path|
    full_path = File.join(path, filename)
    return full_path if File.executable?(full_path)
  end

  nil
end