chromium/ios/web/navigation/resources/navigation.ts

// 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.message.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.message.getFrameId()
  });
};

History.prototype.replaceState =
    function(stateObject: object, pageTitle: string,
        pageUrl: string | URL): void {
  messageQueue.queueNavigationEventMessage({
    'command': 'willChangeState',
    'frame_id': gCrWeb.message.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.message.getFrameId()
  });
};