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: toolkit/global/extensionPermissions.ftl

WebExtension.MetaData properties expected to be set to absolute moz-extension urls (e.g. baseUrl and optionsPageUrl) are not available yet right after installing a new extension. Once the extension has been fully started, the delegate method WebExtensionController.AddonManagerDelegate.onReady will be providing to the embedder app a new instance of the MetaData object where baseUrl is expected to be set to a "moz-extension://..." url (and optionsPageUrl as well if an options page was declared in the extension manifest.json file).

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

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.

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 baseUrl;
    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.