diff options
Diffstat (limited to 'chromium/ui/webui/resources/js')
-rw-r--r-- | chromium/ui/webui/resources/js/BUILD.gn | 5 | ||||
-rw-r--r-- | chromium/ui/webui/resources/js/cr/ui/command.js | 2 | ||||
-rw-r--r-- | chromium/ui/webui/resources/js/load_time_data.js | 6 | ||||
-rw-r--r-- | chromium/ui/webui/resources/js/parse_html_subset.js | 155 | ||||
-rw-r--r-- | chromium/ui/webui/resources/js/plural_string_proxy.js | 33 |
5 files changed, 159 insertions, 42 deletions
diff --git a/chromium/ui/webui/resources/js/BUILD.gn b/chromium/ui/webui/resources/js/BUILD.gn index 42bcefc2b27..2eb7ada9411 100644 --- a/chromium/ui/webui/resources/js/BUILD.gn +++ b/chromium/ui/webui/resources/js/BUILD.gn @@ -167,6 +167,7 @@ js_type_check("js_resources_modules") { ":list_property_update_behavior.m", ":load_time_data.m", ":parse_html_subset.m", + ":plural_string_proxy", ":promise_resolver.m", ":search_highlight_utils.m", ":util.m", @@ -244,6 +245,10 @@ js_library("parse_html_subset.m") { extra_deps = [ ":modulize" ] } +js_library("plural_string_proxy") { + deps = [ ":cr.m" ] +} + js_library("promise_resolver.m") { sources = [ "$root_gen_dir/ui/webui/resources/js/promise_resolver.m.js" ] deps = [ ":assert.m" ] diff --git a/chromium/ui/webui/resources/js/cr/ui/command.js b/chromium/ui/webui/resources/js/cr/ui/command.js index 4f63fbcfab2..4d467c0d66f 100644 --- a/chromium/ui/webui/resources/js/cr/ui/command.js +++ b/chromium/ui/webui/resources/js/cr/ui/command.js @@ -193,7 +193,7 @@ cr.define('cr.ui', function() { const target = e.target; // Ignore focus on a menu button or command item. - if (target.menu || target.command) { + if (target.menu || target.command || (target instanceof cr.ui.MenuItem)) { return; } diff --git a/chromium/ui/webui/resources/js/load_time_data.js b/chromium/ui/webui/resources/js/load_time_data.js index 94545cafa27..a4e3054b620 100644 --- a/chromium/ui/webui/resources/js/load_time_data.js +++ b/chromium/ui/webui/resources/js/load_time_data.js @@ -19,9 +19,9 @@ /** * @typedef {{ - * substitutions: (Array<string>|undefined), - * attrs: (Object<function(Node, string):boolean>|undefined), - * tags: (Array<string>|undefined), + * substitutions: (!Array<string>|undefined), + * attrs: (!Array<string>|undefined), + * tags: (!Array<string>|undefined), * }} */ /* #export */ let SanitizeInnerHtmlOpts; diff --git a/chromium/ui/webui/resources/js/parse_html_subset.js b/chromium/ui/webui/resources/js/parse_html_subset.js index c5869640195..a1432098eb9 100644 --- a/chromium/ui/webui/resources/js/parse_html_subset.js +++ b/chromium/ui/webui/resources/js/parse_html_subset.js @@ -3,11 +3,11 @@ // found in the LICENSE file. /** - * Parses a very small subset of HTML. This ensures that insecure HTML / - * javascript cannot be injected into the new tab page. + * Parses a very small subset of HTML. This ensures that insecure HTML / + * javascript cannot be injected into WebUI. * @param {string} s The string to parse. - * @param {Array<string>=} opt_extraTags Optional extra allowed tags. - * @param {Object<function(Node, string):boolean>=} opt_extraAttrs + * @param {!Array<string>=} opt_extraTags Optional extra allowed tags. + * @param {!Array<string>=} opt_extraAttrs * Optional extra allowed attributes (all tags are run through these). * @throws {Error} In case of non supported markup. * @return {DocumentFragment} A document fragment containing the DOM tree. @@ -15,38 +15,114 @@ /* #export */ const parseHtmlSubset = (function() { 'use strict'; - const allowedAttributes = { - 'href'(node, value) { - // Only allow a[href] starting with chrome:// and https:// - return node.tagName === 'A' && - (value.startsWith('chrome://') || value.startsWith('https://')); - }, - 'target'(node, value) { - // Only allow a[target='_blank']. - // TODO(dbeam): are there valid use cases for target !== '_blank'? - return node.tagName === 'A' && value === '_blank'; - }, - }; + /** @typedef {function(!Node, string):boolean} */ + let AllowFunction; + + /** @type {!AllowFunction} */ + const allowAttribute = (node, value) => true; /** - * Whitelist of tag names allowed in parseHtmlSubset. - * @type {!Array<string>} + * Allow-list of attributes in parseHtmlSubset. + * @type {!Map<string, !AllowFunction>} * @const */ - const allowedTags = ['A', 'B', 'SPAN', 'STRONG']; - - /** @param {...Object} var_args Objects to merge. */ - function merge(var_args) { - const clone = {}; - for (let i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] === 'object') { - for (const key in arguments[i]) { - if (arguments[i].hasOwnProperty(key)) { - clone[key] = arguments[i][key]; - } - } + const allowedAttributes = new Map([ + [ + 'href', + (node, value) => { + // Only allow a[href] starting with chrome:// and https:// + return node.tagName === 'A' && + (value.startsWith('chrome://') || value.startsWith('https://')); } - } + ], + [ + 'target', + (node, value) => { + // Only allow a[target='_blank']. + // TODO(dbeam): are there valid use cases for target !== '_blank'? + return node.tagName === 'A' && value === '_blank'; + } + ], + ]); + + /** + * Allow-list of optional attributes in parseHtmlSubset. + * @type {!Map<string, !AllowFunction>} + * @const + */ + const allowedOptionalAttributes = new Map([ + ['class', allowAttribute], + ['id', allowAttribute], + ['is', (node, value) => value === 'action-link' || value === ''], + ['role', (node, value) => value === 'link'], + [ + 'src', + (node, value) => { + // Only allow img[src] starting with chrome:// + return node.tagName === 'IMG' && value.startsWith('chrome://'); + } + ], + ['tabindex', allowAttribute], + ]); + + /** + * Allow-list of tag names in parseHtmlSubset. + * @type {!Set<string>} + * @const + */ + const allowedTags = + new Set(['A', 'B', 'BR', 'DIV', 'P', 'PRE', 'SPAN', 'STRONG']); + + /** + * Allow-list of optional tag names in parseHtmlSubset. + * @type {!Set<string>} + * @const + */ + const allowedOptionalTags = new Set(['IMG']); + + /** + * This is used to create TrustedHTML. + * @type {TrustedTypePolicy|undefined} + */ + let untrustedHTMLPolicy; + if (window.trustedTypes) { + untrustedHTMLPolicy = trustedTypes.createPolicy('parse-html-subset', { + createHTML: untrustedHTML => { + // This is safe because the untrusted HTML will be sanitized + // later in this function. We are adding this so that + // the sanitization will not cause a Trusted Types violation. + return untrustedHTML; + }, + }); + } + + /** + * @param {!Array<string>} optTags an Array to merge. + * @return {!Set<string>} Set of allowed tags. + */ + function mergeTags(optTags) { + const clone = new Set(allowedTags); + optTags.forEach(str => { + const tag = str.toUpperCase(); + if (allowedOptionalTags.has(tag)) { + clone.add(tag); + } + }); + return clone; + } + + /** + * @param {!Array<string>} optAttrs an Array to merge. + * @return {!Map<string, !AllowFunction>} Map of allowed + * attributes. + */ + function mergeAttrs(optAttrs) { + const clone = new Map([...allowedAttributes]); + optAttrs.forEach(key => { + if (allowedOptionalAttributes.has(key)) { + clone.set(key, allowedOptionalAttributes.get(key)); + } + }); return clone; } @@ -58,7 +134,7 @@ } function assertElement(tags, node) { - if (tags.indexOf(node.tagName) === -1) { + if (!tags.has(node.tagName)) { throw Error(node.tagName + ' is not supported'); } } @@ -66,21 +142,24 @@ function assertAttribute(attrs, attrNode, node) { const n = attrNode.nodeName; const v = attrNode.nodeValue; - if (!attrs.hasOwnProperty(n) || !attrs[n](node, v)) { + if (!attrs.has(n) || !attrs.get(n)(node, v)) { throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); } } return function(s, opt_extraTags, opt_extraAttrs) { - const extraTags = (opt_extraTags || []).map(function(str) { - return str.toUpperCase(); - }); - const tags = allowedTags.concat(extraTags); - const attrs = merge(allowedAttributes, opt_extraAttrs || {}); + const tags = opt_extraTags ? mergeTags(opt_extraTags) : allowedTags; + const attrs = + opt_extraAttrs ? mergeAttrs(opt_extraAttrs) : allowedAttributes; const doc = document.implementation.createHTMLDocument(''); const r = doc.createRange(); r.selectNode(doc.body); + + if (window.trustedTypes) { + s = untrustedHTMLPolicy.createHTML(s); + } + // This does not execute any scripts because the document has no view. const df = r.createContextualFragment(s); walk(df, function(node) { diff --git a/chromium/ui/webui/resources/js/plural_string_proxy.js b/chromium/ui/webui/resources/js/plural_string_proxy.js new file mode 100644 index 00000000000..5d38a70d862 --- /dev/null +++ b/chromium/ui/webui/resources/js/plural_string_proxy.js @@ -0,0 +1,33 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview A helper object used to get a pluralized string. + */ + +// clang-format off +import {addSingletonGetter, sendWithPromise} from './cr.m.js'; +// clang-format on + +/** @interface */ +export class PluralStringProxy { + /** + * Obtains a pluralized string for |messageName| with |itemCount| items. + * @param {!string} messageName The name of the message. + * @param {!number} itemCount The number of items. + * @return {!Promise<string>} Promise resolved with the appropriate plural + * string for |messageName| with |itemCount| items. + */ + getPluralString(messageName, itemCount) {} +} + +/** @implements {PluralStringProxy} */ +export class PluralStringProxyImpl { + /** @override */ + getPluralString(messageName, itemCount) { + return sendWithPromise('getPluralString', messageName, itemCount); + } +} + +addSingletonGetter(PluralStringProxyImpl); |