diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json
index 2905aeab4baf..657292a07f73 100644
--- a/extensions/notebook/package.json
+++ b/extensions/notebook/package.json
@@ -41,6 +41,14 @@
"type": "number",
"default": 5000,
"description": "%notebook.maxTableRows.description%"
+ },
+ "notebook.trustedBooks":{
+ "type": "array",
+ "default": [],
+ "description": "%notebook.trustedBooks.description%",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -159,6 +167,15 @@
"light": "resources/light/save.svg"
}
},
+ {
+ "command": "notebook.command.trustBook",
+ "title": "%title.trustBook%",
+ "category": "%books-preview-category%",
+ "icon": {
+ "dark": "resources/dark/trust_inverse.svg",
+ "light": "resources/light/trust.svg"
+ }
+ },
{
"command": "notebook.command.searchBook",
"title": "%title.searchJupyterBook%",
@@ -292,6 +309,10 @@
"command": "notebook.command.searchUntitledBook",
"when": "false"
},
+ {
+ "command": "notebook.command.trustBook",
+ "when": "view == bookTreeView && viewItem == savedBook"
+ },
{
"command": "notebook.command.closeBook",
"when": "false"
@@ -326,6 +347,11 @@
}
],
"view/item/context": [
+ {
+ "command": "notebook.command.trustBook",
+ "when": "view == bookTreeView && viewItem == savedBook",
+ "group": "inline"
+ },
{
"command": "notebook.command.searchBook",
"when": "view == bookTreeView && viewItem == savedBook || viewItem == section",
diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json
index 8effe01d3cc6..012749963b07 100644
--- a/extensions/notebook/package.nls.json
+++ b/extensions/notebook/package.nls.json
@@ -6,6 +6,7 @@
"notebook.useExistingPython.description": "Local path to a preexisting python installation used by Notebooks.",
"notebook.overrideEditorTheming.description": "Override editor default settings in the Notebook editor. Settings include background color, current line color and border",
"notebook.maxTableRows.description": "Maximum number of rows returned per table in the Notebook editor",
+ "notebook.trustedBooks.description": "Notebooks contained in these books will automatically be trusted.",
"notebook.maxBookSearchDepth.description": "Maximum depth of subdirectories to search for Books (Enter 0 for infinite)",
"notebook.command.new": "New Notebook",
"notebook.command.open": "Open Notebook",
@@ -30,6 +31,7 @@
"title.SQL19PreviewBook": "SQL Server 2019 Guide",
"books-preview-category": "Jupyter Books",
"title.saveJupyterBook": "Save Book",
+ "title.trustBook": "Trust Book",
"title.searchJupyterBook": "Search Book",
"title.SavedBooks": "Saved Books",
"title.UnsavedBooks": "Unsaved Books",
diff --git a/extensions/notebook/resources/dark/trust_inverse.svg b/extensions/notebook/resources/dark/trust_inverse.svg
new file mode 100644
index 000000000000..55101847b419
--- /dev/null
+++ b/extensions/notebook/resources/dark/trust_inverse.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/notebook/resources/light/trust.svg b/extensions/notebook/resources/light/trust.svg
new file mode 100644
index 000000000000..13f149f0de3f
--- /dev/null
+++ b/extensions/notebook/resources/light/trust.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts
index 81b5b9c3d3fe..cd7c71076c0f 100644
--- a/extensions/notebook/src/book/bookTreeView.ts
+++ b/extensions/notebook/src/book/bookTreeView.ts
@@ -7,19 +7,20 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs-extra';
+import * as constants from '../common/constants';
import * as fsw from 'fs';
import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question';
import CodeAdapter from '../prompts/adapter';
import { BookTreeItem } from './bookTreeItem';
import { BookModel } from './bookModel';
import { Deferred } from '../common/promise';
+import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import { ApiWrapper } from '../common/apiWrapper';
const Content = 'content';
export class BookTreeViewProvider implements vscode.TreeDataProvider {
-
private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
private _throttleTimer: any;
@@ -27,21 +28,22 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider = new Deferred();
- private _bookViewer: vscode.TreeView;
private _openAsUntitled: boolean;
- private _apiWrapper: ApiWrapper;
+ private _bookTrustManager: IBookTrustManager;
+
+ private _bookViewer: vscode.TreeView;
public viewId: string;
public books: BookModel[];
public currentBook: BookModel;
- constructor(apiWrapper: ApiWrapper, workspaceFolders: vscode.WorkspaceFolder[], extensionContext: vscode.ExtensionContext, openAsUntitled: boolean, view: string) {
+ constructor(private _apiWrapper: ApiWrapper, workspaceFolders: vscode.WorkspaceFolder[], extensionContext: vscode.ExtensionContext, openAsUntitled: boolean, view: string) {
this._openAsUntitled = openAsUntitled;
this._extensionContext = extensionContext;
this.books = [];
this.initialize(workspaceFolders).catch(e => console.error(e));
this.viewId = view;
this.prompter = new CodeAdapter();
- this._apiWrapper = apiWrapper ? apiWrapper : new ApiWrapper();
+ this._bookTrustManager = new BookTrustManager(this.books, _apiWrapper);
}
private async initialize(workspaceFolders: vscode.WorkspaceFolder[]): Promise {
@@ -60,6 +62,28 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
try {
let books: BookModel[] = this.books.filter(book => book.bookPath === bookPath) || [];
@@ -157,6 +181,19 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
+ document.setTrusted(true);
+ this._visitedNotebooks = this._visitedNotebooks.concat([normalizedResource]);
+ openDocumentListenerUnsubscriber.dispose();
+ });
+ }
+
let doc = await vscode.workspace.openTextDocument(resource);
vscode.window.showTextDocument(doc);
}
@@ -377,6 +414,4 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider trustableBookPaths.some(trustableBookPath => trustableBookPath === path.join(bookItem.book.root, path.sep)))
+ .some(bookItem => normalizedNotebookUri.startsWith(path.join(bookItem.root, 'content', path.sep)));
+ let isNotebookTrusted = hasTrustedBookPath && this.books.some(bookModel => bookModel.getNotebook(normalizedNotebookUri));
+ return isNotebookTrusted;
+ }
+
+ getTrustableBookPaths() {
+ let trustablePaths: string[];
+ let bookPathsInConfig: string[] = this.getTrustedBookPathsInConfig();
+
+ if (this.hasWorkspaceFolders()) {
+ let workspaceFolders = this.apiWrapper.getWorkspaceFolders();
+
+ trustablePaths = bookPathsInConfig
+ .map(trustableBookPath => workspaceFolders
+ .map(workspaceFolder => path.join(workspaceFolder.uri.fsPath, trustableBookPath)))
+ .reduce((accumulator, currentTrustableBookPaths) => accumulator.concat(currentTrustableBookPaths), []);
+
+ } else {
+ trustablePaths = bookPathsInConfig;
+ }
+
+ return trustablePaths;
+ }
+
+ getBookTreeItems(): BookTreeItem[] {
+ return this.books
+ .map(book => book.bookItems) // select all the books
+ .reduce((accumulator, currentBookItemList) => accumulator.concat(currentBookItemList), []);
+ }
+
+ setBookAsTrusted(bookRootPath: string): boolean {
+ return this.updateTrustedBooks(bookRootPath, TrustBookOperation.Add);
+ }
+
+ getTrustedBookPathsInConfig(): string[] {
+ let config: vscode.WorkspaceConfiguration = this.apiWrapper.getConfiguration(constants.notebookConfigKey);
+ let trustedBookDirectories: string[] = config.get(constants.trustedBooksConfigKey);
+
+ return trustedBookDirectories;
+ }
+
+ setTrustedBookPathsInConfig(bookPaths: string[]) {
+ let config: vscode.WorkspaceConfiguration = this.apiWrapper.getConfiguration(constants.notebookConfigKey);
+ let storeInWorspace: boolean = this.hasWorkspaceFolders();
+
+ config.update(constants.trustedBooksConfigKey, bookPaths, storeInWorspace ? false : vscode.ConfigurationTarget.Global);
+ }
+
+ hasWorkspaceFolders(): boolean {
+ let workspaceFolders = this.apiWrapper.getWorkspaceFolders();
+ return workspaceFolders && workspaceFolders.length > 0;
+ }
+
+ updateTrustedBooks(bookPath: string, operation: TrustBookOperation) {
+ let modifiedTrustedBooks = false;
+ let bookPathToChange: string = path.join(bookPath, path.sep);
+
+ if (this.hasWorkspaceFolders()) {
+ let workspaceFolders = this.apiWrapper.getWorkspaceFolders();
+ let matchingWorkspaceFolder: vscode.WorkspaceFolder = workspaceFolders
+ .find(ws => bookPathToChange.startsWith(path.normalize(ws.uri.fsPath)));
+
+ // if notebook is stored in a workspace folder, then store only the relative directory
+ if (matchingWorkspaceFolder) {
+ bookPathToChange = bookPathToChange.replace(path.normalize(matchingWorkspaceFolder.uri.fsPath), '');
+ }
+ }
+
+ let trustedBooks: string[] = this.getTrustedBookPathsInConfig();
+ let existingBookIndex = trustedBooks.map(trustedBookPath => path.normalize(trustedBookPath)).indexOf(bookPathToChange);
+
+ if (existingBookIndex !== -1 && operation === TrustBookOperation.Remove) {
+ trustedBooks.splice(existingBookIndex, 1);
+ modifiedTrustedBooks = true;
+ } else if (existingBookIndex === -1 && operation === TrustBookOperation.Add) {
+ trustedBooks.push(bookPathToChange);
+ modifiedTrustedBooks = true;
+ }
+
+ this.setTrustedBookPathsInConfig(trustedBooks);
+
+ return modifiedTrustedBooks;
+ }
+}
diff --git a/extensions/notebook/src/common/apiWrapper.ts b/extensions/notebook/src/common/apiWrapper.ts
index 96a3d0581ad6..370fb6eafec7 100644
--- a/extensions/notebook/src/common/apiWrapper.ts
+++ b/extensions/notebook/src/common/apiWrapper.ts
@@ -12,6 +12,10 @@ import { CommandContext, BuiltInCommands } from './constants';
* this API from our code
*/
export class ApiWrapper {
+ public getWorkspaceFolders(): vscode.WorkspaceFolder[] {
+ return [].concat(vscode.workspace.workspaceFolders || []);
+ }
+
public createOutputChannel(name: string): vscode.OutputChannel {
return vscode.window.createOutputChannel(name);
}
diff --git a/extensions/notebook/src/common/constants.ts b/extensions/notebook/src/common/constants.ts
index 26722b972a05..90d7dd86c2b7 100644
--- a/extensions/notebook/src/common/constants.ts
+++ b/extensions/notebook/src/common/constants.ts
@@ -16,6 +16,7 @@ export const pythonVersion = '3.6.6';
export const pythonPathConfigKey = 'pythonPath';
export const existingPythonConfigKey = 'useExistingPython';
export const notebookConfigKey = 'notebook';
+export const trustedBooksConfigKey = 'trustedBooks';
export const maxBookSearchDepth = 'maxBookSearchDepth';
export const winPlatform = 'win32';
@@ -33,6 +34,8 @@ export const localhostName = 'localhost';
export const localhostTitle = localize('managePackages.localhost', "localhost");
export const PackageNotFoundError = localize('managePackages.packageNotFound', "Could not find the specified package");
+export const visitedNotebooksMementoKey = 'notebooks.visited';
+
export enum BuiltInCommands {
SetContext = 'setContext'
}
diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts
index ce1a3d2940bb..e4e2fd3015a1 100644
--- a/extensions/notebook/src/common/localizedConstants.ts
+++ b/extensions/notebook/src/common/localizedConstants.ts
@@ -21,8 +21,12 @@ export const confirmReplace = localize('confirmReplace', "Folder already exists.
export const openNotebookCommand = localize('openNotebookCommand', "Open Notebook");
export const openMarkdownCommand = localize('openMarkdownCommand', "Open Markdown");
export const openExternalLinkCommand = localize('openExternalLinkCommand', "Open External Link");
-
+export const msgBookTrusted = localize('msgBookTrusted', "Book is now trusted in the workspace.");
+export const msgBookAlreadyTrusted = localize('msgBookAlreadyTrusted', "Book is already trusted in this workspace.");
+export const msgBookUntrusted = localize('msgBookUntrusted', "Book is no longer trusted in this workspace");
+export const msgBookAlreadyUntrusted = localize('msgBookAlreadyUntrusted', "Book is already untrusted in this workspace.");
export const missingTocError = localize('bookInitializeFailed', "Failed to find a toc.yml.");
+
export function missingFileError(title: string): string { return localize('missingFileError', "Missing file : {0}", title); }
export function invalidTocFileError(): string { return localize('InvalidError.tocFile', "Invalid toc file"); }
export function invalidTocError(title: string): string { return localize('Invalid toc.yml', "Error: {0} has an incorrect toc.yml file", title); }
diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts
index 284ba9f31507..9ba7072a22b2 100644
--- a/extensions/notebook/src/extension.ts
+++ b/extensions/notebook/src/extension.ts
@@ -36,6 +36,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.saveBook', () => untitledBookTreeViewProvider.saveJupyterBooks()));
+ extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.trustBook', (resource) => bookTreeViewProvider.trustBook(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item) => bookTreeViewProvider.searchJupyterBooks(item)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchUntitledBook', () => untitledBookTreeViewProvider.searchJupyterBooks()));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook()));
diff --git a/extensions/notebook/src/test/book/bookTrustManager.test.ts b/extensions/notebook/src/test/book/bookTrustManager.test.ts
new file mode 100644
index 000000000000..976125a38dd6
--- /dev/null
+++ b/extensions/notebook/src/test/book/bookTrustManager.test.ts
@@ -0,0 +1,328 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as should from 'should';
+import * as path from 'path';
+import * as TypeMoq from 'typemoq';
+import * as constants from '../../common/constants';
+import { IBookTrustManager, BookTrustManager } from '../../book/bookTrustManager';
+import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem';
+import { ApiWrapper } from '../../common/apiWrapper';
+import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode';
+import { BookModel } from '../../book/bookModel';
+
+describe('BookTrustManagerTests', function () {
+
+ describe('TrustingInWorkspaces', () => {
+ let bookTrustManager: IBookTrustManager;
+ let trustedSubFolders: string[];
+ let books: BookModel[];
+
+ beforeEach(() => {
+ trustedSubFolders = ['/SubFolder/'];
+
+ // Mock Workspace Configuration
+ let workspaceConfigurtionMock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ workspaceConfigurtionMock.setup(config => config.get(TypeMoq.It.isValue(constants.trustedBooksConfigKey))).returns(() => [].concat(trustedSubFolders));
+ workspaceConfigurtionMock.setup(config => config.update(TypeMoq.It.isValue(constants.trustedBooksConfigKey), TypeMoq.It.isAny(), TypeMoq.It.isValue(false))).returns((key: string, newValues: string[]) => {
+ trustedSubFolders.splice(0, trustedSubFolders.length, ...newValues); // Replace
+ return Promise.resolve();
+ });
+
+ // Mock Api Wrapper
+ let apiWrapperMock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+
+ apiWrapperMock.setup(api => api.getWorkspaceFolders()).returns(() => [
+ {
+ // @ts-ignore - Don't need all URI properties for this tests
+ uri: {
+ fsPath: '/temp/'
+ },
+ },
+ {
+ // @ts-ignore - Don't need all URI properties for this tests
+ uri: {
+ fsPath: '/temp2/'
+ }
+ },
+ ]);
+
+ apiWrapperMock.setup(api => api.getConfiguration(TypeMoq.It.isValue(constants.notebookConfigKey))).returns(() => workspaceConfigurtionMock.object);
+
+ // Mock Book Data
+ let bookTreeItemFormat1: BookTreeItemFormat = {
+ root: '/temp/SubFolder/',
+ tableOfContents: {
+ sections: [
+ {
+ url: path.join(path.sep, 'sample', 'notebook')
+ },
+ {
+ url: path.join(path.sep, 'sample', 'notebook2')
+ }
+ ]
+ },
+ isUntitled: undefined,
+ page: undefined,
+ title: undefined,
+ treeItemCollapsibleState: undefined,
+ type: BookTreeItemType.Book
+ };
+
+ let bookTreeItemFormat2: BookTreeItemFormat = {
+ root: '/temp/SubFolder2/',
+ tableOfContents: {
+ sections: [
+ {
+ url: path.join(path.sep, 'sample', 'notebook')
+ }
+ ]
+ },
+ isUntitled: undefined,
+ page: undefined,
+ title: undefined,
+ treeItemCollapsibleState: undefined,
+ type: BookTreeItemType.Book
+ };
+
+ let bookTreeItemFormat3: BookTreeItemFormat = {
+ root: '/temp2/SubFolder3/',
+ tableOfContents: {
+ sections: [
+ {
+ url: path.join(path.sep, 'sample', 'notebook')
+ }
+ ]
+ },
+ isUntitled: undefined,
+ page: undefined,
+ title: undefined,
+ treeItemCollapsibleState: undefined,
+ type: BookTreeItemType.Book
+ };
+
+ let bookModel1Mock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ bookModel1Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat1, undefined), new BookTreeItem(bookTreeItemFormat2, undefined)]);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder','content','sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder','content','sample', 'notebook2.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder2','content','sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
+
+ let bookModel2Mock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ bookModel2Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat3, undefined)]);
+ bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp2','SubFolder','content','sample','notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
+
+ books = [bookModel1Mock.object, bookModel2Mock.object];
+
+ bookTrustManager = new BookTrustManager(books, apiWrapperMock.object);
+ });
+
+ it('should trust notebooks in a trusted book within a workspace', async () => {
+ let notebookUri1 = path.join(path.sep,'temp','SubFolder','content','sample', 'notebook.ipynb');
+ let notebookUri2 = path.join(path.sep,'temp','SubFolder','content','sample', 'notebook2.ipynb');
+
+ let isNotebook1Trusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri1);
+ let isNotebook2Trusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri2);
+
+ should(isNotebook1Trusted).be.true("Notebook 1 should be trusted");
+ should(isNotebook2Trusted).be.true("Notebook 2 should be trusted");
+
+ });
+
+ it('should NOT trust a notebook in an untrusted book within a workspace', async () => {
+ let notebookUri = path.join(path.sep,'temp','SubFolder2','content', 'sample', 'notebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Notebook should be trusted");
+ });
+
+ it('should trust notebook after book has been trusted within a workspace', async () => {
+ let notebookUri = path.join(path.sep,'temp','SubFolder2','content', 'sample', 'notebook.ipynb');
+ let isNotebookTrustedBeforeChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedBeforeChange).be.false("Notebook should NOT be trusted");
+
+ // add another book subfolder
+ bookTrustManager.setBookAsTrusted('/SubFolder2/');
+
+ let isNotebookTrustedAfterChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedAfterChange).be.true("Notebook should be trusted");
+ });
+
+ it('should NOT trust a notebook when untrusting a book within a workspace', async () => {
+ let notebookUri = path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.true("Notebook should be trusted");
+
+ // remove trusted subfolders
+ trustedSubFolders = [];
+
+ let isNotebookTrustedAfterChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedAfterChange).be.false("Notebook should not be trusted after book removal");
+ });
+
+ it('should NOT trust an unknown book within a workspace', async () => {
+ let notebookUri = path.join(path.sep, 'randomfolder', 'randomsubfolder', 'content', 'randomnotebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Random notebooks should not be trusted");
+ });
+
+ it('should NOT trust notebook inside trusted subfolder when absent in table of contents ', async () => {
+ bookTrustManager.setBookAsTrusted('/temp/SubFolder/');
+
+ let notebookUri = path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notInToc.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Notebook should NOT be trusted");
+ });
+ });
+
+ describe('TrustingInFolder', () => {
+
+ let bookTrustManager: IBookTrustManager;
+ let books: BookModel[];
+ let trustedFolders: string[] = [];
+
+ beforeEach(() => {
+ // Mock Workspace Configuration
+ let workspaceConfigurtionMock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ workspaceConfigurtionMock.setup(config => config.get(TypeMoq.It.isValue(constants.trustedBooksConfigKey))).returns(() => [].concat(trustedFolders));
+ workspaceConfigurtionMock.setup(config => config.update(TypeMoq.It.isValue(constants.trustedBooksConfigKey), TypeMoq.It.isAny(), TypeMoq.It.isValue(ConfigurationTarget.Global))).returns((key: string, newValues: string[], target: ConfigurationTarget) => {
+ trustedFolders.splice(0, trustedFolders.length, ...newValues); // Replace
+ return Promise.resolve();
+ });
+
+
+ // Mock Api Wrapper
+ let apiWrapperMock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+
+ apiWrapperMock.setup(api => api.getWorkspaceFolders()).returns(() => []);
+ apiWrapperMock.setup(api => api.getConfiguration(TypeMoq.It.isValue(constants.notebookConfigKey))).returns(() => workspaceConfigurtionMock.object);
+ let bookTreeItemFormat1: BookTreeItemFormat = {
+ root: '/temp/SubFolder/',
+ tableOfContents: {
+ sections: [
+ {
+ url: path.join(path.sep, 'sample', 'notebook')
+ },
+ {
+ url: path.join(path.sep, 'sample', 'notebook2')
+ }
+ ]
+ },
+ isUntitled: undefined,
+ page: undefined,
+ title: undefined,
+ treeItemCollapsibleState: undefined,
+ type: BookTreeItemType.Book
+ };
+
+ let bookTreeItemFormat2: BookTreeItemFormat = {
+ root: '/temp/SubFolder2/',
+ tableOfContents: {
+ sections: [
+ {
+ url: path.join(path.sep, 'sample', 'notebook')
+ },
+ {
+ url: path.join(path.sep, 'sample', 'notebook2')
+ }
+ ]
+ },
+ isUntitled: undefined,
+ page: undefined,
+ title: undefined,
+ treeItemCollapsibleState: undefined,
+ type: BookTreeItemType.Book
+ };
+
+ let bookModel1Mock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ bookModel1Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat1, undefined)]);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook2.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
+
+ let bookModel2Mock: TypeMoq.IMock = TypeMoq.Mock.ofType();
+ bookModel2Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat2, undefined)]);
+ bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder2','content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep,'temp','SubFolder2','content', 'sample', 'notebook2.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType().object);
+ bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
+
+ books = [bookModel1Mock.object, bookModel2Mock.object];
+
+ bookTrustManager = new BookTrustManager(books, apiWrapperMock.object);
+ });
+
+ it('should trust notebooks in a trusted book in a folder', async () => {
+ bookTrustManager.setBookAsTrusted('/temp/SubFolder/');
+
+ let notebookUri1 = path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook.ipynb');
+ let notebookUri2 = path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook2.ipynb');
+
+ let isNotebook1Trusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri1);
+ let isNotebook2Trusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri2);
+
+ should(isNotebook1Trusted).be.true("Notebook 1 should be trusted");
+ should(isNotebook2Trusted).be.true("Notebook 2 should be trusted");
+
+ });
+
+ it('should NOT trust a notebook in an untrusted book in a folder', async () => {
+ let notebookUri = path.join(path.sep,'temp','SubFolder2','content', 'sample', 'notebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Notebook should be trusted");
+ });
+
+ it('should trust notebook after book has been added to a folder', async () => {
+ let notebookUri = path.join(path.sep,'temp','SubFolder2','content', 'sample','notebook.ipynb');
+ let isNotebookTrustedBeforeChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedBeforeChange).be.false("Notebook should NOT be trusted");
+
+ bookTrustManager.setBookAsTrusted('/temp/SubFolder2/');
+
+ let isNotebookTrustedAfterChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedAfterChange).be.true("Notebook should be trusted");
+ });
+
+ it('should NOT trust a notebook when untrusting a book in folder', async () => {
+ bookTrustManager.setBookAsTrusted('/temp/SubFolder/');
+ let notebookUri = path.join(path.sep,'temp','SubFolder','content', 'sample', 'notebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.true("Notebook should be trusted");
+
+ trustedFolders = [];
+
+ let isNotebookTrustedAfterChange = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrustedAfterChange).be.false("Notebook should not be trusted after book removal");
+ });
+
+ it('should NOT trust an unknown book', async () => {
+ let notebookUri = path.join(path.sep, 'randomfolder', 'randomsubfolder', 'content', 'randomnotebook.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Random notebooks should not be trusted");
+ });
+
+ it('should NOT trust notebook inside trusted subfolder when absent in table of contents ', async () => {
+ bookTrustManager.setBookAsTrusted('/temp/SubFolder/');
+
+ let notebookUri = path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notInToc.ipynb');
+ let isNotebookTrusted = bookTrustManager.isNotebookTrustedByDefault(notebookUri);
+
+ should(isNotebookTrusted).be.false("Notebook should NOT be trusted");
+ });
+
+ });
+});
diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts
index 89e68738aa11..6d54bf77b303 100644
--- a/src/sql/azdata.proposed.d.ts
+++ b/src/sql/azdata.proposed.d.ts
@@ -29,6 +29,14 @@ declare module 'azdata' {
export function getConnection(uri: string): Thenable;
}
+ export namespace nb {
+ export interface NotebookDocument {
+ /**
+ * Sets the trust mode for the notebook document.
+ */
+ setTrusted(state: boolean);
+ }
+ }
export type SqlDbType = 'BigInt' | 'Binary' | 'Bit' | 'Char' | 'DateTime' | 'Decimal'
| 'Float' | 'Image' | 'Int' | 'Money' | 'NChar' | 'NText' | 'NVarChar' | 'Real'
diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
index d40bbaeb5466..189b99474279 100644
--- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
+++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
@@ -362,6 +362,11 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
return Promise.resolve(this.doOpenEditor(resource, options));
}
+ $trySetTrusted(uriComponent: UriComponents, isTrusted: boolean): Promise {
+ let uri = URI.revive(uriComponent);
+ return this._notebookService.setTrusted(uri, isTrusted);
+ }
+
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): Promise {
let editor = this.getEditor(id);
if (!editor) {
diff --git a/src/sql/workbench/api/common/extHostNotebookDocumentData.ts b/src/sql/workbench/api/common/extHostNotebookDocumentData.ts
index 29621f8f16ff..f53793856b5c 100644
--- a/src/sql/workbench/api/common/extHostNotebookDocumentData.ts
+++ b/src/sql/workbench/api/common/extHostNotebookDocumentData.ts
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
-
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ok } from 'vs/base/common/assert';
@@ -50,6 +49,7 @@ export class ExtHostNotebookDocumentData implements IDisposable {
get cells() { return data._cells; },
get kernelSpec() { return data._kernelSpec; },
save() { return data._save(); },
+ setTrusted(isTrusted) { data._setTrusted(isTrusted); },
validateCellRange(range) { return data._validateRange(range); },
};
}
@@ -61,7 +61,13 @@ export class ExtHostNotebookDocumentData implements IDisposable {
return Promise.reject(new Error('Document has been closed'));
}
return this._proxy.$trySaveDocument(this._uri);
+ }
+ private _setTrusted(isTrusted: boolean): Thenable {
+ if (this._isDisposed) {
+ return Promise.reject(new Error('Document has been closed'));
+ }
+ return this._proxy.$trySetTrusted(this._uri, isTrusted);
}
public onModelChanged(data: INotebookModelChangedData) {
diff --git a/src/sql/workbench/api/common/extHostNotebookEditor.ts b/src/sql/workbench/api/common/extHostNotebookEditor.ts
index 45e611acb73f..80fa6fdcbb67 100644
--- a/src/sql/workbench/api/common/extHostNotebookEditor.ts
+++ b/src/sql/workbench/api/common/extHostNotebookEditor.ts
@@ -82,6 +82,10 @@ export class NotebookEditorEdit {
return range;
}
+ setTrusted(isTrusted: boolean) {
+ this._document.setTrusted(isTrusted);
+ }
+
insertCell(value: Partial, index?: number, collapsed?: boolean): void {
if (index === null || index === undefined) {
// If not specified, assume adding to end of list
diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts
index 880478bf0388..3c526d8b429f 100644
--- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts
+++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts
@@ -890,6 +890,7 @@ export interface ExtHostNotebookDocumentsAndEditorsShape {
}
export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable {
+ $trySetTrusted(_uri: UriComponents, isTrusted: boolean): Thenable;
$trySaveDocument(uri: UriComponents): Thenable;
$tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise;
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): Promise;
diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts
index 4c19aad3eecf..a02a8218bdda 100644
--- a/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts
+++ b/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts
@@ -58,7 +58,7 @@ export class LinkHandlerDirective {
// ignore
}
if (uri && this.openerService && this.isSupportedLink(uri)) {
- if (uri.fragment && uri.fragment.length > 0 && uri.fsPath === this.workbenchFilePath.fsPath) {
+ if (uri.fragment && uri.fragment.length > 0 && uri.path === this.workbenchFilePath.path) {
this.notebookService.navigateTo(this.notebookUri, uri.fragment);
} else {
this.openerService.open(uri).catch(onUnexpectedError);
diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts
index c980e754356a..47fbf0ecd269 100644
--- a/src/sql/workbench/contrib/notebook/test/stubs.ts
+++ b/src/sql/workbench/contrib/notebook/test/stubs.ts
@@ -205,6 +205,9 @@ export class NotebookServiceStub implements INotebookService {
get languageMagics(): ILanguageMagic[] {
throw new Error('Method not implemented.');
}
+ setTrusted(notebookUri: URI, isTrusted: boolean): Promise {
+ throw new Error('Method not implemented.');
+ }
registerProvider(providerId: string, provider: INotebookProvider): void {
throw new Error('Method not implemented.');
}
diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts
index 4caa1832458d..33848c003144 100644
--- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts
+++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts
@@ -233,6 +233,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
public set trustedMode(isTrusted: boolean) {
this._trustedMode = isTrusted;
+
if (this._cells) {
this._cells.forEach(c => {
c.trustedMode = this._trustedMode;
@@ -290,6 +291,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
public async loadContents(isTrusted: boolean = false): Promise {
try {
this._trustedMode = isTrusted;
+
let contents = null;
if (this._notebookOptions && this._notebookOptions.contentManager) {
diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts
index ed4932bc671e..db1eb215e0c4 100644
--- a/src/sql/workbench/services/notebook/browser/notebookService.ts
+++ b/src/sql/workbench/services/notebook/browser/notebookService.ts
@@ -112,6 +112,13 @@ export interface INotebookService {
* @param sectionId ID of the section to navigate to
*/
navigateTo(notebookUri: URI, sectionId: string): void;
+
+ /**
+ * Sets the trusted mode for the sepcified notebook.
+ * @param notebookUri URI of the notebook to navigate to
+ * @param isTrusted True if notebook is to be set to trusted, false otherwise.
+ */
+ setTrusted(notebookUri: URI, isTrusted: boolean): Promise;
}
export interface INotebookProvider {
diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts
index 4c39089cdbd1..bfa7c0c1b681 100644
--- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts
+++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts
@@ -606,4 +606,25 @@ export class NotebookService extends Disposable implements INotebookService {
editor.navigateToSection(sectionId);
}
}
+
+ /**
+ * Trusts a notebook with the specified URI.
+ * @param notebookUri The notebook URI to set the trusted mode for.
+ * @param isTrusted True if the notebook is to be trusted, false otherwise.
+ */
+ async setTrusted(notebookUri: URI, isTrusted: boolean): Promise {
+ let editor = this.findNotebookEditor(notebookUri);
+
+ if (editor && editor.model) {
+ if (isTrusted) {
+ this._trustedCacheQueue.push(notebookUri);
+ } else {
+ this._unTrustedCacheQueue.push(notebookUri);
+ }
+ await this.updateTrustedCache();
+ editor.model.trustedMode = isTrusted;
+ }
+
+ return isTrusted;
+ }
}