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