33/* eslint-disable regexp/no-useless-non-capturing-group */
44/* eslint-disable regexp/optimal-quantifier-concatenation */
55
6- import _ from 'lodash' ;
76import type MarkdownIt from 'markdown-it' ;
87import { v4 as uuid } from 'uuid' ;
98
@@ -77,13 +76,22 @@ function resourceUrl(service: string, src: string, url: string) {
7776 if ( service === 'vimeo' ) return `https://player.vimeo.com/video/${ src } ` ;
7877 if ( service === 'vine' ) return `https://vine.co/v/${ src } /embed/simple` ;
7978 if ( service === 'prezi' ) {
80- return `https://prezi.com/embed/${ src } /?bgcolor=ffffff& lock_to_path=0& autoplay=0& autohide_ctrls=0& `
81- + 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0& '
79+ return `https://prezi.com/embed/${ src } /?bgcolor=ffffff&lock_to_path=0&autoplay=0&autohide_ctrls=0&`
80+ + 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&'
8281 + 'landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI' ;
8382 }
8483 return src ;
8584}
8685
86+ function pdfUrlKind ( src : string ) {
87+ const normalizedSrc = src . replace ( / \s / g, '' ) . replace ( / \\ / g, '/' ) ;
88+ if ( / ^ d a t a : a p p l i c a t i o n \/ p d f (?: ; [ ^ , ] * ) ? , / i. test ( normalizedSrc ) ) return 'data' ;
89+ if ( / ^ f i l e : \/ \/ / i. test ( normalizedSrc ) ) return 'local' ;
90+ if ( / ^ h t t p s ? : \/ \/ / i. test ( normalizedSrc ) ) return 'external' ;
91+ if ( normalizedSrc . startsWith ( '//' ) || / ^ [ a - z ] [ a - z \d + . - ] * : / i. test ( normalizedSrc ) ) return 'invalid' ;
92+ return 'local' ;
93+ }
94+
8795declare module 'hydrooj' {
8896 interface ModuleInterfaces {
8997 richmedia : {
@@ -95,37 +103,42 @@ declare module 'hydrooj' {
95103export function Media ( md : MarkdownIt , { pdfToolbar = false } : { pdfToolbar ?: boolean } = { } ) {
96104 const supported = [ 'youtube' , 'vimeo' , 'vine' , 'prezi' , 'bilibili' , 'youku' , 'msoffice' ] ;
97105 md . renderer . rules . video = function tokenizeReturn ( tokens , idx ) {
98- let src = md . utils . escapeHtml ( tokens [ idx ] . attrGet ( 'src' ) ) ;
106+ const src = tokens [ idx ] . attrGet ( 'src' ) ;
99107 const service = tokens [ idx ] . attrGet ( 'service' ) . replace ( / [ ^ A - Z 0 - 9 ] / gi, '' ) . toLowerCase ( ) ;
100108 if ( Hydro ?. module ?. richmedia ?. [ service ] ) {
101109 const result = Hydro ?. module ?. richmedia [ service ] . get ( service , src , md ) ;
102110 if ( result ) return result ;
103111 }
104112 if ( service === 'pdf' ) {
105- if ( [ 'file://' , './' , '../' ] . some ( ( i ) => src . startsWith ( i ) ) || ( src [ 1 ] === '/' && src [ 2 ] !== '/' ) ) src += src . includes ( '?' ) ? '&noDisposition=1' : '?noDisposition=1' ;
113+ const kind = pdfUrlKind ( src ) ;
106114 // A response with has content-disposition header causes the browser to download the file automatically.
107115 // As we cannot control response header from external sites, we block embedding external PDFs.
108- else return `<p>Embedding an external PDF is no longer supported.</p> <a href="${ _ . escape ( src ) } ">Download</a>` ;
116+ if ( kind === 'external' ) return `<p>Embedding an external PDF is no longer supported.</p> <a href="${ md . utils . escapeHtml ( src ) } ">Download</a>` ;
117+ if ( kind === 'invalid' ) return '<p>Embedding this PDF URL is not supported.</p>' ;
118+ const fragmentPos = src . indexOf ( '#' ) ;
119+ const pdfUrl = fragmentPos === - 1 ? src : src . slice ( 0 , fragmentPos ) ;
120+ const fragment = fragmentPos === - 1 ? '' : src . slice ( fragmentPos ) ;
121+ const pdfSrc = kind === 'data' ? src : `${ pdfUrl } ${ pdfUrl . includes ( '?' ) ? '&' : '?' } noDisposition=1${ fragment } ` ;
109122 return `\
110123 <object classid="clsid:${ uuid ( ) . toUpperCase ( ) } ">
111- <param name="SRC" value="${ src } ">
112- <embed type="application/pdf" width="100%" style="min-height:100vh;border:none;" fullscreen="yes" src="${ src } #toolbar=${ pdfToolbar ? '0' : '1' } &navpanes=0&view=FitH">
124+ <param name="SRC" value="${ md . utils . escapeHtml ( pdfSrc ) } ">
125+ <embed type="application/pdf" width="100%" style="min-height:100vh;border:none;" fullscreen="yes" src="${ md . utils . escapeHtml ( ` ${ pdfSrc } #toolbar=${ pdfToolbar ? '0' : '1' } &navpanes=0&view=FitH` ) } ">
113126 <noembed></noembed>
114127 </embed>
115128 </object>` ;
116129 }
117130 if ( [ 'url' , 'video' ] . includes ( service ) ) {
118131 return `\
119132 <video width="100%" controls>
120- <source src="${ src } " type="${ src . endsWith ( 'ogg' ) ? 'video/ogg' : 'video/mp4' } ">
133+ <source src="${ md . utils . escapeHtml ( src ) } " type="${ src . endsWith ( 'ogg' ) ? 'video/ogg' : 'video/mp4' } ">
121134 Your browser doesn't support video tag.
122135 </video>` ;
123136 }
124137 if ( supported . includes ( service ) ) {
125138 return `\
126139 <iframe class="embed-responsive-item ${ service } -player" type="text/html" \
127140 width="100%" style="min-height: 500px" ${ allowFullScreen } \
128- src="${ resourceUrl ( service , src , tokens [ idx ] . attrGet ( 'url' ) ) } "
141+ src="${ md . utils . escapeHtml ( resourceUrl ( service , src , tokens [ idx ] . attrGet ( 'url' ) ) ) } "
129142 scrolling="no" border="0" frameborder="no" framespacing="0"></iframe>` ;
130143 }
131144 return `<div data-${ service } >${ md . utils . escapeHtml ( src ) } </div>` ;
0 commit comments