diff options
Diffstat (limited to 'lib/gitlab/markdown.rb')
-rw-r--r-- | lib/gitlab/markdown.rb | 336 |
1 files changed, 52 insertions, 284 deletions
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 47c456d8dc7..37b250d353e 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -1,5 +1,4 @@ require 'html/pipeline' -require 'html/pipeline/gitlab' module Gitlab # Custom parser for GitLab-flavored Markdown @@ -10,11 +9,11 @@ module Gitlab # Supported reference formats are: # * @foo for team members # * #123 for issues - # * #JIRA-123 for Jira issues + # * JIRA-123 for Jira issues # * !123 for merge requests # * $123 for snippets - # * 123456 for commits - # * 123456...7890123 for commit ranges (comparisons) + # * 1c002d for specific commit + # * 1c002d...35cfb2 for commit ranges (comparisons) # # It also parses Emoji codes to insert images. See # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. @@ -30,10 +29,6 @@ module Gitlab # >> gfm(":trollface:") # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> module Markdown - include IssuesHelper - - attr_reader :options, :html_options - # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text @@ -65,47 +60,24 @@ module Gitlab reference_only_path: true ) - @options = options - @html_options = html_options - - # TODO: add popups with additional information + pipeline = HTML::Pipeline.new(filters) - # Used markdown pipelines in GitLab: - # GitlabEmojiFilter - performs emoji replacement. - # SanitizationFilter - remove unsafe HTML tags and attributes - # - # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters - filters = [ - HTML::Pipeline::Gitlab::GitlabEmojiFilter, - HTML::Pipeline::SanitizationFilter - ] + context = { + # SanitizationFilter + whitelist: sanitization_whitelist, - whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST - whitelist[:attributes][:all].push('class', 'id') - whitelist[:elements].push('span') - - # Remove the rel attribute that the sanitize gem adds, and remove the - # href attribute if it contains inline javascript - fix_anchors = lambda do |env| - name, node = env[:node_name], env[:node] - if name == 'a' - node.remove_attribute('rel') - if node['href'] && node['href'].match('javascript:') - node.remove_attribute('href') - end - end - end - whitelist[:transformers].push(fix_anchors) + # EmojiFilter + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host, - markdown_context = { - asset_root: Gitlab.config.gitlab.url, - asset_host: Gitlab::Application.config.asset_host, - whitelist: whitelist + # ReferenceFilter + current_user: current_user, + only_path: options[:reference_only_path], + project: project, + reference_class: html_options[:class] } - markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline - - result = markdown_pipeline.call(text, markdown_context) + result = pipeline.call(text, context) save_options = 0 if options[:xhtml] @@ -114,21 +86,6 @@ module Gitlab text = result[:output].to_html(save_with: save_options) - # Extract pre blocks so they are not altered - # from http://github.github.com/github-flavored-markdown/ - text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) } - # Extract links with probably parsable hrefs - text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) } - # Extract images with probably parsable src - text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) } - - text = parse(text, project) - - # Insert pre block extractions - text.gsub!(/\{gfm-extraction-(\h{32})\}/) do - insert_piece($1) - end - if options[:parse_tasks] text = parse_tasks(text) end @@ -138,242 +95,53 @@ module Gitlab private - def extract_piece(text) - @extractions ||= {} - - md5 = Digest::MD5.hexdigest(text) - @extractions[md5] = text - "{gfm-extraction-#{md5}}" - end - - def insert_piece(id) - @extractions[id] - end - - # Private: Parses text for references + # Filters used in our pipeline # - # text - Text to parse + # SanitizationFilter should come first so that all generated reference HTML + # goes through untouched. # - # Returns parsed text - def parse(text, project = @project) - parse_references(text, project) if project - - text - end - - NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR - PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" - - REFERENCE_PATTERN = %r{ - (?<prefix>\W)? # Prefix - ( # Reference - @(?<user>#{NAME_STR}) # User name - |~(?<label>\d+) # Label ID - |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID - |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID - |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID - |\$(?<snippet>\d+) # Snippet ID - |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range - |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID - |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit - ) - (?<suffix>\W)? # Suffix - }x.freeze - - TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze - - def parse_references(text, project = @project) - # parse reference links - text.gsub!(REFERENCE_PATTERN) do |match| - type = TYPES.select{|t| !$~[t].nil?}.first - - actual_project = project - project_prefix = nil - project_path = $LAST_MATCH_INFO[:project] - if project_path - actual_project = ::Project.find_with_namespace(project_path) - actual_project = nil unless can?(current_user, :read_project, actual_project) - project_prefix = project_path - end - - parse_result($LAST_MATCH_INFO, type, - actual_project, project_prefix) || match - end - end - - # Called from #parse_references. Attempts to build a gitlab reference - # link. Returns nil if +type+ is nil, if the match string is an HTML - # entity, if the reference is invalid, or if the matched text includes an - # invalid project path. - def parse_result(match_info, type, project, project_prefix) - prefix = match_info[:prefix] - suffix = match_info[:suffix] - - return nil if html_entity?(prefix, suffix) || type.nil? - return nil if project.nil? && !project_prefix.nil? - - identifier = match_info[type] - ref_link = reference_link(type, identifier, project, project_prefix) - - if ref_link - "#{prefix}#{ref_link}#{suffix}" - else - nil - end - end - - # Return true if the +prefix+ and +suffix+ indicate that the matched string - # is an HTML entity like & - def html_entity?(prefix, suffix) - prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' + # See https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters + def filters + [ + HTML::Pipeline::SanitizationFilter, + + Gitlab::Markdown::EmojiFilter, + + Gitlab::Markdown::UserReferenceFilter, + Gitlab::Markdown::IssueReferenceFilter, + Gitlab::Markdown::ExternalIssueReferenceFilter, + Gitlab::Markdown::MergeRequestReferenceFilter, + Gitlab::Markdown::SnippetReferenceFilter, + Gitlab::Markdown::CommitRangeReferenceFilter, + Gitlab::Markdown::CommitReferenceFilter, + Gitlab::Markdown::LabelReferenceFilter, + ] end - # Private: Dispatches to a dedicated processing method based on reference - # - # reference - Object reference ("@1234", "!567", etc.) - # identifier - Object identifier (Issue ID, SHA hash, etc.) + # Customize the SanitizationFilter whitelist # - # Returns string rendered by the processing method - def reference_link(type, identifier, project = @project, prefix_text = nil) - send("reference_#{type}", identifier, project, prefix_text) - end - - def reference_user(identifier, project = @project, _ = nil) - link_options = html_options.merge( - class: "gfm gfm-project_member #{html_options[:class]}" - ) + # - Allow `class` and `id` attributes on all elements + # - Allow `span` elements + # - Remove `rel` attributes from `a` elements + # - Remove `a` nodes with `javascript:` in the `href` attribute + def sanitization_whitelist + whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST + whitelist[:attributes][:all].push('class', 'id') + whitelist[:elements].push('span') - if identifier == "all" - link_to( - "@all", - namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]), - link_options - ) - elsif namespace = Namespace.find_by(path: identifier) - url = - if namespace.is_a?(Group) - return nil unless can?(current_user, :read_group, namespace) - group_url(identifier, only_path: options[:reference_only_path]) - else - user_url(identifier, only_path: options[:reference_only_path]) + fix_anchors = lambda do |env| + name, node = env[:node_name], env[:node] + if name == 'a' + node.remove_attribute('rel') + if node['href'] && node['href'].match('javascript:') + node.remove_attribute('href') end - - link_to("@#{identifier}", url, link_options) - end - end - - def reference_label(identifier, project = @project, _ = nil) - if label = project.labels.find_by(id: identifier) - link_options = html_options.merge( - class: "gfm gfm-label #{html_options[:class]}" - ) - link_to( - render_colored_label(label), - namespace_project_issues_path(project.namespace, project, label_name: label.name), - link_options - ) - end - end - - def reference_issue(identifier, project = @project, prefix_text = nil) - if project.default_issues_tracker? - if project.issue_exists? identifier - url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) - title = title_for_issue(identifier, project) - link_options = html_options.merge( - title: "Issue: #{title}", - class: "gfm gfm-issue #{html_options[:class]}" - ) - - link_to("#{prefix_text}##{identifier}", url, link_options) - end - else - if project.external_issue_tracker.present? - reference_external_issue(identifier, project, - prefix_text) end end - end - - def reference_merge_request(identifier, project = @project, prefix_text = nil) - if merge_request = project.merge_requests.find_by(iid: identifier) - link_options = html_options.merge( - title: "Merge Request: #{merge_request.title}", - class: "gfm gfm-merge_request #{html_options[:class]}" - ) - url = namespace_project_merge_request_url(project.namespace, project, - merge_request, - only_path: options[:reference_only_path]) - link_to("#{prefix_text}!#{identifier}", url, link_options) - end - end - - def reference_snippet(identifier, project = @project, _ = nil) - if snippet = project.snippets.find_by(id: identifier) - link_options = html_options.merge( - title: "Snippet: #{snippet.title}", - class: "gfm gfm-snippet #{html_options[:class]}" - ) - link_to( - "$#{identifier}", - namespace_project_snippet_url(project.namespace, project, snippet, - only_path: options[:reference_only_path]), - link_options - ) - end - end - - def reference_commit(identifier, project = @project, prefix_text = nil) - if project.valid_repo? && commit = project.repository.commit(identifier) - link_options = html_options.merge( - title: commit.link_title, - class: "gfm gfm-commit #{html_options[:class]}" - ) - prefix_text = "#{prefix_text}@" if prefix_text - link_to( - "#{prefix_text}#{identifier}", - namespace_project_commit_url( project.namespace, project, commit, - only_path: options[:reference_only_path]), - link_options - ) - end - end - - def reference_commit_range(identifier, project = @project, prefix_text = nil) - from_id, to_id = identifier.split(/\.{2,3}/, 2) - - inclusive = identifier !~ /\.{3}/ - from_id << "^" if inclusive - - if project.valid_repo? && - from = project.repository.commit(from_id) && - to = project.repository.commit(to_id) - - link_options = html_options.merge( - title: "Commits #{from_id} through #{to_id}", - class: "gfm gfm-commit_range #{html_options[:class]}" - ) - prefix_text = "#{prefix_text}@" if prefix_text - link_to( - "#{prefix_text}#{identifier}", - namespace_project_compare_url(project.namespace, project, - from: from_id, to: to_id, - only_path: options[:reference_only_path]), - link_options - ) - end - end - - def reference_external_issue(identifier, project = @project, prefix_text = nil) - url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) - title = project.external_issue_tracker.title + whitelist[:transformers].push(fix_anchors) - link_options = html_options.merge( - title: "Issue in #{title}", - class: "gfm gfm-issue #{html_options[:class]}" - ) - link_to("#{prefix_text}##{identifier}", url, link_options) + whitelist end # Turn list items that start with "[ ]" into HTML checkbox inputs. |