WebExtensionController.java (72867B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.geckoview; 6 7 import android.annotation.SuppressLint; 8 import android.util.Log; 9 import android.util.SparseArray; 10 import androidx.annotation.AnyThread; 11 import androidx.annotation.IntDef; 12 import androidx.annotation.NonNull; 13 import androidx.annotation.Nullable; 14 import androidx.annotation.StringDef; 15 import androidx.annotation.UiThread; 16 import java.lang.annotation.Retention; 17 import java.lang.annotation.RetentionPolicy; 18 import java.util.ArrayList; 19 import java.util.Arrays; 20 import java.util.HashMap; 21 import java.util.List; 22 import java.util.Map; 23 import java.util.Objects; 24 import java.util.UUID; 25 import org.json.JSONException; 26 import org.mozilla.gecko.EventDispatcher; 27 import org.mozilla.gecko.MultiMap; 28 import org.mozilla.gecko.util.BundleEventListener; 29 import org.mozilla.gecko.util.EventCallback; 30 import org.mozilla.gecko.util.GeckoBundle; 31 import org.mozilla.geckoview.WebExtension.InstallException; 32 import org.mozilla.geckoview.WebExtension.InvalidMetaDataException; 33 34 /** 35 * Controller for managing WebExtensions within a GeckoRuntime instance. Provides functionality for 36 * installing, uninstalling, enabling, disabling, and managing delegates for WebExtensions. 37 */ 38 public class WebExtensionController { 39 private static final String LOGTAG = "WebExtension"; 40 41 private AddonManagerDelegate mAddonManagerDelegate; 42 private ExtensionProcessDelegate mExtensionProcessDelegate; 43 private DebuggerDelegate mDebuggerDelegate; 44 private PromptDelegate mPromptDelegate; 45 private final WebExtension.Listener<WebExtension.TabDelegate> mListener; 46 47 // Map [ (extensionId, nativeApp, session) -> message ] 48 private final MultiMap<MessageRecipient, Message> mPendingMessages; 49 private final MultiMap<String, Message> mPendingNewTab; 50 private final MultiMap<String, Message> mPendingBrowsingData; 51 private final MultiMap<String, Message> mPendingDownload; 52 53 private final SparseArray<WebExtension.Download> mDownloads; 54 55 private static class Message { 56 final GeckoBundle bundle; 57 final EventCallback callback; 58 final String event; 59 final GeckoSession session; 60 61 public Message( 62 final String event, 63 final GeckoBundle bundle, 64 final EventCallback callback, 65 final GeckoSession session) { 66 this.bundle = bundle; 67 this.callback = callback; 68 this.event = event; 69 this.session = session; 70 } 71 } 72 73 private static class ExtensionStore { 74 private final Map<String, WebExtension> mData = new HashMap<>(); 75 private Observer mObserver; 76 77 interface Observer { 78 /** 79 * * This event is fired every time a new extension object is created by the store. 80 * 81 * @param extension the newly-created extension object 82 */ 83 WebExtension onNewExtension(final GeckoBundle extension); 84 } 85 86 public GeckoResult<WebExtension> get(final String id) { 87 final WebExtension extension = mData.get(id); 88 if (extension != null) { 89 return GeckoResult.fromValue(extension); 90 } 91 92 final GeckoBundle bundle = new GeckoBundle(1); 93 bundle.putString("extensionId", id); 94 95 return EventDispatcher.getInstance() 96 .queryBundle("GeckoView:WebExtension:Get", bundle) 97 .map( 98 extensionBundle -> { 99 final WebExtension ext = mObserver.onNewExtension(extensionBundle); 100 mData.put(ext.id, ext); 101 return ext; 102 }); 103 } 104 105 public void setObserver(final Observer observer) { 106 mObserver = observer; 107 } 108 109 public void remove(final String id) { 110 mData.remove(id); 111 } 112 113 /** 114 * Add this extension to the store and update it's current value if it's already present. 115 * 116 * @param id the {@link WebExtension} id. 117 * @param extension the {@link WebExtension} to add to the store. 118 */ 119 public void update(final String id, final WebExtension extension) { 120 mData.put(id, extension); 121 } 122 } 123 124 private ExtensionStore mExtensions = new ExtensionStore(); 125 126 private Internals mInternals = new Internals(); 127 128 // Avoids exposing listeners to the API 129 private class Internals implements BundleEventListener, ExtensionStore.Observer { 130 @Override 131 // BundleEventListener 132 public void handleMessage( 133 final String event, final GeckoBundle message, final EventCallback callback) { 134 WebExtensionController.this.handleMessage(event, message, callback, null); 135 } 136 137 @Override 138 public WebExtension onNewExtension(final GeckoBundle bundle) { 139 return WebExtension.fromBundle(mDelegateControllerProvider, bundle); 140 } 141 } 142 143 /* package */ void releasePendingMessages( 144 final WebExtension extension, final String nativeApp, final GeckoSession session) { 145 Log.i( 146 LOGTAG, 147 "releasePendingMessages:" 148 + " extension=" 149 + extension.id 150 + " nativeApp=" 151 + nativeApp 152 + " session=" 153 + session); 154 final List<Message> messages = 155 mPendingMessages.remove(new MessageRecipient(nativeApp, extension.id, session)); 156 if (messages == null) { 157 return; 158 } 159 160 for (final Message message : messages) { 161 WebExtensionController.this.handleMessage( 162 message.event, message.bundle, message.callback, message.session); 163 } 164 } 165 166 private class DelegateController implements WebExtension.DelegateController { 167 private final WebExtension mExtension; 168 169 public DelegateController(final WebExtension extension) { 170 mExtension = extension; 171 } 172 173 @Override 174 public void onMessageDelegate( 175 final String nativeApp, final WebExtension.MessageDelegate delegate) { 176 mListener.setMessageDelegate(mExtension, delegate, nativeApp); 177 } 178 179 @Override 180 public void onActionDelegate(final WebExtension.ActionDelegate delegate) { 181 mListener.setActionDelegate(mExtension, delegate); 182 } 183 184 @Override 185 public WebExtension.ActionDelegate getActionDelegate() { 186 return mListener.getActionDelegate(mExtension); 187 } 188 189 @Override 190 public void onBrowsingDataDelegate(final WebExtension.BrowsingDataDelegate delegate) { 191 mListener.setBrowsingDataDelegate(mExtension, delegate); 192 193 for (final Message message : mPendingBrowsingData.get(mExtension.id)) { 194 WebExtensionController.this.handleMessage( 195 message.event, message.bundle, message.callback, message.session); 196 } 197 198 mPendingBrowsingData.remove(mExtension.id); 199 } 200 201 @Override 202 public WebExtension.BrowsingDataDelegate getBrowsingDataDelegate() { 203 return mListener.getBrowsingDataDelegate(mExtension); 204 } 205 206 @Override 207 public void onTabDelegate(final WebExtension.TabDelegate delegate) { 208 mListener.setTabDelegate(mExtension, delegate); 209 210 for (final Message message : mPendingNewTab.get(mExtension.id)) { 211 WebExtensionController.this.handleMessage( 212 message.event, message.bundle, message.callback, message.session); 213 } 214 215 mPendingNewTab.remove(mExtension.id); 216 } 217 218 @Override 219 public WebExtension.TabDelegate getTabDelegate() { 220 return mListener.getTabDelegate(mExtension); 221 } 222 223 @Override 224 public void onDownloadDelegate(final WebExtension.DownloadDelegate delegate) { 225 mListener.setDownloadDelegate(mExtension, delegate); 226 227 for (final Message message : mPendingDownload.get(mExtension.id)) { 228 WebExtensionController.this.handleMessage( 229 message.event, message.bundle, message.callback, message.session); 230 } 231 232 mPendingDownload.remove(mExtension.id); 233 } 234 235 @Override 236 public WebExtension.DownloadDelegate getDownloadDelegate() { 237 return mListener.getDownloadDelegate(mExtension); 238 } 239 } 240 241 final WebExtension.DelegateControllerProvider mDelegateControllerProvider = 242 new WebExtension.DelegateControllerProvider() { 243 @Override 244 public WebExtension.DelegateController controllerFor(final WebExtension extension) { 245 return new DelegateController(extension); 246 } 247 }; 248 249 /** 250 * This delegate will be called whenever an extension is about to be installed or it needs new 251 * permissions, e.g during an update or because it called <code>permissions.request</code> 252 */ 253 @UiThread 254 public interface PromptDelegate { 255 256 /** 257 * Called whenever a new extension is being installed. This is intended as an opportunity for 258 * the app to prompt the user for the permissions required by this extension. 259 * 260 * @param extension The {@link WebExtension} that is about to be installed. You can use {@link 261 * WebExtension#metaData} to gather information about this extension when building the user 262 * prompt dialog. 263 * @param permissions The list of permissions that are granted during installation. 264 * @param origins The list of origins that are granted during installation. 265 * @param dataCollectionPermissions The list of data collection permissions that are requested 266 * during installation. 267 * @return A {@link GeckoResult} that completes with a {@link 268 * WebExtension.PermissionPromptResponse} containing all the details from the user response. 269 */ 270 @Nullable 271 default GeckoResult<WebExtension.PermissionPromptResponse> onInstallPromptRequest( 272 @NonNull final WebExtension extension, 273 @NonNull final String[] permissions, 274 @NonNull final String[] origins, 275 @NonNull final String[] dataCollectionPermissions) { 276 return null; 277 } 278 279 /** 280 * Called whenever an updated extension has new permissions. This is intended as an opportunity 281 * for the app to prompt the user for the new permissions required by this extension. 282 * 283 * @param extension The {@link WebExtension} being updated. 284 * @param newPermissions The new permissions that are needed. 285 * @param newOrigins The new origins that are needed. 286 * @param newDataCollectionPermissions The new data collection permissions that are needed. 287 * @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW} if 288 * this extension should be update or {@link AllowOrDeny#DENY DENY} if this extension should 289 * not be update. A null value will be interpreted as {@link AllowOrDeny#DENY DENY}. 290 */ 291 @Nullable 292 default GeckoResult<AllowOrDeny> onUpdatePrompt( 293 @NonNull final WebExtension extension, 294 @NonNull final String[] newPermissions, 295 @NonNull final String[] newOrigins, 296 @NonNull final String[] newDataCollectionPermissions) { 297 return null; 298 } 299 300 /** 301 * Called whenever permissions are requested. This is intended as an opportunity for the app to 302 * prompt the user for the permissions required by this extension at runtime. 303 * 304 * @param extension The {@link WebExtension} that is about to be installed. You can use {@link 305 * WebExtension#metaData} to gather information about this extension when building the user 306 * prompt dialog. 307 * @param permissions The permissions that are requested. 308 * @param origins The requested host permissions. 309 * @param dataCollectionPermissions The requested data collection permissions. 310 * @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW} if the 311 * request should be approved or {@link AllowOrDeny#DENY DENY} if the request should be 312 * denied. A null value will be interpreted as {@link AllowOrDeny#DENY DENY}. 313 */ 314 @Nullable 315 default GeckoResult<AllowOrDeny> onOptionalPrompt( 316 final @NonNull WebExtension extension, 317 final @NonNull String[] permissions, 318 final @NonNull String[] origins, 319 final @NonNull String[] dataCollectionPermissions) { 320 return null; 321 } 322 } 323 324 /** 325 * Delegate for receiving debugger-related WebExtension events. Used to notify about changes to 326 * extensions during development with tools like web-ext. 327 */ 328 public interface DebuggerDelegate { 329 /** 330 * Called whenever the list of installed extensions has been modified using the debugger with 331 * tools like web-ext. 332 * 333 * <p>This is intended as an opportunity to refresh the list of installed extensions using 334 * {@link WebExtensionController#list} and to set delegates on the new {@link WebExtension} 335 * objects, e.g. using {@link WebExtension#setActionDelegate} and {@link 336 * WebExtension#setMessageDelegate}. 337 * 338 * @see <a 339 * href="https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext"> 340 * Getting started with web-ext</a> 341 */ 342 @UiThread 343 default void onExtensionListUpdated() {} 344 } 345 346 /** This delegate will be called whenever the state of an extension has changed. */ 347 public interface AddonManagerDelegate { 348 /** 349 * Called whenever an extension is being disabled. 350 * 351 * @param extension The {@link WebExtension} that is being disabled. 352 */ 353 @UiThread 354 default void onDisabling(@NonNull WebExtension extension) {} 355 356 /** 357 * Called whenever optional permissions of an extension have changed. 358 * 359 * @param extension The {@link WebExtension} that has optional permissions changed. 360 */ 361 @UiThread 362 default void onOptionalPermissionsChanged(@NonNull WebExtension extension) {} 363 364 /** 365 * Called whenever an extension has been disabled. 366 * 367 * @param extension The {@link WebExtension} that is being disabled. 368 */ 369 @UiThread 370 default void onDisabled(final @NonNull WebExtension extension) {} 371 372 /** 373 * Called whenever an extension is being enabled. 374 * 375 * @param extension The {@link WebExtension} that is being enabled. 376 */ 377 @UiThread 378 default void onEnabling(final @NonNull WebExtension extension) {} 379 380 /** 381 * Called whenever an extension has been enabled. 382 * 383 * @param extension The {@link WebExtension} that is being enabled. 384 */ 385 @UiThread 386 default void onEnabled(final @NonNull WebExtension extension) {} 387 388 /** 389 * Called whenever an extension is being uninstalled. 390 * 391 * @param extension The {@link WebExtension} that is being uninstalled. 392 */ 393 @UiThread 394 default void onUninstalling(final @NonNull WebExtension extension) {} 395 396 /** 397 * Called whenever an extension has been uninstalled. 398 * 399 * @param extension The {@link WebExtension} that is being uninstalled. 400 */ 401 @UiThread 402 default void onUninstalled(final @NonNull WebExtension extension) {} 403 404 /** 405 * Called whenever an extension is being installed. 406 * 407 * @param extension The {@link WebExtension} that is being installed. 408 */ 409 @UiThread 410 default void onInstalling(final @NonNull WebExtension extension) {} 411 412 /** 413 * Called whenever an extension has been installed. 414 * 415 * @param extension The {@link WebExtension} that is being installed. 416 */ 417 @UiThread 418 default void onInstalled(final @NonNull WebExtension extension) {} 419 420 /** 421 * Called whenever an error happened when installing a WebExtension. 422 * 423 * @param extension {@link WebExtension} which failed to be installed. 424 * @param installException {@link InstallException} indicates which type of error happened. 425 */ 426 @UiThread 427 default void onInstallationFailed( 428 final @Nullable WebExtension extension, final @NonNull InstallException installException) {} 429 430 /** 431 * Called whenever an extension startup has been completed (and relative urls to assets packaged 432 * with the extension can be resolved into a full moz-extension url, e.g. optionsPageUrl is 433 * going to be empty until the extension has reached this callback). 434 * 435 * @param extension The {@link WebExtension} that has been fully started. 436 */ 437 @UiThread 438 default void onReady(final @NonNull WebExtension extension) {} 439 } 440 441 /** This delegate is used to notify of extension process state changes. */ 442 public interface ExtensionProcessDelegate { 443 /** Called when extension process spawning has been disabled. */ 444 @UiThread 445 default void onDisabledProcessSpawning() {} 446 } 447 448 /** 449 * @return the current {@link PromptDelegate} instance. 450 * @see PromptDelegate 451 */ 452 @UiThread 453 @Nullable 454 public PromptDelegate getPromptDelegate() { 455 return mPromptDelegate; 456 } 457 458 /** 459 * Set the {@link PromptDelegate} for this instance. This delegate will be used to be notified 460 * whenever an extension is being installed or needs new permissions. 461 * 462 * @param delegate the delegate instance. 463 * @see PromptDelegate 464 */ 465 @UiThread 466 public void setPromptDelegate(final @Nullable PromptDelegate delegate) { 467 if (delegate == null && mPromptDelegate != null) { 468 EventDispatcher.getInstance() 469 .unregisterUiThreadListener( 470 mInternals, 471 "GeckoView:WebExtension:InstallPrompt", 472 "GeckoView:WebExtension:UpdatePrompt", 473 "GeckoView:WebExtension:OptionalPrompt"); 474 } else if (delegate != null && mPromptDelegate == null) { 475 EventDispatcher.getInstance() 476 .registerUiThreadListener( 477 mInternals, 478 "GeckoView:WebExtension:InstallPrompt", 479 "GeckoView:WebExtension:UpdatePrompt", 480 "GeckoView:WebExtension:OptionalPrompt"); 481 } 482 483 mPromptDelegate = delegate; 484 } 485 486 /** 487 * Set the {@link DebuggerDelegate} for this instance. This delegate will receive updates about 488 * extension changes using developer tools. 489 * 490 * @param delegate the Delegate instance 491 */ 492 @UiThread 493 public void setDebuggerDelegate(final @NonNull DebuggerDelegate delegate) { 494 if (delegate == null && mDebuggerDelegate != null) { 495 EventDispatcher.getInstance() 496 .unregisterUiThreadListener(mInternals, "GeckoView:WebExtension:DebuggerListUpdated"); 497 } else if (delegate != null && mDebuggerDelegate == null) { 498 EventDispatcher.getInstance() 499 .registerUiThreadListener(mInternals, "GeckoView:WebExtension:DebuggerListUpdated"); 500 } 501 502 mDebuggerDelegate = delegate; 503 } 504 505 /** 506 * Set the {@link AddonManagerDelegate} for this instance. This delegate will be used to be 507 * notified whenever the state of an extension has changed. 508 * 509 * @param delegate the delegate instance 510 * @see AddonManagerDelegate 511 */ 512 @UiThread 513 public void setAddonManagerDelegate(final @Nullable AddonManagerDelegate delegate) { 514 if (delegate == null && mAddonManagerDelegate != null) { 515 EventDispatcher.getInstance() 516 .unregisterUiThreadListener( 517 mInternals, 518 "GeckoView:WebExtension:OnOptionalPermissionsChanged", 519 "GeckoView:WebExtension:OnDisabling", 520 "GeckoView:WebExtension:OnDisabled", 521 "GeckoView:WebExtension:OnEnabling", 522 "GeckoView:WebExtension:OnEnabled", 523 "GeckoView:WebExtension:OnUninstalling", 524 "GeckoView:WebExtension:OnUninstalled", 525 "GeckoView:WebExtension:OnInstalling", 526 "GeckoView:WebExtension:OnInstallationFailed", 527 "GeckoView:WebExtension:OnInstalled", 528 "GeckoView:WebExtension:OnReady"); 529 } else if (delegate != null && mAddonManagerDelegate == null) { 530 EventDispatcher.getInstance() 531 .registerUiThreadListener( 532 mInternals, 533 "GeckoView:WebExtension:OnOptionalPermissionsChanged", 534 "GeckoView:WebExtension:OnDisabling", 535 "GeckoView:WebExtension:OnDisabled", 536 "GeckoView:WebExtension:OnEnabling", 537 "GeckoView:WebExtension:OnEnabled", 538 "GeckoView:WebExtension:OnUninstalling", 539 "GeckoView:WebExtension:OnUninstalled", 540 "GeckoView:WebExtension:OnInstalling", 541 "GeckoView:WebExtension:OnInstallationFailed", 542 "GeckoView:WebExtension:OnInstalled", 543 "GeckoView:WebExtension:OnReady"); 544 } 545 546 mAddonManagerDelegate = delegate; 547 } 548 549 /** 550 * Set the {@link ExtensionProcessDelegate} for this instance. This delegate will be used to 551 * notify when the state of the extension process has changed. 552 * 553 * @param delegate the extension process delegate 554 * @see ExtensionProcessDelegate 555 */ 556 @UiThread 557 public void setExtensionProcessDelegate(final @Nullable ExtensionProcessDelegate delegate) { 558 if (delegate == null && mExtensionProcessDelegate != null) { 559 EventDispatcher.getInstance() 560 .unregisterUiThreadListener( 561 mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning"); 562 } else if (delegate != null && mExtensionProcessDelegate == null) { 563 EventDispatcher.getInstance() 564 .registerUiThreadListener(mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning"); 565 } 566 567 mExtensionProcessDelegate = delegate; 568 } 569 570 /** 571 * Enable extension process spawning. 572 * 573 * <p>Extension process spawning can be disabled when the extension process has been killed or 574 * crashed beyond the threshold set for Gecko. This method can be called to reset the threshold 575 * count and allow the spawning again. If the threshold is reached again, {@link 576 * ExtensionProcessDelegate#onDisabledProcessSpawning()} will still be called. 577 * 578 * @see ExtensionProcessDelegate#onDisabledProcessSpawning() 579 */ 580 @AnyThread 581 public void enableExtensionProcessSpawning() { 582 EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:EnableProcessSpawning", null); 583 } 584 585 /** 586 * Disable extension process spawning. 587 * 588 * <p>Extension process spawning can be re-enabled with {@link 589 * WebExtensionController#enableExtensionProcessSpawning()}. This method does the opposite and 590 * stops the extension process. This method can be called when we no longer want to run extensions 591 * for the rest of the session. 592 * 593 * @see ExtensionProcessDelegate#onDisabledProcessSpawning() 594 */ 595 @AnyThread 596 public void disableExtensionProcessSpawning() { 597 EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:DisableProcessSpawning", null); 598 } 599 600 private static class InstallCanceller implements GeckoResult.CancellationDelegate { 601 public final String installId; 602 603 public InstallCanceller() { 604 installId = UUID.randomUUID().toString(); 605 } 606 607 @Override 608 public GeckoResult<Boolean> cancel() { 609 final GeckoBundle bundle = new GeckoBundle(1); 610 bundle.putString("installId", installId); 611 612 return EventDispatcher.getInstance() 613 .queryBundle("GeckoView:WebExtension:CancelInstall", bundle) 614 .map(response -> response.getBoolean("cancelled")); 615 } 616 } 617 618 /** 619 * Install an extension. 620 * 621 * <p>An installed extension will persist and will be available even when restarting the {@link 622 * GeckoRuntime}. 623 * 624 * <p>Installed extensions through this method need to be signed by Mozilla, see <a 625 * href="https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/#distributing-your-addon"> 626 * Distributing your add-on </a>. 627 * 628 * <p>When calling this method, the GeckoView library will download the extension, validate its 629 * manifest and signature, and give you an opportunity to verify its permissions through {@link 630 * PromptDelegate#onInstallPromptRequest}, you can use this method to prompt the user if 631 * appropriate. 632 * 633 * @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https: 634 * </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app 635 * needs the appropriate permissions for local URIs. 636 * @param installationMethod The method used by the embedder to install the {@link WebExtension}. 637 * @return A {@link GeckoResult} that will complete when the installation process finishes. For 638 * successful installations, the GeckoResult will return the {@link WebExtension} object that 639 * you can use to set delegates and retrieve information about the WebExtension using {@link 640 * WebExtension#metaData}. 641 * <p>If an error occurs during the installation process, the GeckoResult will complete 642 * exceptionally with a {@link WebExtension.InstallException InstallException} that will 643 * contain the relevant error code in {@link WebExtension.InstallException#code 644 * InstallException#code}. 645 * @see PromptDelegate#onInstallPromptRequest(WebExtension, String[], String[], String[]) 646 * @see WebExtension.InstallException.ErrorCodes 647 * @see WebExtension#metaData 648 */ 649 @NonNull 650 @AnyThread 651 public GeckoResult<WebExtension> install( 652 final @NonNull String uri, final @Nullable @InstallationMethod String installationMethod) { 653 final InstallCanceller canceller = new InstallCanceller(); 654 final GeckoBundle bundle = new GeckoBundle(3); 655 bundle.putString("locationUri", uri); 656 bundle.putString("installId", canceller.installId); 657 bundle.putString("installMethod", installationMethod); 658 659 final GeckoResult<WebExtension> result = 660 EventDispatcher.getInstance() 661 .queryBundle("GeckoView:WebExtension:Install", bundle) 662 .map( 663 ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext), 664 WebExtension.InstallException::fromQueryException) 665 .map(this::registerWebExtension); 666 result.setCancellationDelegate(canceller); 667 return result; 668 } 669 670 /** 671 * Install an extension. 672 * 673 * <p>An installed extension will persist and will be available even when restarting the {@link 674 * GeckoRuntime}. 675 * 676 * <p>Installed extensions through this method need to be signed by Mozilla, see <a 677 * href="https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/#distributing-your-addon"> 678 * Distributing your add-on </a>. 679 * 680 * <p>When calling this method, the GeckoView library will download the extension, validate its 681 * manifest and signature, and give you an opportunity to verify its permissions through {@link 682 * PromptDelegate#installPromptRequest(GeckoBundle, EventCallback)}, you can use this method to 683 * prompt the user if appropriate. If you are looking to provide an {@link InstallationMethod}, 684 * please use {@link WebExtensionController#install(String, String)} 685 * 686 * @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https: 687 * </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app 688 * needs the appropriate permissions for local URIs. 689 * @return A {@link GeckoResult} that will complete when the installation process finishes. For 690 * successful installations, the GeckoResult will return the {@link WebExtension} object that 691 * you can use to set delegates and retrieve information about the WebExtension using {@link 692 * WebExtension#metaData}. 693 * <p>If an error occurs during the installation process, the GeckoResult will complete 694 * exceptionally with a {@link WebExtension.InstallException InstallException} that will 695 * contain the relevant error code in {@link WebExtension.InstallException#code 696 * InstallException#code}. 697 * @see PromptDelegate#installPromptRequest 698 * @see WebExtension.InstallException.ErrorCodes 699 * @see WebExtension#metaData 700 */ 701 @NonNull 702 @AnyThread 703 public GeckoResult<WebExtension> install(final @NonNull String uri) { 704 return install(uri, null); 705 } 706 707 /** The method used by the embedder to install the {@link WebExtension}. */ 708 @Retention(RetentionPolicy.SOURCE) 709 @StringDef({ 710 INSTALLATION_METHOD_MANAGER, 711 INSTALLATION_METHOD_FROM_FILE, 712 INSTALLATION_METHOD_ONBOARDING 713 }) 714 public @interface InstallationMethod {}; 715 716 /** Indicates the {@link WebExtension} was installed using from the embedder's add-ons manager. */ 717 public static final String INSTALLATION_METHOD_MANAGER = "manager"; 718 719 /** Indicates the {@link WebExtension} was installed from a file. */ 720 public static final String INSTALLATION_METHOD_FROM_FILE = "install-from-file"; 721 722 /** Indicates the {@link WebExtension} was installed from the embedder's onboarding feature. */ 723 public static final String INSTALLATION_METHOD_ONBOARDING = "onboarding"; 724 725 /** 726 * Set whether an extension should be allowed to run in private browsing or not. 727 * 728 * @param extension the {@link WebExtension} instance to modify. 729 * @param allowed true if this extension should be allowed to run in private browsing pages, false 730 * otherwise. 731 * @return the updated {@link WebExtension} instance. 732 */ 733 @NonNull 734 @AnyThread 735 public GeckoResult<WebExtension> setAllowedInPrivateBrowsing( 736 final @NonNull WebExtension extension, final boolean allowed) { 737 final GeckoBundle bundle = new GeckoBundle(2); 738 bundle.putString("extensionId", extension.id); 739 bundle.putBoolean("allowed", allowed); 740 741 return EventDispatcher.getInstance() 742 .queryBundle("GeckoView:WebExtension:SetPBAllowed", bundle) 743 .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext)) 744 .map(this::registerWebExtension); 745 } 746 747 /** 748 * Add the provided permissions to the {@link WebExtension} with the given id. 749 * 750 * @param extensionId the id of {@link WebExtension} instance to modify. 751 * @param permissions the permissions to add, pass an empty array to not update. 752 * @param origins the origins to add, pass an empty array to not update. 753 * @param dataCollectionPermissions the data collection permissions to add, pass an empty array to 754 * not update. 755 * @return the updated {@link WebExtension} instance. 756 */ 757 @NonNull 758 @AnyThread 759 public GeckoResult<WebExtension> addOptionalPermissions( 760 final @NonNull String extensionId, 761 @NonNull final String[] permissions, 762 @NonNull final String[] origins, 763 @NonNull final String[] dataCollectionPermissions) { 764 final GeckoBundle bundle = new GeckoBundle(4); 765 bundle.putString("extensionId", extensionId); 766 bundle.putStringArray("permissions", permissions); 767 bundle.putStringArray("origins", origins); 768 bundle.putStringArray("dataCollectionPermissions", dataCollectionPermissions); 769 770 return EventDispatcher.getInstance() 771 .queryBundle("GeckoView:WebExtension:AddOptionalPermissions", bundle) 772 .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext)) 773 .map(this::registerWebExtension); 774 } 775 776 /** 777 * Remove the provided permissions from the {@link WebExtension} with the given id. 778 * 779 * @param extensionId the id of {@link WebExtension} instance to modify. 780 * @param permissions the permissions to remove, pass an empty array to not update. 781 * @param origins the origins to remove, pass an empty array to not update. 782 * @param dataCollectionPermissions the data collection permissions to remove, pass an array to 783 * not update. 784 * @return the updated {@link WebExtension} instance. 785 */ 786 @NonNull 787 @AnyThread 788 public GeckoResult<WebExtension> removeOptionalPermissions( 789 final @NonNull String extensionId, 790 @NonNull final String[] permissions, 791 @NonNull final String[] origins, 792 @NonNull final String[] dataCollectionPermissions) { 793 final GeckoBundle bundle = new GeckoBundle(4); 794 bundle.putString("extensionId", extensionId); 795 bundle.putStringArray("permissions", permissions); 796 bundle.putStringArray("origins", origins); 797 bundle.putStringArray("dataCollectionPermissions", dataCollectionPermissions); 798 799 return EventDispatcher.getInstance() 800 .queryBundle("GeckoView:WebExtension:RemoveOptionalPermissions", bundle) 801 .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext)) 802 .map(this::registerWebExtension); 803 } 804 805 /** 806 * Install a built-in extension. 807 * 808 * <p>Built-in extensions have access to native messaging, don't need to be signed and are 809 * installed from a folder in the APK instead of a .xpi bundle. 810 * 811 * <p>Example: 812 * 813 * <p><code> 814 * controller.installBuiltIn("resource://android/assets/example/"); 815 * </code> Will install the built-in extension located at <code>/assets/example/</code> in the 816 * app's APK. 817 * 818 * @param uri Folder where the extension is located. To ensure this folder is inside the APK, only 819 * <code>resource://android</code> URIs are allowed. 820 * @see WebExtension.MessageDelegate 821 * @return A {@link GeckoResult} that completes with the extension once it's installed. 822 */ 823 @NonNull 824 @AnyThread 825 public GeckoResult<WebExtension> installBuiltIn(final @NonNull String uri) { 826 final GeckoBundle bundle = new GeckoBundle(1); 827 bundle.putString("locationUri", uri); 828 829 return EventDispatcher.getInstance() 830 .queryBundle("GeckoView:WebExtension:InstallBuiltIn", bundle) 831 .map( 832 ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext), 833 WebExtension.InstallException::fromQueryException) 834 .map(this::registerWebExtension); 835 } 836 837 /** 838 * Ensure that a built-in extension is installed. 839 * 840 * <p>Similar to {@link #installBuiltIn}, except the extension is not re-installed if it's already 841 * present and it has the same version. 842 * 843 * <p>Example: 844 * 845 * <p><code> 846 * controller.ensureBuiltIn("resource://android/assets/example/", "example@example.com"); 847 * </code> Will install the built-in extension located at <code>/assets/example/</code> in the 848 * app's APK. 849 * 850 * @param uri Folder where the extension is located. To ensure this folder is inside the APK, only 851 * <code>resource://android</code> URIs are allowed. 852 * @param id Extension ID as present in the manifest.json file. 853 * @see WebExtension.MessageDelegate 854 * @return A {@link GeckoResult} that completes with the extension once it's installed. 855 */ 856 @NonNull 857 @AnyThread 858 public GeckoResult<WebExtension> ensureBuiltIn( 859 final @NonNull String uri, final @Nullable String id) { 860 final GeckoBundle bundle = new GeckoBundle(2); 861 bundle.putString("locationUri", uri); 862 bundle.putString("webExtensionId", id); 863 864 return EventDispatcher.getInstance() 865 .queryBundle("GeckoView:WebExtension:EnsureBuiltIn", bundle) 866 .map( 867 ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext), 868 WebExtension.InstallException::fromQueryException) 869 .map(this::registerWebExtension); 870 } 871 872 /** 873 * Uninstall an extension. 874 * 875 * <p>Uninstalling an extension will remove it from the current {@link GeckoRuntime} instance, 876 * delete all its data and trigger a request to close all extension pages currently open. 877 * 878 * @param extension The {@link WebExtension} to be uninstalled. 879 * @return A {@link GeckoResult} that will complete when the uninstall process is completed. 880 */ 881 @NonNull 882 @AnyThread 883 public GeckoResult<Void> uninstall(final @NonNull WebExtension extension) { 884 final GeckoBundle bundle = new GeckoBundle(1); 885 bundle.putString("webExtensionId", extension.id); 886 887 return EventDispatcher.getInstance() 888 .queryBundle("GeckoView:WebExtension:Uninstall", bundle) 889 .accept(result -> unregisterWebExtension(extension)); 890 } 891 892 /** Defines the possible sources that can enable or disable a WebExtension. */ 893 @Retention(RetentionPolicy.SOURCE) 894 @IntDef({EnableSource.USER, EnableSource.APP}) 895 public @interface EnableSources {} 896 897 /** 898 * Contains the possible values for the <code>source</code> parameter in {@link #enable} and 899 * {@link #disable}. 900 */ 901 public static class EnableSource { 902 /** Action has been requested by the user. */ 903 public static final int USER = 1; 904 905 /** 906 * Action requested by the app itself, e.g. to disable an extension that is not supported in 907 * this version of the app. 908 */ 909 public static final int APP = 2; 910 911 static String toString(final @EnableSources int flag) { 912 if (flag == USER) { 913 return "user"; 914 } else if (flag == APP) { 915 return "app"; 916 } else { 917 throw new IllegalArgumentException("Value provided in flags is not valid."); 918 } 919 } 920 } 921 922 /** 923 * Enable an extension that has been disabled. If the extension is already enabled, this method 924 * has no effect. 925 * 926 * @param extension The {@link WebExtension} to be enabled. 927 * @param source The agent that initiated this action, e.g. if the action has been initiated by 928 * the user,use {@link EnableSource#USER}. 929 * @return the new {@link WebExtension} instance, updated to reflect the enablement. 930 */ 931 @AnyThread 932 @NonNull 933 public GeckoResult<WebExtension> enable( 934 final @NonNull WebExtension extension, final @EnableSources int source) { 935 final GeckoBundle bundle = new GeckoBundle(2); 936 bundle.putString("webExtensionId", extension.id); 937 bundle.putString("source", EnableSource.toString(source)); 938 939 return EventDispatcher.getInstance() 940 .queryBundle("GeckoView:WebExtension:Enable", bundle) 941 .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext)) 942 .map(this::registerWebExtension); 943 } 944 945 /** 946 * Disable an extension that is enabled. If the extension is already disabled, this method has no 947 * effect. 948 * 949 * @param extension The {@link WebExtension} to be disabled. 950 * @param source The agent that initiated this action, e.g. if the action has been initiated by 951 * the user, use {@link EnableSource#USER}. 952 * @return the new {@link WebExtension} instance, updated to reflect the disablement. 953 */ 954 @AnyThread 955 @NonNull 956 public GeckoResult<WebExtension> disable( 957 final @NonNull WebExtension extension, final @EnableSources int source) { 958 final GeckoBundle bundle = new GeckoBundle(2); 959 bundle.putString("webExtensionId", extension.id); 960 bundle.putString("source", EnableSource.toString(source)); 961 962 return EventDispatcher.getInstance() 963 .queryBundle("GeckoView:WebExtension:Disable", bundle) 964 .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext)) 965 .map(this::registerWebExtension); 966 } 967 968 private List<WebExtension> listFromBundle(final GeckoBundle response) { 969 final GeckoBundle[] bundles = response.getBundleArray("extensions"); 970 final List<WebExtension> list = new ArrayList<>(bundles.length); 971 972 for (final GeckoBundle bundle : bundles) { 973 final WebExtension extension = new WebExtension(mDelegateControllerProvider, bundle); 974 list.add(registerWebExtension(extension)); 975 } 976 977 return list; 978 } 979 980 /** 981 * List installed extensions for this {@link GeckoRuntime}. 982 * 983 * <p>The returned list can be used to set delegates on the {@link WebExtension} objects using 984 * {@link WebExtension#setActionDelegate}, {@link WebExtension#setMessageDelegate}. 985 * 986 * @return a {@link GeckoResult} that will resolve when the list of extensions is available. 987 */ 988 @AnyThread 989 @NonNull 990 public GeckoResult<List<WebExtension>> list() { 991 return EventDispatcher.getInstance() 992 .queryBundle("GeckoView:WebExtension:List") 993 .map(this::listFromBundle); 994 } 995 996 /** 997 * Update a web extension. 998 * 999 * <p>When checking for an update, GeckoView will download the update manifest that is defined by 1000 * the web extension's manifest property <a 1001 * href="https://extensionworkshop.com/documentation/manage/updating-your-extension/">browser_specific_settings.gecko.update_url</a>. 1002 * If an update is found it will be downloaded and installed. If the extension needs any new 1003 * permissions the {@link PromptDelegate#updatePrompt} will be triggered. 1004 * 1005 * <p>More information about the update manifest format is available <a 1006 * href="https://extensionworkshop.com/documentation/manage/updating-your-extension/#manifest-structure">here</a>. 1007 * 1008 * @param extension The extension to update. 1009 * @return A {@link GeckoResult} that will complete when the update process finishes. If an update 1010 * is found and installed successfully, the GeckoResult will return the updated {@link 1011 * WebExtension}. If no update is available, null will be returned. If the updated extension 1012 * requires new permissions, the {@link PromptDelegate#installPromptRequest} will be called. 1013 * @see PromptDelegate#updatePrompt 1014 */ 1015 @AnyThread 1016 @NonNull 1017 public GeckoResult<WebExtension> update(final @NonNull WebExtension extension) { 1018 final GeckoBundle bundle = new GeckoBundle(1); 1019 bundle.putString("webExtensionId", extension.id); 1020 1021 return EventDispatcher.getInstance() 1022 .queryBundle("GeckoView:WebExtension:Update", bundle) 1023 .map( 1024 ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext), 1025 WebExtension.InstallException::fromQueryException) 1026 .map(this::registerWebExtension); 1027 } 1028 1029 /* package */ WebExtensionController(final GeckoRuntime runtime) { 1030 mListener = new WebExtension.Listener<>(runtime); 1031 mPendingMessages = new MultiMap<>(); 1032 mPendingNewTab = new MultiMap<>(); 1033 mPendingBrowsingData = new MultiMap<>(); 1034 mPendingDownload = new MultiMap<>(); 1035 mExtensions.setObserver(mInternals); 1036 mDownloads = new SparseArray<>(); 1037 } 1038 1039 /* package */ WebExtension registerWebExtension(final WebExtension webExtension) { 1040 if (webExtension != null) { 1041 mExtensions.update(webExtension.id, webExtension); 1042 } 1043 return webExtension; 1044 } 1045 1046 /* package */ void handleMessage( 1047 final String event, 1048 final GeckoBundle bundle, 1049 final EventCallback callback, 1050 final GeckoSession session) { 1051 final Message message = new Message(event, bundle, callback, session); 1052 1053 Log.d(LOGTAG, "handleMessage " + event); 1054 1055 try { 1056 if ("GeckoView:WebExtension:InstallPrompt".equals(event)) { 1057 installPromptRequest(bundle, callback); 1058 return; 1059 } else if ("GeckoView:WebExtension:UpdatePrompt".equals(event)) { 1060 updatePrompt(bundle, callback); 1061 return; 1062 } else if ("GeckoView:WebExtension:DebuggerListUpdated".equals(event)) { 1063 if (mDebuggerDelegate != null) { 1064 mDebuggerDelegate.onExtensionListUpdated(); 1065 } 1066 return; 1067 } else if ("GeckoView:WebExtension:OnDisabling".equals(event)) { 1068 onDisabling(bundle); 1069 return; 1070 } else if ("GeckoView:WebExtension:OnOptionalPermissionsChanged".equals(event)) { 1071 onOptionalPermissionsChanged(bundle); 1072 return; 1073 } else if ("GeckoView:WebExtension:OnDisabled".equals(event)) { 1074 onDisabled(bundle); 1075 return; 1076 } else if ("GeckoView:WebExtension:OnEnabling".equals(event)) { 1077 onEnabling(bundle); 1078 return; 1079 } else if ("GeckoView:WebExtension:OnEnabled".equals(event)) { 1080 onEnabled(bundle); 1081 return; 1082 } else if ("GeckoView:WebExtension:OnUninstalling".equals(event)) { 1083 onUninstalling(bundle); 1084 return; 1085 } else if ("GeckoView:WebExtension:OnUninstalled".equals(event)) { 1086 onUninstalled(bundle); 1087 return; 1088 } else if ("GeckoView:WebExtension:OnInstalling".equals(event)) { 1089 onInstalling(bundle); 1090 return; 1091 } else if ("GeckoView:WebExtension:OnInstalled".equals(event)) { 1092 onInstalled(bundle); 1093 return; 1094 } else if ("GeckoView:WebExtension:OnDisabledProcessSpawning".equals(event)) { 1095 onDisabledProcessSpawning(); 1096 return; 1097 } else if ("GeckoView:WebExtension:OnInstallationFailed".equals(event)) { 1098 onInstallationFailed(bundle); 1099 return; 1100 } else if ("GeckoView:WebExtension:OnReady".equals(event)) { 1101 onReady(bundle); 1102 return; 1103 } 1104 } catch (final InvalidMetaDataException e) { 1105 Log.e(LOGTAG, "Unexpected invalid bundle data on event " + event, e); 1106 if (callback != null) { 1107 callback.sendError("Unexpected invalid WebExtensions metaData on event " + event); 1108 } 1109 return; 1110 } 1111 1112 extensionFromBundle(bundle) 1113 .accept( 1114 extension -> { 1115 if ("GeckoView:WebExtension:NewTab".equals(event)) { 1116 newTab(message, extension); 1117 return; 1118 } else if ("GeckoView:WebExtension:UpdateTab".equals(event)) { 1119 updateTab(message, extension); 1120 return; 1121 } else if ("GeckoView:WebExtension:CloseTab".equals(event)) { 1122 closeTab(message, extension); 1123 return; 1124 } else if ("GeckoView:BrowserAction:Update".equals(event)) { 1125 actionUpdate(message, extension, WebExtension.Action.TYPE_BROWSER_ACTION); 1126 return; 1127 } else if ("GeckoView:PageAction:Update".equals(event)) { 1128 actionUpdate(message, extension, WebExtension.Action.TYPE_PAGE_ACTION); 1129 return; 1130 } else if ("GeckoView:BrowserAction:OpenPopup".equals(event)) { 1131 openPopup(message, extension, WebExtension.Action.TYPE_BROWSER_ACTION); 1132 return; 1133 } else if ("GeckoView:PageAction:OpenPopup".equals(event)) { 1134 openPopup(message, extension, WebExtension.Action.TYPE_PAGE_ACTION); 1135 return; 1136 } else if ("GeckoView:WebExtension:OpenOptionsPage".equals(event)) { 1137 openOptionsPage(message, extension); 1138 return; 1139 } else if ("GeckoView:BrowsingData:GetSettings".equals(event)) { 1140 getSettings(message, extension); 1141 return; 1142 } else if ("GeckoView:BrowsingData:Clear".equals(event)) { 1143 browsingDataClear(message, extension); 1144 return; 1145 } else if ("GeckoView:WebExtension:Download".equals(event)) { 1146 download(message, extension); 1147 return; 1148 } else if ("GeckoView:WebExtension:OptionalPrompt".equals(event)) { 1149 optionalPrompt(message, extension); 1150 return; 1151 } 1152 1153 // GeckoView:WebExtension:Connect and GeckoView:WebExtension:Message 1154 // are handled below. 1155 final String nativeApp = bundle.getString("nativeApp"); 1156 if (nativeApp == null) { 1157 if (BuildConfig.DEBUG_BUILD) { 1158 throw new RuntimeException("Missing required nativeApp message parameter."); 1159 } 1160 callback.sendError("Missing nativeApp parameter."); 1161 return; 1162 } 1163 1164 final GeckoBundle senderBundle = bundle.getBundle("sender"); 1165 final WebExtension.MessageSender sender = 1166 fromBundle(extension, senderBundle, session); 1167 if (sender == null) { 1168 if (callback != null) { 1169 if (BuildConfig.DEBUG_BUILD) { 1170 try { 1171 Log.e( 1172 LOGTAG, "Could not find recipient for message: " + bundle.toJSONObject()); 1173 } catch (final JSONException ex) { 1174 } 1175 } 1176 callback.sendError("Could not find recipient for " + bundle.getBundle("sender")); 1177 } 1178 return; 1179 } 1180 1181 if ("GeckoView:WebExtension:Connect".equals(event)) { 1182 connect(nativeApp, bundle.getLong("portId", -1), message, sender); 1183 } else if ("GeckoView:WebExtension:Message".equals(event)) { 1184 message(nativeApp, message, sender); 1185 } 1186 }); 1187 } 1188 1189 private boolean isBundledExtension(final String extensionId) { 1190 return "{73a6fe31-595d-460b-a920-fcc0f8843232}".equals(extensionId); 1191 } 1192 1193 private boolean promptBypass(final WebExtension extension, final EventCallback callback) { 1194 // allow bundled extensions, e.g. NoScript, to be installed with no prompt 1195 if (isBundledExtension(extension.id)) { 1196 callback.resolveTo( 1197 GeckoResult.allow().map( 1198 allowOrDeny -> { 1199 final GeckoBundle response = new GeckoBundle(1); 1200 response.putBoolean("allow", true); 1201 return response; 1202 } 1203 ) 1204 ); 1205 return true; 1206 } 1207 return false; 1208 } 1209 1210 private void installPromptRequest(final GeckoBundle message, final EventCallback callback) { 1211 final GeckoBundle extensionBundle = message.getBundle("extension"); 1212 if (extensionBundle == null 1213 || !extensionBundle.containsKey("webExtensionId") 1214 || !extensionBundle.containsKey("locationURI")) { 1215 if (BuildConfig.DEBUG_BUILD) { 1216 throw new RuntimeException("Missing webExtensionId or locationURI"); 1217 } 1218 1219 Log.e(LOGTAG, "Missing webExtensionId or locationURI"); 1220 return; 1221 } 1222 1223 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1224 1225 if (promptBypass(extension, callback)) { 1226 return; 1227 } 1228 1229 if (mPromptDelegate == null) { 1230 Log.e( 1231 LOGTAG, "Tried to install extension " + extension.id + " but no delegate is registered"); 1232 return; 1233 } 1234 1235 final GeckoResult<WebExtension.PermissionPromptResponse> promptResponse = 1236 mPromptDelegate.onInstallPromptRequest( 1237 extension, 1238 message.getStringArray("permissions"), 1239 message.getStringArray("origins"), 1240 message.getStringArray("dataCollectionPermissions")); 1241 1242 if (promptResponse == null) { 1243 return; 1244 } 1245 1246 callback.resolveTo( 1247 promptResponse.map( 1248 userResponse -> { 1249 final GeckoBundle response = new GeckoBundle(3); 1250 response.putBoolean("allow", userResponse.isPermissionsGranted); 1251 response.putBoolean("privateBrowsingAllowed", userResponse.isPrivateModeGranted); 1252 response.putBoolean( 1253 "isTechnicalAndInteractionDataGranted", 1254 userResponse.isTechnicalAndInteractionDataGranted); 1255 return response; 1256 })); 1257 } 1258 1259 private void updatePrompt(final GeckoBundle message, final EventCallback callback) { 1260 final WebExtension extension = 1261 new WebExtension(mDelegateControllerProvider, message.getBundle("extension")); 1262 final String[] newPermissions = message.getStringArray("newPermissions"); 1263 final String[] newOrigins = message.getStringArray("newOrigins"); 1264 final String[] newDataCollectionPermissions = 1265 message.getStringArray("newDataCollectionPermissions"); 1266 1267 if (promptBypass(extension, callback)) { 1268 return; 1269 } 1270 1271 if (mPromptDelegate == null) { 1272 Log.e(LOGTAG, "Tried to update extension " + extension.id + " but no delegate is registered"); 1273 return; 1274 } 1275 1276 final GeckoResult<AllowOrDeny> promptResponse = 1277 mPromptDelegate.onUpdatePrompt( 1278 extension, newPermissions, newOrigins, newDataCollectionPermissions); 1279 1280 if (promptResponse == null) { 1281 return; 1282 } 1283 1284 callback.resolveTo( 1285 promptResponse.map( 1286 allowOrDeny -> { 1287 final GeckoBundle response = new GeckoBundle(1); 1288 response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny)); 1289 return response; 1290 })); 1291 } 1292 1293 private void optionalPrompt(final Message message, final WebExtension extension) { 1294 if (mPromptDelegate == null) { 1295 Log.e( 1296 LOGTAG, 1297 "Tried to request optional permissions for extension " 1298 + extension.id 1299 + " but no delegate is registered"); 1300 return; 1301 } 1302 1303 final String[] permissions = 1304 message.bundle.getBundle("permissions").getStringArray("permissions"); 1305 final String[] origins = message.bundle.getBundle("permissions").getStringArray("origins"); 1306 final String[] dataCollectionPermissions = 1307 message.bundle.getBundle("permissions").getStringArray("data_collection"); 1308 1309 final GeckoResult<AllowOrDeny> promptResponse = 1310 mPromptDelegate.onOptionalPrompt( 1311 extension, permissions, origins, dataCollectionPermissions); 1312 1313 if (promptResponse == null) { 1314 return; 1315 } 1316 1317 message.callback.resolveTo( 1318 promptResponse.map( 1319 allowOrDeny -> { 1320 final GeckoBundle response = new GeckoBundle(1); 1321 response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny)); 1322 return response; 1323 })); 1324 } 1325 1326 private void onInstallationFailed(final GeckoBundle bundle) { 1327 if (mAddonManagerDelegate == null) { 1328 Log.e(LOGTAG, "no AddonManager delegate registered"); 1329 return; 1330 } 1331 1332 final int errorCode = bundle.getInt("error"); 1333 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1334 WebExtension extension = null; 1335 final String extensionId = bundle.getString("addonId"); 1336 final String extensionName = bundle.getString("addonName"); 1337 final String extensionVersion = bundle.getString("addonVersion"); 1338 1339 if (extensionBundle != null) { 1340 extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1341 } 1342 mAddonManagerDelegate.onInstallationFailed( 1343 extension, new InstallException(errorCode, extensionId, extensionName, extensionVersion)); 1344 } 1345 1346 private void onDisabling(final GeckoBundle bundle) { 1347 if (mAddonManagerDelegate == null) { 1348 Log.e(LOGTAG, "no AddonManager delegate registered"); 1349 return; 1350 } 1351 1352 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1353 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1354 mAddonManagerDelegate.onDisabling(extension); 1355 } 1356 1357 private void onOptionalPermissionsChanged(final GeckoBundle bundle) { 1358 if (mAddonManagerDelegate == null) { 1359 Log.e(LOGTAG, "no AddonManager delegate registered"); 1360 return; 1361 } 1362 1363 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1364 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1365 mAddonManagerDelegate.onOptionalPermissionsChanged(extension); 1366 } 1367 1368 private void onDisabled(final GeckoBundle bundle) { 1369 if (mAddonManagerDelegate == null) { 1370 Log.e(LOGTAG, "no AddonManager delegate registered"); 1371 return; 1372 } 1373 1374 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1375 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1376 mAddonManagerDelegate.onDisabled(extension); 1377 } 1378 1379 private void onEnabling(final GeckoBundle bundle) { 1380 if (mAddonManagerDelegate == null) { 1381 Log.e(LOGTAG, "no AddonManager delegate registered"); 1382 return; 1383 } 1384 1385 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1386 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1387 mAddonManagerDelegate.onEnabling(extension); 1388 } 1389 1390 private void onEnabled(final GeckoBundle bundle) { 1391 if (mAddonManagerDelegate == null) { 1392 Log.e(LOGTAG, "no AddonManager delegate registered"); 1393 return; 1394 } 1395 1396 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1397 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1398 mAddonManagerDelegate.onEnabled(extension); 1399 } 1400 1401 private void onUninstalling(final GeckoBundle bundle) { 1402 if (mAddonManagerDelegate == null) { 1403 Log.e(LOGTAG, "no AddonManager delegate registered"); 1404 return; 1405 } 1406 1407 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1408 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1409 mAddonManagerDelegate.onUninstalling(extension); 1410 } 1411 1412 private void onUninstalled(final GeckoBundle bundle) { 1413 if (mAddonManagerDelegate == null) { 1414 Log.e(LOGTAG, "no AddonManager delegate registered"); 1415 return; 1416 } 1417 1418 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1419 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1420 mAddonManagerDelegate.onUninstalled(extension); 1421 } 1422 1423 private void onInstalling(final GeckoBundle bundle) { 1424 if (mAddonManagerDelegate == null) { 1425 Log.e(LOGTAG, "no AddonManager delegate registered"); 1426 return; 1427 } 1428 1429 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1430 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1431 mAddonManagerDelegate.onInstalling(extension); 1432 } 1433 1434 private void onInstalled(final GeckoBundle bundle) { 1435 if (mAddonManagerDelegate == null) { 1436 Log.e(LOGTAG, "no AddonManager delegate registered"); 1437 return; 1438 } 1439 1440 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1441 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1442 mAddonManagerDelegate.onInstalled(extension); 1443 } 1444 1445 private void onReady(final GeckoBundle bundle) { 1446 if (mAddonManagerDelegate == null) { 1447 Log.e(LOGTAG, "no AddonManager delegate registered"); 1448 return; 1449 } 1450 1451 final GeckoBundle extensionBundle = bundle.getBundle("extension"); 1452 final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); 1453 mAddonManagerDelegate.onReady(extension); 1454 } 1455 1456 private void onDisabledProcessSpawning() { 1457 if (mExtensionProcessDelegate == null) { 1458 Log.e(LOGTAG, "no extension process delegate registered"); 1459 return; 1460 } 1461 1462 mExtensionProcessDelegate.onDisabledProcessSpawning(); 1463 } 1464 1465 @SuppressLint("WrongThread") // for .toGeckoBundle 1466 private void getSettings(final Message message, final WebExtension extension) { 1467 final WebExtension.BrowsingDataDelegate delegate = mListener.getBrowsingDataDelegate(extension); 1468 if (delegate == null) { 1469 mPendingBrowsingData.add(extension.id, message); 1470 return; 1471 } 1472 1473 final GeckoResult<WebExtension.BrowsingDataDelegate.Settings> settingsResult = 1474 delegate.onGetSettings(); 1475 if (settingsResult == null) { 1476 message.callback.sendError("browsingData.settings is not supported"); 1477 return; 1478 } 1479 message.callback.resolveTo(settingsResult.map(settings -> settings.toGeckoBundle())); 1480 } 1481 1482 private void browsingDataClear(final Message message, final WebExtension extension) { 1483 final WebExtension.BrowsingDataDelegate delegate = mListener.getBrowsingDataDelegate(extension); 1484 if (delegate == null) { 1485 mPendingBrowsingData.add(extension.id, message); 1486 return; 1487 } 1488 1489 final long unixTimestamp = message.bundle.getLong("since"); 1490 final String dataType = message.bundle.getString("dataType"); 1491 1492 final GeckoResult<Void> response; 1493 if ("downloads".equals(dataType)) { 1494 response = delegate.onClearDownloads(unixTimestamp); 1495 } else if ("formData".equals(dataType)) { 1496 response = delegate.onClearFormData(unixTimestamp); 1497 } else if ("history".equals(dataType)) { 1498 response = delegate.onClearHistory(unixTimestamp); 1499 } else if ("passwords".equals(dataType)) { 1500 response = delegate.onClearPasswords(unixTimestamp); 1501 } else { 1502 throw new IllegalStateException("Illegal clear data type: " + dataType); 1503 } 1504 1505 message.callback.resolveTo(response); 1506 } 1507 1508 /* package */ void download(final Message message, final WebExtension extension) { 1509 final WebExtension.DownloadDelegate delegate = mListener.getDownloadDelegate(extension); 1510 if (delegate == null) { 1511 mPendingDownload.add(extension.id, message); 1512 return; 1513 } 1514 1515 final GeckoBundle optionsBundle = message.bundle.getBundle("options"); 1516 1517 final WebExtension.DownloadRequest request = 1518 WebExtension.DownloadRequest.fromBundle(optionsBundle); 1519 1520 final GeckoResult<WebExtension.DownloadInitData> result = 1521 delegate.onDownload(extension, request); 1522 if (result == null) { 1523 message.callback.sendError("downloads.download is not supported"); 1524 return; 1525 } 1526 1527 message.callback.resolveTo( 1528 result.map( 1529 value -> { 1530 if (value == null) { 1531 Log.e(LOGTAG, "onDownload returned invalid null value"); 1532 throw new IllegalArgumentException("downloads.download is not supported"); 1533 } 1534 1535 final GeckoBundle returnMessage = 1536 WebExtension.Download.downloadInfoToBundle(value.initData); 1537 returnMessage.putInt("id", value.download.id); 1538 1539 return returnMessage; 1540 })); 1541 } 1542 1543 /* package */ void openOptionsPage(final Message message, final WebExtension extension) { 1544 final GeckoBundle bundle = message.bundle; 1545 final WebExtension.TabDelegate delegate = mListener.getTabDelegate(extension); 1546 1547 if (delegate != null) { 1548 delegate.onOpenOptionsPage(extension); 1549 } else { 1550 message.callback.sendError("runtime.openOptionsPage is not supported"); 1551 } 1552 1553 message.callback.sendSuccess(null); 1554 } 1555 1556 /* package */ 1557 @SuppressLint("WrongThread") // for .isOpen 1558 void newTab(final Message message, final WebExtension extension) { 1559 final GeckoBundle bundle = message.bundle; 1560 1561 final WebExtension.TabDelegate delegate = mListener.getTabDelegate(extension); 1562 final WebExtension.CreateTabDetails details = 1563 new WebExtension.CreateTabDetails(bundle.getBundle("createProperties")); 1564 1565 final GeckoResult<GeckoSession> result; 1566 if (delegate != null) { 1567 result = delegate.onNewTab(extension, details); 1568 } else { 1569 mPendingNewTab.add(extension.id, message); 1570 return; 1571 } 1572 1573 if (result == null) { 1574 message.callback.sendSuccess(false); 1575 return; 1576 } 1577 1578 final String newSessionId = message.bundle.getString("newSessionId"); 1579 message.callback.resolveTo( 1580 result.map( 1581 session -> { 1582 if (session == null) { 1583 return false; 1584 } 1585 1586 if (session.isOpen()) { 1587 throw new IllegalArgumentException("Must use an unopened GeckoSession instance"); 1588 } 1589 1590 session.open(mListener.runtime, newSessionId); 1591 return true; 1592 })); 1593 } 1594 1595 /* package */ void updateTab(final Message message, final WebExtension extension) { 1596 final WebExtension.SessionTabDelegate delegate = 1597 message.session.getWebExtensionController().getTabDelegate(extension); 1598 final EventCallback callback = message.callback; 1599 1600 if (delegate == null) { 1601 callback.sendError("tabs.update is not supported"); 1602 return; 1603 } 1604 1605 final WebExtension.UpdateTabDetails details = 1606 new WebExtension.UpdateTabDetails(message.bundle.getBundle("updateProperties")); 1607 callback.resolveTo( 1608 delegate 1609 .onUpdateTab(extension, message.session, details) 1610 .map( 1611 value -> { 1612 if (value == AllowOrDeny.ALLOW) { 1613 return null; 1614 } else { 1615 throw new Exception("tabs.update is not supported"); 1616 } 1617 })); 1618 } 1619 1620 /* package */ void closeTab(final Message message, final WebExtension extension) { 1621 final WebExtension.SessionTabDelegate delegate = 1622 message.session.getWebExtensionController().getTabDelegate(extension); 1623 1624 final GeckoResult<AllowOrDeny> result; 1625 if (delegate != null) { 1626 result = delegate.onCloseTab(extension, message.session); 1627 } else { 1628 result = GeckoResult.deny(); 1629 } 1630 1631 message.callback.resolveTo( 1632 result.map( 1633 value -> { 1634 if (value == AllowOrDeny.ALLOW) { 1635 return null; 1636 } else { 1637 throw new Exception("tabs.remove is not supported"); 1638 } 1639 })); 1640 } 1641 1642 /** 1643 * Notifies extensions about a active tab change over the `tabs.onActivated` event. 1644 * 1645 * @param session The {@link GeckoSession} of the newly selected session/tab. 1646 * @param active true if the tab became active, false if the tab became inactive. 1647 */ 1648 @AnyThread 1649 public void setTabActive(@NonNull final GeckoSession session, final boolean active) { 1650 final GeckoBundle bundle = new GeckoBundle(1); 1651 bundle.putBoolean("active", active); 1652 session.getEventDispatcher().dispatch("GeckoView:WebExtension:SetTabActive", bundle); 1653 } 1654 1655 /* package */ void unregisterWebExtension(final WebExtension webExtension) { 1656 mExtensions.remove(webExtension.id); 1657 mListener.unregisterWebExtension(webExtension); 1658 } 1659 1660 private WebExtension.MessageSender fromBundle( 1661 final WebExtension extension, final GeckoBundle sender, final GeckoSession session) { 1662 if (extension == null) { 1663 // All senders should have an extension 1664 return null; 1665 } 1666 1667 final String envType = sender.getString("envType"); 1668 @WebExtension.MessageSender.EnvType final int environmentType; 1669 1670 if ("content_child".equals(envType)) { 1671 environmentType = WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT; 1672 } else if ("addon_child".equals(envType)) { 1673 // TODO Bug 1554277: check that this message is coming from the right process 1674 environmentType = WebExtension.MessageSender.ENV_TYPE_EXTENSION; 1675 } else { 1676 environmentType = WebExtension.MessageSender.ENV_TYPE_UNKNOWN; 1677 } 1678 1679 if (environmentType == WebExtension.MessageSender.ENV_TYPE_UNKNOWN) { 1680 if (BuildConfig.DEBUG_BUILD) { 1681 throw new RuntimeException("Missing or unknown envType: " + envType); 1682 } 1683 1684 return null; 1685 } 1686 1687 final String url = sender.getString("url"); 1688 final boolean isTopLevel; 1689 if (session == null || environmentType == WebExtension.MessageSender.ENV_TYPE_EXTENSION) { 1690 // This message is coming from the background page, a popup, or an extension page 1691 isTopLevel = true; 1692 } else { 1693 // If session is present we are either receiving this message from a content script or 1694 // an extension page, let's make sure we have the proper identification so that 1695 // embedders can check the origin of this message. 1696 // -1 is an invalid frame id 1697 final boolean hasFrameId = 1698 sender.containsKey("frameId") && sender.getInt("frameId", -1) != -1; 1699 final boolean hasUrl = sender.containsKey("url"); 1700 if (!hasFrameId || !hasUrl) { 1701 if (BuildConfig.DEBUG_BUILD) { 1702 throw new RuntimeException( 1703 "Missing sender information. hasFrameId: " + hasFrameId + " hasUrl: " + hasUrl); 1704 } 1705 1706 // This message does not have the proper identification and may be compromised, 1707 // let's ignore it. 1708 return null; 1709 } 1710 1711 isTopLevel = sender.getInt("frameId", -1) == 0; 1712 } 1713 1714 return new WebExtension.MessageSender(extension, session, url, environmentType, isTopLevel); 1715 } 1716 1717 private WebExtension.MessageDelegate getDelegate( 1718 final String nativeApp, 1719 final WebExtension.MessageSender sender, 1720 final EventCallback callback) { 1721 if ((sender.webExtension.flags & WebExtension.Flags.ALLOW_CONTENT_MESSAGING) == 0 1722 && sender.environmentType == WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT) { 1723 callback.sendError("This NativeApp can't receive messages from Content Scripts."); 1724 return null; 1725 } 1726 1727 WebExtension.MessageDelegate delegate = null; 1728 1729 if (sender.session != null) { 1730 delegate = 1731 sender 1732 .session 1733 .getWebExtensionController() 1734 .getMessageDelegate(sender.webExtension, nativeApp); 1735 } else if (sender.environmentType == WebExtension.MessageSender.ENV_TYPE_EXTENSION) { 1736 delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp); 1737 } 1738 1739 return delegate; 1740 } 1741 1742 private static class MessageRecipient { 1743 public final String webExtensionId; 1744 public final String nativeApp; 1745 public final GeckoSession session; 1746 1747 public MessageRecipient( 1748 final String webExtensionId, final String nativeApp, final GeckoSession session) { 1749 this.webExtensionId = webExtensionId; 1750 this.nativeApp = nativeApp; 1751 this.session = session; 1752 } 1753 1754 private static boolean equals(final Object a, final Object b) { 1755 return Objects.equals(a, b); 1756 } 1757 1758 @Override 1759 public boolean equals(final Object other) { 1760 if (!(other instanceof MessageRecipient)) { 1761 return false; 1762 } 1763 1764 final MessageRecipient o = (MessageRecipient) other; 1765 return equals(webExtensionId, o.webExtensionId) 1766 && equals(nativeApp, o.nativeApp) 1767 && equals(session, o.session); 1768 } 1769 1770 @Override 1771 public int hashCode() { 1772 return Arrays.hashCode(new Object[] {webExtensionId, nativeApp, session}); 1773 } 1774 } 1775 1776 private void connect( 1777 final String nativeApp, 1778 final long portId, 1779 final Message message, 1780 final WebExtension.MessageSender sender) { 1781 if (portId == -1) { 1782 message.callback.sendError("Missing portId."); 1783 return; 1784 } 1785 1786 final WebExtension.Port port = new WebExtension.Port(nativeApp, portId, sender); 1787 1788 final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender, message.callback); 1789 if (delegate == null) { 1790 mPendingMessages.add( 1791 new MessageRecipient(nativeApp, sender.webExtension.id, sender.session), message); 1792 return; 1793 } 1794 1795 delegate.onConnect(port); 1796 message.callback.sendSuccess(true); 1797 } 1798 1799 private void message( 1800 final String nativeApp, final Message message, final WebExtension.MessageSender sender) { 1801 final EventCallback callback = message.callback; 1802 1803 final Object content; 1804 try { 1805 content = message.bundle.toJSONObject().get("data"); 1806 } catch (final JSONException ex) { 1807 callback.sendError(ex.getMessage()); 1808 return; 1809 } 1810 1811 final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender, callback); 1812 if (delegate == null) { 1813 mPendingMessages.add( 1814 new MessageRecipient(nativeApp, sender.webExtension.id, sender.session), message); 1815 return; 1816 } 1817 1818 final GeckoResult<Object> response = delegate.onMessage(nativeApp, content, sender); 1819 if (response == null) { 1820 callback.sendSuccess(null); 1821 return; 1822 } 1823 1824 callback.resolveTo(response); 1825 } 1826 1827 private GeckoResult<WebExtension> extensionFromBundle(final GeckoBundle message) { 1828 final String extensionId = message.getString("extensionId"); 1829 return mExtensions.get(extensionId); 1830 } 1831 1832 private void openPopup( 1833 final Message message, 1834 final WebExtension extension, 1835 final @WebExtension.Action.ActionType int actionType) { 1836 if (extension == null) { 1837 return; 1838 } 1839 1840 final WebExtension.Action action = 1841 new WebExtension.Action(actionType, message.bundle.getBundle("action"), extension); 1842 final String popupUri = message.bundle.getString("popupUri"); 1843 1844 final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, message.session); 1845 if (delegate == null) { 1846 return; 1847 } 1848 1849 final GeckoResult<GeckoSession> popup = delegate.onOpenPopup(extension, action); 1850 action.openPopup(popup, popupUri); 1851 } 1852 1853 private WebExtension.ActionDelegate actionDelegateFor( 1854 final WebExtension extension, final GeckoSession session) { 1855 if (session == null) { 1856 return mListener.getActionDelegate(extension); 1857 } 1858 1859 return session.getWebExtensionController().getActionDelegate(extension); 1860 } 1861 1862 private void actionUpdate( 1863 final Message message, 1864 final WebExtension extension, 1865 final @WebExtension.Action.ActionType int actionType) { 1866 if (extension == null) { 1867 return; 1868 } 1869 1870 final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, message.session); 1871 if (delegate == null) { 1872 return; 1873 } 1874 1875 final WebExtension.Action action = 1876 new WebExtension.Action(actionType, message.bundle.getBundle("action"), extension); 1877 if (actionType == WebExtension.Action.TYPE_BROWSER_ACTION) { 1878 delegate.onBrowserAction(extension, message.session, action); 1879 } else if (actionType == WebExtension.Action.TYPE_PAGE_ACTION) { 1880 delegate.onPageAction(extension, message.session, action); 1881 } 1882 } 1883 1884 // TODO: implement bug 1595822 1885 /* package */ static GeckoResult<List<WebExtension.Menu>> getMenu( 1886 final GeckoBundle menuArrayBundle) { 1887 return null; 1888 } 1889 1890 /** 1891 * Creates a new Download object with the specified ID for tracking WebExtension downloads. 1892 * 1893 * @param id The unique identifier for the download 1894 * @return A new Download object for the specified ID 1895 * @throws IllegalArgumentException if a download with this ID already exists 1896 */ 1897 @Nullable 1898 @UiThread 1899 public WebExtension.Download createDownload(final int id) { 1900 if (mDownloads.indexOfKey(id) >= 0) { 1901 throw new IllegalArgumentException("Download with this id already exists"); 1902 } else { 1903 final WebExtension.Download download = new WebExtension.Download(id); 1904 mDownloads.put(id, download); 1905 1906 return download; 1907 } 1908 } 1909 }