Skip to content

Commit f45ae69

Browse files
committed
parse URLs in Web App Manifest relative to manifest itself
1 parent 35eb5a1 commit f45ae69

16 files changed

+505
-126
lines changed

lighthouse-core/audits/cache-start-url.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
'use strict';
1919

20-
const url = require('url');
2120
const Audit = require('./audit');
2221

2322
class CacheStartUrl extends Audit {
@@ -41,7 +40,6 @@ class CacheStartUrl extends Audit {
4140
let cacheHasStartUrl = false;
4241
const manifest = artifacts.Manifest && artifacts.Manifest.value;
4342
const cacheContents = artifacts.CacheContents;
44-
const baseURL = artifacts.URL;
4543

4644
if (!(manifest && manifest.start_url && manifest.start_url.value)) {
4745
return CacheStartUrl.generateAuditResult({
@@ -58,7 +56,7 @@ class CacheStartUrl extends Audit {
5856
}
5957

6058
// Remove any UTM strings.
61-
const startURL = url.resolve(baseURL, manifest.start_url.value).toString();
59+
const startURL = manifest.start_url.value;
6260
const altStartURL = startURL
6361
.replace(/\?utm_([^=]*)=([^&]|$)*/, '')
6462
.replace(/\?$/, '');

lighthouse-core/gather/gatherers/manifest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Manifest extends Gatherer {
5454
return;
5555
}
5656

57-
this.artifact = manifestParser(response.data);
57+
this.artifact = manifestParser(response.data, response.url, options.url);
5858
}, _ => {
5959
this.artifact = Manifest._errorManifest('Unable to retrieve manifest');
6060
return;

lighthouse-core/lib/manifest-parser.js

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
'use strict';
1818

19+
const url = require('url');
1920
const validateColor = require('./web-inspector').Color.parse;
2021

2122
const ALLOWED_DISPLAY_VALUES = [
@@ -61,13 +62,6 @@ function parseString(raw, trim) {
6162
};
6263
}
6364

64-
function parseURL(raw) {
65-
// TODO: resolve url using baseURL
66-
// var baseURL = args.baseURL;
67-
// new URL(parseString(raw).value, baseURL);
68-
return parseString(raw, true);
69-
}
70-
7165
function parseColor(raw) {
7266
const color = parseString(raw);
7367

@@ -94,10 +88,69 @@ function parseShortName(jsonInput) {
9488
return parseString(jsonInput.short_name, true);
9589
}
9690

97-
function parseStartUrl(jsonInput) {
98-
// TODO: parse url using manifest_url as a base (missing).
99-
// start_url must be same-origin as Document of the top-level browsing context.
100-
return parseURL(jsonInput.start_url);
91+
/**
92+
* Returns whether the urls are of the same origin. See https://html.spec.whatwg.org/#same-origin
93+
* @param {string} url1
94+
* @param {string} url2
95+
* @return {boolean}
96+
*/
97+
function checkSameOrigin(url1, url2) {
98+
const parsed1 = url.parse(url1);
99+
const parsed2 = url.parse(url2);
100+
101+
return parsed1.protocol === parsed2.protocol &&
102+
parsed1.hostname === parsed2.hostname &&
103+
parsed1.port === parsed2.port;
104+
}
105+
106+
/**
107+
* https://w3c.github.io/manifest/#start_url-member
108+
*/
109+
function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
110+
const raw = jsonInput.start_url;
111+
112+
// 8.10(3) - discard the empty string and non-strings.
113+
if (raw === '') {
114+
return {
115+
raw,
116+
value: documentUrl,
117+
debugString: 'ERROR: start_url string empty'
118+
};
119+
}
120+
const parsedAsString = parseString(raw);
121+
if (!parsedAsString.value) {
122+
parsedAsString.value = documentUrl;
123+
return parsedAsString;
124+
}
125+
126+
// 8.10(4) - construct URL with raw as input and manifestUrl as the base.
127+
let startUrl;
128+
try {
129+
// TODO(bckenny): need better URL constructor to do this properly. See
130+
// https://github.com/GoogleChrome/lighthouse/issues/602
131+
startUrl = url.resolve(manifestUrl, raw);
132+
} catch (e) {
133+
// 8.10(5) - discard invalid URLs.
134+
return {
135+
raw,
136+
value: documentUrl,
137+
debugString: 'ERROR: invalid start_url relative to ${manifestUrl}'
138+
};
139+
}
140+
141+
// 8.10(6) - discard start_urls that are not same origin as documentUrl.
142+
if (!checkSameOrigin(startUrl, documentUrl)) {
143+
return {
144+
raw,
145+
value: documentUrl,
146+
debugString: 'ERROR: start_url must be same-origin as document'
147+
};
148+
}
149+
150+
return {
151+
raw,
152+
value: startUrl
153+
};
101154
}
102155

103156
function parseDisplay(jsonInput) {
@@ -130,9 +183,20 @@ function parseOrientation(jsonInput) {
130183
return orientation;
131184
}
132185

133-
function parseIcon(raw) {
134-
// TODO: pass manifest url as base.
135-
let src = parseURL(raw.src);
186+
function parseIcon(raw, manifestUrl) {
187+
// 9.4(3)
188+
const src = parseString(raw.src, true);
189+
// 9.4(4) - discard if trimmed value is the empty string.
190+
if (src.value === '') {
191+
src.value = undefined;
192+
}
193+
if (src.value) {
194+
// TODO(bckenny): need better URL constructor to do this properly. See
195+
// https://github.com/GoogleChrome/lighthouse/issues/602
196+
// 9.4(4) - construct URL with manifest URL as the base
197+
src.value = url.resolve(manifestUrl, src.value);
198+
}
199+
136200
let type = parseString(raw.type, true);
137201

138202
let density = {
@@ -167,7 +231,7 @@ function parseIcon(raw) {
167231
};
168232
}
169233

170-
function parseIcons(jsonInput) {
234+
function parseIcons(jsonInput, manifestUrl) {
171235
const raw = jsonInput.icons;
172236
let value;
173237

@@ -187,8 +251,15 @@ function parseIcons(jsonInput) {
187251
};
188252
}
189253

190-
// TODO(bckenny): spec says to skip icons missing `src`. Warn instead?
191-
value = raw.filter(icon => !!icon.src).map(parseIcon);
254+
// TODO(bckenny): spec says to skip icons missing `src`, so debug messages on
255+
// individual icons are lost. Warn instead?
256+
value = raw
257+
// 9.6(3)(1)
258+
.filter(icon => icon.src !== undefined)
259+
// 9.6(3)(2)(1)
260+
.map(icon => parseIcon(icon, manifestUrl))
261+
// 9.6(3)(2)(2)
262+
.filter(parsedIcon => parsedIcon.value.src.value !== undefined);
192263

193264
return {
194265
raw,
@@ -200,15 +271,27 @@ function parseIcons(jsonInput) {
200271
function parseApplication(raw) {
201272
let platform = parseString(raw.platform, true);
202273
let id = parseString(raw.id, true);
203-
// TODO: pass manfiest url as base.
204-
let url = parseURL(raw.url);
274+
275+
// 10.2.(2) and 10.2.(3)
276+
const appUrl = parseString(raw.url, true);
277+
if (appUrl.value) {
278+
try {
279+
// TODO(bckenny): need better URL constructor to do this properly. See
280+
// https://github.com/GoogleChrome/lighthouse/issues/602
281+
// 10.2.(4) - attempt to construct URL.
282+
appUrl.value = url.parse(appUrl.value).href;
283+
} catch (e) {
284+
appUrl.value = undefined;
285+
appUrl.debugString = 'ERROR: invalid application URL ${raw.url}';
286+
}
287+
}
205288

206289
return {
207290
raw,
208291
value: {
209292
platform,
210293
id,
211-
url
294+
url: appUrl
212295
},
213296
debugString: undefined
214297
};
@@ -234,8 +317,12 @@ function parseRelatedApplications(jsonInput) {
234317
};
235318
}
236319

237-
// TODO(bckenny): spec says to skip apps missing `platform`. Warn instead?
238-
value = raw.filter(application => !!application.platform).map(parseApplication);
320+
// TODO(bckenny): spec says to skip apps missing `platform`, so debug messages
321+
// on individual apps are lost. Warn instead?
322+
value = raw
323+
.filter(application => !!application.platform)
324+
.map(parseApplication)
325+
.filter(parsedApp => !!parsedApp.value.id.value || !!parsedApp.value.url.value);
239326

240327
return {
241328
raw,
@@ -273,7 +360,18 @@ function parseBackgroundColor(jsonInput) {
273360
return parseColor(jsonInput.background_color);
274361
}
275362

276-
function parse(string) {
363+
/**
364+
* Parse a manifest from the given inputs.
365+
* @param {string} string Manifest JSON string.
366+
* @param {string} manifestUrl URL of manifest file.
367+
* @param {string} documentUrl URL of document containing manifest link element.
368+
* @return {!ManifestNode<(!Manifest|undefined)>}
369+
*/
370+
function parse(string, manifestUrl, documentUrl) {
371+
if (manifestUrl === undefined || documentUrl === undefined) {
372+
throw new Error('Manifest and document URLs required for manifest parsing.');
373+
}
374+
277375
let jsonInput;
278376

279377
try {
@@ -290,10 +388,10 @@ function parse(string) {
290388
let manifest = {
291389
name: parseName(jsonInput),
292390
short_name: parseShortName(jsonInput),
293-
start_url: parseStartUrl(jsonInput),
391+
start_url: parseStartUrl(jsonInput, manifestUrl, documentUrl),
294392
display: parseDisplay(jsonInput),
295393
orientation: parseOrientation(jsonInput),
296-
icons: parseIcons(jsonInput),
394+
icons: parseIcons(jsonInput, manifestUrl),
297395
related_applications: parseRelatedApplications(jsonInput),
298396
prefer_related_applications: parsePreferRelatedApplications(jsonInput),
299397
theme_color: parseThemeColor(jsonInput),

lighthouse-core/test/audits/background-color-test.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,20 @@ const Audit = require('../../audits/manifest-background-color.js');
1717
const assert = require('assert');
1818
const manifestSrc = JSON.stringify(require('../fixtures/manifest.json'));
1919
const manifestParser = require('../../lib/manifest-parser');
20-
const exampleManifest = manifestParser(manifestSrc);
20+
const exampleManifest = manifestParser(manifestSrc, 'https://example.com/', 'https://example.com/');
21+
22+
const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json';
23+
const EXAMPLE_DOC_URL = 'https://example.com/index.html';
24+
25+
/**
26+
* Simple manifest parsing helper when the manifest URLs aren't material to the
27+
* test. Uses example.com URLs for testing.
28+
* @param {string} manifestSrc
29+
* @return {!ManifestNode<(!Manifest|undefined)>}
30+
*/
31+
function noUrlManifestParser(manifestSrc) {
32+
return manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
33+
}
2134

2235
/* global describe, it*/
2336

@@ -32,14 +45,14 @@ describe('Manifest: background color audit', () => {
3245

3346
it('fails when an empty manifest is present', () => {
3447
const artifacts = {
35-
Manifest: manifestParser('{}')
48+
Manifest: noUrlManifestParser('{}')
3649
};
3750
return assert.equal(Audit.audit(artifacts).rawValue, false);
3851
});
3952

4053
it('fails when a minimal manifest contains no background_color', () => {
4154
const artifacts = {
42-
Manifest: manifestParser(JSON.stringify({
55+
Manifest: noUrlManifestParser(JSON.stringify({
4356
start_url: '/'
4457
}))
4558
};
@@ -50,7 +63,7 @@ describe('Manifest: background color audit', () => {
5063

5164
it('fails when a minimal manifest contains an invalid background_color', () => {
5265
const artifacts = {
53-
Manifest: manifestParser(JSON.stringify({
66+
Manifest: noUrlManifestParser(JSON.stringify({
5467
background_color: 'no'
5568
}))
5669
};
@@ -61,7 +74,7 @@ describe('Manifest: background color audit', () => {
6174

6275
it('succeeds when a minimal manifest contains a valid background_color', () => {
6376
const artifacts = {
64-
Manifest: manifestParser(JSON.stringify({
77+
Manifest: noUrlManifestParser(JSON.stringify({
6578
background_color: '#FAFAFA'
6679
}))
6780
};

lighthouse-core/test/audits/cache-start-url-test.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ const Audit = require('../../audits/cache-start-url.js');
1717
const assert = require('assert');
1818
const manifestSrc = JSON.stringify(require('../fixtures/manifest.json'));
1919
const manifestParser = require('../../lib/manifest-parser');
20-
const exampleManifest = manifestParser(manifestSrc);
2120
const CacheContents = ['https://another.example.com/', 'https://example.com/'];
2221
const URL = 'https://example.com';
2322
const AltURL = 'https://example.com/?utm_source=http203';
23+
const exampleManifest = manifestParser(manifestSrc, URL, URL);
2424

2525
/* global describe, it*/
2626

@@ -43,18 +43,6 @@ describe('Cache: start_url audit', () => {
4343
assert.equal(output.debugString, 'No cache or URL detected');
4444
});
4545

46-
// Need to disable camelcase check for dealing with start_url.
47-
/* eslint-disable camelcase */
48-
it('fails when a manifest contains no start_url', () => {
49-
const artifacts = {
50-
Manifest: manifestParser('{}')
51-
};
52-
const output = Audit.audit(artifacts);
53-
assert.equal(output.rawValue, false);
54-
assert.equal(output.debugString, 'start_url not present in Manifest');
55-
});
56-
/* eslint-enable camelcase */
57-
5846
it('succeeds when given a manifest with a start_url, cache contents, and a URL', () => {
5947
return assert.equal(Audit.audit({
6048
Manifest: exampleManifest,

lighthouse-core/test/audits/display-test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ const manifestParser = require('../../lib/manifest-parser');
1818
const assert = require('assert');
1919

2020
const manifestSrc = JSON.stringify(require('../fixtures/manifest.json'));
21-
const exampleManifest = manifestParser(manifestSrc);
21+
const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json';
22+
const EXAMPLE_DOC_URL = 'https://example.com/index.html';
23+
const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
2224

2325
/* global describe, it*/
2426

@@ -38,7 +40,7 @@ describe('Mobile-friendly: display audit', () => {
3840

3941
it('falls back to the successful default when there is no manifest display property', () => {
4042
const artifacts = {
41-
Manifest: manifestParser('{}')
43+
Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL)
4244
};
4345
const output = Audit.audit(artifacts);
4446

@@ -51,7 +53,7 @@ describe('Mobile-friendly: display audit', () => {
5153
const artifacts = {
5254
Manifest: manifestParser(JSON.stringify({
5355
display: 'standalone'
54-
}))
56+
}), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL)
5557
};
5658
const output = Audit.audit(artifacts);
5759
assert.equal(output.score, true);

0 commit comments

Comments
 (0)