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

core(service-worker): check that start_url is within SW's scope #6678

Merged
merged 1 commit into from
Dec 7, 2018
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
103 changes: 84 additions & 19 deletions lighthouse-core/audits/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,109 @@ class ServiceWorker extends Audit {
static get meta() {
return {
id: 'service-worker',
title: 'Registers a service worker',
failureTitle: 'Does not register a service worker',
title: 'Registers a service worker that controls page and start_url',
failureTitle: 'Does not register a service worker that controls page and start_url',
description: 'The service worker is the technology that enables your app to use many ' +
'Progressive Web App features, such as offline, add to homescreen, and push ' +
'notifications. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/registered-service-worker).',
requiredArtifacts: ['URL', 'ServiceWorker'],
requiredArtifacts: ['URL', 'ServiceWorker', 'Manifest'],
};
}

/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
* Find active service workers for this origin.
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>} versions
* @param {URL} pageUrl
* @return {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>}
*/
static audit(artifacts) {
const {versions, registrations} = artifacts.ServiceWorker;
const pageUrl = new URL(artifacts.URL.finalUrl);

// Find active service workers for this origin. Match against
// artifacts.URL.finalUrl so audit accounts for any redirects.
const matchingSWVersions = versions.filter(v => v.status === 'activated')
static getVersionsForOrigin(versions, pageUrl) {
return versions
.filter(v => v.status === 'activated')
.filter(v => new URL(v.scriptURL).origin === pageUrl.origin);
}

if (matchingSWVersions.length === 0) {
return {rawValue: false};
}

/**
* From the set of active service workers for this origin, find the controlling SW (if any)
* and return its scope URL.
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>} matchingSWVersions
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerRegistration>} registrations
* @param {URL} pageUrl
* @return {string|undefined}
*/
static getControllingScopeUrl(matchingSWVersions, registrations, pageUrl) {
// Find the normalized scope URLs of possibly-controlling SWs.
const matchingScopeUrls = matchingSWVersions
.map(v => registrations.find(r => r.registrationId === v.registrationId))
.filter(/** @return {r is LH.Crdp.ServiceWorker.ServiceWorkerRegistration} */ r => !!r)
.map(r => new URL(r.scopeURL).href);

// Ensure page is included in a SW's scope.
// Find most-specific applicable scope, the one controlling the page.
// See https://w3c.github.io/ServiceWorker/v1/#scope-match-algorithm
const inScope = matchingScopeUrls.some(scopeUrl => pageUrl.href.startsWith(scopeUrl));
const pageControllingScope = matchingScopeUrls
.filter(scopeUrl => pageUrl.href.startsWith(scopeUrl))
.sort((scopeA, scopeB) => scopeA.length - scopeB.length)
.pop();

return pageControllingScope;
}

/**
* Returns a failure message if there is no start_url or if the start_url isn't
* contolled by the scopeUrl.
* @param {LH.Artifacts['Manifest']} manifest
* @param {string} scopeUrl
* @return {string|undefined}
*/
static checkStartUrl(manifest, scopeUrl) {
if (!manifest) {
return 'no start_url was found because no manifest was fetched';
}
if (!manifest.value) {
return 'no start_url was found because manifest failed to parse as valid JSON';
}

const startUrl = manifest.value.start_url.value;
if (!startUrl.startsWith(scopeUrl)) {
return `the start_url ("${startUrl}") is not in the service worker's scope ("${scopeUrl}")`;
}
}

/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
*/
static audit(artifacts) {
// Match against artifacts.URL.finalUrl so audit accounts for any redirects.
const pageUrl = new URL(artifacts.URL.finalUrl);
const {versions, registrations} = artifacts.ServiceWorker;

const versionsForOrigin = ServiceWorker.getVersionsForOrigin(versions, pageUrl);
if (versionsForOrigin.length === 0) {
return {
rawValue: false,
};
}

const controllingScopeUrl = ServiceWorker.getControllingScopeUrl(versionsForOrigin,
registrations, pageUrl);
if (!controllingScopeUrl) {
return {
rawValue: false,
explanation: `This origin has one or more service workers, however the page ("${pageUrl.href}") is not in scope.`, // eslint-disable-line max-len
};
}

const startUrlFailure = ServiceWorker.checkStartUrl(artifacts.Manifest, controllingScopeUrl);
Copy link
Collaborator

Choose a reason for hiding this comment

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

wouldn't we just want to check that a service worker controls the start URL? i.e. if the one controlling this page is more specific but the start URL is the homepage and so it's controlled by a root service worker for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

wouldn't we just want to check that a service worker controls the start URL?

Yeah, we may need to think about this a bit. I originally had that check, and I don't think there's any technical problem with it, but then was thinking of the issues if you're testing a page and the declared web app manifest creating the PWA containing that page had a start_url that was controlled by a different SW than the page...so I guess the start_url is part of a different PWA than the PWA the test page is part of?

So then that seems to violate testing the page as part of a PWA. But also I don't know. But also this should only be possible when running with --disable-storage-reset anyways, so it's fine to be tougher or not tougher on them :)

Copy link
Member Author

Choose a reason for hiding this comment

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

As an example: google.com/maps has a service worker controlling everything under the google.com/maps scope. So if you go to google.com/maps, you're served offline from that SW.

However, if they accidentally put a start_url of google.com in their manifest (and google.com has a SW), trying to load the maps PWA (e.g. from the homescreen) would load a completely different PWA than the one that you saved.

Again, the likely Lighthouse user experience if someone ever accidentally does this is that storage was cleared and the start_url just doesn't work offline, but in the off chance they're running without clearing, I think it's better to fail on that case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, OK I buy that! SGTM

if (startUrlFailure) {
return {
rawValue: false,
explanation: `This page is controlled by a service worker, however ${startUrlFailure}.`,
};
}

// SW controls both finalUrl and start_url.
return {
rawValue: inScope,
rawValue: true,
};
}
}
Expand Down
Loading