1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/AppCacheUtils.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,630 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * validateManifest() warns of the following errors: 1.10 + * - No manifest specified in page 1.11 + * - Manifest is not utf-8 1.12 + * - Manifest mimetype not text/cache-manifest 1.13 + * - Manifest does not begin with "CACHE MANIFEST" 1.14 + * - Page modified since appcache last changed 1.15 + * - Duplicate entries 1.16 + * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache 1.17 + * but blocked by FALLBACK namespace 1.18 + * - Detect referenced files that are not available 1.19 + * - Detect referenced files that have cache-control set to no-store 1.20 + * - Wildcards used in a section other than NETWORK 1.21 + * - Spaces in URI not replaced with %20 1.22 + * - Completely invalid URIs 1.23 + * - Too many dot dot slash operators 1.24 + * - SETTINGS section is valid 1.25 + * - Invalid section name 1.26 + * - etc. 1.27 + */ 1.28 + 1.29 +"use strict"; 1.30 + 1.31 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.32 + 1.33 +let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); 1.34 +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); 1.35 +let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.36 + 1.37 +this.EXPORTED_SYMBOLS = ["AppCacheUtils"]; 1.38 + 1.39 +function AppCacheUtils(documentOrUri) { 1.40 + this._parseManifest = this._parseManifest.bind(this); 1.41 + 1.42 + if (documentOrUri) { 1.43 + if (typeof documentOrUri == "string") { 1.44 + this.uri = documentOrUri; 1.45 + } 1.46 + if (/HTMLDocument/.test(documentOrUri.toString())) { 1.47 + this.doc = documentOrUri; 1.48 + } 1.49 + } 1.50 +} 1.51 + 1.52 +AppCacheUtils.prototype = { 1.53 + get cachePath() { 1.54 + return ""; 1.55 + }, 1.56 + 1.57 + validateManifest: function ACU_validateManifest() { 1.58 + let deferred = promise.defer(); 1.59 + this.errors = []; 1.60 + // Check for missing manifest. 1.61 + this._getManifestURI().then(manifestURI => { 1.62 + this.manifestURI = manifestURI; 1.63 + 1.64 + if (!this.manifestURI) { 1.65 + this._addError(0, "noManifest"); 1.66 + deferred.resolve(this.errors); 1.67 + } 1.68 + 1.69 + this._getURIInfo(this.manifestURI).then(uriInfo => { 1.70 + this._parseManifest(uriInfo).then(() => { 1.71 + // Sort errors by line number. 1.72 + this.errors.sort(function(a, b) { 1.73 + return a.line - b.line; 1.74 + }); 1.75 + deferred.resolve(this.errors); 1.76 + }); 1.77 + }); 1.78 + }); 1.79 + 1.80 + return deferred.promise; 1.81 + }, 1.82 + 1.83 + _parseManifest: function ACU__parseManifest(uriInfo) { 1.84 + let deferred = promise.defer(); 1.85 + let manifestName = uriInfo.name; 1.86 + let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]); 1.87 + 1.88 + if (uriInfo.charset.toLowerCase() != "utf-8") { 1.89 + this._addError(0, "notUTF8", uriInfo.charset); 1.90 + } 1.91 + 1.92 + if (uriInfo.mimeType != "text/cache-manifest") { 1.93 + this._addError(0, "badMimeType", uriInfo.mimeType); 1.94 + } 1.95 + 1.96 + let parser = new ManifestParser(uriInfo.text, this.manifestURI); 1.97 + let parsed = parser.parse(); 1.98 + 1.99 + if (parsed.errors.length > 0) { 1.100 + this.errors.push.apply(this.errors, parsed.errors); 1.101 + } 1.102 + 1.103 + // Check for duplicate entries. 1.104 + let dupes = {}; 1.105 + for (let parsedUri of parsed.uris) { 1.106 + dupes[parsedUri.uri] = dupes[parsedUri.uri] || []; 1.107 + dupes[parsedUri.uri].push({ 1.108 + line: parsedUri.line, 1.109 + section: parsedUri.section, 1.110 + original: parsedUri.original 1.111 + }); 1.112 + } 1.113 + for (let [uri, value] of Iterator(dupes)) { 1.114 + if (value.length > 1) { 1.115 + this._addError(0, "duplicateURI", uri, JSON.stringify(value)); 1.116 + } 1.117 + } 1.118 + 1.119 + // Loop through network entries making sure that fallback and cache don't 1.120 + // contain uris starting with the network uri. 1.121 + for (let neturi of parsed.uris) { 1.122 + if (neturi.section == "NETWORK") { 1.123 + for (let parsedUri of parsed.uris) { 1.124 + if (parsedUri.uri.startsWith(neturi.uri)) { 1.125 + this._addError(neturi.line, "networkBlocksURI", neturi.line, 1.126 + neturi.original, parsedUri.line, parsedUri.original, 1.127 + parsedUri.section); 1.128 + } 1.129 + } 1.130 + } 1.131 + } 1.132 + 1.133 + // Loop through fallback entries making sure that fallback and cache don't 1.134 + // contain uris starting with the network uri. 1.135 + for (let fb of parsed.fallbacks) { 1.136 + for (let parsedUri of parsed.uris) { 1.137 + if (parsedUri.uri.startsWith(fb.namespace)) { 1.138 + this._addError(fb.line, "fallbackBlocksURI", fb.line, 1.139 + fb.original, parsedUri.line, parsedUri.original, 1.140 + parsedUri.section); 1.141 + } 1.142 + } 1.143 + } 1.144 + 1.145 + // Check that all resources exist and that their cach-control headers are 1.146 + // not set to no-store. 1.147 + let current = -1; 1.148 + for (let i = 0, len = parsed.uris.length; i < len; i++) { 1.149 + let parsedUri = parsed.uris[i]; 1.150 + this._getURIInfo(parsedUri.uri).then(uriInfo => { 1.151 + current++; 1.152 + 1.153 + if (uriInfo.success) { 1.154 + // Check that the resource was not modified after the manifest was last 1.155 + // modified. If it was then the manifest file should be refreshed. 1.156 + let resourceLastModified = 1.157 + new Date(uriInfo.responseHeaders["Last-Modified"]); 1.158 + 1.159 + if (manifestLastModified < resourceLastModified) { 1.160 + this._addError(parsedUri.line, "fileChangedButNotManifest", 1.161 + uriInfo.name, manifestName, parsedUri.line); 1.162 + } 1.163 + 1.164 + // If cache-control: no-store the file will not be added to the 1.165 + // appCache. 1.166 + if (uriInfo.nocache) { 1.167 + this._addError(parsedUri.line, "cacheControlNoStore", 1.168 + parsedUri.original, parsedUri.line); 1.169 + } 1.170 + } else { 1.171 + this._addError(parsedUri.line, "notAvailable", 1.172 + parsedUri.original, parsedUri.line); 1.173 + } 1.174 + 1.175 + if (current == len - 1) { 1.176 + deferred.resolve(); 1.177 + } 1.178 + }); 1.179 + } 1.180 + 1.181 + return deferred.promise; 1.182 + }, 1.183 + 1.184 + _getURIInfo: function ACU__getURIInfo(uri) { 1.185 + let inputStream = Cc["@mozilla.org/scriptableinputstream;1"] 1.186 + .createInstance(Ci.nsIScriptableInputStream); 1.187 + let deferred = promise.defer(); 1.188 + let channelCharset = ""; 1.189 + let buffer = ""; 1.190 + let channel = Services.io.newChannel(uri, null, null); 1.191 + 1.192 + // Avoid the cache: 1.193 + channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; 1.194 + channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; 1.195 + 1.196 + channel.asyncOpen({ 1.197 + onStartRequest: function (request, context) { 1.198 + // This empty method is needed in order for onDataAvailable to be 1.199 + // called. 1.200 + }, 1.201 + 1.202 + onDataAvailable: function (request, context, stream, offset, count) { 1.203 + request.QueryInterface(Ci.nsIHttpChannel); 1.204 + inputStream.init(stream); 1.205 + buffer = buffer.concat(inputStream.read(count)); 1.206 + }, 1.207 + 1.208 + onStopRequest: function onStartRequest(request, context, statusCode) { 1.209 + if (statusCode == 0) { 1.210 + request.QueryInterface(Ci.nsIHttpChannel); 1.211 + 1.212 + let result = { 1.213 + name: request.name, 1.214 + success: request.requestSucceeded, 1.215 + status: request.responseStatus + " - " + request.responseStatusText, 1.216 + charset: request.contentCharset || "utf-8", 1.217 + mimeType: request.contentType, 1.218 + contentLength: request.contentLength, 1.219 + nocache: request.isNoCacheResponse() || request.isNoStoreResponse(), 1.220 + prePath: request.URI.prePath + "/", 1.221 + text: buffer 1.222 + }; 1.223 + 1.224 + result.requestHeaders = {}; 1.225 + request.visitRequestHeaders(function(header, value) { 1.226 + result.requestHeaders[header] = value; 1.227 + }); 1.228 + 1.229 + result.responseHeaders = {}; 1.230 + request.visitResponseHeaders(function(header, value) { 1.231 + result.responseHeaders[header] = value; 1.232 + }); 1.233 + 1.234 + deferred.resolve(result); 1.235 + } else { 1.236 + deferred.resolve({ 1.237 + name: request.name, 1.238 + success: false 1.239 + }); 1.240 + } 1.241 + } 1.242 + }, null); 1.243 + return deferred.promise; 1.244 + }, 1.245 + 1.246 + listEntries: function ACU_show(searchTerm) { 1.247 + if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) { 1.248 + throw new Error(l10n.GetStringFromName("cacheDisabled")); 1.249 + } 1.250 + 1.251 + let entries = []; 1.252 + 1.253 + Services.cache.visitEntries({ 1.254 + visitDevice: function(deviceID, deviceInfo) { 1.255 + return true; 1.256 + }, 1.257 + 1.258 + visitEntry: function(deviceID, entryInfo) { 1.259 + if (entryInfo.deviceID == "offline") { 1.260 + let entry = {}; 1.261 + let lowerKey = entryInfo.key.toLowerCase(); 1.262 + 1.263 + if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) { 1.264 + return true; 1.265 + } 1.266 + 1.267 + for (let [key, value] of Iterator(entryInfo)) { 1.268 + if (key == "QueryInterface") { 1.269 + continue; 1.270 + } 1.271 + if (key == "clientID") { 1.272 + entry.key = entryInfo.key; 1.273 + } 1.274 + if (key == "expirationTime" || key == "lastFetched" || key == "lastModified") { 1.275 + value = new Date(value * 1000); 1.276 + } 1.277 + entry[key] = value; 1.278 + } 1.279 + entries.push(entry); 1.280 + } 1.281 + return true; 1.282 + } 1.283 + }); 1.284 + 1.285 + if (entries.length == 0) { 1.286 + throw new Error(l10n.GetStringFromName("noResults")); 1.287 + } 1.288 + return entries; 1.289 + }, 1.290 + 1.291 + viewEntry: function ACU_viewEntry(key) { 1.292 + let uri; 1.293 + 1.294 + Services.cache.visitEntries({ 1.295 + visitDevice: function(deviceID, deviceInfo) { 1.296 + return true; 1.297 + }, 1.298 + 1.299 + visitEntry: function(deviceID, entryInfo) { 1.300 + if (entryInfo.deviceID == "offline" && entryInfo.key == key) { 1.301 + uri = "about:cache-entry?client=" + entryInfo.clientID + 1.302 + "&sb=1&key=" + entryInfo.key; 1.303 + return false; 1.304 + } 1.305 + return true; 1.306 + } 1.307 + }); 1.308 + 1.309 + if (uri) { 1.310 + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] 1.311 + .getService(Ci.nsIWindowMediator); 1.312 + let win = wm.getMostRecentWindow("navigator:browser"); 1.313 + win.gBrowser.selectedTab = win.gBrowser.addTab(uri); 1.314 + } else { 1.315 + return l10n.GetStringFromName("entryNotFound"); 1.316 + } 1.317 + }, 1.318 + 1.319 + clearAll: function ACU_clearAll() { 1.320 + Services.cache.evictEntries(Ci.nsICache.STORE_OFFLINE); 1.321 + }, 1.322 + 1.323 + _getManifestURI: function ACU__getManifestURI() { 1.324 + let deferred = promise.defer(); 1.325 + 1.326 + let getURI = node => { 1.327 + let htmlNode = this.doc.querySelector("html[manifest]"); 1.328 + if (htmlNode) { 1.329 + let pageUri = this.doc.location ? this.doc.location.href : this.uri; 1.330 + let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1); 1.331 + return origin + htmlNode.getAttribute("manifest"); 1.332 + } 1.333 + }; 1.334 + 1.335 + if (this.doc) { 1.336 + let uri = getURI(this.doc); 1.337 + return promise.resolve(uri); 1.338 + } else { 1.339 + this._getURIInfo(this.uri).then(uriInfo => { 1.340 + if (uriInfo.success) { 1.341 + let html = uriInfo.text; 1.342 + let parser = _DOMParser; 1.343 + this.doc = parser.parseFromString(html, "text/html"); 1.344 + let uri = getURI(this.doc); 1.345 + deferred.resolve(uri); 1.346 + } else { 1.347 + this.errors.push({ 1.348 + line: 0, 1.349 + msg: l10n.GetStringFromName("invalidURI") 1.350 + }); 1.351 + } 1.352 + }); 1.353 + } 1.354 + return deferred.promise; 1.355 + }, 1.356 + 1.357 + _addError: function ACU__addError(line, l10nString, ...params) { 1.358 + let msg; 1.359 + 1.360 + if (params) { 1.361 + msg = l10n.formatStringFromName(l10nString, params, params.length); 1.362 + } else { 1.363 + msg = l10n.GetStringFromName(l10nString); 1.364 + } 1.365 + 1.366 + this.errors.push({ 1.367 + line: line, 1.368 + msg: msg 1.369 + }); 1.370 + }, 1.371 +}; 1.372 + 1.373 +/** 1.374 + * We use our own custom parser because we need far more detailed information 1.375 + * than the system manifest parser provides. 1.376 + * 1.377 + * @param {String} manifestText 1.378 + * The text content of the manifest file. 1.379 + * @param {String} manifestURI 1.380 + * The URI of the manifest file. This is used in calculating the path of 1.381 + * relative URIs. 1.382 + */ 1.383 +function ManifestParser(manifestText, manifestURI) { 1.384 + this.manifestText = manifestText; 1.385 + this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1) 1.386 + .replace(" ", "%20"); 1.387 +} 1.388 + 1.389 +ManifestParser.prototype = { 1.390 + parse: function OCIMP_parse() { 1.391 + let lines = this.manifestText.split(/\r?\n/); 1.392 + let fallbacks = this.fallbacks = []; 1.393 + let settings = this.settings = []; 1.394 + let errors = this.errors = []; 1.395 + let uris = this.uris = []; 1.396 + 1.397 + this.currSection = "CACHE"; 1.398 + 1.399 + for (let i = 0; i < lines.length; i++) { 1.400 + let text = this.text = lines[i].replace(/^\s+|\s+$/g); 1.401 + this.currentLine = i + 1; 1.402 + 1.403 + if (i == 0 && text != "CACHE MANIFEST") { 1.404 + this._addError(1, "firstLineMustBeCacheManifest", 1); 1.405 + } 1.406 + 1.407 + // Ignore comments 1.408 + if (/^#/.test(text) || !text.length) { 1.409 + continue; 1.410 + } 1.411 + 1.412 + if (text == "CACHE MANIFEST") { 1.413 + if (this.currentLine != 1) { 1.414 + this._addError(this.currentLine, "cacheManifestOnlyFirstLine2", 1.415 + this.currentLine); 1.416 + } 1.417 + continue; 1.418 + } 1.419 + 1.420 + if (this._maybeUpdateSectionName()) { 1.421 + continue; 1.422 + } 1.423 + 1.424 + switch (this.currSection) { 1.425 + case "CACHE": 1.426 + case "NETWORK": 1.427 + this.parseLine(); 1.428 + break; 1.429 + case "FALLBACK": 1.430 + this.parseFallbackLine(); 1.431 + break; 1.432 + case "SETTINGS": 1.433 + this.parseSettingsLine(); 1.434 + break; 1.435 + } 1.436 + } 1.437 + 1.438 + return { 1.439 + uris: uris, 1.440 + fallbacks: fallbacks, 1.441 + settings: settings, 1.442 + errors: errors 1.443 + }; 1.444 + }, 1.445 + 1.446 + parseLine: function OCIMP_parseLine() { 1.447 + let text = this.text; 1.448 + 1.449 + if (text.indexOf("*") != -1) { 1.450 + if (this.currSection != "NETWORK" || text.length != 1) { 1.451 + this._addError(this.currentLine, "asteriskInWrongSection2", 1.452 + this.currSection, this.currentLine); 1.453 + return; 1.454 + } 1.455 + } 1.456 + 1.457 + if (/\s/.test(text)) { 1.458 + this._addError(this.currentLine, "escapeSpaces", this.currentLine); 1.459 + text = text.replace(/\s/g, "%20") 1.460 + } 1.461 + 1.462 + if (text[0] == "/") { 1.463 + if (text.substr(0, 4) == "/../") { 1.464 + this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine); 1.465 + } else { 1.466 + this.uris.push(this._wrapURI(this.origin + text.substring(1))); 1.467 + } 1.468 + } else if (text.substr(0, 2) == "./") { 1.469 + this.uris.push(this._wrapURI(this.origin + text.substring(2))); 1.470 + } else if (text.substr(0, 4) == "http") { 1.471 + this.uris.push(this._wrapURI(text)); 1.472 + } else { 1.473 + let origin = this.origin; 1.474 + let path = text; 1.475 + 1.476 + while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) { 1.477 + let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1; 1.478 + origin = origin.substr(0, trimIdx); 1.479 + path = path.substr(3); 1.480 + } 1.481 + 1.482 + if (path.substr(0, 3) == "../") { 1.483 + this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine); 1.484 + return; 1.485 + } 1.486 + 1.487 + if (/^https?:\/\//.test(path)) { 1.488 + this.uris.push(this._wrapURI(path)); 1.489 + return; 1.490 + } 1.491 + this.uris.push(this._wrapURI(origin + path)); 1.492 + } 1.493 + }, 1.494 + 1.495 + parseFallbackLine: function OCIMP_parseFallbackLine() { 1.496 + let split = this.text.split(/\s+/); 1.497 + let origURI = this.text; 1.498 + 1.499 + if (split.length != 2) { 1.500 + this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine); 1.501 + return; 1.502 + } 1.503 + 1.504 + let [ namespace, fallback ] = split; 1.505 + 1.506 + if (namespace.indexOf("*") != -1) { 1.507 + this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine); 1.508 + } 1.509 + 1.510 + if (/\s/.test(namespace)) { 1.511 + this._addError(this.currentLine, "escapeSpaces", this.currentLine); 1.512 + namespace = namespace.replace(/\s/g, "%20") 1.513 + } 1.514 + 1.515 + if (namespace.substr(0, 4) == "/../") { 1.516 + this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine); 1.517 + } 1.518 + 1.519 + if (namespace.substr(0, 2) == "./") { 1.520 + namespace = this.origin + namespace.substring(2); 1.521 + } 1.522 + 1.523 + if (namespace.substr(0, 4) != "http") { 1.524 + let origin = this.origin; 1.525 + let path = namespace; 1.526 + 1.527 + while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) { 1.528 + let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1; 1.529 + origin = origin.substr(0, trimIdx); 1.530 + path = path.substr(3); 1.531 + } 1.532 + 1.533 + if (path.substr(0, 3) == "../") { 1.534 + this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine); 1.535 + } 1.536 + 1.537 + if (/^https?:\/\//.test(path)) { 1.538 + namespace = path; 1.539 + } else { 1.540 + if (path[0] == "/") { 1.541 + path = path.substring(1); 1.542 + } 1.543 + namespace = origin + path; 1.544 + } 1.545 + } 1.546 + 1.547 + this.text = fallback; 1.548 + this.parseLine(); 1.549 + 1.550 + this.fallbacks.push({ 1.551 + line: this.currentLine, 1.552 + original: origURI, 1.553 + namespace: namespace, 1.554 + fallback: fallback 1.555 + }); 1.556 + }, 1.557 + 1.558 + parseSettingsLine: function OCIMP_parseSettingsLine() { 1.559 + let text = this.text; 1.560 + 1.561 + if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) { 1.562 + this._addError(this.currentLine, "settingsBadValue", this.currentLine); 1.563 + return; 1.564 + } 1.565 + 1.566 + switch (text) { 1.567 + case "prefer-online": 1.568 + this.settings.push(this._wrapURI(text)); 1.569 + break; 1.570 + case "fast": 1.571 + this.settings.push(this._wrapURI(text)); 1.572 + break; 1.573 + } 1.574 + }, 1.575 + 1.576 + _wrapURI: function OCIMP__wrapURI(uri) { 1.577 + return { 1.578 + section: this.currSection, 1.579 + line: this.currentLine, 1.580 + uri: uri, 1.581 + original: this.text 1.582 + }; 1.583 + }, 1.584 + 1.585 + _addError: function OCIMP__addError(line, l10nString, ...params) { 1.586 + let msg; 1.587 + 1.588 + if (params) { 1.589 + msg = l10n.formatStringFromName(l10nString, params, params.length); 1.590 + } else { 1.591 + msg = l10n.GetStringFromName(l10nString); 1.592 + } 1.593 + 1.594 + this.errors.push({ 1.595 + line: line, 1.596 + msg: msg 1.597 + }); 1.598 + }, 1.599 + 1.600 + _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() { 1.601 + let text = this.text; 1.602 + 1.603 + if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") { 1.604 + text = text.substr(0, text.length - 1); 1.605 + 1.606 + switch (text) { 1.607 + case "CACHE": 1.608 + case "NETWORK": 1.609 + case "FALLBACK": 1.610 + case "SETTINGS": 1.611 + this.currSection = text; 1.612 + return true; 1.613 + default: 1.614 + this._addError(this.currentLine, 1.615 + "invalidSectionName", text, this.currentLine); 1.616 + return false; 1.617 + } 1.618 + } 1.619 + }, 1.620 +}; 1.621 + 1.622 +XPCOMUtils.defineLazyGetter(this, "l10n", function() Services.strings 1.623 + .createBundle("chrome://browser/locale/devtools/appcacheutils.properties")); 1.624 + 1.625 +XPCOMUtils.defineLazyGetter(this, "appcacheservice", function() { 1.626 + return Cc["@mozilla.org/network/application-cache-service;1"] 1.627 + .getService(Ci.nsIApplicationCacheService); 1.628 + 1.629 +}); 1.630 + 1.631 +XPCOMUtils.defineLazyGetter(this, "_DOMParser", function() { 1.632 + return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); 1.633 +});