diff --git a/packages/attachments/src/model.ts b/packages/attachments/src/model.ts index 2a55f28b65df..41e7db3df489 100644 --- a/packages/attachments/src/model.ts +++ b/packages/attachments/src/model.ts @@ -374,6 +374,9 @@ export class AttachmentsResolver implements IRenderMime.IResolver { /** * Get the download url of a given absolute server path. + * + * #### Notes + * The returned URL may include a query parameter. */ getDownloadUrl(path: string): Promise { if (this._parent && !path.startsWith('attachment:')) { diff --git a/packages/filebrowser/src/model.ts b/packages/filebrowser/src/model.ts index 5f6392759a5c..b8253b271205 100644 --- a/packages/filebrowser/src/model.ts +++ b/packages/filebrowser/src/model.ts @@ -323,12 +323,9 @@ export class FileBrowserModel implements IDisposable { async download(path: string): Promise { const url = await this.manager.services.contents.getDownloadUrl(path); let element = document.createElement('a'); + element.href = url; + element.download = ''; document.body.appendChild(element); - element.setAttribute('href', url); - // Chrome doesn't get the right name automatically - const parts = path.split('/'); - const name = parts[parts.length - 1]; - element.setAttribute('download', name); element.click(); document.body.removeChild(element); return void 0; diff --git a/packages/rendermime-interfaces/src/index.ts b/packages/rendermime-interfaces/src/index.ts index 9fa122b42fdf..a9844ebbefdf 100644 --- a/packages/rendermime-interfaces/src/index.ts +++ b/packages/rendermime-interfaces/src/index.ts @@ -340,6 +340,9 @@ export namespace IRenderMime { /** * Get the download url for a given absolute url path. + * + * #### Notes + * This URL may include a query parameter. */ getDownloadUrl(url: string): Promise; diff --git a/packages/rendermime/src/registry.ts b/packages/rendermime/src/registry.ts index 454ba2599b18..3ceab1e2810f 100644 --- a/packages/rendermime/src/registry.ts +++ b/packages/rendermime/src/registry.ts @@ -328,6 +328,9 @@ export namespace RenderMimeRegistry { /** * Get the download url of a given absolute url path. + * + * #### Notes + * This URL may include a query parameter. */ getDownloadUrl(url: string): Promise { if (this.isLocal(url)) { diff --git a/packages/services/src/contents/index.ts b/packages/services/src/contents/index.ts index c6959d236ee1..89ed5e56cc1a 100644 --- a/packages/services/src/contents/index.ts +++ b/packages/services/src/contents/index.ts @@ -276,6 +276,9 @@ export namespace Contents { * * @param A promise which resolves with the absolute POSIX * file path on the server. + * + * #### Notes + * The returned URL may include a query parameter. */ getDownloadUrl(path: string): Promise; @@ -420,6 +423,9 @@ export namespace Contents { * * @param A promise which resolves with the absolute POSIX * file path on the server. + * + * #### Notes + * The returned URL may include a query parameter. */ getDownloadUrl(localPath: string): Promise; @@ -690,6 +696,8 @@ export class ContentsManager implements Contents.IManager { * * #### Notes * It is expected that the path contains no relative paths. + * + * The returned URL may include a query parameter. */ getDownloadUrl(path: string): Promise { let [drive, localPath] = this._driveForPath(path); @@ -1041,13 +1049,17 @@ export class Drive implements Contents.IDrive { * * #### Notes * It is expected that the path contains no relative paths. + * + * The returned URL may include a query parameter. */ getDownloadUrl(localPath: string): Promise { let baseUrl = this.serverSettings.baseUrl; let url = URLExt.join(baseUrl, FILES_URL, URLExt.encodeParts(localPath)); const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b'); if (xsrfTokenMatch) { - url = URLExt.join(url, `?_xsrf=${xsrfTokenMatch[1]}`); + const fullurl = new URL(url); + fullurl.searchParams.append('_xsrf', xsrfTokenMatch[1]); + url = fullurl.toString(); } return Promise.resolve(url); } diff --git a/packages/vega4-extension/src/index.ts b/packages/vega4-extension/src/index.ts index 26de8fb4930d..e66903e13605 100644 --- a/packages/vega4-extension/src/index.ts +++ b/packages/vega4-extension/src/index.ts @@ -42,6 +42,11 @@ export const VEGA_MIME_TYPE = 'application/vnd.vega.v4+json'; */ export const VEGALITE_MIME_TYPE = 'application/vnd.vegalite.v2+json'; +/** + * A regex to test for a protocol in a URI. + */ +const protocolRegex = /^[A-Za-z]:/; + /** * A widget for rendering Vega or Vega-Lite data, for usage with rendermime. */ @@ -76,8 +81,6 @@ export class RenderedVega extends Widget implements IRenderMime.IRenderer { const vega = Private.vega != null ? Private.vega : await Private.ensureVega(); - const path = await this._resolver.resolveUrl(''); - const baseURL = await this._resolver.getDownloadUrl(path); const el = document.createElement('div'); @@ -89,15 +92,24 @@ export class RenderedVega extends Widget implements IRenderMime.IRenderer { this._result.view.finalize(); } + const loader = vega.vega.loader({ + http: { credentials: 'same-origin' } + }); + + const sanitize = async (uri: string, options: any) => { + // If the uri is a path, get the download URI + if (!protocolRegex.test(uri)) { + uri = await this._resolver.getDownloadUrl(uri); + } + return loader.sanitize(uri, options); + }; + this._result = await vega.default(el, spec, { actions: true, defaultStyle: true, ...embedOptions, mode, - loader: { - baseURL, - http: { credentials: 'same-origin' } - } + loader: { ...loader, sanitize } }); if (model.data['image/png']) { diff --git a/packages/vega5-extension/src/index.ts b/packages/vega5-extension/src/index.ts index d9bd96b5a313..e728c3f22c58 100644 --- a/packages/vega5-extension/src/index.ts +++ b/packages/vega5-extension/src/index.ts @@ -42,6 +42,11 @@ export const VEGA_MIME_TYPE = 'application/vnd.vega.v5+json'; */ export const VEGALITE_MIME_TYPE = 'application/vnd.vegalite.v3+json'; +/** + * A regex to test for a protocol in a URI. + */ +const protocolRegex = /^[A-Za-z]:/; + /** * A widget for rendering Vega or Vega-Lite data, for usage with rendermime. */ @@ -76,8 +81,6 @@ export class RenderedVega extends Widget implements IRenderMime.IRenderer { const vega = Private.vega != null ? Private.vega : await Private.ensureVega(); - const path = await this._resolver.resolveUrl(''); - const baseURL = await this._resolver.getDownloadUrl(path); const el = document.createElement('div'); @@ -89,15 +92,23 @@ export class RenderedVega extends Widget implements IRenderMime.IRenderer { this._result.view.finalize(); } + const loader = vega.vega.loader({ + http: { credentials: 'same-origin' } + }); + const sanitize = async (uri: string, options: any) => { + // If the uri is a path, get the download URI + if (!protocolRegex.test(uri)) { + uri = await this._resolver.getDownloadUrl(uri); + } + return loader.sanitize(uri, options); + }; + this._result = await vega.default(el, spec, { actions: true, defaultStyle: true, ...embedOptions, mode, - loader: { - baseURL, - http: { credentials: 'same-origin' } - } + loader: { ...loader, sanitize } }); if (model.data['image/png']) {