runtime/globals/url/
mod.rs1use 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}