Skip to content

Commit

Permalink
Disallow non-templates as interpolations in Polymer.html (#5023)
Browse files Browse the repository at this point in the history
* Disallow non-templates as interpolations in Polymer.html

* Adds `Polymer.htmlLiteral`

Allows composition of strings into html templates constructed with `Polymer.html`. This is useful for composing values into styles or other places where an html template would not compose.

```
const css = Polymer.htmlLiteral`background: red;`
Polymer.html`<style>:host { ${css} }</style>`
```

* lint fix and update types

* Review updates.

* update types.

* [ci-skip] Add comment about accepting htmlLiteral values.

* Update types.

* Add a `toString` for output from `Polymer.htmlLiteral`

Simpler for debugging
  • Loading branch information
justinfagnani authored and dfreedm committed Jan 31, 2018
1 parent 6fc285c commit eeb7160
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 22 deletions.
84 changes: 76 additions & 8 deletions lib/utils/html-tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,68 @@
(function() {
'use strict';

/**
* Class representing a static string value which can be used to filter
* strings by asseting that they have been created via this class. The
* `value` property returns the string passed to the constructor.
*/
class LiteralString {
constructor(string) {
/** @type {string} */
this.value = string.toString();
}
/**
* @return {string} LiteralString string value
*/
toString() {
return this.value;
}
}

/**
* @param {*} value Object to stringify into HTML
* @return {string} HTML stringified form of `obj`
*/
function literalValue(value) {
if (value instanceof LiteralString) {
return /** @type {!LiteralString} */(value).value;
} else {
throw new Error(`non-literal value passed to Polymer.htmlLiteral: ${value}`);
}
}

/**
* @param {*} value Object to stringify into HTML
* @return {string} HTML stringified form of `obj`
*/
function htmlValue(value) {
if (value instanceof HTMLTemplateElement) {
return /** @type {!HTMLTemplateElement} */(value).innerHTML;
return /** @type {!HTMLTemplateElement } */(value).innerHTML;
} else if (value instanceof LiteralString) {
return literalValue(value);
} else {
return String(value);
throw new Error(`non-template value passed to Polymer.html: ${value}`);
}
}

/**
* A template literal tag that creates an HTML <template> element from the contents of the string.
* A template literal tag that creates an HTML <template> element from the
* contents of the string.
*
* This allows you to write a Polymer Template in JavaScript.
*
* Interpolated values are converted to strings when the template is created,
* they are not intended as a replacement for Polymer data-binding.
* Templates can be composed by interpolating `HTMLTemplateElement`s in
* expressions in the JavaScript template literal. The nested template's
* `innerHTML` is included in the containing template. The only other
* values allowed in expressions are those returned from `Polymer.htmlLiteral`
* which ensures only literal values from JS source ever reach the HTML, to
* guard against XSS risks.
*
* There is special support for HTMLTemplateElement values,
* allowing for easy composition of superclass or partial templates.
* All other values are disallowed in expressions to help prevent XSS
* attacks; however, `Polymer.htmlLiteral` can be used to compose static
* string values into templates. This is useful to compose strings into
* places that do not accept html, like the css text of a `style`
* element.
*
* Example:
*
Expand All @@ -54,8 +94,36 @@
Polymer.html = function html(strings, ...values) {
const template = /** @type {!HTMLTemplateElement} */(document.createElement('template'));
template.innerHTML = values.reduce((acc, v, idx) =>
acc + htmlValue(v) + strings[idx + 1], strings[0]);
acc + htmlValue(v) + strings[idx + 1], strings[0]);
return template;
};

/**
* An html literal tag that can be used with `Polymer.html` to compose.
* a literal string.
*
* Example:
*
* static get template() {
* return Polymer.html`
* <style>
* :host { display: block; }
* ${styleTemplate}
* </style>
* <div class="shadowed">${staticValue}</div>
* ${super.template}
* `;
* }
* static get styleTemplate() { return Polymer.htmlLiteral`.shadowed { background: gray; }`; }
*
* @memberof Polymer
* @param {!ITemplateArray} strings Constant parts of tagged template literal
* @param {...*} values Variable parts of tagged template literal
* @return {!LiteralString} Constructed literal string
*/
Polymer.htmlLiteral = function(strings, ...values) {
return new LiteralString(values.reduce((acc, v, idx) =>
acc + literalValue(v) + strings[idx + 1], strings[0]));
};
})();
</script>
25 changes: 16 additions & 9 deletions test/unit/html-tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,26 @@
let t = Polymer.html`<span>the \`price\` is \${{foo}}\n</span>`;
assert.equal(t.innerHTML, '<span>the `price` is ${{foo}}\n</span>');
});
test('variables work', function() {
let a = 3;
let b = 'foo';
let t = Polymer.html`<div>${a} ${b}</div>`;
let inst = document.createElement('div');
inst.appendChild(t.content.cloneNode(true));
assert.equal(inst.textContent, `${a} ${b}`);
test('interpolation of non-templates throw', function() {
assert.throws(() => Polymer.html`<div>${'a'}</div>`);
});
test('template variables only include the template contents', function() {
test('interpolation of templates include the template contents', function() {
let t1 = Polymer.html`<div></div>`;
let t2 = Polymer.html`<span>${t1}</span>`;
assert.equal(t2.innerHTML, '<span><div></div></span>');
});
test('interpolation of literal values', function() {
let s1 = Polymer.htmlLiteral`hello`;
assert.equal(s1.value, `hello`);
let s2 = Polymer.htmlLiteral`hello${Polymer.htmlLiteral` ${Polymer.htmlLiteral`world`}!`}!`;
assert.equal(s2.value, 'hello world!!');
});
test('interpolation of literal values within html values', function() {
let s1 = Polymer.htmlLiteral`foo`;
let s2 = Polymer.htmlLiteral`display: block;`;
let t = Polymer.html`<style>:host {${s2}}</style><div ${s1}></div>`;
assert.equal(t.innerHTML, `<style>:host {display: block;}</style><div foo=""></div>`);
});
});
suite('Polymer + html fn', function() {
suiteSetup(function() {
Expand Down Expand Up @@ -131,4 +138,4 @@
});
</script>
</body>
</html>
</html>
55 changes: 50 additions & 5 deletions types/lib/utils/html-tag.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,41 @@

/// <reference path="boot.d.ts" />

/**
* Class representing a static string value which can be used to filter
* strings by asseting that they have been created via this class. The
* `value` property returns the string passed to the constructor.
*/
declare class LiteralString {
value: string;

/**
* @returns LiteralString string value
*/
toString(): string;
}

declare namespace Polymer {


/**
* A template literal tag that creates an HTML <template> element from the contents of the string.
* A template literal tag that creates an HTML <template> element from the
* contents of the string.
*
* This allows you to write a Polymer Template in JavaScript.
*
* Interpolated values are converted to strings when the template is created,
* they are not intended as a replacement for Polymer data-binding.
* Templates can be composed by interpolating `HTMLTemplateElement`s in
* expressions in the JavaScript template literal. The nested template's
* `innerHTML` is included in the containing template. The only other
* values allowed in expressions are those returned from `Polymer.htmlLiteral`
* which ensures only literal values from JS source ever reach the HTML, to
* guard against XSS risks.
*
* There is special support for HTMLTemplateElement values,
* allowing for easy composition of superclass or partial templates.
* All other values are disallowed in expressions to help prevent XSS
* attacks; however, `Polymer.htmlLiteral` can be used to compose static
* string values into templates. This is useful to compose strings into
* places that do not accept html, like the css text of a `style`
* element.
*
* Example:
*
Expand All @@ -38,4 +60,27 @@ declare namespace Polymer {
* @returns Constructed HTMLTemplateElement
*/
function html(strings: TemplateStringsArray, ...values: any[]): HTMLTemplateElement;


/**
* An html literal tag that can be used with `Polymer.html` to compose.
* a literal string.
*
* Example:
*
* static get template() {
* return Polymer.html`
* <style>
* :host { display: block; }
* ${styleTemplate}
* </style>
* <div class="shadowed">${staticValue}</div>
* ${super.template}
* `;
* }
* static get styleTemplate() { return Polymer.htmlLiteral`.shadowed { background: gray; }`; }
*
* @returns Constructed literal string
*/
function htmlLiteral(strings: TemplateStringsArray, ...values: any[]): LiteralString;
}

0 comments on commit eeb7160

Please sign in to comment.