Commit a9fb9fdc authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add a timeline event pipeline filter to TimelineEvent

Changelog: added
EE: true
parent 0175304d
......@@ -6,8 +6,7 @@ module IncidentManagement
self.table_name = 'incident_management_timeline_events'
# TODO: Implement custom pipeline https://gitlab.com/gitlab-org/gitlab/-/issues/351214
cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true
cache_markdown_field :note, pipeline: :timeline_event, issuable_reference_expansion_enabled: true
belongs_to :project
belongs_to :author, class_name: 'User', foreign_key: :author_id
......
# frozen_string_literal: true
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
module Banzai
module Filter
# HTML filter that wraps links around inline images and replaces image with a link.
class ImageAttachmentLinkFilter < HTML::Pipeline::Filter
# Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image alternative text as a child
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
link = doc.document.create_element(
'a',
class: 'with-attachment-icon',
href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
)
# make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img['alt'] || img['data-src'] || img['src']
img.replace(link)
end
doc
end
end
end
end
# frozen_string_literal: true
module Banzai
module Pipeline
class TimelineEventPipeline < BasePipeline
ALLOWLIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
elements: %w(p b i strong em pre code a img)
)
def self.filters
@filters ||= FilterArray[
Filter::MarkdownFilter,
Filter::EmojiFilter,
Filter::ExternalLinkFilter,
Filter::ImageAttachmentLinkFilter,
Filter::SanitizationFilter,
*reference_filters
]
end
def self.reference_filters
[
Filter::References::UserReferenceFilter,
Filter::References::IssueReferenceFilter,
Filter::References::ExternalIssueReferenceFilter,
Filter::References::MergeRequestReferenceFilter,
Filter::References::SnippetReferenceFilter,
Filter::References::CommitRangeReferenceFilter,
Filter::References::CommitReferenceFilter,
Filter::References::AlertReferenceFilter,
Filter::References::FeatureFlagReferenceFilter
]
end
def self.transform_context(context)
Filter::AssetProxyFilter.transform_context(context).merge(
only_path: true,
no_sourcepos: true,
allowlist: ALLOWLIST
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::ImageAttachmentLinkFilter do
include FilterSpecHelper
let(:path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
def image(path, alt: nil)
alt_tag = alt ? %Q{alt="#{alt}"} : ""
%(<img src="#{path}" #{alt_tag} />)
end
it 'replaces the image with link to image src', :aggregate_failures do
doc = filter(image(path))
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'uses image alt as a link text', :aggregate_failures do
doc = filter(image(path, alt: 'My image'))
expect(doc.to_html).to match(%r{^<a[^>]*>My image</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'adds attachment icon class to the link' do
doc = filter(image(path))
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
end
it 'does not wrap a duplicate link' do
doc = filter(%Q(<a href="/whatever">#{image(path)}</a>))
expect(doc.to_html).to match(%r{^<a href="/whatever"><img[^>]*></a>$})
end
it 'works with external images' do
external_path = 'https://i.imgur.com/DfssX9C.jpg'
doc = filter(image(external_path))
expect(doc.at_css('a')['href']).to eq(external_path)
end
it 'works with inline images' do
doc = filter(%Q(<p>test #{image(path)} inline</p>))
expect(doc.to_html).to match(%r{^<p>test <a[^>]*>#{path}</a> inline</p>$})
end
it 'keep the data-canonical-src' do
data_canonical_src = "http://example.com/test.png"
doc = filter(%Q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="#{data_canonical_src}" />))
expect(doc.at_css('a')['data-canonical-src']).to eq(data_canonical_src)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Pipeline::TimelineEventPipeline do
let_it_be(:project) { create(:project) }
describe '.reference_filters' do
it 'contains required reference filters' do
expect(described_class.reference_filters).to contain_exactly(
Banzai::Filter::References::UserReferenceFilter,
Banzai::Filter::References::IssueReferenceFilter,
Banzai::Filter::References::ExternalIssueReferenceFilter,
Banzai::Filter::References::MergeRequestReferenceFilter,
Banzai::Filter::References::SnippetReferenceFilter,
Banzai::Filter::References::CommitRangeReferenceFilter,
Banzai::Filter::References::CommitReferenceFilter,
Banzai::Filter::References::AlertReferenceFilter,
Banzai::Filter::References::FeatureFlagReferenceFilter
)
end
end
describe '.to_html' do
subject(:output) { described_class.to_html(markdown, project: project) }
context 'when markdown contains font style transformations' do
let(:markdown) { '**bold** _italic_ `code`'}
it { is_expected.to eq('<p><strong>bold</strong> <em>italic</em> <code>code</code></p>') }
end
context 'when markdown contains not allowed HTML tags' do
let(:markdown) { '<div>div</div><h1>h1</h1>'}
it 'filters out not allowed tags' do
is_expected.to eq(' div h1 ')
end
end
context 'when markdown contains links' do
let(:markdown) { '[GitLab](https://gitlab.com)' }
it { is_expected.to eq(%q(<p><a href="https://gitlab.com" target="_blank">GitLab</a></p>)) }
end
context 'when markdown contains images' do
let(:markdown) { '![Name](/path/to/image.png)' }
it 'replaces image with a link to the image' do
is_expected.to eq(%q{<p><a class="with-attachment-icon" href="/path/to/image.png" target="_blank">Name</a></p>})
end
end
context 'when markdown contains emojis' do
let(:markdown) { ':+1:👍'}
it { is_expected.to eq('<p>👍👍</p>') }
end
context 'when markdown contains a reference to an issue' do
let!(:issue) { create(:issue, project: project) }
let(:markdown) { "issue ##{issue.iid}" }
it 'contains a link to the issue' do
is_expected.to match(%r(<p>issue <a\shref="[\w\/]+-\/issues\/#{issue.iid}".*>##{issue.iid}<\/a><\/p>))
end
end
context 'when markdown contains a reference to a merge request' do
let!(:mr) { create(:merge_request, source_project: project, target_project: project) }
let(:markdown) { "MR !#{mr.iid}" }
it 'contains a link to the merge request' do
is_expected.to match(%r(<p>MR <a\shref="[\w\/]+-\/merge_requests\/#{mr.iid}".*>!#{mr.iid}<\/a><\/p>))
end
end
end
end
......@@ -41,8 +41,10 @@ RSpec.describe IncidentManagement::TimelineEvent do
end
describe '#cache_markdown_field' do
let(:note) { '<p>some html</p>' }
let(:expected_note_html) { '<p dir="auto">some html</p>' }
let(:note) { 'note **bold** _italic_ `code` ![image](/path/img.png) :+1:👍' }
let(:expected_note_html) do
'<p>note <strong>bold</strong> <em>italic</em> <code>code</code> <a class="with-attachment-icon" href="/path/img.png" target="_blank">image</a> 👍👍</p>'
end
before do
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_call_original
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment