Skip to content

Modify URL parsing for React Native compat. #4543

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

Merged
merged 2 commits into from
Feb 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 17 additions & 12 deletions packages-exp/auth-exp/src/core/action_code_url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import { extractQuerystring, querystringDecode } from '@firebase/util';
import { ActionCodeOperation } from '../model/public_types';
import { AuthErrorCode } from './errors';
import { _assert } from './util/assert';
Expand Down Expand Up @@ -63,14 +64,18 @@ function parseMode(mode: string | null): ActionCodeOperation | null {
* @param url
*/
function parseDeepLink(url: string): string {
const uri = new URL(url);
const link = uri.searchParams.get('link');
const link = querystringDecode(extractQuerystring(url))['link'];

// Double link case (automatic redirect).
const doubleDeepLink = link ? new URL(link).searchParams.get('link') : null;
const doubleDeepLink = link
? querystringDecode(extractQuerystring(link))['deep_link_id']
: null;
// iOS custom scheme links.
const iOSDeepLink = uri.searchParams.get('deep_link_id');
const iOSDeepLink = querystringDecode(extractQuerystring(url))[
'deep_link_id'
];
const iOSDoubleDeepLink = iOSDeepLink
? new URL(iOSDeepLink).searchParams.get('link')
? querystringDecode(extractQuerystring(iOSDeepLink))['link']
: null;
return iOSDoubleDeepLink || iOSDeepLink || doubleDeepLink || link || url;
}
Expand Down Expand Up @@ -115,18 +120,18 @@ export class ActionCodeURL {
* @internal
*/
constructor(actionLink: string) {
const uri = new URL(actionLink);
const apiKey = uri.searchParams.get(QueryField.API_KEY);
const code = uri.searchParams.get(QueryField.CODE);
const operation = parseMode(uri.searchParams.get(QueryField.MODE));
const searchParams = querystringDecode(extractQuerystring(actionLink));
const apiKey = searchParams[QueryField.API_KEY] ?? null;
const code = searchParams[QueryField.CODE] ?? null;
const operation = parseMode(searchParams[QueryField.MODE] ?? null);
// Validate API key, code and mode.
_assert(apiKey && code && operation, AuthErrorCode.ARGUMENT_ERROR);
this.apiKey = apiKey;
this.operation = operation;
this.code = code;
this.continueUrl = uri.searchParams.get(QueryField.CONTINUE_URL);
this.languageCode = uri.searchParams.get(QueryField.LANGUAGE_CODE);
this.tenantId = uri.searchParams.get(QueryField.TENANT_ID);
this.continueUrl = searchParams[QueryField.CONTINUE_URL] ?? null;
this.languageCode = searchParams[QueryField.LANGUAGE_CODE] ?? null;
this.tenantId = searchParams[QueryField.TENANT_ID] ?? null;
}

/**
Expand Down
50 changes: 44 additions & 6 deletions packages-exp/auth-exp/src/core/auth/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,60 @@ export function useAuthEmulator(
AuthErrorCode.INVALID_EMULATOR_SCHEME
);

const parsedUrl = new URL(url);
const disableWarnings = !!options?.disableWarnings;

// Store the normalized URL whose path is always nonempty (i.e. containing at least a single '/').
authInternal.config.emulator = { url: parsedUrl.toString() };
const protocol = extractProtocol(url);
const { host, port } = extractHostAndPort(url);
const portStr = port === null ? '' : `:${port}`;

// Always replace path with "/" (even if input url had no path at all, or had a different one).
authInternal.config.emulator = { url: `${protocol}//${host}${portStr}/` };
authInternal.settings.appVerificationDisabledForTesting = true;
authInternal.emulatorConfig = Object.freeze({
host: parsedUrl.hostname,
port: parsedUrl.port ? Number(parsedUrl.port) : null,
protocol: parsedUrl.protocol.replace(':', ''),
host,
port,
protocol: protocol.replace(':', ''),
options: Object.freeze({ disableWarnings })
});

emitEmulatorWarning(disableWarnings);
}

function extractProtocol(url: string): string {
const protocolEnd = url.indexOf(':');
return protocolEnd < 0 ? '' : url.substr(0, protocolEnd + 1);
}

function extractHostAndPort(
url: string
): { host: string; port: number | null } {
const protocol = extractProtocol(url);
const authority = /(\/\/)?([^?#/]+)/.exec(url.substr(protocol.length)); // Between // and /, ? or #.
if (!authority) {
return { host: '', port: null };
}
const hostAndPort = authority[2].split('@').pop() || ''; // Strip out "username:password@".
const bracketedIPv6 = /^(\[[^\]]+\])(:|$)/.exec(hostAndPort);
if (bracketedIPv6) {
const host = bracketedIPv6[1];
return { host, port: parsePort(hostAndPort.substr(host.length + 1)) };
} else {
const [host, port] = hostAndPort.split(':');
return { host, port: parsePort(port) };
}
}

function parsePort(portStr: string): number | null {
if (!portStr) {
return null;
}
const port = Number(portStr);
if (isNaN(port)) {
return null;
}
return port;
}

function emitEmulatorWarning(disableBanner: boolean): void {
function attachBanner(): void {
const el = document.createElement('p');
Expand Down
20 changes: 7 additions & 13 deletions packages-exp/auth-exp/src/core/util/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,16 @@ export function _getRedirectUrl(
params.tid = auth.tenantId;
}

for (const key of Object.keys(params)) {
if ((params as Record<string, unknown>)[key] === undefined) {
delete (params as Record<string, unknown>)[key];
}
}

// TODO: maybe set eid as endipointId
// TODO: maybe set fw as Frameworks.join(",")

const url = new URL(
`${getHandlerBase(auth)}?${querystring(
params as Record<string, string | number>
).slice(1)}`
);

return url.toString();
const paramsDict = params as Record<string, string | number>;
for (const key of Object.keys(paramsDict)) {
if (paramsDict[key] === undefined) {
delete paramsDict[key];
}
}
return `${getHandlerBase(auth)}?${querystring(paramsDict).slice(1)}`;
}

function getHandlerBase({ config }: AuthInternal): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ export function getReactNativePersistence(
}

_addListener(_key: string, _listener: StorageEventListener): void {
debugFail('not implemented');
// Listeners are not supported for React Native storage.
return;
}

_removeListener(_key: string, _listener: StorageEventListener): void {
debugFail('not implemented');
// Listeners are not supported for React Native storage.
return;
}
};
}
23 changes: 19 additions & 4 deletions packages/util/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,30 @@ export function querystring(querystringParams: {
* Decodes a querystring (e.g. ?arg=val&arg2=val2) into a params object
* (e.g. {arg: 'val', arg2: 'val2'})
*/
export function querystringDecode(querystring: string): object {
const obj: { [key: string]: unknown } = {};
export function querystringDecode(querystring: string): Record<string, string> {
const obj: Record<string, string> = {};
const tokens = querystring.replace(/^\?/, '').split('&');

tokens.forEach(token => {
if (token) {
const key = token.split('=');
obj[key[0]] = key[1];
const [key, value] = token.split('=');
obj[decodeURIComponent(key)] = decodeURIComponent(value);
}
});
return obj;
}

/**
* Extract the query string part of a URL, including the leading question mark (if present).
*/
export function extractQuerystring(url: string): string {
const queryStart = url.indexOf('?');
if (!queryStart) {
return '';
}
const fragmentStart = url.indexOf('#', queryStart);
return url.substring(
queryStart,
fragmentStart > 0 ? fragmentStart : undefined
);
}