4daf3dc0db
When post-processing relative links to absolute links RelativeLinkFilter didn't take into consideration that internal repository data could be exposed for users that do not have repository access to the project. This commit solves that by checking whether the user can `download_code` at this repository, avoiding any processing of this filter if the user can't. Additionally, if we're processing for a group ( no project was given), we check if the user can read it in order to expand the href as an extra. That doesn't seem necessarily a breach now, but an extra check doesn't hurt as after all the user needs to be able to `read_group`.
548 lines
19 KiB
Ruby
548 lines
19 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe MarkupHelper do
|
|
let!(:project) { create(:project, :repository) }
|
|
|
|
let(:user) { create(:user, username: 'gfm') }
|
|
let(:commit) { project.commit }
|
|
let(:issue) { create(:issue, project: project) }
|
|
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
|
|
let(:snippet) { create(:project_snippet, project: project) }
|
|
|
|
before do
|
|
# Ensure the generated reference links aren't redacted
|
|
project.add_maintainer(user)
|
|
|
|
# Helper expects a @project instance variable
|
|
helper.instance_variable_set(:@project, project)
|
|
|
|
# Stub the `current_user` helper
|
|
allow(helper).to receive(:current_user).and_return(user)
|
|
end
|
|
|
|
describe "#markdown" do
|
|
describe "referencing multiple objects" do
|
|
let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
|
|
|
|
it "links to the merge request" do
|
|
expected = urls.project_merge_request_path(project, merge_request)
|
|
expect(helper.markdown(actual)).to match(expected)
|
|
end
|
|
|
|
it "links to the commit" do
|
|
expected = urls.project_commit_path(project, commit)
|
|
expect(helper.markdown(actual)).to match(expected)
|
|
end
|
|
|
|
it "links to the issue" do
|
|
expected = urls.project_issue_path(project, issue)
|
|
expect(helper.markdown(actual)).to match(expected)
|
|
end
|
|
end
|
|
|
|
describe "override default project" do
|
|
let(:actual) { issue.to_reference }
|
|
let(:second_project) { create(:project, :public) }
|
|
let(:second_issue) { create(:issue, project: second_project) }
|
|
|
|
it 'links to the issue' do
|
|
expected = urls.project_issue_path(second_project, second_issue)
|
|
expect(markdown(actual, project: second_project)).to match(expected)
|
|
end
|
|
end
|
|
|
|
describe 'uploads' do
|
|
let(:text) { "![ImageTest](/uploads/test.png)" }
|
|
let(:group) { create(:group) }
|
|
|
|
subject { helper.markdown(text) }
|
|
|
|
describe 'inside a project' do
|
|
it 'renders uploads relative to project' do
|
|
expect(subject).to include("#{project.full_path}/uploads/test.png")
|
|
end
|
|
end
|
|
|
|
describe 'inside a group' do
|
|
before do
|
|
# Ensure the generated reference links aren't redacted
|
|
group.add_maintainer(user)
|
|
|
|
helper.instance_variable_set(:@group, group)
|
|
helper.instance_variable_set(:@project, nil)
|
|
end
|
|
|
|
it 'renders uploads relative to the group' do
|
|
expect(subject).to include("#{group.full_path}/-/uploads/test.png")
|
|
end
|
|
end
|
|
|
|
describe "with a group in the context" do
|
|
let(:project_in_group) { create(:project, group: group) }
|
|
|
|
before do
|
|
# Ensure the generated reference links aren't redacted
|
|
project_in_group.add_maintainer(user)
|
|
|
|
helper.instance_variable_set(:@group, group)
|
|
helper.instance_variable_set(:@project, project_in_group)
|
|
end
|
|
|
|
it 'renders uploads relative to project' do
|
|
expect(subject).to include("#{project_in_group.path_with_namespace}/uploads/test.png")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#markdown_field' do
|
|
let(:attribute) { :title }
|
|
|
|
describe 'with already redacted attribute' do
|
|
it 'returns the redacted attribute' do
|
|
commit.redacted_title_html = 'commit title'
|
|
|
|
expect(Banzai).not_to receive(:render_field)
|
|
|
|
expect(helper.markdown_field(commit, attribute)).to eq('commit title')
|
|
end
|
|
end
|
|
|
|
describe 'without redacted attribute' do
|
|
it 'renders the markdown value' do
|
|
expect(Banzai).to receive(:render_field).with(commit, attribute, {}).and_call_original
|
|
|
|
helper.markdown_field(commit, attribute)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#link_to_markdown_field' do
|
|
let(:link) { '/commits/0a1b2c3d' }
|
|
let(:issues) { create_list(:issue, 2, project: project) }
|
|
|
|
# Clean the cache to make sure the title is re-rendered from the stubbed one
|
|
it 'handles references nested in links with all the text', :clean_gitlab_redis_cache do
|
|
allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real")
|
|
|
|
actual = helper.link_to_markdown_field(commit, :title, link)
|
|
doc = Nokogiri::HTML.parse(actual)
|
|
|
|
# Make sure we didn't create invalid markup
|
|
expect(doc.errors).to be_empty
|
|
|
|
# Leading commit link
|
|
expect(doc.css('a')[0].attr('href')).to eq link
|
|
expect(doc.css('a')[0].text).to eq 'This should finally fix '
|
|
|
|
# First issue link
|
|
expect(doc.css('a')[1].attr('href'))
|
|
.to eq urls.project_issue_path(project, issues[0])
|
|
expect(doc.css('a')[1].text).to eq issues[0].to_reference
|
|
|
|
# Internal commit link
|
|
expect(doc.css('a')[2].attr('href')).to eq link
|
|
expect(doc.css('a')[2].text).to eq ' and '
|
|
|
|
# Second issue link
|
|
expect(doc.css('a')[3].attr('href'))
|
|
.to eq urls.project_issue_path(project, issues[1])
|
|
expect(doc.css('a')[3].text).to eq issues[1].to_reference
|
|
|
|
# Trailing commit link
|
|
expect(doc.css('a')[4].attr('href')).to eq link
|
|
expect(doc.css('a')[4].text).to eq ' for real'
|
|
end
|
|
end
|
|
|
|
describe '#link_to_markdown' do
|
|
let(:link) { '/commits/0a1b2c3d' }
|
|
let(:issues) { create_list(:issue, 2, project: project) }
|
|
|
|
it 'handles references nested in links with all the text' do
|
|
actual = helper.link_to_markdown("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link)
|
|
doc = Nokogiri::HTML.parse(actual)
|
|
|
|
# Make sure we didn't create invalid markup
|
|
expect(doc.errors).to be_empty
|
|
|
|
# Leading commit link
|
|
expect(doc.css('a')[0].attr('href')).to eq link
|
|
expect(doc.css('a')[0].text).to eq 'This should finally fix '
|
|
|
|
# First issue link
|
|
expect(doc.css('a')[1].attr('href'))
|
|
.to eq urls.project_issue_path(project, issues[0])
|
|
expect(doc.css('a')[1].text).to eq issues[0].to_reference
|
|
|
|
# Internal commit link
|
|
expect(doc.css('a')[2].attr('href')).to eq link
|
|
expect(doc.css('a')[2].text).to eq ' and '
|
|
|
|
# Second issue link
|
|
expect(doc.css('a')[3].attr('href'))
|
|
.to eq urls.project_issue_path(project, issues[1])
|
|
expect(doc.css('a')[3].text).to eq issues[1].to_reference
|
|
|
|
# Trailing commit link
|
|
expect(doc.css('a')[4].attr('href')).to eq link
|
|
expect(doc.css('a')[4].text).to eq ' for real'
|
|
end
|
|
|
|
it 'forwards HTML options' do
|
|
actual = helper.link_to_markdown("Fixed in #{commit.id}", link, class: 'foo')
|
|
doc = Nokogiri::HTML.parse(actual)
|
|
|
|
expect(doc.css('a')).to satisfy do |v|
|
|
# 'foo' gets added to all links
|
|
v.all? { |a| a.attr('class').match(/foo$/) }
|
|
end
|
|
end
|
|
|
|
it "escapes HTML passed in as the body" do
|
|
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
|
|
expect(helper.link_to_markdown(actual, link))
|
|
.to match('<h1>test</h1>')
|
|
end
|
|
|
|
it 'ignores reference links when they are the entire body' do
|
|
text = issues[0].to_reference
|
|
act = helper.link_to_markdown(text, '/foo')
|
|
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
|
|
end
|
|
|
|
it 'replaces commit message with emoji to link' do
|
|
actual = link_to_markdown(':book: Book', '/foo')
|
|
expect(actual)
|
|
.to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
|
|
end
|
|
end
|
|
|
|
describe '#link_to_html' do
|
|
it 'wraps the rendered content in a link' do
|
|
link = '/commits/0a1b2c3d'
|
|
issue = create(:issue, project: project)
|
|
|
|
rendered = helper.markdown("This should finally fix #{issue.to_reference} for real", pipeline: :single_line)
|
|
doc = Nokogiri::HTML.parse(rendered)
|
|
|
|
expect(doc.css('a')[0].attr('href'))
|
|
.to eq urls.project_issue_path(project, issue)
|
|
expect(doc.css('a')[0].text).to eq issue.to_reference
|
|
|
|
wrapped = helper.link_to_html(rendered, link)
|
|
doc = Nokogiri::HTML.parse(wrapped)
|
|
|
|
expect(doc.css('a')[0].attr('href')).to eq link
|
|
expect(doc.css('a')[0].text).to eq 'This should finally fix '
|
|
end
|
|
end
|
|
|
|
describe '#render_wiki_content' do
|
|
before do
|
|
@wiki = double('WikiPage')
|
|
allow(@wiki).to receive(:content).and_return('wiki content')
|
|
allow(@wiki).to receive(:slug).and_return('nested/page')
|
|
helper.instance_variable_set(:@project_wiki, @wiki)
|
|
end
|
|
|
|
it "uses Wiki pipeline for markdown files" do
|
|
allow(@wiki).to receive(:format).and_return(:markdown)
|
|
|
|
expect(helper).to receive(:markdown_unsafe).with('wiki content',
|
|
pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
|
|
issuable_state_filter_enabled: true)
|
|
|
|
helper.render_wiki_content(@wiki)
|
|
end
|
|
|
|
it "uses Asciidoctor for asciidoc files" do
|
|
allow(@wiki).to receive(:format).and_return(:asciidoc)
|
|
|
|
expect(helper).to receive(:asciidoc_unsafe).with('wiki content')
|
|
|
|
helper.render_wiki_content(@wiki)
|
|
end
|
|
|
|
it "uses the Gollum renderer for all other file types" do
|
|
allow(@wiki).to receive(:format).and_return(:rdoc)
|
|
formatted_content_stub = double('formatted_content')
|
|
expect(formatted_content_stub).to receive(:html_safe)
|
|
allow(@wiki).to receive(:formatted_content).and_return(formatted_content_stub)
|
|
|
|
helper.render_wiki_content(@wiki)
|
|
end
|
|
end
|
|
|
|
describe '#markup' do
|
|
let(:content) { 'Noël' }
|
|
|
|
it 'preserves encoding' do
|
|
expect(content.encoding.name).to eq('UTF-8')
|
|
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
|
|
end
|
|
|
|
it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
|
|
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
|
|
expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
|
|
|
|
expect(helper.markup('foo.md', content)).to eq('NOEL')
|
|
end
|
|
|
|
it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
|
|
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
|
|
expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
|
|
|
|
expect(helper.markup('foo.adoc', content)).to eq('NOEL')
|
|
end
|
|
|
|
it 'uses passed in rendered content' do
|
|
expect(helper).not_to receive(:gitlab_markdown?)
|
|
expect(helper).not_to receive(:markdown_unsafe)
|
|
|
|
expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
|
|
end
|
|
|
|
it 'defaults to CommonMark' do
|
|
expect(helper.markup('foo.md', 'x^2')).to include('x^2')
|
|
end
|
|
end
|
|
|
|
describe '#markup_unsafe' do
|
|
subject { helper.markup_unsafe(file_name, text, context) }
|
|
|
|
let(:file_name) { 'foo.bar' }
|
|
let(:text) { 'Noël' }
|
|
let(:project_base) { build(:project, :repository) }
|
|
let(:context) { { project: project_base } }
|
|
|
|
context 'when text is missing' do
|
|
let(:text) { nil }
|
|
|
|
it 'returns an empty string' do
|
|
is_expected.to eq('')
|
|
end
|
|
end
|
|
|
|
context 'when file is a markdown file' do
|
|
let(:file_name) { 'foo.md' }
|
|
|
|
it 'returns html (rendered by Banzai)' do
|
|
expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
|
|
|
|
expect(Banzai).to receive(:render).with(text, context) { expected_html }
|
|
|
|
is_expected.to eq(expected_html)
|
|
end
|
|
|
|
context 'when renderer returns an error' do
|
|
before do
|
|
allow(Banzai).to receive(:render).and_raise("An error")
|
|
end
|
|
|
|
it 'returns html (rendered by ActionView:TextHelper)' do
|
|
is_expected.to eq('<p>Noël</p>')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when file is asciidoc file' do
|
|
let(:file_name) { 'foo.adoc' }
|
|
|
|
it 'returns html (rendered by Gitlab::Asciidoc)' do
|
|
expected_html = "<div>\n<p>Noël</p>\n</div>"
|
|
|
|
expect(Gitlab::Asciidoc).to receive(:render).with(text, context) { expected_html }
|
|
|
|
is_expected.to eq(expected_html)
|
|
end
|
|
end
|
|
|
|
context 'when file is a regular text file' do
|
|
let(:file_name) { 'foo.txt' }
|
|
|
|
it 'returns html (rendered by ActionView::TagHelper)' do
|
|
is_expected.to eq('<pre class="plain-readme">Noël</pre>')
|
|
end
|
|
end
|
|
|
|
context 'when file has an unknown type' do
|
|
let(:file_name) { 'foo' }
|
|
|
|
it 'returns html (rendered by Gitlab::OtherMarkup)' do
|
|
expected_html = 'Noël'
|
|
|
|
expect(Gitlab::OtherMarkup).to receive(:render).with(file_name, text, context) { expected_html }
|
|
|
|
is_expected.to eq(expected_html)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#first_line_in_markdown' do
|
|
shared_examples_for 'common markdown examples' do
|
|
let(:project_base) { build(:project, :repository) }
|
|
|
|
it 'displays inline code' do
|
|
object = create_object('Text with `inline code`')
|
|
expected = 'Text with <code>inline code</code>'
|
|
|
|
expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
|
|
end
|
|
|
|
it 'truncates the text with multiple paragraphs' do
|
|
object = create_object("Paragraph 1\n\nParagraph 2")
|
|
expected = 'Paragraph 1...'
|
|
|
|
expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
|
|
end
|
|
|
|
it 'displays the first line of a code block' do
|
|
object = create_object("```\nCode block\nwith two lines\n```")
|
|
expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
|
|
|
|
expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
|
|
end
|
|
|
|
it 'truncates a single long line of text' do
|
|
text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
|
|
object = create_object(text * 4)
|
|
expected = (text * 2).sub(/.{3}/, '...')
|
|
|
|
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
|
|
end
|
|
|
|
it 'preserves a link href when link text is truncated' do
|
|
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
|
|
input = "#{text}#{text}#{text} " # 133 chars
|
|
link_url = 'http://example.com/foo/bar/baz' # 30 chars
|
|
input << link_url
|
|
object = create_object(input)
|
|
expected_link_text = 'http://example...</a>'
|
|
|
|
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
|
|
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
|
|
end
|
|
|
|
it 'preserves code color scheme' do
|
|
object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
|
|
expected = "<pre class=\"code highlight js-syntax-highlight ruby\">" \
|
|
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
|
|
"</code></pre>"
|
|
|
|
expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
|
|
end
|
|
|
|
context 'when images are allowed' do
|
|
it 'preserves data-src for lazy images' do
|
|
object = create_object("![ImageTest](/uploads/test.png)")
|
|
image_url = "data-src=\".*/uploads/test.png\""
|
|
text = first_line_in_markdown(object, attribute, 150, project: project, allow_images: true)
|
|
|
|
expect(text).to match(image_url)
|
|
expect(text).to match('<a')
|
|
end
|
|
end
|
|
|
|
context 'when images are not allowed' do
|
|
it 'removes any images' do
|
|
object = create_object("![ImageTest](/uploads/test.png)")
|
|
text = first_line_in_markdown(object, attribute, 150, project: project)
|
|
|
|
expect(text).not_to match('<img')
|
|
expect(text).not_to match('<a')
|
|
end
|
|
end
|
|
|
|
context 'labels formatting' do
|
|
let(:label_title) { 'this should be ~label_1' }
|
|
|
|
def create_and_format_label(project)
|
|
create(:label, title: 'label_1', project: project)
|
|
object = create_object(label_title, project: project)
|
|
|
|
first_line_in_markdown(object, attribute, 150, project: project)
|
|
end
|
|
|
|
it 'preserves style attribute for a label that can be accessed by current_user' do
|
|
project = create(:project, :public)
|
|
|
|
expect(create_and_format_label(project)).to match(/span class=.*style=.*/)
|
|
end
|
|
|
|
it 'does not style a label that can not be accessed by current_user' do
|
|
project = create(:project, :private)
|
|
|
|
expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>")
|
|
end
|
|
end
|
|
|
|
it 'truncates Markdown properly' do
|
|
object = create_object("@#{user.username}, can you look at this?\nHello world\n")
|
|
actual = first_line_in_markdown(object, attribute, 100, project: project)
|
|
|
|
doc = Nokogiri::HTML.parse(actual)
|
|
|
|
# Make sure we didn't create invalid markup
|
|
expect(doc.errors).to be_empty
|
|
|
|
# Leading user link
|
|
expect(doc.css('a').length).to eq(1)
|
|
expect(doc.css('a')[0].attr('href')).to eq user_path(user)
|
|
expect(doc.css('a')[0].text).to eq "@#{user.username}"
|
|
|
|
expect(doc.content).to eq "@#{user.username}, can you look at this?..."
|
|
end
|
|
|
|
it 'truncates Markdown with emoji properly' do
|
|
object = create_object("foo :wink:\nbar :grinning:")
|
|
actual = first_line_in_markdown(object, attribute, 100, project: project)
|
|
|
|
doc = Nokogiri::HTML.parse(actual)
|
|
|
|
# Make sure we didn't create invalid markup
|
|
# But also account for the 2 errors caused by the unknown `gl-emoji` elements
|
|
expect(doc.errors.length).to eq(2)
|
|
|
|
expect(doc.css('gl-emoji').length).to eq(2)
|
|
expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
|
|
expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
|
|
|
|
expect(doc.content).to eq "foo 😉\nbar 😀"
|
|
end
|
|
end
|
|
|
|
context 'when the asked attribute can be redacted' do
|
|
include_examples 'common markdown examples' do
|
|
let(:attribute) { :note }
|
|
def create_object(title, project: project_base)
|
|
build(:note, note: title, project: project)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the asked attribute can not be redacted' do
|
|
include_examples 'common markdown examples' do
|
|
let(:attribute) { :body }
|
|
def create_object(title, project: project_base)
|
|
issue = build(:issue, title: title)
|
|
build(:todo, :done, project: project_base, author: user, target: issue)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#cross_project_reference' do
|
|
it 'shows the full MR reference' do
|
|
expect(helper.cross_project_reference(project, merge_request)).to include(project.full_path)
|
|
end
|
|
|
|
it 'shows the full issue reference' do
|
|
expect(helper.cross_project_reference(project, issue)).to include(project.full_path)
|
|
end
|
|
end
|
|
|
|
def urls
|
|
Gitlab::Routing.url_helpers
|
|
end
|
|
end
|