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

import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
});

const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";

const MAPPED_TO_EXTENSION_PERMISSIONS = [
  "persistent-storage",
  // TODO(Bug 1336194): support geolocation manifest permission
  // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1336194#c17)l
];

export class GeckoViewPermissionChild extends GeckoViewActorChild {
  getMediaPermission(aPermission) {
    return this.sendQuery("GeckoView:MediaPermission", {
      ...aPermission,
    });
  }

  addCameraPermission() {
    return this.sendQuery("AddCameraPermission");
  }

  getAppPermissions(aPermissions) {
    return this.sendQuery("GetAppPermissions", aPermissions);
  }

  mediaRecordingStatusChanged(aDevices) {
    return this.sendAsyncMessage("GeckoView:MediaRecordingStatusChanged", {
      devices: aDevices,
    });
  }

  // Some WebAPI permissions can be requested and granted to extensions through the
  // the extension manifest.json, which the user have been already prompted for
  // (e.g. at install time for the one listed in the manifest.json permissions property,
  // or at runtime through the optional_permissions property and the permissions.request
  // WebExtensions API method).
  //
  // WebAPI permission that are expected to be mapped to extensions permissions are listed
  // in the MAPPED_TO_EXTENSION_PERMISSIONS array.
  //
  // @param {nsIContentPermissionType} perm
  //        The WebAPI permission being requested
  // @param {nsIContentPermissionRequest} aRequest
  //        The nsIContentPermissionRequest as received by the promptPermission method.
  //
  // @returns {null | { allow: boolean, permission: Object }
  //          Returns null if the request was not handled and should continue with the
  //          regular permission prompting flow, otherwise it returns an object to
  //          allow or disallow the permission request right away.
  checkIfGrantedByExtensionPermissions(perm, aRequest) {
    if (!aRequest.principal.addonPolicy) {
      // Not an extension, continue with the regular permission prompting flow.
      return null;
    }

    // Return earlier and continue with the regular permission prompting flow if the
    // the permission is no one that can be requested from the extension manifest file.
    if (!MAPPED_TO_EXTENSION_PERMISSIONS.includes(perm.type)) {
      return null;
    }

    // Disallow if the extension is not active anymore.
    if (!aRequest.principal.addonPolicy.active) {
      return { allow: false };
    }

    // Check if the permission have been already granted to the extension, if it is allow it right away.
    const isGranted =
      Services.perms.testPermissionFromPrincipal(
        aRequest.principal,
        perm.type
      ) === Services.perms.ALLOW_ACTION;
    if (isGranted) {
      return {
        allow: true,
        permission: { [perm.type]: Services.perms.ALLOW_ACTION },
      };
    }

    // continue with the regular permission prompting flow otherwise.
    return null;
  }

  async promptPermission(aRequest) {
    // Only allow exactly one permission request here.
    const types = aRequest.types.QueryInterface(Ci.nsIArray);
    if (types.length !== 1) {
      return { allow: false };
    }

    const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);

    // Check if the request principal is an extension principal and if the permission requested
    // should be already granted based on the extension permissions (or disallowed right away
    // because the extension is not enabled anymore.
    const extensionResult = this.checkIfGrantedByExtensionPermissions(
      perm,
      aRequest
    );
    if (extensionResult) {
      return extensionResult;
    }

    if (
      perm.type === "desktop-notification" &&
      !aRequest.hasValidTransientUserGestureActivation &&
      Services.prefs.getBoolPref(
        "dom.webnotifications.requireuserinteraction",
        true
      )
    ) {
      // We need user interaction and don't have it.
      return { allow: false };
    }

    const principal =
      perm.type === "storage-access"
        ? aRequest.principal
        : aRequest.topLevelPrincipal;

    let allowOrDeny;
    try {
      allowOrDeny = await this.sendQuery("GeckoView:ContentPermission", {
        uri: principal.URI.displaySpec,
        thirdPartyOrigin: aRequest.principal.origin,
        principal: lazy.E10SUtils.serializePrincipal(principal),
        perm: perm.type,
        value: perm.capability,
        contextId: principal.originAttributes.geckoViewSessionContextId ?? null,
        privateMode: principal.privateBrowsingId != 0,
      });

      if (allowOrDeny === Services.perms.ALLOW_ACTION) {
        // Ask for app permission after asking for content permission.
        if (perm.type === "geolocation") {
          const granted = await this.getAppPermissions([
            PERM_ACCESS_FINE_LOCATION,
          ]);
          allowOrDeny = granted
            ? Services.perms.ALLOW_ACTION
            : Services.perms.DENY_ACTION;
        }
      }
    } catch (error) {
      console.error("Permission error:", error);
      allowOrDeny = Services.perms.DENY_ACTION;
    }

    // Manually release the target request here to facilitate garbage collection.
    aRequest = undefined;

    const allow = allowOrDeny === Services.perms.ALLOW_ACTION;

    // The storage access code adds itself to the perm manager; no need for us to do it.
    if (perm.type === "storage-access") {
      if (allow) {
        return { allow, permission: { "storage-access": "allow" } };
      }
      return { allow };
    }

    Services.perms.addFromPrincipal(
      principal,
      perm.type,
      allowOrDeny,
      Services.perms.EXPIRE_NEVER
    );

    return { allow };
  }
}

const { debug, warn } = GeckoViewPermissionChild.initLogging(
  "GeckoViewPermissionChild"
);
