diff --git a/src/sql/base/common/network.ts b/src/sql/base/common/network.ts new file mode 100644 index 0000000000..2010f992c9 --- /dev/null +++ b/src/sql/base/common/network.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** +* Checks if the specified URL is already URI-encoded by checking if there are any unencoded reserved URI component characters +* (such as ?, =, &, /, etc.) in the URL. See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent for more details. +* @returns true if the URL contains encoded URI component reserved characters +*/ +export function containsEncodedUriComponentReservedCharacters(url: string): boolean { + // ie ?,=,&,/ etc + return (decodeURI(url) !== decodeURIComponent(url)); +} diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts index 1ae06dca39..d2f1540faf 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts @@ -278,8 +278,8 @@ export class MarkdownToolbarComponent extends AngularDisposable { // Otherwise, re-focus on the output element, and insert the link directly. this.output?.nativeElement?.focus(); // Need to encode URI here in order for user to click the proper encoded link in WYSIWYG - let encodedLinkURL = encodeURI(linkUrl); - document.execCommand('insertHTML', false, `${escape(linkCalloutResult?.insertUnescapedLinkLabel)}`); + let encodedLinkURL = notebookLink.getEncodedLinkUrl(); + document.execCommand('insertHTML', false, `${escape(linkCalloutResult?.insertUnescapedLinkLabel)}`); return; } } else if (type === MarkdownButtonType.IMAGE_PREVIEW) { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookLinkHandler.ts b/src/sql/workbench/contrib/notebook/browser/notebookLinkHandler.ts index a52920513e..4a3f81d8a4 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookLinkHandler.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookLinkHandler.ts @@ -8,6 +8,7 @@ import * as path from 'vs/base/common/path'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { replaceInvalidLinkPath } from 'sql/workbench/contrib/notebook/common/utils'; import { isWindows } from 'vs/base/common/platform'; +import { containsEncodedUriComponentReservedCharacters } from 'sql/base/common/network'; const useAbsolutePathConfigName = 'notebook.useAbsoluteFilePaths'; @@ -118,6 +119,24 @@ export class NotebookLinkHandler { } } + /** + * Function to get encoded url, Gets the link URI-encoded link URL + * (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) + * @returns the encoded url + */ + public getEncodedLinkUrl(): string | undefined { + if (typeof this._link === 'string') { + // Need to encode URI here in order for user to click the proper encoded link in WYSIWYG + // skip encoding it if it's already encoded + if (!containsEncodedUriComponentReservedCharacters(this._link)) { + return encodeURI(this._link); + } + return this._link; + } + // since we only handle strings that come from call out dialogs + return undefined; + } + /** * Creates a URI for for a link with a anchor (#) * @param node is the HTMLAnchorElement of the target notebook diff --git a/src/sql/workbench/contrib/notebook/browser/utils.ts b/src/sql/workbench/contrib/notebook/browser/utils.ts index 284265894a..a71d49b016 100644 --- a/src/sql/workbench/contrib/notebook/browser/utils.ts +++ b/src/sql/workbench/contrib/notebook/browser/utils.ts @@ -43,4 +43,3 @@ export function highlightSelectedText(): void { document.execCommand('hiliteColor', false, 'Yellow'); } } - diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookLinkHandler.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookLinkHandler.test.ts index aaaf480c34..5f042f8e54 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookLinkHandler.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookLinkHandler.test.ts @@ -103,4 +103,31 @@ suite('Noteboook Link Handler', function (): void { result = new NotebookLinkHandler(notebookUri, Object.assign(document.createElement('a'), { href: '/tmp/my%20stuff.png' }), configurationService); assert.strictEqual(result.getLinkUrl(), `.${path.sep}my%2520stuff.png`, 'Basic link test with %20 filename failed'); }); + + test('Should return correctly encoded url/filePath', () => { + test('when given an already-encoded URL', () => { + let notebookLinkHandler = new NotebookLinkHandler(notebookUri, 'https://github.com/search/advanced?q=test&r=microsoft%2Fazuredatastudio&type=Code', configurationService); + assert.strictEqual(notebookLinkHandler.getEncodedLinkUrl(), `https://github.com/search/advanced?q=test&r=microsoft%2Fazuredatastudio&type=Code`, 'HTTPS link does not need encoding'); + }); + + test('when given an already encoded URL with non-reserved characters', () => { + let notebookLinkHandler = new NotebookLinkHandler(notebookUri, 'https://github.com/search/advanced?q=test&r=(microsoft%2Fazuredatastudio)&type=Code', configurationService); + assert.strictEqual(notebookLinkHandler.getEncodedLinkUrl(), `https://github.com/search/advanced?q=test&r=(microsoft%2Fazuredatastudio)&type=Code`, '() in HTTP link should not be encoded'); + }); + + test('when given an unencoded URL with a space', () => { + let notebookLinkHandler = new NotebookLinkHandler(notebookUri, 'https://github.com/search/advanced?q=test&r=(microsoft/azuredata studio)&type=Code', configurationService); + assert.strictEqual(notebookLinkHandler.getEncodedLinkUrl(), `https://github.com/search/advanced?q=test&r=(microsoft/azuredata%20studio)&type=Code`, 'space in the url failed to be encoded'); + }); + + test('when given file path with a space', () => { + let notebookLinkHandler = new NotebookLinkHandler(notebookUri, '/Notebooks/Test_Paths/My File.ipynb', configurationService); + assert.strictEqual(notebookLinkHandler.getEncodedLinkUrl(), `/Notebooks/Test_Paths/My%20File.ipynb`, 'space in file path failed to be encoded'); + }); + + test('when given file path has special characters such as %', () => { + let notebookLinkHandler = new NotebookLinkHandler(notebookUri, '/Notebooks/Test_Paths/My%20File.ipynb', configurationService); + assert.strictEqual(notebookLinkHandler.getEncodedLinkUrl(), `/Notebooks/Test_Paths/My%2520File.ipynb`, '% in file path failed to be encoded'); + }); + }); });