Serenity Operating System
at master 398 lines 15 kB view raw
1/* 2 * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> 3 * Copyright (c) 2021, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/IPv4Address.h> 9#include <AK/IPv6Address.h> 10#include <AK/URLParser.h> 11#include <LibWeb/Bindings/Intrinsics.h> 12#include <LibWeb/URL/URL.h> 13 14namespace Web::URL { 15 16WebIDL::ExceptionOr<JS::NonnullGCPtr<URL>> URL::create(JS::Realm& realm, AK::URL url, JS::NonnullGCPtr<URLSearchParams> query) 17{ 18 return MUST_OR_THROW_OOM(realm.heap().allocate<URL>(realm, realm, move(url), move(query))); 19} 20 21WebIDL::ExceptionOr<JS::NonnullGCPtr<URL>> URL::construct_impl(JS::Realm& realm, String const& url, Optional<String> const& base) 22{ 23 auto& vm = realm.vm(); 24 25 // 1. Let parsedBase be null. 26 Optional<AK::URL> parsed_base; 27 // 2. If base is given, then: 28 if (base.has_value()) { 29 // 1. Let parsedBase be the result of running the basic URL parser on base. 30 parsed_base = base.value(); 31 // 2. If parsedBase is failure, then throw a TypeError. 32 if (!parsed_base->is_valid()) 33 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid base URL"sv }; 34 } 35 // 3. Let parsedURL be the result of running the basic URL parser on url with parsedBase. 36 AK::URL parsed_url; 37 if (parsed_base.has_value()) 38 parsed_url = parsed_base->complete_url(url); 39 else 40 parsed_url = url; 41 // 4. If parsedURL is failure, then throw a TypeError. 42 if (!parsed_url.is_valid()) 43 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid URL"sv }; 44 // 5. Let query be parsedURL’s query, if that is non-null, and the empty string otherwise. 45 auto query = parsed_url.query().is_null() ? String {} : TRY_OR_THROW_OOM(vm, String::from_deprecated_string(parsed_url.query())); 46 // 6. Set this’s URL to parsedURL. 47 // 7. Set this’s query object to a new URLSearchParams object. 48 auto query_object = MUST(URLSearchParams::construct_impl(realm, query)); 49 // 8. Initialize this’s query object with query. 50 auto result_url = TRY(URL::create(realm, move(parsed_url), move(query_object))); 51 // 9. Set this’s query object’s URL object to this. 52 result_url->m_query->m_url = result_url; 53 54 return result_url; 55} 56 57URL::URL(JS::Realm& realm, AK::URL url, JS::NonnullGCPtr<URLSearchParams> query) 58 : PlatformObject(realm) 59 , m_url(move(url)) 60 , m_query(move(query)) 61{ 62} 63 64URL::~URL() = default; 65 66JS::ThrowCompletionOr<void> URL::initialize(JS::Realm& realm) 67{ 68 MUST_OR_THROW_OOM(Base::initialize(realm)); 69 set_prototype(&Bindings::ensure_web_prototype<Bindings::URLPrototype>(realm, "URL")); 70 71 return {}; 72} 73 74void URL::visit_edges(Cell::Visitor& visitor) 75{ 76 Base::visit_edges(visitor); 77 visitor.visit(m_query.ptr()); 78} 79 80WebIDL::ExceptionOr<String> URL::href() const 81{ 82 auto& vm = realm().vm(); 83 84 // return the serialization of this’s URL. 85 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.serialize())); 86} 87 88WebIDL::ExceptionOr<String> URL::to_json() const 89{ 90 auto& vm = realm().vm(); 91 92 // return the serialization of this’s URL. 93 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.serialize())); 94} 95 96WebIDL::ExceptionOr<void> URL::set_href(String const& href) 97{ 98 auto& vm = realm().vm(); 99 100 // 1. Let parsedURL be the result of running the basic URL parser on the given value. 101 AK::URL parsed_url = href; 102 // 2. If parsedURL is failure, then throw a TypeError. 103 if (!parsed_url.is_valid()) 104 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid URL"sv }; 105 // 3. Set this’s URL to parsedURL. 106 m_url = move(parsed_url); 107 // 4. Empty this’s query object’s list. 108 m_query->m_list.clear(); 109 // 5. Let query be this’s URL’s query. 110 auto& query = m_url.query(); 111 // 6. If query is non-null, then set this’s query object’s list to the result of parsing query. 112 if (!query.is_null()) 113 m_query->m_list = TRY_OR_THROW_OOM(vm, url_decode(query)); 114 return {}; 115} 116 117WebIDL::ExceptionOr<String> URL::origin() const 118{ 119 auto& vm = realm().vm(); 120 121 // return the serialization of this’s URL’s origin. 122 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.serialize_origin())); 123} 124 125WebIDL::ExceptionOr<String> URL::protocol() const 126{ 127 auto& vm = realm().vm(); 128 129 // return this’s URL’s scheme, followed by U+003A (:). 130 return TRY_OR_THROW_OOM(vm, String::formatted("{}:", m_url.scheme())); 131} 132 133WebIDL::ExceptionOr<void> URL::set_protocol(String const& protocol) 134{ 135 auto& vm = realm().vm(); 136 137 // basic URL parse the given value, followed by U+003A (:), with this’s URL as url and scheme start state as state override. 138 auto result_url = URLParser::parse(TRY_OR_THROW_OOM(vm, String::formatted("{}:", protocol)), nullptr, m_url, URLParser::State::SchemeStart); 139 if (result_url.is_valid()) 140 m_url = move(result_url); 141 return {}; 142} 143 144WebIDL::ExceptionOr<String> URL::username() const 145{ 146 auto& vm = realm().vm(); 147 148 // return this’s URL’s username. 149 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.username())); 150} 151 152void URL::set_username(String const& username) 153{ 154 // 1. If this’s URL cannot have a username/password/port, then return. 155 if (m_url.cannot_have_a_username_or_password_or_port()) 156 return; 157 // 2. Set the username given this’s URL and the given value. 158 m_url.set_username(AK::URL::percent_encode(username, AK::URL::PercentEncodeSet::Userinfo)); 159} 160 161WebIDL::ExceptionOr<String> URL::password() const 162{ 163 auto& vm = realm().vm(); 164 165 // return this’s URL’s password. 166 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.password())); 167} 168 169void URL::set_password(String const& password) 170{ 171 // 1. If this’s URL cannot have a username/password/port, then return. 172 if (m_url.cannot_have_a_username_or_password_or_port()) 173 return; 174 // 2. Set the password given this’s URL and the given value. 175 m_url.set_password(AK::URL::percent_encode(password, AK::URL::PercentEncodeSet::Userinfo)); 176} 177 178WebIDL::ExceptionOr<String> URL::host() const 179{ 180 auto& vm = realm().vm(); 181 182 // 1. Let url be this’s URL. 183 auto& url = m_url; 184 // 2. If url’s host is null, then return the empty string. 185 if (url.host().is_null()) 186 return String {}; 187 // 3. If url’s port is null, return url’s host, serialized. 188 if (!url.port().has_value()) 189 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url.host())); 190 // 4. Return url’s host, serialized, followed by U+003A (:) and url’s port, serialized. 191 return TRY_OR_THROW_OOM(vm, String::formatted("{}:{}", url.host(), *url.port())); 192} 193 194void URL::set_host(String const& host) 195{ 196 // 1. If this’s URL’s cannot-be-a-base-URL is true, then return. 197 if (m_url.cannot_be_a_base_url()) 198 return; 199 // 2. Basic URL parse the given value with this’s URL as url and host state as state override. 200 auto result_url = URLParser::parse(host, nullptr, m_url, URLParser::State::Host); 201 if (result_url.is_valid()) 202 m_url = move(result_url); 203} 204 205WebIDL::ExceptionOr<String> URL::hostname() const 206{ 207 auto& vm = realm().vm(); 208 209 // 1. If this’s URL’s host is null, then return the empty string. 210 if (m_url.host().is_null()) 211 return String {}; 212 // 2. Return this’s URL’s host, serialized. 213 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.host())); 214} 215 216void URL::set_hostname(String const& hostname) 217{ 218 // 1. If this’s URL’s cannot-be-a-base-URL is true, then return. 219 if (m_url.cannot_be_a_base_url()) 220 return; 221 // 2. Basic URL parse the given value with this’s URL as url and hostname state as state override. 222 auto result_url = URLParser::parse(hostname, nullptr, m_url, URLParser::State::Hostname); 223 if (result_url.is_valid()) 224 m_url = move(result_url); 225} 226 227WebIDL::ExceptionOr<String> URL::port() const 228{ 229 auto& vm = realm().vm(); 230 231 // 1. If this’s URL’s port is null, then return the empty string. 232 if (!m_url.port().has_value()) 233 return String {}; 234 235 // 2. Return this’s URL’s port, serialized. 236 return TRY_OR_THROW_OOM(vm, String::formatted("{}", *m_url.port())); 237} 238 239void URL::set_port(String const& port) 240{ 241 // 1. If this’s URL cannot have a username/password/port, then return. 242 if (m_url.cannot_have_a_username_or_password_or_port()) 243 return; 244 245 // 2. If the given value is the empty string, then set this’s URL’s port to null. 246 if (port.is_empty()) { 247 m_url.set_port({}); 248 return; 249 } 250 251 // 3. Otherwise, basic URL parse the given value with this’s URL as url and port state as state override. 252 auto result_url = URLParser::parse(port, nullptr, m_url, URLParser::State::Port); 253 if (result_url.is_valid()) 254 m_url = move(result_url); 255} 256 257WebIDL::ExceptionOr<String> URL::pathname() const 258{ 259 auto& vm = realm().vm(); 260 261 // 1. If this’s URL’s cannot-be-a-base-URL is true, then return this’s URL’s path[0]. 262 // 2. If this’s URL’s path is empty, then return the empty string. 263 // 3. Return U+002F (/), followed by the strings in this’s URL’s path (including empty strings), if any, separated from each other by U+002F (/). 264 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(m_url.path())); 265} 266 267void URL::set_pathname(String const& pathname) 268{ 269 // 1. If this’s URL’s cannot-be-a-base-URL is true, then return. 270 if (m_url.cannot_be_a_base_url()) 271 return; 272 // 2. Empty this’s URL’s path. 273 auto url = m_url; // We copy the URL here to follow other browser's behaviour of reverting the path change if the parse failed. 274 url.set_paths({}); 275 // 3. Basic URL parse the given value with this’s URL as url and path start state as state override. 276 auto result_url = URLParser::parse(pathname, nullptr, move(url), URLParser::State::PathStart); 277 if (result_url.is_valid()) 278 m_url = move(result_url); 279} 280 281WebIDL::ExceptionOr<String> URL::search() const 282{ 283 auto& vm = realm().vm(); 284 285 // 1. If this’s URL’s query is either null or the empty string, then return the empty string. 286 if (m_url.query().is_null() || m_url.query().is_empty()) 287 return String {}; 288 // 2. Return U+003F (?), followed by this’s URL’s query. 289 return TRY_OR_THROW_OOM(vm, String::formatted("?{}", m_url.query())); 290} 291 292WebIDL::ExceptionOr<void> URL::set_search(String const& search) 293{ 294 auto& vm = realm().vm(); 295 296 // 1. Let url be this’s URL. 297 auto& url = m_url; 298 // If the given value is the empty string, set url’s query to null, empty this’s query object’s list, and then return. 299 if (search.is_empty()) { 300 url.set_query({}); 301 m_query->m_list.clear(); 302 return {}; 303 } 304 // 2. Let input be the given value with a single leading U+003F (?) removed, if any. 305 auto search_as_string_view = search.bytes_as_string_view(); 306 auto input = search_as_string_view.substring_view(search_as_string_view.starts_with('?')); 307 // 3. Set url’s query to the empty string. 308 auto url_copy = url; // We copy the URL here to follow other browser's behaviour of reverting the search change if the parse failed. 309 url_copy.set_query(DeprecatedString::empty()); 310 // 4. Basic URL parse input with url as url and query state as state override. 311 auto result_url = URLParser::parse(input, nullptr, move(url_copy), URLParser::State::Query); 312 if (result_url.is_valid()) { 313 m_url = move(result_url); 314 // 5. Set this’s query object’s list to the result of parsing input. 315 m_query->m_list = TRY_OR_THROW_OOM(vm, url_decode(input)); 316 } 317 318 return {}; 319} 320 321URLSearchParams const* URL::search_params() const 322{ 323 return m_query; 324} 325 326WebIDL::ExceptionOr<String> URL::hash() const 327{ 328 auto& vm = realm().vm(); 329 330 // 1. If this’s URL’s fragment is either null or the empty string, then return the empty string. 331 if (m_url.fragment().is_null() || m_url.fragment().is_empty()) 332 return String {}; 333 // 2. Return U+0023 (#), followed by this’s URL’s fragment. 334 return TRY_OR_THROW_OOM(vm, String::formatted("#{}", m_url.fragment())); 335} 336 337void URL::set_hash(String const& hash) 338{ 339 // 1. If the given value is the empty string, then set this’s URL’s fragment to null and return. 340 if (hash.is_empty()) { 341 m_url.set_fragment({}); 342 return; 343 } 344 // 2. Let input be the given value with a single leading U+0023 (#) removed, if any. 345 auto hash_as_string_view = hash.bytes_as_string_view(); 346 auto input = hash_as_string_view.substring_view(hash_as_string_view.starts_with('#')); 347 // 3. Set this’s URL’s fragment to the empty string. 348 auto url = m_url; // We copy the URL here to follow other browser's behaviour of reverting the hash change if the parse failed. 349 url.set_fragment(DeprecatedString::empty()); 350 // 4. Basic URL parse input with this’s URL as url and fragment state as state override. 351 auto result_url = URLParser::parse(input, nullptr, move(url), URLParser::State::Fragment); 352 if (result_url.is_valid()) 353 m_url = move(result_url); 354} 355 356// https://url.spec.whatwg.org/#concept-url-origin 357HTML::Origin url_origin(AK::URL const& url) 358{ 359 // FIXME: We should probably have an extended version of AK::URL for LibWeb instead of standalone functions like this. 360 361 // The origin of a URL url is the origin returned by running these steps, switching on url’s scheme: 362 // "blob" 363 if (url.scheme() == "blob"sv) { 364 // FIXME: Support 'blob://' URLs 365 return HTML::Origin {}; 366 } 367 368 // "ftp" 369 // "http" 370 // "https" 371 // "ws" 372 // "wss" 373 if (url.scheme().is_one_of("ftp"sv, "http"sv, "https"sv, "ws"sv, "wss"sv)) { 374 // Return the tuple origin (url’s scheme, url’s host, url’s port, null). 375 return HTML::Origin(url.scheme(), url.host(), url.port().value_or(0)); 376 } 377 378 // "file" 379 if (url.scheme() == "file"sv) { 380 // Unfortunate as it is, this is left as an exercise to the reader. When in doubt, return a new opaque origin. 381 // Note: We must return an origin with the `file://' protocol for `file://' iframes to work from `file://' pages. 382 return HTML::Origin(url.scheme(), DeprecatedString(), 0); 383 } 384 385 // Return a new opaque origin. 386 return HTML::Origin {}; 387} 388 389// https://url.spec.whatwg.org/#concept-domain 390bool host_is_domain(StringView host) 391{ 392 // A domain is a non-empty ASCII string that identifies a realm within a network. 393 return !host.is_empty() 394 && !IPv4Address::from_string(host).has_value() 395 && !IPv6Address::from_string(host).has_value(); 396} 397 398}