Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions packages/utils/src/media-embed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ describe('getEmbedInfo', () => {
expect(getEmbedInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual(expected)
expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ')).toEqual(expected)
expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ')).toEqual(expected)
expect(getEmbedInfo('https://www.youtube.com/watch?list=RD&v=dQw4w9WgXcQ&t=5')).toEqual(
expected
)
expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ?si=abc')).toEqual(expected)
expect(getEmbedInfo('https://youtu.be/dQw4w9WgXcQ/')).toEqual(expected)
expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0')).toEqual(expected)
expect(getEmbedInfo('https://www.youtube.com/embed/dQw4w9WgXcQ?v=notAnId')).toEqual(expected)
expect(getEmbedInfo('https://www.youtube.com/watch?v=short')).toBeNull()
})

it('maps Facebook and fb.watch video links to the video plugin', () => {
expect(getEmbedInfo('https://www.facebook.com/some.page/videos/1234567890')).toEqual({
url: 'https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2Fsome.page%2Fvideos%2F1234567890&show_text=false',
type: 'iframe',
})
expect(getEmbedInfo('https://fb.watch/abc123')?.type).toBe('iframe')
expect(getEmbedInfo('https://www.facebook.com/some.page/about')).toBeNull()
})

it('extracts the Giphy id from the trailing slug token', () => {
const expected = { url: 'https://giphy.com/embed/abc123', type: 'iframe', aspectRatio: '1/1' }
expect(getEmbedInfo('https://giphy.com/gifs/funny-cat-abc123')).toEqual(expected)
expect(getEmbedInfo('https://giphy.com/embed/abc123')).toEqual(expected)
})

it('maps Vimeo and Spotify URLs with their aspect ratios', () => {
Expand Down Expand Up @@ -38,13 +61,10 @@ describe('getEmbedInfo', () => {
})

it('only embeds when the parsed host belongs to the provider', () => {
// A provider domain in the path or as a subdomain prefix of an attacker host
// must not be treated as that provider.
expect(getEmbedInfo('https://evil.com/youtube.com/watch?v=dQw4w9WgXcQ')).toBeNull()
expect(getEmbedInfo('https://youtube.com.evil.com/watch?v=dQw4w9WgXcQ')).toBeNull()
expect(getEmbedInfo('https://evil.com/open.spotify.com/track/abc123')).toBeNull()
expect(getEmbedInfo('https://vimeo.com.evil.com/123456')).toBeNull()
// Legitimate subdomains of a provider still embed.
expect(getEmbedInfo('https://m.youtube.com/watch?v=dQw4w9WgXcQ')).toEqual({
url: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
type: 'iframe',
Expand All @@ -71,8 +91,6 @@ describe('getEmbedInfo', () => {
})

it('does not apply the Dropbox direct-link rewrite to look-alike hosts', () => {
// Look-alike hosts fall through to the generic video handler with their
// original (untrusted) host intact — never rewritten as if trusted Dropbox.
expect(getEmbedInfo('https://dropbox.com.evil.com/clip.mp4')?.url).not.toContain(
'dropboxusercontent.com'
)
Expand Down
32 changes: 18 additions & 14 deletions packages/utils/src/media-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ function toDropboxDirectVideoUrl(parsed: URL): string | null {
export function getEmbedInfo(url: string): EmbedInfo | null {
const parsed = parseUrl(url)
const host = parsed?.hostname.toLowerCase() ?? null
if (hostMatches(host, 'youtube.com', 'youtu.be')) {
const youtubeMatch = url.match(
/(?:youtube\.com\/watch\?(?:.*&)?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/
)
if (youtubeMatch) {
return { url: `https://www.youtube.com/embed/${youtubeMatch[1]}`, type: 'iframe' }
if (parsed && hostMatches(host, 'youtube.com', 'youtu.be')) {
const segments = parsed.pathname.split('/')
let id: string | null | undefined
if (hostMatches(host, 'youtu.be')) id = segments[1]
else if (segments[1] === 'embed') id = segments[2]
else id = parsed.searchParams.get('v')
if (id && /^[a-zA-Z0-9_-]{11}$/.test(id)) {
return { url: `https://www.youtube.com/embed/${id}`, type: 'iframe' }
}
}

Expand Down Expand Up @@ -209,10 +211,11 @@ export function getEmbedInfo(url: string): EmbedInfo | null {
}
}

if (hostMatches(host, 'facebook.com', 'fb.watch')) {
const facebookVideoMatch =
url.match(/facebook\.com\/.*\/videos\/(\d+)/) || url.match(/fb\.watch\/([a-zA-Z0-9_-]+)/)
if (facebookVideoMatch) {
if (parsed && hostMatches(host, 'facebook.com', 'fb.watch')) {
const isFacebookVideo = hostMatches(host, 'fb.watch')
? /^\/[a-zA-Z0-9_-]+/.test(parsed.pathname)
: /\/videos\/\d+/.test(parsed.pathname)
Comment thread
waleedlatif1 marked this conversation as resolved.
if (isFacebookVideo) {
return {
url: `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url)}&show_text=false`,
type: 'iframe',
Expand Down Expand Up @@ -320,10 +323,11 @@ export function getEmbedInfo(url: string): EmbedInfo | null {
}
}

if (hostMatches(host, 'giphy.com')) {
const giphyMatch = url.match(/giphy\.com\/(?:gifs|embed)\/(?:.*-)?([a-zA-Z0-9]+)/)
if (giphyMatch) {
return { url: `https://giphy.com/embed/${giphyMatch[1]}`, type: 'iframe', aspectRatio: '1/1' }
if (parsed && hostMatches(host, 'giphy.com')) {
const segment = parsed.pathname.match(/^\/(?:gifs|embed)\/([^/]+)/)?.[1]
const giphyId = segment?.split('-').pop()
if (giphyId && /^[a-zA-Z0-9]+$/.test(giphyId)) {
return { url: `https://giphy.com/embed/${giphyId}`, type: 'iframe', aspectRatio: '1/1' }
}
}

Expand Down
Loading