From aba8d9c75572cb5d7850deb14b14a953075838da Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 17 Jul 2026 05:11:38 +0800 Subject: [PATCH] ui: update pdf link processing --- .../backendlib/markdown-it-media.ts | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/ui-default/backendlib/markdown-it-media.ts b/packages/ui-default/backendlib/markdown-it-media.ts index 5ca23dbfd..b543624bc 100644 --- a/packages/ui-default/backendlib/markdown-it-media.ts +++ b/packages/ui-default/backendlib/markdown-it-media.ts @@ -3,7 +3,6 @@ /* eslint-disable regexp/no-useless-non-capturing-group */ /* eslint-disable regexp/optimal-quantifier-concatenation */ -import _ from 'lodash'; import type MarkdownIt from 'markdown-it'; import { v4 as uuid } from 'uuid'; @@ -77,13 +76,22 @@ function resourceUrl(service: string, src: string, url: string) { if (service === 'vimeo') return `https://player.vimeo.com/video/${src}`; if (service === 'vine') return `https://vine.co/v/${src}/embed/simple`; if (service === 'prezi') { - return `https://prezi.com/embed/${src}/?bgcolor=ffffff&lock_to_path=0&autoplay=0&autohide_ctrls=0&` - + 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&' + return `https://prezi.com/embed/${src}/?bgcolor=ffffff&lock_to_path=0&autoplay=0&autohide_ctrls=0&` + + 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&' + 'landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI'; } return src; } +function pdfUrlKind(src: string) { + const normalizedSrc = src.replace(/\s/g, '').replace(/\\/g, '/'); + if (/^data:application\/pdf(?:;[^,]*)?,/i.test(normalizedSrc)) return 'data'; + if (/^file:\/\//i.test(normalizedSrc)) return 'local'; + if (/^https?:\/\//i.test(normalizedSrc)) return 'external'; + if (normalizedSrc.startsWith('//') || /^[a-z][a-z\d+.-]*:/i.test(normalizedSrc)) return 'invalid'; + return 'local'; +} + declare module 'hydrooj' { interface ModuleInterfaces { richmedia: { @@ -95,21 +103,26 @@ declare module 'hydrooj' { export function Media(md: MarkdownIt, { pdfToolbar = false }: { pdfToolbar?: boolean } = {}) { const supported = ['youtube', 'vimeo', 'vine', 'prezi', 'bilibili', 'youku', 'msoffice']; md.renderer.rules.video = function tokenizeReturn(tokens, idx) { - let src = md.utils.escapeHtml(tokens[idx].attrGet('src')); + const src = tokens[idx].attrGet('src'); const service = tokens[idx].attrGet('service').replace(/[^A-Z0-9]/gi, '').toLowerCase(); if (Hydro?.module?.richmedia?.[service]) { const result = Hydro?.module?.richmedia[service].get(service, src, md); if (result) return result; } if (service === 'pdf') { - if (['file://', './', '../'].some((i) => src.startsWith(i)) || (src[1] === '/' && src[2] !== '/')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1'; + const kind = pdfUrlKind(src); // A response with has content-disposition header causes the browser to download the file automatically. // As we cannot control response header from external sites, we block embedding external PDFs. - else return `

Embedding an external PDF is no longer supported.

Download`; + if (kind === 'external') return `

Embedding an external PDF is no longer supported.

Download`; + if (kind === 'invalid') return '

Embedding this PDF URL is not supported.

'; + const fragmentPos = src.indexOf('#'); + const pdfUrl = fragmentPos === -1 ? src : src.slice(0, fragmentPos); + const fragment = fragmentPos === -1 ? '' : src.slice(fragmentPos); + const pdfSrc = kind === 'data' ? src : `${pdfUrl}${pdfUrl.includes('?') ? '&' : '?'}noDisposition=1${fragment}`; return `\ - - + + `; @@ -117,7 +130,7 @@ export function Media(md: MarkdownIt, { pdfToolbar = false }: { pdfToolbar?: boo if (['url', 'video'].includes(service)) { return `\ `; } @@ -125,7 +138,7 @@ export function Media(md: MarkdownIt, { pdfToolbar = false }: { pdfToolbar?: boo return `\ `; } return `
${md.utils.escapeHtml(src)}
`;