Merge pull request #5295 from Polymer/strict-template-policy-2.x

[2.x] Introduce strictTemplatePolicy
This commit is contained in:
Kevin Schaaf
2018-11-05 11:31:39 -08:00
committed by GitHub
9 changed files with 574 additions and 34 deletions
+3
View File
@@ -57,6 +57,9 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
constructor() {
super();
if (Polymer.strictTemplatePolicy) {
throw new Error(`strictTemplatePolicy: dom-bind not allowed`);
}
this.root = null;
this.$ = null;
this.__children = null;
+6 -2
View File
@@ -255,8 +255,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
if (c$ && c$.length) {
// use first child parent, for case when dom-if may have been detached
let parent = c$[0].parentNode;
for (let i=0, n; (i<c$.length) && (n=c$[i]); i++) {
parent.removeChild(n);
// Instance children may be disconnected from parents when dom-if
// detaches if a tree was innerHTML'ed
if (parent) {
for (let i=0, n; (i<c$.length) && (n=c$[i]); i++) {
parent.removeChild(n);
}
}
}
this.__instance = null;
+13 -5
View File
@@ -15,6 +15,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
let modules = {};
let lcModules = {};
function setModule(id, module) {
// store id separate from lowercased id so that
// in all cases mixedCase id will stored distinctly
// and lowercase version is a fallback
modules[id] = lcModules[id.toLowerCase()] = module;
}
function findModule(id) {
return modules[id] || lcModules[id.toLowerCase()];
}
@@ -124,12 +130,14 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
register(id) {
id = id || this.id;
if (id) {
// Under strictTemplatePolicy, reject and null out any re-registered
// dom-module since it is ambiguous whether first-in or last-in is trusted
if (Polymer.strictTemplatePolicy && findModule(id) !== undefined) {
setModule(id, null);
throw new Error(`strictTemplatePolicy: dom-module ${id} re-registered`);
}
this.id = id;
// store id separate from lowercased id so that
// in all cases mixedCase id will stored distinctly
// and lowercase version is a fallback
modules[id] = this;
lcModules[id.toLowerCase()] = this;
setModule(id, this);
styleOutsideTemplateCheck(this);
}
}
+9 -16
View File
@@ -153,22 +153,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
return info.observers;
}
/**
* @return {HTMLTemplateElement} template for this class
*/
static get template() {
// get template first from any imperative set in `info._template`
return info._template ||
// next look in dom-module associated with this element's is.
Polymer.DomModule && Polymer.DomModule.import(this.is, 'template') ||
// next look for superclass template (note: use superclass symbol
// to ensure correct `this.is`)
Base.template ||
// finally fall back to `_template` in element's prototype.
this.prototype._template ||
null;
}
/**
* @return {void}
*/
@@ -371,6 +355,15 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
const klass = GenerateClassFromInfo(info, baseWithMixin);
// decorate klass with registration info
klass.is = info.is;
// To match 1.x behavior, `_template` supplied on a behavior should take
// precedence over dom-module lookup for `is`; this also prevents
// dom-module injection under strictTemplatePolicy for an element that
// would normally get its template from a behavior.
// TODO(sorvell): Remove once "flattened behaviors" lands, since both
// `_template` and `is` will be on the same class after that change
if (klass.prototype._template !== undefined) {
klass.prototype._template = klass.prototype._template;
}
return klass;
};
+58 -11
View File
@@ -130,7 +130,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
/**
* Returns a memoized version of the the `observers` array.
* Returns a memoized version of the `observers` array.
* @param {PolymerElementConstructor} constructor Element class
* @return {Array} Array containing own observers for the given class
* @protected
@@ -154,7 +154,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* alter these settings. However, additional `observers` may be added
* by subclasses.
*
* The info object should may contain property metadata as follows:
* The info object should contain property metadata as follows:
*
* * `type`: {function} type to which an attribute matching the property
* is deserialized. Note the property is camel-cased from a dash-cased
@@ -166,7 +166,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* property 'foo',
*
* * `computed`: {string} creates a computed property. A computed property
* also automatically is set to `readOnly: true`. The value is calculated
* is also automatically set to `readOnly: true`. The value is calculated
* by running a method and arguments parsed from the given string. For
* example 'compute(foo)' will compute a given property when the
* 'foo' property changes by executing the 'compute' method. This method
@@ -277,6 +277,27 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
/**
* Look up template from dom-module for element
*
* @param {!string} is Element name to look up
* @return {!HTMLTemplateElement} Template found in dom module, or
* undefined if not found
* @protected
*/
function getTemplateFromDomModule(is) {
let template = null;
if (is && Polymer.DomModule) {
template = Polymer.DomModule.import(is, 'template');
// Under strictTemplatePolicy, require any element with an `is`
// specified to have a dom-module
if (Polymer.strictTemplatePolicy && !template) {
throw new Error(`strictTemplatePolicy: expecting dom-module or null template for ${is}`);
}
}
return template;
}
/**
* @polymer
* @mixinClass
* @unrestricted
@@ -374,7 +395,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* class MySubClass extends MySuperClass {
* static get template() {
* if (!memoizedTemplate) {
* memoizedTemplate = super.template.cloneNode(true);
* memoizedTemplate = MySuperClass.template.cloneNode(true);
* let subContent = document.createElement('div');
* subContent.textContent = 'This came from MySubClass';
* memoizedTemplate.content.appendChild(subContent);
@@ -386,17 +407,43 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* @return {HTMLTemplateElement|string} Template to be stamped
*/
static get template() {
// Explanation of template-related properties:
// - constructor.template (this getter): the template for the class.
// This can come from the prototype (for legacy elements), from a
// dom-module, or from the super class's template (or can be overridden
// altogether by the user)
// - constructor._template: memoized version of constructor.template
// - prototype._template: working template for the element, which will be
// parsed and modified in place. It is a cloned version of
// constructor.template, saved in _finalizeClass(). Note that before
// this getter is called, for legacy elements this could be from a
// _template field on the info object passed to Polymer(), a behavior,
// or set in registered(); once the static getter runs, a clone of it
// will overwrite it on the prototype as the working template.
if (!this.hasOwnProperty(JSCompiler_renameProperty('_template', this))) {
this._template = Polymer.DomModule && Polymer.DomModule.import(
/** @type {PolymerElementConstructor}*/ (this).is, 'template') ||
// note: implemented so a subclass can retrieve the super
// template; call the super impl this way so that `this` points
// to the superclass.
Object.getPrototypeOf(/** @type {PolymerElementConstructor}*/ (this).prototype).constructor.template;
this._template =
// If user has put template on prototype (e.g. in legacy via registered
// callback or info object), prefer that first
this.prototype.hasOwnProperty(JSCompiler_renameProperty('_template', this.prototype)) ?
this.prototype._template :
// Look in dom-module associated with this element's is
(getTemplateFromDomModule(/** @type {PolymerElementConstructor}*/ (this).is) ||
// Next look for superclass template (call the super impl this
// way so that `this` points to the superclass)
Object.getPrototypeOf(/** @type {PolymerElementConstructor}*/ (this).prototype).constructor.template);
}
return this._template;
}
/**
* Set the template.
*
* @param {!HTMLTemplateElement|string} value Template to set.
*/
static set template(value) {
this._template = value;
}
/**
* Path matching the url from which the element was imported.
*
@@ -404,7 +451,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* The `importPath` property is also set on element instances and can be
* used to create bindings relative to the import path.
*
* For elements defined in ES modules, users should implement
* For elements defined in ES modules, users should implement
* `static get importMeta() { return import.meta; }`, and the default
* implementation of `importPath` will return `import.meta.url`'s path.
* For elements defined in HTML imports, this getter will return the path
+6
View File
@@ -513,6 +513,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* @suppress {invalidCasts}
*/
templatize(template, owner, options) {
// Under strictTemplatePolicy, the templatized element must be owned
// by a (trusted) Polymer element, indicated by existence of _methodHost;
// e.g. for dom-if & dom-repeat in main document, _methodHost is null
if (Polymer.strictTemplatePolicy && !findMethodHost(template)) {
throw new Error('strictTemplatePolicy: template owner not trusted');
}
options = /** @type {!TemplatizeOptions} */(options || {});
if (template.__templatizeOwner) {
throw new Error('A <template> can only be templatized once');
+1
View File
@@ -45,6 +45,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
'unit/custom-style-async.html',
'unit/custom-style-scope-cache.html',
'unit/events.html',
'unit/strict-template-policy.html',
'unit/template-whitespace.html',
'unit/resolveurl.html',
'unit/case-map.html',
+119
View File
@@ -22,6 +22,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
</template>
</dom-module>
<dom-module id="template-from-base">
<template>
<div id="from-base">should not be used</div>
</template>
</dom-module>
<script>
HTMLImports.whenReady(function() {
window.BehaviorA = {
@@ -283,6 +289,57 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
is: 'behavior-registered'
});
window.templateBehavior1 = {
_template: Polymer.html`<div id="from-behavior1"></div>`
};
window.templateBehavior2 = {
_template: Polymer.html`<div id="from-behavior2"></div>`
};
window.templateBehaviorFromRegistered = {
registered: function() {
this._template = Polymer.html`<div id="behavior-from-registered"></div>`;
}
};
Polymer({
is: 'template-from-registered',
registered: function() {
this._template = Polymer.html`<div id="from-registered"></div>`;
}
});
Polymer({
is: 'template-from-base',
behaviors: [
window.templateBehavior1
]
});
Polymer({
is: 'template-from-behavior',
behaviors: [
window.templateBehavior1
]
});
Polymer({
is: 'template-from-behavior-overridden',
behaviors: [
window.templateBehavior1,
window.templateBehavior2
]
});
Polymer({
is: 'template-from-behavior-registered',
behaviors: [
window.templateBehaviorFromRegistered
]
});
});
</script>
@@ -309,6 +366,37 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<behavior-registered></behavior-registered>
</template>
</test-fixture>
<test-fixture id="from-registered">
<template>
<template-from-registered></template-from-registered>
</template>
</test-fixture>
<test-fixture id="from-base">
<template>
<template-from-base></template-from-base>
</template>
</test-fixture>
<test-fixture id="from-behavior">
<template>
<template-from-behavior></template-from-behavior>
</template>
</test-fixture>
<test-fixture id="from-behavior-overridden">
<template>
<template-from-behavior-overridden></template-from-behavior-overridden>
</template>
</test-fixture>
<test-fixture id="from-behavior-registered">
<template>
<template-from-behavior-registered></template-from-behavior-registered>
</template>
</test-fixture>
<script>
suite('single behavior element', function() {
@@ -505,6 +593,37 @@ suite('nested-behaviors element', function() {
});
suite('templates from behaviors', function() {
test('template from registered callback', function() {
var el = fixture('from-registered');
assert.ok(el.shadowRoot.querySelector('#from-registered'));
});
test('template from base', function() {
var el = fixture('from-base');
assert.notOk(el.shadowRoot.querySelector('#from-base'));
assert.ok(el.shadowRoot.querySelector('#from-behavior1'));
});
test('template from behavior', function() {
var el = fixture('from-behavior');
assert.ok(el.shadowRoot.querySelector('#from-behavior1'));
});
test('template from overriding behavior', function() {
var el = fixture('from-behavior-overridden');
assert.notOk(el.shadowRoot.querySelector('#from-behavior1'));
assert.ok(el.shadowRoot.querySelector('#from-behavior2'));
});
test('template from behavior registered callback', function() {
var el = fixture('from-behavior-registered');
assert.ok(el.shadowRoot.querySelector('#behavior-from-registered'));
});
});
</script>
</body>
+359
View File
@@ -0,0 +1,359 @@
<!doctype html>
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
<script>
Polymer = {
strictTemplatePolicy: true
};
// Errors thrown in custom element reactions are not thrown up
// the call stack to the dom methods that provoked them, so need
// to catch them here and prevent mocha from complaining about them
window.addEventListener('error', event => {
if (window.uncaughtErrorFilter && window.uncaughtErrorFilter(event)) {
event.preventDefault();
event.stopImmediatePropagation();
}
});
</script>
<script src="../../../web-component-tester/browser.js"></script>
<link rel="import" href="../../polymer.html">
<script>
HTMLImports.whenReady(() => {
// Errors thrown in Polymer's debouncer queue get re-thrown
// via setTimeout, making them particulary difficult to verify;
// Wrap debouncer callbacks and store on the globalError to test later
const debounce = Polymer.Debouncer.debounce;
Polymer.Debouncer.debounce = function(debouncer, asyncModule, callback) {
return debounce(debouncer, asyncModule, function() {
try {
callback();
} catch(error) {
if (!window.uncaughtErrorFilter || !window.uncaughtErrorFilter(error)) {
throw error;
}
}
});
};
});
</script>
</head>
<body>
<dom-module id="trusted-element">
<template>Trusted</template>
<script>
HTMLImports.whenReady(function() {
class TrustedElement extends Polymer.Element {
static get is() { return 'trusted-element'; }
}
customElements.define(TrustedElement.is, TrustedElement);
});
</script>
</dom-module>
<dom-module id="trusted-element-legacy">
<template>Trusted</template>
<script>
HTMLImports.whenReady(function() {
Polymer({is: 'trusted-element-legacy'});
});
</script>
</dom-module>
<dom-module id="trusted-templates">
<template>
<dom-repeat items="[0]">
<template>
<div id="dom-repeat-ok"></div>
<dom-if if>
<template><div id="nested-dom-if-ok"></div></template>
</dom-if>
</template>
</dom-repeat>
<dom-if if>
<template>
<div id="dom-if-ok"></div>
<dom-repeat items="[0]">
<template>
<div id="nested-dom-repeat-ok"></div>
</template>
</dom-repeat>
</template>
</dom-if>
</template>
<script>
HTMLImports.whenReady(function() {
class TrustedTemplates extends Polymer.Element {
static get is() { return 'trusted-templates'; }
}
customElements.define(TrustedTemplates.is, TrustedTemplates);
});
</script>
</dom-module>
<dom-module id="trusted-templates-legacy">
<template>
<template is="dom-repeat" items="[0]">
<div id="dom-repeat-ok"></div>
<template is="dom-if" if><div id="nested-dom-if-ok"></div></template>
</template>
<template is="dom-if" if>
<div id="dom-if-ok"></div>
<template is="dom-repeat" items="[0]"><div id="nested-dom-repeat-ok"></div></template>
</template>
</template>
<script>
HTMLImports.whenReady(function() {
Polymer({is: 'trusted-templates-legacy'});
});
</script>
</dom-module>
<div id="target"></div>
<script>
suite('strictTemplatePolicy', function() {
// Capture mocha's default onerror
const onerror = window.onerror;
const topOnerror = window.top.topOnerror;
function restoreOnError() {
window.onerror = onerror;
window.top.onerror = topOnerror;
window.uncaughtErrorFilter = window.top.uncaughtErrorFilter = null;
}
teardown(function() {
// Restore mocha's onerror
restoreOnError();
document.getElementById('target').textContent = '';
});
// Errors thrown in custom element reactions are not thrown up
// the call stack to the dom methods that provoked them, so this
// wraps Chai's assert.throws to re-throw uncaught errors
function assertThrows(fn, re) {
// Safari does not forward error messages
re = new RegExp('(' + re.toString().slice(1,-1) + ')|(Script error\\.)', re.flags);
// Catch uncaught errors; note when running in iframe sometimes
// Safari errors are thrown on the top window, sometimes not, so
// catch in both places
let uncaughtError = null;
window.onerror = window.top.onerror = window.uncaughtErrorFilter =
window.top.uncaughtErrorFilter = function(err) {
if (!uncaughtError) {
uncaughtError = err instanceof Error ? err : new Error(err.message || err);
}
return true;
};
assert.throws(function() {
fn();
// Re-throw any uncaughtErrors
if (uncaughtError) {
throw uncaughtError;
}
// Force polyfill reactions and/or async template stamping
Polymer.flush();
// Re-throw any uncaughtErrors
if (uncaughtError) {
throw uncaughtError;
}
}, re);
restoreOnError();
}
test('dom-bind', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-bind>' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-bind>';
}, /dom-bind not allowed/);
assert.notOk(document.getElementById('injected'));
});
test('dom-if', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-if if>' +
' <template>' +
' <div id="injected"></div>'+
' </template>' +
'</dom-if>';
}, /template owner not trusted/);
assert.notOk(document.getElementById('injected'));
});
test('dom-repeat', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-repeat items="[0]">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-repeat>';
}, /template owner not trusted/);
assert.notOk(document.getElementById('injected'));
});
test('dom-module after registration', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-module id="trusted-element">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
}, /trusted-element re-registered/);
let el;
assertThrows(function() {
el = document.createElement('trusted-element');
document.getElementById('target').appendChild(el);
}, /expecting dom-module or null template for trusted-element/);
assert.notOk(el && el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('dom-module after registration, again', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-module id="trusted-element">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
}, /trusted-element re-registered/);
const el = document.createElement('trusted-element');
document.getElementById('target').appendChild(el);
assert.notOk(el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('dom-module before registration', function() {
document.getElementById('target').innerHTML =
'<dom-module id="has-no-template">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
class HasNoTemplate extends Polymer.Element {
static get is() { return 'has-no-template'; }
static get template() { return null; }
}
customElements.define(HasNoTemplate.is, HasNoTemplate);
let el = document.createElement('has-no-template');
document.getElementById('target').appendChild(el);
assert.notOk(el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('dom-module after registration (legacy)', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-module id="trusted-element-legacy">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
}, /trusted-element-legacy re-registered/);
let el;
assertThrows(function() {
el = document.createElement('trusted-element-legacy');
document.getElementById('target').appendChild(el);
}, /expecting dom-module or null template for trusted-element-legacy/);
assert.notOk(el && el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('dom-module after registration, again (legacy)', function() {
assertThrows(function() {
document.getElementById('target').innerHTML =
'<dom-module id="trusted-element-legacy">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
}, /trusted-element-legacy re-registered/);
const el = document.createElement('trusted-element-legacy');
document.getElementById('target').appendChild(el);
assert.notOk(el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('dom-module before registration (legacy)', function() {
document.getElementById('target').innerHTML =
'<dom-module id="has-no-template-legacy">' +
' <template>' +
' <div id="injected"></div>'+
' </template>`' +
'</dom-module>';
Polymer({
is: 'has-no-template-legacy',
_template: null
});
let el = document.createElement('has-no-template-legacy');
document.getElementById('target').appendChild(el);
assert.notOk(el.shadowRoot);
assert.notOk(document.getElementById('injected'));
});
test('element without explicit template throws', function() {
assertThrows(function() {
class HasNoTemplateThrows extends Polymer.Element {
static get is() { return 'has-no-template-throws'; }
}
customElements.define(HasNoTemplateThrows.is, HasNoTemplateThrows);
var el = document.createElement('has-no-template-throws');
document.getElementById('target').appendChild(el);
}, /expecting dom-module or null template/);
});
test('element without explicit template throws (legacy)', function() {
assertThrows(function() {
Polymer({
is: 'has-no-template-throws-legacy'
});
var el = document.createElement('has-no-template-throws-legacy');
document.getElementById('target').appendChild(el);
}, /expecting dom-module or null template/);
});
test('template helpers in trusted templates work', function() {
var el = document.createElement('trusted-templates');
document.getElementById('target').appendChild(el);
Polymer.flush();
assert.ok(el.shadowRoot.querySelector('#dom-repeat-ok'));
assert.ok(el.shadowRoot.querySelector('#dom-if-ok'));
assert.ok(el.shadowRoot.querySelector('#nested-dom-repeat-ok'));
assert.ok(el.shadowRoot.querySelector('#nested-dom-if-ok'));
});
test('template helpers in trusted templates work (legacy)', function() {
var el = document.createElement('trusted-templates-legacy');
document.getElementById('target').appendChild(el);
Polymer.flush();
assert.ok(el.shadowRoot.querySelector('#dom-repeat-ok'));
assert.ok(el.shadowRoot.querySelector('#dom-if-ok'));
assert.ok(el.shadowRoot.querySelector('#nested-dom-repeat-ok'));
assert.ok(el.shadowRoot.querySelector('#nested-dom-if-ok'));
});
});
</script>
</body>
</html>