Bug 791039 part 2: Fill webapp appcache from its local `cache` folder. [r=vingtetun]
authorAlexandre Poirot <poirot.alex@gmail.com>
Tue, 23 Oct 2012 13:52:57 -0700
changeset 111322 5cce74c60214653c2dbe2d0080f595bb981b3b62
parent 111321 04176aaf8ee46399e75e6532509420ffbaf25e3b
child 111323 93cc1ee9429165ad859ac031ade8fde49eceeeaa
child 111329 986b4cb7678efd8d7a0b1528649aa340a0dcbf60
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersvingtetun
bugs791039
milestone19.0a1
Bug 791039 part 2: Fill webapp appcache from its local `cache` folder. [r=vingtetun]
dom/apps/src/Makefile.in
dom/apps/src/OfflineCacheInstaller.jsm
dom/apps/src/Webapps.jsm
--- a/dom/apps/src/Makefile.in
+++ b/dom/apps/src/Makefile.in
@@ -21,12 +21,13 @@ EXTRA_PP_COMPONENTS = \
 
 EXTRA_PP_JS_MODULES += \
   Webapps.jsm \
   $(NULL)
 
 EXTRA_JS_MODULES += \
   AppsServiceChild.jsm \
   AppsUtils.jsm \
+  OfflineCacheInstaller.jsm \
   PermissionsInstaller.jsm \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/apps/src/OfflineCacheInstaller.jsm
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const CC = Components.Constructor;
+
+let EXPORTED_SYMBOLS = ["OfflineCacheInstaller"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+let Namespace = CC('@mozilla.org/network/application-cache-namespace;1',
+                   'nsIApplicationCacheNamespace',
+                   'init');
+let makeFile = CC('@mozilla.org/file/local;1',
+                'nsIFile',
+                'initWithPath');
+const nsICache = Ci.nsICache;
+const nsIApplicationCache = Ci.nsIApplicationCache;
+const applicationCacheService =
+  Cc['@mozilla.org/network/application-cache-service;1']
+    .getService(Ci.nsIApplicationCacheService);
+
+
+function debug(aMsg) {
+  //dump("-*-*- OfflineCacheInstaller.jsm : " + aMsg + "\n");
+}
+
+
+function enableOfflineCacheForApp(origin, appId) {
+  let originURI = Services.io.newURI(origin, null, null);
+  let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
+                    originURI, appId, false);
+  Services.perms.addFromPrincipal(principal, 'offline-app',
+                                  Ci.nsIPermissionManager.ALLOW_ACTION);
+  // Prevent cache from being evicted:
+  Services.perms.addFromPrincipal(principal, 'pin-app',
+                                  Ci.nsIPermissionManager.ALLOW_ACTION);
+}
+
+
+function storeCache(applicationCache, url, file, itemType) {
+  let session = Services.cache.createSession(applicationCache.clientID,
+                                             nsICache.STORE_OFFLINE, true);
+  session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, {
+    onCacheEntryAvailable: function (cacheEntry, accessGranted, status) {
+      cacheEntry.setMetaDataElement('request-method', 'GET');
+      cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n');
+
+      let outputStream = cacheEntry.openOutputStream(0);
+
+      // Input-Output stream machinery in order to push nsIFile content into cache
+      let inputStream = Cc['@mozilla.org/network/file-input-stream;1']
+                          .createInstance(Ci.nsIFileInputStream);
+      inputStream.init(file, 1, -1, null);
+      let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1']
+                                   .createInstance(Ci.nsIBufferedOutputStream);
+      bufferedOutputStream.init(outputStream, 1024);
+      bufferedOutputStream.writeFrom(inputStream, inputStream.available());
+      bufferedOutputStream.flush();
+      bufferedOutputStream.close();
+      outputStream.close();
+      inputStream.close();
+
+      cacheEntry.markValid();
+      debug (file.path + ' -> ' + url + ' (' + itemType + ')');
+      applicationCache.markEntry(url, itemType);
+      cacheEntry.close();
+    }
+  });
+}
+
+function readFile(aFile, aCallback) {
+  let channel = NetUtil.newChannel(aFile);
+  channel.contentType = "pain/text";
+  NetUtil.asyncFetch(channel, function(aStream, aResult) {
+    if (!Components.isSuccessCode(aResult)) {
+      Cu.reportError("OfflineCacheInstaller: Could not read file " + aFile.path);
+      if (aCallback)
+        aCallback(null);
+      return;
+    }
+
+    // Obtain a converter to read from a UTF-8 encoded input stream.
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+
+    let data = NetUtil.readInputStreamToString(aStream,
+                                               aStream.available());
+    aCallback(converter.ConvertToUnicode(data));
+  });
+}
+
+const OfflineCacheInstaller = {
+  installCache: function installCache(app) {
+    let cacheDir = makeFile(app.basePath)
+    cacheDir.append(app.appId);
+    cacheDir.append("cache");
+    if (!cacheDir.exists())
+      return;
+
+    let cacheManifest = cacheDir.clone();
+    cacheManifest.append("manifest.appcache");
+    if (!cacheManifest.exists())
+      return;
+
+    enableOfflineCacheForApp(app.origin, app.localId);
+
+    // Get the url for the manifest.
+    let appcacheURL = app.origin + "cache/manifest.appcache";
+
+    // The group ID contains application id and 'f' for not being hosted in
+    // a browser element, but a mozbrowser iframe.
+    // See netwerk/cache/nsDiskCacheDeviceSQL.cpp: AppendJARIdentifier
+    let groupID = appcacheURL + '#' + app.localId+ '+f';
+    let applicationCache = applicationCacheService.createApplicationCache(groupID);
+    applicationCache.activate();
+
+    readFile(cacheManifest, function (content) {
+      let lines = content.split(/\r?\n/);
+      // Process each manifest line, read only CACHE entries
+      // (ignore NETWORK entries) and compute absolute URL for each entry
+      let urls = [];
+      for(let i = 0; i < lines.length; i++) {
+        let line = lines[i];
+        // Ignore comments
+        if (/^#/.test(line) || !line.length)
+          continue;
+        if (line == 'CACHE MANIFEST')
+          continue;
+        if (line == 'CACHE:')
+          continue;
+        // Ignore network entries and everything that comes after
+        if (line == 'NETWORK:')
+          break;
+
+        // Prepend webapp origin in case of absolute path
+        if (line[0] == '/') {
+          urls.push(app.origin + line.substring(1));
+        // Just pass along the url, if we have one
+        } else if (line.substr(0, 4) == 'http') {
+          urls.push(line);
+        } else {
+          throw new Error('Invalid line in appcache manifest:\n' + line +
+                          '\nFrom: ' + cacheManifest.path);
+        }
+      }
+      urls.forEach(function processCachedFile(url) {
+        // Get this nsIFile from cache folder for this URL
+        let path = url.replace(/https?:\/\//, '');
+        let file = cacheDir.clone();
+        let paths = path.split('/');
+        paths.forEach(file.append);
+
+        if (!file.exists()) {
+          let msg = 'File ' + file.path + ' exists in the manifest but does ' +
+                    'not points to a real file.';
+          throw new Error(msg);
+        }
+
+        let itemType = nsIApplicationCache.ITEM_EXPLICIT;
+        storeCache(applicationCache, url, file, itemType);
+      });
+    });
+  }
+};
+
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -12,16 +12,17 @@ const Cr = Components.results;
 let EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
+Cu.import("resource://gre/modules/OfflineCacheInstaller.jsm");
 
 function debug(aMsg) {
   //dump("-*-*- Webapps.jsm : " + aMsg + "\n");
 }
 
 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
@@ -170,16 +171,26 @@ let DOMApplicationRegistry = {
         manifestURL: this.webapps[aId].manifestURL,
         origin: this.webapps[aId].origin
       }, true, function() {
         debug("Error installing permissions for " + aId);
       });
     }).bind(this));
   },
 
+  updateOfflineCacheForApp: function updateOfflineCacheForApp(aId) {
+    let app = this.webapps[aId];
+    OfflineCacheInstaller.installCache({
+      basePath: app.basePath,
+      appId: aId,
+      origin: app.origin,
+      localId: app.localId
+    });
+  },
+
   // Implements the core of bug 787439
   // if at first run, go through these steps:
   //   a. load the core apps registry.
   //   b. uninstall any core app from the current registry but not in the
   //      new core apps registry.
   //   c. for all apps in the new core registry, install them if they are not
   //      yet in the current registry, and run installPermissions()
   installSystemApps: function installSystemApps(aNext) {
@@ -238,16 +249,17 @@ let DOMApplicationRegistry = {
   loadAndUpdateApps: function loadAndUpdateApps() {
     let runUpdate = AppsUtils.isFirstRun(Services.prefs);
 
     let onAppsLoaded = (function onAppsLoaded() {
       if (runUpdate) {
         // At first run, set up the permissions
         for (let id in this.webapps) {
           this.updatePermissionsForApp(id);
+          this.updateOfflineCacheForApp(id);
         }
       }
       this.registerAppsHandlers();
     }).bind(this);
 
     this.loadCurrentRegistry((function() {
 #ifdef MOZ_WIDGET_GONK
       // if first run, merge the system apps.
@@ -469,24 +481,24 @@ let DOMApplicationRegistry = {
 
           data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream,
                                                             aStream.available()) || ""));
           aStream.close();
           if (aCallback)
             aCallback(data);
         } catch (ex) {
           Cu.reportError("DOMApplicationRegistry: Could not parse JSON: " +
-                         aFile.path + " " + ex);
+                         aFile.path + " " + ex + "\n" + ex.stack);
           if (aCallback)
             aCallback(null);
         }
       });
     } catch (ex) {
       Cu.reportError("DOMApplicationRegistry: Could not read from " +
-                     aFile.path + " : " + ex);
+                     aFile.path + " : " + ex + "\n" + ex.stack);
       if (aCallback)
         aCallback(null);
     }
   },
 
   addMessageListener: function(aMsgNames, aMm) {
     aMsgNames.forEach(function (aMsgName) {
       if (!(aMsgName in this.children)) {