Skip to content

Commit

Permalink
core(service-worker): check that start_url is within SW's scope (#6678)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendankenny authored and paulirish committed Dec 7, 2018
1 parent ebc9b2b commit 68401c8
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 48 deletions.
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);
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

0 comments on commit 68401c8

Please sign in to comment.