Serenity Operating System
at master 469 lines 21 kB view raw
1/* 2 * Copyright (c) 2020-2023, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/String.h> 9#include <AK/StringBuilder.h> 10#include <AK/URLParser.h> 11#include <LibJS/Heap/MarkedVector.h> 12#include <LibJS/Runtime/Completion.h> 13#include <LibJS/Runtime/PropertyDescriptor.h> 14#include <LibJS/Runtime/PropertyKey.h> 15#include <LibWeb/Bindings/LocationPrototype.h> 16#include <LibWeb/DOM/Document.h> 17#include <LibWeb/HTML/CrossOrigin/AbstractOperations.h> 18#include <LibWeb/HTML/Location.h> 19#include <LibWeb/HTML/Window.h> 20#include <LibWeb/WebIDL/DOMException.h> 21 22namespace Web::HTML { 23 24// https://html.spec.whatwg.org/multipage/history.html#the-location-interface 25Location::Location(JS::Realm& realm) 26 : PlatformObject(realm) 27{ 28} 29 30Location::~Location() = default; 31 32void Location::visit_edges(Cell::Visitor& visitor) 33{ 34 Base::visit_edges(visitor); 35 for (auto& property : m_default_properties) 36 visitor.visit(property); 37} 38 39JS::ThrowCompletionOr<void> Location::initialize(JS::Realm& realm) 40{ 41 MUST_OR_THROW_OOM(Object::initialize(realm)); 42 set_prototype(&Bindings::ensure_web_prototype<Bindings::LocationPrototype>(realm, "Location")); 43 44 // FIXME: Implement steps 2.-4. 45 46 // 5. Set the value of the [[DefaultProperties]] internal slot of location to location.[[OwnPropertyKeys]](). 47 // NOTE: In LibWeb this happens before the ESO is set up, so we must avoid location's custom [[OwnPropertyKeys]]. 48 m_default_properties.extend(MUST(Object::internal_own_property_keys())); 49 50 return {}; 51} 52 53// https://html.spec.whatwg.org/multipage/history.html#relevant-document 54JS::GCPtr<DOM::Document> Location::relevant_document() const 55{ 56 // A Location object has an associated relevant Document, which is this Location object's 57 // relevant global object's browsing context's active document, if this Location object's 58 // relevant global object's browsing context is non-null, and null otherwise. 59 auto* browsing_context = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).browsing_context(); 60 return browsing_context ? browsing_context->active_document() : nullptr; 61} 62 63// https://html.spec.whatwg.org/multipage/history.html#concept-location-url 64AK::URL Location::url() const 65{ 66 // A Location object has an associated url, which is this Location object's relevant Document's URL, 67 // if this Location object's relevant Document is non-null, and about:blank otherwise. 68 auto const relevant_document = this->relevant_document(); 69 return relevant_document ? relevant_document->url() : "about:blank"sv; 70} 71 72// https://html.spec.whatwg.org/multipage/history.html#dom-location-href 73WebIDL::ExceptionOr<String> Location::href() const 74{ 75 auto& vm = this->vm(); 76 77 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 78 auto const relevant_document = this->relevant_document(); 79 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 80 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 81 82 // 2. Return this's url, serialized. 83 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url().serialize())); 84} 85 86// https://html.spec.whatwg.org/multipage/history.html#the-location-interface:dom-location-href-2 87WebIDL::ExceptionOr<void> Location::set_href(String const& new_href) 88{ 89 auto& vm = this->vm(); 90 auto& window = verify_cast<HTML::Window>(HTML::current_global_object()); 91 92 // 1. If this's relevant Document is null, then return. 93 auto const relevant_document = this->relevant_document(); 94 if (!relevant_document) 95 return {}; 96 97 // 2. Parse the given value relative to the entry settings object. If that failed, throw a TypeError exception. 98 auto href_url = window.associated_document().parse_url(new_href.to_deprecated_string()); 99 if (!href_url.is_valid()) 100 return vm.throw_completion<JS::URIError>(TRY_OR_THROW_OOM(vm, String::formatted("Invalid URL '{}'", new_href))); 101 102 // 3. Location-object navigate given the resulting URL record. 103 window.did_set_location_href({}, href_url); 104 105 return {}; 106} 107 108// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-location-origin 109WebIDL::ExceptionOr<String> Location::origin() const 110{ 111 auto& vm = this->vm(); 112 113 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 114 auto const relevant_document = this->relevant_document(); 115 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 116 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 117 118 // 2. Return the serialization of this's url's origin. 119 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url().serialize_origin())); 120} 121 122// https://html.spec.whatwg.org/multipage/history.html#dom-location-protocol 123WebIDL::ExceptionOr<String> Location::protocol() const 124{ 125 auto& vm = this->vm(); 126 127 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 128 auto const relevant_document = this->relevant_document(); 129 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 130 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 131 132 // 2. Return this's url's scheme, followed by ":". 133 return TRY_OR_THROW_OOM(vm, String::formatted("{}:", url().scheme())); 134} 135 136WebIDL::ExceptionOr<void> Location::set_protocol(String const&) 137{ 138 auto& vm = this->vm(); 139 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.protocol setter"); 140} 141 142// https://html.spec.whatwg.org/multipage/history.html#dom-location-host 143WebIDL::ExceptionOr<String> Location::host() const 144{ 145 auto& vm = this->vm(); 146 147 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 148 auto const relevant_document = this->relevant_document(); 149 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 150 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 151 152 // 2. Let url be this's url. 153 auto url = this->url(); 154 155 // 3. If url's host is null, return the empty string. 156 if (url.host().is_null()) 157 return String {}; 158 159 // 4. If url's port is null, return url's host, serialized. 160 if (!url.port().has_value()) 161 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url.host())); 162 163 // 5. Return url's host, serialized, followed by ":" and url's port, serialized. 164 return TRY_OR_THROW_OOM(vm, String::formatted("{}:{}", url.host(), *url.port())); 165} 166 167WebIDL::ExceptionOr<void> Location::set_host(String const&) 168{ 169 auto& vm = this->vm(); 170 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.host setter"); 171} 172 173// https://html.spec.whatwg.org/multipage/history.html#dom-location-hostname 174WebIDL::ExceptionOr<String> Location::hostname() const 175{ 176 auto& vm = this->vm(); 177 178 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 179 auto const relevant_document = this->relevant_document(); 180 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 181 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 182 183 auto url = this->url(); 184 185 // 2. If this's url's host is null, return the empty string. 186 if (url.host().is_null()) 187 return String {}; 188 189 // 3. Return this's url's host, serialized. 190 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url.host())); 191} 192 193WebIDL::ExceptionOr<void> Location::set_hostname(String const&) 194{ 195 auto& vm = this->vm(); 196 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.hostname setter"); 197} 198 199// https://html.spec.whatwg.org/multipage/history.html#dom-location-port 200WebIDL::ExceptionOr<String> Location::port() const 201{ 202 auto& vm = this->vm(); 203 204 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 205 auto const relevant_document = this->relevant_document(); 206 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 207 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 208 209 auto url = this->url(); 210 211 // 2. If this's url's port is null, return the empty string. 212 if (!url.port().has_value()) 213 return String {}; 214 215 // 3. Return this's url's port, serialized. 216 return TRY_OR_THROW_OOM(vm, String::number(*url.port())); 217} 218 219WebIDL::ExceptionOr<void> Location::set_port(String const&) 220{ 221 auto& vm = this->vm(); 222 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.port setter"); 223} 224 225// https://html.spec.whatwg.org/multipage/history.html#dom-location-pathname 226WebIDL::ExceptionOr<String> Location::pathname() const 227{ 228 auto& vm = this->vm(); 229 230 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 231 auto const relevant_document = this->relevant_document(); 232 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 233 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 234 235 // 2. Return the result of URL path serializing this Location object's url. 236 return TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url().path())); 237} 238 239WebIDL::ExceptionOr<void> Location::set_pathname(String const&) 240{ 241 auto& vm = this->vm(); 242 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.pathname setter"); 243} 244 245// https://html.spec.whatwg.org/multipage/history.html#dom-location-search 246WebIDL::ExceptionOr<String> Location::search() const 247{ 248 auto& vm = this->vm(); 249 250 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 251 auto const relevant_document = this->relevant_document(); 252 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 253 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 254 255 auto url = this->url(); 256 257 // 2. If this's url's query is either null or the empty string, return the empty string. 258 if (url.query().is_empty()) 259 return String {}; 260 261 // 3. Return "?", followed by this's url's query. 262 return TRY_OR_THROW_OOM(vm, String::formatted("?{}", url.query())); 263} 264 265WebIDL::ExceptionOr<void> Location::set_search(String const&) 266{ 267 auto& vm = this->vm(); 268 return vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Location.search setter"); 269} 270 271// https://html.spec.whatwg.org/multipage/history.html#dom-location-hash 272WebIDL::ExceptionOr<String> Location::hash() const 273{ 274 auto& vm = this->vm(); 275 276 // 1. If this's relevant Document is non-null and its origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 277 auto const relevant_document = this->relevant_document(); 278 if (relevant_document && !relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 279 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 280 281 auto url = this->url(); 282 283 // 2. If this's url's fragment is either null or the empty string, return the empty string. 284 if (url.fragment().is_empty()) 285 return String {}; 286 287 // 3. Return "#", followed by this's url's fragment. 288 return TRY_OR_THROW_OOM(vm, String::formatted("#{}", url.fragment())); 289} 290 291// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-location-hash 292WebIDL::ExceptionOr<void> Location::set_hash(String const& value) 293{ 294 // The hash setter steps are: 295 auto const relevant_document = this->relevant_document(); 296 297 // 1. If this's relevant Document is null, then return. 298 if (!relevant_document) 299 return {}; 300 301 // 2. If this's relevant Document's origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException. 302 if (!relevant_document->origin().is_same_origin_domain(entry_settings_object().origin())) 303 return WebIDL::SecurityError::create(realm(), "Location's relevant document is not same origin-domain with the entry settings object's origin"sv); 304 305 // 3. Let copyURL be a copy of this's url. 306 auto copy_url = this->url(); 307 308 // 4. Let input be the given value with a single leading "#" removed, if any. 309 auto input = value.bytes_as_string_view().trim("#"sv, TrimMode::Left); 310 311 // 5. Set copyURL's fragment to the empty string. 312 copy_url.set_fragment(""); 313 314 // 6. Basic URL parse input, with copyURL as url and fragment state as state override. 315 auto result_url = URLParser::parse(input, nullptr, copy_url, URLParser::State::Fragment); 316 317 // 7. If copyURL's fragment is this's url's fragment, then return. 318 if (copy_url.fragment() == this->url().fragment()) 319 return {}; 320 321 // 8. Location-object navigate this to copyURL. 322 auto& window = verify_cast<HTML::Window>(HTML::current_global_object()); 323 window.did_set_location_href({}, copy_url); 324 325 return {}; 326} 327 328// https://html.spec.whatwg.org/multipage/history.html#dom-location-reload 329void Location::reload() const 330{ 331 auto& window = verify_cast<HTML::Window>(HTML::current_global_object()); 332 window.did_call_location_reload({}); 333} 334 335// https://html.spec.whatwg.org/multipage/history.html#dom-location-replace 336void Location::replace(String const& url) const 337{ 338 auto& window = verify_cast<HTML::Window>(HTML::current_global_object()); 339 // FIXME: This needs spec compliance work. 340 window.did_call_location_replace({}, url.to_deprecated_string()); 341} 342 343// 7.10.5.1 [[GetPrototypeOf]] ( ), https://html.spec.whatwg.org/multipage/history.html#location-getprototypeof 344JS::ThrowCompletionOr<JS::Object*> Location::internal_get_prototype_of() const 345{ 346 // 1. If IsPlatformObjectSameOrigin(this) is true, then return ! OrdinaryGetPrototypeOf(this). 347 if (HTML::is_platform_object_same_origin(*this)) 348 return MUST(JS::Object::internal_get_prototype_of()); 349 350 // 2. Return null. 351 return nullptr; 352} 353 354// 7.10.5.2 [[SetPrototypeOf]] ( V ), https://html.spec.whatwg.org/multipage/history.html#location-setprototypeof 355JS::ThrowCompletionOr<bool> Location::internal_set_prototype_of(Object* prototype) 356{ 357 // 1. Return ! SetImmutablePrototype(this, V). 358 return MUST(set_immutable_prototype(prototype)); 359} 360 361// 7.10.5.3 [[IsExtensible]] ( ), https://html.spec.whatwg.org/multipage/history.html#location-isextensible 362JS::ThrowCompletionOr<bool> Location::internal_is_extensible() const 363{ 364 // 1. Return true. 365 return true; 366} 367 368// 7.10.5.4 [[PreventExtensions]] ( ), https://html.spec.whatwg.org/multipage/history.html#location-preventextensions 369JS::ThrowCompletionOr<bool> Location::internal_prevent_extensions() 370{ 371 // 1. Return false. 372 return false; 373} 374 375// 7.10.5.5 [[GetOwnProperty]] ( P ), https://html.spec.whatwg.org/multipage/history.html#location-getownproperty 376JS::ThrowCompletionOr<Optional<JS::PropertyDescriptor>> Location::internal_get_own_property(JS::PropertyKey const& property_key) const 377{ 378 auto& vm = this->vm(); 379 380 // 1. If IsPlatformObjectSameOrigin(this) is true, then: 381 if (HTML::is_platform_object_same_origin(*this)) { 382 // 1. Let desc be OrdinaryGetOwnProperty(this, P). 383 auto descriptor = MUST(Object::internal_get_own_property(property_key)); 384 385 // 2. If the value of the [[DefaultProperties]] internal slot of this contains P, then set desc.[[Configurable]] to true. 386 auto property_key_value = property_key.is_symbol() 387 ? JS::Value { property_key.as_symbol() } 388 : JS::PrimitiveString::create(vm, property_key.to_string()); 389 if (m_default_properties.contains_slow(property_key_value)) 390 descriptor->configurable = true; 391 392 // 3. Return desc. 393 return descriptor; 394 } 395 396 // 2. Let property be CrossOriginGetOwnPropertyHelper(this, P). 397 auto property = HTML::cross_origin_get_own_property_helper(const_cast<Location*>(this), property_key); 398 399 // 3. If property is not undefined, then return property. 400 if (property.has_value()) 401 return property; 402 403 // 4. Return ? CrossOriginPropertyFallback(P). 404 return TRY(HTML::cross_origin_property_fallback(vm, property_key)); 405} 406 407// 7.10.5.6 [[DefineOwnProperty]] ( P, Desc ), https://html.spec.whatwg.org/multipage/history.html#location-defineownproperty 408JS::ThrowCompletionOr<bool> Location::internal_define_own_property(JS::PropertyKey const& property_key, JS::PropertyDescriptor const& descriptor) 409{ 410 // 1. If IsPlatformObjectSameOrigin(this) is true, then: 411 if (HTML::is_platform_object_same_origin(*this)) { 412 // 1. If the value of the [[DefaultProperties]] internal slot of this contains P, then return false. 413 // 2. Return ? OrdinaryDefineOwnProperty(this, P, Desc). 414 return JS::Object::internal_define_own_property(property_key, descriptor); 415 } 416 417 // 2. Throw a "SecurityError" DOMException. 418 return throw_completion(WebIDL::SecurityError::create(realm(), DeprecatedString::formatted("Can't define property '{}' on cross-origin object", property_key))); 419} 420 421// 7.10.5.7 [[Get]] ( P, Receiver ), https://html.spec.whatwg.org/multipage/history.html#location-get 422JS::ThrowCompletionOr<JS::Value> Location::internal_get(JS::PropertyKey const& property_key, JS::Value receiver) const 423{ 424 auto& vm = this->vm(); 425 426 // 1. If IsPlatformObjectSameOrigin(this) is true, then return ? OrdinaryGet(this, P, Receiver). 427 if (HTML::is_platform_object_same_origin(*this)) 428 return JS::Object::internal_get(property_key, receiver); 429 430 // 2. Return ? CrossOriginGet(this, P, Receiver). 431 return HTML::cross_origin_get(vm, static_cast<JS::Object const&>(*this), property_key, receiver); 432} 433 434// 7.10.5.8 [[Set]] ( P, V, Receiver ), https://html.spec.whatwg.org/multipage/history.html#location-set 435JS::ThrowCompletionOr<bool> Location::internal_set(JS::PropertyKey const& property_key, JS::Value value, JS::Value receiver) 436{ 437 auto& vm = this->vm(); 438 439 // 1. If IsPlatformObjectSameOrigin(this) is true, then return ? OrdinarySet(this, P, V, Receiver). 440 if (HTML::is_platform_object_same_origin(*this)) 441 return JS::Object::internal_set(property_key, value, receiver); 442 443 // 2. Return ? CrossOriginSet(this, P, V, Receiver). 444 return HTML::cross_origin_set(vm, static_cast<JS::Object&>(*this), property_key, value, receiver); 445} 446 447// 7.10.5.9 [[Delete]] ( P ), https://html.spec.whatwg.org/multipage/history.html#location-delete 448JS::ThrowCompletionOr<bool> Location::internal_delete(JS::PropertyKey const& property_key) 449{ 450 // 1. If IsPlatformObjectSameOrigin(this) is true, then return ? OrdinaryDelete(this, P). 451 if (HTML::is_platform_object_same_origin(*this)) 452 return JS::Object::internal_delete(property_key); 453 454 // 2. Throw a "SecurityError" DOMException. 455 return throw_completion(WebIDL::SecurityError::create(realm(), DeprecatedString::formatted("Can't delete property '{}' on cross-origin object", property_key))); 456} 457 458// 7.10.5.10 [[OwnPropertyKeys]] ( ), https://html.spec.whatwg.org/multipage/history.html#location-ownpropertykeys 459JS::ThrowCompletionOr<JS::MarkedVector<JS::Value>> Location::internal_own_property_keys() const 460{ 461 // 1. If IsPlatformObjectSameOrigin(this) is true, then return OrdinaryOwnPropertyKeys(this). 462 if (HTML::is_platform_object_same_origin(*this)) 463 return JS::Object::internal_own_property_keys(); 464 465 // 2. Return CrossOriginOwnPropertyKeys(this). 466 return HTML::cross_origin_own_property_keys(this); 467} 468 469}