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

Fix SW scope handling of base html tag #754

Merged
merged 5 commits into from
Feb 26, 2021
Merged
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
3 changes: 3 additions & 0 deletions express_webpack/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<!-- Some sites use this HTML base tag to change the origin for all relative links.
Ensure OneSignal SDK correctly handles this. -->
<base href="https://www.example.com/">
<script src="/sdks/Dev-OneSignalSDK.js" async=""></script>
<script>
const SERVICE_WORKER_PATH = "push/onesignal/";
Expand Down
7 changes: 5 additions & 2 deletions src/helpers/page/ServiceWorkerUtilHelper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Log from "../../libraries/Log";

export default class ServiceWorkerUtilHelper {
// Get the service worker based on a scope as a domain can have none to many service workers.
// Get the service worker based on a relative scope.
// A relative scope is required as a domain can have none to many service workers installed.
static async getRegistration(scope: string): Promise<ServiceWorkerRegistration | null | undefined> {
try {
return await navigator.serviceWorker.getRegistration(scope);
// Adding location.origin to negate the effects of a possible <base> html tag the page may have.
const url = location.origin + scope;
return await navigator.serviceWorker.getRegistration(url);
} catch (e) {
// This could be null in an HTTP context or error if the user doesn't accept cookies
Log.warn("[Service Worker Status] Error Checking service worker registration", scope, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain
return this.dispatchEventUtil.dispatchEvent(evt);
}

// clientURL can be relative or a full URL
async getRegistration(clientURL?: string): Promise<ServiceWorkerRegistration | undefined> {
const scope = clientURL || "/";
const scope = clientURL ?
clientURL.replace(location.origin, "") : // Turn a possible full URL into a scope
"/";

// 1. If we find an exact path match
let registration = this.serviceWorkerRegistrations.get(scope);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export class MockServiceWorkerContainerWithAPIBan extends MockServiceWorkerConta
if (!clientURL) {
throw new Error("Must include clientURL to get the SW of the scope we registered, not the current page being viewed.")
}

if (!clientURL.startsWith(location.origin)) {
throw new Error("Must always use full URL as the HTML <base> tag can change the relative path.");
}

return super.getRegistration(clientURL);
}

Expand Down
2 changes: 1 addition & 1 deletion test/unit/managers/ServiceWorkerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ test('installWorker() installs Worker new scope when it changes', async t => {
// 4. Ensure we kept the original ServiceWorker.
// A. Original could contain more than just OneSignal code
// B. New ServiceWorker instance will have it's own pushToken, this may have not been sent onesignal.com yet.
const orgRegistration = await navigator.serviceWorker.getRegistration("/");
const orgRegistration = await navigator.serviceWorker.getRegistration(`${location.origin}/`);
t.is(new URL(orgRegistration!.scope).pathname, "/");
});

Expand Down
2 changes: 1 addition & 1 deletion test/unit/meta/mockPushManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { MockPushManager } from "../../support/mocks/service-workers/models/Mock
import { MockPushSubscription } from "../../support/mocks/service-workers/models/MockPushSubscription";

async function getServiceWorkerRegistration(): Promise<ServiceWorkerRegistration | undefined> {
return await navigator.serviceWorker.getRegistration("/");
return await navigator.serviceWorker.getRegistration(`${location.origin}/`);
}

test.beforeEach(async t => {
Expand Down
14 changes: 14 additions & 0 deletions test/unit/meta/mockServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ test('mock service worker registration should return the registered worker', asy
t.deepEqual(registrations, [registration]);
});

test('mock service worker registration should return registered worker with relative scope', async t => {
const scope = '/';
await navigator.serviceWorker.register('/worker.js', { scope });
const registration = await navigator.serviceWorker.getRegistration(scope);
t.truthy(registration);
});

test('mock service worker registration should return registered worker with full url scope', async t => {
const scope = `${location.origin}/`;
await navigator.serviceWorker.register('/worker.js', { scope });
const registration = await navigator.serviceWorker.getRegistration(scope);
t.truthy(registration);
});

test('mock service worker getRegistrations should return multiple registered workers', async t => {
const expectedRegistrations = [] as ServiceWorkerRegistration[];
expectedRegistrations.push(await navigator.serviceWorker.register('/workerA.js', { scope: '/' }));
Expand Down
39 changes: 24 additions & 15 deletions test/unit/meta/mockServiceWorkerContainerWithAPIBan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test('mock service worker browser API properties should exist', async t => {
});

test('mock service worker should not return an existing registration for a clean run', async t => {
const registration = await navigator.serviceWorker.getRegistration("/");
const registration = await navigator.serviceWorker.getRegistration(`${location.origin}/`);
t.is(registration, undefined);

const registrations = await navigator.serviceWorker.getRegistrations();
Expand All @@ -25,67 +25,76 @@ test('mock service worker should not return an existing registration for a clean
test('mock service worker unregistration should return no registered workers', async t => {
await navigator.serviceWorker.register('/worker.js', { scope: '/' });

const initialRegistration = await navigator.serviceWorker.getRegistration("/");
const initialRegistration = await navigator.serviceWorker.getRegistration(`${location.origin}/`);
await initialRegistration!.unregister();

const postUnsubscribeRegistration = await navigator.serviceWorker.getRegistration("/");
const postUnsubscribeRegistration = await navigator.serviceWorker.getRegistration(`${location.origin}/`);
t.is(postUnsubscribeRegistration, undefined);

const registrations = await navigator.serviceWorker.getRegistrations();
t.deepEqual(registrations, []);
});

test('Should thow when calling navigator.serviceWorker.controller', async t => {
test('Should throw when calling navigator.serviceWorker.controller', async t => {
try {
navigator.serviceWorker!.controller;
t.fail("Should have thrown!")
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Don't use, assumes page control!"));
}
});

test('Should thow when calling navigator.serviceWorker.ready', async t => {
test('Should throw when calling navigator.serviceWorker.ready', async t => {
try {
await navigator.serviceWorker!.ready;
t.fail("Should have thrown!")
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Don't use, assumes page control!"));
}
});

test('Should thow when calling navigator.serviceWorker.getRegistration without a url', async t => {
test('Should throw when calling navigator.serviceWorker.getRegistration without a url', async t => {
try {
await navigator.serviceWorker!.getRegistration();
t.fail("Should have thrown!")
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Must include clientURL to get the SW of the scope we registered, not the current page being viewed."));
}
});

test('Should thow when setting navigator.serviceWorker.oncontrollerchange', async t => {
test('Should throw when calling navigator.serviceWorker.getRegistration with a relative URL', async t => {
try {
await navigator.serviceWorker!.getRegistration("/");
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Must always use full URL as the HTML <base> tag can change the relative path."));
}
});

test('Should throw when setting navigator.serviceWorker.oncontrollerchange', async t => {
try {
navigator.serviceWorker.oncontrollerchange = function(){};
t.fail("Should have thrown!")
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Don't use, assumes page control!"));
}
});

test('Should thow when setting navigator.serviceWorker.addEventListener(controllerchange, ...)', async t => {
test('Should throw when setting navigator.serviceWorker.addEventListener(controllerchange, ...)', async t => {
try {
navigator.serviceWorker.addEventListener('controllerchange', function(){});
t.fail("Should have thrown!")
t.fail("Should have thrown!");
} catch (e) {
t.deepEqual(e, new Error("Don't use, assumes page control!"));
}
});

test('Should not thow when setting navigator.serviceWorker.addEventListener(message, ...)', async t => {
test('Should not throw when setting navigator.serviceWorker.addEventListener(message, ...)', async t => {
navigator.serviceWorker.addEventListener('message', function(){});
t.pass();
});

test('Should not thow when setting navigator.serviceWorker.addEventListener(messageerror, ...)', async t => {
test('Should not throw when setting navigator.serviceWorker.addEventListener(messageerror, ...)', async t => {
navigator.serviceWorker.addEventListener('messageerror', function(){});
t.pass();
});