browser/devtools/shared/AppCacheUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 });

mercurial