| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Navigation related APIs. |
| */ |
| |
| import {gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js'; |
| import {sendWebKitMessage} from '//ios/web/public/js_messaging/resources/utils.js'; |
| |
| declare interface Message { |
| command: 'willChangeState'|'didPushState'|'didReplaceState'; |
| frame_id: string; |
| stateObject?: string; |
| baseUrl?: string; |
| pageUrl?: string; |
| } |
| |
| class DataCloneError { |
| // The name and code for this error are defined by the WebIDL spec. See |
| // https://heycam.github.io/webidl/#datacloneerror |
| name = 'DataCloneError'; |
| code = 25; |
| message = 'Cyclic structures are not supported.'; |
| } |
| |
| // Stores queued messages until they can be sent to the "NavigationEventMessage" |
| // handler. |
| class MessageQueue { |
| queuedMessages: Message[] = []; |
| |
| // Attempts to send any queued messages. Messages |
| // will be only be removed once they have been sent. |
| sendQueuedMessages(): void { |
| while (this.queuedMessages.length > 0) { |
| try { |
| sendWebKitMessage( |
| 'NavigationEventMessage', this.queuedMessages[0] as Message); |
| this.queuedMessages.shift(); |
| } catch (e) { |
| // 'NavigationEventMessage' message handler |
| // is not currently registered. Send the |
| // message later when possible. |
| break; |
| } |
| } |
| } |
| |
| // Queues the `message` and triggers the queue to be sent. |
| queueNavigationEventMessage(message: Message): void { |
| this.queuedMessages.push(message); |
| this.sendQueuedMessages(); |
| } |
| } |
| |
| const messageQueue = new MessageQueue(); |
| |
| /** |
| * Retain the original JSON.stringify method where possible to reduce the |
| * impact of sites overriding it |
| */ |
| const JSONStringify: Function = JSON.stringify; |
| |
| /** |
| * Keep the original pushState() and replaceState() methods. It's needed to |
| * update the web view's URL and window.history.state property during history |
| * navigations that don't cause a page load. |
| */ |
| const originalWindowHistoryPushState = window.history.pushState; |
| const originalWindowHistoryReplaceState = window.history.replaceState; |
| |
| /** |
| * Intercepts window.history methods so native code can differentiate between |
| * same-document navigation that are state navigations vs. hash navigations. |
| * This is needed for backward compatibility of DidStartLoading, which is |
| * triggered for fragment navigation but not state navigation. |
| * TODO(crbug.com/41354482): Remove this once DidStartLoading is no longer |
| * called for same-document navigation. |
| */ |
| History.prototype.pushState = function( |
| stateObject: object, pageTitle: string, pageUrl: string|URL): void { |
| messageQueue.queueNavigationEventMessage({ |
| 'command': 'willChangeState', |
| 'frame_id': gCrWeb.getFrameId(), |
| }); |
| |
| // JSONStringify throws an exception when given a cyclical object. This |
| // internal implementation detail should not be exposed to callers of |
| // pushState. Instead, throw a standard exception when stringification fails. |
| let serializedState = ''; |
| try { |
| // Calling stringify() on undefined causes a JSON parse error. |
| if (typeof (stateObject) !== 'undefined') { |
| serializedState = JSONStringify(stateObject); |
| } |
| } catch (e) { |
| throw new DataCloneError(); |
| } |
| pageUrl = pageUrl || window.location.href; |
| originalWindowHistoryPushState.call(history, stateObject, pageTitle, pageUrl); |
| messageQueue.queueNavigationEventMessage({ |
| 'command': 'didPushState', |
| 'stateObject': serializedState, |
| 'baseUrl': document.baseURI, |
| 'pageUrl': pageUrl.toString(), |
| 'frame_id': gCrWeb.getFrameId(), |
| }); |
| }; |
| |
| History.prototype.replaceState = function( |
| stateObject: object, pageTitle: string, pageUrl: string|URL): void { |
| messageQueue.queueNavigationEventMessage({ |
| 'command': 'willChangeState', |
| 'frame_id': gCrWeb.getFrameId(), |
| }); |
| |
| // JSONStringify throws an exception when given a cyclical object. This |
| // internal implementation detail should not be exposed to callers of |
| // replaceState. Instead, throw a standard exception when stringification |
| // fails. |
| let serializedState = ''; |
| try { |
| // Calling stringify() on undefined causes a JSON parse error. |
| if (typeof (stateObject) !== 'undefined') { |
| serializedState = JSONStringify(stateObject); |
| } |
| } catch (e) { |
| throw new DataCloneError(); |
| } |
| pageUrl = pageUrl || window.location.href; |
| originalWindowHistoryReplaceState.call( |
| history, stateObject, pageTitle, pageUrl); |
| messageQueue.queueNavigationEventMessage({ |
| 'command': 'didReplaceState', |
| 'stateObject': serializedState, |
| 'baseUrl': document.baseURI, |
| 'pageUrl': pageUrl.toString(), |
| 'frame_id': gCrWeb.getFrameId(), |
| }); |
| }; |