GeckoView Extension Managing API
================================

Agi Sferro <agi@sferro.dev>

November 19th, 2019

Introduction
------------

This document describes the API for installing, uninstalling and updating
Extensions with GeckoView.

Installing an extension provides the extension the ability to run at startup
time, especially useful for e.g. extensions that intercept network requests,
like an ad-blocker or a proxy extension. It also provides additional security
from third-party extensions like signature checking and prompting the user for
permissions.

For this version of the API we will assume that the extension store is backed
by ``addons.mozilla.org``, and so are the signatures. Running a third-party
extension store is something we might consider in the future but explicitly not
in scope for this document.

API
---

The embedder will be able to install, uninstall, enable, disable and update
extensions using the similarly-named APIs.

Installing
^^^^^^^^^^

Gecko will download the extension pointed by the URI provided in install, parse
the manifest and signature and provide an ``onInstallPrompt`` callback with the
list of permissions requested by the extension and some information about the
extension.

The embedder will be able to install bundled first-party extensions using
``installBuiltIn``. This method will only accept URIs that start with
``resource://`` and will give additional privileges like being able to use app
messaging and not needing a signature.

Each permission will have a machine readable name that the embedder will use to
produce user-facing internationalized strings. E.g. “bookmarks” gives access to
bookmarks, “sessions” gives access to recently closed sessions. The full list
of permissions that are currently shown to the UI in Firefox Desktop is
available at: ``chrome/browser/browser.properties``

Updating
^^^^^^^^

To update an extension, the embedder will be able to call update which will
check if any update is available (using the update_url provided by the
extension, or addons.mozilla.org if no update_url has been provided). The
embedder will receive a GeckoResult that will provide the updated extension
object. This result can also be used to know when the update process is
complete, e.g. the embedder could use it to display a persistent notification
to the user to avoid having the app be killed while updates are in process.

If the updated extension needs additional permissions, ``GeckoView`` will call
``onUpdatePrompt``.

Until this callback is resolved (i.e. the embedder’s returned ``GeckoResult``
is completed), the old addon will be running, only when the prompt is resolved
and the update is applied the new version of the addon starts running and the
``GeckoResult`` returned from update is resolved.

This callback will provide both the current ``WebExtension`` object and the
updated WebExtension object so that the embedder can show appropriate
information to the user, e.g. the app might decide to remember whether the user
denied the request for a certain version and only prompt the user once the
version string changes.

As a side effect of updating, Gecko will check its internal blocklist and might
disable extensions that are incompatible with the current version of Gecko or
deemed unsafe. The resulting ``WebExtension`` object will reflect that by
having isEnabled set to false. The embedder will be able to inspect the reason
why the extension was disabled using ``metaData.blockedReason``.

Gecko will not update any extension or blocklist state without the embedder’s
input.

Enabling and Disabling
^^^^^^^^^^^^^^^^^^^^^^

Embedders will be able to enable and disabling extension using the homonymous
APIs. Calling enable on an extension might not actually enable it if the
extension has been added to the Gecko blocklist. Embedders can check the value
of ``metaData.blockedReason`` to display to the user whether the extension can
actually be enabled or not. The returned WebExtension object will reflect the
updated enablement state in isEnabled.

Listing
^^^^^^^

The embedder is expected to keep a collection of all available extensions using
the result of install and update. To retrieve the extensions that are already
installed the embedder will be able to use ``listInstalled`` which will
asynchronously retrieve the full list of extensions. We recommend calling
``listInstalled`` every time the user is presented with the extension manager
UI to ensure all information is up to date.

Outline
^^^^^^^

.. code:: java

  public class WebExtensionController {
    // Start the process of installing an extension,
    // the embedder will either get the installed extension
    // or an error
    GeckoResult<WebExtension> install(String uri);

    // Install a built-in WebExtension with privileged
    // permissions, uri must be resource://
    // Privileged WebExtensions have access to experiments
    // (i.e. they can run chrome code), don’t need signatures
    // and have access to native messaging to the app
    GeckoResult<WebExtension> installBuiltIn(String uri)

    GeckoResult<Void> uninstall(WebExtension extension);

    GeckoResult<WebExtension> enable(WebExtension extension);

    GeckoResult<WebExtension> disable(WebExtension extension);

    GeckoResult<List<WebExtension>> listInstalled();

    // Checks for updates. This method returns a GeckoResult that is
    // resolved either with the updated WebExtension object or null
    // if the extension does not have pending updates.
    GeckoResult<WebExtension> update(WebExtension extension);

    public interface PromptDelegate {
        GeckoResult<AllowOrDeny> onInstallPrompt(WebExtension extension);

        GeckoResult<AllowOrDeny> onUpdatePrompt(
            WebExtension currentlyInstalled,
            WebExtension updatedExtension,
            List<String> newPermissions);

        // Called when the extension calls browser.permission.request
        GeckoResult<AllowOrDeny> onOptionalPrompt(
            WebExtension extension,
            List<String> optionalPermissions);
    }

    void setPromptDelegate(PromptDelegate promptDelegate);
  }

As part of this document, we will add a ``MetaData`` field to WebExtension
which will contain all the information known about the extension. Note: we will
rename ``ActionIcon`` to Icon to represent its generic use as the
``WebExtension`` icon class.

.. code:: java

  public class WebExtension {
    // Renamed from ActionIcon
    static class Icon {}

    final MetaData metadata;
    final boolean isBuiltIn;

    final boolean isEnabled;

    public static class SignedStateFlags {
      final static int UNKNOWN;
      final static int PRELIMINARY;
      final static int SIGNED;
      final static int SYSTEM;
      final static int PRIVILEGED;
    }

    // See nsIBlocklistService.idl
    public static class BlockedReason {
      final static int NOT_BLOCKED;
      final static int SOFTBLOCKED;
      final static int BLOCKED;
      final static int OUTDATED;
      final static int VULNERABLE_UPDATE_AVAILABLE;
      final static int VULNERABLE_NO_UPDATE;
    }

    public class MetaData {
      final Icon icon;
      final String[] permissions;
      final String[] origins;
      final String name;
      final String description;
      final String version;
      final String creatorName;
      final String creatorUrl;
      final String homepageUrl;
      final String optionsPageUrl;
      final boolean openOptionsPageInTab;
      final boolean isRecommended;
      final @BlockedReason int blockedReason;
      final @SignedState int signedState;
      // more if needed
    }
  }

Implementation Details
^^^^^^^^^^^^^^^^^^^^^^

We will use ``AddonManager`` as a backend for ``WebExtensionController`` and
delegate the prompt to the app using ``PromptDelegate``. We will also merge
``WebExtensionController`` and ``WebExtensionEventDispatcher`` for ease of
implementation.

Existing APIs
^^^^^^^^^^^^^

Some APIs today return a ``WebExtension`` object that might have not been
fetched yet by ``listInstalled``. In these cases, GeckoView will return a stub
``WebExtension`` object in which the metadata field will be null to avoid
waiting for a addon list call. To ensure that the metadata field is populated,
the embedder will need to call ``listInstalled`` at least once during the app
startup.

Deprecation Path
^^^^^^^^^^^^^^^^

The existing ``registerWebExtension`` and ``unregisterWebExtension`` APIs will
be deprecated by ``installBuiltIn`` and ``uninstall``. We will remove the above
APIs 6 releases after the implementation of ``installBuiltIn`` lands and mark
it as deprecated in the API.
