1use 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}