Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibWeb/DOMURL/DOMURL.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
3
 * Copyright (c) 2021, the SerenityOS developers.
4
 * Copyright (c) 2023, networkException <networkexception@serenityos.org>
5
 * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
6
 *
7
 * SPDX-License-Identifier: BSD-2-Clause
8
 */
9
10
#include <AK/IPv4Address.h>
11
#include <AK/IPv6Address.h>
12
#include <LibURL/Parser.h>
13
#include <LibWeb/Bindings/DOMURLPrototype.h>
14
#include <LibWeb/Bindings/Intrinsics.h>
15
#include <LibWeb/DOMURL/DOMURL.h>
16
#include <LibWeb/FileAPI/Blob.h>
17
#include <LibWeb/FileAPI/BlobURLStore.h>
18
19
namespace Web::DOMURL {
20
21
JS_DEFINE_ALLOCATOR(DOMURL);
22
23
JS::NonnullGCPtr<DOMURL> DOMURL::create(JS::Realm& realm, URL::URL url, JS::NonnullGCPtr<URLSearchParams> query)
24
0
{
25
0
    return realm.heap().allocate<DOMURL>(realm, realm, move(url), move(query));
26
0
}
27
28
// https://url.spec.whatwg.org/#api-url-parser
29
static Optional<URL::URL> parse_api_url(String const& url, Optional<String> const& base)
30
0
{
31
    // FIXME: We somewhat awkwardly have two failure states encapsulated in the return type (and convert between them in the steps),
32
    //        ideally we'd get rid of URL's valid flag
33
34
    // 1. Let parsedBase be null.
35
0
    Optional<URL::URL> parsed_base;
36
37
    // 2. If base is non-null:
38
0
    if (base.has_value()) {
39
        // 1. Set parsedBase to the result of running the basic URL parser on base.
40
0
        auto parsed_base_url = URL::Parser::basic_parse(*base);
41
42
        // 2. If parsedBase is failure, then return failure.
43
0
        if (!parsed_base_url.is_valid())
44
0
            return {};
45
46
0
        parsed_base = parsed_base_url;
47
0
    }
48
49
    // 3. Return the result of running the basic URL parser on url with parsedBase.
50
0
    auto parsed = URL::Parser::basic_parse(url, parsed_base);
51
0
    return parsed.is_valid() ? parsed : Optional<URL::URL> {};
52
0
}
53
54
// https://url.spec.whatwg.org/#url-initialize
55
JS::NonnullGCPtr<DOMURL> DOMURL::initialize_a_url(JS::Realm& realm, URL::URL const& url_record)
56
0
{
57
    // 1. Let query be urlRecord’s query, if that is non-null; otherwise the empty string.
58
0
    auto query = url_record.query().value_or(String {});
59
60
    // 2. Set url’s URL to urlRecord.
61
    // 3. Set url’s query object to a new URLSearchParams object.
62
0
    auto query_object = URLSearchParams::create(realm, query);
63
64
    // 4. Initialize url’s query object with query.
65
0
    auto result_url = DOMURL::create(realm, url_record, move(query_object));
66
67
    // 5. Set url’s query object’s URL object to url.
68
0
    result_url->m_query->m_url = result_url;
69
70
0
    return result_url;
71
0
}
72
73
// https://url.spec.whatwg.org/#dom-url-parse
74
JS::GCPtr<DOMURL> DOMURL::parse_for_bindings(JS::VM& vm, String const& url, Optional<String> const& base)
75
0
{
76
0
    auto& realm = *vm.current_realm();
77
78
    // 1. Let parsedURL be the result of running the API URL parser on url with base, if given.
79
0
    auto parsed_url = parse_api_url(url, base);
80
81
    // 2. If parsedURL is failure, then return null.
82
0
    if (!parsed_url.has_value())
83
0
        return nullptr;
84
85
    // 3. Let url be a new URL object.
86
    // 4. Initialize url with parsedURL.
87
    // 5. Return url.
88
0
    return initialize_a_url(realm, parsed_url.value());
89
0
}
90
91
// https://url.spec.whatwg.org/#dom-url-url
92
WebIDL::ExceptionOr<JS::NonnullGCPtr<DOMURL>> DOMURL::construct_impl(JS::Realm& realm, String const& url, Optional<String> const& base)
93
0
{
94
    // 1. Let parsedURL be the result of running the API URL parser on url with base, if given.
95
0
    auto parsed_url = parse_api_url(url, base);
96
97
    // 2. If parsedURL is failure, then throw a TypeError.
98
0
    if (!parsed_url.has_value())
99
0
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid URL"sv };
100
101
    // 3. Initialize this with parsedURL.
102
0
    return initialize_a_url(realm, parsed_url.value());
103
0
}
104
105
DOMURL::DOMURL(JS::Realm& realm, URL::URL url, JS::NonnullGCPtr<URLSearchParams> query)
106
0
    : PlatformObject(realm)
107
0
    , m_url(move(url))
108
0
    , m_query(move(query))
109
0
{
110
0
}
111
112
0
DOMURL::~DOMURL() = default;
113
114
void DOMURL::initialize(JS::Realm& realm)
115
0
{
116
0
    Base::initialize(realm);
117
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE_WITH_CUSTOM_NAME(DOMURL, URL);
118
0
}
119
120
void DOMURL::visit_edges(Cell::Visitor& visitor)
121
0
{
122
0
    Base::visit_edges(visitor);
123
0
    visitor.visit(m_query);
124
0
}
125
126
// https://w3c.github.io/FileAPI/#dfn-createObjectURL
127
WebIDL::ExceptionOr<String> DOMURL::create_object_url(JS::VM& vm, JS::NonnullGCPtr<FileAPI::Blob> object)
128
0
{
129
    // The createObjectURL(obj) static method must return the result of adding an entry to the blob URL store for obj.
130
0
    return TRY_OR_THROW_OOM(vm, FileAPI::add_entry_to_blob_url_store(object));
131
0
}
132
133
// https://w3c.github.io/FileAPI/#dfn-revokeObjectURL
134
WebIDL::ExceptionOr<void> DOMURL::revoke_object_url(JS::VM& vm, StringView url)
135
0
{
136
    // 1. Let url record be the result of parsing url.
137
0
    auto url_record = parse(url);
138
139
    // 2. If url record’s scheme is not "blob", return.
140
0
    if (url_record.scheme() != "blob"sv)
141
0
        return {};
142
143
    // 3. Let origin be the origin of url record.
144
0
    auto origin = url_record.origin();
145
146
    // 4. Let settings be the current settings object.
147
0
    auto& settings = HTML::current_settings_object();
148
149
    // 5. If origin is not same origin with settings’s origin, return.
150
0
    if (!origin.is_same_origin(settings.origin()))
151
0
        return {};
152
153
    // 6. Remove an entry from the Blob URL Store for url.
154
0
    TRY_OR_THROW_OOM(vm, FileAPI::remove_entry_from_blob_url_store(url));
155
0
    return {};
156
0
}
157
158
// https://url.spec.whatwg.org/#dom-url-canparse
159
bool DOMURL::can_parse(JS::VM&, String const& url, Optional<String> const& base)
160
0
{
161
    // 1. Let parsedURL be the result of running the API URL parser on url with base, if given.
162
0
    auto parsed_url = parse_api_url(url, base);
163
164
    // 2. If parsedURL is failure, then return false.
165
0
    if (!parsed_url.has_value())
166
0
        return false;
167
168
    // 3. Return true.
169
0
    return true;
170
0
}
171
172
// https://url.spec.whatwg.org/#dom-url-href
173
WebIDL::ExceptionOr<String> DOMURL::href() const
174
0
{
175
0
    auto& vm = realm().vm();
176
177
    // The href getter steps and the toJSON() method steps are to return the serialization of this’s URL.
178
0
    return TRY_OR_THROW_OOM(vm, String::from_byte_string(m_url.serialize()));
179
0
}
180
181
// https://url.spec.whatwg.org/#dom-url-tojson
182
WebIDL::ExceptionOr<String> DOMURL::to_json() const
183
0
{
184
0
    auto& vm = realm().vm();
185
186
    // The href getter steps and the toJSON() method steps are to return the serialization of this’s URL.
187
0
    return TRY_OR_THROW_OOM(vm, String::from_byte_string(m_url.serialize()));
188
0
}
189
190
// https://url.spec.whatwg.org/#ref-for-dom-url-href②
191
WebIDL::ExceptionOr<void> DOMURL::set_href(String const& href)
192
0
{
193
    // 1. Let parsedURL be the result of running the basic URL parser on the given value.
194
0
    URL::URL parsed_url = href;
195
196
    // 2. If parsedURL is failure, then throw a TypeError.
197
0
    if (!parsed_url.is_valid())
198
0
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid URL"sv };
199
200
    // 3. Set this’s URL to parsedURL.
201
0
    m_url = move(parsed_url);
202
203
    // 4. Empty this’s query object’s list.
204
0
    m_query->m_list.clear();
205
206
    // 5. Let query be this’s URL’s query.
207
0
    auto query = m_url.query();
208
209
    // 6. If query is non-null, then set this’s query object’s list to the result of parsing query.
210
0
    if (query.has_value())
211
0
        m_query->m_list = url_decode(*query);
212
0
    return {};
213
0
}
214
215
// https://url.spec.whatwg.org/#dom-url-origin
216
WebIDL::ExceptionOr<String> DOMURL::origin() const
217
0
{
218
0
    auto& vm = realm().vm();
219
220
    // The origin getter steps are to return the serialization of this’s URL’s origin. [HTML]
221
0
    return TRY_OR_THROW_OOM(vm, String::from_byte_string(m_url.origin().serialize()));
222
0
}
223
224
// https://url.spec.whatwg.org/#dom-url-protocol
225
WebIDL::ExceptionOr<String> DOMURL::protocol() const
226
0
{
227
0
    auto& vm = realm().vm();
228
229
    // The protocol getter steps are to return this’s URL’s scheme, followed by U+003A (:).
230
0
    return TRY_OR_THROW_OOM(vm, String::formatted("{}:", m_url.scheme()));
231
0
}
232
233
// https://url.spec.whatwg.org/#ref-for-dom-url-protocol%E2%91%A0
234
WebIDL::ExceptionOr<void> DOMURL::set_protocol(String const& protocol)
235
0
{
236
0
    auto& vm = realm().vm();
237
238
    // The protocol setter steps are to basic URL parse the given value, followed by U+003A (:), with this’s URL as
239
    // url and scheme start state as state override.
240
0
    (void)URL::Parser::basic_parse(TRY_OR_THROW_OOM(vm, String::formatted("{}:", protocol)), {}, &m_url, URL::Parser::State::SchemeStart);
241
0
    return {};
242
0
}
243
244
// https://url.spec.whatwg.org/#dom-url-username
245
String const& DOMURL::username() const
246
0
{
247
    // The username getter steps are to return this’s URL’s username.
248
0
    return m_url.username();
249
0
}
250
251
// https://url.spec.whatwg.org/#ref-for-dom-url-username%E2%91%A0
252
void DOMURL::set_username(String const& username)
253
0
{
254
    // 1. If this’s URL cannot have a username/password/port, then return.
255
0
    if (m_url.cannot_have_a_username_or_password_or_port())
256
0
        return;
257
258
    // 2. Set the username given this’s URL and the given value.
259
0
    m_url.set_username(username);
260
0
}
261
262
// https://url.spec.whatwg.org/#dom-url-password
263
String const& DOMURL::password() const
264
0
{
265
    // The password getter steps are to return this’s URL’s password.
266
0
    return m_url.password();
267
0
}
268
269
// https://url.spec.whatwg.org/#ref-for-dom-url-password%E2%91%A0
270
void DOMURL::set_password(String const& password)
271
0
{
272
    // 1. If this’s URL cannot have a username/password/port, then return.
273
0
    if (m_url.cannot_have_a_username_or_password_or_port())
274
0
        return;
275
276
    // 2. Set the password given this’s URL and the given value.
277
0
    m_url.set_password(password);
278
0
}
279
280
// https://url.spec.whatwg.org/#dom-url-host
281
WebIDL::ExceptionOr<String> DOMURL::host() const
282
0
{
283
0
    auto& vm = realm().vm();
284
285
    // 1. Let url be this’s URL.
286
0
    auto& url = m_url;
287
288
    // 2. If url’s host is null, then return the empty string.
289
0
    if (url.host().has<Empty>())
290
0
        return String {};
291
292
    // 3. If url’s port is null, return url’s host, serialized.
293
0
    if (!url.port().has_value())
294
0
        return TRY_OR_THROW_OOM(vm, url.serialized_host());
295
296
    // 4. Return url’s host, serialized, followed by U+003A (:) and url’s port, serialized.
297
0
    return TRY_OR_THROW_OOM(vm, String::formatted("{}:{}", TRY_OR_THROW_OOM(vm, url.serialized_host()), *url.port()));
298
0
}
299
300
// https://url.spec.whatwg.org/#dom-url-hostref-for-dom-url-host%E2%91%A0
301
void DOMURL::set_host(String const& host)
302
0
{
303
    // 1. If this’s URL’s cannot-be-a-base-URL is true, then return.
304
0
    if (m_url.cannot_be_a_base_url())
305
0
        return;
306
307
    // 2. Basic URL parse the given value with this’s URL as url and host state as state override.
308
0
    (void)URL::Parser::basic_parse(host, {}, &m_url, URL::Parser::State::Host);
309
0
}
310
311
// https://url.spec.whatwg.org/#dom-url-hostname
312
WebIDL::ExceptionOr<String> DOMURL::hostname() const
313
0
{
314
0
    auto& vm = realm().vm();
315
316
    // 1. If this’s URL’s host is null, then return the empty string.
317
0
    if (m_url.host().has<Empty>())
318
0
        return String {};
319
320
    // 2. Return this’s URL’s host, serialized.
321
0
    return TRY_OR_THROW_OOM(vm, m_url.serialized_host());
322
0
}
323
324
// https://url.spec.whatwg.org/#ref-for-dom-url-hostname①
325
void DOMURL::set_hostname(String const& hostname)
326
0
{
327
    // 1. If this’s URL’s cannot-be-a-base-URL is true, then return.
328
0
    if (m_url.cannot_be_a_base_url())
329
0
        return;
330
331
    // 2. Basic URL parse the given value with this’s URL as url and hostname state as state override.
332
0
    (void)URL::Parser::basic_parse(hostname, {}, &m_url, URL::Parser::State::Hostname);
333
0
}
334
335
// https://url.spec.whatwg.org/#dom-url-port
336
WebIDL::ExceptionOr<String> DOMURL::port() const
337
0
{
338
0
    auto& vm = realm().vm();
339
340
    // 1. If this’s URL’s port is null, then return the empty string.
341
0
    if (!m_url.port().has_value())
342
0
        return String {};
343
344
    // 2. Return this’s URL’s port, serialized.
345
0
    return TRY_OR_THROW_OOM(vm, String::formatted("{}", *m_url.port()));
346
0
}
347
348
// https://url.spec.whatwg.org/#ref-for-dom-url-port%E2%91%A0
349
void DOMURL::set_port(String const& port)
350
0
{
351
    // 1. If this’s URL cannot have a username/password/port, then return.
352
0
    if (m_url.cannot_have_a_username_or_password_or_port())
353
0
        return;
354
355
    // 2. If the given value is the empty string, then set this’s URL’s port to null.
356
0
    if (port.is_empty()) {
357
0
        m_url.set_port({});
358
0
    }
359
    // 3. Otherwise, basic URL parse the given value with this’s URL as url and port state as state override.
360
0
    else {
361
0
        (void)URL::Parser::basic_parse(port, {}, &m_url, URL::Parser::State::Port);
362
0
    }
363
0
}
364
365
// https://url.spec.whatwg.org/#dom-url-pathname
366
String DOMURL::pathname() const
367
0
{
368
    // The pathname getter steps are to return the result of URL path serializing this’s URL.
369
0
    return m_url.serialize_path();
370
0
}
371
372
// https://url.spec.whatwg.org/#ref-for-dom-url-pathname%E2%91%A0
373
void DOMURL::set_pathname(String const& pathname)
374
0
{
375
    // FIXME: These steps no longer match the speci.
376
    // 1. If this’s URL’s cannot-be-a-base-URL is true, then return.
377
0
    if (m_url.cannot_be_a_base_url())
378
0
        return;
379
380
    // 2. Empty this’s URL’s path.
381
0
    m_url.set_paths({});
382
383
    // 3. Basic URL parse the given value with this’s URL as url and path start state as state override.
384
0
    (void)URL::Parser::basic_parse(pathname, {}, &m_url, URL::Parser::State::PathStart);
385
0
}
386
387
// https://url.spec.whatwg.org/#dom-url-search
388
WebIDL::ExceptionOr<String> DOMURL::search() const
389
0
{
390
0
    auto& vm = realm().vm();
391
392
    // 1. If this’s URL’s query is either null or the empty string, then return the empty string.
393
0
    if (!m_url.query().has_value() || m_url.query()->is_empty())
394
0
        return String {};
395
396
    // 2. Return U+003F (?), followed by this’s URL’s query.
397
0
    return TRY_OR_THROW_OOM(vm, String::formatted("?{}", *m_url.query()));
398
0
}
399
400
// https://url.spec.whatwg.org/#ref-for-dom-url-search%E2%91%A0
401
void DOMURL::set_search(String const& search)
402
0
{
403
    // 1. Let url be this’s URL.
404
0
    auto& url = m_url;
405
406
    // 2. If the given value is the empty string:
407
0
    if (search.is_empty()) {
408
        // 1. Set url’s query to null.
409
0
        url.set_query({});
410
411
        // 2. Empty this’s query object’s list.
412
0
        m_query->m_list.clear();
413
414
        // 3. Potentially strip trailing spaces from an opaque path with this.
415
0
        strip_trailing_spaces_from_an_opaque_path(*this);
416
417
        // 4. Return.
418
0
        return;
419
0
    }
420
421
    // 3. Let input be the given value with a single leading U+003F (?) removed, if any.
422
0
    auto search_as_string_view = search.bytes_as_string_view();
423
0
    auto input = search_as_string_view.substring_view(search_as_string_view.starts_with('?'));
424
425
    // 4. Set url’s query to the empty string.
426
0
    url.set_query(String {});
427
428
    // 5. Basic URL parse input with url as url and query state as state override.
429
0
    (void)URL::Parser::basic_parse(input, {}, &url, URL::Parser::State::Query);
430
431
    // 6. Set this’s query object’s list to the result of parsing input.
432
0
    m_query->m_list = url_decode(input);
433
0
}
434
435
// https://url.spec.whatwg.org/#dom-url-searchparams
436
JS::NonnullGCPtr<URLSearchParams const> DOMURL::search_params() const
437
0
{
438
    // The searchParams getter steps are to return this’s query object.
439
0
    return m_query;
440
0
}
441
442
// https://url.spec.whatwg.org/#dom-url-hash
443
WebIDL::ExceptionOr<String> DOMURL::hash() const
444
0
{
445
0
    auto& vm = realm().vm();
446
447
    // 1. If this’s URL’s fragment is either null or the empty string, then return the empty string.
448
0
    if (!m_url.fragment().has_value() || m_url.fragment()->is_empty())
449
0
        return String {};
450
451
    // 2. Return U+0023 (#), followed by this’s URL’s fragment.
452
0
    return TRY_OR_THROW_OOM(vm, String::formatted("#{}", m_url.fragment()));
453
0
}
454
455
// https://url.spec.whatwg.org/#ref-for-dom-url-hash%E2%91%A0
456
void DOMURL::set_hash(String const& hash)
457
0
{
458
    // 1. If the given value is the empty string:
459
0
    if (hash.is_empty()) {
460
        // 1. Set this’s URL’s fragment to null.
461
0
        m_url.set_fragment({});
462
463
        // 2. Potentially strip trailing spaces from an opaque path with this.
464
0
        strip_trailing_spaces_from_an_opaque_path(*this);
465
466
        // 3. Return.
467
0
        return;
468
0
    }
469
470
    // 2. Let input be the given value with a single leading U+0023 (#) removed, if any.
471
0
    auto hash_as_string_view = hash.bytes_as_string_view();
472
0
    auto input = hash_as_string_view.substring_view(hash_as_string_view.starts_with('#'));
473
474
    // 3. Set this’s URL’s fragment to the empty string.
475
0
    m_url.set_fragment(String {});
476
477
    // 4. Basic URL parse input with this’s URL as url and fragment state as state override.
478
0
    (void)URL::Parser::basic_parse(input, {}, &m_url, URL::Parser::State::Fragment);
479
0
}
480
481
// https://url.spec.whatwg.org/#concept-domain
482
bool host_is_domain(URL::Host const& host)
483
0
{
484
    // A domain is a non-empty ASCII string that identifies a realm within a network.
485
0
    return host.has<String>() && host.get<String>() != String {};
486
0
}
487
488
// https://url.spec.whatwg.org/#potentially-strip-trailing-spaces-from-an-opaque-path
489
void strip_trailing_spaces_from_an_opaque_path(DOMURL& url)
490
0
{
491
    // 1. If url’s URL does not have an opaque path, then return.
492
    // FIXME: Reimplement this step once we modernize the URL implementation to meet the spec.
493
0
    if (!url.cannot_be_a_base_url())
494
0
        return;
495
496
    // 2. If url’s URL’s fragment is non-null, then return.
497
0
    if (url.fragment().has_value())
498
0
        return;
499
500
    // 3. If url’s URL’s query is non-null, then return.
501
0
    if (url.query().has_value())
502
0
        return;
503
504
    // 4. Remove all trailing U+0020 SPACE code points from url’s URL’s path.
505
    // NOTE: At index 0 since the first step tells us that the URL only has one path segment.
506
0
    auto opaque_path = url.path_segment_at_index(0);
507
0
    auto trimmed_path = opaque_path.trim(" "sv, TrimMode::Right);
508
0
    url.set_paths({ trimmed_path });
509
0
}
510
511
// https://url.spec.whatwg.org/#concept-url-parser
512
URL::URL parse(StringView input, Optional<URL::URL> const& base_url, Optional<StringView> encoding)
513
0
{
514
    // FIXME: We should probably have an extended version of URL::URL for LibWeb instead of standalone functions like this.
515
516
    // 1. Let url be the result of running the basic URL parser on input with base and encoding.
517
0
    auto url = URL::Parser::basic_parse(input, base_url, {}, {}, encoding);
518
519
    // 2. If url is failure, return failure.
520
0
    if (!url.is_valid())
521
0
        return {};
522
523
    // 3. If url’s scheme is not "blob", return url.
524
0
    if (url.scheme() != "blob")
525
0
        return url;
526
527
    // 4. Set url’s blob URL entry to the result of resolving the blob URL url, if that did not return failure, and null otherwise.
528
0
    auto blob_url_entry = FileAPI::resolve_a_blob_url(url);
529
0
    if (blob_url_entry.has_value()) {
530
0
        url.set_blob_url_entry(URL::BlobURLEntry {
531
0
            .type = blob_url_entry->object->type(),
532
0
            .byte_buffer = MUST(ByteBuffer::copy(blob_url_entry->object->raw_bytes())),
533
0
            .environment_origin = blob_url_entry->environment->origin(),
534
0
        });
535
0
    }
536
537
    // 5. Return url
538
0
    return url;
539
0
}
540
541
}