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