Serenity Operating System
1/*
2 * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/QuickSort.h>
8#include <AK/StringBuilder.h>
9#include <AK/Utf8View.h>
10#include <LibWeb/Bindings/ExceptionOrUtils.h>
11#include <LibWeb/Bindings/Intrinsics.h>
12#include <LibWeb/URL/URL.h>
13#include <LibWeb/URL/URLSearchParams.h>
14
15namespace Web::URL {
16
17URLSearchParams::URLSearchParams(JS::Realm& realm, Vector<QueryParam> list)
18 : PlatformObject(realm)
19 , m_list(move(list))
20{
21}
22
23URLSearchParams::~URLSearchParams() = default;
24
25JS::ThrowCompletionOr<void> URLSearchParams::initialize(JS::Realm& realm)
26{
27 MUST_OR_THROW_OOM(Base::initialize(realm));
28 set_prototype(&Bindings::ensure_web_prototype<Bindings::URLSearchParamsPrototype>(realm, "URLSearchParams"));
29
30 return {};
31}
32
33void URLSearchParams::visit_edges(Cell::Visitor& visitor)
34{
35 Base::visit_edges(visitor);
36 visitor.visit(m_url);
37}
38
39ErrorOr<String> url_encode(Vector<QueryParam> const& pairs, AK::URL::PercentEncodeSet percent_encode_set)
40{
41 StringBuilder builder;
42 for (size_t i = 0; i < pairs.size(); ++i) {
43 TRY(builder.try_append(AK::URL::percent_encode(pairs[i].name, percent_encode_set, AK::URL::SpaceAsPlus::Yes)));
44 TRY(builder.try_append('='));
45 TRY(builder.try_append(AK::URL::percent_encode(pairs[i].value, percent_encode_set, AK::URL::SpaceAsPlus::Yes)));
46 if (i != pairs.size() - 1)
47 TRY(builder.try_append('&'));
48 }
49 return builder.to_string();
50}
51
52ErrorOr<Vector<QueryParam>> url_decode(StringView input)
53{
54 // 1. Let sequences be the result of splitting input on 0x26 (&).
55 auto sequences = input.split_view('&');
56
57 // 2. Let output be an initially empty list of name-value tuples where both name and value hold a string.
58 Vector<QueryParam> output;
59
60 // 3. For each byte sequence bytes in sequences:
61 for (auto bytes : sequences) {
62 // 1. If bytes is the empty byte sequence, then continue.
63 if (bytes.is_empty())
64 continue;
65
66 StringView name;
67 StringView value;
68
69 // 2. If bytes contains a 0x3D (=), then let name be the bytes from the start of bytes up to but excluding its first 0x3D (=), and let value be the bytes, if any, after the first 0x3D (=) up to the end of bytes. If 0x3D (=) is the first byte, then name will be the empty byte sequence. If it is the last, then value will be the empty byte sequence.
70 if (auto index = bytes.find('='); index.has_value()) {
71 name = bytes.substring_view(0, *index);
72 value = bytes.substring_view(*index + 1);
73 }
74 // 3. Otherwise, let name have the value of bytes and let value be the empty byte sequence.
75 else {
76 name = bytes;
77 value = ""sv;
78 }
79
80 // 4. Replace any 0x2B (+) in name and value with 0x20 (SP).
81 auto space_decoded_name = name.replace("+"sv, " "sv, ReplaceMode::All);
82
83 // 5. Let nameString and valueString be the result of running UTF-8 decode without BOM on the percent-decoding of name and value, respectively.
84 auto name_string = TRY(String::from_deprecated_string(AK::URL::percent_decode(space_decoded_name)));
85 auto value_string = TRY(String::from_deprecated_string(AK::URL::percent_decode(value)));
86
87 TRY(output.try_empend(move(name_string), move(value_string)));
88 }
89
90 return output;
91}
92
93WebIDL::ExceptionOr<JS::NonnullGCPtr<URLSearchParams>> URLSearchParams::create(JS::Realm& realm, Vector<QueryParam> list)
94{
95 return MUST_OR_THROW_OOM(realm.heap().allocate<URLSearchParams>(realm, realm, move(list)));
96}
97
98// https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
99// https://url.spec.whatwg.org/#urlsearchparams-initialize
100WebIDL::ExceptionOr<JS::NonnullGCPtr<URLSearchParams>> URLSearchParams::construct_impl(JS::Realm& realm, Variant<Vector<Vector<String>>, OrderedHashMap<String, String>, String> const& init)
101{
102 auto& vm = realm.vm();
103
104 // 1. If init is a string and starts with U+003F (?), then remove the first code point from init.
105 // NOTE: We do this when we know that it's a string on step 3 of initialization.
106
107 // 2. Initialize this with init.
108
109 // URLSearchParams init from this point forward
110
111 // 1. If init is a sequence, then for each pair in init:
112 if (init.has<Vector<Vector<String>>>()) {
113 auto const& init_sequence = init.get<Vector<Vector<String>>>();
114
115 Vector<QueryParam> list;
116 list.ensure_capacity(init_sequence.size());
117
118 for (auto const& pair : init_sequence) {
119 // a. If pair does not contain exactly two items, then throw a TypeError.
120 if (pair.size() != 2)
121 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, TRY_OR_THROW_OOM(vm, String::formatted("Expected only 2 items in pair, got {}", pair.size())) };
122
123 // b. Append a new name-value pair whose name is pair’s first item, and value is pair’s second item, to query’s list.
124 list.append(QueryParam { .name = pair[0], .value = pair[1] });
125 }
126
127 return URLSearchParams::create(realm, move(list));
128 }
129
130 // 2. Otherwise, if init is a record, then for each name → value of init, append a new name-value pair whose name is name and value is value, to query’s list.
131 if (init.has<OrderedHashMap<String, String>>()) {
132 auto const& init_record = init.get<OrderedHashMap<String, String>>();
133
134 Vector<QueryParam> list;
135 list.ensure_capacity(init_record.size());
136
137 for (auto const& pair : init_record)
138 list.append(QueryParam { .name = pair.key, .value = pair.value });
139
140 return URLSearchParams::create(realm, move(list));
141 }
142
143 // 3. Otherwise:
144 // a. Assert: init is a string.
145 // NOTE: `get` performs `VERIFY(has<T>())`
146 auto const& init_string = init.get<String>();
147
148 // See NOTE at the start of this function.
149 auto init_string_view = init_string.bytes_as_string_view();
150 auto stripped_init = init_string_view.substring_view(init_string_view.starts_with('?'));
151
152 // b. Set query’s list to the result of parsing init.
153 return URLSearchParams::create(realm, TRY_OR_THROW_OOM(vm, url_decode(stripped_init)));
154}
155
156// https://url.spec.whatwg.org/#dom-urlsearchparams-size
157size_t URLSearchParams::size() const
158{
159 // The size getter steps are to return this’s list’s size.
160 return m_list.size();
161}
162
163WebIDL::ExceptionOr<void> URLSearchParams::append(String const& name, String const& value)
164{
165 auto& vm = realm().vm();
166
167 // 1. Append a new name-value pair whose name is name and value is value, to list.
168 TRY_OR_THROW_OOM(vm, m_list.try_empend(name, value));
169 // 2. Update this.
170 TRY(update());
171
172 return {};
173}
174
175WebIDL::ExceptionOr<void> URLSearchParams::update()
176{
177 // 1. If query’s URL object is null, then return.
178 if (!m_url)
179 return {};
180 // 2. Let serializedQuery be the serialization of query’s list.
181 auto serialized_query = TRY(to_string());
182 // 3. If serializedQuery is the empty string, then set serializedQuery to null.
183 if (serialized_query.is_empty())
184 serialized_query = {};
185 // 4. Set query’s URL object’s URL’s query to serializedQuery.
186 m_url->set_query({}, move(serialized_query));
187
188 return {};
189}
190
191WebIDL::ExceptionOr<void> URLSearchParams::delete_(String const& name)
192{
193 // 1. Remove all name-value pairs whose name is name from list.
194 m_list.remove_all_matching([&name](auto& entry) {
195 return entry.name == name;
196 });
197 // 2. Update this.
198 TRY(update());
199
200 return {};
201}
202
203Optional<String> URLSearchParams::get(String const& name)
204{
205 // return the value of the first name-value pair whose name is name in this’s list, if there is such a pair, and null otherwise.
206 auto result = m_list.find_if([&name](auto& entry) {
207 return entry.name == name;
208 });
209 if (result.is_end())
210 return {};
211 return result->value;
212}
213
214// https://url.spec.whatwg.org/#dom-urlsearchparams-getall
215WebIDL::ExceptionOr<Vector<String>> URLSearchParams::get_all(String const& name)
216{
217 auto& vm = realm().vm();
218
219 // return the values of all name-value pairs whose name is name, in this’s list, in list order, and the empty sequence otherwise.
220 Vector<String> values;
221 for (auto& entry : m_list) {
222 if (entry.name == name)
223 TRY_OR_THROW_OOM(vm, values.try_append(entry.value));
224 }
225 return values;
226}
227
228bool URLSearchParams::has(String const& name)
229{
230 // return true if there is a name-value pair whose name is name in this’s list, and false otherwise.
231 return !m_list.find_if([&name](auto& entry) {
232 return entry.name == name;
233 })
234 .is_end();
235}
236
237WebIDL::ExceptionOr<void> URLSearchParams::set(String const& name, String const& value)
238{
239 auto& vm = realm().vm();
240
241 // 1. If this’s list contains any name-value pairs whose name is name, then set the value of the first such name-value pair to value and remove the others.
242 auto existing = m_list.find_if([&name](auto& entry) {
243 return entry.name == name;
244 });
245 if (!existing.is_end()) {
246 existing->value = value;
247 m_list.remove_all_matching([&name, &existing](auto& entry) {
248 return &entry != &*existing && entry.name == name;
249 });
250 }
251 // 2. Otherwise, append a new name-value pair whose name is name and value is value, to this’s list.
252 else {
253 TRY_OR_THROW_OOM(vm, m_list.try_empend(name, value));
254 }
255 // 3. Update this.
256 TRY(update());
257
258 return {};
259}
260
261WebIDL::ExceptionOr<void> URLSearchParams::sort()
262{
263 // 1. Sort all name-value pairs, if any, by their names. Sorting must be done by comparison of code units. The relative order between name-value pairs with equal names must be preserved.
264 quick_sort(m_list.begin(), m_list.end(), [](auto& a, auto& b) {
265 Utf8View a_code_points { a.name };
266 Utf8View b_code_points { b.name };
267
268 if (a_code_points.starts_with(b_code_points))
269 return false;
270 if (b_code_points.starts_with(a_code_points))
271 return true;
272
273 for (auto k = a_code_points.begin(), l = b_code_points.begin();
274 k != a_code_points.end() && l != b_code_points.end();
275 ++k, ++l) {
276 if (*k != *l) {
277 if (*k < *l) {
278 return true;
279 } else {
280 return false;
281 }
282 }
283 }
284 VERIFY_NOT_REACHED();
285 });
286 // 2. Update this.
287 TRY(update());
288
289 return {};
290}
291
292WebIDL::ExceptionOr<String> URLSearchParams::to_string() const
293{
294 auto& vm = realm().vm();
295
296 // return the serialization of this’s list.
297 return TRY_OR_THROW_OOM(vm, url_encode(m_list, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
298}
299
300JS::ThrowCompletionOr<void> URLSearchParams::for_each(ForEachCallback callback)
301{
302 for (auto i = 0u; i < m_list.size(); ++i) {
303 auto& query_param = m_list[i]; // We are explicitly iterating over the indices here as the callback might delete items from the list
304 TRY(callback(query_param.name, query_param.value));
305 }
306
307 return {};
308}
309
310}