1'use strict'; 2 3const { 4 Array, 5 Number, 6 ObjectCreate, 7 ObjectDefineProperties, 8 ObjectDefineProperty, 9 ObjectGetOwnPropertySymbols, 10 ObjectGetPrototypeOf, 11 ObjectKeys, 12 ReflectGetOwnPropertyDescriptor, 13 ReflectOwnKeys, 14 Symbol, 15 SymbolIterator, 16 SymbolToStringTag, 17} = primordials; 18 19const { inspect } = require('internal/util/inspect'); 20const { 21 encodeStr, 22 hexTable, 23 isHexTable 24} = require('internal/querystring'); 25 26const { getConstructorOf, removeColors } = require('internal/util'); 27const { 28 ERR_ARG_NOT_ITERABLE, 29 ERR_INVALID_ARG_TYPE, 30 ERR_INVALID_ARG_VALUE, 31 ERR_INVALID_CALLBACK, 32 ERR_INVALID_FILE_URL_HOST, 33 ERR_INVALID_FILE_URL_PATH, 34 ERR_INVALID_THIS, 35 ERR_INVALID_TUPLE, 36 ERR_INVALID_URL, 37 ERR_INVALID_URL_SCHEME, 38 ERR_MISSING_ARGS 39} = require('internal/errors').codes; 40const { 41 CHAR_AMPERSAND, 42 CHAR_BACKWARD_SLASH, 43 CHAR_EQUAL, 44 CHAR_FORWARD_SLASH, 45 CHAR_LOWERCASE_A, 46 CHAR_LOWERCASE_Z, 47 CHAR_PERCENT, 48 CHAR_PLUS 49} = require('internal/constants'); 50const path = require('path'); 51 52// Lazy loaded for startup performance. 53let querystring; 54 55const { platform } = process; 56const isWindows = platform === 'win32'; 57 58const { 59 domainToASCII: _domainToASCII, 60 domainToUnicode: _domainToUnicode, 61 encodeAuth, 62 toUSVString: _toUSVString, 63 parse, 64 setURLConstructor, 65 URL_FLAGS_CANNOT_BE_BASE, 66 URL_FLAGS_HAS_FRAGMENT, 67 URL_FLAGS_HAS_HOST, 68 URL_FLAGS_HAS_PASSWORD, 69 URL_FLAGS_HAS_PATH, 70 URL_FLAGS_HAS_QUERY, 71 URL_FLAGS_HAS_USERNAME, 72 URL_FLAGS_IS_DEFAULT_SCHEME_PORT, 73 URL_FLAGS_SPECIAL, 74 kFragment, 75 kHost, 76 kHostname, 77 kPathStart, 78 kPort, 79 kQuery, 80 kSchemeStart 81} = internalBinding('url'); 82 83const context = Symbol('context'); 84const cannotBeBase = Symbol('cannot-be-base'); 85const cannotHaveUsernamePasswordPort = 86 Symbol('cannot-have-username-password-port'); 87const special = Symbol('special'); 88const searchParams = Symbol('query'); 89const kFormat = Symbol('format'); 90 91// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object 92const IteratorPrototype = ObjectGetPrototypeOf( 93 ObjectGetPrototypeOf([][SymbolIterator]()) 94); 95 96const unpairedSurrogateRe = 97 /(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/; 98function toUSVString(val) { 99 const str = `${val}`; 100 // As of V8 5.5, `str.search()` (and `unpairedSurrogateRe[@@search]()`) are 101 // slower than `unpairedSurrogateRe.exec()`. 102 const match = unpairedSurrogateRe.exec(str); 103 if (!match) 104 return str; 105 return _toUSVString(str, match.index); 106} 107 108// Refs: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque 109const kOpaqueOrigin = 'null'; 110 111// Refs: https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin 112function serializeTupleOrigin(scheme, host, port) { 113 return `${scheme}//${host}${port === null ? '' : `:${port}`}`; 114} 115 116// This class provides the internal state of a URL object. An instance of this 117// class is stored in every URL object and is accessed internally by setters 118// and getters. It roughly corresponds to the concept of a URL record in the 119// URL Standard, with a few differences. It is also the object transported to 120// the C++ binding. 121// Refs: https://url.spec.whatwg.org/#concept-url 122class URLContext { 123 constructor() { 124 this.flags = 0; 125 this.scheme = ':'; 126 this.username = ''; 127 this.password = ''; 128 this.host = null; 129 this.port = null; 130 this.path = []; 131 this.query = null; 132 this.fragment = null; 133 } 134} 135 136class URLSearchParams { 137 // URL Standard says the default value is '', but as undefined and '' have 138 // the same result, undefined is used to prevent unnecessary parsing. 139 // Default parameter is necessary to keep URLSearchParams.length === 0 in 140 // accordance with Web IDL spec. 141 constructor(init = undefined) { 142 if (init === null || init === undefined) { 143 this[searchParams] = []; 144 } else if (typeof init === 'object' || typeof init === 'function') { 145 const method = init[SymbolIterator]; 146 if (method === this[SymbolIterator]) { 147 // While the spec does not have this branch, we can use it as a 148 // shortcut to avoid having to go through the costly generic iterator. 149 const childParams = init[searchParams]; 150 this[searchParams] = childParams.slice(); 151 } else if (method !== null && method !== undefined) { 152 if (typeof method !== 'function') { 153 throw new ERR_ARG_NOT_ITERABLE('Query pairs'); 154 } 155 156 // Sequence<sequence<USVString>> 157 // Note: per spec we have to first exhaust the lists then process them 158 const pairs = []; 159 for (const pair of init) { 160 if ((typeof pair !== 'object' && typeof pair !== 'function') || 161 pair === null || 162 typeof pair[SymbolIterator] !== 'function') { 163 throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); 164 } 165 const convertedPair = []; 166 for (const element of pair) 167 convertedPair.push(toUSVString(element)); 168 pairs.push(convertedPair); 169 } 170 171 this[searchParams] = []; 172 for (const pair of pairs) { 173 if (pair.length !== 2) { 174 throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); 175 } 176 this[searchParams].push(pair[0], pair[1]); 177 } 178 } else { 179 // Record<USVString, USVString> 180 // Need to use reflection APIs for full spec compliance. 181 this[searchParams] = []; 182 const keys = ReflectOwnKeys(init); 183 for (let i = 0; i < keys.length; i++) { 184 const key = keys[i]; 185 const desc = ReflectGetOwnPropertyDescriptor(init, key); 186 if (desc !== undefined && desc.enumerable) { 187 const typedKey = toUSVString(key); 188 const typedValue = toUSVString(init[key]); 189 this[searchParams].push(typedKey, typedValue); 190 } 191 } 192 } 193 } else { 194 // USVString 195 init = toUSVString(init); 196 if (init[0] === '?') init = init.slice(1); 197 initSearchParams(this, init); 198 } 199 200 // "associated url object" 201 this[context] = null; 202 } 203 204 [inspect.custom](recurseTimes, ctx) { 205 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 206 throw new ERR_INVALID_THIS('URLSearchParams'); 207 } 208 209 if (typeof recurseTimes === 'number' && recurseTimes < 0) 210 return ctx.stylize('[Object]', 'special'); 211 212 const separator = ', '; 213 const innerOpts = { ...ctx }; 214 if (recurseTimes !== null) { 215 innerOpts.depth = recurseTimes - 1; 216 } 217 const innerInspect = (v) => inspect(v, innerOpts); 218 219 const list = this[searchParams]; 220 const output = []; 221 for (let i = 0; i < list.length; i += 2) 222 output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`); 223 224 const length = output.reduce( 225 (prev, cur) => prev + removeColors(cur).length + separator.length, 226 -separator.length 227 ); 228 if (length > ctx.breakLength) { 229 return `${this.constructor.name} {\n ${output.join(',\n ')} }`; 230 } else if (output.length) { 231 return `${this.constructor.name} { ${output.join(separator)} }`; 232 } 233 return `${this.constructor.name} {}`; 234 } 235} 236 237function onParseComplete(flags, protocol, username, password, 238 host, port, path, query, fragment) { 239 const ctx = this[context]; 240 ctx.flags = flags; 241 ctx.scheme = protocol; 242 ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : ''; 243 ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : ''; 244 ctx.port = port; 245 ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : []; 246 ctx.query = query; 247 ctx.fragment = fragment; 248 ctx.host = host; 249 if (!this[searchParams]) { // Invoked from URL constructor 250 this[searchParams] = new URLSearchParams(); 251 this[searchParams][context] = this; 252 } 253 initSearchParams(this[searchParams], query); 254} 255 256function onParseError(flags, input) { 257 throw new ERR_INVALID_URL(input); 258} 259 260function onParseProtocolComplete(flags, protocol, username, password, 261 host, port, path, query, fragment) { 262 const ctx = this[context]; 263 if ((flags & URL_FLAGS_SPECIAL) !== 0) { 264 ctx.flags |= URL_FLAGS_SPECIAL; 265 } else { 266 ctx.flags &= ~URL_FLAGS_SPECIAL; 267 } 268 ctx.scheme = protocol; 269 ctx.port = port; 270} 271 272function onParseHostnameComplete(flags, protocol, username, password, 273 host, port, path, query, fragment) { 274 const ctx = this[context]; 275 if ((flags & URL_FLAGS_HAS_HOST) !== 0) { 276 ctx.host = host; 277 ctx.flags |= URL_FLAGS_HAS_HOST; 278 } else { 279 ctx.host = null; 280 ctx.flags &= ~URL_FLAGS_HAS_HOST; 281 } 282} 283 284function onParsePortComplete(flags, protocol, username, password, 285 host, port, path, query, fragment) { 286 this[context].port = port; 287} 288 289function onParseHostComplete(flags, protocol, username, password, 290 host, port, path, query, fragment) { 291 onParseHostnameComplete.apply(this, arguments); 292 if (port !== null || ((flags & URL_FLAGS_IS_DEFAULT_SCHEME_PORT) !== 0)) 293 onParsePortComplete.apply(this, arguments); 294} 295 296function onParsePathComplete(flags, protocol, username, password, 297 host, port, path, query, fragment) { 298 const ctx = this[context]; 299 if ((flags & URL_FLAGS_HAS_PATH) !== 0) { 300 ctx.path = path; 301 ctx.flags |= URL_FLAGS_HAS_PATH; 302 } else { 303 ctx.path = []; 304 ctx.flags &= ~URL_FLAGS_HAS_PATH; 305 } 306 307 // The C++ binding may set host to empty string. 308 if ((flags & URL_FLAGS_HAS_HOST) !== 0) { 309 ctx.host = host; 310 ctx.flags |= URL_FLAGS_HAS_HOST; 311 } 312} 313 314function onParseSearchComplete(flags, protocol, username, password, 315 host, port, path, query, fragment) { 316 this[context].query = query; 317} 318 319function onParseHashComplete(flags, protocol, username, password, 320 host, port, path, query, fragment) { 321 this[context].fragment = fragment; 322} 323 324class URL { 325 constructor(input, base) { 326 // toUSVString is not needed. 327 input = `${input}`; 328 let base_context; 329 if (base !== undefined) { 330 base_context = new URL(base)[context]; 331 } 332 this[context] = new URLContext(); 333 parse(input, -1, base_context, undefined, onParseComplete.bind(this), 334 onParseError); 335 } 336 337 get [special]() { 338 return (this[context].flags & URL_FLAGS_SPECIAL) !== 0; 339 } 340 341 get [cannotBeBase]() { 342 return (this[context].flags & URL_FLAGS_CANNOT_BE_BASE) !== 0; 343 } 344 345 // https://url.spec.whatwg.org/#cannot-have-a-username-password-port 346 get [cannotHaveUsernamePasswordPort]() { 347 const { host, scheme } = this[context]; 348 return ((host == null || host === '') || 349 this[cannotBeBase] || 350 scheme === 'file:'); 351 } 352 353 [inspect.custom](depth, opts) { 354 if (this == null || 355 ObjectGetPrototypeOf(this[context]) !== URLContext.prototype) { 356 throw new ERR_INVALID_THIS('URL'); 357 } 358 359 if (typeof depth === 'number' && depth < 0) 360 return this; 361 362 const ctor = getConstructorOf(this); 363 364 const obj = ObjectCreate({ 365 constructor: ctor === null ? URL : ctor 366 }); 367 368 obj.href = this.href; 369 obj.origin = this.origin; 370 obj.protocol = this.protocol; 371 obj.username = this.username; 372 obj.password = this.password; 373 obj.host = this.host; 374 obj.hostname = this.hostname; 375 obj.port = this.port; 376 obj.pathname = this.pathname; 377 obj.search = this.search; 378 obj.searchParams = this.searchParams; 379 obj.hash = this.hash; 380 381 if (opts.showHidden) { 382 obj.cannotBeBase = this[cannotBeBase]; 383 obj.special = this[special]; 384 obj[context] = this[context]; 385 } 386 387 return inspect(obj, opts); 388 } 389} 390 391ObjectDefineProperties(URL.prototype, { 392 [kFormat]: { 393 enumerable: false, 394 configurable: false, 395 // eslint-disable-next-line func-name-matching 396 value: function format(options) { 397 if (options && typeof options !== 'object') 398 throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); 399 options = { 400 fragment: true, 401 unicode: false, 402 search: true, 403 auth: true, 404 ...options 405 }; 406 const ctx = this[context]; 407 let ret = ctx.scheme; 408 if (ctx.host !== null) { 409 ret += '//'; 410 const has_username = ctx.username !== ''; 411 const has_password = ctx.password !== ''; 412 if (options.auth && (has_username || has_password)) { 413 if (has_username) 414 ret += ctx.username; 415 if (has_password) 416 ret += `:${ctx.password}`; 417 ret += '@'; 418 } 419 ret += options.unicode ? 420 domainToUnicode(this.hostname) : this.hostname; 421 if (ctx.port !== null) 422 ret += `:${ctx.port}`; 423 } else if (ctx.scheme === 'file:') { 424 ret += '//'; 425 } 426 if (this.pathname) 427 ret += this.pathname; 428 if (options.search && ctx.query !== null) 429 ret += `?${ctx.query}`; 430 if (options.fragment && ctx.fragment !== null) 431 ret += `#${ctx.fragment}`; 432 return ret; 433 } 434 }, 435 [SymbolToStringTag]: { 436 configurable: true, 437 value: 'URL' 438 }, 439 toString: { 440 // https://heycam.github.io/webidl/#es-stringifier 441 writable: true, 442 enumerable: true, 443 configurable: true, 444 // eslint-disable-next-line func-name-matching 445 value: function toString() { 446 return this[kFormat]({}); 447 } 448 }, 449 href: { 450 enumerable: true, 451 configurable: true, 452 get() { 453 return this[kFormat]({}); 454 }, 455 set(input) { 456 // toUSVString is not needed. 457 input = `${input}`; 458 parse(input, -1, undefined, undefined, onParseComplete.bind(this), 459 onParseError); 460 } 461 }, 462 origin: { // readonly 463 enumerable: true, 464 configurable: true, 465 get() { 466 // Refs: https://url.spec.whatwg.org/#concept-url-origin 467 const ctx = this[context]; 468 switch (ctx.scheme) { 469 case 'blob:': 470 if (ctx.path.length > 0) { 471 try { 472 return (new URL(ctx.path[0])).origin; 473 } catch { 474 // Fall through... do nothing 475 } 476 } 477 return kOpaqueOrigin; 478 case 'ftp:': 479 case 'gopher:': 480 case 'http:': 481 case 'https:': 482 case 'ws:': 483 case 'wss:': 484 return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port); 485 } 486 return kOpaqueOrigin; 487 } 488 }, 489 protocol: { 490 enumerable: true, 491 configurable: true, 492 get() { 493 return this[context].scheme; 494 }, 495 set(scheme) { 496 // toUSVString is not needed. 497 scheme = `${scheme}`; 498 if (scheme.length === 0) 499 return; 500 const ctx = this[context]; 501 if (ctx.scheme === 'file:' && 502 (ctx.host === '' || ctx.host === null)) { 503 return; 504 } 505 parse(scheme, kSchemeStart, null, ctx, 506 onParseProtocolComplete.bind(this)); 507 } 508 }, 509 username: { 510 enumerable: true, 511 configurable: true, 512 get() { 513 return this[context].username; 514 }, 515 set(username) { 516 // toUSVString is not needed. 517 username = `${username}`; 518 if (this[cannotHaveUsernamePasswordPort]) 519 return; 520 const ctx = this[context]; 521 if (username === '') { 522 ctx.username = ''; 523 ctx.flags &= ~URL_FLAGS_HAS_USERNAME; 524 return; 525 } 526 ctx.username = encodeAuth(username); 527 ctx.flags |= URL_FLAGS_HAS_USERNAME; 528 } 529 }, 530 password: { 531 enumerable: true, 532 configurable: true, 533 get() { 534 return this[context].password; 535 }, 536 set(password) { 537 // toUSVString is not needed. 538 password = `${password}`; 539 if (this[cannotHaveUsernamePasswordPort]) 540 return; 541 const ctx = this[context]; 542 if (password === '') { 543 ctx.password = ''; 544 ctx.flags &= ~URL_FLAGS_HAS_PASSWORD; 545 return; 546 } 547 ctx.password = encodeAuth(password); 548 ctx.flags |= URL_FLAGS_HAS_PASSWORD; 549 } 550 }, 551 host: { 552 enumerable: true, 553 configurable: true, 554 get() { 555 const ctx = this[context]; 556 let ret = ctx.host || ''; 557 if (ctx.port !== null) 558 ret += `:${ctx.port}`; 559 return ret; 560 }, 561 set(host) { 562 const ctx = this[context]; 563 // toUSVString is not needed. 564 host = `${host}`; 565 if (this[cannotBeBase]) { 566 // Cannot set the host if cannot-be-base is set 567 return; 568 } 569 parse(host, kHost, null, ctx, onParseHostComplete.bind(this)); 570 } 571 }, 572 hostname: { 573 enumerable: true, 574 configurable: true, 575 get() { 576 return this[context].host || ''; 577 }, 578 set(host) { 579 const ctx = this[context]; 580 // toUSVString is not needed. 581 host = `${host}`; 582 if (this[cannotBeBase]) { 583 // Cannot set the host if cannot-be-base is set 584 return; 585 } 586 parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this)); 587 } 588 }, 589 port: { 590 enumerable: true, 591 configurable: true, 592 get() { 593 const port = this[context].port; 594 return port === null ? '' : String(port); 595 }, 596 set(port) { 597 // toUSVString is not needed. 598 port = `${port}`; 599 if (this[cannotHaveUsernamePasswordPort]) 600 return; 601 const ctx = this[context]; 602 if (port === '') { 603 ctx.port = null; 604 return; 605 } 606 parse(port, kPort, null, ctx, onParsePortComplete.bind(this)); 607 } 608 }, 609 pathname: { 610 enumerable: true, 611 configurable: true, 612 get() { 613 const ctx = this[context]; 614 if (this[cannotBeBase]) 615 return ctx.path[0]; 616 if (ctx.path.length === 0) 617 return ''; 618 return `/${ctx.path.join('/')}`; 619 }, 620 set(path) { 621 // toUSVString is not needed. 622 path = `${path}`; 623 if (this[cannotBeBase]) 624 return; 625 parse(path, kPathStart, null, this[context], 626 onParsePathComplete.bind(this)); 627 } 628 }, 629 search: { 630 enumerable: true, 631 configurable: true, 632 get() { 633 const { query } = this[context]; 634 if (query === null || query === '') 635 return ''; 636 return `?${query}`; 637 }, 638 set(search) { 639 const ctx = this[context]; 640 search = toUSVString(search); 641 if (search === '') { 642 ctx.query = null; 643 ctx.flags &= ~URL_FLAGS_HAS_QUERY; 644 } else { 645 if (search[0] === '?') search = search.slice(1); 646 ctx.query = ''; 647 ctx.flags |= URL_FLAGS_HAS_QUERY; 648 if (search) { 649 parse(search, kQuery, null, ctx, onParseSearchComplete.bind(this)); 650 } 651 } 652 initSearchParams(this[searchParams], search); 653 } 654 }, 655 searchParams: { // readonly 656 enumerable: true, 657 configurable: true, 658 get() { 659 return this[searchParams]; 660 } 661 }, 662 hash: { 663 enumerable: true, 664 configurable: true, 665 get() { 666 const { fragment } = this[context]; 667 if (fragment === null || fragment === '') 668 return ''; 669 return `#${fragment}`; 670 }, 671 set(hash) { 672 const ctx = this[context]; 673 // toUSVString is not needed. 674 hash = `${hash}`; 675 if (!hash) { 676 ctx.fragment = null; 677 ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT; 678 return; 679 } 680 if (hash[0] === '#') hash = hash.slice(1); 681 ctx.fragment = ''; 682 ctx.flags |= URL_FLAGS_HAS_FRAGMENT; 683 parse(hash, kFragment, null, ctx, onParseHashComplete.bind(this)); 684 } 685 }, 686 toJSON: { 687 writable: true, 688 enumerable: true, 689 configurable: true, 690 // eslint-disable-next-line func-name-matching 691 value: function toJSON() { 692 return this[kFormat]({}); 693 } 694 } 695}); 696 697function update(url, params) { 698 if (!url) 699 return; 700 701 const ctx = url[context]; 702 const serializedParams = params.toString(); 703 if (serializedParams) { 704 ctx.query = serializedParams; 705 ctx.flags |= URL_FLAGS_HAS_QUERY; 706 } else { 707 ctx.query = null; 708 ctx.flags &= ~URL_FLAGS_HAS_QUERY; 709 } 710} 711 712function initSearchParams(url, init) { 713 if (!init) { 714 url[searchParams] = []; 715 return; 716 } 717 url[searchParams] = parseParams(init); 718} 719 720// application/x-www-form-urlencoded parser 721// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser 722function parseParams(qs) { 723 const out = []; 724 let pairStart = 0; 725 let lastPos = 0; 726 let seenSep = false; 727 let buf = ''; 728 let encoded = false; 729 let encodeCheck = 0; 730 let i; 731 for (i = 0; i < qs.length; ++i) { 732 const code = qs.charCodeAt(i); 733 734 // Try matching key/value pair separator 735 if (code === CHAR_AMPERSAND) { 736 if (pairStart === i) { 737 // We saw an empty substring between pair separators 738 lastPos = pairStart = i + 1; 739 continue; 740 } 741 742 if (lastPos < i) 743 buf += qs.slice(lastPos, i); 744 if (encoded) 745 buf = querystring.unescape(buf); 746 out.push(buf); 747 748 // If `buf` is the key, add an empty value. 749 if (!seenSep) 750 out.push(''); 751 752 seenSep = false; 753 buf = ''; 754 encoded = false; 755 encodeCheck = 0; 756 lastPos = pairStart = i + 1; 757 continue; 758 } 759 760 // Try matching key/value separator (e.g. '=') if we haven't already 761 if (!seenSep && code === CHAR_EQUAL) { 762 // Key/value separator match! 763 if (lastPos < i) 764 buf += qs.slice(lastPos, i); 765 if (encoded) 766 buf = querystring.unescape(buf); 767 out.push(buf); 768 769 seenSep = true; 770 buf = ''; 771 encoded = false; 772 encodeCheck = 0; 773 lastPos = i + 1; 774 continue; 775 } 776 777 // Handle + and percent decoding. 778 if (code === CHAR_PLUS) { 779 if (lastPos < i) 780 buf += qs.slice(lastPos, i); 781 buf += ' '; 782 lastPos = i + 1; 783 } else if (!encoded) { 784 // Try to match an (valid) encoded byte (once) to minimize unnecessary 785 // calls to string decoding functions 786 if (code === CHAR_PERCENT) { 787 encodeCheck = 1; 788 } else if (encodeCheck > 0) { 789 if (isHexTable[code] === 1) { 790 if (++encodeCheck === 3) { 791 querystring = require('querystring'); 792 encoded = true; 793 } 794 } else { 795 encodeCheck = 0; 796 } 797 } 798 } 799 } 800 801 // Deal with any leftover key or value data 802 803 // There is a trailing &. No more processing is needed. 804 if (pairStart === i) 805 return out; 806 807 if (lastPos < i) 808 buf += qs.slice(lastPos, i); 809 if (encoded) 810 buf = querystring.unescape(buf); 811 out.push(buf); 812 813 // If `buf` is the key, add an empty value. 814 if (!seenSep) 815 out.push(''); 816 817 return out; 818} 819 820// Adapted from querystring's implementation. 821// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer 822const noEscape = [ 823/* 824 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F 825*/ 826 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F 827 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F 828 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F 829 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F 830 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F 831 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F 832 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F 833 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 // 0x70 - 0x7F 834]; 835 836// Special version of hexTable that uses `+` for U+0020 SPACE. 837const paramHexTable = hexTable.slice(); 838paramHexTable[0x20] = '+'; 839 840// application/x-www-form-urlencoded serializer 841// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer 842function serializeParams(array) { 843 const len = array.length; 844 if (len === 0) 845 return ''; 846 847 const firstEncodedParam = encodeStr(array[0], noEscape, paramHexTable); 848 const firstEncodedValue = encodeStr(array[1], noEscape, paramHexTable); 849 let output = `${firstEncodedParam}=${firstEncodedValue}`; 850 851 for (let i = 2; i < len; i += 2) { 852 const encodedParam = encodeStr(array[i], noEscape, paramHexTable); 853 const encodedValue = encodeStr(array[i + 1], noEscape, paramHexTable); 854 output += `&${encodedParam}=${encodedValue}`; 855 } 856 857 return output; 858} 859 860// Mainly to mitigate func-name-matching ESLint rule 861function defineIDLClass(proto, classStr, obj) { 862 // https://heycam.github.io/webidl/#dfn-class-string 863 ObjectDefineProperty(proto, SymbolToStringTag, { 864 writable: false, 865 enumerable: false, 866 configurable: true, 867 value: classStr 868 }); 869 870 // https://heycam.github.io/webidl/#es-operations 871 for (const key of ObjectKeys(obj)) { 872 ObjectDefineProperty(proto, key, { 873 writable: true, 874 enumerable: true, 875 configurable: true, 876 value: obj[key] 877 }); 878 } 879 for (const key of ObjectGetOwnPropertySymbols(obj)) { 880 ObjectDefineProperty(proto, key, { 881 writable: true, 882 enumerable: false, 883 configurable: true, 884 value: obj[key] 885 }); 886 } 887} 888 889// for merge sort 890function merge(out, start, mid, end, lBuffer, rBuffer) { 891 const sizeLeft = mid - start; 892 const sizeRight = end - mid; 893 let l, r, o; 894 895 for (l = 0; l < sizeLeft; l++) 896 lBuffer[l] = out[start + l]; 897 for (r = 0; r < sizeRight; r++) 898 rBuffer[r] = out[mid + r]; 899 900 l = 0; 901 r = 0; 902 o = start; 903 while (l < sizeLeft && r < sizeRight) { 904 if (lBuffer[l] <= rBuffer[r]) { 905 out[o++] = lBuffer[l++]; 906 out[o++] = lBuffer[l++]; 907 } else { 908 out[o++] = rBuffer[r++]; 909 out[o++] = rBuffer[r++]; 910 } 911 } 912 while (l < sizeLeft) 913 out[o++] = lBuffer[l++]; 914 while (r < sizeRight) 915 out[o++] = rBuffer[r++]; 916} 917 918defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { 919 append(name, value) { 920 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 921 throw new ERR_INVALID_THIS('URLSearchParams'); 922 } 923 if (arguments.length < 2) { 924 throw new ERR_MISSING_ARGS('name', 'value'); 925 } 926 927 name = toUSVString(name); 928 value = toUSVString(value); 929 this[searchParams].push(name, value); 930 update(this[context], this); 931 }, 932 933 delete(name) { 934 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 935 throw new ERR_INVALID_THIS('URLSearchParams'); 936 } 937 if (arguments.length < 1) { 938 throw new ERR_MISSING_ARGS('name'); 939 } 940 941 const list = this[searchParams]; 942 name = toUSVString(name); 943 for (let i = 0; i < list.length;) { 944 const cur = list[i]; 945 if (cur === name) { 946 list.splice(i, 2); 947 } else { 948 i += 2; 949 } 950 } 951 update(this[context], this); 952 }, 953 954 get(name) { 955 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 956 throw new ERR_INVALID_THIS('URLSearchParams'); 957 } 958 if (arguments.length < 1) { 959 throw new ERR_MISSING_ARGS('name'); 960 } 961 962 const list = this[searchParams]; 963 name = toUSVString(name); 964 for (let i = 0; i < list.length; i += 2) { 965 if (list[i] === name) { 966 return list[i + 1]; 967 } 968 } 969 return null; 970 }, 971 972 getAll(name) { 973 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 974 throw new ERR_INVALID_THIS('URLSearchParams'); 975 } 976 if (arguments.length < 1) { 977 throw new ERR_MISSING_ARGS('name'); 978 } 979 980 const list = this[searchParams]; 981 const values = []; 982 name = toUSVString(name); 983 for (let i = 0; i < list.length; i += 2) { 984 if (list[i] === name) { 985 values.push(list[i + 1]); 986 } 987 } 988 return values; 989 }, 990 991 has(name) { 992 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 993 throw new ERR_INVALID_THIS('URLSearchParams'); 994 } 995 if (arguments.length < 1) { 996 throw new ERR_MISSING_ARGS('name'); 997 } 998 999 const list = this[searchParams]; 1000 name = toUSVString(name); 1001 for (let i = 0; i < list.length; i += 2) { 1002 if (list[i] === name) { 1003 return true; 1004 } 1005 } 1006 return false; 1007 }, 1008 1009 set(name, value) { 1010 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1011 throw new ERR_INVALID_THIS('URLSearchParams'); 1012 } 1013 if (arguments.length < 2) { 1014 throw new ERR_MISSING_ARGS('name', 'value'); 1015 } 1016 1017 const list = this[searchParams]; 1018 name = toUSVString(name); 1019 value = toUSVString(value); 1020 1021 // If there are any name-value pairs whose name is `name`, in `list`, set 1022 // the value of the first such name-value pair to `value` and remove the 1023 // others. 1024 let found = false; 1025 for (let i = 0; i < list.length;) { 1026 const cur = list[i]; 1027 if (cur === name) { 1028 if (!found) { 1029 list[i + 1] = value; 1030 found = true; 1031 i += 2; 1032 } else { 1033 list.splice(i, 2); 1034 } 1035 } else { 1036 i += 2; 1037 } 1038 } 1039 1040 // Otherwise, append a new name-value pair whose name is `name` and value 1041 // is `value`, to `list`. 1042 if (!found) { 1043 list.push(name, value); 1044 } 1045 1046 update(this[context], this); 1047 }, 1048 1049 sort() { 1050 const a = this[searchParams]; 1051 const len = a.length; 1052 1053 if (len <= 2) { 1054 // Nothing needs to be done. 1055 } else if (len < 100) { 1056 // 100 is found through testing. 1057 // Simple stable in-place insertion sort 1058 // Derived from v8/src/js/array.js 1059 for (let i = 2; i < len; i += 2) { 1060 const curKey = a[i]; 1061 const curVal = a[i + 1]; 1062 let j; 1063 for (j = i - 2; j >= 0; j -= 2) { 1064 if (a[j] > curKey) { 1065 a[j + 2] = a[j]; 1066 a[j + 3] = a[j + 1]; 1067 } else { 1068 break; 1069 } 1070 } 1071 a[j + 2] = curKey; 1072 a[j + 3] = curVal; 1073 } 1074 } else { 1075 // Bottom-up iterative stable merge sort 1076 const lBuffer = new Array(len); 1077 const rBuffer = new Array(len); 1078 for (let step = 2; step < len; step *= 2) { 1079 for (let start = 0; start < len - 2; start += 2 * step) { 1080 const mid = start + step; 1081 let end = mid + step; 1082 end = end < len ? end : len; 1083 if (mid > end) 1084 continue; 1085 merge(a, start, mid, end, lBuffer, rBuffer); 1086 } 1087 } 1088 } 1089 1090 update(this[context], this); 1091 }, 1092 1093 // https://heycam.github.io/webidl/#es-iterators 1094 // Define entries here rather than [Symbol.iterator] as the function name 1095 // must be set to `entries`. 1096 entries() { 1097 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1098 throw new ERR_INVALID_THIS('URLSearchParams'); 1099 } 1100 1101 return createSearchParamsIterator(this, 'key+value'); 1102 }, 1103 1104 forEach(callback, thisArg = undefined) { 1105 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1106 throw new ERR_INVALID_THIS('URLSearchParams'); 1107 } 1108 if (typeof callback !== 'function') { 1109 throw new ERR_INVALID_CALLBACK(callback); 1110 } 1111 1112 let list = this[searchParams]; 1113 1114 let i = 0; 1115 while (i < list.length) { 1116 const key = list[i]; 1117 const value = list[i + 1]; 1118 callback.call(thisArg, value, key, this); 1119 // In case the URL object's `search` is updated 1120 list = this[searchParams]; 1121 i += 2; 1122 } 1123 }, 1124 1125 // https://heycam.github.io/webidl/#es-iterable 1126 keys() { 1127 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1128 throw new ERR_INVALID_THIS('URLSearchParams'); 1129 } 1130 1131 return createSearchParamsIterator(this, 'key'); 1132 }, 1133 1134 values() { 1135 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1136 throw new ERR_INVALID_THIS('URLSearchParams'); 1137 } 1138 1139 return createSearchParamsIterator(this, 'value'); 1140 }, 1141 1142 // https://heycam.github.io/webidl/#es-stringifier 1143 // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior 1144 toString() { 1145 if (!this || !this[searchParams] || this[searchParams][searchParams]) { 1146 throw new ERR_INVALID_THIS('URLSearchParams'); 1147 } 1148 1149 return serializeParams(this[searchParams]); 1150 } 1151}); 1152 1153// https://heycam.github.io/webidl/#es-iterable-entries 1154ObjectDefineProperty(URLSearchParams.prototype, SymbolIterator, { 1155 writable: true, 1156 configurable: true, 1157 value: URLSearchParams.prototype.entries 1158}); 1159 1160// https://heycam.github.io/webidl/#dfn-default-iterator-object 1161function createSearchParamsIterator(target, kind) { 1162 const iterator = ObjectCreate(URLSearchParamsIteratorPrototype); 1163 iterator[context] = { 1164 target, 1165 kind, 1166 index: 0 1167 }; 1168 return iterator; 1169} 1170 1171// https://heycam.github.io/webidl/#dfn-iterator-prototype-object 1172const URLSearchParamsIteratorPrototype = ObjectCreate(IteratorPrototype); 1173 1174defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParams Iterator', { 1175 next() { 1176 if (!this || 1177 ObjectGetPrototypeOf(this) !== URLSearchParamsIteratorPrototype) { 1178 throw new ERR_INVALID_THIS('URLSearchParamsIterator'); 1179 } 1180 1181 const { 1182 target, 1183 kind, 1184 index 1185 } = this[context]; 1186 const values = target[searchParams]; 1187 const len = values.length; 1188 if (index >= len) { 1189 return { 1190 value: undefined, 1191 done: true 1192 }; 1193 } 1194 1195 const name = values[index]; 1196 const value = values[index + 1]; 1197 this[context].index = index + 2; 1198 1199 let result; 1200 if (kind === 'key') { 1201 result = name; 1202 } else if (kind === 'value') { 1203 result = value; 1204 } else { 1205 result = [name, value]; 1206 } 1207 1208 return { 1209 value: result, 1210 done: false 1211 }; 1212 }, 1213 [inspect.custom](recurseTimes, ctx) { 1214 if (this == null || this[context] == null || this[context].target == null) 1215 throw new ERR_INVALID_THIS('URLSearchParamsIterator'); 1216 1217 if (typeof recurseTimes === 'number' && recurseTimes < 0) 1218 return ctx.stylize('[Object]', 'special'); 1219 1220 const innerOpts = { ...ctx }; 1221 if (recurseTimes !== null) { 1222 innerOpts.depth = recurseTimes - 1; 1223 } 1224 const { 1225 target, 1226 kind, 1227 index 1228 } = this[context]; 1229 const output = target[searchParams].slice(index).reduce((prev, cur, i) => { 1230 const key = i % 2 === 0; 1231 if (kind === 'key' && key) { 1232 prev.push(cur); 1233 } else if (kind === 'value' && !key) { 1234 prev.push(cur); 1235 } else if (kind === 'key+value' && !key) { 1236 prev.push([target[searchParams][index + i - 1], cur]); 1237 } 1238 return prev; 1239 }, []); 1240 const breakLn = inspect(output, innerOpts).includes('\n'); 1241 const outputStrs = output.map((p) => inspect(p, innerOpts)); 1242 let outputStr; 1243 if (breakLn) { 1244 outputStr = `\n ${outputStrs.join(',\n ')}`; 1245 } else { 1246 outputStr = ` ${outputStrs.join(', ')}`; 1247 } 1248 return `${this[SymbolToStringTag]} {${outputStr} }`; 1249 } 1250}); 1251 1252function domainToASCII(domain) { 1253 if (arguments.length < 1) 1254 throw new ERR_MISSING_ARGS('domain'); 1255 1256 // toUSVString is not needed. 1257 return _domainToASCII(`${domain}`); 1258} 1259 1260function domainToUnicode(domain) { 1261 if (arguments.length < 1) 1262 throw new ERR_MISSING_ARGS('domain'); 1263 1264 // toUSVString is not needed. 1265 return _domainToUnicode(`${domain}`); 1266} 1267 1268// Utility function that converts a URL object into an ordinary 1269// options object as expected by the http.request and https.request 1270// APIs. 1271function urlToOptions(url) { 1272 const options = { 1273 protocol: url.protocol, 1274 hostname: typeof url.hostname === 'string' && url.hostname.startsWith('[') ? 1275 url.hostname.slice(1, -1) : 1276 url.hostname, 1277 hash: url.hash, 1278 search: url.search, 1279 pathname: url.pathname, 1280 path: `${url.pathname || ''}${url.search || ''}`, 1281 href: url.href 1282 }; 1283 if (url.port !== '') { 1284 options.port = Number(url.port); 1285 } 1286 if (url.username || url.password) { 1287 options.auth = `${url.username}:${url.password}`; 1288 } 1289 return options; 1290} 1291 1292const forwardSlashRegEx = /\//g; 1293 1294function getPathFromURLWin32(url) { 1295 const hostname = url.hostname; 1296 let pathname = url.pathname; 1297 for (let n = 0; n < pathname.length; n++) { 1298 if (pathname[n] === '%') { 1299 const third = pathname.codePointAt(n + 2) | 0x20; 1300 if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F / 1301 (pathname[n + 1] === '5' && third === 99)) { // 5c 5C \ 1302 throw new ERR_INVALID_FILE_URL_PATH( 1303 'must not include encoded \\ or / characters' 1304 ); 1305 } 1306 } 1307 } 1308 pathname = pathname.replace(forwardSlashRegEx, '\\'); 1309 pathname = decodeURIComponent(pathname); 1310 if (hostname !== '') { 1311 // If hostname is set, then we have a UNC path 1312 // Pass the hostname through domainToUnicode just in case 1313 // it is an IDN using punycode encoding. We do not need to worry 1314 // about percent encoding because the URL parser will have 1315 // already taken care of that for us. Note that this only 1316 // causes IDNs with an appropriate `xn--` prefix to be decoded. 1317 return `\\\\${domainToUnicode(hostname)}${pathname}`; 1318 } 1319 // Otherwise, it's a local path that requires a drive letter 1320 const letter = pathname.codePointAt(1) | 0x20; 1321 const sep = pathname[2]; 1322 if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z 1323 (sep !== ':')) { 1324 throw new ERR_INVALID_FILE_URL_PATH('must be absolute'); 1325 } 1326 return pathname.slice(1); 1327} 1328 1329function getPathFromURLPosix(url) { 1330 if (url.hostname !== '') { 1331 throw new ERR_INVALID_FILE_URL_HOST(platform); 1332 } 1333 const pathname = url.pathname; 1334 for (let n = 0; n < pathname.length; n++) { 1335 if (pathname[n] === '%') { 1336 const third = pathname.codePointAt(n + 2) | 0x20; 1337 if (pathname[n + 1] === '2' && third === 102) { 1338 throw new ERR_INVALID_FILE_URL_PATH( 1339 'must not include encoded / characters' 1340 ); 1341 } 1342 } 1343 } 1344 return decodeURIComponent(pathname); 1345} 1346 1347function fileURLToPath(path) { 1348 if (typeof path === 'string') 1349 path = new URL(path); 1350 else if (!isURLInstance(path)) 1351 throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path); 1352 if (path.protocol !== 'file:') 1353 throw new ERR_INVALID_URL_SCHEME('file'); 1354 return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); 1355} 1356 1357// The following characters are percent-encoded when converting from file path 1358// to URL: 1359// - %: The percent character is the only character not encoded by the 1360// `pathname` setter. 1361// - \: Backslash is encoded on non-windows platforms since it's a valid 1362// character but the `pathname` setters replaces it by a forward slash. 1363// - LF: The newline character is stripped out by the `pathname` setter. 1364// (See whatwg/url#419) 1365// - CR: The carriage return character is also stripped out by the `pathname` 1366// setter. 1367// - TAB: The tab character is also stripped out by the `pathname` setter. 1368const percentRegEx = /%/g; 1369const backslashRegEx = /\\/g; 1370const newlineRegEx = /\n/g; 1371const carriageReturnRegEx = /\r/g; 1372const tabRegEx = /\t/g; 1373 1374function encodePathChars(filepath) { 1375 if (filepath.includes('%')) 1376 filepath = filepath.replace(percentRegEx, '%25'); 1377 // In posix, backslash is a valid character in paths: 1378 if (!isWindows && filepath.includes('\\')) 1379 filepath = filepath.replace(backslashRegEx, '%5C'); 1380 if (filepath.includes('\n')) 1381 filepath = filepath.replace(newlineRegEx, '%0A'); 1382 if (filepath.includes('\r')) 1383 filepath = filepath.replace(carriageReturnRegEx, '%0D'); 1384 if (filepath.includes('\t')) 1385 filepath = filepath.replace(tabRegEx, '%09'); 1386 return filepath; 1387} 1388 1389function pathToFileURL(filepath) { 1390 const outURL = new URL('file://'); 1391 if (isWindows && filepath.startsWith('\\\\')) { 1392 // UNC path format: \\server\share\resource 1393 const paths = filepath.split('\\'); 1394 if (paths.length <= 3) { 1395 throw new ERR_INVALID_ARG_VALUE( 1396 'filepath', 1397 filepath, 1398 'Missing UNC resource path' 1399 ); 1400 } 1401 const hostname = paths[2]; 1402 if (hostname.length === 0) { 1403 throw new ERR_INVALID_ARG_VALUE( 1404 'filepath', 1405 filepath, 1406 'Empty UNC servername' 1407 ); 1408 } 1409 outURL.hostname = domainToASCII(hostname); 1410 outURL.pathname = encodePathChars(paths.slice(3).join('/')); 1411 } else { 1412 let resolved = path.resolve(filepath); 1413 // path.resolve strips trailing slashes so we must add them back 1414 const filePathLast = filepath.charCodeAt(filepath.length - 1); 1415 if ((filePathLast === CHAR_FORWARD_SLASH || 1416 (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && 1417 resolved[resolved.length - 1] !== path.sep) 1418 resolved += '/'; 1419 outURL.pathname = encodePathChars(resolved); 1420 } 1421 return outURL; 1422} 1423 1424function isURLInstance(fileURLOrPath) { 1425 return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin; 1426} 1427 1428function toPathIfFileURL(fileURLOrPath) { 1429 if (!isURLInstance(fileURLOrPath)) 1430 return fileURLOrPath; 1431 return fileURLToPath(fileURLOrPath); 1432} 1433 1434function constructUrl(flags, protocol, username, password, 1435 host, port, path, query, fragment) { 1436 const ctx = new URLContext(); 1437 ctx.flags = flags; 1438 ctx.scheme = protocol; 1439 ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : ''; 1440 ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : ''; 1441 ctx.port = port; 1442 ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : []; 1443 ctx.query = query; 1444 ctx.fragment = fragment; 1445 ctx.host = host; 1446 1447 const url = ObjectCreate(URL.prototype); 1448 url[context] = ctx; 1449 const params = new URLSearchParams(); 1450 url[searchParams] = params; 1451 params[context] = url; 1452 initSearchParams(params, query); 1453 return url; 1454} 1455setURLConstructor(constructUrl); 1456 1457module.exports = { 1458 toUSVString, 1459 fileURLToPath, 1460 pathToFileURL, 1461 toPathIfFileURL, 1462 isURLInstance, 1463 URL, 1464 URLSearchParams, 1465 domainToASCII, 1466 domainToUnicode, 1467 urlToOptions, 1468 formatSymbol: kFormat, 1469 searchParamsSymbol: searchParams, 1470 encodeStr 1471}; 1472