Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(engine): issue 990 textContent and innerHTML restrictions #1001

Merged
merged 2 commits into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/@lwc/engine/src/env/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ const nodeValueSetter: (this: Node, value: string) => void = nodeValueDescriptor

const nodeValueGetter: (this: Node) => string = nodeValueDescriptor.get!;

const isConnected = hasOwnProperty.call(Node.prototype, 'isConnected') ?
getOwnPropertyDescriptor(Node.prototype, 'isConnected')!.get! :
function(this: Node): boolean { // IE11
return (
(compareDocumentPosition.call(document, this) &
DOCUMENT_POSITION_CONTAINED_BY) !==
0
);
};

export {
// Node.prototype
compareDocumentPosition,
Expand All @@ -58,6 +68,7 @@ export {
nodeValueGetter,
nodeValueSetter,
cloneNode,
isConnected,

// Node
DOCUMENT_POSITION_CONTAINS,
Expand Down
17 changes: 11 additions & 6 deletions packages/@lwc/engine/src/faux-shadow/shadow-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { getInternalField, setInternalField, createFieldName } from "../shared/f
import { getTextContent } from "../3rdparty/polymer/text-content";
import { createStaticNodeList } from "../shared/static-node-list";
import { DocumentPrototypeActiveElement, elementFromPoint, createComment } from "../env/document";
import { compareDocumentPosition, DOCUMENT_POSITION_CONTAINED_BY, parentElementGetter } from "../env/node";
import { compareDocumentPosition, DOCUMENT_POSITION_CONTAINED_BY, parentElementGetter, textContextSetter, isConnected } from "../env/node";
import { isNativeShadowRootAvailable } from "../env/dom";
import { createStaticHTMLCollection } from "../shared/static-html-collection";
import { getOuterHTML } from "../3rdparty/polymer/outer-html";
import { retarget } from "../3rdparty/polymer/retarget";
import { pathComposer } from "../3rdparty/polymer/path-composer";
import { getInternalChildNodes } from "./node";
import { innerHTMLSetter } from "../env/element";

const HostKey = createFieldName('host');
const ShadowRootKey = createFieldName('shadowRoot');
Expand Down Expand Up @@ -295,11 +296,7 @@ const NodePatchDescriptors = {
enumerable: true,
configurable: true,
get(this: SyntheticShadowRootInterface) {
return (
(compareDocumentPosition.call(document, getHost(this)) &
DOCUMENT_POSITION_CONTAINED_BY) !==
0
);
return isConnected.call(getHost(this));
},
},
nextSibling: {
Expand Down Expand Up @@ -369,6 +366,10 @@ const NodePatchDescriptors = {
}
return textContent;
},
set(this: SyntheticShadowRootInterface, v: string) {
const host = getHost(this);
textContextSetter.call(host, v);
}
},
getRootNode: {
writable: true,
Expand All @@ -392,6 +393,10 @@ const ElementPatchDescriptors = {
}
return innerHTML;
},
set(this: SyntheticShadowRootInterface, v: string) {
const host = getHost(this);
innerHTMLSetter.call(host, v);
},
},
localName: {
enumerable: true,
Expand Down
96 changes: 78 additions & 18 deletions packages/@lwc/engine/src/framework/restrictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
appendChild: {
value(this: Node, aChild: Node) {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`appendChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
assert.logError(`appendChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
return appendChild.call(this, aChild);
},
Expand All @@ -57,7 +57,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
insertBefore: {
value(this: Node, newNode: Node, referenceNode: Node) {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`insertBefore is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
assert.logError(`insertBefore is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
return insertBefore.call(this, newNode, referenceNode);
},
Expand All @@ -68,7 +68,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
removeChild: {
value(this: Node, aChild: Node) {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`removeChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
assert.logError(`removeChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
return removeChild.call(this, aChild);
},
Expand All @@ -79,7 +79,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
replaceChild: {
value(this: Node, newChild: Node, oldChild: Node) {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`replaceChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
assert.logError(`replaceChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
return replaceChild.call(this, newChild, oldChild);
},
Expand All @@ -92,10 +92,8 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
return originalNodeValueDescriptor.get!.call(this);
},
set(this: Node, value: string) {
if (process.env.NODE_ENV !== 'production') {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`nodeValue is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
}
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`nodeValue is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
originalNodeValueDescriptor.set!.call(this, value);
}
Expand All @@ -105,10 +103,8 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions
return originalTextContentDescriptor.get!.call(this);
},
set(this: Node, value: string) {
if (process.env.NODE_ENV !== 'production') {
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`textContent is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element);
}
if (this instanceof Element && options.isPortal !== true) {
assert.logError(`textContent is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
originalTextContentDescriptor.set!.call(this, value);
}
Expand All @@ -124,22 +120,31 @@ function getElementRestrictionsDescriptors(elm: HTMLElement, options: Restrictio
}
const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(elm, options);
const originalInnerHTMLDescriptor = getPropertyDescriptor(elm, 'innerHTML')!;
const originalOuterHTMLDescriptor = getPropertyDescriptor(elm, 'outerHTML')!;
assign(descriptors, {
innerHTML: {
get(): string {
return originalInnerHTMLDescriptor.get!.call(this);
},
set(this: HTMLElement, value: string) {
if (process.env.NODE_ENV !== 'production') {
if (options.isPortal !== true) {
assert.logError(`innerHTML is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
if (options.isPortal !== true) {
assert.logError(`innerHTML is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this);
}
return originalInnerHTMLDescriptor.set!.call(this, value);
},
enumerable: true,
configurable: true,
}
},
outerHTML: {
get(this: HTMLElement): string {
return originalOuterHTMLDescriptor.get!.call(this);
},
set(this: HTMLElement, value: string) {
throw new TypeError(`Invalid attempt to set outerHTML on Element.`);
},
enumerable: true,
configurable: true,
},
});
return descriptors;
}
Expand All @@ -156,7 +161,29 @@ function getShadowRootRestrictionsDescriptors(sr: ShadowRoot, options: Restricti
const originalQuerySelectorAll = sr.querySelectorAll;
const originalAddEventListener = sr.addEventListener;
const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(sr, options);
const originalInnerHTMLDescriptor = getPropertyDescriptor(sr, 'innerHTML')!;
const originalTextContentDescriptor = getPropertyDescriptor(sr, 'textContent')!;
assign(descriptors, {
innerHTML: {
get(this: ShadowRoot): string {
return originalInnerHTMLDescriptor.get!.call(this);
},
set(this: ShadowRoot, value: string) {
throw new TypeError(`Invalid attempt to set innerHTML on ShadowRoot.`);
},
enumerable: true,
configurable: true,
},
textContent: {
get(this: ShadowRoot): string {
return originalTextContentDescriptor.get!.call(this);
},
set(this: ShadowRoot, value: string) {
throw new TypeError(`Invalid attempt to set textContent on ShadowRoot.`);
},
enumerable: true,
configurable: true,
},
addEventListener: {
value(this: ShadowRoot, type: string) {
assert.invariant(!isRendering, `${vmBeingRendered}.render() method has side effects on the state of ${toString(sr)} by adding an event listener for "${type}".`);
Expand Down Expand Up @@ -281,9 +308,42 @@ function getCustomElementRestrictionsDescriptors(elm: HTMLElement, options: Rest
}
const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(elm, options);
const originalAddEventListener = elm.addEventListener;
const originalInnerHTMLDescriptor = getPropertyDescriptor(elm, 'innerHTML')!;
const originalOuterHTMLDescriptor = getPropertyDescriptor(elm, 'outerHTML')!;
const originalTextContentDescriptor = getPropertyDescriptor(elm, 'textContent')!;
return assign(descriptors, {
innerHTML: {
get(this: HTMLElement): string {
return originalInnerHTMLDescriptor.get!.call(this);
},
set(this: HTMLElement, value: string) {
throw new TypeError(`Invalid attempt to set innerHTML on HTMLElement.`);
},
enumerable: true,
configurable: true,
},
outerHTML: {
get(this: HTMLElement): string {
return originalOuterHTMLDescriptor.get!.call(this);
},
set(this: HTMLElement, value: string) {
throw new TypeError(`Invalid attempt to set outerHTML on HTMLElement.`);
},
enumerable: true,
configurable: true,
},
textContent: {
get(this: HTMLElement): string {
return originalTextContentDescriptor.get!.call(this);
},
set(this: HTMLElement, value: string) {
throw new TypeError(`Invalid attempt to set textContent on HTMLElement.`);
},
enumerable: true,
configurable: true,
},
addEventListener: {
value(this: ShadowRoot, type: string) {
value(this: HTMLElement, type: string) {
assert.invariant(!isRendering, `${vmBeingRendered}.render() method has side effects on the state of ${toString(elm)} by adding an event listener for "${type}".`);
return originalAddEventListener.apply(this, arguments);
}
Expand Down