tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

web-extensions.rst (15964B)


      1 .. -*- Mode: rst; fill-column: 80; -*-
      2 
      3 ============================
      4 Interacting with Web content
      5 ============================
      6 
      7 Interacting with Web content and WebExtensions
      8 ==============================================
      9 
     10 GeckoView allows embedder applications to register and run
     11 `WebExtensions <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions>`_
     12 in a GeckoView instance. Extensions are the preferred way to interact
     13 with Web content.
     14 
     15 .. contents:: :local:
     16 
     17 Running extensions in GeckoView
     18 -------------------------------
     19 
     20 Extensions bundled with applications can be provided in a folder in the
     21 ``/assets`` section of the APK. Like ordinary extensions, every
     22 extension bundled with GeckoView requires a
     23 `manifest.json <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json>`_
     24 file.
     25 
     26 To locate files bundled with the APK, GeckoView provides a shorthand
     27 ``resource://android/`` that points to the root of the APK.
     28 
     29 E.g. ``resource://android/assets/messaging/`` will point to the
     30 ``/assets/messaging/`` folder present in the APK.
     31 
     32 Note: Every installed extension will need an
     33 `id <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings>`_
     34 and
     35 `version <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version>`_
     36 specified in the ``manifest.json`` file.
     37 
     38 To install a bundled extension in GeckoView, simply call
     39 `WebExtensionController.installBuiltIn <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtensionController.html#installBuiltIn(java.lang.String)>`_.
     40 
     41 .. code:: java
     42 
     43   runtime.getWebExtensionController()
     44     .installBuiltIn("resource://android/assets/messaging/")
     45 
     46 Note that the lifetime of the extension is not tied with the lifetime of
     47 the
     48 `GeckoRuntime <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html>`_
     49 instance. The extension persists even when your app is restarted.
     50 Installing at every start up is fine, but it could be slow. To avoid
     51 installing multiple times you can use ``WebExtensionRuntime.ensureBuiltIn``,
     52 which will only install if the extension is not installed yet.
     53 
     54 .. code:: java
     55 
     56   runtime.getWebExtensionController()
     57     .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com")
     58     .accept(
     59           extension -> Log.i("MessageDelegate", "Extension installed: " + extension),
     60           e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
     61     );
     62 
     63 Communicating with Web Content
     64 ------------------------------
     65 
     66 GeckoView allows bidirectional communication with Web pages through
     67 extensions.
     68 
     69 When using GeckoView, `native
     70 messaging <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#Exchanging_messages>`_
     71 can be used for communicating to and from the browser.
     72 
     73 - `runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_
     74  for one-off messages.
     75 - `runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_
     76  for connection-based messaging.
     77 
     78 Note: these APIs are only available when the ``geckoViewAddons``
     79 `permission <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>`_
     80 is present in the ``manifest.json`` file of the extension.
     81 
     82 One-off messages
     83 ~~~~~~~~~~~~~~~~
     84 
     85 The easiest way to send messages from a `content
     86 script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts>`_
     87 or a `background
     88 script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#Background_scripts>`_
     89 is using
     90 `runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_.
     91 
     92 Note: Ordinarily, native extensions would use a `native
     93 manifest <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#App_manifest>`_
     94 to define what native app identifier to use. For GeckoView this is *not*
     95 needed, the ``nativeApp`` parameter in ``setMessageDelegate`` will be
     96 use to determine what native app string is used.
     97 
     98 Messaging Example
     99 ~~~~~~~~~~~~~~~~~
    100 
    101 To receive messages from the background script, call
    102 `setMessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html#setMessageDelegate(org.mozilla.geckoview.WebExtension.MessageDelegate,java.lang.String)>`_
    103 on the
    104 `WebExtension <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html>`_
    105 object.
    106 
    107 .. code:: java
    108 
    109   extension.setMessageDelegate(messageDelegate, "browser");
    110 
    111 `SessionController.setMessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.SessionController.html#setMessageDelegate(org.mozilla.geckoview.WebExtension,org.mozilla.geckoview.WebExtension.MessageDelegate,java.lang.String)>`_
    112 allows the app to receive messages from content scripts.
    113 
    114 .. code:: java
    115 
    116   session.getWebExtensionController()
    117       .setMessageDelegate(extension, messageDelegate, "browser");
    118 
    119 Note: the ``"browser"`` parameter in the code above determines what
    120 native app id the extension can use to send native messages.
    121 
    122 Note: extension can only send messages from content scripts if
    123 explicitly authorized by the app by adding
    124 ``nativeMessagingFromContent`` in the manifest.json file, e.g.
    125 
    126 .. code:: json
    127 
    128     "permissions": [
    129       "nativeMessaging",
    130       "nativeMessagingFromContent",
    131       "geckoViewAddons"
    132     ]
    133 
    134 Example
    135 ~~~~~~~
    136 
    137 Let’s set up an activity that registers an extension located in the
    138 ``/assets/messaging/`` folder of the APK. This activity will set up a
    139 `MessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html>`_
    140 that will be used to communicate with Web Content.
    141 
    142 You can find the full example here:
    143 `MessagingExample <https://searchfox.org/mozilla-central/source/mobile/android/examples/messaging_example>`_.
    144 
    145 Activity.java
    146 ^^^^^^^^^^^^^
    147 
    148 .. code:: java
    149 
    150   WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
    151       @Nullable
    152       public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
    153                                            final @NonNull Object message,
    154                                            final @NonNull WebExtension.MessageSender sender) {
    155           // The sender object contains information about the session that
    156           // originated this message and can be used to validate that the message
    157           // has been sent from the expected location.
    158 
    159           // Be careful when handling the type of message as it depends on what
    160           // type of object was sent from the WebExtension script.
    161           if (message instanceof JSONObject) {
    162               // Do something with message
    163           }
    164           return null;
    165       }
    166   };
    167 
    168   // Let's make sure the extension is installed
    169   runtime.getWebExtensionController()
    170           .ensureBuiltIn(EXTENSION_LOCATION, "messaging@example.com").accept(
    171               // Set delegate that will receive messages coming from this extension.
    172               extension -> session.getWebExtensionController()
    173                       .setMessageDelegate(extension, messageDelegate, "browser"),
    174               // Something bad happened, let's log an error
    175               e -> Log.e("MessageDelegate", "Error registering extension", e)
    176           );
    177 
    178 
    179 Now add the ``geckoViewAddons``, ``nativeMessaging`` and
    180 ``nativeMessagingFromContent`` permissions to your ``manifest.json``
    181 file.
    182 
    183 /assets/messaging/manifest.json
    184 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    185 
    186 .. code:: json
    187 
    188   {
    189     "manifest_version": 2,
    190     "name": "messaging",
    191     "version": "1.0",
    192     "description": "Example messaging web extension.",
    193     "browser_specific_settings": {
    194       "gecko": {
    195         "id": "messaging@example.com"
    196       }
    197     },
    198     "content_scripts": [
    199       {
    200         "matches": ["*://*.twitter.com/*"],
    201         "js": ["messaging.js"]
    202       }
    203     ],
    204     "permissions": [
    205       "nativeMessaging",
    206       "nativeMessagingFromContent",
    207       "geckoViewAddons"
    208     ]
    209   }
    210 
    211 And finally, write a content script that will send a message to the app
    212 when a certain event occurs. For example, you could send a message
    213 whenever a `WPA
    214 manifest <https://developer.mozilla.org/en-US/docs/Web/Manifest>`_ is
    215 found on the page. Note that our ``nativeApp`` identifier used for
    216 ``sendNativeMessage`` is the same as the one used in the
    217 ``setMessageDelegate`` call in `Activity.java <#activityjava>`_.
    218 
    219 /assets/messaging/messaging.js
    220 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    221 
    222 .. code:: JavaScript
    223 
    224   let manifest = document.querySelector("head > link[rel=manifest]");
    225   if (manifest) {
    226        fetch(manifest.href)
    227           .then(response => response.json())
    228           .then(json => {
    229                let message = {type: "WPAManifest", manifest: json};
    230                browser.runtime.sendNativeMessage("browser", message);
    231           });
    232   }
    233 
    234 You can handle this message in the ``onMessage`` method in the
    235 ``messageDelegate`` `above <#activityjava>`_.
    236 
    237 .. code:: java
    238 
    239   @Nullable
    240   public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
    241                                        final @NonNull Object message,
    242                                        final @NonNull WebExtension.MessageSender sender) {
    243       if (message instanceof JSONObject) {
    244           JSONObject json = (JSONObject) message;
    245           try {
    246               if (json.has("type") && "WPAManifest".equals(json.getString("type"))) {
    247                   JSONObject manifest = json.getJSONObject("manifest");
    248                   Log.d("MessageDelegate", "Found WPA manifest: " + manifest);
    249               }
    250           } catch (JSONException ex) {
    251               Log.e("MessageDelegate", "Invalid manifest", ex);
    252           }
    253       }
    254       return null;
    255   }
    256 
    257 Note that, in the case of content scripts, ``sender.session`` will be a
    258 reference to the ``GeckoSession`` instance from which the message
    259 originated. For background scripts, ``sender.session`` will always be
    260 ``null``.
    261 
    262 Also note that the type of ``message`` will depend on what was sent from
    263 the extension.
    264 
    265 The type of ``message`` will be ``JSONObject`` when the extension sends
    266 a javascript object, but could also be a primitive type if the extension
    267 sends one, e.g. for
    268 
    269 .. code:: javascript
    270 
    271   runtime.browser.sendNativeMessage("browser", "Hello World!");
    272 
    273 the type of ``message`` will be ``java.util.String``.
    274 
    275 Connection-based messaging
    276 --------------------------
    277 
    278 For more complex scenarios or for when you want to send messages *from*
    279 the app to the extension,
    280 `runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_
    281 is the appropriate API to use.
    282 
    283 ``connectNative`` returns a
    284 `runtime.Port <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port>`_
    285 that can be used to send messages to the app. On the app side,
    286 implementing
    287 `MessageDelegate#onConnect <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html#onConnect(org.mozilla.geckoview.WebExtension.Port)>`_
    288 will allow you to receive a
    289 `Port <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.Port.html>`_
    290 object that can be used to receive and send messages to the extension.
    291 
    292 The following example can be found
    293 `here <https://searchfox.org/mozilla-central/source/mobile/android/examples/port_messaging_example>`_.
    294 
    295 For this example, the extension side will do the following:
    296 
    297 - open a port on the background script using ``connectNative``
    298 - listen on the port and log to console every message received
    299 - send a message immediately after opening the port.
    300 
    301 /assets/messaging/background.js
    302 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    303 
    304 .. code:: JavaScript
    305 
    306   // Establish connection with app
    307   let port = browser.runtime.connectNative("browser");
    308   port.onMessage.addListener(response => {
    309       // Let's just echo the message back
    310       port.postMessage(`Received: ${JSON.stringify(response)}`);
    311   });
    312   port.postMessage("Hello from WebExtension!");
    313 
    314 On the app side, following the `above <#activityjava>`_ example,
    315 ``onConnect`` will be storing the ``Port`` object in a member variable
    316 and then using it when needed.
    317 
    318 .. code:: java
    319 
    320   private WebExtension.Port mPort;
    321 
    322   @Override
    323   protected void onCreate(Bundle savedInstanceState) {
    324       // ... initialize GeckoView
    325 
    326       // This delegate will handle all communications from and to a specific Port
    327       // object
    328       WebExtension.PortDelegate portDelegate = new WebExtension.PortDelegate() {
    329           public WebExtension.Port port = null;
    330 
    331           public void onPortMessage(final @NonNull Object message,
    332                                     final @NonNull WebExtension.Port port) {
    333               // This method will be called every time a message is sent from the
    334               // extension through this port. For now, let's just log a
    335               // message.
    336               Log.d("PortDelegate", "Received message from WebExtension: "
    337                       + message);
    338           }
    339 
    340           public void onDisconnect(final @NonNull WebExtension.Port port) {
    341               // After this method is called, this port is not usable anymore.
    342               if (port == mPort) {
    343                   mPort = null;
    344               }
    345           }
    346       };
    347 
    348       // This delegate will handle requests to open a port coming from the
    349       // extension
    350       WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
    351           @Nullable
    352           public void onConnect(final @NonNull WebExtension.Port port) {
    353               // Let's store the Port object in a member variable so it can be
    354               // used later to exchange messages with the WebExtension.
    355               mPort = port;
    356 
    357               // Registering the delegate will allow us to receive messages sent
    358               // through this port.
    359               mPort.setDelegate(portDelegate);
    360           }
    361       };
    362 
    363       runtime.getWebExtensionController()
    364           .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com")
    365           .accept(
    366               // Register message delegate for background script
    367               extension -> extension.setMessageDelegate(messageDelegate, "browser"),
    368               e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
    369           );
    370 
    371       // ... other
    372   }
    373 
    374 For example, let’s send a message to the extension every time the user
    375 long presses on a key on the virtual keyboard, e.g. on the back button.
    376 
    377 .. code:: java
    378 
    379   @Override
    380   public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    381       if (mPort == null) {
    382           // No extension registered yet, let's ignore this message
    383           return false;
    384       }
    385 
    386       JSONObject message = new JSONObject();
    387       try {
    388           message.put("keyCode", keyCode);
    389           message.put("event", KeyEvent.keyCodeToString(event.getKeyCode()));
    390       } catch (JSONException ex) {
    391           throw new RuntimeException(ex);
    392       }
    393 
    394       mPort.postMessage(message);
    395       return true;
    396   }
    397 
    398 This allows bidirectional communication between the app and the
    399 extension.
    400 
    401 .. _GeckoRuntime: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html
    402 .. _runtime.sendNativeMessage: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage
    403 .. _WebExtension: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html