From 2be9d573f2609eb08e477ea191872f842b4e19e3 Mon Sep 17 00:00:00 2001 From: Mimi <1119186082@qq.com> Date: Thu, 13 Aug 2026 09:44:31 +0800 Subject: [PATCH] refactor: migrate to URL.parse() --- lib/decode_url.ts | 11 +++++------ lib/encode_url.ts | 11 +++++------ lib/full_url_for.ts | 8 +++----- lib/is_external_link.ts | 13 ++++--------- lib/url_for.ts | 8 +++----- test/decode_url.spec.ts | 10 ++++++++++ test/encode_url.spec.ts | 11 +++++++++++ test/full_url_for.spec.ts | 9 +++++++++ test/is_external_link.spec.ts | 6 ++++++ test/url_for.spec.ts | 10 ++++++++++ 10 files changed, 66 insertions(+), 31 deletions(-) diff --git a/lib/decode_url.ts b/lib/decode_url.ts index 6c976baa..9c779ec9 100644 --- a/lib/decode_url.ts +++ b/lib/decode_url.ts @@ -1,14 +1,13 @@ -// eslint-disable-next-line n/no-deprecated-api -import { parse, format } from 'url'; +import { format } from 'url'; import { unescape } from 'querystring'; const decodeURL = (str: string) => { - const index = str.indexOf(':'); - if (index < 0) { + if (!str.includes(':')) { return unescape(str); } - if (parse(str.slice(0, index + 1)).protocol) { - const parsed = new URL(str); + + const parsed = URL.parse(str); + if (parsed) { // Exit if input is a data url if (parsed.origin === 'null') return str; diff --git a/lib/encode_url.ts b/lib/encode_url.ts index a6f409a0..b7f50e54 100644 --- a/lib/encode_url.ts +++ b/lib/encode_url.ts @@ -1,14 +1,13 @@ -// eslint-disable-next-line n/no-deprecated-api -import { parse, format } from 'url'; +import { format } from 'url'; import { unescape } from 'querystring'; const encodeURL = (str: string) => { - const index = str.indexOf(':'); - if (index < 0) { + if (!str.includes(':')) { return encodeURI(unescape(str)); } - if (parse(str.slice(0, index + 1)).protocol) { - const parsed = new URL(str); + + const parsed = URL.parse(str); + if (parsed) { // Exit if input is a data url if (parsed.origin === 'null') return str; diff --git a/lib/full_url_for.ts b/lib/full_url_for.ts index 6b2bc7f0..e3501254 100644 --- a/lib/full_url_for.ts +++ b/lib/full_url_for.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line n/no-deprecated-api -import { parse } from 'url'; import encodeURL from './encode_url'; import prettyUrls from './pretty_urls'; import Cache from './cache'; @@ -16,11 +14,11 @@ function fullUrlForHelper(path = '/') { return cache.apply(`${config.url}-${prettyUrlsOptions.trailing_index}-${prettyUrlsOptions.trailing_html}-${path}`, () => { if (/^(\/\/|http(s)?:)/.test(path)) return path; - const sitehost = parse(config.url).hostname || config.url; - const data = new URL(path, `http://${sitehost}`); + const sitehost = URL.parse(config.url)?.hostname || config.url; + const data = URL.parse(path, `http://${sitehost}`); // Exit if input is an external link or a data url - if (data.hostname !== sitehost || data.origin === 'null') return path; + if (!data || data.hostname !== sitehost || data.origin === 'null') return path; path = encodeURL(config.url + `/${path}`.replace(/\/{2,}/g, '/')); path = prettyUrls(path, prettyUrlsOptions); diff --git a/lib/is_external_link.ts b/lib/is_external_link.ts index 09790580..7a6676db 100644 --- a/lib/is_external_link.ts +++ b/lib/is_external_link.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line n/no-deprecated-api -import { parse } from 'url'; import Cache from './cache'; const cache = new Cache(); @@ -16,18 +14,15 @@ function isExternalLink(input: string, sitehost: string, exclude?: string | stri // Return false early for internal link if (!/^(\/\/|http(s)?:)/.test(input)) return false; - sitehost = parse(sitehost).hostname || sitehost; + sitehost = URL.parse(sitehost)?.hostname || sitehost; if (!sitehost) return false; // handle relative url and invalid url - let data; - try { - data = new URL(input, `http://${sitehost}`); - } catch { } + const data = URL.parse(input, `http://${sitehost}`); - // if input is invalid url, data should be undefined - if (typeof data !== 'object') return false; + // if input is invalid url, data should be null + if (!data) return false; // handle mailto: javascript: vbscript: and so on if (data.origin === 'null') return false; diff --git a/lib/url_for.ts b/lib/url_for.ts index 7d5df11b..9d434124 100644 --- a/lib/url_for.ts +++ b/lib/url_for.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line n/no-deprecated-api -import { parse } from 'url'; import encodeURL from './encode_url'; import relative_url from './relative_url'; import prettyUrls from './pretty_urls'; @@ -57,11 +55,11 @@ function urlForHelper(path = '/', options: UrlForOptions | null = {}) { return cache.apply( `${config.url}-${root}-${prettyUrlsOptions.trailing_index}-${prettyUrlsOptions.trailing_html}-${path}`, () => { - const sitehost = parse(config.url).hostname || config.url; - const data = new URL(path, `http://${sitehost}`); + const sitehost = URL.parse(config.url)?.hostname || config.url; + const data = URL.parse(path, `http://${sitehost}`); // Exit if input is an external link or a data url - if (data.hostname !== sitehost || data.origin === 'null') { + if (!data || data.hostname !== sitehost || data.origin === 'null') { return path; } diff --git a/test/decode_url.spec.ts b/test/decode_url.spec.ts index 95945475..93ad4f07 100644 --- a/test/decode_url.spec.ts +++ b/test/decode_url.spec.ts @@ -68,6 +68,11 @@ describe('decodeURL', () => { decodeURL(content).should.eql('/foo bar/baz/'); }); + it('path with colon', () => { + const content = '/foo:bar%20baz/'; + decodeURL(content).should.eql('/foo:bar baz/'); + }); + it('path with unicode', () => { const content = '/foo/b%C3%A1r/'; decodeURL(content).should.eql('/foo/bár/'); @@ -87,4 +92,9 @@ describe('decodeURL', () => { const content = 'data:image/png;base64'; decodeURL(content).should.eql('data:image/png;base64'); }); + + it('invalid absolute URL', () => { + decodeURL('https:').should.eql('https:'); + decodeURL('http://[invalid').should.eql('http://[invalid'); + }); }); diff --git a/test/encode_url.spec.ts b/test/encode_url.spec.ts index 824d53a6..7dd8028d 100644 --- a/test/encode_url.spec.ts +++ b/test/encode_url.spec.ts @@ -89,6 +89,11 @@ describe('encodeURL', () => { encodeURL(content).should.eql('/foo%20bar/baz/'); }); + it('path with colon', () => { + const content = '/foo:bar baz/'; + encodeURL(content).should.eql('/foo:bar%20baz/'); + }); + it('path with unicode', () => { const content = '/foo/bár/'; encodeURL(content).should.eql('/foo/b%C3%A1r/'); @@ -108,6 +113,12 @@ describe('encodeURL', () => { const content = 'data:,Hello%2C%20World!'; encodeURL(content).should.eql(content); }); + + it('invalid absolute URL', () => { + encodeURL('https:').should.eql('https:'); + encodeURL('http://[invalid').should.eql('http://%5Binvalid'); + }); + it('encode pathname', () => { const content = 'https://fóo.com/páth%20[square]'; encodeURL(content).should.eql('https://fóo.com/p%C3%A1th%20%5Bsquare%5D'); diff --git a/test/full_url_for.spec.ts b/test/full_url_for.spec.ts index b7b1f0d8..4fd5d3fc 100644 --- a/test/full_url_for.spec.ts +++ b/test/full_url_for.spec.ts @@ -23,6 +23,11 @@ describe('full_url_for', () => { fullUrlFor('/').should.eql(ctx.config.url + '/'); }); + it('internal url - IPv6 host', () => { + ctx.config.url = 'http://[::1]:4000/blog'; + fullUrlFor('index.html').should.eql(ctx.config.url + '/index.html'); + }); + it('internal url - no duplicate slash', () => { ctx.config.url = 'https://example.com'; fullUrlFor('/index.html').should.eql('https://example.com/index.html'); @@ -89,4 +94,8 @@ describe('full_url_for', () => { fullUrlFor(url).should.eql(url); }); }); + + it('invalid URL', () => { + fullUrlFor('httpsx://[invalid').should.eql('httpsx://[invalid'); + }); }); diff --git a/test/is_external_link.spec.ts b/test/is_external_link.spec.ts index b9ca0843..af6f283f 100644 --- a/test/is_external_link.spec.ts +++ b/test/is_external_link.spec.ts @@ -24,6 +24,12 @@ describe('isExternalLink', () => { isExternalLink('/archives//hexo.io', ctx.config.url).should.eql(false); }); + it('IPv6 host', () => { + const sitehost = 'http://[::1]:4000'; + isExternalLink('http://[::1]:5000/foo', sitehost).should.eql(false); + isExternalLink('http://[::2]:4000/foo', sitehost).should.eql(true); + }); + it('hash, mailto, javascript', () => { isExternalLink('#top', ctx.config.url).should.eql(false); isExternalLink('mailto:hi@hexo.io', ctx.config.url).should.eql(false); diff --git a/test/url_for.spec.ts b/test/url_for.spec.ts index da2e6a27..a0788e1c 100644 --- a/test/url_for.spec.ts +++ b/test/url_for.spec.ts @@ -29,6 +29,12 @@ describe('url_for', () => { urlFor('/index.html').should.eql('/blog/index.html'); }); + it('internal url (relative off) - IPv6 host', () => { + ctx.config.url = 'http://[::1]:4000/blog'; + ctx.config.root = '/blog/'; + urlFor('index.html').should.eql('/blog/index.html'); + }); + it('internal url (relative on)', () => { ctx.config.relative_link = true; ctx.config.root = '/'; @@ -153,4 +159,8 @@ describe('url_for', () => { urlFor(url).should.eql(url); }); }); + + it('invalid URL', () => { + urlFor('httpsx://[invalid').should.eql('httpsx://[invalid'); + }); });