Skip to content
Snippets Groups Projects
Commit 88def131 authored by Bartłomiej Górnicki's avatar Bartłomiej Górnicki
Browse files

feat: rich embed for Spotify links and iframes

parent 774a57f8
No related branches found
No related tags found
1 merge request!557Move hive renderer to internal packages
Showing
with 469 additions and 27 deletions
......@@ -8,8 +8,12 @@ Portable library that renders Hive posts and comments to string. It supports mar
Features:
- supports markdown and html
- sanitizes html and protects from XSS
- supports markdown and html
- sanitizes html and protects from XSS
- embeds images, videos, and other assets via links or iframes
- ensures links are safe to display and begins with `https://` protocol
- linkify #tags and @username mentions
- proxify images if needed and appropriate function is provided
**Credit**: this library is based on the code from condenser. It's aim is to allow other projects display Hive content the right way without porting the same code over and over.
......
......@@ -91,7 +91,79 @@ describe('DefaultRender', () => {
{
name: 'Should remove additional unsafe attributes from a tag',
raw: "<a fake='test'></a>",
expected: '<p><a class="hive-test"></a></p>'
expected: '<p><a class="hive-test external"></a></p>'
},
{
name: 'Spotify playlist link should be embedded correctly',
raw: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed/playlist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify track link should be embedded correctly',
raw: 'https://open.spotify.com/track/3Qm86XLflmIXVm1wcwkgDK',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed/track/3Qm86XLflmIXVm1wcwkgDK" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify album link should be embedded correctly',
raw: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed/album/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify episode link should be embedded correctly',
raw: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed-podcast/episode/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify show link should be embedded correctly',
raw: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed-podcast/show/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify artist link should be embedded correctly',
raw: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
expected:
'<p><div class="videoWrapper"><iframe src="https://open.spotify.com/embed/artist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>'
},
{
name: 'Spotify embed playlist via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed/playlist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/playlist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Spotify embed track via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed/track/3Qm86XLflmIXVm1wcwkgDK" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/track/3Qm86XLflmIXVm1wcwkgDK" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Spotify embed album via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed/album/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/album/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Spotify embed episode via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed-podcast/episode/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed-podcast/episode/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Spotify embed show via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed-podcast/show/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed-podcast/show/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Spotify embed artist via iframe should be embedded correctly',
raw: '<iframe src="https://open.spotify.com/embed/artist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/artist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
}
];
......
......@@ -2,7 +2,7 @@ import ow from 'ow';
import {Remarkable} from 'remarkable';
import {SecurityChecker} from '../../security/SecurityChecker';
import {AssetEmbedder} from './embedder/AssetEmbedder';
import {Localization, LocalizationOptions} from './LocalizationOptions';
import {Localization, LocalizationOptions} from './Localization';
import {PreliminarySanitizer} from './sanitization/PreliminarySanitizer';
import {TagTransformingSanitizer} from './sanitization/TagTransformingSanitizer';
......
......@@ -39,12 +39,7 @@ export class StaticConfig {
if (!m || m.length !== 2) {
return null;
}
return (
'https://w.soundcloud.com/player/?url=' +
m[1] +
'&auto_play=false&hide_related=false&show_comments=true' +
'&show_user=true&show_reposts=false&visual=true'
);
return `https://w.soundcloud.com/player/?url=${m[1]}&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true`;
}
},
{
......@@ -54,6 +49,12 @@ export class StaticConfig {
// <iframe src="https://player.twitch.tv/?channel=ninja" frameborder="0" allowfullscreen="true" scrolling="no" height="378" width="620">
return src;
}
},
{
re: /^https:\/\/open\.spotify\.com\/(embed|embed-podcast)\/(playlist|show|episode|album|track|artist)\/(.*)/i,
fn: (src: string) => {
return src;
}
}
],
noImageText: '(Image not shown due to low ratings)',
......
import ow from 'ow';
import {LocalizationOptions} from '../LocalizationOptions';
import {LocalizationOptions} from '../Localization';
import {Embedders} from './embedders/Embedders';
import {HtmlDOMParser} from './HtmlDOMParser';
import {VideoEmbedders} from './videoembedders/VideoEmbedders';
export class AssetEmbedder {
private options: AssetEmbedderOptions;
......@@ -23,7 +23,7 @@ export class AssetEmbedder {
width: this.options.width,
height: this.options.height
};
return VideoEmbedders.insertMarkedEmbedsToRenderedOutput(input, size);
return Embedders.insertMarkedEmbedsToRenderedOutput(input, size);
}
public static validate(o: AssetEmbedderOptions) {
......
......@@ -6,12 +6,12 @@ import * as xmldom from '@xmldom/xmldom';
import ChainedError from 'typescript-chained-error';
import {Log} from '../../../Log';
import {LinkSanitizer} from '../../../security/LinkSanitizer';
import {Localization, LocalizationOptions} from '../LocalizationOptions';
import {Localization, LocalizationOptions} from '../Localization';
import {AssetEmbedder, AssetEmbedderOptions} from './AssetEmbedder';
import {Embedders} from './embedders/Embedders';
import {YoutubeEmbedder} from './embedders/YoutubeEmbedder';
import {AccountNameValidator} from './utils/AccountNameValidator';
import linksRe, {any as linksAny} from './utils/Links';
import {VideoEmbedders} from './videoembedders/VideoEmbedders';
import {YoutubeEmbedder} from './videoembedders/YoutubeEmbedder';
export class HtmlDOMParser {
private options: AssetEmbedderOptions;
......@@ -146,6 +146,7 @@ export class HtmlDOMParser {
(child as any).parentNode.replaceChild(this.domParser.parseFromString(`<div class="videoWrapper">${html}</div>`), child);
}
// TODO this is youtube specific but should be executed for all iframes and embedders
private reportIframeLink(url: string) {
const yt = YoutubeEmbedder.getYoutubeMetadataFromLink(url);
if (yt) {
......@@ -185,7 +186,7 @@ export class HtmlDOMParser {
return;
}
const embedResp = VideoEmbedders.processTextNodeAndInsertEmbeds(child);
const embedResp = Embedders.processTextNodeAndInsertEmbeds(child);
embedResp.images.forEach((img) => this.state.images.add(img));
embedResp.links.forEach((link) => this.state.links.add(link));
......@@ -221,8 +222,7 @@ export class HtmlDOMParser {
}
this.state.links.add(sanitizedLink);
const out = `<a href="${this.normalizeUrl(ln)}">${sanitizedLink}</a>`;
return out;
return `<a href="${this.normalizeUrl(ln)}">${sanitizedLink}</a>`;
});
// hashtag
......
export abstract class AbstractVideoEmbedder {
public abstract markEmbedIfFound(textNode: HTMLObjectElement): {image?: string; link?: string} | undefined;
public abstract processEmbedIfRelevant(embedType: string, id: string, size: {width: number; height: number}, htmlElementKey: string): string | undefined;
export abstract class AbstractEmbedder {
public abstract type: string;
/**
* Sanitize the URL to prevent XSS attacks. It should return a URL that is safe to embed.
*/
// public abstract sanitizeIFrameUrl(url: string): string;
/**
* Get the metadata for the embed. This is used to generate the embed marker and to insert the embed into the rendered output.
*/
public abstract getEmbedMetadata(textNode: HTMLObjectElement): EmbedMetadata | undefined;
/**
* Process the embed if it is relevant to this embedder. If it is not relevant, return undefined.
*/
public abstract processEmbed(id: string, size: {width: number; height: number}): string;
public static getEmbedMarker(id: string, type: string) {
return `~~~ embed:${id} ${type} ~~~`;
}
public static insertAllEmbeds(embedders: AbstractVideoEmbedder[], input: string, size: {width: number; height: number}): string {
// In addition to inserting the youtube component, this allows
// react to compare separately preventing excessive re-rendering.
let idx = 0;
public static insertAllEmbeds(embedders: AbstractEmbedder[], input: string, size: {width: number; height: number}): string {
const sections = [];
// HtmlReady inserts ~~~ embed:${id} type ~~~
for (let section of input.split('~~~ embed:')) {
const match = section.match(/^([A-Za-z0-9?=_-]+) ([^ ]*) ~~~/);
const match = section.match(/^([A-Za-z0-9?/=_-]+) ([^ ]*) ~~~/);
if (match && match.length >= 3) {
const id = match[1];
const type = match[2];
for (const embedder of embedders) {
const resp = embedder.processEmbedIfRelevant(type, id, size, idx++ + '');
if (resp) {
sections.push(resp);
if (embedder.type == type) {
sections.push(embedder.processEmbed(id, size));
break;
}
}
section = section.substring(`${id} ${type} ~~~`.length);
// section = section.substring(`${id} ${type} ~~~`.length);
if (section === '') {
continue;
}
......@@ -36,3 +45,14 @@ export abstract class AbstractVideoEmbedder {
return sections.join('');
}
}
export interface EmbedMetadata {
/** The ID of the embed which will be used later on to convert it into rich embed */
id: string;
/** The URL from which the embed takes its source */
url: string;
/** Optional image to be used as a thumbnail */
image?: string;
/** Optional link detected */
link?: string;
}
import {AbstractVideoEmbedder} from './AbstractVideoEmbedder';
import {AbstractEmbedder} from './AbstractEmbedder';
import {SpotifyEmbedder} from './SpotifyEmbedder';
import {TwitchEmbedder} from './TwitchEmbedder';
import {VimeoEmbedder} from './VimeoEmbedder';
import {YoutubeEmbedder} from './YoutubeEmbedder';
export class VideoEmbedders {
public static LIST: AbstractVideoEmbedder[] = [
export class Embedders {
public static LIST: AbstractEmbedder[] = [
//
new YoutubeEmbedder(),
new VimeoEmbedder(),
new TwitchEmbedder()
new TwitchEmbedder(),
new SpotifyEmbedder()
];
public static processTextNodeAndInsertEmbeds(node: HTMLObjectElement): {links: string[]; images: string[]} {
const out: {links: string[]; images: string[]} = {links: [], images: []};
for (const embedder of VideoEmbedders.LIST) {
const markResult = embedder.markEmbedIfFound(node);
if (markResult) {
if (markResult.image) out.images.push(markResult.image);
if (markResult.link) out.links.push(markResult.link);
for (const embedder of Embedders.LIST) {
const metadata = embedder.getEmbedMetadata(node);
if (metadata) {
node.data = node.data.replace(metadata.url, AbstractEmbedder.getEmbedMarker(metadata.id, embedder.type));
if (metadata.image) out.images.push(metadata.image);
if (metadata.link) out.links.push(metadata.link);
}
}
return out;
}
public static insertMarkedEmbedsToRenderedOutput(input: string, size: {width: number; height: number}): string {
return AbstractVideoEmbedder.insertAllEmbeds(VideoEmbedders.LIST, input, size);
return AbstractEmbedder.insertAllEmbeds(Embedders.LIST, input, size);
}
}
import {expect} from 'chai';
import {JSDOM} from 'jsdom';
import {SpotifyEmbedder} from './SpotifyEmbedder';
describe('SpotifyEmbedder', () => {
[
{
description: 'should properly return metadata for spotify playlist',
input: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/playlist/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should properly return metadata for spotify show',
input: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed-podcast/show/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should properly return metadata for spotify episode',
input: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed-podcast/episode/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should properly return metadata for spotify album',
input: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/album/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should properly return metadata for spotify track',
input: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/track/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should properly return metadata for spotify artist',
input: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/artist/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk'
}
},
{
description: 'should return undefined for invalid input',
input: 'https://open.spotify.com/invalid/1zLvUhumbFIEdfxYQcgUxk',
expected: undefined
}
].forEach((test) => {
it(test.description, () => {
const embedder = new SpotifyEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = test.input;
const result = embedder.getEmbedMetadata(node);
expect(result).to.be.deep.equal(test.expected);
});
});
it('should properly process embed', () => {
const embedder = new SpotifyEmbedder();
const result = embedder.processEmbed('embed/playlist/1zLvUhumbFIEdfxYQcgUxk', {width: 300, height: 300});
const expected =
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/playlist/1zLvUhumbFIEdfxYQcgUxk" width="300" height="300" frameBorder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen ></iframe></div>';
expect(result).to.be.equal(expected);
});
});
import {Log} from '../../../../Log';
import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder';
interface SpotifyMetadata {
id: string;
url: string;
canonical: string;
}
export class SpotifyEmbedder extends AbstractEmbedder {
public type = 'spotify';
private static readonly regex = {
main: /https?:\/\/open.spotify.com\/(playlist|show|episode|album|track|artist)\/(.*)/i,
sanitize: /^https:\/\/open\.spotify\.com\/(embed|embed-podcast)\/(playlist|show|episode|album|track|artist)\/(.*)/i // TODO ??
};
private static extractMetadata(data: string): SpotifyMetadata | undefined {
if (!data) return undefined;
const m = data.match(SpotifyEmbedder.regex.main);
if (!m || m.length < 2) return undefined;
const embed = m[1] === 'show' || m[1] === 'episode' ? 'embed-podcast' : 'embed';
return {
id: `${embed}/${m[1]}/${m[2]}`,
url: m[0],
canonical: `https://open.spotify.com/${m[1]}/${m[2]}`
};
}
public getEmbedMetadata(child: HTMLObjectElement): EmbedMetadata | undefined {
try {
const metadata = SpotifyEmbedder.extractMetadata(child.data);
if (!metadata) {
return undefined;
}
return {
...metadata
};
} catch (error) {
Log.log().error(error);
}
return undefined;
}
public processEmbed(id: string, size: {width: number; height: number}): string {
const url = `https://open.spotify.com/${id}`;
return `<div class="videoWrapper"><iframe src="${url}" width="${size.width}" height="${size.height}" frameBorder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen ></iframe></div>`;
}
}
import {Log} from '../../../../Log';
import linksRe from '../utils/Links';
import {AbstractVideoEmbedder} from './AbstractVideoEmbedder';
import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder';
export class TwitchEmbedder extends AbstractVideoEmbedder {
private static TYPE = 'twitch';
export class TwitchEmbedder extends AbstractEmbedder {
public type = 'twitch';
public markEmbedIfFound(child: HTMLObjectElement) {
public getEmbedMetadata(child: HTMLObjectElement): EmbedMetadata | undefined {
try {
const data = child.data;
const twitch = this.twitchId(data);
......@@ -13,29 +13,18 @@ export class TwitchEmbedder extends AbstractVideoEmbedder {
return undefined;
}
const embedMarker = AbstractVideoEmbedder.getEmbedMarker(twitch.id, TwitchEmbedder.TYPE);
child.data = data.replace(twitch.url, embedMarker);
return {link: twitch.canonical};
return {
...twitch
};
} catch (error) {
Log.log().error(error);
}
return undefined;
}
public processEmbedIfRelevant(embedType: string, id: string, size: {width: number; height: number}, htmlElementKey: string): string | undefined {
if (embedType !== TwitchEmbedder.TYPE) return undefined;
public processEmbed(id: string, size: {width: number; height: number}): string {
const url = `https://player.twitch.tv/${id}`;
return `<div className="videoWrapper">
<iframe
key=${htmlElementKey}
src=${url}
width=${size.width}
height=${size.height}
rameBorder="0"
allowFullScreen
/>
</div>`;
return `<div class="videoWrapper"><iframe src=${url} width=${size.width} height=${size.height} frameBorder="0" allowFullScreen></iframe></div>`;
}
private twitchId(data: any) {
......
import {Log} from '../../../../Log';
import linksRe from '../utils/Links';
import {AbstractVideoEmbedder} from './AbstractVideoEmbedder';
import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder';
export class VimeoEmbedder extends AbstractVideoEmbedder {
private static TYPE = 'vimeo';
export class VimeoEmbedder extends AbstractEmbedder {
public type = 'vimeo';
public markEmbedIfFound(child: HTMLObjectElement) {
public getEmbedMetadata(child: HTMLObjectElement): EmbedMetadata | undefined {
try {
const data = child.data;
const vimeo = this.vimeoId(data);
if (!vimeo) {
return undefined;
}
const embedMarker = AbstractVideoEmbedder.getEmbedMarker(vimeo.id, VimeoEmbedder.TYPE);
child.data = data.replace(vimeo.url, embedMarker);
return {link: vimeo.canonical};
return {
...vimeo
};
} catch (error) {
Log.log().error(error);
}
return undefined;
}
public processEmbedIfRelevant(embedType: string, id: string, size: {width: number; height: number}, htmlElementKey: string): string | undefined {
if (embedType !== VimeoEmbedder.TYPE) return undefined;
public processEmbed(id: string, size: {width: number; height: number}): string {
const url = `https://player.vimeo.com/video/${id}`;
return `<div className="videoWrapper">
<iframe
key=${htmlElementKey}
src=${url}
width=${size.width}
height=${size.height}
frameBorder="0"
webkitallowfullscreen
mozallowfullscreen
allowFullScreen
/>
</div>`;
return `<div class="videoWrapper"><iframe src=${url} width=${size.width} height=${size.height} frameBorder="0" webkitallowfullscreen mozallowfullscreen allowFullScreen></iframe></div>`;
}
private vimeoId(data: string) {
......
import {Log} from '../../../../Log';
import linksRe from '../utils/Links';
import {AbstractVideoEmbedder} from './AbstractVideoEmbedder';
import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder';
export class YoutubeEmbedder extends AbstractVideoEmbedder {
/** @return {id, url} or <b>null</b> */
export class YoutubeEmbedder extends AbstractEmbedder {
public static getYoutubeMetadataFromLink(data: string): {id: string; url: string; thumbnail: string} | null {
if (!data) {
return null;
......@@ -28,38 +27,25 @@ export class YoutubeEmbedder extends AbstractVideoEmbedder {
};
}
private static TYPE = 'youtube';
public type = 'youtube';
public markEmbedIfFound(child: HTMLObjectElement) {
public getEmbedMetadata(child: HTMLObjectElement): EmbedMetadata | undefined {
try {
const data = child.data;
const yt = YoutubeEmbedder.getYoutubeMetadataFromLink(data);
if (!yt) {
const metadata = YoutubeEmbedder.getYoutubeMetadataFromLink(child.data);
if (!metadata) {
return undefined;
}
const embedMarker = AbstractVideoEmbedder.getEmbedMarker(yt.id, YoutubeEmbedder.TYPE);
child.data = data.replace(yt.url, embedMarker);
return {image: yt.thumbnail, link: yt.url};
return {
...metadata
};
} catch (error) {
Log.log().error(error);
}
return undefined;
}
public processEmbedIfRelevant(embedType: string, id: string, size: {width: number; height: number}): string | undefined {
if (embedType !== YoutubeEmbedder.TYPE) return undefined;
public processEmbed(id: string, size: {width: number; height: number}): string {
const ytUrl = `https://www.youtube.com/embed/${id}`;
return `<div class="videoWrapper"><iframe
width="${size.width}"
height="${size.height}"
src="${ytUrl}"
allowfullscreen="allowfullscreen"
webkitallowfullscreen="webkitallowfullscreen"
mozallowfullscreen="mozallowfullscreen"
frameborder="0"
></iframe></div>`;
return `<div class="videoWrapper"><iframe width="${size.width}" height="${size.height}" src="${ytUrl}" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div>`;
}
}
import {expect} from 'chai';
import {Localization} from '../../LocalizationOptions';
import {Localization} from '../../Localization';
import {AccountNameValidator} from './AccountNameValidator';
describe('AccountNameValidator', () => {
......
/**
* Based on: https://raw.githubusercontent.com/openhive-network/condenser/master/src/app/utils/ChainValidation.js
*/
import {LocalizationOptions} from '../../LocalizationOptions';
import {LocalizationOptions} from '../../Localization';
import BadActorList from './BadActorList';
export class AccountNameValidator {
......
......@@ -4,7 +4,7 @@
import ow from 'ow';
import sanitize from 'sanitize-html';
import {Log} from '../../../Log';
import {Localization, LocalizationOptions} from '../LocalizationOptions';
import {Localization, LocalizationOptions} from '../Localization';
import {StaticConfig} from '../StaticConfig';
export class TagTransformingSanitizer {
......@@ -50,8 +50,8 @@ export class TagTransformingSanitizer {
},
allowedSchemes: ['http', 'https', 'hive'],
transformTags: {
iframe: (tagName: string, attribs: sanitize.Attributes) => {
const srcAtty = attribs.src;
iframe: (tagName: string, attributes: sanitize.Attributes) => {
const srcAtty = attributes.src;
for (const item of StaticConfig.sanitization.iframeWhitelist) {
if (item.re.test(srcAtty)) {
const src = typeof item.fn === 'function' ? item.fn(srcAtty) : srcAtty;
......@@ -63,10 +63,8 @@ export class TagTransformingSanitizer {
attribs: {
frameborder: '0',
allowfullscreen: 'allowfullscreen',
// deprecated but required for vimeo : https://vimeo.com/forums/help/topic:278181
webkitallowfullscreen: 'webkitallowfullscreen',
mozallowfullscreen: 'mozallowfullscreen', // deprecated but required for vimeo
src,
width: this.options.iframeWidth + '',
......@@ -76,7 +74,7 @@ export class TagTransformingSanitizer {
return iframeToBeReturned;
}
}
Log.log().warn('Blocked, did not match iframe "src" white list urls:', tagName, attribs);
Log.log().warn('Blocked, did not match iframe "src" white list urls:', tagName, attributes);
this.sanitizationErrors.push('Invalid iframe URL: ' + srcAtty);
const retTag: sanitize.Tag = {tagName: 'div', text: `(Unsupported ${srcAtty})`, attribs: {}};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment