/* 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/. */

/**
 * This module handles wrapping privileged objects so that they can be exposed
 * to unprivileged contexts. It is only to be used in automated tests.
 *
 * Its exact semantics are also liable to change at any time, so any callers
 * relying on undocumented behavior or subtle platform features should expect
 * breakage. Those callers should, wherever possible, migrate to fully
 * chrome-privileged scripts when they need to interact with privileged APIs.
 */

// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
// See bug 1481337.
/* globals XPCNativeWrapper */

Cu.crashIfNotInAutomation();

let wrappedObjects = new WeakMap();
let perWindowInfo = new WeakMap();
let noAutoWrap = new WeakSet();

function isWrappable(x) {
  if (typeof x === "object") {
    return x !== null;
  }
  return typeof x === "function";
}

function isWrapper(x) {
  try {
    return isWrappable(x) && wrappedObjects.has(x);
  } catch (e) {
    // If `x` is a remote object proxy, trying to access an unexpected property
    // on it will throw a security error, even though we're chrome privileged.
    // However, remote proxies are not SpecialPowers wrappers, so:
    return false;
  }
}

function unwrapIfWrapped(x) {
  return isWrapper(x) ? unwrapPrivileged(x) : x;
}

function wrapIfUnwrapped(x, w) {
  return isWrapper(x) ? x : wrapPrivileged(x, w);
}

function isObjectOrArray(obj) {
  if (Object(obj) !== obj) {
    return false;
  }
  let arrayClasses = [
    "Object",
    "Array",
    "Int8Array",
    "Uint8Array",
    "Int16Array",
    "Uint16Array",
    "Int32Array",
    "Uint32Array",
    "Float32Array",
    "Float64Array",
    "Uint8ClampedArray",
  ];
  let className = Cu.getClassName(obj, true);
  return arrayClasses.includes(className);
}

// In general, we want Xray wrappers for content DOM objects, because waiving
// Xray gives us Xray waiver wrappers that clamp the principal when we cross
// compartment boundaries. However, there are some exceptions where we want
// to use a waiver:
//
// * Xray adds some gunk to toString(), which has the potential to confuse
//   consumers that aren't expecting Xray wrappers. Since toString() is a
//   non-privileged method that returns only strings, we can just waive Xray
//   for that case.
//
// * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
//   filter out tricky things like callables. This is the right thing for
//   security in general, but tends to break tests that try to pass object
//   literals into SpecialPowers. So we waive [[Object]] and [[Array]]
//   instances before inspecting properties.
//
// * When we don't have meaningful Xray semantics, we create an Opaque
//   XrayWrapper for security reasons. For test code, we generally want to see
//   through that sort of thing.
function waiveXraysIfAppropriate(obj, propName) {
  if (
    propName == "toString" ||
    isObjectOrArray(obj) ||
    /Opaque/.test(Object.prototype.toString.call(obj))
  ) {
    return XPCNativeWrapper.unwrap(obj);
  }
  return obj;
}

// We can't call apply() directy on Xray-wrapped functions, so we have to be
// clever.
function doApply(fun, invocant, args) {
  // We implement Xrays to pure JS [[Object]] instances that filter out tricky
  // things like callables. This is the right thing for security in general,
  // but tends to break tests that try to pass object literals into
  // SpecialPowers. So we waive [[Object]] instances when they're passed to a
  // SpecialPowers-wrapped callable.
  //
  // Note that the transitive nature of Xray waivers means that any property
  // pulled off such an object will also be waived, and so we'll get principal
  // clamping for Xrayed DOM objects reached from literals, so passing things
  // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
  // go that deep.
  args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x));
  return Reflect.apply(fun, invocant, args);
}

function wrapPrivileged(obj, win) {
  // Primitives pass straight through.
  if (!isWrappable(obj)) {
    return obj;
  }

  // No double wrapping.
  if (isWrapper(obj)) {
    throw new Error("Trying to double-wrap object!");
  }

  let { windowID, proxies, handler } = perWindowInfo.get(win) || {};
  // |windowUtils| is undefined if |win| is a non-window object
  // such as a sandbox.
  let currentID = win.windowGlobalChild
    ? win.windowGlobalChild.innerWindowId
    : 0;
  // Values are dead objects if the inner window is changed.
  if (windowID !== currentID) {
    windowID = currentID;
    proxies = new WeakMap();
    handler = Cu.cloneInto(SpecialPowersHandler, win, {
      cloneFunctions: true,
    });
    handler.wrapped = new win.WeakMap();
    perWindowInfo.set(win, { windowID, proxies, handler });
  }

  if (proxies.has(obj)) {
    return proxies.get(obj).proxy;
  }

  let className = Cu.getClassName(obj, true);
  if (className === "ArrayBuffer") {
    // Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to
    // return a real ArrayBuffer.
    return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win);
  }

  let dummy;
  if (typeof obj === "function") {
    dummy = Cu.exportFunction(function () {}, win);
  } else {
    dummy = new win.Object();
  }
  handler.wrapped.set(dummy, { obj });

  let proxy = new win.Proxy(dummy, handler);
  wrappedObjects.set(proxy, obj);
  switch (className) {
    case "AnonymousContent":
      // Caching anonymous content will cause crashes (bug 1636015).
      break;
    case "CSSStyleProperties":
    case "CSSStyleRule":
    case "CSSStyleSheet":
      // Caching these classes will cause memory leaks.
      break;
    default:
      proxies.set(obj, { proxy });
      break;
  }
  return proxy;
}

function unwrapPrivileged(x) {
  // We don't wrap primitives, so sometimes we have a primitive where we'd
  // expect to have a wrapper. The proxy pretends to be the type that it's
  // emulating, so we can just as easily check isWrappable() on a proxy as
  // we can on an unwrapped object.
  if (!isWrappable(x)) {
    return x;
  }

  // If we have a wrappable type, make sure it's wrapped.
  if (!isWrapper(x)) {
    throw new Error("Trying to unwrap a non-wrapped object!");
  }

  // unwrapped.
  return wrappedObjects.get(x);
}

function wrapExceptions(global, fn) {
  try {
    return fn();
  } catch (e) {
    throw wrapIfUnwrapped(e, global);
  }
}

let SpecialPowersHandler = {
  construct(target, args) {
    // The arguments may or may not be wrappers. Unwrap them if necessary.
    var unwrappedArgs = Array.from(Cu.waiveXrays(args), x =>
      unwrapIfWrapped(Cu.unwaiveXrays(x))
    );

    // We want to invoke "obj" as a constructor, but using unwrappedArgs as
    // the arguments.
    let global = Cu.getGlobalForObject(this);
    return wrapExceptions(global, () =>
      wrapIfUnwrapped(
        Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs),
        global
      )
    );
  },

  apply(target, thisValue, args) {
    let wrappedObject = this.wrapped.get(target).obj;
    let global = Cu.getGlobalForObject(this);
    // The invocant and arguments may or may not be wrappers. Unwrap
    // them if necessary.
    var invocant = unwrapIfWrapped(thisValue);

    return wrapExceptions(global, () => {
      if (noAutoWrap.has(wrappedObject)) {
        args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x));
        return doApply(wrappedObject, invocant, args);
      }

      if (wrappedObject.name == "then") {
        args = Array.from(Cu.waiveXrays(args), x =>
          wrapCallback(Cu.unwaiveXrays(x), global)
        );
      } else {
        args = Array.from(Cu.waiveXrays(args), x =>
          unwrapIfWrapped(Cu.unwaiveXrays(x))
        );
      }

      return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global);
    });
  },

  has(target, prop) {
    return Reflect.has(this.wrapped.get(target).obj, prop);
  },

  get(target, prop) {
    let global = Cu.getGlobalForObject(this);
    return wrapExceptions(global, () => {
      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
      let val = Reflect.get(obj, prop);
      return wrapIfUnwrapped(val, global);
    });
  },

  set(target, prop, val) {
    return wrapExceptions(Cu.getGlobalForObject(this), () => {
      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
      return Reflect.set(obj, prop, unwrapIfWrapped(val));
    });
  },

  delete(target, prop) {
    return wrapExceptions(Cu.getGlobalForObject(this), () => {
      return Reflect.deleteProperty(this.wrapped.get(target).obj, prop);
    });
  },

  defineProperty() {
    throw new Error(
      "Can't call defineProperty on SpecialPowers wrapped object"
    );
  },

  getOwnPropertyDescriptor(target, prop) {
    let global = Cu.getGlobalForObject(this);
    return wrapExceptions(global, () => {
      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
      let desc = Reflect.getOwnPropertyDescriptor(obj, prop);

      if (desc === undefined) {
        return undefined;
      }

      // Transitively maintain the wrapper membrane.
      let wrapIfExists = key => {
        if (key in desc) {
          desc[key] = wrapIfUnwrapped(desc[key], global);
        }
      };

      wrapIfExists("value");
      wrapIfExists("get");
      wrapIfExists("set");

      // A trapping proxy's properties must always be configurable, but sometimes
      // we come across non-configurable properties. Tell a white lie.
      desc.configurable = true;

      return wrapIfUnwrapped(desc, global);
    });
  },

  ownKeys(target) {
    let props = [];

    // Do the normal thing.
    let wrappedObject = this.wrapped.get(target).obj;
    let flt = a => !props.includes(a);
    props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt));

    // If we've got an Xray wrapper, include the expandos as well.
    if ("wrappedJSObject" in wrappedObject) {
      props = props.concat(
        Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt)
      );
    }

    return Cu.cloneInto(props, Cu.getGlobalForObject(this));
  },

  preventExtensions() {
    throw new Error(
      "Can't call preventExtensions on SpecialPowers wrapped object"
    );
  },
};

function wrapCallback(cb, win) {
  // Do not wrap if it is already privileged.
  if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) {
    return cb;
  }
  return function SpecialPowersCallbackWrapper() {
    var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win));
    let invocant = wrapIfUnwrapped(this, win);
    return unwrapIfWrapped(cb.apply(invocant, args));
  };
}

function wrapCallbackObject(obj, win) {
  // Do not wrap if it is already privileged.
  if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) {
    return obj;
  }
  obj = Cu.waiveXrays(obj);
  var wrapper = {};
  for (var i in obj) {
    if (typeof obj[i] == "function") {
      wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win);
    } else {
      wrapper[i] = obj[i];
    }
  }
  return wrapper;
}

function disableAutoWrap(...objs) {
  objs.forEach(x => noAutoWrap.add(x));
}

export var WrapPrivileged = {
  wrap: wrapIfUnwrapped,
  unwrap: unwrapIfWrapped,

  isWrapper,

  wrapCallback,
  wrapCallbackObject,

  disableAutoWrap,
};
