/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

ChromeUtils.defineModuleGetter(
  this,
  "E10SUtils",
  "resource://gre/modules/E10SUtils.jsm"
);

XPCOMUtils.defineLazyGetter(this, "ConsoleAPIStorage", () => {
  return Cc["@mozilla.org/consoleAPI-storage;1"].getService(
    Ci.nsIConsoleAPIStorage
  );
});

/*
 * The message manager has an upper limit on message sizes that it can
 * reliably forward to the parent so we limit the size of console log event
 * messages that we forward here. The web console is local and receives the
 * full console message, but addons subscribed to console event messages
 * in the parent receive the truncated version. Due to fragmentation,
 * messages as small as 1MB have resulted in IPC allocation failures on
 * 32-bit platforms. To limit IPC allocation sizes, console.log messages
 * with arguments with total size > MSG_MGR_CONSOLE_MAX_SIZE (bytes) have
 * their arguments completely truncated. MSG_MGR_CONSOLE_VAR_SIZE is an
 * approximation of how much space (in bytes) a JS non-string variable will
 * require in the manager's implementation. For strings, we use 2 bytes per
 * char. The console message URI and function name are limited to
 * MSG_MGR_CONSOLE_INFO_MAX characters. We don't attempt to calculate
 * the exact amount of space the message manager implementation will require
 * for a given message so this is imperfect.
 */
const MSG_MGR_CONSOLE_MAX_SIZE = 1024 * 1024; // 1MB
const MSG_MGR_CONSOLE_VAR_SIZE = 8;
const MSG_MGR_CONSOLE_INFO_MAX = 1024;

function ContentProcessForward() {
  this.onConsoleAPILogEvent = this.onConsoleAPILogEvent.bind(this);
  ConsoleAPIStorage.addLogEventListener(
    this.onConsoleAPILogEvent,
    Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
  );
  Services.obs.addObserver(this, "xpcom-shutdown");
  Services.cpmm.addMessageListener(
    "DevTools:StopForwardingContentProcessMessage",
    this
  );
}
ContentProcessForward.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIObserver",
    "nsISupportsWeakReference",
  ]),

  receiveMessage(message) {
    if (message.name == "DevTools:StopForwardingContentProcessMessage") {
      this.uninit();
    }
  },

  onConsoleAPILogEvent(subject, data) {
    const consoleMsg = subject.wrappedJSObject;

    const msgData = {
      ...consoleMsg,
      arguments: [],
      filename: consoleMsg.filename.substring(0, MSG_MGR_CONSOLE_INFO_MAX),
      functionName:
        consoleMsg.functionName &&
        consoleMsg.functionName.substring(0, MSG_MGR_CONSOLE_INFO_MAX),
      // Prevents cyclic object error when using msgData in sendAsyncMessage
      wrappedJSObject: null,
    };

    // We can't send objects over the message manager, so we sanitize
    // them out, replacing those arguments with "<unavailable>".
    const unavailString = "<unavailable>";
    const unavailStringLength = unavailString.length * 2; // 2-bytes per char

    // When the sum of argument sizes reaches MSG_MGR_CONSOLE_MAX_SIZE,
    // replace all arguments with "<truncated>".
    let totalArgLength = 0;

    // Walk through the arguments, checking the type and size.
    for (let arg of consoleMsg.arguments) {
      if (
        (typeof arg == "object" || typeof arg == "function") &&
        arg !== null
      ) {
        if (Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE) {
          // For OOP extensions: we want the developer to be able to see the
          // logs in the Browser Console. When the Addon Toolbox will be more
          // prominent we can revisit.
          try {
            // If the argument is clonable, then send it as-is. If
            // cloning fails, fall back to the unavailable string.
            arg = Cu.cloneInto(arg, {});
          } catch (e) {
            arg = unavailString;
          }
        } else {
          arg = unavailString;
        }
        totalArgLength += unavailStringLength;
      } else if (typeof arg == "string") {
        totalArgLength += arg.length * 2; // 2-bytes per char
      } else {
        totalArgLength += MSG_MGR_CONSOLE_VAR_SIZE;
      }

      if (totalArgLength <= MSG_MGR_CONSOLE_MAX_SIZE) {
        msgData.arguments.push(arg);
      } else {
        // arguments take up too much space
        msgData.arguments = ["<truncated>"];
        break;
      }
    }

    Services.cpmm.sendAsyncMessage("Console:Log", msgData);
  },

  observe(subject, topic, data) {
    if (topic == "xpcom-shutdown") {
      this.uninit();
    }
  },

  uninit() {
    ConsoleAPIStorage.removeLogEventListener(this.onConsoleAPILogEvent);
    Services.obs.removeObserver(this, "xpcom-shutdown");
    Services.cpmm.removeMessageListener(
      "DevTools:StopForwardingContentProcessMessage",
      this
    );
  },
};

// loadProcessScript loads in all processes, including the parent,
// in which we don't need any forwarding
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
  new ContentProcessForward();
}
