Skip to content

Commit

Permalink
fix(engine): issue 990 textContent and innerHTML restrictions (#1001)
Browse files Browse the repository at this point in the history
* fix(engine): issue 990 textContent and innerHTML restrictions

* fix(engine): PR #1001 feedback from @pm
  • Loading branch information
caridy authored Jan 25, 2019
1 parent f8423e8 commit 514c1f5
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 24 deletions.
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);
},
},
};

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

0 comments on commit 514c1f5

Please sign in to comment.