diff options
100 files changed, 2662 insertions, 2551 deletions
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 98c6d9d5d2e..5004e02ea0b 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -20,7 +20,7 @@ module GitlabMarkdownHelper end user = current_user if defined?(current_user) - gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user) + gfm_body = Gitlab::Markdown.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -46,23 +46,35 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) - process_markdown(text, context) - end + return "" unless text.present? + + context[:project] ||= @project - # TODO (rspeicher): Remove all usages of this helper and just call `markdown` - # with a custom pipeline depending on the content being rendered - def gfm(text, options = {}) - process_markdown(text, options, :gfm) + html = Gitlab::Markdown.render(text, context) + + context.merge!( + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + requested_path: @path, + project_wiki: @project_wiki, + ref: @ref + ) + + Gitlab::Markdown.post_process(html, context) end def asciidoc(text) - Gitlab::Asciidoc.render(text, { - commit: @commit, - project: @project, - project_wiki: @project_wiki, + Gitlab::Asciidoc.render(text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, requested_path: @path, - ref: @ref - }) + ref: @ref, + commit: @commit + ) end # Return the first line of +text+, up to +max_chars+, after parsing the line @@ -178,26 +190,4 @@ module GitlabMarkdownHelper '' end end - - def process_markdown(text, options, method = :markdown) - return "" unless text.present? - - options.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref - ) - - user = current_user if defined?(current_user) - - html = if method == :gfm - Gitlab::Markdown.gfm(text, options) - else - Gitlab::Markdown.render(text, options) - end - - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) - end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 8ae5325d16a..ac22fc38ace 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -7,7 +7,7 @@ class Commit include Referable include StaticModel - attr_mentionable :safe_message + attr_mentionable :safe_message, pipeline: :single_line participant :author, :committer, :notes attr_accessor :project diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index badeadfa418..f56fd3e02d4 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -50,7 +50,8 @@ module Issuable allow_nil: true, prefix: true - attr_mentionable :title, :description + attr_mentionable :title, pipeline: :single_line + attr_mentionable :description, cache: true participant :author, :assignee, :notes_with_associations strip_attributes :title end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 634a8d0f274..d2ea9ab7313 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -10,8 +10,9 @@ module Mentionable module ClassMethods # Indicate which attributes of the Mentionable to search for GFM references. - def attr_mentionable(*attrs) - mentionable_attrs.concat(attrs.map(&:to_s)) + def attr_mentionable(attr, options = {}) + attr = attr.to_s + mentionable_attrs << [attr, options] end # Accessor for attributes marked mentionable. @@ -37,19 +38,24 @@ module Mentionable "#{friendly_name} #{to_reference(from_project)}" end - # Construct a String that contains possible GFM references. - def mentionable_text - self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n") - end - # The GFM reference to this Mentionable, which shouldn't be included in its #references. def local_reference self end - def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true) + def all_references(current_user = self.author, text = nil, load_lazy_references: true) ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) - ext.analyze(text) + + if text + ext.analyze(text) + else + self.class.mentionable_attrs.each do |attr, options| + text = send(attr) + options[:cache_key] = [self, attr] if options.delete(:cache) + ext.analyze(text, options) + end + end + ext end @@ -58,9 +64,7 @@ module Mentionable end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true) - return [] if text.blank? - + def referenced_mentionables(current_user = self.author, text = nil, load_lazy_references: true) refs = all_references(current_user, text, load_lazy_references: load_lazy_references) refs = (refs.issues + refs.merge_requests + refs.commits) @@ -70,8 +74,8 @@ module Mentionable refs.reject { |ref| ref == local_reference } end - # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. - def create_cross_references!(author = self.author, without = [], text = self.mentionable_text) + # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. + def create_cross_references!(author = self.author, without = [], text = nil) refs = referenced_mentionables(author, text) # We're using this method instead of Array diffing because that requires @@ -111,7 +115,7 @@ module Mentionable def detect_mentionable_changes source = (changes.present? ? changes : previous_changes).dup - mentionable = self.class.mentionable_attrs + mentionable = self.class.mentionable_attrs.map { |attr, options| attr } # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } diff --git a/app/models/note.rb b/app/models/note.rb index 8d433c57ceb..8f7ff75d0d2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -28,7 +28,7 @@ class Note < ActiveRecord::Base default_value_for :system, false - attr_mentionable :note + attr_mentionable :note, cache: true, pipeline: :note participant :author belongs_to :project diff --git a/app/models/project.rb b/app/models/project.rb index af034a6692b..9c28f782cdf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -661,6 +661,7 @@ class Project < ActiveRecord::Base gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) reset_events_cache + @repository = nil rescue # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 3536bbeaf4b..44b7efe5232 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -12,7 +12,7 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@milestone.title) + = markdown escape_once(@milestone.title), pipeline: :single_line - if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index ad63841ccf3..4ba8b84fd92 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '' · - = gfm event_commit_title(commit[:message]), project: project + = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 3c1d8815013..350e216fcc6 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -18,7 +18,7 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@milestone.title) + = markdown escape_once(@milestone.title), pipeline: :single_line - if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index bb37e4a7049..132cdc35c94 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -52,10 +52,10 @@ .commit-box.gray-content-block.middle-block %h3.commit-title - = gfm escape_once(@commit.title) + = markdown escape_once(@commit.title), pipeline: :single_line - if @commit.description.present? %pre.commit-description - = preserve(gfm(escape_once(@commit.description))) + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) :javascript $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 2e489a0a4d5..0d64486164e 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -32,7 +32,7 @@ - if commit.description? .commit-row-description.js-toggle-content %pre - = preserve(gfm(escape_once(commit.description))) + = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) .commit-row-info = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 268b9b815ee..7ffa7317196 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -17,7 +17,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.name commit.author_name xml.email commit.author_email end - xml.summary gfm(commit.description) + xml.summary markdown(commit.description, pipeline: :single_line) end end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b28ada909b7..d865d299135 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -24,10 +24,10 @@ .col-sm-10 = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - if @project.repository.exists? && @project.repository.branch_names.any? + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'control-label' - .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) + .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index 917d5181689..3c491c1a8b8 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,3 @@ .issue-closed-by-widget = icon('check') - This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests))} is accepted. + This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted. diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index e2de17cee6d..a78d20cc07e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,13 +38,13 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@issue.title) + = markdown escape_once(@issue.title), pipeline: :single_line %div - if @issue.description.present? .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@issue.description) + = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description - if @closed_by_merge_requests.present? diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index b4f62a75890..9bfe202589e 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,12 +1,12 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@merge_request.title) + = markdown escape_once(@merge_request.title), pipeline: :single_line %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description) + = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 0aad9bb3e88..8629129c0b1 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -24,4 +24,4 @@ %i.fa.fa-check Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do - != gfm(issues_sentence(@closes_issues)) + != markdown issues_sentence(@closes_issues), pipeline: :gfm diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c3bda794c65..7ecee440337 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -32,7 +32,7 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@milestone.title) + = markdown escape_once(@milestone.title), pipeline: :single_line %div - if @milestone.description.present? .description diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index dd0abc8c746..922535e5c4a 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -38,7 +38,7 @@ .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, {no_header_anchors: true}) + = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable?(note) = render 'projects/notes/edit_form', note: note diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index f3526ad0747..6ca919f7f80 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = gfm escape_once(truncate(commit.title, length: 40)) + = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 89c1d7122b0..f8faa076026 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -22,4 +22,4 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@snippet.title) + = markdown escape_once(@snippet.title), pipeline: :single_line diff --git a/config/environments/test.rb b/config/environments/test.rb index f96ac6f9753..d6842affa6c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,6 +7,8 @@ Rails.application.configure do # and recreated between test runs. Don't rely on the data there! config.cache_classes = false + config.cache_store = :null_store + # Configure static asset server for tests with Cache-Control for performance config.serve_static_files = true config.static_cache_control = "public, max-age=3600" diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d5208b8c93e..0fc725842ba 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -10,7 +10,9 @@ rescue Settings.gitlab['session_expire_delay'] ||= 10080 end -unless Rails.env.test? +if Rails.env.test? + Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" +else Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index bf33e5b1b1e..330d3342dd1 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,14 +1,10 @@ require 'asciidoctor' -require 'html/pipeline' module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters # the resulting HTML through HTML pipeline filters. module Asciidoc - # Provide autoload paths for filters to prevent a circular dependency error - autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' - DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', 'env-gitlab', 'source-highlighter=html-pipeline' @@ -24,13 +20,11 @@ module Gitlab # :requested_path # :ref # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter - # html_opts - a Hash of options for HTML output: - # :xhtml - output XHTML instead of HTML # - def self.render(input, context, asciidoc_opts = {}, html_opts = {}) - asciidoc_opts = asciidoc_opts.reverse_merge( + def self.render(input, context, asciidoc_opts = {}) + asciidoc_opts.reverse_merge!( safe: :secure, - backend: html_opts[:xhtml] ? :xhtml5 : :html5, + backend: :html5, attributes: [] ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) @@ -38,23 +32,10 @@ module Gitlab html = ::Asciidoctor.convert(input, asciidoc_opts) if context[:project] - result = HTML::Pipeline.new(filters).call(html, context) - - save_opts = html_opts[:xhtml] ? - Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0 - - html = result[:output].to_html(save_with: save_opts) + html = Gitlab::Markdown.render(html, context.merge(pipeline: :asciidoc)) end html.html_safe end - - private - - def self.filters - [ - Gitlab::Markdown::RelativeLinkFilter - ] - end end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 886a09f52af..f4e2cefca51 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -19,24 +19,21 @@ module Gitlab # context - Hash of context options passed to our HTML Pipeline # # Returns an HTML-safe String - def self.render(markdown, context = {}) - html = renderer.render(markdown) - html = gfm(html, context) - - html.html_safe + def self.render(text, context = {}) + cache_key = context.delete(:cache_key) + cache_key = full_cache_key(cache_key, context[:pipeline]) + + if cache_key + Rails.cache.fetch(cache_key) do + cacheless_render(text, context) + end + else + cacheless_render(text, context) + end end - # Convert a Markdown String into HTML without going through the HTML - # Pipeline. - # - # Note that because the pipeline is skipped, SanitizationFilter is as well. - # Do not output the result of this method to the user. - # - # markdown - Markdown String - # - # Returns a String - def self.render_without_gfm(markdown) - renderer.render(markdown) + def self.render_result(text, context = {}) + Pipeline[context[:pipeline]].call(text, context) end # Perform post-processing on an HTML String @@ -46,156 +43,73 @@ module Gitlab # permission to make (`RedactorFilter`). # # html - String to process - # options - Hash of options to customize output + # context - Hash of options to customize output # :pipeline - Symbol pipeline type # :project - Project # :user - User object # # Returns an HTML-safe String - def self.post_process(html, options) - context = { - project: options[:project], - current_user: options[:user] - } - doc = post_processor.to_document(html, context) + def self.post_process(html, context) + context = Pipeline[context[:pipeline]].transform_context(context) - if options[:pipeline] == :atom - doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) + pipeline = Pipeline[:post_process] + if context[:xhtml] + pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) else - doc.to_html + pipeline.to_html(html, context) end.html_safe end - # Provide autoload paths for filters to prevent a circular dependency error - autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' - autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' - autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter' - autoload :EmojiFilter, 'gitlab/markdown/emoji_filter' - autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter' - autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' - autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' - autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' - autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' - autoload :RedactorFilter, 'gitlab/markdown/redactor_filter' - autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' - autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' - autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' - autoload :SyntaxHighlightFilter, 'gitlab/markdown/syntax_highlight_filter' - autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' - autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' - autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' - - # Public: Parse the provided HTML with GitLab-Flavored Markdown - # - # html - HTML String - # options - A Hash of options used to customize output (default: {}) - # :no_header_anchors - Disable header anchors in TableOfContentsFilter - # :path - Current path String - # :pipeline - Symbol pipeline type - # :project - Current Project object - # :project_wiki - Current ProjectWiki object - # :ref - Current ref String - # - # Returns an HTML-safe String - def self.gfm(html, options = {}) - return '' unless html.present? - - @pipeline ||= HTML::Pipeline.new(filters) - - context = { - # SanitizationFilter - pipeline: options[:pipeline], - - # EmojiFilter - asset_host: Gitlab::Application.config.asset_host, - asset_root: Gitlab.config.gitlab.base_url, - - # ReferenceFilter - only_path: only_path_pipeline?(options[:pipeline]), - project: options[:project], - - # RelativeLinkFilter - project_wiki: options[:project_wiki], - ref: options[:ref], - requested_path: options[:path], - - # TableOfContentsFilter - no_header_anchors: options[:no_header_anchors] - } - - @pipeline.to_html(html, context).html_safe - end - private - # Check if a pipeline enables the `only_path` context option - # - # Returns Boolean - def self.only_path_pipeline?(pipeline) - case pipeline - when :atom, :email - false - else - true - end - end - - def self.redcarpet_options - # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @redcarpet_options ||= { - fenced_code_blocks: true, - footnotes: true, - lax_spacing: true, - no_intra_emphasis: true, - space_after_headers: true, - strikethrough: true, - superscript: true, - tables: true - }.freeze - end + def self.cacheless_render(text, context = {}) + result = render_result(text, context) - def self.renderer - @markdown ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) + output = result[:output] + if output.respond_to?(:to_html) + output.to_html + else + output.to_s end end - def self.post_processor - @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter]) + def self.full_cache_key(cache_key, pipeline_name) + return unless cache_key + ["markdown", *cache_key, pipeline_name || :full] end - # Filters used in our pipeline - # - # SanitizationFilter should come first so that all generated reference HTML - # goes through untouched. - # - # See https://github.com/jch/html-pipeline#filters for more filters. - def self.filters - [ - Gitlab::Markdown::SyntaxHighlightFilter, - Gitlab::Markdown::SanitizationFilter, - - Gitlab::Markdown::UploadLinkFilter, - Gitlab::Markdown::EmojiFilter, - Gitlab::Markdown::TableOfContentsFilter, - Gitlab::Markdown::AutolinkFilter, - Gitlab::Markdown::ExternalLinkFilter, - - Gitlab::Markdown::UserReferenceFilter, - Gitlab::Markdown::IssueReferenceFilter, - Gitlab::Markdown::ExternalIssueReferenceFilter, - Gitlab::Markdown::MergeRequestReferenceFilter, - Gitlab::Markdown::SnippetReferenceFilter, - Gitlab::Markdown::CommitRangeReferenceFilter, - Gitlab::Markdown::CommitReferenceFilter, - Gitlab::Markdown::LabelReferenceFilter, - - Gitlab::Markdown::RelativeLinkFilter, - - Gitlab::Markdown::TaskListFilter - ] - end + # Provide autoload paths for filters to prevent a circular dependency error + autoload :AutolinkFilter, 'gitlab/markdown/filter/autolink_filter' + autoload :CommitRangeReferenceFilter, 'gitlab/markdown/filter/commit_range_reference_filter' + autoload :CommitReferenceFilter, 'gitlab/markdown/filter/commit_reference_filter' + autoload :EmojiFilter, 'gitlab/markdown/filter/emoji_filter' + autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/filter/external_issue_reference_filter' + autoload :ExternalLinkFilter, 'gitlab/markdown/filter/external_link_filter' + autoload :IssueReferenceFilter, 'gitlab/markdown/filter/issue_reference_filter' + autoload :LabelReferenceFilter, 'gitlab/markdown/filter/label_reference_filter' + autoload :MarkdownFilter, 'gitlab/markdown/filter/markdown_filter' + autoload :MergeRequestReferenceFilter, 'gitlab/markdown/filter/merge_request_reference_filter' + autoload :RedactorFilter, 'gitlab/markdown/filter/redactor_filter' + autoload :ReferenceGathererFilter, 'gitlab/markdown/filter/reference_gatherer_filter' + autoload :RelativeLinkFilter, 'gitlab/markdown/filter/relative_link_filter' + autoload :SanitizationFilter, 'gitlab/markdown/filter/sanitization_filter' + autoload :SnippetReferenceFilter, 'gitlab/markdown/filter/snippet_reference_filter' + autoload :SyntaxHighlightFilter, 'gitlab/markdown/filter/syntax_highlight_filter' + autoload :TableOfContentsFilter, 'gitlab/markdown/filter/table_of_contents_filter' + autoload :TaskListFilter, 'gitlab/markdown/filter/task_list_filter' + autoload :UserReferenceFilter, 'gitlab/markdown/filter/user_reference_filter' + autoload :UploadLinkFilter, 'gitlab/markdown/filter/upload_link_filter' + + autoload :AsciidocPipeline, 'gitlab/markdown/pipeline/asciidoc_pipeline' + autoload :AtomPipeline, 'gitlab/markdown/pipeline/atom_pipeline' + autoload :DescriptionPipeline, 'gitlab/markdown/pipeline/description_pipeline' + autoload :EmailPipeline, 'gitlab/markdown/pipeline/email_pipeline' + autoload :FullPipeline, 'gitlab/markdown/pipeline/full_pipeline' + autoload :GfmPipeline, 'gitlab/markdown/pipeline/gfm_pipeline' + autoload :NotePipeline, 'gitlab/markdown/pipeline/note_pipeline' + autoload :PlainMarkdownPipeline, 'gitlab/markdown/pipeline/plain_markdown_pipeline' + autoload :PostProcessPipeline, 'gitlab/markdown/pipeline/post_process_pipeline' + autoload :ReferenceExtractionPipeline, 'gitlab/markdown/pipeline/reference_extraction_pipeline' + autoload :SingleLinePipeline, 'gitlab/markdown/pipeline/single_line_pipeline' end end diff --git a/lib/gitlab/markdown/combined_pipeline.rb b/lib/gitlab/markdown/combined_pipeline.rb new file mode 100644 index 00000000000..6b08a5e9f72 --- /dev/null +++ b/lib/gitlab/markdown/combined_pipeline.rb @@ -0,0 +1,27 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + module CombinedPipeline + def self.new(*pipelines) + Class.new(Pipeline) do + const_set :PIPELINES, pipelines + + def self.pipelines + self::PIPELINES + end + + def self.filters + pipelines.flat_map(&:filters) + end + + def self.transform_context(context) + pipelines.reduce(context) do |context, pipeline| + pipeline.transform_context(context) + end + end + end + end + end + end +end diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/filter/autolink_filter.rb index c37c3bc55bf..c37c3bc55bf 100644 --- a/lib/gitlab/markdown/autolink_filter.rb +++ b/lib/gitlab/markdown/filter/autolink_filter.rb diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/filter/commit_range_reference_filter.rb index 36b3258ef76..36b3258ef76 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/filter/commit_range_reference_filter.rb diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/filter/commit_reference_filter.rb index b4036578e60..b4036578e60 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/filter/commit_reference_filter.rb diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/filter/emoji_filter.rb index da10e4d3760..da10e4d3760 100644 --- a/lib/gitlab/markdown/emoji_filter.rb +++ b/lib/gitlab/markdown/filter/emoji_filter.rb diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/filter/external_issue_reference_filter.rb index 14bdf5521fc..14bdf5521fc 100644 --- a/lib/gitlab/markdown/external_issue_reference_filter.rb +++ b/lib/gitlab/markdown/filter/external_issue_reference_filter.rb diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/filter/external_link_filter.rb index e09dfcb83c8..e09dfcb83c8 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/filter/external_link_filter.rb diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/filter/issue_reference_filter.rb index 1ed69e2f431..1ed69e2f431 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/filter/issue_reference_filter.rb diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/filter/label_reference_filter.rb index a2026eecaeb..a2026eecaeb 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/filter/label_reference_filter.rb diff --git a/lib/gitlab/markdown/filter/markdown_filter.rb b/lib/gitlab/markdown/filter/markdown_filter.rb new file mode 100644 index 00000000000..921e2a0794e --- /dev/null +++ b/lib/gitlab/markdown/filter/markdown_filter.rb @@ -0,0 +1,39 @@ +module Gitlab + module Markdown + class MarkdownFilter < HTML::Pipeline::TextFilter + def initialize(text, context = nil, result = nil) + super text, context, result + @text = @text.gsub "\r", '' + end + + def call + html = self.class.renderer.render(@text) + html.rstrip! + html + end + + private + + def self.redcarpet_options + # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + @redcarpet_options ||= { + fenced_code_blocks: true, + footnotes: true, + lax_spacing: true, + no_intra_emphasis: true, + space_after_headers: true, + strikethrough: true, + superscript: true, + tables: true + }.freeze + end + + def self.renderer + @renderer ||= begin + renderer = Redcarpet::Render::HTML.new + Redcarpet::Markdown.new(renderer, redcarpet_options) + end + end + end + end +end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/filter/merge_request_reference_filter.rb index de71fc76a9b..de71fc76a9b 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/filter/merge_request_reference_filter.rb diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/filter/redactor_filter.rb index bea714a01e7..33ef7ce18b5 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/filter/redactor_filter.rb @@ -27,7 +27,7 @@ module Gitlab def user_can_reference?(node) if node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') - reference_filter = reference_type.constantize + reference_filter = Gitlab::Markdown.const_get(reference_type) reference_filter.user_can_reference?(current_user, node, context) else diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb index 00f983675e6..62f241b4967 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb @@ -31,7 +31,7 @@ module Gitlab return unless node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') - reference_filter = reference_type.constantize + reference_filter = Gitlab::Markdown.const_get(reference_type) return if context[:reference_filter] && reference_filter != context[:reference_filter] diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/filter/relative_link_filter.rb index 692c51fd324..81f60120fcd 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/filter/relative_link_filter.rb @@ -16,10 +16,7 @@ module Gitlab def call return doc unless linkable_files? - doc.search('a').each do |el| - klass = el.attr('class') - next if klass && klass.include?('gfm') - + doc.search('a:not(.gfm)').each do |el| process_link_attr el.attribute('href') end diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/filter/sanitization_filter.rb index ffb9dc33b64..cf153f30622 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/filter/sanitization_filter.rb @@ -11,7 +11,7 @@ module Gitlab def whitelist # Descriptions are more heavily sanitized, allowing only a few elements. # See http://git.io/vkuAN - if pipeline == :description + if context[:inline_sanitization] whitelist = LIMITED whitelist[:elements] -= %w(pre code img ol ul li) else @@ -25,10 +25,6 @@ module Gitlab private - def pipeline - context[:pipeline] || :default - end - def customized?(transformers) transformers.last.source_location[0] == __FILE__ end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/filter/snippet_reference_filter.rb index f7bd07c2a34..f7bd07c2a34 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/filter/snippet_reference_filter.rb diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/gitlab/markdown/filter/syntax_highlight_filter.rb index 8597e02f0de..8597e02f0de 100644 --- a/lib/gitlab/markdown/syntax_highlight_filter.rb +++ b/lib/gitlab/markdown/filter/syntax_highlight_filter.rb diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/gitlab/markdown/filter/table_of_contents_filter.rb index bbb3bf7fc8b..bbb3bf7fc8b 100644 --- a/lib/gitlab/markdown/table_of_contents_filter.rb +++ b/lib/gitlab/markdown/filter/table_of_contents_filter.rb diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/gitlab/markdown/filter/task_list_filter.rb index 2f133ae8500..2f133ae8500 100644 --- a/lib/gitlab/markdown/task_list_filter.rb +++ b/lib/gitlab/markdown/filter/task_list_filter.rb diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/filter/upload_link_filter.rb index fbada73ab86..fbada73ab86 100644 --- a/lib/gitlab/markdown/upload_link_filter.rb +++ b/lib/gitlab/markdown/filter/upload_link_filter.rb diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/filter/user_reference_filter.rb index 0a20d9c0347..0a20d9c0347 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/filter/user_reference_filter.rb diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb new file mode 100644 index 00000000000..d683756f95a --- /dev/null +++ b/lib/gitlab/markdown/pipeline.rb @@ -0,0 +1,34 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class Pipeline + def self.[](name) + name ||= :full + Markdown.const_get("#{name.to_s.camelize}Pipeline") + end + + def self.filters + [] + end + + def self.transform_context(context) + context + end + + def self.html_pipeline + @html_pipeline ||= HTML::Pipeline.new(filters) + end + + class << self + %i(call to_document to_html).each do |meth| + define_method(meth) do |text, context| + context = transform_context(context) + + html_pipeline.send(meth, text, context) + end + end + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb b/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb new file mode 100644 index 00000000000..6829b4acb95 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class AsciidocPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/atom_pipeline.rb b/lib/gitlab/markdown/pipeline/atom_pipeline.rb new file mode 100644 index 00000000000..e151f8f5e5a --- /dev/null +++ b/lib/gitlab/markdown/pipeline/atom_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class AtomPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + only_path: false, + xhtml: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/description_pipeline.rb b/lib/gitlab/markdown/pipeline/description_pipeline.rb new file mode 100644 index 00000000000..76f6948af8f --- /dev/null +++ b/lib/gitlab/markdown/pipeline/description_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class DescriptionPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + # SanitizationFilter + inline_sanitization: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/email_pipeline.rb b/lib/gitlab/markdown/pipeline/email_pipeline.rb new file mode 100644 index 00000000000..b88cb790270 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/email_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class EmailPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + only_path: false + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/full_pipeline.rb b/lib/gitlab/markdown/pipeline/full_pipeline.rb new file mode 100644 index 00000000000..b3b7a3c27c0 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/full_pipeline.rb @@ -0,0 +1,9 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline) + + end + end +end diff --git a/lib/gitlab/markdown/pipeline/gfm_pipeline.rb b/lib/gitlab/markdown/pipeline/gfm_pipeline.rb new file mode 100644 index 00000000000..ca90bd75d77 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/gfm_pipeline.rb @@ -0,0 +1,41 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class GfmPipeline < Pipeline + def self.filters + @filters ||= [ + Gitlab::Markdown::SyntaxHighlightFilter, + Gitlab::Markdown::SanitizationFilter, + + Gitlab::Markdown::UploadLinkFilter, + Gitlab::Markdown::EmojiFilter, + Gitlab::Markdown::TableOfContentsFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, + + Gitlab::Markdown::UserReferenceFilter, + Gitlab::Markdown::IssueReferenceFilter, + Gitlab::Markdown::ExternalIssueReferenceFilter, + Gitlab::Markdown::MergeRequestReferenceFilter, + Gitlab::Markdown::SnippetReferenceFilter, + Gitlab::Markdown::CommitRangeReferenceFilter, + Gitlab::Markdown::CommitReferenceFilter, + Gitlab::Markdown::LabelReferenceFilter, + + Gitlab::Markdown::TaskListFilter + ] + end + + def self.transform_context(context) + context.merge( + only_path: true, + + # EmojiFilter + asset_host: Gitlab::Application.config.asset_host, + asset_root: Gitlab.config.gitlab.base_url + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/note_pipeline.rb b/lib/gitlab/markdown/pipeline/note_pipeline.rb new file mode 100644 index 00000000000..a8bf5f42d8e --- /dev/null +++ b/lib/gitlab/markdown/pipeline/note_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class NotePipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + # TableOfContentsFilter + no_header_anchors: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb b/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb new file mode 100644 index 00000000000..0abb93f8a03 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class PlainMarkdownPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::MarkdownFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/post_process_pipeline.rb b/lib/gitlab/markdown/pipeline/post_process_pipeline.rb new file mode 100644 index 00000000000..60cc32f490e --- /dev/null +++ b/lib/gitlab/markdown/pipeline/post_process_pipeline.rb @@ -0,0 +1,20 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class PostProcessPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::RedactorFilter + ] + end + + def self.transform_context(context) + context.merge( + post_process: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb b/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb new file mode 100644 index 00000000000..a89ab462bac --- /dev/null +++ b/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class ReferenceExtractionPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::ReferenceGathererFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline/single_line_pipeline.rb b/lib/gitlab/markdown/pipeline/single_line_pipeline.rb new file mode 100644 index 00000000000..2f24927b879 --- /dev/null +++ b/lib/gitlab/markdown/pipeline/single_line_pipeline.rb @@ -0,0 +1,9 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class SingleLinePipeline < GfmPipeline + + end + end +end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index b6d93e05ec7..3b83b8bd8f8 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -29,6 +29,10 @@ module Gitlab end end + def self.[](name) + Markdown.const_get("#{name.to_s.camelize}ReferenceFilter") + end + def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i @@ -53,14 +57,14 @@ module Gitlab # Examples: # # data_attribute(project: 1, issue: 2) - # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" # # data_attribute(project: 3, merge_request: 4) - # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" + # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) - attributes[:reference_filter] = self.class.name + attributes[:reference_filter] = self.class.name.demodulize attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 3c3478a1271..e83167fa7d7 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -9,30 +9,23 @@ module Gitlab @project = project @current_user = current_user @load_lazy_references = load_lazy_references + + @texts = [] + @references = {} end - def analyze(text) - references.clear - @text = Gitlab::Markdown.render_without_gfm(text) + def analyze(text, options = {}) + @texts << Gitlab::Markdown.render(text, options.merge(project: project)) end %i(user label issue merge_request snippet commit commit_range).each do |type| define_method("#{type}s") do - references[type] + @references[type] ||= pipeline_result(type) end end private - def references - @references ||= Hash.new do |references, type| - type = type.to_sym - next references[type] if references.has_key?(type) - - references[type] = pipeline_result(type) - end - end - # Instantiate and call HTML::Pipeline with a single reference filter type, # returning the result # @@ -40,36 +33,24 @@ module Gitlab # # Returns the results Array for the requested filter type def pipeline_result(filter_type) - return [] if @text.blank? - - klass = "#{filter_type.to_s.camelize}ReferenceFilter" - filter = Gitlab::Markdown.const_get(klass) + filter = Gitlab::Markdown::ReferenceFilter[filter_type] context = { - project: project, - current_user: current_user, + pipeline: :reference_extraction, - # We don't actually care about the links generated - only_path: true, - ignore_blockquotes: true, + project: project, + current_user: current_user, # ReferenceGathererFilter load_lazy_references: false, reference_filter: filter } - # We need to autolink first to finds links to referables, and to prevent - # numeric anchors to be parsed as issue references. - filters = [ - Gitlab::Markdown::AutolinkFilter, - filter, - Gitlab::Markdown::ReferenceGathererFilter - ] - - pipeline = HTML::Pipeline.new(filters, context) - result = pipeline.call(@text) - - values = result[:references][filter_type].uniq + values = @texts.flat_map do |html| + text_context = context.dup + result = Gitlab::Markdown.render_result(html, text_context) + result[:references][filter_type] + end.uniq if @load_lazy_references values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 03e36fd3552..e8c8180e1c4 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -50,9 +50,9 @@ module Gitlab filtered_html = '<b>ASCII</b>' allow(Asciidoctor).to receive(:convert).and_return(html) - expect_any_instance_of(HTML::Pipeline).to receive(:call) - .with(html, context) - .and_return(output: Nokogiri::HTML.fragment(filtered_html)) + expect(Gitlab::Markdown).to receive(:render) + .with(html, context.merge(pipeline: :asciidoc)) + .and_return(filtered_html) expect( render('foo', context) ).to eql filtered_html end diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb deleted file mode 100644 index 26332ba5217..00000000000 --- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe AutolinkFilter do - include FilterSpecHelper - - let(:link) { 'http://about.gitlab.com/' } - - it 'does nothing when :autolink is false' do - exp = act = link - expect(filter(act, autolink: false).to_html).to eq exp - end - - it 'does nothing with non-link text' do - exp = act = 'This text contains no links to autolink' - expect(filter(act).to_html).to eq exp - end - - context 'Rinku schemes' do - it 'autolinks http' do - doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks https' do - link = 'https://google.com/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks ftp' do - link = 'ftp://ftp.us.debian.org/debian/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks short URLs' do - link = 'http://localhost:3000/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) - - expect(doc.at_css('a')['class']).to eq 'custom' - end - - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end - end - - context 'other schemes' do - let(:link) { 'foo://bar.baz/' } - - it 'autolinks smb' do - link = 'smb:///Volumes/shared/foo.pdf' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks irc' do - link = 'irc://irc.freenode.net/git' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'does not include trailing punctuation' do - doc = filter("See #{link}.") - expect(doc.at_css('a').text).to eq link - - doc = filter("See #{link}, ok?") - expect(doc.at_css('a').text).to eq link - - doc = filter("See #{link}...") - expect(doc.at_css('a').text).to eq link - end - - it 'does not include trailing HTML entities' do - doc = filter("See <<<#{link}>>>") - - expect(doc.at_css('a')['href']).to eq link - expect(doc.text).to eq "See <<<#{link}>>>" - end - - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) - expect(doc.at_css('a')['class']).to eq 'custom' - end - - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb deleted file mode 100644 index 9ce63f9af46..00000000000 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ /dev/null @@ -1,184 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe CommitRangeReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:commit1) { project.commit("HEAD~2") } - let(:commit2) { project.commit } - - let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) } - let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { range.to_reference } - let(:reference2) { range2.to_reference } - - it 'links to a valid two-dot reference' do - doc = reference_filter("See #{reference2}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) - end - - it 'links to a valid three-dot reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) - end - - it 'links to a valid short ID' do - reference = "#{commit1.short_id}...#{commit2.id}" - reference2 = "#{commit1.id}...#{commit2.short_id}" - - exp = commit1.short_id + '...' + commit2.short_id - - expect(reference_filter("See #{reference}").css('a').first.text).to eq exp - expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp - end - - it 'links with adjacent text' do - doc = reference_filter("See (#{reference}.)") - - exp = Regexp.escape(range.reference_link_text) - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs' do - exp = act = "See #{commit1.id.reverse}...#{commit2.id}" - - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(project.repository).to receive(:commit).with(commit2.id) - expect(reference_filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq range.reference_title - end - - it 'includes default classes' do - doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' - end - - it 'includes a data-project attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-commit-range attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-commit-range') - expect(link.attr('data-commit-range')).to eq range.to_s - end - - it 'supports an :only_path option' do - doc = reference_filter("See #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { range.to_reference(project) } - - before do - range.project = project2 - end - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - - exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(reference_filter(act).to_html).to eq exp - - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end - end - - context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:range) { CommitRange.new("#{commit1.id}...master", project) } - let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } - - before do - range.project = project2 - end - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq reference - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - - exp = Regexp.escape(range.reference_link_text(project)) - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(reference_filter(act).to_html).to eq exp - - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end - end - end -end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb deleted file mode 100644 index 462a41b4756..00000000000 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ /dev/null @@ -1,165 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe CommitReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:commit) { project.commit } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { commit.id } - - # Let's test a variety of commit SHA sizes just to be paranoid - [6, 8, 12, 18, 20, 32, 40].each do |size| - it "links to a valid reference of #{size} characters" do - doc = reference_filter("See #{reference[0...size]}") - - expect(doc.css('a').first.text).to eq commit.short_id - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project.namespace, project, reference) - end - end - - it 'always uses the short ID as the link text' do - doc = reference_filter("See #{commit.id}") - expect(doc.text).to eq "See #{commit.short_id}" - - doc = reference_filter("See #{commit.id[0...6]}") - expect(doc.text).to eq "See #{commit.short_id}" - end - - it 'links with adjacent text' do - doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs' do - invalid = invalidate_reference(reference) - exp = act = "See #{invalid}" - - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(invalid) - expect(reference_filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq commit.link_title - end - - it 'escapes the title attribute' do - allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) - - doc = reference_filter("See #{reference}") - expect(doc.text).to eq "See #{commit.short_id}" - end - - it 'includes default classes' do - doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' - end - - it 'includes a data-project attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-commit attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-commit') - expect(link.attr('data-commit')).to eq commit.id - end - - it 'supports an :only_path context' do - doc = reference_filter("See #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:commit) { project2.commit } - let(:reference) { commit.to_reference(project) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{invalidate_reference(reference)}" - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end - end - - context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:commit) { project2.commit } - let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - - expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - act = "Committed #{invalidate_reference(reference)}" - expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end - end - end -end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb index 8d4f9e403a6..f86d52b9d0a 100644 --- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -1,35 +1,33 @@ require 'spec_helper' -module Gitlab::Markdown - describe CrossProjectReference do - include described_class +describe Gitlab::Markdown::CrossProjectReference do + include described_class - describe '#project_from_ref' do - context 'when no project was referenced' do - it 'returns the project from context' do - project = double + describe '#project_from_ref' do + context 'when no project was referenced' do + it 'returns the project from context' do + project = double - allow(self).to receive(:context).and_return({ project: project }) + allow(self).to receive(:context).and_return({ project: project }) - expect(project_from_ref(nil)).to eq project - end + expect(project_from_ref(nil)).to eq project end + end - context 'when referenced project does not exist' do - it 'returns nil' do - expect(project_from_ref('invalid/reference')).to be_nil - end + context 'when referenced project does not exist' do + it 'returns nil' do + expect(project_from_ref('invalid/reference')).to be_nil end + end - context 'when referenced project exists' do - it 'returns the referenced project' do - project2 = double('referenced project') + context 'when referenced project exists' do + it 'returns the referenced project' do + project2 = double('referenced project') - expect(Project).to receive(:find_with_namespace). - with('cross/reference').and_return(project2) + expect(Project).to receive(:find_with_namespace). + with('cross/reference').and_return(project2) - expect(project_from_ref('cross/reference')).to eq project2 - end + expect(project_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb deleted file mode 100644 index 11efd9bb4cd..00000000000 --- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe EmojiFilter do - include FilterSpecHelper - - before do - ActionController::Base.asset_host = 'https://foo.com' - end - - it 'replaces supported emoji' do - doc = filter('<p>:heart:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' - end - - it 'ignores unsupported emoji' do - exp = act = '<p>:foo:</p>' - doc = filter(act) - expect(doc.to_html).to match Regexp.escape(exp) - end - - it 'correctly encodes the URL' do - doc = filter('<p>:+1:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' - end - - it 'matches at the start of a string' do - doc = filter(':+1:') - expect(doc.css('img').size).to eq 1 - end - - it 'matches at the end of a string' do - doc = filter('This gets a :-1:') - expect(doc.css('img').size).to eq 1 - end - - it 'matches with adjacent text' do - doc = filter('+1 (:+1:)') - expect(doc.css('img').size).to eq 1 - end - - it 'matches multiple emoji in a row' do - doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') - expect(doc.css('img').size).to eq 3 - end - - it 'has a title attribute' do - doc = filter(':-1:') - expect(doc.css('img').first.attr('title')).to eq ':-1:' - end - - it 'has an alt attribute' do - doc = filter(':-1:') - expect(doc.css('img').first.attr('alt')).to eq ':-1:' - end - - it 'has an align attribute' do - doc = filter(':8ball:') - expect(doc.css('img').first.attr('align')).to eq 'absmiddle' - end - - it 'has an emoji class' do - doc = filter(':cat:') - expect(doc.css('img').first.attr('class')).to eq 'emoji' - end - - it 'has height and width attributes' do - doc = filter(':dog:') - img = doc.css('img').first - - expect(img.attr('width')).to eq '20' - expect(img.attr('height')).to eq '20' - end - - it 'keeps whitespace intact' do - doc = filter('This deserves a :+1:, big time.') - - expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) - end - - it 'uses a custom asset_root context' do - root = Gitlab.config.gitlab.url + 'gitlab/root' - - doc = filter(':smile:', asset_root: root) - expect(doc.css('img').first.attr('src')).to start_with(root) - end - - it 'uses a custom asset_host context' do - ActionController::Base.asset_host = 'https://cdn.example.com' - - doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') - expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') - end - end -end diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb deleted file mode 100644 index d8c2970b6bd..00000000000 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe ExternalIssueReferenceFilter do - include FilterSpecHelper - - def helper - IssuesHelper - end - - let(:project) { create(:jira_project) } - - context 'JIRA issue references' do - let(:issue) { ExternalIssue.new('JIRA-123', project) } - let(:reference) { issue.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end - - it 'ignores valid references when using default tracker' do - expect(project).to receive(:default_issues_tracker?).and_return(true) - - exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('href')) - .to eq helper.url_for_issue(reference, project) - end - - it 'links to the external tracker' do - doc = filter("Issue #{reference}") - link = doc.css('a').first.attr('href') - - expect(link).to eq "http://jira.example/browse/#{reference}" - end - - it 'links with adjacent text' do - doc = filter("Issue (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) - end - - it 'includes a title attribute' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" - end - - it 'escapes the title attribute' do - allow(project.external_issue_tracker).to receive(:title). - and_return(%{"></a>whatever<a title="}) - - doc = filter("Issue #{reference}") - expect(doc.text).to eq "Issue #{reference}" - end - - it 'includes default classes' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' - end - - it 'supports an :only_path context' do - doc = filter("Issue #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true) - end - end - end -end diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb deleted file mode 100644 index a040b34577b..00000000000 --- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe ExternalLinkFilter do - include FilterSpecHelper - - it 'ignores elements without an href attribute' do - exp = act = %q(<a id="ignored">Ignore Me</a>) - expect(filter(act).to_html).to eq exp - end - - it 'ignores non-HTTP(S) links' do - exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>) - expect(filter(act).to_html).to eq exp - end - - it 'skips internal links' do - internal = Gitlab.config.gitlab.url - exp = act = %Q(<a href="#{internal}/sign_in">Login</a>) - expect(filter(act).to_html).to eq exp - end - - it 'adds rel="nofollow" to external links' do - act = %q(<a href="https://google.com/">Google</a>) - doc = filter(act) - - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to eq 'nofollow' - end - end -end diff --git a/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb new file mode 100644 index 00000000000..837bdb56f74 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Gitlab::Markdown::AutolinkFilter do + include FilterSpecHelper + + let(:link) { 'http://about.gitlab.com/' } + + it 'does nothing when :autolink is false' do + exp = act = link + expect(filter(act, autolink: false).to_html).to eq exp + end + + it 'does nothing with non-link text' do + exp = act = 'This text contains no links to autolink' + expect(filter(act).to_html).to eq exp + end + + context 'Rinku schemes' do + it 'autolinks http' do + doc = filter("See #{link}") + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks https' do + link = 'https://google.com/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks ftp' do + link = 'ftp://ftp.us.debian.org/debian/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks short URLs' do + link = 'http://localhost:3000/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) + + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + end + + context 'other schemes' do + let(:link) { 'foo://bar.baz/' } + + it 'autolinks smb' do + link = 'smb:///Volumes/shared/foo.pdf' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks irc' do + link = 'irc://irc.freenode.net/git' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not include trailing punctuation' do + doc = filter("See #{link}.") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}, ok?") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}...") + expect(doc.at_css('a').text).to eq link + end + + it 'does not include trailing HTML entities' do + doc = filter("See <<<#{link}>>>") + + expect(doc.at_css('a')['href']).to eq link + expect(doc.text).to eq "See <<<#{link}>>>" + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb new file mode 100644 index 00000000000..05404fea724 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe Gitlab::Markdown::CommitRangeReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:commit1) { project.commit("HEAD~2") } + let(:commit2) { project.commit } + + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { range.to_reference } + let(:reference2) { range2.to_reference } + + it 'links to a valid two-dot reference' do + doc = reference_filter("See #{reference2}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) + end + + it 'links to a valid three-dot reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) + end + + it 'links to a valid short ID' do + reference = "#{commit1.short_id}...#{commit2.id}" + reference2 = "#{commit1.id}...#{commit2.short_id}" + + exp = commit1.short_id + '...' + commit2.short_id + + expect(reference_filter("See #{reference}").css('a').first.text).to eq exp + expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + + exp = Regexp.escape(range.reference_link_text) + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + exp = act = "See #{commit1.id.reverse}...#{commit2.id}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(commit1.id.reverse) + expect(project.repository).to receive(:commit).with(commit2.id) + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq range.reference_title + end + + it 'includes default classes' do + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + end + + it 'includes a data-project attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-commit-range attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-commit-range') + expect(link.attr('data-commit-range')).to eq range.to_s + end + + it 'supports an :only_path option' do + doc = reference_filter("See #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:reference) { range.to_reference(project) } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:range) { CommitRange.new("#{commit1.id}...master", project) } + let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + exp = Regexp.escape(range.reference_link_text(project)) + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb new file mode 100644 index 00000000000..eeddf33e175 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb @@ -0,0 +1,163 @@ +require 'spec_helper' + +describe Gitlab::Markdown::CommitReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:commit) { project.commit } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { commit.id } + + # Let's test a variety of commit SHA sizes just to be paranoid + [6, 8, 12, 18, 20, 32, 40].each do |size| + it "links to a valid reference of #{size} characters" do + doc = reference_filter("See #{reference[0...size]}") + + expect(doc.css('a').first.text).to eq commit.short_id + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project.namespace, project, reference) + end + end + + it 'always uses the short ID as the link text' do + doc = reference_filter("See #{commit.id}") + expect(doc.text).to eq "See #{commit.short_id}" + + doc = reference_filter("See #{commit.id[0...6]}") + expect(doc.text).to eq "See #{commit.short_id}" + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + invalid = invalidate_reference(reference) + exp = act = "See #{invalid}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(invalid) + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq commit.link_title + end + + it 'escapes the title attribute' do + allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + expect(doc.text).to eq "See #{commit.short_id}" + end + + it 'includes default classes' do + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + end + + it 'includes a data-project attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-commit attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-commit') + expect(link.attr('data-commit')).to eq commit.id + end + + it 'supports an :only_path context' do + doc = reference_filter("See #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { commit.to_reference(project) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb new file mode 100644 index 00000000000..e0c5622e85d --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe Gitlab::Markdown::EmojiFilter do + include FilterSpecHelper + + before do + @original_asset_host = ActionController::Base.asset_host + ActionController::Base.asset_host = 'https://foo.com' + end + + after do + ActionController::Base.asset_host = @original_asset_host + end + + it 'replaces supported emoji' do + doc = filter('<p>:heart:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + end + + it 'ignores unsupported emoji' do + exp = act = '<p>:foo:</p>' + doc = filter(act) + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly encodes the URL' do + doc = filter('<p>:+1:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + end + + it 'matches at the start of a string' do + doc = filter(':+1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches at the end of a string' do + doc = filter('This gets a :-1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches with adjacent text' do + doc = filter('+1 (:+1:)') + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple emoji in a row' do + doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') + expect(doc.css('img').size).to eq 3 + end + + it 'has a title attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('title')).to eq ':-1:' + end + + it 'has an alt attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('alt')).to eq ':-1:' + end + + it 'has an align attribute' do + doc = filter(':8ball:') + expect(doc.css('img').first.attr('align')).to eq 'absmiddle' + end + + it 'has an emoji class' do + doc = filter(':cat:') + expect(doc.css('img').first.attr('class')).to eq 'emoji' + end + + it 'has height and width attributes' do + doc = filter(':dog:') + img = doc.css('img').first + + expect(img.attr('width')).to eq '20' + expect(img.attr('height')).to eq '20' + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :+1:, big time.') + + expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) + end + + it 'uses a custom asset_root context' do + root = Gitlab.config.gitlab.url + 'gitlab/root' + + doc = filter(':smile:', asset_root: root) + expect(doc.css('img').first.attr('src')).to start_with(root) + end + + it 'uses a custom asset_host context' do + ActionController::Base.asset_host = 'https://cdn.example.com' + + doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') + expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') + end +end diff --git a/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb new file mode 100644 index 00000000000..216fcb66cef --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ExternalIssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:jira_project) } + + context 'JIRA issue references' do + let(:issue) { ExternalIssue.new('JIRA-123', project) } + let(:reference) { issue.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'ignores valid references when using default tracker' do + expect(project).to receive(:default_issues_tracker?).and_return(true) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(reference, project) + end + + it 'links to the external tracker' do + doc = filter("Issue #{reference}") + link = doc.css('a').first.attr('href') + + expect(link).to eq "http://jira.example/browse/#{reference}" + end + + it 'links with adjacent text' do + doc = filter("Issue (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" + end + + it 'escapes the title attribute' do + allow(project.external_issue_tracker).to receive(:title). + and_return(%{"></a>whatever<a title="}) + + doc = filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'supports an :only_path context' do + doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true) + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb new file mode 100644 index 00000000000..20d37e0c978 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ExternalLinkFilter do + include FilterSpecHelper + + it 'ignores elements without an href attribute' do + exp = act = %q(<a id="ignored">Ignore Me</a>) + expect(filter(act).to_html).to eq exp + end + + it 'ignores non-HTTP(S) links' do + exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>) + expect(filter(act).to_html).to eq exp + end + + it 'skips internal links' do + internal = Gitlab.config.gitlab.url + exp = act = %Q(<a href="#{internal}/sign_in">Login</a>) + expect(filter(act).to_html).to eq exp + end + + it 'adds rel="nofollow" to external links' do + act = %q(<a href="https://google.com/">Google</a>) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to eq 'nofollow' + end +end diff --git a/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb new file mode 100644 index 00000000000..cb04b4a7aca --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb @@ -0,0 +1,209 @@ +require 'spec_helper' + +describe Gitlab::Markdown::IssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { issue.to_reference } + + it 'ignores valid references when using non-default tracker' do + expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs' do + invalid = invalidate_reference(reference) + exp = act = "Fixed #{invalid}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + end + + it 'escapes the title attribute' do + issue.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = reference_filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = reference_filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Issue #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-issue attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-issue') + expect(link.attr('data-issue')).to eq issue.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { issue.to_reference(project) } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:get_issue). + with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project reference in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project URL in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + "#note_123" + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb new file mode 100644 index 00000000000..1126bf2fd84 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' +require 'html/pipeline' + +describe Gitlab::Markdown::LabelReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:label) { create(:label, project: project) } + let(:reference) { label.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Label #{reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = reference_filter("Label #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Label #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] + end + + describe 'label span element' do + it 'includes default classes' do + doc = reference_filter("Label #{reference}") + expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + end + + it 'includes a style attribute' do + doc = reference_filter("Label #{reference}") + expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) + end + end + + context 'Integer-based references' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label IDs' do + exp = act = "Label #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based single-word references' do + let(:label) { create(:label, name: 'gfm', project: project) } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:label) { create(:label, name: 'gfm references', project: project) } + let(:reference) { label.to_reference(:name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'edge cases' do + it 'gracefully handles non-references matching the pattern' do + exp = act = '(format nil "~0f" 3.0) ; 3.0' + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a label in a link href' do + let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\))) + end + + it 'includes a data-project attribute' do + doc = reference_filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb new file mode 100644 index 00000000000..731dca5fe1e --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::Markdown::MergeRequestReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:merge) { create(:merge_request, source_project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { merge.to_reference } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_merge_request_url(project.namespace, project, merge) + end + + it 'links with adjacent text' do + doc = reference_filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Merge #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + end + + it 'escapes the title attribute' do + merge.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = reference_filter("Merge #{reference}") + expect(doc.text).to eq "Merge #{reference}" + end + + it 'includes default classes' do + doc = reference_filter("Merge #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Merge #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-merge-request attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-merge-request') + expect(link.attr('data-merge-request')).to eq merge.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Merge #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { merge.to_reference(project) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project, merge) + end + + it 'links with adjacent text' do + doc = reference_filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } + let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb new file mode 100644 index 00000000000..7760a1c86fa --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Gitlab::Markdown::RedactorFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + it 'ignores non-GFM links' do + html = %(See <a href="https://google.com/">Google</a>) + doc = filter(html, current_user: double) + + expect(doc.css('a').length).to eq 1 + end + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context 'with data-project' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + + link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + project.team << [user, :master] + + link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + + it 'handles invalid Project references' do + link = reference_link(project: 12345, reference_filter: 'ReferenceFilter') + + expect { filter(link) }.not_to raise_error + end + end + + context "for user references" do + + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') + + expect { filter(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') + doc = filter(link) + + expect(doc.css('a').length).to eq 1 + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb new file mode 100644 index 00000000000..caa9d5f36cc --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ReferenceGathererFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context "for issue references" do + + context 'with data-project' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to be_empty + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + project.team << [user, :master] + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to eq([issue]) + end + + it 'handles invalid Project references' do + link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter') + + expect { pipeline_result(link) }.not_to raise_error + end + end + end + + context "for user references" do + + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to be_empty + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to eq([user]) + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') + + expect { pipeline_result(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') + result = pipeline_result(link) + + expect(result[:references][:user]).to eq([user]) + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb new file mode 100644 index 00000000000..3d978505564 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb @@ -0,0 +1,147 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::RelativeLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + commit: project.commit, + project: project, + project_wiki: project_wiki, + ref: ref, + requested_path: requested_path + }) + + described_class.call(doc, contexts) + end + + def image(path) + %(<img src="#{path}" />) + end + + def link(path) + %(<a href="#{path}">#{path}</a>) + end + + let(:project) { create(:project) } + let(:project_path) { project.path_with_namespace } + let(:ref) { 'markdown' } + let(:project_wiki) { nil } + let(:requested_path) { '/' } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + shared_examples :relative_to_requested do + it 'rebuilds URL relative to the requested path' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" + end + end + + context 'with a project_wiki' do + let(:project_wiki) { double('ProjectWiki') } + include_examples :preserve_unchanged + end + + context 'without a repository' do + let(:project) { create(:empty_project) } + include_examples :preserve_unchanged + end + + context 'with an empty repository' do + let(:project) { create(:project_empty_repo) } + include_examples :preserve_unchanged + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a file in the repo' do + doc = filter(link('doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo up one directory' do + relative_link = link('../api/README.md') + doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo up multiple directories' do + relative_link = link('../../../api/README.md') + doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo with an anchor' do + doc = filter(link('README.md#section')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/README.md#section" + end + + it 'rebuilds relative URL for a directory in the repo' do + doc = filter(link('doc/api/')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/tree/#{ref}/doc/api" + end + + it 'rebuilds relative URL for an image in the repo' do + doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'does not modify relative URL with an anchor only' do + doc = filter(link('#section-1')) + expect(doc.at_css('a')['href']).to eq '#section-1' + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = 'files/images/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match '/raw/' + end + + context 'when requested path is a file in the repo' do + let(:requested_path) { 'doc/api/README.md' } + include_examples :relative_to_requested + end + + context 'when requested path is a directory in the repo' do + let(:requested_path) { 'doc/api' } + include_examples :relative_to_requested + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb new file mode 100644 index 00000000000..33d850d0dd3 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SanitizationFilter do + include FilterSpecHelper + + describe 'default whitelist' do + it 'sanitizes tags that are not whitelisted' do + act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} + exp = 'no inputs and no blinks' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes tag attributes' do + act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>} + exp = %q{<a href="http://example.com/bar.html">Text</a>} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes javascript in attributes' do + act = %q(<a href="javascript:alert('foo')">Text</a>) + exp = '<a>Text</a>' + expect(filter(act).to_html).to eq exp + end + + it 'allows whitelisted HTML tags from the user' do + exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute on any element' do + act = %q{<strong class="foo">Strong</strong>} + expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} + end + + it 'sanitizes `id` attribute on any element' do + act = %q{<em id="foo">Emphasis</em>} + expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} + end + end + + describe 'custom whitelist' do + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 5 + end + + it 'allows syntax highlighting' do + exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute from non-highlight spans' do + act = %q{<span class="k">def</span>} + expect(filter(act).to_html).to eq %q{<span>def</span>} + end + + it 'allows `style` attribute on table elements' do + html = <<-HTML.strip_heredoc + <table> + <tr><th style="text-align: center">Head</th></tr> + <tr><td style="text-align: right">Body</th></tr> + </table> + HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to eq 'text-align: center' + expect(doc.at_css('td')['style']).to eq 'text-align: right' + end + + it 'allows `span` elements' do + exp = act = %q{<span>Hello</span>} + expect(filter(act).to_html).to eq exp + end + + it 'removes `rel` attribute from `a` elements' do + act = %q{<a href="#" rel="nofollow">Link</a>} + exp = %q{<a href="#">Link</a>} + + expect(filter(act).to_html).to eq exp + end + + # Adapted from the Sanitize test suite: http://git.io/vczrM + protocols = { + 'protocol-based JS injection: simple, no spaces' => { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces before' => { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces after' => { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces before and after' => { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: preceding colon' => { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: UTF-8 encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long UTF-8 encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: hex encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long hex encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: hex encoding without semicolons' => { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: null char' => { + input: "<a href=java\0script:alert(\"XSS\")>foo</a>", + output: '<a href="java"></a>' + }, + + 'protocol-based JS injection: spaces and entities' => { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a href="">foo</a>' + }, + } + + protocols.each do |name, data| + it "handles #{name}" do + doc = filter(data[:input]) + + expect(doc.to_html).to eq data[:output] + end + end + + it 'allows non-standard anchor schemes' do + exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows relative links' do + exp = %q{<a href="foo/bar.md">foo/bar.md</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + end + + context 'when inline_sanitization is true' do + it 'uses a stricter whitelist' do + doc = filter('<h1>Description</h1>', inline_sanitization: true) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + expect(filter(act, inline_sanitization: true).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + expect(filter(act, inline_sanitization: true).to_html).to eq exp + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb new file mode 100644 index 00000000000..e54dc3cfa33 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SnippetReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:snippet) { create(:project_snippet, project: project) } + let(:reference) { snippet.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Snippet #{reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_snippet_url(project.namespace, project, snippet) + end + + it 'links with adjacent text' do + doc = reference_filter("Snippet (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs' do + exp = act = "Snippet #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Snippet #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + end + + it 'escapes the title attribute' do + snippet.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = reference_filter("Snippet #{reference}") + expect(doc.text).to eq "Snippet #{reference}" + end + + it 'includes default classes' do + doc = reference_filter("Snippet #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Snippet #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-snippet attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-snippet') + expect(link.attr('data-snippet')).to eq snippet.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Snippet #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { snippet.to_reference(project) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb new file mode 100644 index 00000000000..2df81282fa2 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SyntaxHighlightFilter do + include FilterSpecHelper + + it 'highlights valid code blocks' do + result = filter('<pre><code>def fun end</code>') + expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n") + end + + it 'passes through invalid code blocks' do + allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError) + + result = filter('<pre><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre>This is a test</pre>') + end +end diff --git a/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb new file mode 100644 index 00000000000..3aba29f0f27 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb @@ -0,0 +1,97 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::TableOfContentsFilter do + include FilterSpecHelper + + def header(level, text) + "<h#{level}>#{text}</h#{level}>\n" + end + + it 'does nothing when :no_header_anchors is truthy' do + exp = act = header(1, 'Header') + expect(filter(act, no_header_anchors: 1).to_html).to eq exp + end + + it 'does nothing with empty headers' do + exp = act = header(1, nil) + expect(filter(act).to_html).to eq exp + end + + 1.upto(6) do |i| + it "processes h#{i} elements" do + html = header(i, "Header #{i}") + doc = filter(html) + + expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + end + end + + describe 'anchor tag' do + it 'has an `anchor` class' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' + end + + it 'links to the id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('href')).to eq '#header' + end + + describe 'generated IDs' do + it 'translates spaces to dashes' do + doc = filter(header(1, 'This header has spaces in it')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + end + + it 'squeezes multiple spaces and dashes' do + doc = filter(header(1, 'This---header is poorly-formatted')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + end + + it 'removes punctuation' do + doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + end + + it 'appends a unique number to duplicates' do + doc = filter(header(1, 'One') + header(2, 'One')) + + expect(doc.css('h1 a').first.attr('id')).to eq 'one' + expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + end + + it 'supports Unicode' do + doc = filter(header(1, '한글')) + expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('href')).to eq '#한글' + end + end + end + + describe 'result' do + def result(html) + HTML::Pipeline.new([described_class]).call(html) + end + + let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } + let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } + + it 'is contained within a `ul` element' do + expect(doc.children.first.name).to eq 'ul' + expect(doc.children.first.attr('class')).to eq 'section-nav' + end + + it 'contains an `li` element for each header' do + expect(doc.css('li').length).to eq 2 + + links = doc.css('li a') + + expect(links.first.attr('href')).to eq '#header-1' + expect(links.first.text).to eq 'Header 1' + expect(links.last.attr('href')).to eq '#header-2' + expect(links.last.text).to eq 'Header 2' + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb new file mode 100644 index 00000000000..8b205adbbc4 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe Gitlab::Markdown::TaskListFilter do + include FilterSpecHelper + + it 'does not apply `task-list` class to non-task lists' do + exp = act = %(<ul><li>Item</li></ul>) + expect(filter(act).to_html).to eq exp + end +end diff --git a/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb new file mode 100644 index 00000000000..aec22ee4168 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb @@ -0,0 +1,73 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::UploadLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def image(path) + %(<img src="#{path}" />) + end + + def link(path) + %(<a href="#{path}">#{path}</a>) + end + + let(:project) { create(:project) } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a link' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'rebuilds relative URL for an image' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = '/uploads/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb new file mode 100644 index 00000000000..4ebf5704c70 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' + +describe Gitlab::Markdown::UserReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:reference) { user.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + it 'ignores invalid users' do + exp = act = "Hey #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to eq(exp) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey #{reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'mentioning @all' do + let(:reference) { User.reference_prefix + 'all' } + + before do + project.team << [project.creator, :developer] + end + + it 'supports a special @all mention' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_url(project.namespace, project) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [project.creator] + end + end + + context 'mentioning a user' do + it 'links to a User' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links to a User with a period' do + user = create(:user, name: 'alphA.Beta') + + doc = reference_filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a User with an underscore' do + user = create(:user, name: 'ping_pong_king') + + doc = reference_filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'includes a data-user attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end + + context 'mentioning a group' do + let(:group) { create(:group) } + let(:reference) { group.to_reference } + + it 'links to the Group' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'includes a data-group attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-group') + expect(link.attr('data-group')).to eq group.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq group.users + end + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + end + + it 'includes default classes' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + end + + it 'supports an :only_path context' do + doc = reference_filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.user_path(user) + end + + context 'referencing a user in a link href' do + let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} } + + it 'links to a User' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/) + end + + it 'includes a data-user attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end +end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb deleted file mode 100644 index 078ff3ed4b2..00000000000 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ /dev/null @@ -1,211 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe IssueReferenceFilter do - include FilterSpecHelper - - def helper - IssuesHelper - end - - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { issue.to_reference } - - it 'ignores valid references when using non-default tracker' do - expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = reference_filter("Fixed #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid issue IDs' do - invalid = invalidate_reference(reference) - exp = act = "Fixed #{invalid}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" - end - - it 'escapes the title attribute' do - issue.update_attribute(:title, %{"></a>whatever<a title="}) - - doc = reference_filter("Issue #{reference}") - expect(doc.text).to eq "Issue #{reference}" - end - - it 'includes default classes' do - doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' - end - - it 'includes a data-project attribute' do - doc = reference_filter("Issue #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-issue attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-issue') - expect(link.attr('data-issue')).to eq issue.id.to_s - end - - it 'supports an :only_path context' do - doc = reference_filter("Issue #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { issue.to_reference(project) } - - it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(Project).to receive(:get_issue). - with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - - context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq reference - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - - context 'cross-project reference in link href' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - - context 'cross-project URL in link href' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + "#note_123" - end - - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb deleted file mode 100644 index ef6dd524aba..00000000000 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ /dev/null @@ -1,181 +0,0 @@ -require 'spec_helper' -require 'html/pipeline' - -module Gitlab::Markdown - describe LabelReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:label) { create(:label, project: project) } - let(:reference) { label.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Label #{reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - it 'includes default classes' do - doc = reference_filter("Label #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' - end - - it 'includes a data-project attribute' do - doc = reference_filter("Label #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-label attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-label') - expect(link.attr('data-label')).to eq label.id.to_s - end - - it 'supports an :only_path context' do - doc = reference_filter("Label #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end - - describe 'label span element' do - it 'includes default classes' do - doc = reference_filter("Label #{reference}") - expect(doc.css('a span').first.attr('class')).to eq 'label color-label' - end - - it 'includes a style attribute' do - doc = reference_filter("Label #{reference}") - expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) - end - end - - context 'Integer-based references' do - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - end - - it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end - - it 'ignores invalid label IDs' do - exp = act = "Label #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'String-based single-word references' do - let(:label) { create(:label, name: 'gfm', project: project) } - let(:reference) { "#{Label.reference_prefix}#{label.name}" } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm' - end - - it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end - - it 'ignores invalid label names' do - exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" - - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'String-based multi-word references in quotes' do - let(:label) { create(:label, name: 'gfm references', project: project) } - let(:reference) { label.to_reference(:name) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end - - it 'ignores invalid label names' do - exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - - expect(reference_filter(act).to_html).to eq exp - end - end - - describe 'edge cases' do - it 'gracefully handles non-references matching the pattern' do - exp = act = '(format nil "~0f" 3.0) ; 3.0' - expect(reference_filter(act).to_html).to eq exp - end - end - - describe 'referencing a label in a link href' do - let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - end - - it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\))) - end - - it 'includes a data-project attribute' do - doc = reference_filter("Label #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-label attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-label') - expect(link.attr('data-label')).to eq label.id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb deleted file mode 100644 index 4a232051127..00000000000 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe MergeRequestReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:merge) { create(:merge_request, source_project: project) } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { merge.to_reference } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_merge_request_url(project.namespace, project, merge) - end - - it 'links with adjacent text' do - doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid merge IDs' do - exp = act = "Merge #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" - end - - it 'escapes the title attribute' do - merge.update_attribute(:title, %{"></a>whatever<a title="}) - - doc = reference_filter("Merge #{reference}") - expect(doc.text).to eq "Merge #{reference}" - end - - it 'includes default classes' do - doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' - end - - it 'includes a data-project attribute' do - doc = reference_filter("Merge #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-merge-request attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-merge-request') - expect(link.attr('data-merge-request')).to eq merge.id.to_s - end - - it 'supports an :only_path context' do - doc = reference_filter("Merge #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } - let(:reference) { merge.to_reference(project) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_merge_request_url(project2.namespace, - project2, merge) - end - - it 'links with adjacent text' do - doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid merge IDs on the referenced project' do - exp = act = "Merge #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end - end - - context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } - let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq reference - end - - it 'links with adjacent text' do - doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb deleted file mode 100644 index eea3f1cf370..00000000000 --- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe RedactorFilter do - include ActionView::Helpers::UrlHelper - include FilterSpecHelper - - it 'ignores non-GFM links' do - html = %(See <a href="https://google.com/">Google</a>) - doc = filter(html, current_user: double) - - expect(doc.css('a').length).to eq 1 - end - - def reference_link(data) - link_to('text', '', class: 'gfm', data: data) - end - - context 'with data-project' do - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) - - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 0 - end - - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - project.team << [user, :master] - - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 1 - end - - it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - - expect { filter(link) }.not_to raise_error - end - end - - context "for user references" do - - context 'with data-group' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 0 - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 1 - end - - it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - - expect { filter(link) }.not_to raise_error - end - end - - context 'with data-user' do - it 'allows any User reference' do - user = create(:user) - - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link) - - expect(doc.css('a').length).to eq 1 - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb deleted file mode 100644 index 4fa473ad191..00000000000 --- a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe ReferenceGathererFilter do - include ActionView::Helpers::UrlHelper - include FilterSpecHelper - - def reference_link(data) - link_to('text', '', class: 'gfm', data: data) - end - - context "for issue references" do - - context 'with data-project' do - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to be_empty - end - - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - project.team << [user, :master] - - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to eq([issue]) - end - - it 'handles invalid Project references' do - link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - - expect { pipeline_result(link) }.not_to raise_error - end - end - end - - context "for user references" do - - context 'with data-group' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to be_empty - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to eq([user]) - end - - it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - - expect { pipeline_result(link) }.not_to raise_error - end - end - - context 'with data-user' do - it 'allows any User reference' do - user = create(:user) - - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link) - - expect(result[:references][:user]).to eq([user]) - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb deleted file mode 100644 index 027336ceb73..00000000000 --- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe RelativeLinkFilter do - def filter(doc, contexts = {}) - contexts.reverse_merge!({ - commit: project.commit, - project: project, - project_wiki: project_wiki, - ref: ref, - requested_path: requested_path - }) - - described_class.call(doc, contexts) - end - - def image(path) - %(<img src="#{path}" />) - end - - def link(path) - %(<a href="#{path}">#{path}</a>) - end - - let(:project) { create(:project) } - let(:project_path) { project.path_with_namespace } - let(:ref) { 'markdown' } - let(:project_wiki) { nil } - let(:requested_path) { '/' } - - shared_examples :preserve_unchanged do - it 'does not modify any relative URL in anchor' do - doc = filter(link('README.md')) - expect(doc.at_css('a')['href']).to eq 'README.md' - end - - it 'does not modify any relative URL in image' do - doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' - end - end - - shared_examples :relative_to_requested do - it 'rebuilds URL relative to the requested path' do - doc = filter(link('users.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" - end - end - - context 'with a project_wiki' do - let(:project_wiki) { double('ProjectWiki') } - include_examples :preserve_unchanged - end - - context 'without a repository' do - let(:project) { create(:empty_project) } - include_examples :preserve_unchanged - end - - context 'with an empty repository' do - let(:project) { create(:project_empty_repo) } - include_examples :preserve_unchanged - end - - it 'does not raise an exception on invalid URIs' do - act = link("://foo") - expect { filter(act) }.not_to raise_error - end - - context 'with a valid repository' do - it 'rebuilds relative URL for a file in the repo' do - doc = filter(link('doc/api/README.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo up one directory' do - relative_link = link('../api/README.md') - doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') - - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo up multiple directories' do - relative_link = link('../../../api/README.md') - doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') - - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo with an anchor' do - doc = filter(link('README.md#section')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/README.md#section" - end - - it 'rebuilds relative URL for a directory in the repo' do - doc = filter(link('doc/api/')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/tree/#{ref}/doc/api" - end - - it 'rebuilds relative URL for an image in the repo' do - doc = filter(link('files/images/logo-black.png')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" - end - - it 'does not modify relative URL with an anchor only' do - doc = filter(link('#section-1')) - expect(doc.at_css('a')['href']).to eq '#section-1' - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com')) - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - - it 'supports Unicode filenames' do - path = 'files/images/한글.png' - escaped = Addressable::URI.escape(path) - - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) - - doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' - end - - context 'when requested path is a file in the repo' do - let(:requested_path) { 'doc/api/README.md' } - include_examples :relative_to_requested - end - - context 'when requested path is a directory in the repo' do - let(:requested_path) { 'doc/api' } - include_examples :relative_to_requested - end - end - end -end diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb deleted file mode 100644 index 27cd00e8054..00000000000 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SanitizationFilter do - include FilterSpecHelper - - describe 'default whitelist' do - it 'sanitizes tags that are not whitelisted' do - act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} - exp = 'no inputs and no blinks' - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes tag attributes' do - act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>} - exp = %q{<a href="http://example.com/bar.html">Text</a>} - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes javascript in attributes' do - act = %q(<a href="javascript:alert('foo')">Text</a>) - exp = '<a>Text</a>' - expect(filter(act).to_html).to eq exp - end - - it 'allows whitelisted HTML tags from the user' do - exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes `class` attribute on any element' do - act = %q{<strong class="foo">Strong</strong>} - expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} - end - - it 'sanitizes `id` attribute on any element' do - act = %q{<em id="foo">Emphasis</em>} - expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} - end - end - - describe 'custom whitelist' do - it 'customizes the whitelist only once' do - instance = described_class.new('Foo') - 3.times { instance.whitelist } - - expect(instance.whitelist[:transformers].size).to eq 5 - end - - it 'allows syntax highlighting' do - exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes `class` attribute from non-highlight spans' do - act = %q{<span class="k">def</span>} - expect(filter(act).to_html).to eq %q{<span>def</span>} - end - - it 'allows `style` attribute on table elements' do - html = <<-HTML.strip_heredoc - <table> - <tr><th style="text-align: center">Head</th></tr> - <tr><td style="text-align: right">Body</th></tr> - </table> - HTML - - doc = filter(html) - - expect(doc.at_css('th')['style']).to eq 'text-align: center' - expect(doc.at_css('td')['style']).to eq 'text-align: right' - end - - it 'allows `span` elements' do - exp = act = %q{<span>Hello</span>} - expect(filter(act).to_html).to eq exp - end - - it 'removes `rel` attribute from `a` elements' do - act = %q{<a href="#" rel="nofollow">Link</a>} - exp = %q{<a href="#">Link</a>} - - expect(filter(act).to_html).to eq exp - end - - # Adapted from the Sanitize test suite: http://git.io/vczrM - protocols = { - 'protocol-based JS injection: simple, no spaces' => { - input: '<a href="javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces before' => { - input: '<a href="javascript :alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces after' => { - input: '<a href="javascript: alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces before and after' => { - input: '<a href="javascript : alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: preceding colon' => { - input: '<a href=":javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: UTF-8 encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long UTF-8 encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { - input: '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: hex encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long hex encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: hex encoding without semicolons' => { - input: '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: null char' => { - input: "<a href=java\0script:alert(\"XSS\")>foo</a>", - output: '<a href="java"></a>' - }, - - 'protocol-based JS injection: spaces and entities' => { - input: '<a href="  javascript:alert(\'XSS\');">foo</a>', - output: '<a href="">foo</a>' - }, - } - - protocols.each do |name, data| - it "handles #{name}" do - doc = filter(data[:input]) - - expect(doc.to_html).to eq data[:output] - end - end - - it 'allows non-standard anchor schemes' do - exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} - act = filter(exp) - - expect(act.to_html).to eq exp - end - - it 'allows relative links' do - exp = %q{<a href="foo/bar.md">foo/bar.md</a>} - act = filter(exp) - - expect(act.to_html).to eq exp - end - end - - context 'when pipeline is :description' do - it 'uses a stricter whitelist' do - doc = filter('<h1>Description</h1>', pipeline: :description) - expect(doc.to_html.strip).to eq 'Description' - end - - %w(pre code img ol ul li).each do |elem| - it "removes '#{elem}' elements" do - act = "<#{elem}>Description</#{elem}>" - expect(filter(act, pipeline: :description).to_html.strip). - to eq 'Description' - end - end - - %w(b i strong em a ins del sup sub p).each do |elem| - it "still allows '#{elem}' elements" do - exp = act = "<#{elem}>Description</#{elem}>" - expect(filter(act, pipeline: :description).to_html).to eq exp - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb deleted file mode 100644 index 3a9acc9d6d4..00000000000 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ /dev/null @@ -1,148 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SnippetReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:snippet) { create(:project_snippet, project: project) } - let(:reference) { snippet.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Snippet #{reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_snippet_url(project.namespace, project, snippet) - end - - it 'links with adjacent text' do - doc = reference_filter("Snippet (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs' do - exp = act = "Snippet #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" - end - - it 'escapes the title attribute' do - snippet.update_attribute(:title, %{"></a>whatever<a title="}) - - doc = reference_filter("Snippet #{reference}") - expect(doc.text).to eq "Snippet #{reference}" - end - - it 'includes default classes' do - doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' - end - - it 'includes a data-project attribute' do - doc = reference_filter("Snippet #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-snippet attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-snippet') - expect(link.attr('data-snippet')).to eq snippet.id.to_s - end - - it 'supports an :only_path context' do - doc = reference_filter("Snippet #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { snippet.to_reference(project) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) - end - - it 'links with adjacent text' do - doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end - end - - context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) - end - - it 'links with adjacent text' do - doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs on the referenced project' do - act = "See #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb deleted file mode 100644 index 6a490673728..00000000000 --- a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SyntaxHighlightFilter do - include FilterSpecHelper - - it 'highlights valid code blocks' do - result = filter('<pre><code>def fun end</code>') - expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n") - end - - it 'passes through invalid code blocks' do - allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError) - - result = filter('<pre><code>This is a test</code></pre>') - expect(result.to_html).to eq('<pre>This is a test</pre>') - end - end -end diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb deleted file mode 100644 index ddf583a72c1..00000000000 --- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe TableOfContentsFilter do - include FilterSpecHelper - - def header(level, text) - "<h#{level}>#{text}</h#{level}>\n" - end - - it 'does nothing when :no_header_anchors is truthy' do - exp = act = header(1, 'Header') - expect(filter(act, no_header_anchors: 1).to_html).to eq exp - end - - it 'does nothing with empty headers' do - exp = act = header(1, nil) - expect(filter(act).to_html).to eq exp - end - - 1.upto(6) do |i| - it "processes h#{i} elements" do - html = header(i, "Header #{i}") - doc = filter(html) - - expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" - end - end - - describe 'anchor tag' do - it 'has an `anchor` class' do - doc = filter(header(1, 'Header')) - expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' - end - - it 'links to the id' do - doc = filter(header(1, 'Header')) - expect(doc.css('h1 a').first.attr('href')).to eq '#header' - end - - describe 'generated IDs' do - it 'translates spaces to dashes' do - doc = filter(header(1, 'This header has spaces in it')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' - end - - it 'squeezes multiple spaces and dashes' do - doc = filter(header(1, 'This---header is poorly-formatted')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' - end - - it 'removes punctuation' do - doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' - end - - it 'appends a unique number to duplicates' do - doc = filter(header(1, 'One') + header(2, 'One')) - - expect(doc.css('h1 a').first.attr('id')).to eq 'one' - expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' - end - - it 'supports Unicode' do - doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq '한글' - expect(doc.css('h1 a').first.attr('href')).to eq '#한글' - end - end - end - - describe 'result' do - def result(html) - HTML::Pipeline.new([described_class]).call(html) - end - - let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } - let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } - - it 'is contained within a `ul` element' do - expect(doc.children.first.name).to eq 'ul' - expect(doc.children.first.attr('class')).to eq 'section-nav' - end - - it 'contains an `li` element for each header' do - expect(doc.css('li').length).to eq 2 - - links = doc.css('li a') - - expect(links.first.attr('href')).to eq '#header-1' - expect(links.first.text).to eq 'Header 1' - expect(links.last.attr('href')).to eq '#header-2' - expect(links.last.text).to eq 'Header 2' - end - end - end -end diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb deleted file mode 100644 index 94f39cc966e..00000000000 --- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe TaskListFilter do - include FilterSpecHelper - - it 'does not apply `task-list` class to non-task lists' do - exp = act = %(<ul><li>Item</li></ul>) - expect(filter(act).to_html).to eq exp - end - end -end diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb deleted file mode 100644 index 9ae45a6f559..00000000000 --- a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe UploadLinkFilter do - def filter(doc, contexts = {}) - contexts.reverse_merge!({ - project: project - }) - - described_class.call(doc, contexts) - end - - def image(path) - %(<img src="#{path}" />) - end - - def link(path) - %(<a href="#{path}">#{path}</a>) - end - - let(:project) { create(:project) } - - shared_examples :preserve_unchanged do - it 'does not modify any relative URL in anchor' do - doc = filter(link('README.md')) - expect(doc.at_css('a')['href']).to eq 'README.md' - end - - it 'does not modify any relative URL in image' do - doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' - end - end - - it 'does not raise an exception on invalid URIs' do - act = link("://foo") - expect { filter(act) }.not_to raise_error - end - - context 'with a valid repository' do - it 'rebuilds relative URL for a link' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'rebuilds relative URL for an image' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com')) - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - - it 'supports Unicode filenames' do - path = '/uploads/한글.png' - escaped = Addressable::URI.escape(path) - - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) - - doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" - end - end - end -end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb deleted file mode 100644 index 25379f0670e..00000000000 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe UserReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - let(:reference) { user.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - it 'ignores invalid users' do - exp = act = "Hey #{invalidate_reference(reference)}" - expect(reference_filter(act).to_html).to eq(exp) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Hey #{reference}</#{elem}>" - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'mentioning @all' do - let(:reference) { User.reference_prefix + 'all' } - - before do - project.team << [project.creator, :developer] - end - - it 'supports a special @all mention' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').length).to eq 1 - expect(doc.css('a').first.attr('href')) - .to eq urls.namespace_project_url(project.namespace, project) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [project.creator] - end - end - - context 'mentioning a user' do - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links to a User with a period' do - user = create(:user, name: 'alphA.Beta') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with an underscore' do - user = create(:user, name: 'ping_pong_king') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end - end - - context 'mentioning a group' do - let(:group) { create(:group) } - let(:reference) { group.to_reference } - - it 'links to the Group' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) - end - - it 'includes a data-group attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-group') - expect(link.attr('data-group')).to eq group.id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq group.users - end - end - - it 'links with adjacent text' do - doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) - end - - it 'includes default classes' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' - end - - it 'supports an :only_path context' do - doc = reference_filter("Hey #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.user_path(user) - end - - context 'referencing a user in a link href' do - let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} } - - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links with adjacent text' do - doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/) - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end - end - end -end |