diff --git a/README.md b/README.md index 5d5ee55c63a..e45f7ff9e75 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Visual Studio Code - Open Source [![Build Status](https://travis-ci.org/Microsoft/vscode.svg?branch=master)](https://travis-ci.org/Microsoft/vscode) -[![Build status](https://ci.appveyor.com/api/projects/status/vuhlhg80tj3e2a0l?svg=true)](https://ci.appveyor.com/project/VSCode/vscode) +[![Build Status](https://ci.appveyor.com/api/projects/status/vuhlhg80tj3e2a0l/branch/master?svg=true)](https://ci.appveyor.com/project/VSCode/vscode) [![Coverage Status](https://img.shields.io/coveralls/Microsoft/vscode/master.svg)](https://coveralls.io/github/Microsoft/vscode?branch=master) [![Gitter](https://img.shields.io/badge/chat-on%20gitter-blue.svg)](https://gitter.im/Microsoft/vscode) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index f7ce869e469..3b90f377b7e 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -40,7 +40,7 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.11.0' }, + { name: 'ms-vscode.node-debug', version: '1.11.1' }, { name: 'ms-vscode.node-debug2', version: '1.11.0' } ]; diff --git a/build/lib/compilation.js b/build/lib/compilation.js index cb61bfedfce..66078676a5a 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -118,27 +118,32 @@ function reloadTypeScriptNodeModule() { }); } function monacodtsTask(out, isWatch) { - var timer = null; - var runSoon = function (howSoon) { - if (timer !== null) { - clearTimeout(timer); - timer = null; + var neededFiles = {}; + monacodts.getFilesToWatch(out).forEach(function (filePath) { + filePath = path.normalize(filePath); + neededFiles[filePath] = true; + }); + var inputFiles = {}; + for (var filePath in neededFiles) { + if (/\bsrc(\/|\\)vs\b/.test(filePath)) { + // This file is needed from source => simply read it now + inputFiles[filePath] = fs.readFileSync(filePath).toString(); + } + } + var setInputFile = function (filePath, contents) { + if (inputFiles[filePath] === contents) { + // no change + return; + } + inputFiles[filePath] = contents; + var neededInputFilesCount = Object.keys(neededFiles).length; + var availableInputFilesCount = Object.keys(inputFiles).length; + if (neededInputFilesCount === availableInputFilesCount) { + run(); } - timer = setTimeout(function () { - timer = null; - runNow(); - }, howSoon); }; - var runNow = function () { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - // if (reporter.hasErrors()) { - // monacodts.complainErrors(); - // return; - // } - var result = monacodts.run(out); + var run = function () { + var result = monacodts.run(out, inputFiles); if (!result.isTheSame) { if (isWatch) { fs.writeFileSync(result.filePath, result.content); @@ -150,26 +155,16 @@ function monacodtsTask(out, isWatch) { }; var resultStream; if (isWatch) { - var filesToWatchMap_1 = {}; - monacodts.getFilesToWatch(out).forEach(function (filePath) { - filesToWatchMap_1[path.normalize(filePath)] = true; - }); watch('build/monaco/*').pipe(es.through(function () { - runSoon(5000); + run(); })); - resultStream = es.through(function (data) { - var filePath = path.normalize(data.path); - if (filesToWatchMap_1[filePath]) { - runSoon(5000); - } - this.emit('data', data); - }); - } - else { - resultStream = es.through(null, function () { - runNow(); - this.emit('end'); - }); } + resultStream = es.through(function (data) { + var filePath = path.normalize(data.path); + if (neededFiles[filePath]) { + setInputFile(filePath, data.contents.toString()); + } + this.emit('data', data); + }); return resultStream; } diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index dd812e6c4ae..be2d8820f5c 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -150,29 +150,36 @@ function reloadTypeScriptNodeModule(): NodeJS.ReadWriteStream { } function monacodtsTask(out: string, isWatch: boolean): NodeJS.ReadWriteStream { - let timer: NodeJS.Timer = null; - const runSoon = function (howSoon: number) { - if (timer !== null) { - clearTimeout(timer); - timer = null; + const neededFiles: { [file: string]: boolean; } = {}; + monacodts.getFilesToWatch(out).forEach(function (filePath) { + filePath = path.normalize(filePath); + neededFiles[filePath] = true; + }); + + const inputFiles: { [file: string]: string; } = {}; + for (let filePath in neededFiles) { + if (/\bsrc(\/|\\)vs\b/.test(filePath)) { + // This file is needed from source => simply read it now + inputFiles[filePath] = fs.readFileSync(filePath).toString(); + } + } + + const setInputFile = (filePath: string, contents:string) => { + if (inputFiles[filePath] === contents) { + // no change + return; + } + inputFiles[filePath] = contents; + const neededInputFilesCount = Object.keys(neededFiles).length; + const availableInputFilesCount = Object.keys(inputFiles).length; + if (neededInputFilesCount === availableInputFilesCount) { + run(); } - timer = setTimeout(function () { - timer = null; - runNow(); - }, howSoon); }; - const runNow = function () { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - // if (reporter.hasErrors()) { - // monacodts.complainErrors(); - // return; - // } - const result = monacodts.run(out); + const run = () => { + const result = monacodts.run(out, inputFiles); if (!result.isTheSame) { if (isWatch) { fs.writeFileSync(result.filePath, result.content); @@ -185,32 +192,18 @@ function monacodtsTask(out: string, isWatch: boolean): NodeJS.ReadWriteStream { let resultStream: NodeJS.ReadWriteStream; if (isWatch) { - - const filesToWatchMap: { [file: string]: boolean; } = {}; - monacodts.getFilesToWatch(out).forEach(function (filePath) { - filesToWatchMap[path.normalize(filePath)] = true; - }); - watch('build/monaco/*').pipe(es.through(function () { - runSoon(5000); + run(); })); - - resultStream = es.through(function (data) { - const filePath = path.normalize(data.path); - if (filesToWatchMap[filePath]) { - runSoon(5000); - } - this.emit('data', data); - }); - - } else { - - resultStream = es.through(null, function () { - runNow(); - this.emit('end'); - }); - } + resultStream = es.through(function (data) { + const filePath = path.normalize(data.path); + if (neededFiles[filePath]) { + setInputFile(filePath, data.contents.toString()); + } + this.emit('data', data); + }); + return resultStream; } diff --git a/build/monaco/api.js b/build/monaco/api.js index 393aa8b28db..db8852e1f90 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -34,17 +34,14 @@ function moduleIdToPath(out, moduleId) { return path.join(OUT_ROOT, out, moduleId) + '.d.ts'; } var SOURCE_FILE_MAP = {}; -function getSourceFile(out, moduleId) { +function getSourceFile(out, inputFiles, moduleId) { if (!SOURCE_FILE_MAP[moduleId]) { - var filePath = moduleIdToPath(out, moduleId); - var fileContents = void 0; - try { - fileContents = fs.readFileSync(filePath).toString(); - } - catch (err) { - logErr('CANNOT FIND FILE ' + filePath); + var filePath = path.normalize(moduleIdToPath(out, moduleId)); + if (!inputFiles.hasOwnProperty(filePath)) { + logErr('CANNOT FIND FILE ' + filePath + '. YOU MIGHT NEED TO RESTART gulp'); return null; } + var fileContents = inputFiles[filePath]; var sourceFile = ts.createSourceFile(filePath, fileContents, ts.ScriptTarget.ES5); SOURCE_FILE_MAP[moduleId] = sourceFile; } @@ -202,7 +199,7 @@ function format(text) { insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: true, placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, + placeOpenBraceOnNewLineForControlBlocks: false }; } } @@ -228,7 +225,7 @@ function createReplacer(data) { return str; }; } -function generateDeclarationFile(out, recipe) { +function generateDeclarationFile(out, inputFiles, recipe) { var lines = recipe.split(/\r\n|\n|\r/); var result = []; lines.forEach(function (line) { @@ -236,7 +233,7 @@ function generateDeclarationFile(out, recipe) { if (m1) { CURRENT_PROCESSING_RULE = line; var moduleId = m1[1]; - var sourceFile_1 = getSourceFile(out, moduleId); + var sourceFile_1 = getSourceFile(out, inputFiles, moduleId); if (!sourceFile_1) { return; } @@ -260,7 +257,7 @@ function generateDeclarationFile(out, recipe) { if (m2) { CURRENT_PROCESSING_RULE = line; var moduleId = m2[1]; - var sourceFile_2 = getSourceFile(out, moduleId); + var sourceFile_2 = getSourceFile(out, inputFiles, moduleId); if (!sourceFile_2) { return; } @@ -326,11 +323,11 @@ function getFilesToWatch(out) { return result; } exports.getFilesToWatch = getFilesToWatch; -function run(out) { +function run(out, inputFiles) { log('Starting monaco.d.ts generation'); SOURCE_FILE_MAP = {}; var recipe = fs.readFileSync(RECIPE_PATH).toString(); - var result = generateDeclarationFile(out, recipe); + var result = generateDeclarationFile(out, inputFiles, recipe); var currentContent = fs.readFileSync(DECLARATION_PATH).toString(); log('Finished monaco.d.ts generation'); return { diff --git a/build/monaco/api.ts b/build/monaco/api.ts index ff227fd0156..57be4e5874f 100644 --- a/build/monaco/api.ts +++ b/build/monaco/api.ts @@ -31,18 +31,16 @@ function moduleIdToPath(out:string, moduleId:string): string { } let SOURCE_FILE_MAP: {[moduleId:string]:ts.SourceFile;} = {}; -function getSourceFile(out:string, moduleId:string): ts.SourceFile { +function getSourceFile(out:string, inputFiles: { [file: string]: string; }, moduleId:string): ts.SourceFile { if (!SOURCE_FILE_MAP[moduleId]) { - let filePath = moduleIdToPath(out, moduleId); + let filePath = path.normalize(moduleIdToPath(out, moduleId)); - let fileContents: string; - try { - fileContents = fs.readFileSync(filePath).toString(); - } catch (err) { - logErr('CANNOT FIND FILE ' + filePath); + if (!inputFiles.hasOwnProperty(filePath)) { + logErr('CANNOT FIND FILE ' + filePath + '. YOU MIGHT NEED TO RESTART gulp'); return null; } + let fileContents = inputFiles[filePath]; let sourceFile = ts.createSourceFile(filePath, fileContents, ts.ScriptTarget.ES5); SOURCE_FILE_MAP[moduleId] = sourceFile; @@ -262,7 +260,7 @@ function createReplacer(data:string): (str:string)=>string { }; } -function generateDeclarationFile(out:string, recipe:string): string { +function generateDeclarationFile(out: string, inputFiles: { [file: string]: string; }, recipe:string): string { let lines = recipe.split(/\r\n|\n|\r/); let result = []; @@ -273,7 +271,7 @@ function generateDeclarationFile(out:string, recipe:string): string { if (m1) { CURRENT_PROCESSING_RULE = line; let moduleId = m1[1]; - let sourceFile = getSourceFile(out, moduleId); + let sourceFile = getSourceFile(out, inputFiles, moduleId); if (!sourceFile) { return; } @@ -300,7 +298,7 @@ function generateDeclarationFile(out:string, recipe:string): string { if (m2) { CURRENT_PROCESSING_RULE = line; let moduleId = m2[1]; - let sourceFile = getSourceFile(out, moduleId); + let sourceFile = getSourceFile(out, inputFiles, moduleId); if (!sourceFile) { return; } @@ -383,12 +381,12 @@ export interface IMonacoDeclarationResult { isTheSame: boolean; } -export function run(out:string): IMonacoDeclarationResult { +export function run(out: string, inputFiles: { [file: string]: string; }): IMonacoDeclarationResult { log('Starting monaco.d.ts generation'); SOURCE_FILE_MAP = {}; let recipe = fs.readFileSync(RECIPE_PATH).toString(); - let result = generateDeclarationFile(out, recipe); + let result = generateDeclarationFile(out, inputFiles, recipe); let currentContent = fs.readFileSync(DECLARATION_PATH).toString(); log('Finished monaco.d.ts generation'); diff --git a/extensions/html/server/npm-shrinkwrap.json b/extensions/html/server/npm-shrinkwrap.json index f0a0b4ffd9d..775f46c0b47 100644 --- a/extensions/html/server/npm-shrinkwrap.json +++ b/extensions/html/server/npm-shrinkwrap.json @@ -8,9 +8,9 @@ "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.0.0.tgz" }, "vscode-html-languageservice": { - "version": "2.0.2-next.1", + "version": "2.0.2-next.2", "from": "vscode-html-languageservice@next", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.2-next.1.tgz" + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.2-next.2.tgz" }, "vscode-jsonrpc": { "version": "3.1.0-alpha.1", diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json index e832be45586..e8c38a75bce 100644 --- a/extensions/html/server/package.json +++ b/extensions/html/server/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "vscode-css-languageservice": "^2.0.0", - "vscode-html-languageservice": "^2.0.2-next.1", + "vscode-html-languageservice": "^2.0.2-next.2", "vscode-languageserver": "^3.1.0-alpha.1", "vscode-nls": "^2.0.2", "vscode-uri": "^1.0.0" diff --git a/extensions/markdown/media/csp.js b/extensions/markdown/media/csp.js new file mode 100644 index 00000000000..f43b14c8d77 --- /dev/null +++ b/extensions/markdown/media/csp.js @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +(function () { + const settings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-settings')); + const strings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-strings')); + + let didShow = false; + + document.addEventListener('securitypolicyviolation', () => { + if (didShow) { + return; + } + didShow = true; + const args = [settings.previewUri]; + + const notification = document.createElement('a'); + notification.innerText = strings.cspAlertMessageText; + notification.setAttribute('id', 'code-csp-warning'); + notification.setAttribute('title', strings.cspAlertMessageTitle); + + notification.setAttribute('role', 'button'); + notification.setAttribute('aria-label', strings.cspAlertMessageLabel); + notification.setAttribute('href', `command:markdown.showPreviewSecuritySelector?${encodeURIComponent(JSON.stringify(args))}`); + + document.body.appendChild(notification); + }); +}()); \ No newline at end of file diff --git a/extensions/markdown/media/main.js b/extensions/markdown/media/main.js index c4614d853aa..f927c0fa90f 100644 --- a/extensions/markdown/media/main.js +++ b/extensions/markdown/media/main.js @@ -89,7 +89,7 @@ function scrollToRevealSourceLine(line) { const {previous, next} = getElementsForSourceLine(line); marker.update(previous && previous.element); - if (previous && window.initialData.scrollPreviewWithEditorSelection) { + if (previous && settings.scrollPreviewWithEditorSelection) { let scrollTo = 0; if (next) { // Between two elements. Go to percentage offset between them. @@ -141,10 +141,11 @@ var scrollDisabled = true; var marker = new ActiveLineMarker(); + const settings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-settings')); window.onload = () => { - if (window.initialData.scrollPreviewWithEditorSelection) { - const initialLine = +window.initialData.line; + if (settings.scrollPreviewWithEditorSelection) { + const initialLine = +settings.line; if (!isNaN(initialLine)) { setTimeout(() => { scrollDisabled = true; @@ -172,7 +173,7 @@ })(), false); document.addEventListener('dblclick', event => { - if (!window.initialData.doubleClickToSwitchToEditor) { + if (!settings.doubleClickToSwitchToEditor) { return; } @@ -186,7 +187,7 @@ const offset = event.pageY; const line = getEditorLineNumberForPageOffset(offset); if (!isNaN(line)) { - const args = [window.initialData.source, line]; + const args = [settings.source, line]; window.parent.postMessage({ command: "did-click-link", data: `command:_markdown.didClick?${encodeURIComponent(JSON.stringify(args))}` @@ -194,14 +195,14 @@ } }); - if (window.initialData.scrollEditorWithPreview) { + if (settings.scrollEditorWithPreview) { window.addEventListener('scroll', throttle(() => { if (scrollDisabled) { scrollDisabled = false; } else { const line = getEditorLineNumberForPageOffset(window.scrollY); if (!isNaN(line)) { - const args = [window.initialData.source, line]; + const args = [settings.source, line]; window.parent.postMessage({ command: "did-click-link", data: `command:_markdown.revealLine?${encodeURIComponent(JSON.stringify(args))}` diff --git a/extensions/markdown/media/markdown.css b/extensions/markdown/media/markdown.css index 7cefe421dc6..dfb4666bd19 100644 --- a/extensions/markdown/media/markdown.css +++ b/extensions/markdown/media/markdown.css @@ -11,6 +11,28 @@ body { word-wrap: break-word; } +#code-csp-warning { + position: fixed; + top: 0; + right: 0; + color: white; + margin: 16px; + text-align: center; + font-size: 12px; + font-family: sans-serif; + background-color:#444444; + cursor: pointer; + padding: 6px; + box-shadow: 1px 1px 1px rgba(0,0,0,.25); +} + +#code-csp-warning:hover { + text-decoration: none; + background-color:#007acc; + box-shadow: 2px 2px 2px rgba(0,0,0,.25); +} + + body.scrollBeyondLastLine { margin-bottom: calc(100vh - 22px); } diff --git a/extensions/markdown/npm-shrinkwrap.json b/extensions/markdown/npm-shrinkwrap.json index 0f93176c555..de14b48e12d 100644 --- a/extensions/markdown/npm-shrinkwrap.json +++ b/extensions/markdown/npm-shrinkwrap.json @@ -62,6 +62,11 @@ "from": "vscode-extension-telemetry@>=0.0.6 <0.0.7", "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.6.tgz" }, + "vscode-nls": { + "version": "2.0.2", + "from": "vscode-nls@latest", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-2.0.2.tgz" + }, "winreg": { "version": "1.2.3", "from": "winreg@1.2.3", diff --git a/extensions/markdown/package.json b/extensions/markdown/package.json index 593c213056b..713e53b873d 100644 --- a/extensions/markdown/package.json +++ b/extensions/markdown/package.json @@ -69,6 +69,11 @@ "light": "./media/ViewSource.svg", "dark": "./media/ViewSource_inverse.svg" } + }, + { + "command": "markdown.showPreviewSecuritySelector", + "title": "%markdown.showPreviewSecuritySelector.title%", + "category": "Markdown" } ], "menus": { @@ -83,6 +88,10 @@ "when": "resourceScheme == markdown", "command": "markdown.showSource", "group": "navigation" + }, + { + "when": "resourceScheme == markdown", + "command": "markdown.showPreviewSecuritySelector" } ], "explorer/context": [ @@ -183,7 +192,8 @@ "highlight.js": "^9.3.0", "markdown-it": "^8.2.2", "markdown-it-named-headers": "0.0.4", - "vscode-extension-telemetry": "^0.0.6" + "vscode-extension-telemetry": "^0.0.6", + "vscode-nls": "^2.0.2" }, "devDependencies": { "@types/node": "^7.0.4" diff --git a/extensions/markdown/package.nls.json b/extensions/markdown/package.nls.json index 253ab8910ef..c79bb2952a9 100644 --- a/extensions/markdown/package.nls.json +++ b/extensions/markdown/package.nls.json @@ -10,5 +10,6 @@ "markdown.previewFrontMatter.dec": "Sets how YAML front matter should be rendered in the markdown preview. 'hide' removes the front matter. Otherwise, the front matter is treated as markdown content.", "markdown.previewSide.title" : "Open Preview to the Side", "markdown.showSource.title" : "Show Source", - "markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the markdown preview. Relative paths are interpreted relative to the folder open in the explorer. If there is no open folder, they are interpreted relative to the location of the markdown file. All '\\' need to be written as '\\\\'." + "markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the markdown preview. Relative paths are interpreted relative to the folder open in the explorer. If there is no open folder, they are interpreted relative to the location of the markdown file. All '\\' need to be written as '\\\\'.", + "markdown.showPreviewSecuritySelector.title": "Change Markdown Preview Security Settings" } \ No newline at end of file diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 354d380c7fb..1220ffe9f0c 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -11,9 +11,14 @@ import TelemetryReporter from 'vscode-extension-telemetry'; import { MarkdownEngine } from './markdownEngine'; import DocumentLinkProvider from './documentLinkProvider'; import MDDocumentSymbolProvider from './documentSymbolProvider'; -import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile } from './previewContentProvider'; +import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile, ContentSecurityPolicyArbiter } from './previewContentProvider'; import { TableOfContentsProvider } from './tableOfContentsProvider'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + + interface IPackageInfo { name: string; version: string; @@ -25,6 +30,37 @@ interface OpenDocumentLinkArgs { fragment: string; } +enum PreviewSecuritySelection { + None, + DisableEnhancedSecurityForWorkspace, + EnableEnhancedSecurityForWorkspace +} + +interface PreviewSecurityPickItem extends vscode.QuickPickItem { + id: PreviewSecuritySelection; +} + +class ExtensionContentSecurityProlicyArbiter implements ContentSecurityPolicyArbiter { + private readonly key = 'trusted_preview_workspace:'; + + constructor( + private globalState: vscode.Memento + ) { } + + public isEnhancedSecurityDisableForWorkspace(): boolean { + return this.globalState.get(this.key + vscode.workspace.rootPath, false); + } + + public addTrustedWorkspace(rootPath: string): Thenable { + return this.globalState.update(this.key + rootPath, true); + } + + public removeTrustedWorkspace(rootPath: string): Thenable { + return this.globalState.update(this.key + rootPath, false); + } + +} + var telemetryReporter: TelemetryReporter | null; export function activate(context: vscode.ExtensionContext) { @@ -34,9 +70,10 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(telemetryReporter); } + const cspArbiter = new ExtensionContentSecurityProlicyArbiter(context.globalState); const engine = new MarkdownEngine(); - const contentProvider = new MDDocumentContentProvider(engine, context); + const contentProvider = new MDDocumentContentProvider(engine, context, cspArbiter); const contentProviderRegistration = vscode.workspace.registerTextDocumentContentProvider('markdown', contentProvider); const symbolsProvider = new MDDocumentSymbolProvider(engine); @@ -64,7 +101,7 @@ export function activate(context: vscode.ExtensionContext) { }); })); - context.subscriptions.push(vscode.commands.registerCommand('_markdown.didClick', (uri, line) => { + context.subscriptions.push(vscode.commands.registerCommand('_markdown.didClick', (uri: string, line) => { const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); return vscode.workspace.openTextDocument(sourceUri) .then(document => vscode.window.showTextDocument(document)) @@ -100,6 +137,68 @@ export function activate(context: vscode.ExtensionContext) { } })); + context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewSecuritySelector', (resource: string | undefined) => { + const workspacePath = vscode.workspace.rootPath || resource; + if (!workspacePath) { + return; + } + + let sourceUri: vscode.Uri | null = null; + if (resource) { + sourceUri = vscode.Uri.parse(decodeURIComponent(resource)); + } + + if (!sourceUri && vscode.window.activeTextEditor) { + const activeDocument = vscode.window.activeTextEditor.document; + if (activeDocument.uri.scheme === 'markdown') { + sourceUri = activeDocument.uri; + } else { + sourceUri = getMarkdownUri(activeDocument.uri); + } + } + + vscode.window.showQuickPick( + [ + { + id: PreviewSecuritySelection.EnableEnhancedSecurityForWorkspace, + label: localize( + 'preview.showPreviewSecuritySelector.disallowScriptsForWorkspaceTitle', + 'Disable script execution in markdown previews for this workspace'), + description: '', + detail: cspArbiter.isEnhancedSecurityDisableForWorkspace() + ? '' + : localize('preview.showPreviewSecuritySelector.currentSelection', 'Current setting') + }, { + id: PreviewSecuritySelection.DisableEnhancedSecurityForWorkspace, + label: localize( + 'preview.showPreviewSecuritySelector.allowScriptsForWorkspaceTitle', + 'Enable script execution in markdown previews for this workspace'), + description: '', + detail: cspArbiter.isEnhancedSecurityDisableForWorkspace() + ? localize('preview.showPreviewSecuritySelector.currentSelection', 'Current setting') + : '' + }, + ], { + placeHolder: localize('preview.showPreviewSecuritySelector.title', 'Change security settings for the Markdown preview'), + }).then(selection => { + if (!workspacePath) { + return false; + } + switch (selection && selection.id) { + case PreviewSecuritySelection.DisableEnhancedSecurityForWorkspace: + return cspArbiter.addTrustedWorkspace(workspacePath).then(() => true); + + case PreviewSecuritySelection.EnableEnhancedSecurityForWorkspace: + return cspArbiter.removeTrustedWorkspace(workspacePath).then(() => true); + } + return false; + }).then(shouldUpdate => { + if (shouldUpdate && sourceUri) { + contentProvider.update(sourceUri); + } + }); + })); + context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { if (isMarkdownFile(document)) { const uri = getMarkdownUri(document.uri); diff --git a/extensions/markdown/src/previewContentProvider.ts b/extensions/markdown/src/previewContentProvider.ts index 6a65997b19f..0c50c8727c0 100644 --- a/extensions/markdown/src/previewContentProvider.ts +++ b/extensions/markdown/src/previewContentProvider.ts @@ -9,6 +9,13 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { MarkdownEngine } from './markdownEngine'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export interface ContentSecurityPolicyArbiter { + isEnhancedSecurityDisableForWorkspace(): boolean; +} + export function isMarkdownFile(document: vscode.TextDocument) { return document.languageId === 'markdown' && document.uri.scheme !== 'markdown'; // prevent processing of own documents @@ -24,7 +31,8 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv constructor( private engine: MarkdownEngine, - private context: vscode.ExtensionContext + private context: vscode.ExtensionContext, + private cspArbiter: ContentSecurityPolicyArbiter ) { } private getMediaPath(mediaFile: string): string { @@ -60,7 +68,7 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString(); } - private computeCustomStyleSheetIncludes(uri: vscode.Uri): string { + private computeCustomStyleSheetIncludes(uri: vscode.Uri, _nonce: string): string { const styles = vscode.workspace.getConfiguration('markdown')['styles']; if (styles && Array.isArray(styles) && styles.length > 0) { return styles.map((style) => { @@ -70,13 +78,13 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv return ''; } - private getSettingsOverrideStyles(): string { + private getSettingsOverrideStyles(nonce: string): string { const previewSettings = vscode.workspace.getConfiguration('markdown')['preview']; if (!previewSettings) { return ''; } const { fontFamily, fontSize, lineHeight } = previewSettings; - return `