File
Blob: src/workerd/api/url-standard.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "url-standard.h" |
| 6 | |
| 7 | #include "blob.h" |
| 8 | #include "util.h" |
| 9 | |
| 10 | #include <workerd/io/features.h> |
| 11 | |
| 12 | #include <unicode/uchar.h> |
| 13 | #include <unicode/uidna.h> |
| 14 | #include <unicode/ustring.h> |
| 15 | #include <unicode/utf8.h> |
| 16 | |
| 17 | #include <kj/array.h> |
| 18 | |
| 19 | #include <algorithm> |
| 20 | #include <cmath> |
| 21 | #include <cstring> |
| 22 | #include <map> |
| 23 | #include <numeric> |
| 24 | |
| 25 | #if _WIN32 |
| 26 | #include <ws2tcpip.h> |
| 27 | #undef RELATIVE |
| 28 | #else |
| 29 | #include <arpa/inet.h> |
| 30 | #endif |
| 31 | |
| 32 | namespace workerd::api::url { |
| 33 | |
| 34 | namespace { |
| 35 | jsg::Url parseImpl(kj::StringPtr url, kj::Maybe<kj::StringPtr> maybeBase) { |
| 36 | return JSG_REQUIRE_NONNULL(jsg::Url::tryParse(url, maybeBase), TypeError, "Invalid URL string."); |
| 37 | } |
| 38 | } // namespace |
| 39 | |
| 40 | jsg::Ref<URL> URL::constructor( |
| 41 | jsg::Lock& js, jsg::USVString url, jsg::Optional<jsg::USVString> base) { |
| 42 | return js.alloc<URL>(kj::mv(url), base.map([](jsg::USVString& base) { return base.asPtr(); })); |
| 43 | } |
| 44 | |
| 45 | URL::URL(kj::StringPtr url, kj::Maybe<kj::StringPtr> base): inner(parseImpl(url, base)) {} |
| 46 | |
| 47 | URL::~URL() noexcept(false) { |
| 48 | KJ_IF_SOME(searchParams, maybeSearchParams) { |
| 49 | searchParams->maybeUrl = kj::none; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | bool URL::canParse(jsg::USVString url, jsg::Optional<jsg::USVString> maybeBase) { |
| 54 | return jsg::Url::canParse(url, maybeBase.map([](jsg::USVString& str) { return str.asPtr(); })); |
| 55 | } |
| 56 | |
| 57 | jsg::JsString URL::createObjectURL( |
| 58 | jsg::Lock& js, kj::OneOf<jsg::Ref<File>, jsg::Ref<Blob>> object) { |
| 59 | // TODO(soon): Implement this |
| 60 | JSG_FAIL_REQUIRE(Error, "URL.createObjectURL() is not implemented"_kj); |
| 61 | } |
| 62 | |
| 63 | void URL::revokeObjectURL(jsg::Lock& js, jsg::USVString object_url) { |
| 64 | // TODO(soon): Implement this |
| 65 | JSG_FAIL_REQUIRE(Error, "URL.revokeObjectURL() is not implemented"_kj); |
| 66 | } |
| 67 | |
| 68 | jsg::Ref<URLSearchParams> URL::getSearchParams(jsg::Lock& js) { |
| 69 | KJ_IF_SOME(searchParams, maybeSearchParams) { |
| 70 | return searchParams.addRef(); |
| 71 | } |
| 72 | return maybeSearchParams.emplace(js.alloc<URLSearchParams>(inner.getSearch(), *this)).addRef(); |
| 73 | } |
| 74 | |
| 75 | kj::Array<const char> URL::getOrigin() { |
| 76 | return inner.getOrigin(); |
| 77 | } |
| 78 | |
| 79 | kj::ArrayPtr<const char> URL::getHref() { |
| 80 | return inner.getHref(); |
| 81 | } |
| 82 | |
| 83 | void URL::setHref(jsg::Lock& js, jsg::USVString value) { |
| 84 | // Href setter is the only place in URL parser that is allowed to throw except the constructor. |
| 85 | if (!inner.setHref(value)) { |
| 86 | auto context = jsg::TypeErrorContext::setterArgument(typeid(URL), "href"); |
| 87 | jsg::throwTypeError(js.v8Isolate, context, "valid URL string"); |
| 88 | } |
| 89 | KJ_IF_SOME(searchParams, maybeSearchParams) { |
| 90 | searchParams->reset(); |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | kj::ArrayPtr<const char> URL::getProtocol() { |
| 95 | return inner.getProtocol(); |
| 96 | } |
| 97 | |
| 98 | void URL::setProtocol(jsg::USVString value) { |
| 99 | inner.setProtocol(value); |
| 100 | } |
| 101 | |
| 102 | kj::ArrayPtr<const char> URL::getUsername() { |
| 103 | return inner.getUsername(); |
| 104 | } |
| 105 | |
| 106 | void URL::setUsername(jsg::USVString value) { |
| 107 | inner.setUsername(value); |
| 108 | } |
| 109 | |
| 110 | kj::ArrayPtr<const char> URL::getPassword() { |
| 111 | return inner.getPassword(); |
| 112 | } |
| 113 | |
| 114 | void URL::setPassword(jsg::USVString value) { |
| 115 | inner.setPassword(value); |
| 116 | } |
| 117 | |
| 118 | kj::ArrayPtr<const char> URL::getHost() { |
| 119 | return inner.getHost(); |
| 120 | } |
| 121 | |
| 122 | void URL::setHost(jsg::USVString value) { |
| 123 | inner.setHost(value); |
| 124 | } |
| 125 | |
| 126 | kj::ArrayPtr<const char> URL::getHostname() { |
| 127 | return inner.getHostname(); |
| 128 | } |
| 129 | |
| 130 | void URL::setHostname(jsg::USVString value) { |
| 131 | inner.setHostname(value); |
| 132 | } |
| 133 | |
| 134 | kj::ArrayPtr<const char> URL::getPort() { |
| 135 | return inner.getPort(); |
| 136 | } |
| 137 | |
| 138 | void URL::setPort(jsg::USVString value) { |
| 139 | inner.setPort(kj::Maybe(value.asPtr())); |
| 140 | } |
| 141 | |
| 142 | kj::ArrayPtr<const char> URL::getPathname() { |
| 143 | return inner.getPathname(); |
| 144 | } |
| 145 | |
| 146 | void URL::setPathname(jsg::USVString value) { |
| 147 | inner.setPathname(value); |
| 148 | } |
| 149 | |
| 150 | kj::ArrayPtr<const char> URL::getSearch() { |
| 151 | return inner.getSearch(); |
| 152 | } |
| 153 | |
| 154 | void URL::setSearch(jsg::USVString value) { |
| 155 | inner.setSearch(kj::Maybe(value.asPtr())); |
| 156 | KJ_IF_SOME(searchParams, maybeSearchParams) { |
| 157 | searchParams->reset(); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | kj::ArrayPtr<const char> URL::getHash() { |
| 162 | return inner.getHash(); |
| 163 | } |
| 164 | |
| 165 | void URL::setHash(jsg::USVString hash) { |
| 166 | inner.setHash(kj::Maybe(hash.asPtr())); |
| 167 | } |
| 168 | |
| 169 | void URL::visitForGc(jsg::GcVisitor& visitor) { |
| 170 | visitor.visit(maybeSearchParams); |
| 171 | } |
| 172 | |
| 173 | // ====================================================================================== |
| 174 | namespace { |
| 175 | jsg::UrlSearchParams initSearchParams(const URLSearchParams::Initializer& init) { |
| 176 | KJ_SWITCH_ONEOF(init) { |
| 177 | KJ_CASE_ONEOF(pairs, URLSearchParams::StringPairs) { |
| 178 | jsg::UrlSearchParams params; |
| 179 | for (auto& item: pairs) { |
| 180 | JSG_REQUIRE(item.size() == 2, TypeError, "Invalid URL search params sequence."); |
| 181 | params.append(item[0], item[1]); |
| 182 | } |
| 183 | return kj::mv(params); |
| 184 | } |
| 185 | KJ_CASE_ONEOF(dict, jsg::Dict<jsg::USVString, jsg::USVString>) { |
| 186 | kj::HashMap<kj::StringPtr, kj::StringPtr> paramsMap; |
| 187 | for (auto& item: dict.fields) { |
| 188 | paramsMap.upsert(item.name, item.value); |
| 189 | } |
| 190 | |
| 191 | jsg::UrlSearchParams params; |
| 192 | for (auto& item: paramsMap) { |
| 193 | params.append(item.key, item.value); |
| 194 | } |
| 195 | return kj::mv(params); |
| 196 | } |
| 197 | KJ_CASE_ONEOF(str, jsg::USVString) { |
| 198 | return JSG_REQUIRE_NONNULL( |
| 199 | jsg::UrlSearchParams::tryParse(str), TypeError, "Invalid URL search params string."); |
| 200 | } |
| 201 | } |
| 202 | KJ_UNREACHABLE; |
| 203 | } |
| 204 | |
| 205 | jsg::UrlSearchParams initFromSearch(kj::Maybe<kj::ArrayPtr<const char>>& maybeQuery) { |
| 206 | KJ_IF_SOME(query, maybeQuery) { |
| 207 | return JSG_REQUIRE_NONNULL( |
| 208 | jsg::UrlSearchParams::tryParse(query), TypeError, "Invalid URL search params string."); |
| 209 | } |
| 210 | return jsg::UrlSearchParams(); |
| 211 | } |
| 212 | } // namespace |
| 213 | |
| 214 | jsg::Ref<URLSearchParams> URLSearchParams::constructor( |
| 215 | jsg::Lock& js, jsg::Optional<Initializer> init) { |
| 216 | return js.alloc<URLSearchParams>(kj::mv(init).orDefault(jsg::USVString())); |
| 217 | } |
| 218 | |
| 219 | URLSearchParams::URLSearchParams(Initializer initializer): inner(initSearchParams(initializer)) {} |
| 220 | |
| 221 | URLSearchParams::URLSearchParams(kj::Maybe<kj::ArrayPtr<const char>> maybeQuery, URL& url) |
| 222 | : inner(initFromSearch(maybeQuery)), |
| 223 | maybeUrl(url) {} |
| 224 | |
| 225 | uint URLSearchParams::getSize() { |
| 226 | return inner.size(); |
| 227 | } |
| 228 | |
| 229 | void URLSearchParams::update() { |
| 230 | KJ_IF_SOME(url, maybeUrl) { |
| 231 | auto serialized = toString(); |
| 232 | url.inner.setSearch(kj::Maybe(serialized.asPtr())); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | void URLSearchParams::reset() { |
| 237 | KJ_IF_SOME(url, maybeUrl) { |
| 238 | auto search = kj::Maybe(url.inner.getSearch()); |
| 239 | inner.reset(search); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | void URLSearchParams::append(jsg::USVString name, jsg::USVString value) { |
| 244 | inner.append(name, value); |
| 245 | update(); |
| 246 | } |
| 247 | |
| 248 | void URLSearchParams::delete_( |
| 249 | jsg::Lock& js, jsg::USVString name, jsg::Optional<jsg::USVString> value) { |
| 250 | KJ_ON_SCOPE_SUCCESS(update()); |
| 251 | if (FeatureFlags::get(js).getUrlSearchParamsDeleteHasValueArg()) { |
| 252 | // The whatwg url spec was updated to add a second optional argument to delete() |
| 253 | // and has(). While it was determined that it likely didn't break browser users, |
| 254 | // the change could break at least a couple existing deployed workers so we have |
| 255 | // to gate support behind a compat flag. |
| 256 | KJ_IF_SOME(v, value) { |
| 257 | inner.delete_(name, kj::Maybe(v.asPtr())); |
| 258 | return; |
| 259 | } |
| 260 | } |
| 261 | inner.delete_(name); |
| 262 | } |
| 263 | |
| 264 | kj::Maybe<kj::ArrayPtr<const char>> URLSearchParams::get(jsg::USVString name) { |
| 265 | return inner.get(name); |
| 266 | } |
| 267 | |
| 268 | kj::Array<kj::ArrayPtr<const char>> URLSearchParams::getAll(jsg::USVString name) { |
| 269 | return inner.getAll(name); |
| 270 | } |
| 271 | |
| 272 | bool URLSearchParams::has(jsg::Lock& js, jsg::USVString name, jsg::Optional<jsg::USVString> value) { |
| 273 | if (FeatureFlags::get(js).getUrlSearchParamsDeleteHasValueArg()) { |
| 274 | // The whatwg url spec was updated to add a second optional argument to delete() |
| 275 | // and has(). While it was determined that it likely didn't break browser users, |
| 276 | // the change could break at least a couple existing deployed workers so we have |
| 277 | // to gate support behind a compat flag. |
| 278 | KJ_IF_SOME(v, value) { |
| 279 | return inner.has(name, kj::Maybe(v.asPtr())); |
| 280 | } |
| 281 | } |
| 282 | return inner.has(name); |
| 283 | } |
| 284 | |
| 285 | void URLSearchParams::set(jsg::USVString name, jsg::USVString value) { |
| 286 | inner.set(name, value); |
| 287 | update(); |
| 288 | } |
| 289 | |
| 290 | void URLSearchParams::sort() { |
| 291 | inner.sort(); |
| 292 | update(); |
| 293 | } |
| 294 | |
| 295 | jsg::Ref<URLSearchParams::EntryIterator> URLSearchParams::entries(jsg::Lock& js) { |
| 296 | return js.alloc<URLSearchParams::EntryIterator>( |
| 297 | IteratorState<jsg::UrlSearchParams::EntryIterator>(JSG_THIS, inner.getEntries())); |
| 298 | } |
| 299 | |
| 300 | jsg::Ref<URLSearchParams::KeyIterator> URLSearchParams::keys(jsg::Lock& js) { |
| 301 | return js.alloc<URLSearchParams::KeyIterator>( |
| 302 | IteratorState<jsg::UrlSearchParams::KeyIterator>(JSG_THIS, inner.getKeys())); |
| 303 | } |
| 304 | |
| 305 | jsg::Ref<URLSearchParams::ValueIterator> URLSearchParams::values(jsg::Lock& js) { |
| 306 | return alloc<URLSearchParams::ValueIterator>( |
| 307 | IteratorState<jsg::UrlSearchParams::ValueIterator>(JSG_THIS, inner.getValues())); |
| 308 | } |
| 309 | |
| 310 | kj::Maybe<kj::Array<kj::ArrayPtr<const char>>> URLSearchParams::entryIteratorNext( |
| 311 | jsg::Lock& js, URLSearchParams::IteratorState<jsg::UrlSearchParams::EntryIterator>& state) { |
| 312 | return state.inner.next().map([](const jsg::UrlSearchParams::EntryIterator::Entry& entry) { |
| 313 | return kj::arr(entry.key, entry.value); |
| 314 | }); |
| 315 | } |
| 316 | |
| 317 | kj::Maybe<kj::ArrayPtr<const char>> URLSearchParams::keyIteratorNext( |
| 318 | jsg::Lock& js, URLSearchParams::IteratorState<jsg::UrlSearchParams::KeyIterator>& state) { |
| 319 | return state.inner.next(); |
| 320 | } |
| 321 | |
| 322 | kj::Maybe<kj::ArrayPtr<const char>> URLSearchParams::valueIteratorNext( |
| 323 | jsg::Lock& js, URLSearchParams::IteratorState<jsg::UrlSearchParams::ValueIterator>& state) { |
| 324 | return state.inner.next(); |
| 325 | } |
| 326 | |
| 327 | void URLSearchParams::forEach(jsg::Lock& js, |
| 328 | jsg::Function<void(kj::StringPtr, kj::StringPtr, jsg::Ref<URLSearchParams>)> callback, |
| 329 | jsg::Optional<jsg::JsValue> thisArg) { |
| 330 | auto receiver = js.undefined(); |
| 331 | KJ_IF_SOME(arg, thisArg) { |
| 332 | if (!arg.isNullOrUndefined()) { |
| 333 | receiver = arg; |
| 334 | } |
| 335 | } |
| 336 | callback.setReceiver(js.v8Ref<v8::Value>(receiver)); |
| 337 | |
| 338 | auto entries = inner.getEntries(); |
| 339 | while (entries.hasNext()) { |
| 340 | auto next = KJ_ASSERT_NONNULL(entries.next()); |
| 341 | kj::String value( |
| 342 | const_cast<char*>(next.value.begin()), next.value.size(), kj::NullArrayDisposer::instance); |
| 343 | kj::String key( |
| 344 | const_cast<char*>(next.key.begin()), next.key.size(), kj::NullArrayDisposer::instance); |
| 345 | callback(js, value, key, JSG_THIS); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | kj::String URLSearchParams::toString() { |
| 350 | return kj::str(inner); |
| 351 | } |
| 352 | |
| 353 | } // namespace workerd::api::url |