Bug 1814798 - pt 1. Add bool to enable/disable PHC at runtime r=glandium
[gecko.git] / remote / marionette / navigate.sys.mjs
blobac8de5fd6cea2021c2b0cbbf63280abfef1d9d68
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
9   EventDispatcher:
10     "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs",
11   Log: "chrome://remote/content/shared/Log.sys.mjs",
12   PageLoadStrategy:
13     "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
14   ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
15   TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs",
16   truncate: "chrome://remote/content/shared/Format.sys.mjs",
17 });
19 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
20   lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
23 // Timeouts used to check if a new navigation has been initiated.
24 const TIMEOUT_BEFOREUNLOAD_EVENT = 200;
25 const TIMEOUT_UNLOAD_EVENT = 5000;
27 /** @namespace */
28 export const navigate = {};
30 /**
31  * Checks the value of readyState for the current page
32  * load activity, and resolves the command if the load
33  * has been finished. It also takes care of the selected
34  * page load strategy.
35  *
36  * @param {PageLoadStrategy} pageLoadStrategy
37  *     Strategy when navigation is considered as finished.
38  * @param {object} eventData
39  * @param {string} eventData.documentURI
40  *     Current document URI of the document.
41  * @param {string} eventData.readyState
42  *     Current ready state of the document.
43  *
44  * @returns {boolean}
45  *     True if the page load has been finished.
46  */
47 function checkReadyState(pageLoadStrategy, eventData = {}) {
48   const { documentURI, readyState } = eventData;
50   const result = { error: null, finished: false };
52   switch (readyState) {
53     case "interactive":
54       if (documentURI.startsWith("about:certerror")) {
55         result.error = new lazy.error.InsecureCertificateError();
56         result.finished = true;
57       } else if (/about:.*(error)\?/.exec(documentURI)) {
58         result.error = new lazy.error.UnknownError(
59           `Reached error page: ${documentURI}`
60         );
61         result.finished = true;
63         // Return early with a page load strategy of eager, and also
64         // special-case about:blocked pages which should be treated as
65         // non-error pages but do not raise a pageshow event. about:blank
66         // is also treaded specifically here, because it gets temporary
67         // loaded for new content processes, and we only want to rely on
68         // complete loads for it.
69       } else if (
70         (pageLoadStrategy === lazy.PageLoadStrategy.Eager &&
71           documentURI != "about:blank") ||
72         /about:blocked\?/.exec(documentURI)
73       ) {
74         result.finished = true;
75       }
76       break;
78     case "complete":
79       result.finished = true;
80       break;
81   }
83   return result;
86 /**
87  * Determines if we expect to get a DOM load event (DOMContentLoaded)
88  * on navigating to the <code>future</code> URL.
89  *
90  * @param {URL} current
91  *     URL the browser is currently visiting.
92  * @param {object} options
93  * @param {BrowsingContext=} options.browsingContext
94  *     The current browsing context. Needed for targets of _parent and _top.
95  * @param {URL=} options.future
96  *     Destination URL, if known.
97  * @param {target=} options.target
98  *     Link target, if known.
99  *
100  * @returns {boolean}
101  *     Full page load would be expected if future is followed.
103  * @throws TypeError
104  *     If <code>current</code> is not defined, or any of
105  *     <code>current</code> or <code>future</code>  are invalid URLs.
106  */
107 navigate.isLoadEventExpected = function (current, options = {}) {
108   const { browsingContext, future, target } = options;
110   if (typeof current == "undefined") {
111     throw new TypeError("Expected at least one URL");
112   }
114   if (["_parent", "_top"].includes(target) && !browsingContext) {
115     throw new TypeError(
116       "Expected browsingContext when target is _parent or _top"
117     );
118   }
120   // Don't wait if the navigation happens in a different browsing context
121   if (
122     target === "_blank" ||
123     (target === "_parent" && browsingContext.parent) ||
124     (target === "_top" && browsingContext.top != browsingContext)
125   ) {
126     return false;
127   }
129   // Assume we will go somewhere exciting
130   if (typeof future == "undefined") {
131     return true;
132   }
134   // Assume javascript:<whatever> will modify the current document
135   // but this is not an entirely safe assumption to make,
136   // considering it could be used to set window.location
137   if (future.protocol == "javascript:") {
138     return false;
139   }
141   // If hashes are present and identical
142   if (
143     current.href.includes("#") &&
144     future.href.includes("#") &&
145     current.hash === future.hash
146   ) {
147     return false;
148   }
150   return true;
154  * Load the given URL in the specified browsing context.
156  * @param {CanonicalBrowsingContext} browsingContext
157  *     Browsing context to load the URL into.
158  * @param {string} url
159  *     URL to navigate to.
160  */
161 navigate.navigateTo = async function (browsingContext, url) {
162   const opts = {
163     loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
164     triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
165     // Fake user activation.
166     hasValidUserGestureActivation: true,
167   };
168   browsingContext.fixupAndLoadURIString(url, opts);
172  * Reload the page.
174  * @param {CanonicalBrowsingContext} browsingContext
175  *     Browsing context to refresh.
176  */
177 navigate.refresh = async function (browsingContext) {
178   const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
179   browsingContext.reload(flags);
183  * Execute a callback and wait for a possible navigation to complete
185  * @param {GeckoDriver} driver
186  *     Reference to driver instance.
187  * @param {Function} callback
188  *     Callback to execute that might trigger a navigation.
189  * @param {object} options
190  * @param {BrowsingContext=} options.browsingContext
191  *     Browsing context to observe. Defaults to the current browsing context.
192  * @param {boolean=} options.loadEventExpected
193  *     If false, return immediately and don't wait for
194  *     the navigation to be completed. Defaults to true.
195  * @param {boolean=} options.requireBeforeUnload
196  *     If false and no beforeunload event is fired, abort waiting
197  *     for the navigation. Defaults to true.
198  */
199 navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
200   driver,
201   callback,
202   options = {}
203 ) {
204   const {
205     browsingContextFn = driver.getBrowsingContext.bind(driver),
206     loadEventExpected = true,
207     requireBeforeUnload = true,
208   } = options;
210   const browsingContext = browsingContextFn();
211   const chromeWindow = browsingContext.topChromeWindow;
212   const pageLoadStrategy = driver.currentSession.pageLoadStrategy;
214   // Return immediately if no load event is expected
215   if (!loadEventExpected) {
216     await callback();
217     return Promise.resolve();
218   }
220   // When not waiting for page load events, do not return until the navigation has actually started.
221   if (pageLoadStrategy === lazy.PageLoadStrategy.None) {
222     const listener = new lazy.ProgressListener(browsingContext.webProgress, {
223       resolveWhenStarted: true,
224       waitForExplicitStart: true,
225     });
226     const navigated = listener.start();
227     navigated.finally(() => {
228       if (listener.isStarted) {
229         listener.stop();
230       }
231     });
233     await callback();
234     await navigated;
236     return Promise.resolve();
237   }
239   let rejectNavigation;
240   let resolveNavigation;
242   let browsingContextChanged = false;
243   let seenBeforeUnload = false;
244   let seenUnload = false;
246   let unloadTimer;
248   const checkDone = ({ finished, error }) => {
249     if (finished) {
250       if (error) {
251         rejectNavigation(error);
252       } else {
253         resolveNavigation();
254       }
255     }
256   };
258   const onPromptOpened = action => {
259     lazy.logger.trace("Canceled page load listener because a dialog opened");
260     checkDone({ finished: true });
261   };
263   const onTimer = timer => {
264     // In the case when a document has a beforeunload handler
265     // registered, the currently active command will return immediately
266     // due to the modal dialog observer.
267     //
268     // Otherwise the timeout waiting for the document to start
269     // navigating is increased by 5000 ms to ensure a possible load
270     // event is not missed. In the common case such an event should
271     // occur pretty soon after beforeunload, and we optimise for this.
272     if (seenBeforeUnload) {
273       seenBeforeUnload = false;
274       unloadTimer.initWithCallback(
275         onTimer,
276         TIMEOUT_UNLOAD_EVENT,
277         Ci.nsITimer.TYPE_ONE_SHOT
278       );
280       // If no page unload has been detected, ensure to properly stop
281       // the load listener, and return from the currently active command.
282     } else if (!seenUnload) {
283       lazy.logger.trace(
284         "Canceled page load listener because no navigation " +
285           "has been detected"
286       );
287       checkDone({ finished: true });
288     }
289   };
291   const onNavigation = (eventName, data) => {
292     const browsingContext = browsingContextFn();
294     // Ignore events from other browsing contexts than the selected one.
295     if (data.browsingContext != browsingContext) {
296       return;
297     }
299     lazy.logger.trace(
300       lazy.truncate`[${data.browsingContext.id}] Received event ${data.type} for ${data.documentURI}`
301     );
303     switch (data.type) {
304       case "beforeunload":
305         seenBeforeUnload = true;
306         break;
308       case "pagehide":
309         seenUnload = true;
310         break;
312       case "hashchange":
313       case "popstate":
314         checkDone({ finished: true });
315         break;
317       case "DOMContentLoaded":
318       case "pageshow":
319         // Don't require an unload event when a top-level browsing context
320         // change occurred.
321         if (!seenUnload && !browsingContextChanged) {
322           return;
323         }
324         const result = checkReadyState(pageLoadStrategy, data);
325         checkDone(result);
326         break;
327     }
328   };
330   // In the case when the currently selected frame is closed,
331   // there will be no further load events. Stop listening immediately.
332   const onBrowsingContextDiscarded = (subject, topic, why) => {
333     // If the BrowsingContext is being discarded to be replaced by another
334     // context, we don't want to stop waiting for the pageload to complete, as
335     // we will continue listening to the newly created context.
336     if (subject == browsingContextFn() && why != "replace") {
337       lazy.logger.trace(
338         "Canceled page load listener " +
339           `because browsing context with id ${subject.id} has been removed`
340       );
341       checkDone({ finished: true });
342     }
343   };
345   // Detect changes to the top-level browsing context to not
346   // necessarily require an unload event.
347   const onBrowsingContextChanged = event => {
348     if (event.target === driver.curBrowser.contentBrowser) {
349       browsingContextChanged = true;
350     }
351   };
353   const onUnload = event => {
354     lazy.logger.trace(
355       "Canceled page load listener " +
356         "because the top-browsing context has been closed"
357     );
358     checkDone({ finished: true });
359   };
361   chromeWindow.addEventListener("TabClose", onUnload);
362   chromeWindow.addEventListener("unload", onUnload);
363   driver.curBrowser.tabBrowser?.addEventListener(
364     "XULFrameLoaderCreated",
365     onBrowsingContextChanged
366   );
367   driver.promptListener.on("opened", onPromptOpened);
368   Services.obs.addObserver(
369     onBrowsingContextDiscarded,
370     "browsing-context-discarded"
371   );
373   lazy.EventDispatcher.on("page-load", onNavigation);
375   return new lazy.TimedPromise(
376     async (resolve, reject) => {
377       rejectNavigation = reject;
378       resolveNavigation = resolve;
380       try {
381         await callback();
383         // Certain commands like clickElement can cause a navigation. Setup a timer
384         // to check if a "beforeunload" event has been emitted within the given
385         // time frame. If not resolve the Promise.
386         if (!requireBeforeUnload) {
387           unloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
388           unloadTimer.initWithCallback(
389             onTimer,
390             TIMEOUT_BEFOREUNLOAD_EVENT,
391             Ci.nsITimer.TYPE_ONE_SHOT
392           );
393         }
394       } catch (e) {
395         // Executing the callback above could destroy the actor pair before the
396         // command returns. Such an error has to be ignored.
397         if (e.name !== "AbortError") {
398           checkDone({ finished: true, error: e });
399         }
400       }
401     },
402     {
403       errorMessage: "Navigation timed out",
404       timeout: driver.currentSession.timeouts.pageLoad,
405     }
406   ).finally(() => {
407     // Clean-up all registered listeners and timers
408     Services.obs.removeObserver(
409       onBrowsingContextDiscarded,
410       "browsing-context-discarded"
411     );
412     chromeWindow.removeEventListener("TabClose", onUnload);
413     chromeWindow.removeEventListener("unload", onUnload);
414     driver.curBrowser.tabBrowser?.removeEventListener(
415       "XULFrameLoaderCreated",
416       onBrowsingContextChanged
417     );
418     driver.promptListener?.off(onPromptOpened);
419     unloadTimer?.cancel();
421     lazy.EventDispatcher.off("page-load", onNavigation);
422   });