AppCacheUtils.jsm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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/. */
  4. /**
  5. * validateManifest() warns of the following errors:
  6. * - No manifest specified in page
  7. * - Manifest is not utf-8
  8. * - Manifest mimetype not text/cache-manifest
  9. * - Manifest does not begin with "CACHE MANIFEST"
  10. * - Page modified since appcache last changed
  11. * - Duplicate entries
  12. * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
  13. * but blocked by FALLBACK namespace
  14. * - Detect referenced files that are not available
  15. * - Detect referenced files that have cache-control set to no-store
  16. * - Wildcards used in a section other than NETWORK
  17. * - Spaces in URI not replaced with %20
  18. * - Completely invalid URIs
  19. * - Too many dot dot slash operators
  20. * - SETTINGS section is valid
  21. * - Invalid section name
  22. * - etc.
  23. */
  24. "use strict";
  25. const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
  26. var { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
  27. var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
  28. var { LoadContextInfo } = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
  29. var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  30. var { gDevTools } = require("devtools/client/framework/devtools");
  31. var Services = require("Services");
  32. var promise = require("promise");
  33. var defer = require("devtools/shared/defer");
  34. this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
  35. function AppCacheUtils(documentOrUri) {
  36. this._parseManifest = this._parseManifest.bind(this);
  37. if (documentOrUri) {
  38. if (typeof documentOrUri == "string") {
  39. this.uri = documentOrUri;
  40. }
  41. if (/HTMLDocument/.test(documentOrUri.toString())) {
  42. this.doc = documentOrUri;
  43. }
  44. }
  45. }
  46. AppCacheUtils.prototype = {
  47. get cachePath() {
  48. return "";
  49. },
  50. validateManifest: function ACU_validateManifest() {
  51. let deferred = defer();
  52. this.errors = [];
  53. // Check for missing manifest.
  54. this._getManifestURI().then(manifestURI => {
  55. this.manifestURI = manifestURI;
  56. if (!this.manifestURI) {
  57. this._addError(0, "noManifest");
  58. deferred.resolve(this.errors);
  59. }
  60. this._getURIInfo(this.manifestURI).then(uriInfo => {
  61. this._parseManifest(uriInfo).then(() => {
  62. // Sort errors by line number.
  63. this.errors.sort(function (a, b) {
  64. return a.line - b.line;
  65. });
  66. deferred.resolve(this.errors);
  67. });
  68. });
  69. });
  70. return deferred.promise;
  71. },
  72. _parseManifest: function ACU__parseManifest(uriInfo) {
  73. let deferred = defer();
  74. let manifestName = uriInfo.name;
  75. let manifestLastModified = new Date(uriInfo.responseHeaders["last-modified"]);
  76. if (uriInfo.charset.toLowerCase() != "utf-8") {
  77. this._addError(0, "notUTF8", uriInfo.charset);
  78. }
  79. if (uriInfo.mimeType != "text/cache-manifest") {
  80. this._addError(0, "badMimeType", uriInfo.mimeType);
  81. }
  82. let parser = new ManifestParser(uriInfo.text, this.manifestURI);
  83. let parsed = parser.parse();
  84. if (parsed.errors.length > 0) {
  85. this.errors.push.apply(this.errors, parsed.errors);
  86. }
  87. // Check for duplicate entries.
  88. let dupes = {};
  89. for (let parsedUri of parsed.uris) {
  90. dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
  91. dupes[parsedUri.uri].push({
  92. line: parsedUri.line,
  93. section: parsedUri.section,
  94. original: parsedUri.original
  95. });
  96. }
  97. for (let [uri, value] of Object.entries(dupes)) {
  98. if (value.length > 1) {
  99. this._addError(0, "duplicateURI", uri, JSON.stringify(value));
  100. }
  101. }
  102. // Loop through network entries making sure that fallback and cache don't
  103. // contain uris starting with the network uri.
  104. for (let neturi of parsed.uris) {
  105. if (neturi.section == "NETWORK") {
  106. for (let parsedUri of parsed.uris) {
  107. if (parsedUri.section !== "NETWORK" &&
  108. parsedUri.uri.startsWith(neturi.uri)) {
  109. this._addError(neturi.line, "networkBlocksURI", neturi.line,
  110. neturi.original, parsedUri.line, parsedUri.original,
  111. parsedUri.section);
  112. }
  113. }
  114. }
  115. }
  116. // Loop through fallback entries making sure that fallback and cache don't
  117. // contain uris starting with the network uri.
  118. for (let fb of parsed.fallbacks) {
  119. for (let parsedUri of parsed.uris) {
  120. if (parsedUri.uri.startsWith(fb.namespace)) {
  121. this._addError(fb.line, "fallbackBlocksURI", fb.line,
  122. fb.original, parsedUri.line, parsedUri.original,
  123. parsedUri.section);
  124. }
  125. }
  126. }
  127. // Check that all resources exist and that their cach-control headers are
  128. // not set to no-store.
  129. let current = -1;
  130. for (let i = 0, len = parsed.uris.length; i < len; i++) {
  131. let parsedUri = parsed.uris[i];
  132. this._getURIInfo(parsedUri.uri).then(uriInfo => {
  133. current++;
  134. if (uriInfo.success) {
  135. // Check that the resource was not modified after the manifest was last
  136. // modified. If it was then the manifest file should be refreshed.
  137. let resourceLastModified =
  138. new Date(uriInfo.responseHeaders["last-modified"]);
  139. if (manifestLastModified < resourceLastModified) {
  140. this._addError(parsedUri.line, "fileChangedButNotManifest",
  141. uriInfo.name, manifestName, parsedUri.line);
  142. }
  143. // If cache-control: no-store the file will not be added to the
  144. // appCache.
  145. if (uriInfo.nocache) {
  146. this._addError(parsedUri.line, "cacheControlNoStore",
  147. parsedUri.original, parsedUri.line);
  148. }
  149. } else if (parsedUri.original !== "*") {
  150. this._addError(parsedUri.line, "notAvailable",
  151. parsedUri.original, parsedUri.line);
  152. }
  153. if (current == len - 1) {
  154. deferred.resolve();
  155. }
  156. });
  157. }
  158. return deferred.promise;
  159. },
  160. _getURIInfo: function ACU__getURIInfo(uri) {
  161. let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
  162. .createInstance(Ci.nsIScriptableInputStream);
  163. let deferred = defer();
  164. let buffer = "";
  165. var channel = NetUtil.newChannel({
  166. uri: uri,
  167. loadUsingSystemPrincipal: true,
  168. securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
  169. });
  170. // Avoid the cache:
  171. channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
  172. channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  173. channel.asyncOpen2({
  174. onStartRequest: function (request, context) {
  175. // This empty method is needed in order for onDataAvailable to be
  176. // called.
  177. },
  178. onDataAvailable: function (request, context, stream, offset, count) {
  179. request.QueryInterface(Ci.nsIHttpChannel);
  180. inputStream.init(stream);
  181. buffer = buffer.concat(inputStream.read(count));
  182. },
  183. onStopRequest: function onStartRequest(request, context, statusCode) {
  184. if (statusCode === 0) {
  185. request.QueryInterface(Ci.nsIHttpChannel);
  186. let result = {
  187. name: request.name,
  188. success: request.requestSucceeded,
  189. status: request.responseStatus + " - " + request.responseStatusText,
  190. charset: request.contentCharset || "utf-8",
  191. mimeType: request.contentType,
  192. contentLength: request.contentLength,
  193. nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
  194. prePath: request.URI.prePath + "/",
  195. text: buffer
  196. };
  197. result.requestHeaders = {};
  198. request.visitRequestHeaders(function (header, value) {
  199. result.responseHeaders[header.toLowerCase()] = value;
  200. });
  201. result.responseHeaders = {};
  202. request.visitResponseHeaders(function (header, value) {
  203. result.responseHeaders[header.toLowerCase()] = value;
  204. });
  205. deferred.resolve(result);
  206. } else {
  207. deferred.resolve({
  208. name: request.name,
  209. success: false
  210. });
  211. }
  212. }
  213. });
  214. return deferred.promise;
  215. },
  216. listEntries: function ACU_show(searchTerm) {
  217. if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
  218. throw new Error(l10n.GetStringFromName("cacheDisabled"));
  219. }
  220. let entries = [];
  221. let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
  222. appCacheStorage.asyncVisitStorage({
  223. onCacheStorageInfo: function () {},
  224. onCacheEntryInfo: function (aURI, aIdEnhance, aDataSize, aFetchCount, aLastModifiedTime, aExpirationTime) {
  225. let lowerKey = aURI.asciiSpec.toLowerCase();
  226. if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
  227. return;
  228. }
  229. if (aIdEnhance) {
  230. aIdEnhance += ":";
  231. }
  232. let entry = {
  233. "deviceID": "offline",
  234. "key": aIdEnhance + aURI.asciiSpec,
  235. "fetchCount": aFetchCount,
  236. "lastFetched": null,
  237. "lastModified": new Date(aLastModifiedTime * 1000),
  238. "expirationTime": new Date(aExpirationTime * 1000),
  239. "dataSize": aDataSize
  240. };
  241. entries.push(entry);
  242. return true;
  243. }
  244. }, true);
  245. if (entries.length === 0) {
  246. throw new Error(l10n.GetStringFromName("noResults"));
  247. }
  248. return entries;
  249. },
  250. viewEntry: function ACU_viewEntry(key) {
  251. let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
  252. .getService(Ci.nsIWindowMediator);
  253. let win = wm.getMostRecentWindow(gDevTools.chromeWindowType);
  254. let url = "about:cache-entry?storage=appcache&context=&eid=&uri=" + key;
  255. win.openUILinkIn(url, "tab");
  256. },
  257. clearAll: function ACU_clearAll() {
  258. if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
  259. throw new Error(l10n.GetStringFromName("cacheDisabled"));
  260. }
  261. let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
  262. appCacheStorage.asyncEvictStorage({
  263. onCacheEntryDoomed: function (result) {}
  264. });
  265. },
  266. _getManifestURI: function ACU__getManifestURI() {
  267. let deferred = defer();
  268. let getURI = () => {
  269. let htmlNode = this.doc.querySelector("html[manifest]");
  270. if (htmlNode) {
  271. let pageUri = this.doc.location ? this.doc.location.href : this.uri;
  272. let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1);
  273. let manifestURI = htmlNode.getAttribute("manifest");
  274. if (manifestURI.startsWith("/")) {
  275. manifestURI = manifestURI.substr(1);
  276. }
  277. return origin + manifestURI;
  278. }
  279. };
  280. if (this.doc) {
  281. let uri = getURI();
  282. return promise.resolve(uri);
  283. } else {
  284. this._getURIInfo(this.uri).then(uriInfo => {
  285. if (uriInfo.success) {
  286. let html = uriInfo.text;
  287. let parser = _DOMParser;
  288. this.doc = parser.parseFromString(html, "text/html");
  289. let uri = getURI();
  290. deferred.resolve(uri);
  291. } else {
  292. this.errors.push({
  293. line: 0,
  294. msg: l10n.GetStringFromName("invalidURI")
  295. });
  296. }
  297. });
  298. }
  299. return deferred.promise;
  300. },
  301. _addError: function ACU__addError(line, l10nString, ...params) {
  302. let msg;
  303. if (params) {
  304. msg = l10n.formatStringFromName(l10nString, params, params.length);
  305. } else {
  306. msg = l10n.GetStringFromName(l10nString);
  307. }
  308. this.errors.push({
  309. line: line,
  310. msg: msg
  311. });
  312. },
  313. };
  314. /**
  315. * We use our own custom parser because we need far more detailed information
  316. * than the system manifest parser provides.
  317. *
  318. * @param {String} manifestText
  319. * The text content of the manifest file.
  320. * @param {String} manifestURI
  321. * The URI of the manifest file. This is used in calculating the path of
  322. * relative URIs.
  323. */
  324. function ManifestParser(manifestText, manifestURI) {
  325. this.manifestText = manifestText;
  326. this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
  327. .replace(" ", "%20");
  328. }
  329. ManifestParser.prototype = {
  330. parse: function OCIMP_parse() {
  331. let lines = this.manifestText.split(/\r?\n/);
  332. let fallbacks = this.fallbacks = [];
  333. let settings = this.settings = [];
  334. let errors = this.errors = [];
  335. let uris = this.uris = [];
  336. this.currSection = "CACHE";
  337. for (let i = 0; i < lines.length; i++) {
  338. let text = this.text = lines[i].trim();
  339. this.currentLine = i + 1;
  340. if (i === 0 && text !== "CACHE MANIFEST") {
  341. this._addError(1, "firstLineMustBeCacheManifest", 1);
  342. }
  343. // Ignore comments
  344. if (/^#/.test(text) || !text.length) {
  345. continue;
  346. }
  347. if (text == "CACHE MANIFEST") {
  348. if (this.currentLine != 1) {
  349. this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
  350. this.currentLine);
  351. }
  352. continue;
  353. }
  354. if (this._maybeUpdateSectionName()) {
  355. continue;
  356. }
  357. switch (this.currSection) {
  358. case "CACHE":
  359. case "NETWORK":
  360. this.parseLine();
  361. break;
  362. case "FALLBACK":
  363. this.parseFallbackLine();
  364. break;
  365. case "SETTINGS":
  366. this.parseSettingsLine();
  367. break;
  368. }
  369. }
  370. return {
  371. uris: uris,
  372. fallbacks: fallbacks,
  373. settings: settings,
  374. errors: errors
  375. };
  376. },
  377. parseLine: function OCIMP_parseLine() {
  378. let text = this.text;
  379. if (text.indexOf("*") != -1) {
  380. if (this.currSection != "NETWORK" || text.length != 1) {
  381. this._addError(this.currentLine, "asteriskInWrongSection2",
  382. this.currSection, this.currentLine);
  383. return;
  384. }
  385. }
  386. if (/\s/.test(text)) {
  387. this._addError(this.currentLine, "escapeSpaces", this.currentLine);
  388. text = text.replace(/\s/g, "%20");
  389. }
  390. if (text[0] == "/") {
  391. if (text.substr(0, 4) == "/../") {
  392. this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
  393. } else {
  394. this.uris.push(this._wrapURI(this.origin + text.substring(1)));
  395. }
  396. } else if (text.substr(0, 2) == "./") {
  397. this.uris.push(this._wrapURI(this.origin + text.substring(2)));
  398. } else if (text.substr(0, 4) == "http") {
  399. this.uris.push(this._wrapURI(text));
  400. } else {
  401. let origin = this.origin;
  402. let path = text;
  403. while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
  404. let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
  405. origin = origin.substr(0, trimIdx);
  406. path = path.substr(3);
  407. }
  408. if (path.substr(0, 3) == "../") {
  409. this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
  410. return;
  411. }
  412. if (/^https?:\/\//.test(path)) {
  413. this.uris.push(this._wrapURI(path));
  414. return;
  415. }
  416. this.uris.push(this._wrapURI(origin + path));
  417. }
  418. },
  419. parseFallbackLine: function OCIMP_parseFallbackLine() {
  420. let split = this.text.split(/\s+/);
  421. let origURI = this.text;
  422. if (split.length != 2) {
  423. this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
  424. return;
  425. }
  426. let [ namespace, fallback ] = split;
  427. if (namespace.indexOf("*") != -1) {
  428. this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
  429. }
  430. if (/\s/.test(namespace)) {
  431. this._addError(this.currentLine, "escapeSpaces", this.currentLine);
  432. namespace = namespace.replace(/\s/g, "%20");
  433. }
  434. if (namespace.substr(0, 4) == "/../") {
  435. this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
  436. }
  437. if (namespace.substr(0, 2) == "./") {
  438. namespace = this.origin + namespace.substring(2);
  439. }
  440. if (namespace.substr(0, 4) != "http") {
  441. let origin = this.origin;
  442. let path = namespace;
  443. while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
  444. let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
  445. origin = origin.substr(0, trimIdx);
  446. path = path.substr(3);
  447. }
  448. if (path.substr(0, 3) == "../") {
  449. this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
  450. }
  451. if (/^https?:\/\//.test(path)) {
  452. namespace = path;
  453. } else {
  454. if (path[0] == "/") {
  455. path = path.substring(1);
  456. }
  457. namespace = origin + path;
  458. }
  459. }
  460. this.text = fallback;
  461. this.parseLine();
  462. this.fallbacks.push({
  463. line: this.currentLine,
  464. original: origURI,
  465. namespace: namespace,
  466. fallback: fallback
  467. });
  468. },
  469. parseSettingsLine: function OCIMP_parseSettingsLine() {
  470. let text = this.text;
  471. if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
  472. this._addError(this.currentLine, "settingsBadValue", this.currentLine);
  473. return;
  474. }
  475. switch (text) {
  476. case "prefer-online":
  477. this.settings.push(this._wrapURI(text));
  478. break;
  479. case "fast":
  480. this.settings.push(this._wrapURI(text));
  481. break;
  482. }
  483. },
  484. _wrapURI: function OCIMP__wrapURI(uri) {
  485. return {
  486. section: this.currSection,
  487. line: this.currentLine,
  488. uri: uri,
  489. original: this.text
  490. };
  491. },
  492. _addError: function OCIMP__addError(line, l10nString, ...params) {
  493. let msg;
  494. if (params) {
  495. msg = l10n.formatStringFromName(l10nString, params, params.length);
  496. } else {
  497. msg = l10n.GetStringFromName(l10nString);
  498. }
  499. this.errors.push({
  500. line: line,
  501. msg: msg
  502. });
  503. },
  504. _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
  505. let text = this.text;
  506. if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
  507. text = text.substr(0, text.length - 1);
  508. switch (text) {
  509. case "CACHE":
  510. case "NETWORK":
  511. case "FALLBACK":
  512. case "SETTINGS":
  513. this.currSection = text;
  514. return true;
  515. default:
  516. this._addError(this.currentLine,
  517. "invalidSectionName", text, this.currentLine);
  518. return false;
  519. }
  520. }
  521. },
  522. };
  523. XPCOMUtils.defineLazyGetter(this, "l10n", () => Services.strings
  524. .createBundle("chrome://devtools/locale/appcacheutils.properties"));
  525. XPCOMUtils.defineLazyGetter(this, "appcacheservice", function () {
  526. return Cc["@mozilla.org/network/application-cache-service;1"]
  527. .getService(Ci.nsIApplicationCacheService);
  528. });
  529. XPCOMUtils.defineLazyGetter(this, "_DOMParser", function () {
  530. return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
  531. });