/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";

const lazy = {};

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

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "serviceMode",
  "cookiebanners.service.mode",
  Ci.nsICookieBannerService.MODE_DISABLED
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "serviceModePBM",
  "cookiebanners.service.mode.privateBrowsing",
  Ci.nsICookieBannerService.MODE_DISABLED
);

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

// Keep in sync with StorageController.ClearFlags and nsIClearDataService.idl.
const ClearFlags = [
  [
    // COOKIES
    1 << 0,
    Ci.nsIClearDataService.CLEAR_COOKIES |
      Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES |
      Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD |
      Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE |
      Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE,
  ],
  [
    // NETWORK_CACHE
    1 << 1,
    Ci.nsIClearDataService.CLEAR_NETWORK_CACHE,
  ],
  [
    // IMAGE_CACHE
    1 << 2,
    Ci.nsIClearDataService.CLEAR_IMAGE_CACHE,
  ],
  [
    // HISTORY
    1 << 3,
    Ci.nsIClearDataService.CLEAR_HISTORY,
  ],
  [
    // DOM_STORAGES
    1 << 4,
    Ci.nsIClearDataService.CLEAR_DOM_QUOTA |
      Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS |
      Ci.nsIClearDataService.CLEAR_REPORTS |
      Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD |
      Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE |
      Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE,
  ],
  [
    // AUTH_SESSIONS
    1 << 5,
    Ci.nsIClearDataService.CLEAR_AUTH_TOKENS |
      Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
  ],
  [
    // PERMISSIONS
    1 << 6,
    Ci.nsIClearDataService.CLEAR_PERMISSIONS,
  ],
  [
    // SITE_SETTINGS
    1 << 7,
    Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES |
      Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS |
      // former a part of SECURITY_SETTINGS_CLEANER
      Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE |
      Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE,
  ],
  [
    // SITE_DATA
    1 << 8,
    Ci.nsIClearDataService.CLEAR_EME,
    // former a part of SECURITY_SETTINGS_CLEANER
    Ci.nsIClearDataService.CLEAR_HSTS,
  ],
  [
    // ALL
    1 << 9,
    Ci.nsIClearDataService.CLEAR_ALL,
  ],
];

function convertFlags(aJavaFlags) {
  const flags = ClearFlags.filter(cf => {
    return cf[0] & aJavaFlags;
  }).reduce((acc, cf) => {
    return acc | cf[1];
  }, 0);
  return flags;
}

export const GeckoViewStorageController = {
  onEvent(aEvent, aData, aCallback) {
    debug`onEvent ${aEvent} ${aData}`;

    switch (aEvent) {
      case "GeckoView:ClearData": {
        this.clearData(aData.flags, aCallback);
        break;
      }
      case "GeckoView:ClearSessionContextData": {
        this.clearSessionContextData(aData.contextId);
        break;
      }
      case "GeckoView:ClearHostData": {
        this.clearHostData(aData.host, aData.flags, aCallback);
        break;
      }
      case "GeckoView:ClearBaseDomainData": {
        this.clearBaseDomainData(aData.baseDomain, aData.flags, aCallback);
        break;
      }
      case "GeckoView:GetAllPermissions": {
        const rawPerms = Services.perms.all;
        const permissions = rawPerms.map(p => {
          return {
            uri: Services.io.createExposableURI(p.principal.URI).displaySpec,
            principal: lazy.E10SUtils.serializePrincipal(p.principal),
            perm: p.type,
            value: p.capability,
            contextId: p.principal.originAttributes.geckoViewSessionContextId,
            privateMode: p.principal.privateBrowsingId != 0,
          };
        });
        aCallback.onSuccess({ permissions });
        break;
      }
      case "GeckoView:GetPermissionsByURI": {
        const uri = Services.io.newURI(aData.uri);
        const principal = Services.scriptSecurityManager.createContentPrincipal(
          uri,
          aData.contextId
            ? {
                geckoViewSessionContextId: aData.contextId,
                privateBrowsingId: aData.privateBrowsingId,
              }
            : { privateBrowsingId: aData.privateBrowsingId }
        );
        const rawPerms = Services.perms.getAllForPrincipal(principal);
        const permissions = rawPerms.map(p => {
          return {
            uri: Services.io.createExposableURI(p.principal.URI).displaySpec,
            principal: lazy.E10SUtils.serializePrincipal(p.principal),
            perm: p.type,
            value: p.capability,
            contextId: p.principal.originAttributes.geckoViewSessionContextId,
            privateMode: p.principal.privateBrowsingId != 0,
          };
        });
        aCallback.onSuccess({ permissions });
        break;
      }
      case "GeckoView:SetPermission": {
        const principal = lazy.E10SUtils.deserializePrincipal(aData.principal);
        let key = aData.perm;
        if (key == "storage-access") {
          key = "3rdPartyFrameStorage^" + aData.thirdPartyOrigin;
        }
        if (aData.allowPermanentPrivateBrowsing) {
          Services.perms.addFromPrincipalAndPersistInPrivateBrowsing(
            principal,
            key,
            aData.newValue
          );
        } else {
          const expirePolicy = aData.privateMode
            ? Ci.nsIPermissionManager.EXPIRE_SESSION
            : Ci.nsIPermissionManager.EXPIRE_NEVER;
          Services.perms.addFromPrincipal(
            principal,
            key,
            aData.newValue,
            expirePolicy
          );
        }
        break;
      }
      case "GeckoView:SetPermissionByURI": {
        const uri = Services.io.newURI(aData.uri);
        const expirePolicy = aData.privateId
          ? Ci.nsIPermissionManager.EXPIRE_SESSION
          : Ci.nsIPermissionManager.EXPIRE_NEVER;
        const principal = Services.scriptSecurityManager.createContentPrincipal(
          uri,
          {
            geckoViewSessionContextId: aData.contextId ?? undefined,
            privateBrowsingId: aData.privateId,
          }
        );
        Services.perms.addFromPrincipal(
          principal,
          aData.perm,
          aData.newValue,
          expirePolicy
        );
        break;
      }

      case "GeckoView:SetCookieBannerModeForDomain": {
        let exceptionLabel = "SetCookieBannerModeForDomain";
        try {
          const uri = Services.io.newURI(aData.uri);
          if (aData.allowPermanentPrivateBrowsing) {
            exceptionLabel = "setDomainPrefAndPersistInPrivateBrowsing";
            Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing(
              uri,
              aData.mode
            );
          } else {
            Services.cookieBanners.setDomainPref(
              uri,
              aData.mode,
              aData.isPrivateBrowsing
            );
          }
          aCallback.onSuccess();
        } catch (ex) {
          debug`Failed ${exceptionLabel} ${ex}`;
        }
        break;
      }

      case "GeckoView:RemoveCookieBannerModeForDomain": {
        try {
          const uri = Services.io.newURI(aData.uri);
          Services.cookieBanners.removeDomainPref(uri, aData.isPrivateBrowsing);
          aCallback.onSuccess();
        } catch (ex) {
          debug`Failed RemoveCookieBannerModeForDomain ${ex}`;
        }
        break;
      }

      case "GeckoView:GetCookieBannerModeForDomain": {
        try {
          let globalMode;
          if (aData.isPrivateBrowsing) {
            globalMode = lazy.serviceModePBM;
          } else {
            globalMode = lazy.serviceMode;
          }

          if (globalMode === Ci.nsICookieBannerService.MODE_DISABLED) {
            aCallback.onSuccess({ mode: globalMode });
            return;
          }

          const uri = Services.io.newURI(aData.uri);
          const mode = Services.cookieBanners.getDomainPref(
            uri,
            aData.isPrivateBrowsing
          );
          if (mode !== Ci.nsICookieBannerService.MODE_UNSET) {
            aCallback.onSuccess({ mode });
          } else {
            aCallback.onSuccess({ mode: globalMode });
          }
        } catch (ex) {
          aCallback.onError(`Unexpected error: ${ex}`);
          debug`Failed GetCookieBannerModeForDomain ${ex}`;
        }
        break;
      }
    }
  },

  async clearData(aFlags, aCallback) {
    const flags = convertFlags(aFlags);

    // storageAccessAPI permissions record every site that the user
    // interacted with and thus mirror history quite closely. It makes
    // sense to clear them when we clear history. However, since their absence
    // indicates that we can purge cookies and site data for tracking origins without
    // user interaction, we need to ensure that we only delete those permissions that
    // do not have any existing storage.
    if (flags & Ci.nsIClearDataService.CLEAR_HISTORY) {
      const principalsCollector = new lazy.PrincipalsCollector();
      const principals = await principalsCollector.getAllPrincipals();
      await new Promise(resolve => {
        Services.clearData.deleteUserInteractionForClearingHistory(
          principals,
          0,
          resolve
        );
      });
    }

    new Promise(resolve => {
      Services.clearData.deleteData(flags, resolve);
    }).then(() => {
      aCallback.onSuccess();
    });
  },

  clearHostData(aHost, aFlags, aCallback) {
    new Promise(resolve => {
      Services.clearData.deleteDataFromHost(
        aHost,
        /* isUserRequest */ true,
        convertFlags(aFlags),
        resolve
      );
    }).then(() => {
      aCallback.onSuccess();
    });
  },

  clearBaseDomainData(aBaseDomain, aFlags, aCallback) {
    // Ensure we have a site at this point.
    let schemelessSite = Services.eTLD.getSchemelessSiteFromHost(aBaseDomain);
    new Promise(resolve => {
      Services.clearData.deleteDataFromSite(
        schemelessSite,
        {},
        /* isUserRequest */ true,
        convertFlags(aFlags),
        resolve
      );
    }).then(() => {
      aCallback.onSuccess();
    });
  },

  clearSessionContextData(aContextId) {
    const pattern = { geckoViewSessionContextId: aContextId };
    debug`clearSessionContextData ${pattern}`;
    Services.clearData.deleteDataFromOriginAttributesPattern(pattern);
    // Call QMS explicitly to work around bug 1537882.
    Services.qms.clearStoragesForOriginAttributesPattern(
      JSON.stringify(pattern)
    );
  },
};
