diff --git a/lib/internal/url.js b/lib/internal/url.js index 0fe819fe129..3f1db6ba937 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -1065,7 +1065,9 @@ class URL { } set protocol(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kProtocol, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kProtocol, value); if (href) { this.#updateContext(href); } @@ -1079,7 +1081,9 @@ class URL { } set username(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kUsername, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kUsername, value); if (href) { this.#updateContext(href); } @@ -1093,7 +1097,9 @@ class URL { } set password(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPassword, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kPassword, value); if (href) { this.#updateContext(href); } @@ -1113,7 +1119,9 @@ class URL { } set host(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHost, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kHost, value); if (href) { this.#updateContext(href); } @@ -1129,7 +1137,9 @@ class URL { } set hostname(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHostname, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kHostname, value); if (href) { this.#updateContext(href); } @@ -1143,7 +1153,9 @@ class URL { } set port(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPort, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kPort, value); if (href) { this.#updateContext(href); } @@ -1160,7 +1172,9 @@ class URL { } set pathname(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPathname, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kPathname, value); if (href) { this.#updateContext(href); } @@ -1173,7 +1187,9 @@ class URL { } set search(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`)); + const context = this.#context; + value = StringPrototypeToWellFormed(`${value}`); + const href = bindingUrl.update(context.href, updateActions.kSearch, value); if (href) { this.#updateContext(href, true); } @@ -1198,7 +1214,9 @@ class URL { } set hash(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHash, `${value}`); + const context = this.#context; + value = `${value}`; + const href = bindingUrl.update(context.href, updateActions.kHash, value); if (href) { this.#updateContext(href); } diff --git a/test/parallel/test-whatwg-url-custom-setters-conversion.js b/test/parallel/test-whatwg-url-custom-setters-conversion.js new file mode 100644 index 00000000000..70d74d9013d --- /dev/null +++ b/test/parallel/test-whatwg-url-custom-setters-conversion.js @@ -0,0 +1,76 @@ +'use strict'; + +// Tests below are not from WPT. + +require('../common'); +const assert = require('assert'); + +// Web IDL converts the assigned value to a string before the setter steps +// run, so the steps must see a URL that the conversion itself changed. +// Refs: https://github.com/nodejs/node/issues/67345 +const setters = { + protocol: ['http:', 'http://after.example/base'], + username: ['user', 'https://user@after.example/base'], + password: ['pass', 'https://:pass@after.example/base'], + host: ['other.example:8080', 'https://other.example:8080/base'], + hostname: ['other.example', 'https://other.example/base'], + port: ['8080', 'https://after.example:8080/base'], + pathname: ['/new', 'https://after.example/new'], + search: ['?q=1', 'https://after.example/base?q=1'], + hash: ['#frag', 'https://after.example/base#frag'], +}; + +for (const [name, [value, expected]] of Object.entries(setters)) { + const url = new URL('https://before.example/old'); + url[name] = { + toString() { + url.href = 'https://after.example/base'; + return value; + }, + }; + assert.strictEqual(url.href, expected, `URL.prototype.${name} setter`); +} + +// A URL with an opaque path has no host to set, even when it only became +// one while the value was converted. +{ + const url = new URL('https://before.example/old'); + url.host = { + toString() { + url.href = 'mailto:someone@after.example'; + return 'other.example'; + }, + }; + assert.strictEqual(url.href, 'mailto:someone@after.example'); +} + +// The search params follow the URL the setter steps saw. +{ + const url = new URL('https://before.example/old?a=1'); + const { searchParams } = url; + url.search = { + toString() { + url.href = 'https://after.example/base?b=2'; + return '?c=3'; + }, + }; + assert.strictEqual(url.href, 'https://after.example/base?c=3'); + assert.strictEqual(searchParams.toString(), 'c=3'); +} + +// The receiver is checked before the value is converted. +for (const name of Object.keys(setters)) { + const { set } = Object.getOwnPropertyDescriptor(URL.prototype, name); + let conversions = 0; + assert.throws(() => { + set.call({}, { + toString() { + conversions++; + return ''; + }, + }); + }, { + name: 'TypeError', + }); + assert.strictEqual(conversions, 0, `URL.prototype.${name} setter`); +}