Serenity Operating System
at master 310 lines 11 kB view raw
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}