runtime/globals/url/
search_params.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 form_urlencoded::{Serializer, parse};
8use ion::class::Reflector;
9use ion::conversions::{FromValue, ToValue as _};
10use ion::function::Opt;
11use ion::symbol::WellKnownSymbolCode;
12use ion::{
13	ClassDefinition as _, Context, Error, ErrorKind, JSIterator, Local, Object, OwnedKey, Result, Value, js_class,
14};
15use mozjs::jsapi::{Heap, JSObject};
16use url::Url;
17
18use crate::globals::url::URL;
19
20pub struct URLSearchParamsInit(Vec<(String, String)>);
21
22impl<'cx> FromValue<'cx> for URLSearchParamsInit {
23	type Config = ();
24	fn from_value(cx: &'cx Context, value: &Value, strict: bool, _: ()) -> Result<URLSearchParamsInit> {
25		if let Ok(vec) = <Vec<Value>>::from_value(cx, value, strict, ()) {
26			let entries = vec
27				.iter()
28				.map(|value| {
29					let vec = <Vec<String>>::from_value(cx, value, strict, ())?;
30					let boxed: Box<[String; 2]> = vec
31						.try_into()
32						.map_err(|_| Error::new("Expected Search Parameter Entry with Length 2", ErrorKind::Type))?;
33					let [name, value] = *boxed;
34					Ok((name, value))
35				})
36				.collect::<Result<_>>()?;
37			Ok(URLSearchParamsInit(entries))
38		} else if let Ok(object) = Object::from_value(cx, value, strict, ()) {
39			let vec = object
40				.iter(cx, None)
41				.filter_map(|(key, value)| {
42					let value = match value.and_then(|v| String::from_value(cx, &v, strict, ())) {
43						Ok(v) => v,
44						Err(e) => return Some(Err(e)),
45					};
46					match key.to_owned_key(cx) {
47						Ok(OwnedKey::Int(i)) => Some(Ok((i.to_string(), value))),
48						Ok(OwnedKey::String(key)) => Some(Ok((key, value))),
49						Err(e) => Some(Err(e)),
50						_ => None,
51					}
52				})
53				.collect::<Result<_>>()?;
54			Ok(URLSearchParamsInit(vec))
55		} else if let Ok(string) = String::from_value(cx, value, strict, ()) {
56			let string = string.strip_prefix('?').unwrap_or(&string);
57			Ok(URLSearchParamsInit(parse(string.as_bytes()).into_owned().collect()))
58		} else {
59			Err(Error::new("Invalid Search Params Initialiser", ErrorKind::Type))
60		}
61	}
62}
63
64#[js_class]
65pub struct URLSearchParams {
66	reflector: Reflector,
67	pub(super) pairs: Vec<(String, String)>,
68	url: Option<Heap<*mut JSObject>>,
69}
70
71impl URLSearchParams {
72	pub(super) fn from_url(url: &Url, url_object: *mut JSObject) -> Box<URLSearchParams> {
73		let search_params = Box::new(URLSearchParams {
74			reflector: Reflector::default(),
75			pairs: url.query_pairs().into_owned().collect(),
76			url: Some(Heap::default()),
77		});
78		if let Some(url) = search_params.url.as_ref() {
79			url.set(url_object);
80		}
81		search_params
82	}
83
84	pub(super) fn set_pairs_from_url(&mut self, url: &Url) {
85		self.pairs = url.query_pairs().into_owned().collect();
86	}
87
88	pub fn pairs(&self) -> &[(String, String)] {
89		&self.pairs
90	}
91}
92
93#[js_class]
94impl URLSearchParams {
95	#[ion(constructor)]
96	pub fn constructor(Opt(init): Opt<URLSearchParamsInit>) -> URLSearchParams {
97		let pairs = init.map(|init| init.0).unwrap_or_default();
98		URLSearchParams {
99			reflector: Reflector::default(),
100			pairs,
101			url: None,
102		}
103	}
104
105	#[ion(get)]
106	pub fn get_size(&self) -> i32 {
107		self.pairs.len() as i32
108	}
109
110	pub fn append(&mut self, name: String, value: String) {
111		self.pairs.push((name, value));
112		self.update();
113	}
114
115	pub fn delete(&mut self, name: String, Opt(value): Opt<String>) {
116		if let Some(value) = value {
117			self.pairs.retain(|(k, v)| k != &name || v != &value);
118		} else {
119			self.pairs.retain(|(k, _)| k != &name);
120		}
121		self.update();
122	}
123
124	pub fn get(&self, key: String) -> Option<String> {
125		self.pairs.iter().find(|(k, _)| k == &key).map(|(_, v)| v.clone())
126	}
127
128	#[ion(name = "getAll")]
129	pub fn get_all(&self, key: String) -> Vec<String> {
130		self.pairs.iter().filter(|(k, _)| k == &key).map(|(_, v)| v.clone()).collect()
131	}
132
133	pub fn has(&self, key: String, Opt(value): Opt<String>) -> bool {
134		if let Some(value) = value {
135			self.pairs.iter().any(|(k, v)| k == &key && v == &value)
136		} else {
137			self.pairs.iter().any(|(k, _)| k == &key)
138		}
139	}
140
141	pub fn set(&mut self, key: String, value: String) {
142		let mut i = 0;
143		let mut index = None;
144
145		self.pairs.retain(|(k, _)| {
146			if index.is_none() {
147				if k == &key {
148					index = Some(i);
149				} else {
150					i += 1;
151				}
152				true
153			} else {
154				k != &key
155			}
156		});
157
158		match index {
159			Some(index) => self.pairs[index].1 = value,
160			None => self.pairs.push((key, value)),
161		}
162
163		self.update();
164	}
165
166	pub fn sort(&mut self) {
167		self.pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
168		self.update();
169	}
170
171	#[ion(name = "toString")]
172	#[expect(clippy::inherent_to_string)]
173	pub fn to_string(&self) -> String {
174		Serializer::new(String::new()).extend_pairs(&*self.pairs).finish()
175	}
176
177	fn update(&mut self) {
178		if let Some(url) = &self.url {
179			let url = Object::from(unsafe { Local::from_heap(url) });
180			let url = unsafe { URL::get_mut_private_unchecked(&url) };
181			if self.pairs.is_empty() {
182				url.url.set_query(None);
183			} else {
184				url.url.query_pairs_mut().clear().extend_pairs(&self.pairs);
185			}
186		}
187	}
188
189	#[ion(name = WellKnownSymbolCode::Iterator)]
190	pub fn iterator(cx: &Context, #[ion(this)] this: &Object) -> ion::Iterator {
191		let thisv = this.as_value(cx);
192		ion::Iterator::new(SearchParamsIterator::default(), &thisv)
193	}
194}
195
196#[derive(Default)]
197pub struct SearchParamsIterator(usize);
198
199impl JSIterator for SearchParamsIterator {
200	fn next_value<'cx>(&mut self, cx: &'cx Context, private: &Value<'cx>) -> Option<Value<'cx>> {
201		let object = private.to_object(cx);
202		let search_params = URLSearchParams::get_private(cx, &object).unwrap();
203		let pair = search_params.pairs.get(self.0);
204		pair.map(move |(k, v)| {
205			self.0 += 1;
206			[k, v].as_value(cx)
207		})
208	}
209}