Skip to content

Commit

Permalink
Support external 3DOM scripts (google#1049)
Browse files Browse the repository at this point in the history
* Support importing external scripts

* Support external 3DOM scripts

* Add marker to delimit unsafe eval
  • Loading branch information
Christopher Joel authored Feb 25, 2020
1 parent a44dfd5 commit 31b5153
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 14 deletions.
28 changes: 28 additions & 0 deletions packages/3dom/src/context-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ suite('context', () => {
}
});

test('can import external script', async () => {
const context = new ThreeDOMExecutionContext(['messaging']);
const scriptText = 'self.postMessage("hello")';
const blob = new Blob([scriptText], {type: 'text/javascript'});
const url = URL.createObjectURL(blob);

try {
const scriptEvaluates = new Promise((resolve) => {
context.worker.addEventListener('message', (event) => {
expect(event.data).to.be.equal('hello');
resolve();
}, {once: true});
});

context.import(url);

await scriptEvaluates;
} finally {
if (context != null) {
context.terminate();
}

if (url != null) {
URL.revokeObjectURL(url);
}
}
});

suite('when the model changes', () => {
test('dispatches an event in the worker', async () => {
const modelGraft = new ModelGraft('', createFakeGLTF());
Expand Down
12 changes: 10 additions & 2 deletions packages/3dom/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,17 @@ export class ThreeDOMExecutionContext extends EventTarget {
* Workers, so for now all scripts must be valid non-module scripts.
*/
async eval(scriptSource: string): Promise<void> {
await this.import(URL.createObjectURL(
new Blob([scriptSource], {type: 'text/javascript'})));
} /* end eval marker (do not remove) */

/**
* Load a script by URL in the scene graph execution context. Generally works
* the same as eval, but is generally safer because it allows you full control
* of the script text. Like eval, does not support module scripts.
*/
async import(url: string): Promise<void> {
const port = await this[$workerInitializes];
const url = URL.createObjectURL(
new Blob([scriptSource], {type: 'text/javascript'}));
port.postMessage({type: ThreeDOMMessageType.IMPORT_SCRIPT, url});
}

Expand Down
41 changes: 29 additions & 12 deletions packages/model-viewer/src/features/scene-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const $updateExecutionContextModel = Symbol('updateExecutionContextModel');
const $modelGraft = Symbol('modelGraft');
const $onModelGraftMutation = Symbol('onModelGraftMutation');
const $modelGraftMutationHandler = Symbol('modelGraftMutationHandler');
const $isValid3DOMScript = Symbol('isValid3DOMScript');

export interface SceneGraphInterface {
worklet: Worker|null;
Expand Down Expand Up @@ -128,7 +129,7 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
const script = this.querySelector<HTMLScriptElement>(
`script[type="${SCENE_GRAPH_SCRIPT_TYPE}"]:last-of-type`);

if (script != null && script.textContent) {
if (script != null && this[$isValid3DOMScript](script)) {
this[$onScriptElementAdded](script);
}
}
Expand Down Expand Up @@ -169,6 +170,12 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
this[$updateExecutionContextModel]();
}

[$isValid3DOMScript](node: Node) {
return node instanceof HTMLScriptElement &&
(node.textContent || node.src) &&
node.getAttribute('type') === SCENE_GRAPH_SCRIPT_TYPE
}

[$onChildListMutation](records: Array<MutationRecord>) {
if (this.parentNode == null) {
// Ignore a lazily reported list of mutations if we are detached from
Expand All @@ -180,9 +187,8 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(

for (const record of records) {
for (const node of Array.from(record.addedNodes)) {
if (node instanceof HTMLScriptElement && node.textContent &&
node.getAttribute('type') === SCENE_GRAPH_SCRIPT_TYPE) {
lastScriptElement = node;
if (this[$isValid3DOMScript](node)) {
lastScriptElement = node as HTMLScriptElement;
}
}
}
Expand All @@ -193,8 +199,7 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
}

[$onScriptElementAdded](script: HTMLScriptElement) {
if (!script.textContent ||
script.getAttribute('type') !== SCENE_GRAPH_SCRIPT_TYPE) {
if (!this[$isValid3DOMScript](script)) {
return;
}

Expand All @@ -206,23 +211,35 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
(capability): capability is ThreeDOMCapability =>
VALID_CAPABILITIES.has(capability as ThreeDOMCapability));

this[$createExecutionContext](script.textContent, allowList);
if (script.src) {
this[$createExecutionContext](script.src, allowList);
} else {
this[$createExecutionContext](
script.textContent!, allowList, {eval: true});
}
}

[$createExecutionContext](
scriptSource: string, capabilities: Array<ThreeDOMCapability>) {
const executionContext = this[$executionContext];
async[$createExecutionContext](
scriptSource: string, capabilities: Array<ThreeDOMCapability>,
options = {eval: false}) {
let executionContext = this[$executionContext];

if (executionContext != null) {
executionContext.terminate();
}

this[$executionContext] = new ThreeDOMExecutionContext(capabilities);
this[$executionContext]!.eval(scriptSource);
this[$executionContext] = executionContext =
new ThreeDOMExecutionContext(capabilities);

this.dispatchEvent(new CustomEvent(
'worklet-created', {detail: {worklet: this.worklet}}));

if (options.eval) {
await executionContext.eval(scriptSource);
} else {
await executionContext.import(scriptSource);
}

this[$updateExecutionContextModel]();
}

Expand Down
24 changes: 24 additions & 0 deletions packages/model-viewer/src/test/features/scene-graph-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ suite('ModelViewerElementBase with SceneGraphMixin', () => {
expect(element.worklet).to.be.ok;
});

suite('in an external script file', () => {
test('eventually creates a new worklet', async () => {
const scriptText = 'console.log("Hello, worklet!");';
const url = URL.createObjectURL(
new Blob([scriptText], {type: 'text/javascript'}));

const script = document.createElement('script');
script.type = 'experimental-scene-graph-worklet';
script.src = url;

try {
element.appendChild(script);

await waitForEvent(element, 'worklet-created');

expect(element.worklet).to.be.ok;
} finally {
if (url != null) {
URL.revokeObjectURL(url);
}
}
});
});

suite('with a loaded model', () => {
setup(async () => {
element.src = ASTRONAUT_GLB_PATH;
Expand Down

0 comments on commit 31b5153

Please sign in to comment.