Serenity Operating System
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}