2018-11-20 02:01:13 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-06-30 15:34:19 +00:00
|
|
|
module Gitlab
|
2017-05-31 05:50:53 +00:00
|
|
|
module QuickActions
|
2016-06-30 15:34:19 +00:00
|
|
|
# This class takes an array of commands that should be extracted from a
|
|
|
|
# given text.
|
|
|
|
#
|
|
|
|
# ```
|
2017-05-31 05:50:53 +00:00
|
|
|
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
|
2016-06-30 15:34:19 +00:00
|
|
|
# ```
|
|
|
|
class Extractor
|
2016-08-13 01:17:18 +00:00
|
|
|
attr_reader :command_definitions
|
2016-06-30 15:34:19 +00:00
|
|
|
|
2016-08-13 01:17:18 +00:00
|
|
|
def initialize(command_definitions)
|
|
|
|
@command_definitions = command_definitions
|
2020-02-25 09:09:10 +00:00
|
|
|
@commands_regex = {}
|
2016-06-30 15:34:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Extracts commands from content and return an array of commands.
|
|
|
|
# The array looks like the following:
|
|
|
|
# [
|
|
|
|
# ['command1'],
|
|
|
|
# ['command3', 'arg1 arg2'],
|
|
|
|
# ]
|
|
|
|
# The command and the arguments are stripped.
|
|
|
|
# The original command text is removed from the given `content`.
|
|
|
|
#
|
|
|
|
# Usage:
|
|
|
|
# ```
|
2017-05-31 05:50:53 +00:00
|
|
|
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
|
2016-06-30 15:34:19 +00:00
|
|
|
# msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
|
2016-08-13 01:17:18 +00:00
|
|
|
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
|
2016-06-30 15:34:19 +00:00
|
|
|
# msg #=> "hello\nworld"
|
|
|
|
# ```
|
2018-11-07 12:33:42 +00:00
|
|
|
def extract_commands(content, only: nil)
|
2016-08-13 16:58:51 +00:00
|
|
|
return [content, []] unless content
|
2016-06-30 15:34:19 +00:00
|
|
|
|
2020-01-24 15:09:00 +00:00
|
|
|
content, commands = perform_regex(content, only: only)
|
2016-08-13 01:17:18 +00:00
|
|
|
|
2020-01-24 15:09:00 +00:00
|
|
|
perform_substitutions(content, commands)
|
|
|
|
end
|
2016-06-30 15:34:19 +00:00
|
|
|
|
2020-01-24 15:09:00 +00:00
|
|
|
# Encloses quick action commands into code span markdown
|
|
|
|
# avoiding them being executed, for example, when sent via email
|
|
|
|
# to GitLab service desk.
|
|
|
|
# Example: /label ~label1 becomes `/label ~label1`
|
|
|
|
def redact_commands(content)
|
|
|
|
return "" unless content
|
|
|
|
|
|
|
|
content, _ = perform_regex(content, redact: true)
|
|
|
|
|
|
|
|
content
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def perform_regex(content, only: nil, redact: false)
|
|
|
|
commands = []
|
|
|
|
content = content.dup
|
2016-08-09 20:47:29 +00:00
|
|
|
content.delete!("\r")
|
2020-01-24 15:09:00 +00:00
|
|
|
|
2020-02-25 09:09:10 +00:00
|
|
|
names = command_names(limit_to_commands: only).map(&:to_s)
|
|
|
|
content.gsub!(commands_regex(names: names)) do
|
2020-01-24 15:09:00 +00:00
|
|
|
command, output = process_commands($~, redact)
|
|
|
|
commands << command
|
|
|
|
output
|
2016-06-30 15:34:19 +00:00
|
|
|
end
|
|
|
|
|
2020-01-24 15:09:00 +00:00
|
|
|
[content.rstrip, commands.reject(&:empty?)]
|
2016-06-30 15:34:19 +00:00
|
|
|
end
|
|
|
|
|
2020-01-24 15:09:00 +00:00
|
|
|
def process_commands(matched_text, redact)
|
|
|
|
output = matched_text[0]
|
|
|
|
command = []
|
|
|
|
|
|
|
|
if matched_text[:cmd]
|
|
|
|
command = [matched_text[:cmd].downcase, matched_text[:arg]].reject(&:blank?)
|
|
|
|
output = ''
|
|
|
|
|
|
|
|
if redact
|
|
|
|
output = "`/#{matched_text[:cmd]}#{" " + matched_text[:arg] if matched_text[:arg]}`"
|
|
|
|
output += "\n" if matched_text[0].include?("\n")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
[command, output]
|
|
|
|
end
|
2016-08-18 19:21:52 +00:00
|
|
|
|
2016-06-30 15:34:19 +00:00
|
|
|
# Builds a regular expression to match known commands.
|
|
|
|
# First match group captures the command name and
|
|
|
|
# second match group captures its arguments.
|
|
|
|
#
|
|
|
|
# It looks something like:
|
|
|
|
#
|
2016-08-18 19:21:52 +00:00
|
|
|
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
|
2020-02-25 09:09:10 +00:00
|
|
|
def commands_regex(names:)
|
|
|
|
@commands_regex[names] ||= %r{
|
2016-08-09 20:47:29 +00:00
|
|
|
(?<code>
|
|
|
|
# Code blocks:
|
|
|
|
# ```
|
2016-08-18 19:21:52 +00:00
|
|
|
# Anything, including `/cmd arg` which are ignored by this filter
|
2016-08-09 20:47:29 +00:00
|
|
|
# ```
|
|
|
|
|
|
|
|
^```
|
|
|
|
.+?
|
|
|
|
\n```$
|
|
|
|
)
|
2020-03-12 09:09:55 +00:00
|
|
|
|
|
|
|
|
(?<inline_code>
|
|
|
|
# Inline code on separate rows:
|
|
|
|
# `
|
|
|
|
# Anything, including `/cmd arg` which are ignored by this filter
|
|
|
|
# `
|
|
|
|
|
|
|
|
^.*`\n*
|
|
|
|
.+?
|
|
|
|
\n*`$
|
|
|
|
)
|
2016-08-09 20:47:29 +00:00
|
|
|
|
|
|
|
|
(?<html>
|
|
|
|
# HTML block:
|
|
|
|
# <tag>
|
2016-08-18 19:21:52 +00:00
|
|
|
# Anything, including `/cmd arg` which are ignored by this filter
|
2016-08-09 20:47:29 +00:00
|
|
|
# </tag>
|
|
|
|
|
|
|
|
^<[^>]+?>\n
|
|
|
|
.+?
|
|
|
|
\n<\/[^>]+?>$
|
|
|
|
)
|
|
|
|
|
|
|
|
|
(?<html>
|
|
|
|
# Quote block:
|
|
|
|
# >>>
|
2016-08-18 19:21:52 +00:00
|
|
|
# Anything, including `/cmd arg` which are ignored by this filter
|
2016-08-09 20:47:29 +00:00
|
|
|
# >>>
|
|
|
|
|
|
|
|
^>>>
|
|
|
|
.+?
|
|
|
|
\n>>>$
|
|
|
|
)
|
|
|
|
|
|
|
|
|
(?:
|
|
|
|
# Command not in a blockquote, blockcode, or HTML tag:
|
|
|
|
# /close
|
|
|
|
|
2016-08-13 16:58:51 +00:00
|
|
|
^\/
|
2018-06-13 10:23:57 +00:00
|
|
|
(?<cmd>#{Regexp.new(Regexp.union(names).source, Regexp::IGNORECASE)})
|
2016-08-13 16:58:51 +00:00
|
|
|
(?:
|
|
|
|
[ ]
|
2017-02-17 09:25:34 +00:00
|
|
|
(?<arg>[^\n]*)
|
2016-08-13 16:58:51 +00:00
|
|
|
)?
|
2019-10-17 12:07:33 +00:00
|
|
|
(?:\s*\n|$)
|
2016-08-09 20:47:29 +00:00
|
|
|
)
|
2018-06-13 10:23:57 +00:00
|
|
|
}mix
|
2016-06-30 15:34:19 +00:00
|
|
|
end
|
2016-08-17 23:58:44 +00:00
|
|
|
|
2017-07-25 03:11:22 +00:00
|
|
|
def perform_substitutions(content, commands)
|
|
|
|
return unless content
|
|
|
|
|
|
|
|
substitution_definitions = self.command_definitions.select do |definition|
|
|
|
|
definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
|
|
|
|
end
|
|
|
|
|
|
|
|
substitution_definitions.each do |substitution|
|
2020-02-25 09:09:10 +00:00
|
|
|
regex = commands_regex(names: substitution.all_names)
|
|
|
|
content = content.gsub(regex) do |text|
|
|
|
|
if $~[:cmd]
|
|
|
|
command = [substitution.name.to_s]
|
|
|
|
command << $~[:arg] if $~[:arg].present?
|
|
|
|
commands << command
|
|
|
|
|
|
|
|
substitution.perform_substitution(self, text)
|
|
|
|
else
|
|
|
|
text
|
|
|
|
end
|
2017-07-25 03:11:22 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
[content, commands]
|
|
|
|
end
|
|
|
|
|
2018-11-07 12:33:42 +00:00
|
|
|
def command_names(limit_to_commands:)
|
2016-08-17 23:58:44 +00:00
|
|
|
command_definitions.flat_map do |command|
|
|
|
|
next if command.noop?
|
|
|
|
|
2018-11-07 12:33:42 +00:00
|
|
|
if limit_to_commands && (command.all_names & limit_to_commands).empty?
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
2016-08-17 23:58:44 +00:00
|
|
|
command.all_names
|
|
|
|
end.compact
|
|
|
|
end
|
2016-06-30 15:34:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|