From 588fa5e94aefe0ac6c822bc773183afaff1c0e77 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 15 Aug 2019 09:44:30 -0700 Subject: [PATCH 1/2] Fix getDownloadURL to properly append the xsrf token without a slash. Before, we would get URLs like http://example.com/files/file.txt/?_xsrf=token, and now the slash after file.txt is eliminated. --- packages/attachments/src/model.ts | 3 +++ packages/filebrowser/src/model.ts | 7 ++----- packages/rendermime-interfaces/src/index.ts | 3 +++ packages/rendermime/src/registry.ts | 3 +++ packages/services/src/contents/index.ts | 14 +++++++++++++- 5 files changed, 24 insertions(+), 6 deletions(-) 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); } From 70ac6fae421fbad28a3b08b40c807008deec21c9 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 15 Aug 2019 09:45:17 -0700 Subject: [PATCH 2/2] Make vega extensions use getDownloadUrl to retrieve files from the server. Perhaps we should also use the services package to actually send the request to the server. That would complicate these plugins beyond simple rendermime plugins, though. Fixes #7017 --- packages/vega4-extension/src/index.ts | 24 ++++++++++++++++++------ packages/vega5-extension/src/index.ts | 23 +++++++++++++++++------ 2 files changed, 35 insertions(+), 12 deletions(-) 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']) {