debian-mirror-gitlab/lib/gitlab/quick_actions/extractor.rb

227 lines
6.1 KiB
Ruby
Raw Normal View History

2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2016-09-13 17:45:13 +05:30
module Gitlab
2017-09-10 17:25:29 +05:30
module QuickActions
2016-09-13 17:45:13 +05:30
# This class takes an array of commands that should be extracted from a
# given text.
#
# ```
2017-09-10 17:25:29 +05:30
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
2016-09-13 17:45:13 +05:30
# ```
class Extractor
2020-11-24 15:15:51 +05:30
CODE_REGEX = %r{
(?<code>
# Code blocks:
# ```
# Anything, including `/cmd arg` which are ignored by this filter
# ```
^```
.+?
\n```$
)
}mix.freeze
INLINE_CODE_REGEX = %r{
(?<inline_code>
# Inline code on separate rows:
# `
# Anything, including `/cmd arg` which are ignored by this filter
# `
2021-01-29 00:20:46 +05:30
`\n*
2020-11-24 15:15:51 +05:30
.+?
2021-01-29 00:20:46 +05:30
\n*`
2020-11-24 15:15:51 +05:30
)
}mix.freeze
HTML_BLOCK_REGEX = %r{
(?<html>
# HTML block:
# <tag>
# Anything, including `/cmd arg` which are ignored by this filter
# </tag>
^<[^>]+?>\n
.+?
\n<\/[^>]+?>$
)
}mix.freeze
QUOTE_BLOCK_REGEX = %r{
(?<html>
# Quote block:
# >>>
# Anything, including `/cmd arg` which are ignored by this filter
# >>>
^>>>
.+?
\n>>>$
)
}mix.freeze
EXCLUSION_REGEX = %r{
#{CODE_REGEX} | #{INLINE_CODE_REGEX} | #{HTML_BLOCK_REGEX} | #{QUOTE_BLOCK_REGEX}
}mix.freeze
2016-09-13 17:45:13 +05:30
attr_reader :command_definitions
def initialize(command_definitions)
@command_definitions = command_definitions
2020-04-08 14:13:33 +05:30
@commands_regex = {}
2016-09-13 17:45:13 +05:30
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-09-10 17:25:29 +05:30
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
2016-09-13 17:45:13 +05:30
# msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
2018-12-13 13:39:08 +05:30
def extract_commands(content, only: nil)
2016-09-13 17:45:13 +05:30
return [content, []] unless content
2020-11-24 15:15:51 +05:30
perform_regex(content, only: only)
2020-03-13 15:44:24 +05:30
end
2016-09-13 17:45:13 +05:30
2020-03-13 15:44:24 +05:30
# 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)
2020-11-24 15:15:51 +05:30
names = command_names(limit_to_commands: only).map(&:to_s)
sub_names = substitution_names.map(&:to_s)
commands = []
content = content.dup
2016-09-13 17:45:13 +05:30
content.delete!("\r")
2020-03-13 15:44:24 +05:30
2020-11-24 15:15:51 +05:30
content.gsub!(commands_regex(names: names, sub_names: sub_names)) do
command, output = if $~[:substitution]
process_substitutions($~)
else
process_commands($~, redact)
end
2020-03-13 15:44:24 +05:30
commands << command
output
2016-09-13 17:45:13 +05:30
end
2020-03-13 15:44:24 +05:30
[content.rstrip, commands.reject(&:empty?)]
2016-09-13 17:45:13 +05:30
end
2020-03-13 15:44:24 +05:30
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-09-13 17:45:13 +05:30
2020-11-24 15:15:51 +05:30
def process_substitutions(matched_text)
output = matched_text[0]
command = []
if matched_text[:substitution]
cmd = matched_text[:substitution].downcase
command = [cmd, matched_text[:arg]].reject(&:blank?)
substitution = substitution_definitions.find { |definition| definition.all_names.include?(cmd.to_sym) }
output = substitution.perform_substitution(self, output) if substitution
end
[command, output]
end
2016-09-13 17:45:13 +05:30
# 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:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
2020-11-24 15:15:51 +05:30
def commands_regex(names:, sub_names:)
2020-04-08 14:13:33 +05:30
@commands_regex[names] ||= %r{
2020-11-24 15:15:51 +05:30
#{EXCLUSION_REGEX}
2016-09-13 17:45:13 +05:30
|
(?:
# Command not in a blockquote, blockcode, or HTML tag:
# /close
^\/
2018-11-08 19:23:39 +05:30
(?<cmd>#{Regexp.new(Regexp.union(names).source, Regexp::IGNORECASE)})
2016-09-13 17:45:13 +05:30
(?:
[ ]
2017-08-17 22:00:37 +05:30
(?<arg>[^\n]*)
2016-09-13 17:45:13 +05:30
)?
2019-12-21 20:55:43 +05:30
(?:\s*\n|$)
2016-09-13 17:45:13 +05:30
)
2020-11-24 15:15:51 +05:30
|
(?:
# Substitution not in a blockquote, blockcode, or HTML tag:
2017-09-10 17:25:29 +05:30
2020-11-24 15:15:51 +05:30
^\/
(?<substitution>#{Regexp.new(Regexp.union(sub_names).source, Regexp::IGNORECASE)})
(?:
[ ]
(?<arg>[^\n]*)
)?
(?:\s*\n|$)
)
}mix
2017-09-10 17:25:29 +05:30
end
2018-12-13 13:39:08 +05:30
def command_names(limit_to_commands:)
2016-09-13 17:45:13 +05:30
command_definitions.flat_map do |command|
next if command.noop?
2018-12-13 13:39:08 +05:30
if limit_to_commands && (command.all_names & limit_to_commands).empty?
next
end
2016-09-13 17:45:13 +05:30
command.all_names
end.compact
end
2020-11-24 15:15:51 +05:30
def substitution_names
substitution_definitions.flat_map { |command| command.all_names }
.compact
end
def substitution_definitions
@substition_definitions ||= command_definitions.select do |command|
command.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
end
end
2016-09-13 17:45:13 +05:30
end
end
end