Commit 2e99ec23 authored by Quoc Huy Nguyen Dinh's avatar Quoc Huy Nguyen Dinh
Browse files

Closes #3643 - Refactor support for 3rd party embedded player

parent 9ffb80c5
......@@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Component } from 'react';
import Remarkable from 'remarkable';
import YoutubePreview from 'app/components/elements/YoutubePreview';
import sanitizeConfig, { noImageText } from 'app/utils/SanitizeConfig';
import sanitize from 'sanitize-html';
import HtmlReady from 'shared/HtmlReady';
import tt from 'counterpart';
import { generateMd as EmbeddedPlayerGenerateMd } from 'app/components/modules/EmbeddedPlayers';
const remarkable = new Remarkable({
html: true, // remarkable renders first then sanitize runs...
......@@ -143,85 +143,17 @@ class MarkdownViewer extends Component {
// HtmlReady inserts ~~~ embed:${id} type ~~~
for (let section of cleanText.split('~~~ embed:')) {
const match = section.match(
/^([A-Za-z0-9\?\=\_\-\/\.]+) (youtube|vimeo|twitch|dtube)\s?(\d+)? ~~~/
);
if (match && match.length >= 3) {
const id = match[1];
const type = match[2];
const startTime = match[3] ? parseInt(match[3]) : 0;
const w = large ? 640 : 480,
h = large ? 360 : 270;
if (type === 'youtube') {
sections.push(
<YoutubePreview
key={idx++}
width={w}
height={h}
youTubeId={id}
startTime={startTime}
frameBorder="0"
allowFullScreen="true"
/>
);
} else if (type === 'vimeo') {
const url = `https://player.vimeo.com/video/${id}#t=${
startTime
}s`;
sections.push(
<div className="videoWrapper">
<iframe
key={idx++}
src={url}
width={w}
height={h}
frameBorder="0"
webkitallowfullscreen
mozallowfullscreen
allowFullScreen
/>
</div>
);
} else if (type === 'twitch') {
const url = `https://player.twitch.tv/${id}`;
sections.push(
<div className="videoWrapper">
<iframe
key={idx++}
src={url}
width={w}
height={h}
frameBorder="0"
allowFullScreen
/>
</div>
);
} else if (type === 'dtube') {
const url = `https://emb.d.tube/#!/${id}`;
sections.push(
<div className="videoWrapper">
<iframe
key={idx++}
src={url}
width={w}
height={h}
frameBorder="0"
allowFullScreen
/>
</div>
);
} else {
console.error('MarkdownViewer unknown embed type', type);
}
if (match[3]) {
section = section.substring(
`${id} ${type} ${startTime} ~~~`.length
);
} else {
section = section.substring(`${id} ${type} ~~~`.length);
const embedMd = EmbeddedPlayerGenerateMd(section, idx++, large);
if (embedMd) {
const { section: newSection, markdown } = embedMd;
section = newSection;
sections.push(markdown);
if (section === '') {
continue;
}
if (section === '') continue;
}
sections.push(
<div
key={idx++}
......
import React from 'react';
const regex = {
sanitize: /^https:\/\/emb.d.tube\/\#\!\/([a-zA-Z0-9\-\.\/]+)$/,
main: /https:\/\/(?:emb\.)?(?:d.tube\/\#\!\/(?:v\/)?)([a-zA-Z0-9\-\.\/]*)/,
contentId: /(?:d\.tube\/#!\/(?:v\/)?([a-zA-Z0-9\-\.\/]*))+/,
};
export default regex;
export function genIframeMd(idx, dtubeId, w, h) {
const url = `https://emb.d.tube/#!/${dtubeId}`;
return (
<div className="videoWrapper">
<iframe
title="DTube embedded player"
key={idx}
src={url}
width={w}
height={h}
frameBorder="0"
allowFullScreen
/>
</div>
);
}
// <iframe title="DTube embedded player" src="https://emb.d.tube/#!/lemwong/QmQqxBCkoVusMRwP6D9oBMRQdASFzABdKQxE7xLysfmsR6" width="640" height="360" frameborder="0" allowfullscreen=""></iframe>
export function validateIframeUrl(url) {
const match = url.match(regex.sanitize);
if (match) {
return url;
}
return false;
}
export function normalizeEmbedUrl(url) {
const match = url.match(regex.contentId);
if (match && match.length >= 2) {
return `https://emb.d.tube/#!/${match[1]}`;
}
return false;
}
function extractContentId(data) {
if (!data) return null;
const m = data.match(regex.main);
if (!m || m.length < 2) return null;
return {
id: m[1],
url: m[0],
canonical: `https://emb.d.tube/#!/${m[1]}`,
};
}
export function embedNode(child, links /*images*/) {
try {
const data = child.data;
const dtube = extractContentId(data);
if (!dtube) return child;
child.data = data.replace(dtube.url, `~~~ embed:${dtube.id} dtube ~~~`);
if (links) links.add(dtube.canonical);
} catch (error) {
console.log(error);
}
return child;
}
import React from 'react';
import {
genIframeMd as genDtubeIframeMd,
validateIframeUrl as validateDtubeIframeUrl,
normalizeEmbedUrl as normalizeDtubeEmbedUrl,
embedNode as embedDtubeNode,
} from 'app/components/modules/EmbeddedPlayers/dtube';
import {
genIframeMd as genTwitchIframeMd,
validateIframeUrl as validateTwitchIframeUrl,
normalizeEmbedUrl as normalizeTwitchEmbedUrl,
embedNode as embedTwitchNode,
} from 'app/components/modules/EmbeddedPlayers/twitch';
import { validateIframeUrl as validateSoundcloudIframeUrl } from 'app/components/modules/EmbeddedPlayers/soundcloud';
import {
genIframeMd as genYoutubeIframeMd,
validateIframeUrl as validateYoutubeIframeUrl,
normalizeEmbedUrl as normalizeYoutubeEmbedUrl,
embedNode as embedYoutubeNode,
} from 'app/components/modules/EmbeddedPlayers/youtube';
import {
genIframeMd as genVimeoIframeMd,
validateIframeUrl as validateVimeoIframeUrl,
normalizeEmbedUrl as normalizeVimeoEmbedUrl,
embedNode as embedVimeoNode,
} from 'app/components/modules/EmbeddedPlayers/vimeo';
const supportedProviders = [
{
id: 'dtube',
validateIframeUrlFn: validateDtubeIframeUrl,
normalizeEmbedUrlFn: normalizeDtubeEmbedUrl,
embedNodeFn: embedDtubeNode,
genIframeMdFn: genDtubeIframeMd,
},
{
id: 'twitch',
validateIframeUrlFn: validateTwitchIframeUrl,
normalizeEmbedUrlFn: normalizeTwitchEmbedUrl,
embedNodeFn: embedTwitchNode,
genIframeMdFn: embedTwitchNode,
},
{
id: 'soundcloud',
validateIframeUrlFn: validateSoundcloudIframeUrl,
normalizeEmbedUrlFn: null,
embedNodeFn: null,
genIframeMdFn: null,
},
{
id: 'youtube',
validateIframeUrlFn: validateYoutubeIframeUrl,
normalizeEmbedUrlFn: normalizeYoutubeEmbedUrl,
embedNodeFn: embedYoutubeNode,
genIframeMdFn: genYoutubeIframeMd,
},
{
id: 'vimeo',
validateIframeUrlFn: validateVimeoIframeUrl,
normalizeEmbedUrlFn: normalizeVimeoEmbedUrl,
embedNodeFn: embedVimeoNode,
genIframeMdFn: genVimeoIframeMd,
},
];
export default supportedProviders;
export function validateIframeUrl(url) {
for (let pi = 0; pi < supportedProviders.length; pi += 1) {
const provider = supportedProviders[pi];
const validIframeUrl = provider.validateIframeUrlFn(url);
if (validIframeUrl !== false) {
console.log(`Found a valid ${provider.id} iframe URL`);
return validIframeUrl;
}
}
return false;
}
export function normalizeEmbedUrl(url) {
for (let pi = 0; pi < supportedProviders.length; pi += 1) {
const provider = supportedProviders[pi];
if (typeof provider.normalizeEmbedUrlFn === 'function') {
const validEmbedUrl = provider.normalizeEmbedUrlFn(url);
if (validEmbedUrl !== false) {
console.log(`Found a valid ${provider.id} embedded URL`);
return validEmbedUrl;
}
}
}
return false;
}
export function embedNode(child, links, images) {
for (let pi = 0; pi < supportedProviders.length; pi += 1) {
const provider = supportedProviders[pi];
if (typeof provider.embedNodeFn === 'function') {
child = provider.embedNodeFn(child, links, images);
}
}
return child;
}
function getProviderById(id) {
for (let pi = 0; pi < supportedProviders.length; pi += 1) {
const provider = supportedProviders[pi];
if (provider.id === id) {
return provider;
}
}
return null;
}
function getProviderIds() {
return supportedProviders.map(o => {
return o.id;
});
}
export function generateMd(section, idx, large) {
let markdown = null;
const supportedProvidersIds = getProviderIds();
const regex = new RegExp(
`^([A-Za-z0-9\\?\\=\\_\\-\\/\\.]+) (${supportedProvidersIds.join(
'|'
)})\\s?(\\d+)? ~~~`
);
const match = section.match(regex);
if (match && match.length >= 3) {
const id = match[1];
const type = match[2];
const startTime = match[3] ? parseInt(match[3]) : 0;
const w = large ? 640 : 480,
h = large ? 360 : 270;
const provider = getProviderById(type);
if (provider) {
markdown = provider.genIframeMdFn(idx, id, w, h, startTime);
} else {
console.error('MarkdownViewer unknown embed type', type);
}
if (match[3]) {
section = section.substring(
`${id} ${type} ${startTime} ~~~`.length
);
} else {
section = section.substring(`${id} ${type} ~~~`.length);
}
return {
section,
markdown,
};
}
return null;
}
import React from 'react';
const regex = {
sanitize: /^https:\/\/w.soundcloud.com\/player\/.*?url=(.+?)&.*/i,
};
export default regex;
// <iframe width="100%" height="450" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/257659076&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;visual=true"></iframe>
export function validateIframeUrl(url) {
const match = url.match(regex.sanitize);
if (!match || match.length !== 2) {
return false;
}
return `https://w.soundcloud.com/player/?url=${
match[1]
}&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true`;
}
import React from 'react';
const regex = {
sanitize: /^(https?:)?\/\/player.twitch.tv\/.*/i,
main: /https?:\/\/(?:www.)?twitch.tv\/(?:(videos)\/)?([a-zA-Z0-9][\w]{3,24})/i,
};
export default regex;
// <iframe src="https://player.twitch.tv/?channel=tfue" frameborder="0" allowfullscreen="true" scrolling="no" height="378" width="620"></iframe>
export function validateIframeUrl(url) {
const match = url.match(regex.sanitize);
if (match) {
return url;
}
return false;
}
export function normalizeEmbedUrl(url) {
const match = url.match(regex.main);
if (match && match.length >= 3) {
if (match[1] === undefined) {
return `https://player.twitch.tv/?autoplay=false&channel=${
match[2]
}`;
}
return `https://player.twitch.tv/?autoplay=false&video=${match[1]}`;
}
return false;
}
function extractContentId(data) {
if (!data) return null;
const m = data.match(regex.main);
if (!m || m.length < 3) return null;
return {
id: m[1] === `videos` ? `?video=${m[2]}` : `?channel=${m[2]}`,
url: m[0],
canonical:
m[1] === `videos`
? `https://player.twitch.tv/?video=${m[2]}`
: `https://player.twitch.tv/?channel=${m[2]}`,
};
}
export function embedNode(child, links /*images*/) {
try {
const data = child.data;
const twitch = extractContentId(data);
if (!twitch) return child;
child.data = data.replace(
twitch.url,
`~~~ embed:${twitch.id} twitch ~~~`
);
if (links) links.add(twitch.canonical);
} catch (error) {
console.error(error);
}
return child;
}
export function genIframeMd(idx, id, w, h) {
const url = `https://player.twitch.tv/${id}`;
return (
<div className="videoWrapper" key={idx}>
<iframe
title="Twitch embedded player"
src={url}
width={w}
height={h}
frameBorder="0"
allowFullScreen
/>
</div>
);
}
import React from 'react';
const regex = {
sanitize: /^(https?:)?\/\/player.vimeo.com\/video\/([0-9]*)/i,
main: /https?:\/\/(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)\/?(#t=((\d+)s?))?\/?/,
contentId: /(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)/,
};
export default regex;
// <iframe src="https://player.vimeo.com/video/179213493" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
export function validateIframeUrl(url) {
const match = url.match(regex.sanitize);
if (!match || match.length !== 3) {
return false;
}
return 'https://player.vimeo.com/video/' + match[2];
}
export function normalizeEmbedUrl(url) {
const match = url.match(regex.contentId);
if (match && match.length >= 2) {
return `https://player.vimeo.com/video/${match[1]}`;
}
return false;
}
function extractContentId(data) {
if (!data) return null;
const m = data.match(regex.main);
if (!m || m.length < 2) return null;
const startTime = m.input.match(/t=(\d+)s?/);
return {
id: m[1],
url: m[0],
startTime: startTime ? startTime[1] : 0,
canonical: `https://player.vimeo.com/video/${m[1]}`,
// thumbnail: requires a callback - http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo
};
}
export function embedNode(child, links /*images*/) {
try {
const data = child.data;
const vimeo = extractContentId(data);
if (!vimeo) return child;
const vimeoRegex = new RegExp(`${vimeo.url}(#t=${vimeo.startTime}s?)?`);
if (vimeo.startTime > 0) {
child.data = data.replace(
vimeoRegex,
`~~~ embed:${vimeo.id} vimeo ${vimeo.startTime} ~~~`
);
} else {
child.data = data.replace(
vimeoRegex,
`~~~ embed:${vimeo.id} vimeo ~~~`
);
}
if (links) links.add(vimeo.canonical);
// if(images) images.add(vimeo.thumbnail) // not available
} catch (error) {
console.log(error);
}
return child;
}
export function genIframeMd(idx, id, w, h, startTime) {
const url = `https://player.vimeo.com/video/${id}#t=${startTime}s`;
return (
<div className="videoWrapper" key={idx}>
<iframe
title="Vimeo embedded player"
src={url}
width={w}
height={h}
frameBorder="0"
webkitallowfullscreen
mozallowfullscreen
allowFullScreen
/>
</div>
);
}
import React from 'react';
import YoutubePreview from 'app/components/elements/YoutubePreview';
const regex = {
sanitize: /^(https?:)?\/\/www.youtube.com\/embed\/.*/i,
//main: new RegExp(urlSet({ domain: '(?:(?:.*.)?youtube.com|youtu.be)' }), flags),
main: /(?:(?:.*.)?youtube.com|youtu.be)\/watch\?v=.*/i,
contentId: /(?:(?:youtube.com\/watch\?v=)|(?:youtu.be\/)|(?:youtube.com\/embed\/))([A-Za-z0-9\_\-]+)/i,
};
export default regex;
// <iframe width="560" height="315" src="https://www.youtube.com/embed/KOnk7Nbqkhs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
export function validateIframeUrl(url) {
const match = url.match(regex.sanitize);
if (match) {
// strip query string (yt: autoplay=1,controls=0,showinfo=0, etc)
return url.replace(/\?.+$/, '');
}
return false;
}
export function normalizeEmbedUrl(url) {
const match = url.match(regex.contentId);