require 'spec_helper' require 'nokogiri' module Gitlab describe Asciidoc do include FakeBlobHelpers before do allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) end context "without project" do let(:input) { 'ascii' } let(:context) { {} } let(:html) { 'H2O' } it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS, extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) expect(render(input, context)).to eq(html) end context "with asciidoc_opts" do it "merges the options with default ones" do expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS, extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) render(input, context) end end context "XSS" do links = { 'links' => { input: 'link:mylink"onmouseover="alert(1)[Click Here]', output: "
\n

Click Here

\n
" }, 'images' => { input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', output: "
\n

Alt text\" onerror=\"alert(7)

\n
" }, 'pre' => { input: '```mypre">', output: "
\n
\n
\">
\n
\n
" } } links.each do |name, data| it "does not convert dangerous #{name} into HTML" do expect(render(data[:input], context)).to include(data[:output]) end end end context 'external links' do it 'adds the `rel` attribute to the link' do output = render('link:https://google.com[Google]', context) expect(output).to include('rel="nofollow noreferrer noopener"') end end context 'LaTex code' do it 'adds class js-render-math to the output' do input = <<~MD :stem: latexmath [stem] ++++ \sqrt{4} = 2 ++++ another part [latexmath] ++++ \beta_x \gamma ++++ stem:[2+2] is 4 MD expect(render(input, context)).to include('
eta_x gamma
') expect(render(input, context)).to include('

2+2 is 4

') end end context 'outfilesuffix' do it 'defaults to adoc' do output = render("Inter-document reference <>", context) expect(output).to include("a href=\"README.adoc\"") end end end context 'with project' do let(:context) do { commit: commit, project: project, ref: ref, requested_path: requested_path } end let(:commit) { project.commit(ref) } let(:project) { create(:project, :repository) } let(:ref) { 'asciidoc' } let(:requested_path) { '/' } context 'include directive' do subject(:output) { render(input, context) } let(:input) { "Include this:\n\ninclude::#{include_path}[]" } before do current_file = requested_path current_file += 'README.adoc' if requested_path.end_with? '/' create_file(current_file, "= AsciiDoc\n") end context 'with path to non-existing file' do let(:include_path) { 'not-exists.adoc' } it 'renders Unresolved directive placeholder' do is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]") end end shared_examples :invalid_include do let(:include_path) { 'dk.png' } before do allow(project.repository).to receive(:blob_at).and_return(blob) end it 'does not read the blob' do expect(blob).not_to receive(:data) end it 'renders Unresolved directive placeholder' do is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]") end end context 'with path to a binary file' do let(:blob) { fake_blob(path: 'dk.png', binary: true) } include_examples :invalid_include end context 'with path to file in external storage' do let(:blob) { fake_blob(path: 'dk.png', lfs: true) } before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) project.update_attribute(:lfs_enabled, true) end include_examples :invalid_include end context 'with path to a textual file' do let(:include_path) { 'sample.adoc' } before do create_file(file_path, "Content from #{include_path}") end shared_examples :valid_include do [ ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'], ['sample.adoc', 'doc/api/sample.adoc', 'relative path'], ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'], ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'], ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories'] ].each do |include_path_, file_path_, desc| context "the file is specified by #{desc}" do let(:include_path) { include_path_ } let(:file_path) { file_path_ } it 'includes content of the file' do is_expected.to include('

Include this:

') is_expected.to include("

Content from #{include_path}

") end end end end context 'when requested path is a file in the repo' do let(:requested_path) { 'doc/api/README.adoc' } include_examples :valid_include context 'without a commit (only ref)' do let(:commit) { nil } include_examples :valid_include end end context 'when requested path is a directory in the repo' do let(:requested_path) { 'doc/api/' } include_examples :valid_include context 'without a commit (only ref)' do let(:commit) { nil } include_examples :valid_include end end end context 'recursive includes with relative paths' do let(:input) do <<~ADOC Source: requested file include::doc/README.adoc[] include::license.adoc[] ADOC end before do create_file 'doc/README.adoc', <<~ADOC Source: doc/README.adoc include::../license.adoc[] include::api/hello.adoc[] ADOC create_file 'license.adoc', <<~ADOC Source: license.adoc ADOC create_file 'doc/api/hello.adoc', <<~ADOC Source: doc/api/hello.adoc include::./common.adoc[] ADOC create_file 'doc/api/common.adoc', <<~ADOC Source: doc/api/common.adoc ADOC end it 'includes content of the included files recursively' do expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip Source: requested file Source: doc/README.adoc Source: license.adoc Source: doc/api/hello.adoc Source: doc/api/common.adoc Source: license.adoc ADOC end end def create_file(path, content) project.repository.create_file(project.creator, path, content, message: "Add #{path}", branch_name: 'asciidoc') end end end def render(*args) described_class.render(*args) end end end