runtime/globals/url/
mod.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::cmp::Ordering;
8
9use ion::class::{NativeObject as _, Reflector};
10use ion::function::Opt;
11use ion::{ClassDefinition as _, Context, Error, FromValue, Local, Object, Result, js_class};
12use mozjs::jsapi::{Heap, JSObject};
13pub use search_params::URLSearchParams;
14use url::Url;
15use uuid::Uuid;
16
17use crate::globals::file::Blob;
18use crate::runtime::ContextExt as _;
19
20mod search_params;
21
22const BLOB_ORIGIN: &str = "spiderfire";
23
24pub fn parse_uuid_from_url_path(url: &Url) -> Option<Uuid> {
25	let path = url.path().get((BLOB_ORIGIN.len() + 1)..).filter(|path| path.len() == 36)?;
26	Uuid::try_parse(path).ok()
27}
28
29#[derive(Default, FromValue)]
30pub struct FormatOptions {
31	#[ion(default)]
32	auth: bool,
33	#[ion(default)]
34	fragment: bool,
35	#[ion(default)]
36	search: bool,
37}
38
39#[js_class]
40pub struct URL {
41	reflector: Reflector,
42	#[trace(no_trace)]
43	pub(crate) url: Url,
44	search_params: Box<Heap<*mut JSObject>>,
45}
46
47impl URL {
48	fn search_params(&mut self, cx: &Context) -> &mut URLSearchParams {
49		let search_params = Object::from(unsafe { Local::from_heap(&self.search_params) });
50		URLSearchParams::get_mut_private(cx, &search_params).unwrap()
51	}
52}
53
54#[js_class]
55impl URL {
56	#[ion(constructor)]
57	pub fn constructor(#[ion(this)] this: &Object, cx: &Context, input: String, Opt(base): Opt<String>) -> Result<URL> {
58		let base = base.as_ref().and_then(|base| Url::parse(base).ok());
59		let url = Url::options()
60			.base_url(base.as_ref())
61			.parse(&input)
62			.map_err(|error| Error::new(error.to_string(), None))?;
63
64		let search_params = Heap::boxed(URLSearchParams::new_object(
65			cx,
66			URLSearchParams::from_url(&url, this.handle().get()),
67		));
68
69		Ok(URL {
70			reflector: Reflector::default(),
71			url,
72			search_params,
73		})
74	}
75
76	#[ion(name = "canParse")]
77	pub fn can_parse(input: String, Opt(base): Opt<String>) -> bool {
78		let base = base.as_ref().and_then(|base| Url::parse(base).ok());
79		Url::options().base_url(base.as_ref()).parse(&input).is_ok()
80	}
81
82	pub fn parse(cx: &Context, input: String, Opt(base): Opt<String>) -> Option<*mut JSObject> {
83		let base = base.as_ref().and_then(|base| Url::parse(base).ok());
84		let url = Url::options().base_url(base.as_ref()).parse(&input).ok()?;
85
86		let url_object = URL::new_raw_object(cx);
87		let search_params = Heap::boxed(URLSearchParams::new_object(
88			cx,
89			URLSearchParams::from_url(&url, url_object),
90		));
91
92		unsafe {
93			URL::set_private(
94				url_object,
95				Box::new(URL {
96					reflector: Reflector::default(),
97					url,
98					search_params,
99				}),
100			);
101		}
102
103		Some(url_object)
104	}
105
106	#[ion(name = "createObjectURL")]
107	pub fn create_object_url(cx: &Context, blob: &Blob) -> String {
108		let uuid = Uuid::new_v4();
109		unsafe {
110			cx.get_private().blob_store.insert(uuid, Heap::boxed(blob.reflector().get()));
111		}
112		format!("blob:{BLOB_ORIGIN}/{}", uuid.hyphenated())
113	}
114
115	#[ion(name = "revokeObjectURL")]
116	pub fn revoke_object_url(cx: &Context, url: String) {
117		if let Some(uuid) = Url::parse(&url).ok().and_then(|url| parse_uuid_from_url_path(&url)) {
118			unsafe {
119				cx.get_private().blob_store.remove(&uuid);
120			}
121		}
122	}
123
124	pub fn format(&self, Opt(options): Opt<FormatOptions>) -> Result<String> {
125		let mut url = self.url.clone();
126
127		let options = options.unwrap_or_default();
128		if !options.auth {
129			url.set_username("").map_err(|_| Error::new("Invalid Url", None))?;
130		}
131		if !options.fragment {
132			url.set_fragment(None);
133		}
134		if !options.search {
135			url.set_query(None);
136		}
137
138		Ok(url.to_string())
139	}
140
141	#[ion(name = "toString", alias = ["toJSON"])]
142	#[expect(clippy::inherent_to_string)]
143	pub fn to_string(&self) -> String {
144		self.url.to_string()
145	}
146
147	#[ion(get)]
148	pub fn get_href(&self) -> String {
149		self.url.to_string()
150	}
151
152	#[ion(set)]
153	pub fn set_href(&mut self, cx: &Context, input: String) -> Result<()> {
154		match Url::parse(&input) {
155			Ok(url) => {
156				self.search_params(cx).set_pairs_from_url(&url);
157				self.url = url;
158				Ok(())
159			}
160			Err(error) => Err(Error::new(error.to_string(), None)),
161		}
162	}
163
164	#[ion(get)]
165	pub fn get_protocol(&self) -> String {
166		String::from(self.url.scheme())
167	}
168
169	#[ion(set)]
170	pub fn set_protocol(&mut self, protocol: String) -> Result<()> {
171		self.url.set_scheme(&protocol).map_err(|_| Error::new("Invalid Protocol", None))
172	}
173
174	#[ion(get)]
175	pub fn get_host(&self) -> String {
176		self.url
177			.host_str()
178			.map(|host| {
179				if let Some(port) = self.url.port() {
180					format!("{host}:{port}")
181				} else {
182					String::from(host)
183				}
184			})
185			.unwrap_or_default()
186	}
187
188	#[ion(set)]
189	pub fn set_host(&mut self, host: String) -> Result<()> {
190		let segments: Vec<&str> = host.split(':').collect();
191		let (host, port) = match segments.len().cmp(&2) {
192			Ordering::Less => Ok((segments[0], None)),
193			Ordering::Greater => Err(Error::new("Invalid Host", None)),
194			Ordering::Equal => {
195				let port = match segments[1].parse::<u16>() {
196					Ok(port) => Ok(port),
197					Err(error) => Err(Error::new(error.to_string(), None)),
198				}?;
199				Ok((segments[0], Some(port)))
200			}
201		}?;
202
203		self.url.set_host(Some(host))?;
204		self.url.set_port(port).map_err(|_| Error::new("Invalid Url", None))
205	}
206
207	#[ion(get)]
208	pub fn get_hostname(&self) -> String {
209		self.url.host_str().map(String::from).unwrap_or_default()
210	}
211
212	#[ion(set)]
213	pub fn set_hostname(&mut self, hostname: String) -> Result<()> {
214		self.url.set_host(Some(&hostname)).map_err(|error| Error::new(error.to_string(), None))
215	}
216
217	#[ion(get)]
218	pub fn get_origin(&self) -> String {
219		self.url.origin().ascii_serialization()
220	}
221
222	#[ion(get)]
223	pub fn get_port(&self) -> String {
224		self.url.port_or_known_default().map(|port| port.to_string()).unwrap_or_default()
225	}
226
227	#[ion(set)]
228	pub fn set_port(&mut self, port: String) -> Result<()> {
229		let port = if port.is_empty() { None } else { Some(port.parse()?) };
230		self.url.set_port(port).map_err(|_| Error::new("Invalid Port", None))
231	}
232
233	#[ion(get)]
234	pub fn get_pathname(&self) -> String {
235		String::from(self.url.path())
236	}
237
238	#[ion(set)]
239	pub fn set_pathname(&mut self, path: String) -> Result<()> {
240		self.url.set_path(&path);
241		Ok(())
242	}
243
244	#[ion(get)]
245	pub fn get_username(&self) -> String {
246		String::from(self.url.username())
247	}
248
249	#[ion(set)]
250	pub fn set_username(&mut self, username: String) -> Result<()> {
251		self.url.set_username(&username).map_err(|_| Error::new("Invalid Url", None))
252	}
253
254	#[ion(get)]
255	pub fn get_password(&self) -> Option<String> {
256		self.url.password().map(String::from)
257	}
258
259	#[ion(set)]
260	pub fn set_password(&mut self, password: String) -> Result<()> {
261		self.url.set_password(Some(&password)).map_err(|_| Error::new("Invalid Url", None))
262	}
263
264	#[ion(get)]
265	pub fn get_search(&self) -> String {
266		self.url.query().map(|search| format!("?{search}")).unwrap_or_default()
267	}
268
269	#[ion(set)]
270	pub fn set_search(&mut self, cx: &Context, search: String) {
271		if search.is_empty() {
272			self.url.set_query(None);
273		} else {
274			self.url.set_query(Some(&search));
275		}
276
277		self.search_params(cx).pairs = self.url.query_pairs().into_owned().collect();
278	}
279
280	#[ion(get)]
281	pub fn get_hash(&self) -> String {
282		self.url.fragment().map(|hash| format!("#{hash}")).unwrap_or_default()
283	}
284
285	#[ion(set)]
286	pub fn set_hash(&mut self, hash: String) {
287		self.url.set_fragment(Some(&*hash).filter(|hash| !hash.is_empty()));
288	}
289
290	#[ion(get)]
291	pub fn get_search_params(&self) -> *mut JSObject {
292		self.search_params.get()
293	}
294}
295
296pub fn define(cx: &Context, global: &Object) -> bool {
297	URL::init_class(cx, global).0 && URLSearchParams::init_class(cx, global).0
298}