Bug 1499895 - [1.3] Add triggering URI to onLoadRequest. r=snorp,jchen
authorEugen Sawin <esawin@mozilla.com>
Wed, 17 Oct 2018 10:06:42 +0200
changeset 490358 45e3efb5e07666bd7e6e7bb0c0db34aa2cd75b4a
parent 490357 1f63ece13c07c287fad4f5684da002747705bdd1
child 490359 b9614de52765a2da6de9eb067c2431c9517b95cd
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerssnorp, jchen
bugs1499895
milestone64.0a1
Bug 1499895 - [1.3] Add triggering URI to onLoadRequest. r=snorp,jchen
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
mobile/android/chrome/geckoview/GeckoViewNavigationContent.js
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/modules/geckoview/GeckoViewNavigation.jsm
mobile/android/modules/geckoview/LoadURIDelegate.jsm
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -579,49 +579,48 @@ public class CustomTabsActivity extends 
 
     @Override
     public void onCanGoForward(GeckoSession session, boolean canGoForward) {
         mCanGoForward = canGoForward;
         updateMenuItemForward();
     }
 
     @Override
-    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String urlStr,
-                                                  final int target,
-                                                  final int flags) {
-        if (target != GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW) {
+    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session,
+                                                  final LoadRequest request) {
+        if (request.target != GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW) {
             return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
-        final Uri uri = Uri.parse(urlStr);
+        final Uri uri = Uri.parse(request.uri);
         if (uri == null) {
             // We can't handle this, so deny it.
-            Log.w(LOGTAG, "Failed to parse URL for navigation: " + urlStr);
+            Log.w(LOGTAG, "Failed to parse URL for navigation: " + request.uri);
             return GeckoResult.fromValue(AllowOrDeny.DENY);
         }
 
         // Always use Fennec for these schemes.
         if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme()) ||
             "data".equals(uri.getScheme()) || "blob".equals(uri.getScheme())) {
             final Intent intent = new Intent(this, BrowserApp.class);
             intent.setAction(Intent.ACTION_VIEW);
             intent.setData(uri);
             intent.setPackage(getPackageName());
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
-                Log.w(LOGTAG, "No activity handler found for: " + urlStr);
+                Log.w(LOGTAG, "No activity handler found for: " + request.uri);
             }
         } else {
             final Intent intent = new Intent(Intent.ACTION_VIEW);
             intent.setData(uri);
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
-                Log.w(LOGTAG, "No activity handler found for: " + urlStr);
+                Log.w(LOGTAG, "No activity handler found for: " + request.uri);
             }
         }
 
         return GeckoResult.fromValue(AllowOrDeny.DENY);
     }
 
     @Override
     public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -373,27 +373,26 @@ public class WebAppActivity extends AppC
     }
 
     @Override // GeckoSession.ContentDelegate
     public void onFullScreen(GeckoSession session, boolean fullScreen) {
         updateFullScreenContent(fullScreen);
     }
 
     @Override
-    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String urlStr,
-                                                  final int target,
-                                                  final int flags) {
-        final Uri uri = Uri.parse(urlStr);
+    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session,
+                                                  final LoadRequest request) {
+        final Uri uri = Uri.parse(request.uri);
         if (uri == null) {
             // We can't really handle this, so deny it?
-            Log.w(LOGTAG, "Failed to parse URL for navigation: " + urlStr);
+            Log.w(LOGTAG, "Failed to parse URL for navigation: " + request.uri);
             return GeckoResult.fromValue(AllowOrDeny.DENY);
         }
 
-        if (mManifest.isInScope(uri) && target != TARGET_WINDOW_NEW) {
+        if (mManifest.isInScope(uri) && request.target != TARGET_WINDOW_NEW) {
             // This is in scope and wants to load in the same frame, so
             // let Gecko handle it.
             return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme()) ||
             "data".equals(uri.getScheme()) || "blob".equals(uri.getScheme())) {
             final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder()
@@ -411,17 +410,17 @@ public class WebAppActivity extends AppC
             tab.launchUrl(this, uri);
         } else {
             final Intent intent = new Intent();
             intent.setAction(Intent.ACTION_VIEW);
             intent.setData(uri);
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
-                Log.w(LOGTAG, "No activity handler found for: " + urlStr);
+                Log.w(LOGTAG, "No activity handler found for: " + request.uri);
             }
         }
 
         return GeckoResult.fromValue(AllowOrDeny.DENY);
     }
 
     @Override
     public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
--- a/mobile/android/chrome/geckoview/GeckoViewNavigationContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewNavigationContent.js
@@ -15,29 +15,31 @@ XPCOMUtils.defineLazyModuleGetters(this,
 class GeckoViewNavigationContent extends GeckoViewContentModule {
   onInit() {
     docShell.loadURIDelegate = this;
   }
 
   // nsILoadURIDelegate.
   loadURI(aUri, aWhere, aFlags, aTriggeringPrincipal) {
     debug `loadURI: uri=${aUri && aUri.spec}
-                    where=${aWhere} flags=${aFlags}`;
+                    where=${aWhere} flags=${aFlags}
+                    tp=${aTriggeringPrincipal && aTriggeringPrincipal.URI &&
+                         aTriggeringPrincipal.URI.spec}`;
 
     if (!this.enabled) {
       return false;
     }
 
     // TODO: Remove this when we have a sensible error API.
     if (aUri && aUri.displaySpec.startsWith("about:certerror")) {
       addEventListener("click", ErrorPageEventHandler, true);
     }
 
     return LoadURIDelegate.load(content, this.eventDispatcher,
-                                aUri, aWhere, aFlags);
+                                aUri, aWhere, aFlags, aTriggeringPrincipal);
   }
 
   // nsILoadURIDelegate.
   handleLoadError(aUri, aError, aErrorModule) {
     debug `handleLoadError: uri=${aUri && aUri.spec}
                              uri2=${aUri && aUri.displaySpec}
                              error=${aError}`;
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -169,32 +169,16 @@ public class GeckoSession extends LayerS
             "GeckoViewNavigation", this,
             new String[]{
                 "GeckoView:LocationChange",
                 "GeckoView:OnLoadError",
                 "GeckoView:OnLoadRequest",
                 "GeckoView:OnNewSession"
             }
         ) {
-            // This needs to match nsIBrowserDOMWindow.idl
-            private int convertGeckoTarget(int geckoTarget) {
-                switch (geckoTarget) {
-                    case 0: // OPEN_DEFAULTWINDOW
-                    case 1: // OPEN_CURRENTWINDOW
-                        return NavigationDelegate.TARGET_WINDOW_CURRENT;
-                    default: // OPEN_NEWWINDOW, OPEN_NEWTAB, OPEN_SWITCHTAB
-                        return NavigationDelegate.TARGET_WINDOW_NEW;
-                }
-            }
-
-            // The flags are already matched with nsIDocShell.idl.
-            private int filterFlags(int flags) {
-                return flags & NavigationDelegate.LOAD_REQUEST_IS_USER_TRIGGERED;
-            }
-
             private @NavigationDelegate.LoadErrorCategory int getErrorCategory(
                     long errorModule, @NavigationDelegate.LoadError int error) {
                 // Match flags with XPCOM ErrorList.h.
                 if (errorModule == 21) {
                     return NavigationDelegate.ERROR_CATEGORY_SECURITY;
                 }
                 return error & 0xF;
             }
@@ -299,32 +283,35 @@ public class GeckoSession extends LayerS
                         delegate.onLocationChange(GeckoSession.this,
                                                   message.getString("uri"));
                     }
                     delegate.onCanGoBack(GeckoSession.this,
                                          message.getBoolean("canGoBack"));
                     delegate.onCanGoForward(GeckoSession.this,
                                             message.getBoolean("canGoForward"));
                 } else if ("GeckoView:OnLoadRequest".equals(event)) {
-                    final String uri = message.getString("uri");
-                    final int where = convertGeckoTarget(message.getInt("where"));
-                    final int flags = filterFlags(message.getInt("flags"));
-
-                    if (!IntentUtils.isUriSafeForScheme(uri)) {
+                    final NavigationDelegate.LoadRequest request =
+                        new NavigationDelegate.LoadRequest(
+                              message.getString("uri"),
+                              message.getString("triggerUri"),
+                              message.getInt("where"),
+                              message.getInt("flags"));
+
+                    if (!IntentUtils.isUriSafeForScheme(request.uri)) {
                         callback.sendError("Blocked unsafe intent URI");
 
-                        delegate.onLoadError(GeckoSession.this, uri,
+                        delegate.onLoadError(GeckoSession.this, request.uri,
                                              NavigationDelegate.ERROR_CATEGORY_URI,
                                              NavigationDelegate.ERROR_MALFORMED_URI);
 
                         return;
                     }
 
                     final GeckoResult<AllowOrDeny> result =
-                        delegate.onLoadRequest(GeckoSession.this, uri, where, flags);
+                        delegate.onLoadRequest(GeckoSession.this, request);
 
                     if (result == null) {
                         callback.sendSuccess(null);
                         return;
                     }
 
                     result.then(new GeckoResult.OnValueListener<AllowOrDeny, Void>() {
                         @Override
@@ -2551,50 +2538,83 @@ public class GeckoSession extends LayerS
         void onCanGoForward(GeckoSession session, boolean canGoForward);
 
         @IntDef({TARGET_WINDOW_NONE, TARGET_WINDOW_CURRENT, TARGET_WINDOW_NEW})
         /* package */ @interface TargetWindow {}
         public static final int TARGET_WINDOW_NONE = 0;
         public static final int TARGET_WINDOW_CURRENT = 1;
         public static final int TARGET_WINDOW_NEW = 2;
 
-        @IntDef(flag = true,
-                value = {LOAD_REQUEST_IS_USER_TRIGGERED})
-        /* package */ @interface LoadRequestFlags {}
-
-        // Match with nsIDocShell.idl.
         /**
-         * The load request was triggered by user input.
+         * Load request details.
          */
-        public static final int LOAD_REQUEST_IS_USER_TRIGGERED = 0x1000;
+        public static class LoadRequest {
+            /* package */ LoadRequest(@NonNull final String uri,
+                                      @Nullable final String triggerUri,
+                                      int geckoTarget,
+                                      int flags) {
+                this.uri = uri;
+                this.triggerUri = triggerUri;
+                this.target = convertGeckoTarget(geckoTarget);
+
+                // Match with nsIDocShell.idl.
+                this.isUserTriggered = (flags & 0x1000) != 0;
+            }
+
+            // This needs to match nsIBrowserDOMWindow.idl
+            private @TargetWindow int convertGeckoTarget(int geckoTarget) {
+                switch (geckoTarget) {
+                    case 0: // OPEN_DEFAULTWINDOW
+                    case 1: // OPEN_CURRENTWINDOW
+                        return TARGET_WINDOW_CURRENT;
+                    default: // OPEN_NEWWINDOW, OPEN_NEWTAB, OPEN_SWITCHTAB
+                        return TARGET_WINDOW_NEW;
+                }
+            }
+
+            /**
+             * The URI to be loaded.
+             */
+            public final @NonNull String uri;
+
+            /**
+             * The URI of the origin page that triggered the load request.
+             * null for initial loads and loads originating from data: URIs.
+             */
+            public final @Nullable String triggerUri;
+
+            /**
+             * The target where the window has requested to open.
+             * One of {@link #TARGET_WINDOW_NONE TARGET_WINDOW_*}.
+             */
+            public final @TargetWindow int target;
+
+            /**
+             * True if and only if the request was triggered by user interaction.
+             */
+            public final boolean isUserTriggered;
+        }
 
         /**
          * A request to open an URI. This is called before each page load to
          * allow custom behavior implementation.
          * For example, this can be used to override the behavior of
          * TAGET_WINDOW_NEW requests, which defaults to requesting a new
          * GeckoSession via onNewSession.
          *
          * @param session The GeckoSession that initiated the callback.
-         * @param uri The URI to be loaded.
-         * @param target The target where the window has requested to open.
-         *               One of {@link #TARGET_WINDOW_NONE TARGET_WINDOW_*}.
-         * @param flags The load request flags.
-         *              One or more of {@link #LOAD_REQUEST_IS_USER_TRIGGERED
-         *              LOAD_REQUEST_*}.
+         * @param request The {@link LoadRequest} containing the request details.
          *
          * @return A {@link GeckoResult} with a AllowOrDeny value which indicates whether
          *         or not the load was handled. If unhandled, Gecko will continue the
          *         load as normal. If handled (true value), Gecko will abandon the load.
          *         A null return value is interpreted as false (unhandled).
          */
         @Nullable GeckoResult<AllowOrDeny> onLoadRequest(@NonNull GeckoSession session,
-                                                         @NonNull String uri,
-                                                         @TargetWindow int target,
-                                                         @LoadRequestFlags int flags);
+                                                         @NonNull LoadRequest request);
 
         /**
         * A request has been made to open a new session. The URI is provided only for
         * informational purposes. Do not call GeckoSession.loadUri() here. Additionally, the
         * returned GeckoSession must be a newly-created one.
         *
         * @param session The GeckoSession that initiated the callback.
         * @param uri The URI to be loaded.
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -627,20 +627,23 @@ public class GeckoViewActivity extends A
         }
 
         @Override
         public void onCanGoForward(GeckoSession session, boolean canGoForward) {
             mCanGoForward = canGoForward;
         }
 
         @Override
-        public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String uri,
-                                                       final int target, final int flags) {
-            Log.d(LOGTAG, "onLoadRequest=" + uri + " where=" + target +
-                  " flags=" + flags);
+        public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session,
+                                                      final LoadRequest request) {
+            Log.d(LOGTAG, "onLoadRequest=" + request.uri +
+                  " triggerUri=" + request.triggerUri +
+                  " where=" + request.target +
+                  " isUserTriggered=" + request.isUserTriggered);
+
             return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         @Override
         public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
             GeckoSession newSession = new GeckoSession(session.getSettings());
 
             Intent intent = new Intent(GeckoViewActivity.this, SessionActivity.class);
--- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
@@ -151,17 +151,17 @@ class GeckoViewNavigation extends GeckoV
   }
 
   // nsIBrowserDOMWindow.
   createContentWindow(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) {
     debug `createContentWindow: uri=${aUri && aUri.spec}
                                 where=${aWhere} flags=${aFlags}`;
 
     if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+                             aUri, aWhere, aFlags, aTriggeringPrincipal)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
     const browser = this.handleNewSession(aUri, aOpener, aWhere, aFlags, null);
     if (!browser) {
       Components.returnCode = Cr.NS_ERROR_ABORT;
@@ -175,17 +175,18 @@ class GeckoViewNavigation extends GeckoV
   createContentWindowInFrame(aUri, aParams, aWhere, aFlags, aNextTabParentId,
                              aName) {
     debug `createContentWindowInFrame: uri=${aUri && aUri.spec}
                                        where=${aWhere} flags=${aFlags}
                                        nextTabParentId=${aNextTabParentId}
                                        name=${aName}`;
 
     if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+                             aUri, aWhere, aFlags,
+                             aParams.triggeringPrincipal)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
     const browser = this.handleNewSession(aUri, null, aWhere, aFlags, aNextTabParentId);
     if (!browser) {
       Components.returnCode = Cr.NS_ERROR_ABORT;
@@ -196,17 +197,17 @@ class GeckoViewNavigation extends GeckoV
   }
 
   handleOpenUri(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal,
                 aNextTabParentId) {
     debug `handleOpenUri: uri=${aUri && aUri.spec}
                           where=${aWhere} flags=${aFlags}`;
 
     if (LoadURIDelegate.load(this.window, this.eventDispatcher,
-                             aUri, aWhere, aFlags)) {
+                             aUri, aWhere, aFlags, aTriggeringPrincipal)) {
       return null;
     }
 
     let browser = this.browser;
 
     if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
         aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
         aWhere === Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
@@ -226,17 +227,18 @@ class GeckoViewNavigation extends GeckoV
   openURI(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) {
     const browser = this.handleOpenUri(aUri, aOpener, aWhere, aFlags,
                                        aTriggeringPrincipal, null);
     return browser && browser.contentWindow;
   }
 
   // nsIBrowserDOMWindow.
   openURIInFrame(aUri, aParams, aWhere, aFlags, aNextTabParentId, aName) {
-    const browser = this.handleOpenUri(aUri, null, aWhere, aFlags, null,
+    const browser = this.handleOpenUri(aUri, null, aWhere, aFlags,
+                                       aParams.triggeringPrincipal,
                                        aNextTabParentId);
     return browser;
   }
 
   // nsIBrowserDOMWindow.
   isTabContentWindow(aWindow) {
     return this.browser.contentWindow === aWindow;
   }
--- a/mobile/android/modules/geckoview/LoadURIDelegate.jsm
+++ b/mobile/android/modules/geckoview/LoadURIDelegate.jsm
@@ -13,26 +13,33 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Services: "resource://gre/modules/Services.jsm",
 });
 
 GeckoViewUtils.initLogging("LoadURIDelegate", this);
 
 const LoadURIDelegate = {
   // Delegate URI loading to the app.
   // Return whether the loading has been handled.
-  load: function(aWindow, aEventDispatcher, aUri, aWhere, aFlags) {
+  load: function(aWindow, aEventDispatcher, aUri, aWhere, aFlags,
+                 aTriggeringPrincipal) {
     if (!aWindow) {
       return false;
     }
 
+    const triggerUri = aTriggeringPrincipal &&
+                       (aTriggeringPrincipal.isNullPrincipal
+                        ? null
+                        : aTriggeringPrincipal.URI);
+
     const message = {
       type: "GeckoView:OnLoadRequest",
       uri: aUri ? aUri.displaySpec : "",
       where: aWhere,
-      flags: aFlags
+      flags: aFlags,
+      triggerUri: triggerUri && triggerUri.displaySpec,
     };
 
     let handled = undefined;
     aEventDispatcher.sendRequestForResult(message).then(response => {
       handled = response;
     }, () => {
       // There was an error or listener was not registered in GeckoSession,
       // treat as unhandled.