Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6 * validateManifest() warns of the following errors:
7 * - No manifest specified in page
8 * - Manifest is not utf-8
9 * - Manifest mimetype not text/cache-manifest
10 * - Manifest does not begin with "CACHE MANIFEST"
11 * - Page modified since appcache last changed
12 * - Duplicate entries
13 * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
14 * but blocked by FALLBACK namespace
15 * - Detect referenced files that are not available
16 * - Detect referenced files that have cache-control set to no-store
17 * - Wildcards used in a section other than NETWORK
18 * - Spaces in URI not replaced with %20
19 * - Completely invalid URIs
20 * - Too many dot dot slash operators
21 * - SETTINGS section is valid
22 * - Invalid section name
23 * - etc.
24 */
26 "use strict";
28 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
30 let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
31 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
32 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
34 this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
36 function AppCacheUtils(documentOrUri) {
37 this._parseManifest = this._parseManifest.bind(this);
39 if (documentOrUri) {
40 if (typeof documentOrUri == "string") {
41 this.uri = documentOrUri;
42 }
43 if (/HTMLDocument/.test(documentOrUri.toString())) {
44 this.doc = documentOrUri;
45 }
46 }
47 }
49 AppCacheUtils.prototype = {
50 get cachePath() {
51 return "";
52 },
54 validateManifest: function ACU_validateManifest() {
55 let deferred = promise.defer();
56 this.errors = [];
57 // Check for missing manifest.
58 this._getManifestURI().then(manifestURI => {
59 this.manifestURI = manifestURI;
61 if (!this.manifestURI) {
62 this._addError(0, "noManifest");
63 deferred.resolve(this.errors);
64 }
66 this._getURIInfo(this.manifestURI).then(uriInfo => {
67 this._parseManifest(uriInfo).then(() => {
68 // Sort errors by line number.
69 this.errors.sort(function(a, b) {
70 return a.line - b.line;
71 });
72 deferred.resolve(this.errors);
73 });
74 });
75 });
77 return deferred.promise;
78 },
80 _parseManifest: function ACU__parseManifest(uriInfo) {
81 let deferred = promise.defer();
82 let manifestName = uriInfo.name;
83 let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]);
85 if (uriInfo.charset.toLowerCase() != "utf-8") {
86 this._addError(0, "notUTF8", uriInfo.charset);
87 }
89 if (uriInfo.mimeType != "text/cache-manifest") {
90 this._addError(0, "badMimeType", uriInfo.mimeType);
91 }
93 let parser = new ManifestParser(uriInfo.text, this.manifestURI);
94 let parsed = parser.parse();
96 if (parsed.errors.length > 0) {
97 this.errors.push.apply(this.errors, parsed.errors);
98 }
100 // Check for duplicate entries.
101 let dupes = {};
102 for (let parsedUri of parsed.uris) {
103 dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
104 dupes[parsedUri.uri].push({
105 line: parsedUri.line,
106 section: parsedUri.section,
107 original: parsedUri.original
108 });
109 }
110 for (let [uri, value] of Iterator(dupes)) {
111 if (value.length > 1) {
112 this._addError(0, "duplicateURI", uri, JSON.stringify(value));
113 }
114 }
116 // Loop through network entries making sure that fallback and cache don't
117 // contain uris starting with the network uri.
118 for (let neturi of parsed.uris) {
119 if (neturi.section == "NETWORK") {
120 for (let parsedUri of parsed.uris) {
121 if (parsedUri.uri.startsWith(neturi.uri)) {
122 this._addError(neturi.line, "networkBlocksURI", neturi.line,
123 neturi.original, parsedUri.line, parsedUri.original,
124 parsedUri.section);
125 }
126 }
127 }
128 }
130 // Loop through fallback entries making sure that fallback and cache don't
131 // contain uris starting with the network uri.
132 for (let fb of parsed.fallbacks) {
133 for (let parsedUri of parsed.uris) {
134 if (parsedUri.uri.startsWith(fb.namespace)) {
135 this._addError(fb.line, "fallbackBlocksURI", fb.line,
136 fb.original, parsedUri.line, parsedUri.original,
137 parsedUri.section);
138 }
139 }
140 }
142 // Check that all resources exist and that their cach-control headers are
143 // not set to no-store.
144 let current = -1;
145 for (let i = 0, len = parsed.uris.length; i < len; i++) {
146 let parsedUri = parsed.uris[i];
147 this._getURIInfo(parsedUri.uri).then(uriInfo => {
148 current++;
150 if (uriInfo.success) {
151 // Check that the resource was not modified after the manifest was last
152 // modified. If it was then the manifest file should be refreshed.
153 let resourceLastModified =
154 new Date(uriInfo.responseHeaders["Last-Modified"]);
156 if (manifestLastModified < resourceLastModified) {
157 this._addError(parsedUri.line, "fileChangedButNotManifest",
158 uriInfo.name, manifestName, parsedUri.line);
159 }
161 // If cache-control: no-store the file will not be added to the
162 // appCache.
163 if (uriInfo.nocache) {
164 this._addError(parsedUri.line, "cacheControlNoStore",
165 parsedUri.original, parsedUri.line);
166 }
167 } else {
168 this._addError(parsedUri.line, "notAvailable",
169 parsedUri.original, parsedUri.line);
170 }
172 if (current == len - 1) {
173 deferred.resolve();
174 }
175 });
176 }
178 return deferred.promise;
179 },
181 _getURIInfo: function ACU__getURIInfo(uri) {
182 let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
183 .createInstance(Ci.nsIScriptableInputStream);
184 let deferred = promise.defer();
185 let channelCharset = "";
186 let buffer = "";
187 let channel = Services.io.newChannel(uri, null, null);
189 // Avoid the cache:
190 channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
191 channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
193 channel.asyncOpen({
194 onStartRequest: function (request, context) {
195 // This empty method is needed in order for onDataAvailable to be
196 // called.
197 },
199 onDataAvailable: function (request, context, stream, offset, count) {
200 request.QueryInterface(Ci.nsIHttpChannel);
201 inputStream.init(stream);
202 buffer = buffer.concat(inputStream.read(count));
203 },
205 onStopRequest: function onStartRequest(request, context, statusCode) {
206 if (statusCode == 0) {
207 request.QueryInterface(Ci.nsIHttpChannel);
209 let result = {
210 name: request.name,
211 success: request.requestSucceeded,
212 status: request.responseStatus + " - " + request.responseStatusText,
213 charset: request.contentCharset || "utf-8",
214 mimeType: request.contentType,
215 contentLength: request.contentLength,
216 nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
217 prePath: request.URI.prePath + "/",
218 text: buffer
219 };
221 result.requestHeaders = {};
222 request.visitRequestHeaders(function(header, value) {
223 result.requestHeaders[header] = value;
224 });
226 result.responseHeaders = {};
227 request.visitResponseHeaders(function(header, value) {
228 result.responseHeaders[header] = value;
229 });
231 deferred.resolve(result);
232 } else {
233 deferred.resolve({
234 name: request.name,
235 success: false
236 });
237 }
238 }
239 }, null);
240 return deferred.promise;
241 },
243 listEntries: function ACU_show(searchTerm) {
244 if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
245 throw new Error(l10n.GetStringFromName("cacheDisabled"));
246 }
248 let entries = [];
250 Services.cache.visitEntries({
251 visitDevice: function(deviceID, deviceInfo) {
252 return true;
253 },
255 visitEntry: function(deviceID, entryInfo) {
256 if (entryInfo.deviceID == "offline") {
257 let entry = {};
258 let lowerKey = entryInfo.key.toLowerCase();
260 if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
261 return true;
262 }
264 for (let [key, value] of Iterator(entryInfo)) {
265 if (key == "QueryInterface") {
266 continue;
267 }
268 if (key == "clientID") {
269 entry.key = entryInfo.key;
270 }
271 if (key == "expirationTime" || key == "lastFetched" || key == "lastModified") {
272 value = new Date(value * 1000);
273 }
274 entry[key] = value;
275 }
276 entries.push(entry);
277 }
278 return true;
279 }
280 });
282 if (entries.length == 0) {
283 throw new Error(l10n.GetStringFromName("noResults"));
284 }
285 return entries;
286 },
288 viewEntry: function ACU_viewEntry(key) {
289 let uri;
291 Services.cache.visitEntries({
292 visitDevice: function(deviceID, deviceInfo) {
293 return true;
294 },
296 visitEntry: function(deviceID, entryInfo) {
297 if (entryInfo.deviceID == "offline" && entryInfo.key == key) {
298 uri = "about:cache-entry?client=" + entryInfo.clientID +
299 "&sb=1&key=" + entryInfo.key;
300 return false;
301 }
302 return true;
303 }
304 });
306 if (uri) {
307 let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
308 .getService(Ci.nsIWindowMediator);
309 let win = wm.getMostRecentWindow("navigator:browser");
310 win.gBrowser.selectedTab = win.gBrowser.addTab(uri);
311 } else {
312 return l10n.GetStringFromName("entryNotFound");
313 }
314 },
316 clearAll: function ACU_clearAll() {
317 Services.cache.evictEntries(Ci.nsICache.STORE_OFFLINE);
318 },
320 _getManifestURI: function ACU__getManifestURI() {
321 let deferred = promise.defer();
323 let getURI = node => {
324 let htmlNode = this.doc.querySelector("html[manifest]");
325 if (htmlNode) {
326 let pageUri = this.doc.location ? this.doc.location.href : this.uri;
327 let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1);
328 return origin + htmlNode.getAttribute("manifest");
329 }
330 };
332 if (this.doc) {
333 let uri = getURI(this.doc);
334 return promise.resolve(uri);
335 } else {
336 this._getURIInfo(this.uri).then(uriInfo => {
337 if (uriInfo.success) {
338 let html = uriInfo.text;
339 let parser = _DOMParser;
340 this.doc = parser.parseFromString(html, "text/html");
341 let uri = getURI(this.doc);
342 deferred.resolve(uri);
343 } else {
344 this.errors.push({
345 line: 0,
346 msg: l10n.GetStringFromName("invalidURI")
347 });
348 }
349 });
350 }
351 return deferred.promise;
352 },
354 _addError: function ACU__addError(line, l10nString, ...params) {
355 let msg;
357 if (params) {
358 msg = l10n.formatStringFromName(l10nString, params, params.length);
359 } else {
360 msg = l10n.GetStringFromName(l10nString);
361 }
363 this.errors.push({
364 line: line,
365 msg: msg
366 });
367 },
368 };
370 /**
371 * We use our own custom parser because we need far more detailed information
372 * than the system manifest parser provides.
373 *
374 * @param {String} manifestText
375 * The text content of the manifest file.
376 * @param {String} manifestURI
377 * The URI of the manifest file. This is used in calculating the path of
378 * relative URIs.
379 */
380 function ManifestParser(manifestText, manifestURI) {
381 this.manifestText = manifestText;
382 this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
383 .replace(" ", "%20");
384 }
386 ManifestParser.prototype = {
387 parse: function OCIMP_parse() {
388 let lines = this.manifestText.split(/\r?\n/);
389 let fallbacks = this.fallbacks = [];
390 let settings = this.settings = [];
391 let errors = this.errors = [];
392 let uris = this.uris = [];
394 this.currSection = "CACHE";
396 for (let i = 0; i < lines.length; i++) {
397 let text = this.text = lines[i].replace(/^\s+|\s+$/g);
398 this.currentLine = i + 1;
400 if (i == 0 && text != "CACHE MANIFEST") {
401 this._addError(1, "firstLineMustBeCacheManifest", 1);
402 }
404 // Ignore comments
405 if (/^#/.test(text) || !text.length) {
406 continue;
407 }
409 if (text == "CACHE MANIFEST") {
410 if (this.currentLine != 1) {
411 this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
412 this.currentLine);
413 }
414 continue;
415 }
417 if (this._maybeUpdateSectionName()) {
418 continue;
419 }
421 switch (this.currSection) {
422 case "CACHE":
423 case "NETWORK":
424 this.parseLine();
425 break;
426 case "FALLBACK":
427 this.parseFallbackLine();
428 break;
429 case "SETTINGS":
430 this.parseSettingsLine();
431 break;
432 }
433 }
435 return {
436 uris: uris,
437 fallbacks: fallbacks,
438 settings: settings,
439 errors: errors
440 };
441 },
443 parseLine: function OCIMP_parseLine() {
444 let text = this.text;
446 if (text.indexOf("*") != -1) {
447 if (this.currSection != "NETWORK" || text.length != 1) {
448 this._addError(this.currentLine, "asteriskInWrongSection2",
449 this.currSection, this.currentLine);
450 return;
451 }
452 }
454 if (/\s/.test(text)) {
455 this._addError(this.currentLine, "escapeSpaces", this.currentLine);
456 text = text.replace(/\s/g, "%20")
457 }
459 if (text[0] == "/") {
460 if (text.substr(0, 4) == "/../") {
461 this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
462 } else {
463 this.uris.push(this._wrapURI(this.origin + text.substring(1)));
464 }
465 } else if (text.substr(0, 2) == "./") {
466 this.uris.push(this._wrapURI(this.origin + text.substring(2)));
467 } else if (text.substr(0, 4) == "http") {
468 this.uris.push(this._wrapURI(text));
469 } else {
470 let origin = this.origin;
471 let path = text;
473 while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
474 let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
475 origin = origin.substr(0, trimIdx);
476 path = path.substr(3);
477 }
479 if (path.substr(0, 3) == "../") {
480 this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
481 return;
482 }
484 if (/^https?:\/\//.test(path)) {
485 this.uris.push(this._wrapURI(path));
486 return;
487 }
488 this.uris.push(this._wrapURI(origin + path));
489 }
490 },
492 parseFallbackLine: function OCIMP_parseFallbackLine() {
493 let split = this.text.split(/\s+/);
494 let origURI = this.text;
496 if (split.length != 2) {
497 this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
498 return;
499 }
501 let [ namespace, fallback ] = split;
503 if (namespace.indexOf("*") != -1) {
504 this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
505 }
507 if (/\s/.test(namespace)) {
508 this._addError(this.currentLine, "escapeSpaces", this.currentLine);
509 namespace = namespace.replace(/\s/g, "%20")
510 }
512 if (namespace.substr(0, 4) == "/../") {
513 this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
514 }
516 if (namespace.substr(0, 2) == "./") {
517 namespace = this.origin + namespace.substring(2);
518 }
520 if (namespace.substr(0, 4) != "http") {
521 let origin = this.origin;
522 let path = namespace;
524 while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
525 let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
526 origin = origin.substr(0, trimIdx);
527 path = path.substr(3);
528 }
530 if (path.substr(0, 3) == "../") {
531 this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
532 }
534 if (/^https?:\/\//.test(path)) {
535 namespace = path;
536 } else {
537 if (path[0] == "/") {
538 path = path.substring(1);
539 }
540 namespace = origin + path;
541 }
542 }
544 this.text = fallback;
545 this.parseLine();
547 this.fallbacks.push({
548 line: this.currentLine,
549 original: origURI,
550 namespace: namespace,
551 fallback: fallback
552 });
553 },
555 parseSettingsLine: function OCIMP_parseSettingsLine() {
556 let text = this.text;
558 if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
559 this._addError(this.currentLine, "settingsBadValue", this.currentLine);
560 return;
561 }
563 switch (text) {
564 case "prefer-online":
565 this.settings.push(this._wrapURI(text));
566 break;
567 case "fast":
568 this.settings.push(this._wrapURI(text));
569 break;
570 }
571 },
573 _wrapURI: function OCIMP__wrapURI(uri) {
574 return {
575 section: this.currSection,
576 line: this.currentLine,
577 uri: uri,
578 original: this.text
579 };
580 },
582 _addError: function OCIMP__addError(line, l10nString, ...params) {
583 let msg;
585 if (params) {
586 msg = l10n.formatStringFromName(l10nString, params, params.length);
587 } else {
588 msg = l10n.GetStringFromName(l10nString);
589 }
591 this.errors.push({
592 line: line,
593 msg: msg
594 });
595 },
597 _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
598 let text = this.text;
600 if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
601 text = text.substr(0, text.length - 1);
603 switch (text) {
604 case "CACHE":
605 case "NETWORK":
606 case "FALLBACK":
607 case "SETTINGS":
608 this.currSection = text;
609 return true;
610 default:
611 this._addError(this.currentLine,
612 "invalidSectionName", text, this.currentLine);
613 return false;
614 }
615 }
616 },
617 };
619 XPCOMUtils.defineLazyGetter(this, "l10n", function() Services.strings
620 .createBundle("chrome://browser/locale/devtools/appcacheutils.properties"));
622 XPCOMUtils.defineLazyGetter(this, "appcacheservice", function() {
623 return Cc["@mozilla.org/network/application-cache-service;1"]
624 .getService(Ci.nsIApplicationCacheService);
626 });
628 XPCOMUtils.defineLazyGetter(this, "_DOMParser", function() {
629 return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
630 });