mirror of
https://github.com/denoland/deno
synced 2024-09-17 23:22:46 +00:00
Fix URL encoding (#5557)
This commit is contained in:
parent
c3ec16535f
commit
93c2164673
|
@ -169,6 +169,42 @@ unitTest(function urlDriveLetter() {
|
||||||
assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:");
|
assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
unitTest(function urlHostnameUpperCase() {
|
||||||
|
assertEquals(new URL("https://EXAMPLE.COM").href, "https://example.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
unitTest(function urlTrim() {
|
||||||
|
assertEquals(new URL(" https://example.com ").href, "https://example.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
unitTest(function urlEncoding() {
|
||||||
|
assertEquals(
|
||||||
|
new URL("https://a !$&*()=,;+'\"@example.com").username,
|
||||||
|
"a%20!$&*()%3D,%3B+%27%22"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
new URL("https://:a !$&*()=,;+'\"@example.com").password,
|
||||||
|
"a%20!$&*()%3D,%3B+%27%22"
|
||||||
|
);
|
||||||
|
// FIXME: https://url.spec.whatwg.org/#idna
|
||||||
|
// assertEquals(
|
||||||
|
// new URL("https://a !$&*()=,+'\"").hostname,
|
||||||
|
// "a%20%21%24%26%2A%28%29%3D%2C+%27%22"
|
||||||
|
// );
|
||||||
|
assertEquals(
|
||||||
|
new URL("https://example.com/a ~!@$&*()=:/,;+'\"\\").pathname,
|
||||||
|
"/a%20~!@$&*()=:/,;+'%22/"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
new URL("https://example.com?a ~!@$&*()=:/,;?+'\"\\").search,
|
||||||
|
"?a%20~!@$&*()=:/,;?+%27%22\\"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
new URL("https://example.com#a ~!@#$&*()=:/,;?+'\"\\").hash,
|
||||||
|
"#a%20~!@#$&*()=:/,;?+'%22\\"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
unitTest(function urlBaseURL(): void {
|
unitTest(function urlBaseURL(): void {
|
||||||
const base = new URL(
|
const base = new URL(
|
||||||
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface URLParts {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
port: string;
|
port: string;
|
||||||
path: string;
|
path: string;
|
||||||
query: string | null;
|
query: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ function takePattern(string: string, pattern: RegExp): [string, string] {
|
||||||
function parse(url: string, isBase = true): URLParts | undefined {
|
function parse(url: string, isBase = true): URLParts | undefined {
|
||||||
const parts: Partial<URLParts> = {};
|
const parts: Partial<URLParts> = {};
|
||||||
let restUrl;
|
let restUrl;
|
||||||
[parts.protocol, restUrl] = takePattern(url, /^([a-z]+):/);
|
[parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/);
|
||||||
if (isBase && parts.protocol == "") {
|
if (isBase && parts.protocol == "") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -61,9 +61,6 @@ function parse(url: string, isBase = true): URLParts | undefined {
|
||||||
parts.username = "";
|
parts.username = "";
|
||||||
parts.password = "";
|
parts.password = "";
|
||||||
[parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/);
|
[parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/);
|
||||||
if (parts.hostname.includes(":")) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
parts.port = "";
|
parts.port = "";
|
||||||
} else if (specialSchemes.includes(parts.protocol)) {
|
} else if (specialSchemes.includes(parts.protocol)) {
|
||||||
let restAuthority;
|
let restAuthority;
|
||||||
|
@ -80,7 +77,9 @@ function parse(url: string, isBase = true): URLParts | undefined {
|
||||||
restAuthentication,
|
restAuthentication,
|
||||||
/^([^:]*)/
|
/^([^:]*)/
|
||||||
);
|
);
|
||||||
|
parts.username = encodeUserinfo(parts.username);
|
||||||
[parts.password] = takePattern(restAuthentication, /^:(.*)/);
|
[parts.password] = takePattern(restAuthentication, /^:(.*)/);
|
||||||
|
parts.password = encodeUserinfo(parts.password);
|
||||||
[parts.hostname, restAuthority] = takePattern(restAuthority, /^([^:]+)/);
|
[parts.hostname, restAuthority] = takePattern(restAuthority, /^([^:]+)/);
|
||||||
[parts.port] = takePattern(restAuthority, /^:(.*)/);
|
[parts.port] = takePattern(restAuthority, /^:(.*)/);
|
||||||
if (!isValidPort(parts.port)) {
|
if (!isValidPort(parts.port)) {
|
||||||
|
@ -92,10 +91,17 @@ function parse(url: string, isBase = true): URLParts | undefined {
|
||||||
parts.hostname = "";
|
parts.hostname = "";
|
||||||
parts.port = "";
|
parts.port = "";
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
parts.hostname = encodeHostname(parts.hostname).toLowerCase();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
[parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/);
|
[parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/);
|
||||||
parts.path = parts.path.replace(/\\/g, "/");
|
parts.path = encodePathname(parts.path.replace(/\\/g, "/"));
|
||||||
[parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/);
|
[parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/);
|
||||||
|
parts.query = encodeSearch(parts.query);
|
||||||
[parts.hash] = takePattern(restUrl, /^(#.*)/);
|
[parts.hash] = takePattern(restUrl, /^(#.*)/);
|
||||||
|
parts.hash = encodeHash(parts.hash);
|
||||||
return parts as URLParts;
|
return parts as URLParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,9 +265,7 @@ export class URLImpl implements URL {
|
||||||
value = `#${value}`;
|
value = `#${value}`;
|
||||||
}
|
}
|
||||||
// hashes can contain % and # unescaped
|
// hashes can contain % and # unescaped
|
||||||
parts.get(this)!.hash = escape(value)
|
parts.get(this)!.hash = encodeHash(value);
|
||||||
.replace(/%25/g, "%")
|
|
||||||
.replace(/%23/g, "#");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +286,9 @@ export class URLImpl implements URL {
|
||||||
|
|
||||||
set hostname(value: string) {
|
set hostname(value: string) {
|
||||||
value = String(value);
|
value = String(value);
|
||||||
parts.get(this)!.hostname = encodeURIComponent(value);
|
try {
|
||||||
|
parts.get(this)!.hostname = encodeHostname(value);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
get href(): string {
|
get href(): string {
|
||||||
|
@ -319,7 +325,7 @@ export class URLImpl implements URL {
|
||||||
|
|
||||||
set password(value: string) {
|
set password(value: string) {
|
||||||
value = String(value);
|
value = String(value);
|
||||||
parts.get(this)!.password = encodeURIComponent(value);
|
parts.get(this)!.password = encodeUserinfo(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pathname(): string {
|
get pathname(): string {
|
||||||
|
@ -332,7 +338,7 @@ export class URLImpl implements URL {
|
||||||
value = `/${value}`;
|
value = `/${value}`;
|
||||||
}
|
}
|
||||||
// paths can contain % unescaped
|
// paths can contain % unescaped
|
||||||
parts.get(this)!.path = escape(value).replace(/%25/g, "%");
|
parts.get(this)!.path = encodePathname(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get port(): string {
|
get port(): string {
|
||||||
|
@ -366,27 +372,13 @@ export class URLImpl implements URL {
|
||||||
}
|
}
|
||||||
|
|
||||||
get search(): string {
|
get search(): string {
|
||||||
const query = parts.get(this)!.query;
|
return parts.get(this)!.query;
|
||||||
if (query === null || query === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set search(value: string) {
|
set search(value: string) {
|
||||||
value = String(value);
|
value = String(value);
|
||||||
let query: string | null;
|
const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`;
|
||||||
|
parts.get(this)!.query = encodeSearch(query);
|
||||||
if (value === "") {
|
|
||||||
query = null;
|
|
||||||
} else if (value.charAt(0) !== "?") {
|
|
||||||
query = `?${value}`;
|
|
||||||
} else {
|
|
||||||
query = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.get(this)!.query = query;
|
|
||||||
this.#updateSearchParams();
|
this.#updateSearchParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +388,7 @@ export class URLImpl implements URL {
|
||||||
|
|
||||||
set username(value: string) {
|
set username(value: string) {
|
||||||
value = String(value);
|
value = String(value);
|
||||||
parts.get(this)!.username = encodeURIComponent(value);
|
parts.get(this)!.username = encodeUserinfo(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get searchParams(): URLSearchParams {
|
get searchParams(): URLSearchParams {
|
||||||
|
@ -474,3 +466,56 @@ export class URLImpl implements URL {
|
||||||
blobURLMap.delete(url);
|
blobURLMap.delete(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function charInC0ControlSet(c: string): boolean {
|
||||||
|
return c >= "\u0000" && c <= "\u001F";
|
||||||
|
}
|
||||||
|
|
||||||
|
function charInSearchSet(c: string): boolean {
|
||||||
|
// prettier-ignore
|
||||||
|
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || c > "\u007E";
|
||||||
|
}
|
||||||
|
|
||||||
|
function charInFragmentSet(c: string): boolean {
|
||||||
|
// prettier-ignore
|
||||||
|
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function charInPathSet(c: string): boolean {
|
||||||
|
// prettier-ignore
|
||||||
|
return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function charInUserinfoSet(c: string): boolean {
|
||||||
|
// "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox.
|
||||||
|
// prettier-ignore
|
||||||
|
return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeChar(c: string): string {
|
||||||
|
return `%${c.charCodeAt(0).toString(16)}`.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUserinfo(s: string): string {
|
||||||
|
return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeHostname(s: string): string {
|
||||||
|
// FIXME: https://url.spec.whatwg.org/#idna
|
||||||
|
if (s.includes(":")) {
|
||||||
|
throw new TypeError("Invalid hostname.");
|
||||||
|
}
|
||||||
|
return encodeURIComponent(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodePathname(s: string): string {
|
||||||
|
return [...s].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeSearch(s: string): string {
|
||||||
|
return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeHash(s: string): string {
|
||||||
|
return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join("");
|
||||||
|
}
|
||||||
|
|
|
@ -76,13 +76,7 @@ export class URLSearchParamsImpl implements URLSearchParams {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
parts.get(url)!.query = this.toString();
|
||||||
let query: string | null = this.toString();
|
|
||||||
if (query === "") {
|
|
||||||
query = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.get(url)!.query = query;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#append = (name: string, value: string): void => {
|
#append = (name: string, value: string): void => {
|
||||||
|
|
Loading…
Reference in a new issue