diff --git a/src/main-thread/commands/location.ts b/src/main-thread/commands/location.ts new file mode 100644 index 000000000..106acea9a --- /dev/null +++ b/src/main-thread/commands/location.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandExecutorInterface } from './interface'; +import { + TransferrableMutationType, + LocationMutationIndex +} from '../../transfer/TransferrableMutation'; +import { TransferrableKeys } from '../../transfer/TransferrableKeys'; +import { MessageType, LocationToWorker, GetOrSet } from '../../transfer/Messages'; +import { Location } from '../../worker-thread/dom/Location'; + +export const LocationProcessor: CommandExecutorInterface = (strings, nodeContext, workerContext, objectContext, config) => { + const allowedExecution = config.executorsAllowed.includes(TransferrableMutationType.LOCATION); + + const get = (): void => { + if (config.sanitizer) { + config.sanitizer.getLocation().then((value: Location) => { + const message: LocationToWorker = { + [TransferrableKeys.type]: MessageType.LOCATION, + [TransferrableKeys.location]: value, + }; + workerContext.messageToWorker(message); + }); + } else { + console.error(`LOCATION: Sanitizer not found`); + } + }; + + const set = (location: string): void => { + if (config.sanitizer) { + config.sanitizer.setLocation(location); + } else { + window.location.href = location; + } + }; + + return { + execute(mutations: Uint16Array, startPosition: number, allowedMutation: boolean): number { + if (allowedExecution && allowedMutation) { + const operation = mutations[startPosition + LocationMutationIndex.Operation]; + const locationIndex = mutations[startPosition + LocationMutationIndex.Location]; + + const location = strings.get(locationIndex); + + if (operation === GetOrSet.GET) { + get(); + } else if (operation === GetOrSet.SET) { + set(location); + } + } + + return startPosition + LocationMutationIndex.End; + }, + print(mutations: Uint16Array, startPosition: number): {} { + const operation = mutations[startPosition + LocationMutationIndex.Operation]; + const location = mutations[startPosition + LocationMutationIndex.Location]; + + return { + type: 'LOCATION', + operation, + location, + allowedExecution, + }; + }, + }; +}; diff --git a/src/main-thread/main-thread.d.ts b/src/main-thread/main-thread.d.ts index c40c84b90..10407fc8d 100644 --- a/src/main-thread/main-thread.d.ts +++ b/src/main-thread/main-thread.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Location } from '../worker-thread/dom/Location'; + /** * Allows clients to apply restrictions on DOM and storage changes. */ @@ -58,6 +60,19 @@ declare interface Sanitizer { * @return True if storage change was applied. */ setStorage(location: number, key: string | null, value: string | null): boolean; + + /** + * Retrieves the current location. + * @return current location + */ + getLocation(): Promise; + + /** + * Requests a change in location. + * @param location href + * @return True if location change was applied. + */ + setLocation(location: string): boolean; } type StorageValue = { [key: string]: string }; diff --git a/src/test/DocumentCreation.ts b/src/test/DocumentCreation.ts index b77578ad8..3268137f8 100644 --- a/src/test/DocumentCreation.ts +++ b/src/test/DocumentCreation.ts @@ -60,6 +60,7 @@ import { DOMTokenList } from '../worker-thread/dom/DOMTokenList'; import { HTMLDataListElement } from '../worker-thread/dom/HTMLDataListElement'; import { Element } from '../worker-thread/dom/Element'; import { rafPolyfill, cafPolyfill } from '../worker-thread/AnimationFrame'; +import { Location } from '../worker-thread/dom/Location'; Object.defineProperty(global, 'ServiceWorkerContainer', { configurable: true, @@ -130,6 +131,7 @@ const GlobalScope: GlobalScope = { MutationObserver, requestAnimationFrame: rafPolyfill, cancelAnimationFrame: cafPolyfill, + location: Location, }; /** diff --git a/src/transfer/Messages.ts b/src/transfer/Messages.ts index f0c757f99..d4d028eb3 100644 --- a/src/transfer/Messages.ts +++ b/src/transfer/Messages.ts @@ -21,6 +21,7 @@ import { HydrateableNode, TransferredNode } from './TransferrableNodes'; import { TransferrableBoundingClientRect } from './TransferrableBoundClientRect'; import { Phase } from './Phase'; import { StorageLocation } from './TransferrableStorage'; +import { Location } from '../worker-thread/dom/Location'; export const enum MessageType { // INIT = 0, @@ -36,6 +37,7 @@ export const enum MessageType { IMAGE_BITMAP_INSTANCE = 10, GET_STORAGE = 11, FUNCTION = 12, + LOCATION = 13, // NAVIGATION_PUSH_STATE = 8, // NAVIGATION_REPLACE_STATE = 9, // NAVIGATION_POP_STATE = 10, @@ -90,7 +92,10 @@ export interface StorageValueToWorker { [TransferrableKeys.storageLocation]: StorageLocation; [TransferrableKeys.value]: { [key: string]: string }; } - +export interface LocationToWorker { + [TransferrableKeys.type]: MessageType.LOCATION; + [TransferrableKeys.location]: Location; +} export interface FunctionCallToWorker { [TransferrableKeys.type]: MessageType.FUNCTION; [TransferrableKeys.index]: number; @@ -106,7 +111,8 @@ export type MessageToWorker = | OffscreenCanvasToWorker | ImageBitmapToWorker | StorageValueToWorker - | FunctionCallToWorker; + | FunctionCallToWorker + | LocationToWorker; /** * Can parameterize a method invocation message as a getter or setter. diff --git a/src/transfer/TransferrableKeys.ts b/src/transfer/TransferrableKeys.ts index ad8c4e914..49396043c 100644 --- a/src/transfer/TransferrableKeys.ts +++ b/src/transfer/TransferrableKeys.ts @@ -94,6 +94,7 @@ export const enum TransferrableKeys { propertyEventHandlers = 76, functionIdentifier = 77, functionArguments = 78, + location = 79, // This must always be the last numerically ordered Key, for testing purposes. - END = 78, + END = 79, } diff --git a/src/transfer/TransferrableMutation.ts b/src/transfer/TransferrableMutation.ts index 9833dbed8..3090dea2c 100644 --- a/src/transfer/TransferrableMutation.ts +++ b/src/transfer/TransferrableMutation.ts @@ -30,6 +30,7 @@ export const enum TransferrableMutationType { STORAGE = 12, FUNCTION_CALL = 13, SCROLL_INTO_VIEW = 14, + LOCATION = 15, } /** @@ -67,6 +68,7 @@ export const DefaultAllowedMutations = [ TransferrableMutationType.STORAGE, TransferrableMutationType.FUNCTION_CALL, TransferrableMutationType.SCROLL_INTO_VIEW, + TransferrableMutationType.LOCATION, ]; export const ReadableMutationType: { [key: number]: string } = { @@ -85,6 +87,7 @@ export const ReadableMutationType: { [key: number]: string } = { 12: 'STORAGE', 13: 'FUNCTION_INVOCATION', 14: 'SCROLL_INTO_VIEW', + 15: 'LOCATION', }; /** @@ -286,3 +289,17 @@ export const enum ScrollIntoViewMutationIndex { Target = 1, End = 2, } + +/** + * Location Mutation + * [ + * TransferrableMutationType.LOCATION, + * GetOrSet, + * Location + * ] + */ +export const enum LocationMutationIndex { + Operation = 1, + Location = 2, + End = 3, +} diff --git a/src/worker-thread/LocationPropagation.ts b/src/worker-thread/LocationPropagation.ts new file mode 100644 index 000000000..74506c63c --- /dev/null +++ b/src/worker-thread/LocationPropagation.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageToWorker, MessageType, LocationToWorker } from '../transfer/Messages'; +import { TransferrableKeys } from '../transfer/TransferrableKeys'; +import { WorkerDOMGlobalScope } from './WorkerDOMGlobalScope'; + +export function propagate(global: WorkerDOMGlobalScope): void { + const document = global.document; + if (!document.addGlobalEventListener) { + return; + } + document.addGlobalEventListener('message', ({ data }: { data: MessageToWorker }) => { + if (data[TransferrableKeys.type] !== MessageType.LOCATION) { + return; + } + const location = (data as LocationToWorker)[TransferrableKeys.location]; + if (location) { + document.location = location; + } + }); +} diff --git a/src/worker-thread/WorkerDOMGlobalScope.ts b/src/worker-thread/WorkerDOMGlobalScope.ts index 430efcfa1..4f4bdf486 100644 --- a/src/worker-thread/WorkerDOMGlobalScope.ts +++ b/src/worker-thread/WorkerDOMGlobalScope.ts @@ -57,6 +57,7 @@ import { DocumentFragment } from './dom/DocumentFragment'; import { DOMTokenList } from './dom/DOMTokenList'; import { Element } from './dom/Element'; import { DocumentStub } from './dom/DocumentLite'; +import { Location } from './dom/Location'; /** * Should only contain properties that exist on Window. @@ -114,6 +115,7 @@ export interface GlobalScope { ImageBitmap?: typeof ImageBitmap; requestAnimationFrame: typeof requestAnimationFrame; cancelAnimationFrame: typeof cancelAnimationFrame; + location: typeof Location; } export interface WorkerDOMGlobalScope extends GlobalScope { diff --git a/src/worker-thread/dom/Document.ts b/src/worker-thread/dom/Document.ts index b647af889..55579a0aa 100644 --- a/src/worker-thread/dom/Document.ts +++ b/src/worker-thread/dom/Document.ts @@ -52,6 +52,7 @@ import { Text } from './Text'; import { Comment } from './Comment'; import { toLower } from '../../utils'; import { DocumentFragment } from './DocumentFragment'; +import { Location } from "./Location"; import { PostMessage } from '../worker-thread'; import { NodeType, HTML_NAMESPACE, HydrateableNode } from '../../transfer/TransferrableNodes'; import { Phase } from '../../transfer/Phase'; @@ -68,6 +69,7 @@ export class Document extends Element { public defaultView: WorkerDOMGlobalScope; public documentElement: Document; public body: Element; + public location: Location; // Internal variables. public postMessage: PostMessage; @@ -80,11 +82,11 @@ export class Document extends Element { // Element uppercases its nodeName, but Document doesn't. this.nodeName = DOCUMENT_NAME; this.documentElement = this; // TODO(choumx): Should be the element. - this.defaultView = Object.assign(global, { document: this, addEventListener: this.addEventListener.bind(this), removeEventListener: this.removeEventListener.bind(this), + location: this.location, }); } diff --git a/src/worker-thread/dom/Location.ts b/src/worker-thread/dom/Location.ts new file mode 100644 index 000000000..4f29bfda5 --- /dev/null +++ b/src/worker-thread/dom/Location.ts @@ -0,0 +1,242 @@ +/** + * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document} from './Document'; +import { GetOrSet } from '../../transfer/Messages'; +import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { store } from '../strings'; +import { transfer } from '../MutationTransfer'; + +// @see https://developer.mozilla.org/en-US/docs/Web/API/Location +export class Location { + private _href: string; + private _origin: string; + private _protocol: string; + private _host: string; + private _hostname: string; + private _port: string; + private _pathname: string; + private _search: string; + private _hash: string; + private _document: Document; + + /** + * Getter for href + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/href + * @return string href + */ + get href(): string { + return this._href; + } + + /** + * Setter for href + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/href + * @param href + */ + set href(href: string) { + this._href = href; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for origin + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/origin + * @return string origin + */ + get origin(): string { + return this._origin; + } + + /** + * Setter for origin + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/origin + * @param origin + */ + set origin(origin: string) { + this._origin = origin; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for protocol + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol + * @return string protocol + */ + get protocol(): string { + return this._protocol; + } + + /** + * Setter for protocol + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol + * @param protocol + */ + set protocol(protocol: string) { + this._protocol = protocol; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for host + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/host + * @return string host + */ + get host(): string { + return this._host; + } + + /** + * Setter for host + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/host + * @param host + */ + set host(host: string) { + this._host = host; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for hostname + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname + * @return string hostname + */ + get hostname(): string { + return this._hostname; + } + + /** + * Setter for hostname + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname + * @param hostname + */ + set hostname(hostname: string) { + this._hostname = hostname; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for port + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/port + * @return string port + */ + get port(): string { + return this._port; + } + + /** + * Setter for port + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/port + * @param port + */ + set port(port: string) { + this._port = port; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for pathname + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname + * @return string pathname + */ + get pathname(): string { + return this._pathname; + } + + /** + * Setter for pathname + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname + * @param pathname + */ + set pathname(pathname: string) { + this._pathname = pathname; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for search + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/search + * @return string search + */ + get search(): string { + return this._search; + } + + /** + * Setter for search + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/search + * @param search + */ + set search(search: string) { + this._search = search; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Getter for hash + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/hash + * @return string hash + */ + get hash(): string { + return this._hash; + } + + /** + * Setter for hash + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/hash + * @param hash + */ + set hash(hash: string) { + this._hash = hash; + transfer(this._document, [TransferrableMutationType.LOCATION, GetOrSet.SET, store(this.toString())]); + } + + /** + * Loads the resource at the URL provided in parameter. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/assign + * @param url URL + */ + public assign(url: string): void { + + } + + + /** + * Redirects to the provided URL. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/replace + * @param url URL + */ + public replace(url: string): void { + + } + + /** + * Reloads the current URL, like the Refresh button. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/reload + */ + public reload(): void { + + } + + /** + * Returns a string containing the whole URL. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Location/toString + * @return string URL + */ + public toString(): string { + return this.href; + } +} diff --git a/src/worker-thread/index.amp.ts b/src/worker-thread/index.amp.ts index e7eab7257..4f0e08798 100644 --- a/src/worker-thread/index.amp.ts +++ b/src/worker-thread/index.amp.ts @@ -63,6 +63,7 @@ import { SVGElement } from './dom/SVGElement'; import { Text } from './dom/Text'; import { wrap as longTaskWrap } from './long-task'; import { HydrateFunction } from './hydrate'; +import { Location } from './dom/Location'; declare const WORKER_DOM_DEBUG: boolean; @@ -112,6 +113,7 @@ const globalScope: GlobalScope = { MutationObserver, requestAnimationFrame: self.requestAnimationFrame || rafPolyfill, cancelAnimationFrame: self.cancelAnimationFrame || cafPolyfill, + location: Location, }; const noop = () => void 0; diff --git a/src/worker-thread/index.ts b/src/worker-thread/index.ts index 9a653bcad..def5e3d33 100644 --- a/src/worker-thread/index.ts +++ b/src/worker-thread/index.ts @@ -59,6 +59,7 @@ import { DocumentFragment } from './dom/DocumentFragment'; import { Element } from './dom/Element'; import { rafPolyfill, cafPolyfill } from './AnimationFrame'; import { HydrateFunction } from './hydrate'; +import { Location } from './dom/Location'; const globalScope: GlobalScope = { innerWidth: 0, @@ -106,6 +107,7 @@ const globalScope: GlobalScope = { MutationObserver, requestAnimationFrame: self.requestAnimationFrame || rafPolyfill, cancelAnimationFrame: self.cancelAnimationFrame || cafPolyfill, + location: Location }; const noop = () => void 0; diff --git a/web_compat_table.md b/web_compat_table.md index aed29248c..36d705c70 100644 --- a/web_compat_table.md +++ b/web_compat_table.md @@ -95,7 +95,7 @@ This section highlights the DOM APIs that are implemented in WorkerDOM currently | Document.lastModified | ✖️ | | | Document.lastStyleSheetSet | ✖️ | | | Document.links | ✖️ | | -| Document.location | ✖️ | | +| Document.location | ✔️ | | | Document.mozSetImageElement() | ✖️ | | | Document.mozSyntheticDocument | ✖️ | | | Document.normalizeDocument() | ✖️ | | @@ -900,6 +900,7 @@ This section highlights the DOM APIs that are implemented in WorkerDOM currently | Window.isSecureContext | ✖️ | | | Window.length | ✖️ | | | Window.localStorage | ✔️ | | +| Window.location | ✔️ | | | Window.locationbar | ✖️ | | | Window.matchMedia() | ✖️ | | | Window.maximize() | ✖️ | |