@@ -157,10 +157,24 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
157157 targetAttr = 'src' ;
158158 url = this . url ?? this . src ?? '' ; // eslint-disable-line @typescript-eslint/no-deprecated
159159
160+ await this . handleSrcset ( this . siteId ) ;
161+
160162 } else if ( tagName === 'AUDIO' || tagName === 'VIDEO' || tagName === 'SOURCE' || tagName === 'TRACK' ) {
161163 targetAttr = 'src' ;
162164 url = this . url ?? this . src ?? '' ; // eslint-disable-line @typescript-eslint/no-deprecated
163165
166+ if ( tagName === 'SOURCE' ) {
167+ await this . handleSrcset ( this . siteId ) ;
168+
169+ // For elements that only use srcset, url can be empty.
170+ // Resolve early when tagName === 'SOURCE' and there is no src URL.
171+ if ( ! url ) {
172+ this . onReadyPromise . resolve ( ) ;
173+
174+ return ;
175+ }
176+ }
177+
164178 if ( tagName === 'VIDEO' && this . posterUrl ) {
165179 // Handle poster.
166180
@@ -201,28 +215,19 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
201215 }
202216
203217 const site = await CorePromiseUtils . ignoreErrors ( CoreSites . getSite ( this . siteId ) ) ;
204- const isSiteFile = site ?. isSitePluginFileUrl ( url ) ;
205-
206- // Try to convert the URL to absolute. This will only work for URLs relative to the site URL, it won't work for
207- // URLs relative to a subpath (e.g. relative to the course page URL).
208- url = site && url ? CoreUrl . toAbsoluteURL ( site . getURL ( ) , url ) : url ;
209-
210- if ( ! url || ! url . match ( / ^ h t t p s ? : \/ \/ / i) || CoreUrl . isLocalFileUrl ( url ) ||
211- ( tagName === 'A' && ! ( isSiteFile || site ?. isSiteThemeImageUrl ( url ) || CoreUrl . isGravatarUrl ( url ) ) ) ) {
212-
213- this . logger . debug ( `Ignoring non-downloadable URL: ${ url } ` ) ;
214-
215- throw new CoreError ( 'Non-downloadable URL' ) ;
216- }
217218
218- if ( site && ! site . canDownloadFiles ( ) && isSiteFile ) {
219- this . element . parentElement ?. removeChild ( this . element ) ; // Remove element since it'll be broken.
219+ let finalUrl : string ;
220+ try {
221+ finalUrl = await this . getFinalUrl ( url , tagName , targetAttr , site ) ;
222+ } catch ( error ) {
223+ if ( error instanceof CannotDownloadError ) {
224+ // Remove element since it'll be broken.
225+ this . element . parentElement ?. removeChild ( this . element ) ;
226+ }
220227
221- throw new CoreError ( Translate . instant ( 'core.cannotdownloadfiles' ) ) ;
228+ throw error ;
222229 }
223230
224- const finalUrl = await this . getUrlToUse ( targetAttr , url , site ) ;
225-
226231 this . logger . debug ( `Using URL ${ finalUrl } for ${ url } ` ) ;
227232
228233 this . setElementUrl ( targetAttr , finalUrl ) ;
@@ -276,6 +281,93 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
276281 }
277282 }
278283
284+ /**
285+ * Handle srcset attribute, replacing each URL with the local/downloaded version.
286+ *
287+ * @param siteId Site ID.
288+ * @returns Promise resolved when done.
289+ */
290+ protected async handleSrcset ( siteId ?: string ) : Promise < void > {
291+ const srcset = this . element . getAttribute ( 'data-original-srcset' ) ?? this . element . getAttribute ( 'srcset' ) ;
292+ if ( ! srcset ) {
293+ return ;
294+ }
295+
296+ if ( ! this . element . hasAttribute ( 'data-original-srcset' ) ) {
297+ this . element . setAttribute ( 'data-original-srcset' , srcset ) ;
298+ }
299+
300+ const site = await CorePromiseUtils . ignoreErrors ( CoreSites . getSite ( siteId ) ) ;
301+
302+ // Parse srcset: comma-separated list of "url [descriptor]" pairs.
303+ const parts = srcset . split ( ',' ) . map ( part => part . trim ( ) ) . filter ( part => part . length > 0 ) ;
304+
305+ const promises = parts . map ( async ( part ) => {
306+ const tokens = part . split ( / \s + / ) ;
307+ const url = tokens [ 0 ] ;
308+ const descriptor = tokens . slice ( 1 ) . join ( ' ' ) ;
309+
310+ if ( ! url ) {
311+ return part ;
312+ }
313+
314+ let finalUrl : string ;
315+ try {
316+ finalUrl = await this . getFinalUrl ( url , this . element . tagName , 'srcset' , site ) ;
317+
318+ this . logger . debug ( `Using URL ${ finalUrl } for ${ url } in srcset` ) ;
319+
320+ return descriptor ? `${ finalUrl } ${ descriptor } ` : finalUrl ;
321+ } catch ( error ) {
322+ if ( error instanceof CannotDownloadError ) {
323+ // URL might be broken.
324+ return '' ;
325+ }
326+
327+ return part ;
328+ }
329+ } ) ;
330+
331+ try {
332+ const newParts = await Promise . all ( promises ) ;
333+ this . element . setAttribute ( 'srcset' , newParts . filter ( part => part . length > 0 ) . join ( ', ' ) ) ;
334+ } catch {
335+ this . logger . error ( 'Error treating srcset.' , this . element ) ;
336+ }
337+ }
338+
339+ /**
340+ * Get the final URL to use in the element, checking if it's valid and can be downloaded.
341+ *
342+ * @param url URL to treat.
343+ * @param tagName Name of the tag using the URL.
344+ * @param targetAttr Attribute using the URL.
345+ * @param site Site.
346+ * @returns Promise resolved with the URL to use in the element.
347+ * If the URL is not valid or can't be downloaded, the promise will be rejected.
348+ */
349+ protected async getFinalUrl ( url : string , tagName : string , targetAttr : string , site ?: CoreSite ) : Promise < string > {
350+ const isSiteFile = site ?. isSitePluginFileUrl ( url ) ;
351+
352+ // Try to convert the URL to absolute. This will only work for URLs relative to the site URL, it won't work for
353+ // URLs relative to a subpath (e.g. relative to the course page URL).
354+ url = site && url ? CoreUrl . toAbsoluteURL ( site . getURL ( ) , url ) : url ;
355+
356+ if ( ! url || ! url . match ( / ^ h t t p s ? : \/ \/ / i) || CoreUrl . isLocalFileUrl ( url ) ||
357+ ( tagName === 'A' && ! ( isSiteFile || site ?. isSiteThemeImageUrl ( url ) || CoreUrl . isGravatarUrl ( url ) ) ) ) {
358+
359+ this . logger . debug ( `Ignoring non-downloadable URL: ${ url } ` ) ;
360+
361+ throw new CoreError ( 'Non-downloadable URL' ) ;
362+ }
363+
364+ if ( site && ! site . canDownloadFiles ( ) && isSiteFile ) {
365+ throw new CannotDownloadError ( Translate . instant ( 'core.cannotdownloadfiles' ) ) ;
366+ }
367+
368+ return await this . getUrlToUse ( targetAttr , url , site ) ;
369+ }
370+
279371 /**
280372 * Handle inline styles, trying to download referenced files.
281373 *
@@ -587,3 +679,5 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
587679 }
588680
589681}
682+
683+ class CannotDownloadError extends CoreError { } ;
0 commit comments