Skip to main content

script/
navigation.rs

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 https://mozilla.org/MPL/2.0/. */
4
5//! The listener that encapsulates all state for an in-progress document request.
6//! Any redirects that are encountered are followed. Whenever a non-redirect
7//! response is received, it is forwarded to the appropriate script thread.
8
9use std::cell::Cell;
10
11use content_security_policy::sandboxing_directive::SandboxingFlagSet;
12use crossbeam_channel::Sender;
13use embedder_traits::user_contents::UserContentManagerId;
14use embedder_traits::{Theme, ViewportDetails, WebDriverLoadStatus};
15use http::header;
16use js::context::JSContext;
17use net_traits::blob_url_store::UrlWithBlobClaim;
18use net_traits::policy_container::RequestPolicyContainer;
19use net_traits::request::{
20    CredentialsMode, InsecureRequestsPolicy, Origin, PreloadedResources, RedirectMode,
21    RequestBuilder, RequestClient, RequestMode,
22};
23use net_traits::response::ResponseInit;
24use net_traits::{
25    BoxedFetchCallback, CoreResourceThread, DOCUMENT_ACCEPT_HEADER_VALUE, FetchResponseMsg,
26    Metadata, ReferrerPolicy, fetch_async, set_default_accept_language,
27};
28use script_bindings::inheritance::Castable;
29use script_traits::{DocumentActivity, NewPipelineInfo};
30use servo_base::cross_process_instant::CrossProcessInstant;
31use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
32use servo_constellation_traits::{
33    LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationMessage,
34    TargetSnapshotParams,
35};
36use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
37use url::Position;
38
39use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
40use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
41use crate::dom::bindings::refcounted::Trusted;
42use crate::dom::element::Element;
43use crate::dom::html::htmliframeelement::HTMLIFrameElement;
44use crate::dom::node::node::NodeTraits;
45use crate::dom::window::Window;
46use crate::dom::windowproxy::WindowProxy;
47use crate::fetch::FetchCanceller;
48use crate::messaging::MainThreadScriptMsg;
49use crate::script_thread::ScriptThread;
50
51#[derive(Clone)]
52pub struct NavigationListener {
53    request_builder: RequestBuilder,
54    main_thread_sender: Sender<MainThreadScriptMsg>,
55    // Whether or not results are sent to the main thread. After a redirect results are no longer sent,
56    // as the main thread has already started a new request.
57    send_results_to_main_thread: Cell<bool>,
58}
59
60impl NavigationListener {
61    pub(crate) fn into_callback(self) -> BoxedFetchCallback {
62        Box::new(move |response_msg| self.notify_fetch(response_msg))
63    }
64
65    pub fn new(
66        request_builder: RequestBuilder,
67        main_thread_sender: Sender<MainThreadScriptMsg>,
68    ) -> NavigationListener {
69        NavigationListener {
70            request_builder,
71            main_thread_sender,
72            send_results_to_main_thread: Cell::new(true),
73        }
74    }
75
76    pub fn initiate_fetch(
77        self,
78        core_resource_thread: &CoreResourceThread,
79        response_init: Option<ResponseInit>,
80    ) {
81        fetch_async(
82            core_resource_thread,
83            self.request_builder.clone(),
84            response_init,
85            self.into_callback(),
86        );
87    }
88
89    fn notify_fetch(&self, message: FetchResponseMsg) {
90        // If we've already asked the main thread to redirect the response, then stop sending results
91        // for this fetch. The main thread has already replaced it.
92        if !self.send_results_to_main_thread.get() {
93            return;
94        }
95
96        // If this is a redirect, don't send any more message after this one.
97        if Self::http_redirect_metadata(&message).is_some() {
98            self.send_results_to_main_thread.set(false);
99        }
100
101        let pipeline_id = self
102            .request_builder
103            .pipeline_id
104            .expect("Navigation should always have an associated Pipeline");
105        let result = self
106            .main_thread_sender
107            .send(MainThreadScriptMsg::NavigationResponse {
108                pipeline_id,
109                message: Box::new(message),
110            });
111
112        if let Err(error) = result {
113            warn!(
114                "Failed to send network message to pipeline {:?}: {error:?}",
115                pipeline_id
116            );
117        }
118    }
119
120    pub(crate) fn http_redirect_metadata(message: &FetchResponseMsg) -> Option<&Metadata> {
121        let FetchResponseMsg::ProcessResponse(_, Ok(metadata)) = message else {
122            return None;
123        };
124
125        // Don't allow redirects for non HTTP(S) URLs.
126        let metadata = metadata.metadata();
127        if !matches!(
128            metadata.location_url,
129            Some(Ok(ref location_url)) if matches!(location_url.scheme(), "http" | "https")
130        ) {
131            return None;
132        }
133
134        Some(metadata)
135    }
136}
137
138/// A document load that is in the process of fetching the requested resource. Contains
139/// data that will need to be present when the document and frame tree entry are created,
140/// but is only easily available at initiation of the load and on a push basis (so some
141/// data will be updated according to future resize events, viewport changes, etc.)
142#[derive(JSTraceable)]
143pub(crate) struct InProgressLoad {
144    /// The pipeline which requested this load.
145    #[no_trace]
146    pub(crate) pipeline_id: PipelineId,
147    /// The browsing context being loaded into.
148    #[no_trace]
149    pub(crate) browsing_context_id: BrowsingContextId,
150    /// The top level ancestor browsing context.
151    #[no_trace]
152    pub(crate) webview_id: WebViewId,
153    /// The parent pipeline and frame type associated with this load, if any.
154    #[no_trace]
155    pub(crate) parent_info: Option<PipelineId>,
156    /// The opener, if this is an auxiliary.
157    #[no_trace]
158    pub(crate) opener: Option<BrowsingContextId>,
159    /// The current window size associated with this pipeline.
160    #[no_trace]
161    pub(crate) viewport_details: ViewportDetails,
162    /// The activity level of the document (inactive, active or fully active).
163    #[no_trace]
164    pub(crate) activity: DocumentActivity,
165    /// Window is throttled, running timers at a heavily limited rate.
166    pub(crate) throttled: bool,
167    /// Timestamp reporting the time when the browser started this load.
168    #[no_trace]
169    pub(crate) navigation_start: CrossProcessInstant,
170    /// For cancelling the fetch
171    pub(crate) canceller: FetchCanceller,
172    /// The [`LoadData`] associated with this load.
173    #[no_trace]
174    pub(crate) load_data: LoadData,
175    /// A list of URL to keep track of all the redirects that have happened during
176    /// this load.
177    #[no_trace]
178    pub(crate) url_list: Vec<ServoUrl>,
179    #[no_trace]
180    /// The [`UserContentManagerId`] associated with this load's `WebView`.
181    pub(crate) user_content_manager_id: Option<UserContentManagerId>,
182    /// The [`Theme`] to use for this page, once it loads.
183    #[no_trace]
184    pub(crate) theme: Theme,
185    /// The [`TargetSnapshotParams`] to use when creating this document.
186    #[no_trace]
187    pub(crate) target_snapshot_params: TargetSnapshotParams,
188}
189
190impl InProgressLoad {
191    /// Create a new InProgressLoad object.
192    pub(crate) fn new(new_pipeline_info: NewPipelineInfo) -> InProgressLoad {
193        let url = new_pipeline_info.load_data.url.clone();
194        InProgressLoad {
195            pipeline_id: new_pipeline_info.new_pipeline_id,
196            browsing_context_id: new_pipeline_info.browsing_context_id,
197            webview_id: new_pipeline_info.webview_id,
198            parent_info: new_pipeline_info.parent_info,
199            opener: new_pipeline_info.opener,
200            viewport_details: new_pipeline_info.viewport_details,
201            activity: DocumentActivity::FullyActive,
202            throttled: false,
203            navigation_start: CrossProcessInstant::now(),
204            canceller: Default::default(),
205            load_data: new_pipeline_info.load_data,
206            url_list: vec![url],
207            user_content_manager_id: new_pipeline_info.user_content_manager_id,
208            theme: new_pipeline_info.theme,
209            target_snapshot_params: new_pipeline_info.target_snapshot_params,
210        }
211    }
212
213    pub(crate) fn request_builder(&mut self) -> RequestBuilder {
214        let client_origin = match self.load_data.load_origin {
215            LoadOrigin::Script(ref initiator_origin) => initiator_origin.immutable().clone(),
216            _ => ImmutableOrigin::new_opaque(),
217        };
218
219        let id = self.pipeline_id;
220        let webview_id = self.webview_id;
221
222        let insecure_requests_policy = self
223            .load_data
224            .inherited_insecure_requests_policy
225            .unwrap_or(InsecureRequestsPolicy::DoNotUpgrade);
226
227        let request_client = RequestClient {
228            preloaded_resources: PreloadedResources::default(),
229            policy_container: RequestPolicyContainer::PolicyContainer(
230                self.load_data.policy_container.clone().unwrap_or_default(),
231            ),
232            origin: Origin::Origin(client_origin),
233            is_nested_browsing_context: self.parent_info.is_some(),
234            insecure_requests_policy,
235        };
236
237        let mut request_builder = RequestBuilder::new(
238            Some(webview_id),
239            UrlWithBlobClaim::from_url_without_having_claimed_blob(self.load_data.url.clone()),
240            self.load_data.referrer.clone(),
241        )
242        .method(self.load_data.method.clone())
243        .destination(self.load_data.destination)
244        .mode(RequestMode::Navigate)
245        .credentials_mode(CredentialsMode::Include)
246        .use_url_credentials(true)
247        .pipeline_id(Some(id))
248        .referrer_policy(self.load_data.referrer_policy)
249        .policy_container(self.load_data.policy_container.clone().unwrap_or_default())
250        .insecure_requests_policy(insecure_requests_policy)
251        .has_trustworthy_ancestor_origin(self.load_data.has_trustworthy_ancestor_origin)
252        .headers(self.load_data.headers.clone())
253        .body(self.load_data.data.clone())
254        .redirect_mode(RedirectMode::Manual)
255        .crash(self.load_data.crash.clone())
256        .client(request_client)
257        .url_list(self.url_list.clone());
258
259        if !request_builder.headers.contains_key(header::ACCEPT) {
260            request_builder
261                .headers
262                .insert(header::ACCEPT, DOCUMENT_ACCEPT_HEADER_VALUE);
263        }
264        set_default_accept_language(&mut request_builder.headers);
265
266        request_builder
267    }
268}
269
270/// <https://html.spec.whatwg.org/multipage/#determining-the-origin>
271pub(crate) fn determine_the_origin(
272    url: Option<&ServoUrl>,
273    sandbox_flags: SandboxingFlagSet,
274    source_origin: Option<MutableOrigin>,
275) -> MutableOrigin {
276    // Step 1. If sandboxFlags has its sandboxed origin browsing context flag set, then return a new opaque origin.
277    let is_sandboxed =
278        sandbox_flags.contains(SandboxingFlagSet::SANDBOXED_ORIGIN_BROWSING_CONTEXT_FLAG);
279    if is_sandboxed {
280        return MutableOrigin::new(ImmutableOrigin::new_opaque());
281    }
282
283    // Step 2. If url is null, then return a new opaque origin.
284    let Some(url) = url else {
285        return MutableOrigin::new(ImmutableOrigin::new_opaque());
286    };
287
288    // Step 3. If url is about:srcdoc, then:
289    if url.as_str() == "about:srcdoc" {
290        // Step 3.1 Assert: sourceOrigin is non-null.
291        let source_origin =
292            source_origin.expect("Can't have a null source origin for about:srcdoc");
293        // Step 3.2 Return sourceOrigin
294        return source_origin;
295    }
296
297    // Step 4. If url matches about:blank and sourceOrigin is non-null, then return sourceOrigin.
298    if url.as_str() == "about:blank" &&
299        let Some(source_origin) = source_origin
300    {
301        return source_origin;
302    }
303
304    // Step 5. Return url's origin.
305    MutableOrigin::new(url.origin())
306}
307
308/// <https://html.spec.whatwg.org/multipage/#navigate-fragid>
309fn navigate_to_fragment(
310    cx: &mut JSContext,
311    window: &Window,
312    url: &ServoUrl,
313    history_handling: NavigationHistoryBehavior,
314) {
315    let doc = window.Document();
316    // Step 1. Let navigation be navigable's active window's navigation API.
317    // TODO
318    // Step 2. Let destinationNavigationAPIState be navigable's active session history entry's navigation API state.
319    // TODO
320    // Step 3. If navigationAPIState is not null, then set destinationNavigationAPIState to navigationAPIState.
321    // TODO
322
323    // Step 4. Let continue be the result of firing a push/replace/reload navigate event
324    // at navigation with navigationType set to historyHandling, isSameDocument set to true,
325    // userInvolvement set to userInvolvement, sourceElement set to sourceElement,
326    // destinationURL set to url, and navigationAPIState set to destinationNavigationAPIState.
327    // TODO
328    // Step 5. If continue is false, then return.
329    // TODO
330
331    // Step 6. Let historyEntry be a new session history entry, with
332    // Step 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null.
333    // Step 8. Let history be navigable's active document's history object.
334    // Step 9. Let scriptHistoryIndex be history's index.
335    // Step 10. Let scriptHistoryLength be history's length.
336    // Step 11. If historyHandling is "push", then:
337    // Step 13. Set navigable's active session history entry to historyEntry.
338    window.send_to_constellation(ScriptToConstellationMessage::NavigatedToFragment(
339        url.clone(),
340        history_handling,
341    ));
342    // Step 12. Set navigable's active document's URL to url.
343    let old_url = doc.url();
344    doc.set_url(url.clone());
345    // Step 14. Update document for history step application given navigable's active document,
346    // historyEntry, true, scriptHistoryIndex, scriptHistoryLength, and historyHandling.
347    doc.update_document_for_history_step_application(&old_url, url);
348    // Step 15. Scroll to the fragment given navigable's active document.
349    let Some(fragment) = url.fragment() else {
350        unreachable!("Must always have a fragment");
351    };
352    doc.scroll_to_the_fragment(cx, fragment);
353    // Step 16. Let traversable be navigable's traversable navigable.
354    // TODO
355    // Step 17. Append the following session history synchronous navigation steps involving navigable to traversable:
356    // TODO
357}
358
359/// <https://html.spec.whatwg.org/multipage/#navigate>
360pub(crate) fn navigate(
361    cx: &mut JSContext,
362    window: &Window,
363    history_handling: NavigationHistoryBehavior,
364    force_reload: bool,
365    load_data: LoadData,
366) {
367    let doc = window.Document();
368
369    // Step 3. Let initiatorOriginSnapshot be sourceDocument's origin.
370    let initiator_origin_snapshot = &load_data.load_origin;
371
372    // TODO: Important re security. See https://github.com/servo/servo/issues/23373
373    // Step 5. check that the source browsing-context is "allowed to navigate" this window.
374
375    // Step 4 and 5
376    let pipeline_id = window.pipeline_id();
377    let window_proxy = window.window_proxy();
378    if let Some(active) = window_proxy.currently_active() &&
379        pipeline_id == active &&
380        doc.is_prompting_or_unloading()
381    {
382        return;
383    }
384
385    // Step 12. If historyHandling is "auto", then:
386    let history_handling = if history_handling == NavigationHistoryBehavior::Auto {
387        // Step 12.1. If url equals navigable's active document's URL, and
388        // initiatorOriginSnapshot is same origin with targetNavigable's active document's
389        // origin, then set historyHandling to "replace".
390        //
391        // Note: `targetNavigable` is not actually defined in the spec, "active document" is
392        // assumed to be the correct reference based on WPT results
393        if let LoadOrigin::Script(initiator_origin) = initiator_origin_snapshot {
394            if load_data.url == doc.url() && initiator_origin.same_origin(&*doc.origin()) {
395                NavigationHistoryBehavior::Replace
396            } else {
397                // Step 12.2. Otherwise, set historyHandling to "push".
398                NavigationHistoryBehavior::Push
399            }
400        } else {
401            // Step 12.2. Otherwise, set historyHandling to "push".
402            NavigationHistoryBehavior::Push
403        }
404    } else {
405        history_handling
406    };
407
408    // Step 13. If the navigation must be a replace given url and navigable's active
409    // document, then set historyHandling to "replace".
410    //
411    // Inlines implementation of https://html.spec.whatwg.org/multipage/#the-navigation-must-be-a-replace
412    let history_handling = if load_data.url.scheme() == "javascript" || doc.is_initial_about_blank()
413    {
414        NavigationHistoryBehavior::Replace
415    } else {
416        history_handling
417    };
418
419    // Step 14. If all of the following are true:
420    // > documentResource is null;
421    // > response is null;
422    if !force_reload
423        // > url equals navigable's active session history entry's URL with exclude fragments set to true; and
424        && load_data.url.as_url()[..Position::AfterQuery] ==
425            doc.url().as_url()[..Position::AfterQuery]
426        // > url's fragment is non-null,
427        && load_data.url.fragment().is_some()
428    {
429        // Step 14.1. Navigate to a fragment given navigable, url, historyHandling,
430        // userInvolvement, sourceElement, navigationAPIState, and navigationId.
431        let webdriver_sender = window.webdriver_load_status_sender();
432        if let Some(ref sender) = webdriver_sender {
433            let _ = sender.send(WebDriverLoadStatus::NavigationStart);
434        }
435        navigate_to_fragment(cx, window, &load_data.url, history_handling);
436        // Step 14.2. Return.
437        if let Some(sender) = webdriver_sender {
438            let _ = sender.send(WebDriverLoadStatus::NavigationStop);
439        }
440        return;
441    }
442
443    // Step 15. If navigable's parent is non-null, then set navigable's is delaying load events to true.
444    let window_proxy = window.window_proxy();
445    if window_proxy.parent().is_some() {
446        window_proxy.start_delaying_load_events_mode();
447    }
448
449    // Step 16. Let targetSnapshotParams be the result of snapshotting target
450    // snapshot params given navigable.
451    let target_snapshot_params = snapshot_target_snapshot_params(&window_proxy);
452
453    // Step 17. Invoke WebDriver BiDi navigation started with navigable
454    // and a new WebDriver BiDi navigation status whose id is navigationId,
455    // status is "pending", and url is url.
456    // TODO
457    if let Some(sender) = window.webdriver_load_status_sender() {
458        let _ = sender.send(WebDriverLoadStatus::NavigationStart);
459    }
460
461    // Step 18. If navigable's ongoing navigation is "traversal", then:
462    // TODO
463    // Step 19. Set the ongoing navigation for navigable to navigationId.
464    // TODO
465
466    // Step 20. If url's scheme is "javascript", then:
467    if load_data.url.scheme() == "javascript" {
468        // Step 20.1. Queue a global task on the navigation and traversal task source given
469        // navigable's active window to navigate to a javascript: URL given navigable, url,
470        // historyHandling, sourceSnapshotParams, initiatorOriginSnapshot, userInvolvement,
471        // cspNavigationType, initialInsertion, and navigationId.
472
473        let Some(initiator_pipeline_id) = load_data.creator_pipeline_id else {
474            unreachable!("javascript: URL navigations must have a creator pipeline");
475        };
476        let Some(initiator_window) = ScriptThread::find_window(initiator_pipeline_id) else {
477            warn!("Can't find global for navigation initiator");
478            return;
479        };
480
481        let target_window = Trusted::new(window);
482        let mut load_data = load_data;
483        let initiator_window = Trusted::new(&*initiator_window);
484        let task = task!(navigate_javascript: move |cx| {
485            // Important re security. See https://github.com/servo/servo/issues/23373
486            let target_window = target_window.root();
487            let initiator_window = initiator_window.root();
488            if ScriptThread::navigate_to_javascript_url(cx, initiator_window.upcast(), target_window.upcast(), &mut load_data, None, None) {
489                target_window
490                    .as_global_scope()
491                    .script_to_constellation_chan()
492                    .send(ScriptToConstellationMessage::LoadUrl(load_data, history_handling, target_snapshot_params))
493                    .unwrap();
494            }
495        });
496        window
497            .as_global_scope()
498            .task_manager()
499            .navigation_and_traversal_task_source()
500            .queue(task);
501        // Step 20.2. Return.
502        return;
503    }
504
505    // Step 23. In parallel, run these steps:
506    //
507    // TODO: in parallel
508
509    // Step 23.1. Let unloadPromptCanceled be the result of checking if unloading
510    // is canceled for navigable's active document's inclusive descendant navigables.
511    let unload_prompt_canceled = doc.check_if_unloading_is_cancelled(cx, false);
512    // Step 23.2. If unloadPromptCanceled is not "continue",
513    // or navigable's ongoing navigation is no longer navigationId:
514    //
515    // TODO: Check for ongoing navigation
516    if !unload_prompt_canceled {
517        // Step 23.2.1. Invoke WebDriver BiDi navigation failed with navigable
518        // and a new WebDriver BiDi navigation status whose id is navigationId,
519        // status is "canceled", and url is url.
520        // TODO
521        // Step 23.2.2. Abort these steps.
522        return;
523    }
524
525    // Step 23.9. Attempt to populate the history entry's document for historyEntry,
526    // given navigable, "navigate", sourceSnapshotParams, targetSnapshotParams,
527    // userInvolvement, navigationId, navigationParams, cspNavigationType,
528    // with allowPOST set to true and completionSteps set to the following step:
529    window.send_to_constellation(ScriptToConstellationMessage::LoadUrl(
530        load_data,
531        history_handling,
532        target_snapshot_params,
533    ));
534}
535
536/// <https://html.spec.whatwg.org/multipage/#determining-the-creation-sandboxing-flags>
537pub(crate) fn determine_creation_sandboxing_flags(
538    browsing_context: Option<&WindowProxy>,
539    element: Option<&Element>,
540) -> SandboxingFlagSet {
541    // To determine the creation sandboxing flags for a browsing context
542    // browsing context, given null or an element embedder, return the union
543    // of the flags that are present in the following sandboxing flag sets:
544    match element {
545        // If embedder is null, then: the flags set on browsing context's
546        // popup sandboxing flag set.
547        None => browsing_context
548            .and_then(|browsing_context| browsing_context.document())
549            .map(|document| document.active_sandboxing_flag_set())
550            .unwrap_or(SandboxingFlagSet::empty()),
551        Some(element) => {
552            // If embedder is an element, then: the flags set on embedder's
553            // iframe sandboxing flag set.
554            // If embedder is an element, then: the flags set on embedder's
555            // node document's active sandboxing flag set.
556            element
557                .downcast::<HTMLIFrameElement>()
558                .map(|iframe| iframe.sandboxing_flag_set())
559                .unwrap_or(SandboxingFlagSet::empty())
560                .union(element.owner_document().active_sandboxing_flag_set())
561        },
562    }
563}
564
565/// <https://html.spec.whatwg.org/multipage/#determining-the-iframe-element-referrer-policy>
566pub(crate) fn determine_iframe_element_referrer_policy(
567    element: Option<&Element>,
568) -> ReferrerPolicy {
569    // Step 1. If embedder is an iframe element, then return embedder's referrerpolicy
570    // attribute's state's corresponding keyword.
571    element
572        .and_then(|element| element.downcast::<HTMLIFrameElement>())
573        .map(|iframe| {
574            let token = iframe.ReferrerPolicy();
575            ReferrerPolicy::from(&*token.str())
576        })
577        // Step 2. Return the empty string.
578        .unwrap_or(ReferrerPolicy::EmptyString)
579}
580
581/// <https://html.spec.whatwg.org/multipage/#snapshotting-target-snapshot-params>
582pub(crate) fn snapshot_target_snapshot_params(navigable: &WindowProxy) -> TargetSnapshotParams {
583    // TODO(jdm): This doesn't work for cross-origin parent frames.
584    let container = navigable.frame_element();
585    // the result of determining the creation sandboxing flags given targetNavigable's
586    // active browsing context and targetNavigable's container
587    let sandboxing_flags = determine_creation_sandboxing_flags(Some(navigable), container);
588    // the result of determining the iframe element referrer policy given
589    // targetNavigable's container
590    let iframe_element_referrer_policy = determine_iframe_element_referrer_policy(container);
591    TargetSnapshotParams {
592        sandboxing_flags,
593        iframe_element_referrer_policy,
594    }
595}