gitlab-org--gitlab-foss/spec/helpers/markup_helper_spec.rb
Oswaldo Ferreira 4daf3dc0db Avoid exposing unaccessible repo data upon GFM processing
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`.
2019-08-21 12:23:44 -03:00

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('&lt;h1&gt;test&lt;/h1&gt;')
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