Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Missing Methods in URL Class to Handle Base URL and Relative Path Combinations #45055

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 88 additions & 81 deletions packages/react-native/Libraries/Blob/URL.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,96 +114,104 @@ export class URLSearchParams {
}
}

function validateBaseUrl(url: string) {
// from this MIT-licensed gist: https://gist.github.com/dperini/729294
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test(
url,
);
}

export class URL {
_url: string;
_searchParamsInstance: ?URLSearchParams = null;

static createObjectURL(blob: Blob): string {
if (BLOB_URL_PREFIX === null) {
throw new Error('Cannot create URL for blob!');
}
return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}`;
}

static revokeObjectURL(url: string) {
// Do nothing.
}

// $FlowFixMe[missing-local-annot]
constructor(url: string, base: string | URL) {
let baseUrl = null;
if (!base || validateBaseUrl(url)) {
this._url = url;
if (!this._url.endsWith('/')) {
this._url += '/';
}
} else {
if (typeof base === 'string') {
baseUrl = base;
if (!validateBaseUrl(baseUrl)) {
throw new TypeError(`Invalid base URL: ${baseUrl}`);
}
} else {
baseUrl = base.toString();
}
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!url.startsWith('/')) {
url = `/${url}`;
}
if (baseUrl.endsWith(url)) {
url = '';
}
this._url = `${baseUrl}${url}`;
}
}
function resolveRelativeUrl(relative: string, base: string): string {
const baseUrl = new URL(base);

get hash(): string {
throw new Error('URL.hash is not implemented');
if (relative.startsWith('http://') || relative.startsWith('https://')) {
return relative;
}

get host(): string {
throw new Error('URL.host is not implemented');
if (relative.startsWith('/')) {
return baseUrl.protocol + '//' + baseUrl.host + encodeURI(relative);
}

get hostname(): string {
throw new Error('URL.hostname is not implemented');
}
const baseParts = baseUrl.pathname.split('/');
const relativeParts = relative.split('/');

get href(): string {
return this.toString();
}
baseParts.pop();

get origin(): string {
throw new Error('URL.origin is not implemented');
for (const part of relativeParts) {
if (part === '.') continue;
if (part === '..') baseParts.pop();
else baseParts.push(part);
}

get password(): string {
throw new Error('URL.password is not implemented');
}
return baseUrl.protocol + '//' + baseUrl.host + baseParts.join('/');
}

get pathname(): string {
throw new Error('URL.pathname not implemented');
}
export class URL {
protocol: string;
username: string;
password: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
origin: string;
_url: string;
_searchParamsInstance: ?URLSearchParams = null;

get port(): string {
throw new Error('URL.port is not implemented');
}
constructor(url: string, base?: string | URL) {
if (base) {
if (typeof base === 'string') {
base = new URL(base);
}
url = resolveRelativeUrl(url, base.href);
} else {
url = encodeURI(url);
}

get protocol(): string {
throw new Error('URL.protocol is not implemented');
}
const parser = this.parseURL(url);
this._url = url;
this.protocol = parser.protocol;
this.username = parser.username;
this.password = parser.password;
this.host = parser.host;
this.hostname = parser.hostname;
this.port = parser.port;
this.pathname = parser.pathname;
this.search = parser.search;
this.hash = parser.hash;
this.origin = parser.origin;

if (this.pathname === '/' && !this.href.endsWith('/')) {
this._url += '/';
}

get search(): string {
throw new Error('URL.search is not implemented');
this._searchParamsInstance = new URLSearchParams(this.search);
}

parseURL(url: string): {
protocol: string,
username: string,
password: string,
host: string,
hostname: string,
port: string,
pathname: string,
search: string,
hash: string,
origin: string,
} {
const urlPattern =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this regex come from?

At a quick glance, I know we should be more permissive in allowing schemes that are not just http.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman Oh I almost missed that, I'm not good enough to see that, thanks for the advice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we getting this regex from a source, or trying to derive it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman No, I didn't get this regex from a source. I derived it myself.

/^(https?:\/\/)?(([^:\/?#]*)(?::([^:\/?#]*))?@)?([^:\/?#]*)(?::(\d+))?((?:\/[^?#]*)*)(\?[^#]*)?(#.*)?$/;

const matches = url.match(urlPattern);

return {
protocol: matches?.[1] ? matches[1].slice(0, -2) : '',
username: matches?.[3] || '',
password: matches?.[4] || '',
host: matches?.[6] ? matches[5] + ':' + matches[6] : matches?.[5] || '',
hostname: matches?.[5] || '',
port: matches?.[6] || '',
pathname: matches?.[7] || '/',
search: matches?.[8] || '',
hash: matches?.[9] || '',
origin: matches?.[1] ? matches[1] + matches[5] : '',
};
}

get searchParams(): URLSearchParams {
Expand All @@ -213,21 +221,20 @@ export class URL {
return this._searchParamsInstance;
}

toJSON(): string {
get href(): string {
return this.toString();
}

toString(): string {
if (this._searchParamsInstance === null) {
return this._url;
}
// $FlowFixMe[incompatible-use]
const instanceString = this._searchParamsInstance.toString();
const separator = this._url.indexOf('?') > -1 ? '&' : '?';
return this._url + separator + instanceString;
}

get username(): string {
throw new Error('URL.username is not implemented');
toJSON(): string {
return this.toString();
}
}
113 changes: 113 additions & 0 deletions packages/react-native/Libraries/Blob/__tests__/URL-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,117 @@ describe('URL', function () {
const k = new URL('en-US/docs', 'https://developer.mozilla.org');
expect(k.href).toBe('https://developer.mozilla.org/en-US/docs');
});

it('should handle URLs with no base', () => {
const url = new URL('https://example.com');
expect(url.href).toBe('https://example.com/');
expect(url.hash).toBe('');
expect(url.host).toBe('example.com');
expect(url.hostname).toBe('example.com');
expect(url.origin).toBe('https://example.com');
expect(url.password).toBe('');
expect(url.pathname).toBe('/');
expect(url.port).toBe('');
expect(url.protocol).toBe('https:');
expect(url.search).toBe('');
expect(url.username).toBe('');
});

it('should return correct protocol', () => {
const url = new URL('https://example.com');
expect(url.protocol).toBe('https:');
});

it('should return correct host', () => {
const url = new URL('https://example.com:8080/path');
expect(url.host).toBe('example.com:8080');
});

it('should return correct hostname', () => {
const url = new URL('https://example.com:8080/path');
expect(url.hostname).toBe('example.com');
});

it('should return correct port', () => {
const url = new URL('https://example.com:8080/path');
expect(url.port).toBe('8080');
});

it('should return correct pathname', () => {
const url = new URL('https://example.com/path/name');
expect(url.pathname).toBe('/path/name');
});

it('should return correct search', () => {
const url = new URL('https://example.com/path?query=123');
expect(url.search).toBe('?query=123');
});

it('should return correct hash', () => {
const url = new URL('https://example.com/path#section');
expect(url.hash).toBe('#section');
});

it('should return correct href', () => {
const url = new URL('https://example.com/path');
expect(url.href).toBe('https://example.com/path');
});

it('should return correct origin', () => {
const url = new URL('https://example.com/path');
expect(url.origin).toBe('https://example.com');
});

it('should return correct username', () => {
const url = new URL('https://user:pass@example.com/path');
expect(url.username).toBe('user');
});

it('should return correct password', () => {
const url = new URL('https://user:pass@example.com/path');
expect(url.password).toBe('pass');
});

it('should handle URLs with special characters', () => {
const url = new URL('https://example.com/path with spaces');
expect(url.href).toBe('https://example.com/path%20with%20spaces');
});

it('should handle URLs with unicode characters', () => {
const url = new URL('https://example.com/路径');
expect(url.href).toBe('https://example.com/%E8%B7%AF%E5%BE%84');
});

it('should handle relative URLs with ..', () => {
const base = new URL('https://example.com/path/subpath');
const url = new URL('../up', base);
expect(url.href).toBe('https://example.com/up');
});

it('should handle relative URLs with ./', () => {
const base = new URL('https://example.com/path/subpath');
const url = new URL('./next', base);
expect(url.href).toBe('https://example.com/path/next');
});

it('should handle empty paths', () => {
const url = new URL('https://example.com');
expect(url.pathname).toBe('/');
});

it('should handle URLs with multiple query parameters', () => {
const url = new URL('https://example.com/path?query1=123&query2=456');
expect(url.search).toBe('?query1=123&query2=456');
});

it('should handle URLs with fragment identifiers', () => {
const url = new URL('https://example.com/path#section');
expect(url.hash).toBe('#section');
});

it('should handle URLs with authentication', () => {
const url = new URL('https://user:password@example.com');
expect(url.username).toBe('user');
expect(url.password).toBe('password');
});
});
Loading