script/dom/location.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
5use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
6use dom_struct::dom_struct;
7use net_traits::request::Referrer;
8use servo_url::{MutableOrigin, ServoUrl};
9
10use crate::dom::bindings::codegen::Bindings::LocationBinding::LocationMethods;
11use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
12use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
13use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
14use crate::dom::bindings::root::{Dom, DomRoot};
15use crate::dom::bindings::str::USVString;
16use crate::dom::document::Document;
17use crate::dom::globalscope::GlobalScope;
18use crate::dom::urlhelper::UrlHelper;
19use crate::dom::window::Window;
20use crate::script_runtime::CanGc;
21
22#[derive(PartialEq)]
23pub(crate) enum NavigationType {
24 /// The "[`Location`-object navigate][1]" steps.
25 ///
26 /// [1]: https://html.spec.whatwg.org/multipage/#location-object-navigate
27 Normal,
28
29 /// The last step of [`reload()`][1] (`reload_triggered == true`)
30 ///
31 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
32 ReloadByScript,
33
34 /// User-requested navigation (the unlabeled paragraph after
35 /// [`reload()`][1]).
36 ///
37 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
38 ReloadByConstellation,
39}
40
41#[dom_struct]
42pub(crate) struct Location {
43 reflector_: Reflector,
44 window: Dom<Window>,
45}
46
47impl Location {
48 fn new_inherited(window: &Window) -> Location {
49 Location {
50 reflector_: Reflector::new(),
51 window: Dom::from_ref(window),
52 }
53 }
54
55 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Location> {
56 reflect_dom_object(Box::new(Location::new_inherited(window)), window, can_gc)
57 }
58
59 /// <https://html.spec.whatwg.org/multipage/#location-object-navigate>
60 fn navigate_a_location(
61 &self,
62 url: ServoUrl,
63 history_handling: NavigationHistoryBehavior,
64 can_gc: CanGc,
65 ) {
66 // Step 1. Let navigable be location's relevant global object's navigable.
67 let navigable = &self.window;
68 let navigable_document = navigable.Document();
69 // Step 2. Let sourceDocument be the incumbent global object's associated Document.
70 let incumbent_global = GlobalScope::incumbent().expect("no incumbent global object");
71 let mut load_data = incumbent_global
72 .as_window()
73 .load_data_for_document(url, navigable.pipeline_id());
74 load_data.about_base_url = navigable_document.about_base_url();
75 // Step 3. If location's relevant Document is not yet completely loaded,
76 // and the incumbent global object does not have transient activation, then set historyHandling to "replace".
77 //
78 // TODO: check for transient activation
79 let history_handling = if !navigable_document.completely_loaded() {
80 NavigationHistoryBehavior::Replace
81 } else {
82 history_handling
83 };
84 // Step 4. Navigate navigable to url using sourceDocument, with exceptionsEnabled set to true and historyHandling set to historyHandling.
85 navigable.load_url(history_handling, false, load_data, can_gc);
86 }
87
88 /// Navigate the relevant `Document`'s browsing context.
89 ///
90 /// This is ostensibly an implementation of
91 /// <https://html.spec.whatwg.org/multipage/#navigate>, but the specification has
92 /// greatly deviated from our code.
93 pub(crate) fn navigate(
94 &self,
95 url: ServoUrl,
96 history_handling: NavigationHistoryBehavior,
97 navigation_type: NavigationType,
98 can_gc: CanGc,
99 ) {
100 fn incumbent_window() -> DomRoot<Window> {
101 let incumbent_global = GlobalScope::incumbent().expect("no incumbent global object");
102 DomRoot::downcast(incumbent_global).expect("global object is not a Window")
103 }
104
105 // The active document of the source browsing context used for
106 // navigation determines the request's referrer and referrer policy.
107 let source_window = match navigation_type {
108 NavigationType::ReloadByScript | NavigationType::ReloadByConstellation => {
109 // > Navigate the browsing context [...] the source browsing context
110 // > set to the browsing context being navigated.
111 DomRoot::from_ref(&*self.window)
112 },
113 NavigationType::Normal => {
114 // > 2. Let `sourceBrowsingContext` be the incumbent global object's
115 // > browsing context.
116 incumbent_window()
117 },
118 };
119 let source_document = source_window.Document();
120
121 let referrer = Referrer::ReferrerUrl(source_document.url());
122 let referrer_policy = source_document.get_referrer_policy();
123
124 // <https://html.spec.whatwg.org/multipage/#navigate>
125 // > Let `incumbentNavigationOrigin` be the origin of the incumbent
126 // > settings object, or if no script was involved, the origin of the
127 // > node document of the element that initiated the navigation.
128 let navigation_origin_window = match navigation_type {
129 NavigationType::Normal | NavigationType::ReloadByScript => incumbent_window(),
130 NavigationType::ReloadByConstellation => DomRoot::from_ref(&*self.window),
131 };
132 let (load_origin, creator_pipeline_id) = (
133 navigation_origin_window.origin().snapshot(),
134 Some(navigation_origin_window.pipeline_id()),
135 );
136
137 // Is `historyHandling` `reload`?
138 let reload_triggered = match navigation_type {
139 NavigationType::ReloadByScript | NavigationType::ReloadByConstellation => true,
140 NavigationType::Normal => false,
141 };
142
143 // Initiate navigation
144 // TODO: rethrow exceptions, set exceptions enabled flag.
145 let load_data = LoadData::new(
146 LoadOrigin::Script(load_origin),
147 url,
148 source_document.about_base_url(),
149 creator_pipeline_id,
150 referrer,
151 referrer_policy,
152 None, // Top navigation doesn't inherit secure context
153 Some(source_document.insecure_requests_policy()),
154 source_document.has_trustworthy_ancestor_origin(),
155 source_document.creation_sandboxing_flag_set_considering_parent_iframe(),
156 );
157 self.window
158 .load_url(history_handling, reload_triggered, load_data, can_gc);
159 }
160
161 /// Get if this `Location`'s [relevant `Document`][1] is non-null.
162 ///
163 /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document
164 fn has_document(&self) -> bool {
165 // <https://html.spec.whatwg.org/multipage/#relevant-document>
166 //
167 // > A `Location` object has an associated relevant `Document`, which is
168 // > this `Location` object's relevant global object's browsing
169 // > context's active document, if this `Location` object's relevant
170 // > global object's browsing context is non-null, and null otherwise.
171 self.window.Document().browsing_context().is_some()
172 }
173
174 /// Get this `Location` object's [relevant `Document`][1], or
175 /// `Err(Error::Security(None))` if it's non-null and its origin is not same
176 /// origin-domain with the entry setting object's origin.
177 ///
178 /// In the specification's terms:
179 ///
180 /// 1. If this `Location` object's relevant `Document` is null, then return
181 /// null.
182 ///
183 /// 2. If this `Location` object's relevant `Document`'s origin is not same
184 /// origin-domain with the entry settings object's origin, then throw a
185 /// "`SecurityError`" `DOMException`.
186 ///
187 /// 3. Return this `Location` object's relevant `Document`.
188 ///
189 /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document
190 fn document_if_same_origin(&self) -> Fallible<Option<DomRoot<Document>>> {
191 // <https://html.spec.whatwg.org/multipage/#relevant-document>
192 //
193 // > A `Location` object has an associated relevant `Document`, which is
194 // > this `Location` object's relevant global object's browsing
195 // > context's active document, if this `Location` object's relevant
196 // > global object's browsing context is non-null, and null otherwise.
197 if let Some(window_proxy) = self.window.Document().browsing_context() {
198 // `Location`'s many other operations:
199 //
200 // > If this `Location` object's relevant `Document` is non-null and
201 // > its origin is not same origin-domain with the entry settings
202 // > object's origin, then throw a "SecurityError" `DOMException`.
203 //
204 // FIXME: We should still return the active document if it's same
205 // origin but not fully active. `WindowProxy::document`
206 // currently returns `None` in this case.
207 if let Some(document) = window_proxy.document().filter(|document| {
208 self.entry_settings_object()
209 .origin()
210 .same_origin_domain(document.origin())
211 }) {
212 Ok(Some(document))
213 } else {
214 Err(Error::Security(None))
215 }
216 } else {
217 // The browsing context is null
218 Ok(None)
219 }
220 }
221
222 /// Get this `Location` object's [relevant url][1] or
223 /// `Err(Error::Security(None))` if the [relevant `Document`][2] if it's non-null
224 /// and its origin is not same origin-domain with the entry setting object's
225 /// origin.
226 ///
227 /// [1]: https://html.spec.whatwg.org/multipage/#concept-location-url
228 /// [2]: https://html.spec.whatwg.org/multipage/#relevant-document
229 fn get_url_if_same_origin(&self) -> Fallible<ServoUrl> {
230 Ok(if let Some(document) = self.document_if_same_origin()? {
231 document.url()
232 } else {
233 ServoUrl::parse("about:blank").unwrap()
234 })
235 }
236
237 fn entry_settings_object(&self) -> DomRoot<GlobalScope> {
238 GlobalScope::entry()
239 }
240
241 /// The common algorithm for `Location`'s setters and `Location::Assign`.
242 #[inline]
243 fn setter_common(
244 &self,
245 f: impl FnOnce(ServoUrl) -> Fallible<Option<ServoUrl>>,
246 can_gc: CanGc,
247 ) -> ErrorResult {
248 // Step 1: If this Location object's relevant Document is null, then return.
249 // Step 2: If this Location object's relevant Document's origin is not
250 // same origin-domain with the entry settings object's origin, then
251 // throw a "SecurityError" DOMException.
252 if let Some(document) = self.document_if_same_origin()? {
253 // Step 3: Let copyURL be a copy of this Location object's url.
254 // Step 4: Assign the result of running f(copyURL) to copyURL.
255 if let Some(copy_url) = f(document.url())? {
256 // Step 5: Terminate these steps if copyURL is null.
257 // Step 6: Location-object navigate to copyURL.
258 self.navigate(
259 copy_url,
260 NavigationHistoryBehavior::Push,
261 NavigationType::Normal,
262 can_gc,
263 );
264 }
265 }
266 Ok(())
267 }
268
269 /// Perform a user-requested reload (the unlabeled paragraph after
270 /// [`reload()`][1]).
271 ///
272 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
273 pub(crate) fn reload_without_origin_check(&self, can_gc: CanGc) {
274 // > When a user requests that the active document of a browsing context
275 // > be reloaded through a user interface element, the user agent should
276 // > navigate the browsing context to the same resource as that
277 // > `Document`, with `historyHandling` set to "reload".
278 let url = self.window.get_url();
279 self.navigate(
280 url,
281 NavigationHistoryBehavior::Replace,
282 NavigationType::ReloadByConstellation,
283 can_gc,
284 );
285 }
286
287 #[expect(dead_code)]
288 pub(crate) fn origin(&self) -> &MutableOrigin {
289 self.window.origin()
290 }
291}
292
293impl LocationMethods<crate::DomTypeHolder> for Location {
294 /// <https://html.spec.whatwg.org/multipage/#dom-location-assign>
295 fn Assign(&self, url: USVString, can_gc: CanGc) -> ErrorResult {
296 self.setter_common(
297 |_copy_url| {
298 // Step 3: Parse url relative to the entry settings object. If that failed,
299 // throw a "SyntaxError" DOMException.
300 let base_url = self.entry_settings_object().api_base_url();
301 let url = match base_url.join(&url.0) {
302 Ok(url) => url,
303 Err(_) => return Err(Error::Syntax(None)),
304 };
305
306 Ok(Some(url))
307 },
308 can_gc,
309 )
310 }
311
312 /// <https://html.spec.whatwg.org/multipage/#dom-location-reload>
313 fn Reload(&self, can_gc: CanGc) -> ErrorResult {
314 let url = self.get_url_if_same_origin()?;
315 self.navigate(
316 url,
317 NavigationHistoryBehavior::Replace,
318 NavigationType::ReloadByScript,
319 can_gc,
320 );
321 Ok(())
322 }
323
324 /// <https://html.spec.whatwg.org/multipage/#dom-location-replace>
325 fn Replace(&self, url: USVString, can_gc: CanGc) -> ErrorResult {
326 // Step 1: If this Location object's relevant Document is null, then return.
327 if self.has_document() {
328 // Step 2. Let urlRecord be the result of encoding-parsing a URL given url, relative to the entry settings object.
329 let base_url = self.entry_settings_object().api_base_url();
330 let url = match base_url.join(&url.0) {
331 Ok(url) => url,
332 // Step 3. If urlRecord is failure, then throw a "SyntaxError" DOMException.
333 Err(_) => return Err(Error::Syntax(None)),
334 };
335 // Step 4. Location-object navigate this to urlRecord given "replace".
336 self.navigate_a_location(url, NavigationHistoryBehavior::Replace, can_gc);
337 }
338 Ok(())
339 }
340
341 /// <https://html.spec.whatwg.org/multipage/#dom-location-hash>
342 fn GetHash(&self) -> Fallible<USVString> {
343 Ok(UrlHelper::Hash(&self.get_url_if_same_origin()?))
344 }
345
346 /// <https://html.spec.whatwg.org/multipage/#dom-location-hash>
347 fn SetHash(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
348 // Step 1. If this's relevant Document is null, then return.
349 if self.has_document() {
350 // Step 2. If this's relevant Document's origin is not same origin-domain
351 // with the entry settings object's origin, then throw a "SecurityError" DOMException.
352 // Step 3. Let copyURL be a copy of this's url.
353 let mut copy_url = self.get_url_if_same_origin()?;
354 // Step 4. Let thisURLFragment be copyURL's fragment if it is non-null; otherwise the empty string.
355 let this_url_fragment = copy_url.fragment().map(str::to_owned).unwrap_or_default();
356 // Step 6. Set copyURL's fragment to the empty string.
357 // Step 7. Basic URL parse input, with copyURL as url and fragment state as state override.
358 let input = &value.0;
359 // Note that if the hash is the empty string, we shouldn't then set the fragment to `None`.
360 // That's because the empty string is a valid hash target and should then scroll to the
361 // top of the document. Therefore, we don't use `UrlHelpers::SetHash` here, which would
362 // set it to `None`.
363 copy_url.set_fragment(match input {
364 // Step 5. Let input be the given value with a single leading "#" removed, if any.
365 _ if input.starts_with('#') => Some(&input[1..]),
366 _ => Some(input),
367 });
368 // Step 8. If copyURL's fragment is thisURLFragment, then return.
369 if copy_url.fragment() != Some(&this_url_fragment) {
370 // Step 9. Location-object navigate this to copyURL.
371 self.navigate_a_location(copy_url, NavigationHistoryBehavior::Auto, can_gc);
372 }
373 }
374 Ok(())
375 }
376
377 /// <https://html.spec.whatwg.org/multipage/#dom-location-host>
378 fn GetHost(&self) -> Fallible<USVString> {
379 Ok(UrlHelper::Host(&self.get_url_if_same_origin()?))
380 }
381
382 /// <https://html.spec.whatwg.org/multipage/#dom-location-host>
383 fn SetHost(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
384 self.setter_common(
385 |mut copy_url| {
386 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
387 if copy_url.cannot_be_a_base() {
388 return Ok(None);
389 }
390
391 // Step 5: Basic URL parse the given value, with copyURL as url and host state
392 // as state override.
393 let _ = copy_url.as_mut_url().set_host(Some(&value.0));
394
395 Ok(Some(copy_url))
396 },
397 can_gc,
398 )
399 }
400
401 /// <https://html.spec.whatwg.org/multipage/#dom-location-origin>
402 fn GetOrigin(&self) -> Fallible<USVString> {
403 Ok(UrlHelper::Origin(&self.get_url_if_same_origin()?))
404 }
405
406 /// <https://html.spec.whatwg.org/multipage/#dom-location-hostname>
407 fn GetHostname(&self) -> Fallible<USVString> {
408 Ok(UrlHelper::Hostname(&self.get_url_if_same_origin()?))
409 }
410
411 /// <https://html.spec.whatwg.org/multipage/#dom-location-hostname>
412 fn SetHostname(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
413 self.setter_common(
414 |mut copy_url| {
415 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
416 if copy_url.cannot_be_a_base() {
417 return Ok(None);
418 }
419
420 // Step 5: Basic URL parse the given value, with copyURL as url and hostname
421 // state as state override.
422 let _ = copy_url.as_mut_url().set_host(Some(&value.0));
423
424 Ok(Some(copy_url))
425 },
426 can_gc,
427 )
428 }
429
430 /// <https://html.spec.whatwg.org/multipage/#dom-location-href>
431 fn GetHref(&self) -> Fallible<USVString> {
432 Ok(UrlHelper::Href(&self.get_url_if_same_origin()?))
433 }
434
435 /// <https://html.spec.whatwg.org/multipage/#dom-location-href>
436 fn SetHref(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
437 // Step 1. If this's relevant Document is null, then return.
438 if self.has_document() {
439 // Note: no call to self.check_same_origin_domain()
440 // Step 2: Let url be the result of encoding-parsing a URL given the given value, relative to the entry settings object.
441 let base_url = self.entry_settings_object().api_base_url();
442 let url = match base_url.join(&value.0) {
443 Ok(url) => url,
444 // Step 3: If url is failure, then throw a "SyntaxError" DOMException.
445 Err(e) => return Err(Error::Syntax(Some(format!("Couldn't parse URL: {}", e)))),
446 };
447 // Step 4: Location-object navigate this to url.
448 self.navigate_a_location(url, NavigationHistoryBehavior::Auto, can_gc);
449 }
450 Ok(())
451 }
452
453 /// <https://html.spec.whatwg.org/multipage/#dom-location-pathname>
454 fn GetPathname(&self) -> Fallible<USVString> {
455 Ok(UrlHelper::Pathname(&self.get_url_if_same_origin()?))
456 }
457
458 /// <https://html.spec.whatwg.org/multipage/#dom-location-pathname>
459 fn SetPathname(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
460 self.setter_common(
461 |mut copy_url| {
462 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
463 if copy_url.cannot_be_a_base() {
464 return Ok(None);
465 }
466
467 // Step 5: Set copyURL's path to the empty list.
468 // Step 6: Basic URL parse the given value, with copyURL as url and path
469 // start state as state override.
470 copy_url.as_mut_url().set_path(&value.0);
471
472 Ok(Some(copy_url))
473 },
474 can_gc,
475 )
476 }
477
478 /// <https://html.spec.whatwg.org/multipage/#dom-location-port>
479 fn GetPort(&self) -> Fallible<USVString> {
480 Ok(UrlHelper::Port(&self.get_url_if_same_origin()?))
481 }
482
483 /// <https://html.spec.whatwg.org/multipage/#dom-location-port>
484 fn SetPort(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
485 self.setter_common(
486 |mut copy_url| {
487 // Step 4: If copyURL cannot have a username/password/port, then return.
488 // https://url.spec.whatwg.org/#cannot-have-a-username-password-port
489 if copy_url.host().is_none() ||
490 copy_url.cannot_be_a_base() ||
491 copy_url.scheme() == "file"
492 {
493 return Ok(None);
494 }
495
496 // Step 5: If the given value is the empty string, then set copyURL's
497 // port to null.
498 // Step 6: Otherwise, basic URL parse the given value, with copyURL as url
499 // and port state as state override.
500 let _ = url::quirks::set_port(copy_url.as_mut_url(), &value.0);
501
502 Ok(Some(copy_url))
503 },
504 can_gc,
505 )
506 }
507
508 /// <https://html.spec.whatwg.org/multipage/#dom-location-protocol>
509 fn GetProtocol(&self) -> Fallible<USVString> {
510 Ok(UrlHelper::Protocol(&self.get_url_if_same_origin()?))
511 }
512
513 /// <https://html.spec.whatwg.org/multipage/#dom-location-protocol>
514 fn SetProtocol(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
515 self.setter_common(
516 |mut copy_url| {
517 // Step 4: Let possibleFailure be the result of basic URL parsing the given
518 // value, followed by ":", with copyURL as url and scheme start state as
519 // state override.
520 let scheme = match value.0.find(':') {
521 Some(position) => &value.0[..position],
522 None => &value.0,
523 };
524
525 if copy_url.as_mut_url().set_scheme(scheme).is_err() {
526 // Step 5: If possibleFailure is failure, then throw a "SyntaxError" DOMException.
527 return Err(Error::Syntax(None));
528 }
529
530 // Step 6: If copyURL's scheme is not an HTTP(S) scheme, then terminate these steps.
531 if !copy_url.scheme().eq_ignore_ascii_case("http") &&
532 !copy_url.scheme().eq_ignore_ascii_case("https")
533 {
534 return Ok(None);
535 }
536
537 Ok(Some(copy_url))
538 },
539 can_gc,
540 )
541 }
542
543 /// <https://html.spec.whatwg.org/multipage/#dom-location-search>
544 fn GetSearch(&self) -> Fallible<USVString> {
545 Ok(UrlHelper::Search(&self.get_url_if_same_origin()?))
546 }
547
548 /// <https://html.spec.whatwg.org/multipage/#dom-location-search>
549 fn SetSearch(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
550 self.setter_common(
551 |mut copy_url| {
552 // Step 4: If the given value is the empty string, set copyURL's query to null.
553 // Step 5: Otherwise, run these substeps:
554 // 1. Let input be the given value with a single leading "?" removed, if any.
555 // 2. Set copyURL's query to the empty string.
556 // 3. Basic URL parse input, with copyURL as url and query state as state
557 // override, and the relevant Document's document's character encoding as
558 // encoding override.
559 copy_url.as_mut_url().set_query(match value.0.as_str() {
560 "" => None,
561 _ if value.0.starts_with('?') => Some(&value.0[1..]),
562 _ => Some(&value.0),
563 });
564
565 Ok(Some(copy_url))
566 },
567 can_gc,
568 )
569 }
570}