Commit ef5e4f47 authored by Enrique Alcantara's avatar Enrique Alcantara Committed by jerasmus

Display youtube videos in SSE

Customize SSE HTML sanitizer to allow displaying
youtube videos referenced with iframes.
parent c33201cc
......@@ -4,6 +4,8 @@ export const CUSTOM_EVENTS = {
openAddImageModal: 'gl_openAddImageModal',
};
export const ALLOWED_VIDEO_ORIGINS = ['https://www.youtube.com'];
/* eslint-disable @gitlab/require-i18n-strings */
export const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
......
......@@ -4,6 +4,7 @@ import ToolbarItem from '../toolbar_item.vue';
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
import { TOOLBAR_ITEM_CONFIGS } from '../constants';
import sanitizeHTML from './sanitize_html';
const buildWrapper = propsData => {
const instance = new Vue({
......@@ -62,5 +63,6 @@ export const getEditorOptions = externalOptions => {
return defaults({
customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)),
customHTMLSanitizer: html => sanitizeHTML(html),
});
};
import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
const canRender = ({ type }) => {
return type === 'htmlBlock';
const isVideoFrame = html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const { children: { length } } = doc;
const iframe = doc.querySelector('iframe');
const origin = iframe && getURLOrigin(iframe.getAttribute('src'));
return length === 1 && ALLOWED_VIDEO_ORIGINS.includes(origin);
};
const canRender = ({ type, literal }) => {
return type === 'htmlBlock' && !isVideoFrame(literal);
};
const render = node => buildUneditableHtmlAsTextTokens(node);
......
import createSanitizer from 'dompurify';
import { ALLOWED_VIDEO_ORIGINS } from '../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
const sanitizer = createSanitizer(window);
const ADD_TAGS = ['iframe'];
sanitizer.addHook('uponSanitizeElement', node => {
if (node.tagName !== 'IFRAME') {
return;
}
const origin = getURLOrigin(node.getAttribute('src'));
if (!ALLOWED_VIDEO_ORIGINS.includes(origin)) {
node.remove();
}
});
const sanitize = content => sanitizer.sanitize(content, { ADD_TAGS });
export default sanitize;
......@@ -39,6 +39,10 @@ Below this line is a codeblock of the same HTML that should be ignored and prese
<p>Some paragraph...</p>
</div>
\`\`\`
Below this line is a iframe that should be ignored and preserved
<iframe></iframe>
`;
const sourceTemplated = `Below this line is a simple ERB (single-line erb block) example.
......@@ -87,6 +91,10 @@ Below this line is a codeblock of the same HTML that should be ignored and prese
<p>Some paragraph...</p>
</div>
\`\`\`
Below this line is a iframe that should be ignored and preserved
<iframe></iframe>
`;
it.each`
......
......@@ -9,9 +9,11 @@ import {
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html';
jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer');
jest.mock('~/vue_shared/components/rich_content_editor/services/sanitize_html');
describe('Editor Service', () => {
let mockInstance;
......@@ -143,5 +145,14 @@ describe('Editor Service', () => {
getEditorOptions(externalOptions);
expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers);
});
it('uses the internal sanitizeHTML service for HTML sanitization', () => {
const options = getEditorOptions();
const html = '<div></div>';
options.customHTMLSanitizer(html);
expect(sanitizeHTML).toHaveBeenCalledWith(html);
});
});
});
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { normalTextNode } from './mock_data';
describe('rich_content_editor/services/renderers/render_html_block', () => {
const htmlBlockNode = {
literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
type: 'htmlBlock',
};
const htmlBlockNode = {
firstChild: null,
literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
type: 'htmlBlock',
};
describe('Render HTML renderer', () => {
describe('canRender', () => {
it('should return true when the argument is an html block', () => {
expect(renderer.canRender(htmlBlockNode)).toBe(true);
});
it('should return false when the argument is not an html block', () => {
expect(renderer.canRender(normalTextNode)).toBe(false);
it.each`
input | result
${htmlBlockNode} | ${true}
${{ literal: '<iframe></iframe>', type: 'htmlBlock' }} | ${true}
${{ literal: '<iframe src="https://www.youtube.com"></iframe>', type: 'htmlBlock' }} | ${false}
${{ literal: '<iframe></iframe>', type: 'text' }} | ${false}
`('returns $result when input=$input', ({ input, result }) => {
expect(renderer.canRender(input)).toBe(result);
});
});
......
import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html';
describe('rich_content_editor/services/sanitize_html', () => {
it.each`
input | result
${'<iframe src="https://www.youtube.com"></iframe>'} | ${'<iframe src="https://www.youtube.com"></iframe>'}
${'<iframe src="https://gitlab.com"></iframe>'} | ${''}
`('removes iframes if the iframe source origin is not allowed', ({ input, result }) => {
expect(sanitizeHTML(input)).toBe(result);
});
});
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