Skip to content
Closed
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
42 changes: 42 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,48 @@ describe('React OneSignal', () => {
});
});

describe('identity guards', () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);

beforeEach(() => {
error.mockClear();
});

test('login rejects empty and a null byte and keeps whitespace', async () => {
const login = vi.fn().mockResolvedValue(undefined);
window.OneSignal = { login } as unknown as typeof window.OneSignal;
await OneSignal.login('');
await OneSignal.login('ab\u0000c');
await OneSignal.login(' user ');
expect(login).toHaveBeenCalledTimes(1);
expect(login).toHaveBeenCalledWith(' user ', undefined);
expect(error).toHaveBeenCalledWith('[OneSignal] login: externalId is required');
expect(error).toHaveBeenCalledWith('[OneSignal] login: externalId contains a null byte');
});

test('empty tag values are kept and a null byte in a key is rejected', () => {
const addTag = vi.fn();
const addTags = vi.fn();
const setLanguage = vi.fn();
window.OneSignal = {
User: { addTag, addTags, setLanguage },
} as unknown as typeof window.OneSignal;
OneSignal.User.addTag('kept', '');
OneSignal.User.addTag('nul-value', 'a\u0000b');
OneSignal.User.addTag('', 'nope');
OneSignal.User.addTags({ '\u0000': 'nope', sibling: 'nope' });
OneSignal.User.addTags({ ok: null as unknown as string });
OneSignal.User.setLanguage('');
OneSignal.User.setLanguage('en\u0000');
expect(addTag).toHaveBeenCalledTimes(2);
expect(addTag).toHaveBeenCalledWith('kept', '');
expect(addTag).toHaveBeenCalledWith('nul-value', 'a\u0000b');
expect(addTags).not.toHaveBeenCalled();
expect(setLanguage).toHaveBeenCalledTimes(1);
expect(setLanguage).toHaveBeenCalledWith('');
});
});

describe('init() rejects instead of hanging', () => {
let OneSignalModule: typeof import('./index').default;

Expand Down
91 changes: 76 additions & 15 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,50 @@ function supportsVapidPush(): boolean {
}
/* E N D */

function isMissing(value: unknown, api: string): boolean {
// A NUL cannot be stored in a text column, so it is never a usable value.
if (typeof value === 'string' && value.includes('\u0000')) {
console.error(`[OneSignal] ${api} contains a null byte`);
return true;
}
if (typeof value === 'string' && value.length > 0) return false;
console.error(`[OneSignal] ${api} is required`);
return true;
}

function hasMissingEntries(
values: unknown,
api: string,
allowEmptyValue = false,
): boolean {
if (typeof values !== 'object' || values === null || Array.isArray(values)) {
return isMissing(values, api);
}
return Object.entries(values as Record<string, unknown>).some(([key, item]) => {
if (isMissing(key, `${api}: key`)) return true;
if (allowEmptyValue) return item == null && isMissing(item, `${api}: value`);
return isMissing(item, `${api}: value`);
});
}

function hasMissingItems(values: unknown, api: string): boolean {
if (!Array.isArray(values)) return isMissing(values, api);
return values.some((value) => isMissing(value, api));
}

function keepsLanguage(language: unknown): boolean {
if (typeof language !== 'string') {
console.error('[OneSignal] setLanguage: language is required');
return false;
}
if (language.includes('\u0000')) {
console.error('[OneSignal] setLanguage: language contains a null byte');
return false;
}
return true;
}


/**
* This is a SPECIAL FUNCTION
* It is a hardcoded implementation copied from the upstream/native WebSDK since we want to return a boolean immediately
Expand All @@ -101,7 +145,7 @@ const init = (options: IInitObject): Promise<void> => {
return Promise.reject(`OneSignal is already initialized.`);
}

if (!options || !options.appId) {
if (!options || !options.appId || isMissing(options.appId, 'init: appId')) {
return Promise.reject('You need to provide your OneSignal appId.');
}

Expand Down Expand Up @@ -559,6 +603,8 @@ export interface IOneSignalPushSubscription {
removeEventListener(event: 'change', listener: (change: SubscriptionChangeEvent) => void): void;
}
function oneSignalLogin(externalId: string, jwtToken?: string): Promise<void> {
if (isMissing(externalId, 'login: externalId')) return Promise.resolve();

return new Promise((resolve, reject) => {
if (isOneSignalScriptFailed) {
reject(new Error('OneSignal script failed to load.'));
Expand Down Expand Up @@ -783,84 +829,97 @@ function sessionSendUniqueOutcome(outcomeName: string): Promise<void> {
});
}
function userAddAlias(label: string, id: string): void {

if (isMissing(label, 'addAlias: label') || isMissing(id, 'addAlias: id')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addAlias(label, id);
});

}
function userAddAliases(aliases: { [key: string]: string }): void {

if (hasMissingEntries(aliases, 'addAliases')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addAliases(aliases);
});

}
function userRemoveAlias(label: string): void {

if (isMissing(label, 'removeAlias: label')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeAlias(label);
});

}
function userRemoveAliases(labels: string[]): void {

if (hasMissingItems(labels, 'removeAliases: label')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeAliases(labels);
});

}
function userAddEmail(email: string): void {

if (isMissing(email, 'addEmail: email')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addEmail(email);
});

}
function userRemoveEmail(email: string): void {

if (isMissing(email, 'removeEmail: email')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeEmail(email);
});

}
function userAddSms(smsNumber: string): void {

if (isMissing(smsNumber, 'addSms: smsNumber')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addSms(smsNumber);
});

}
function userRemoveSms(smsNumber: string): void {

if (isMissing(smsNumber, 'removeSms: smsNumber')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeSms(smsNumber);
});

}
function userAddTag(key: string, value: string): void {

if (isMissing(key, 'addTag: key')) return;
if (value == null && isMissing(value, 'addTag: value')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addTag(key, value);
});

}
function userAddTags(tags: { [key: string]: string }): void {

if (hasMissingEntries(tags, 'addTags', true)) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.addTags(tags);
});

}
function userRemoveTag(key: string): void {

if (isMissing(key, 'removeTag: key')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeTag(key);
});

}
function userRemoveTags(keys: string[]): void {

if (hasMissingItems(keys, 'removeTags: key')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.removeTags(keys);
});
Expand Down Expand Up @@ -889,7 +948,8 @@ function userRemoveEventListener(event: 'change', listener: (change: UserChangeE

}
function userSetLanguage(language: string): void {

if (!keepsLanguage(language)) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.setLanguage(language);
});
Expand All @@ -904,7 +964,8 @@ async function userGetLanguage(): string {
return retVal;
}
function userTrackEvent(name: string, properties?: Record<string, unknown>): void {

if (isMissing(name, 'trackEvent: name')) return;

window.OneSignalDeferred?.push((OneSignal) => {
OneSignal.User.trackEvent(name, properties);
});
Expand Down
Loading