Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / OfflineCacheInstaller.jsm
blob0f9122260bbb506809634b2c6c8da335a00c7654
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10 const CC = Components.Constructor;
12 this.EXPORTED_SYMBOLS = ["OfflineCacheInstaller"];
14 Cu.import("resource://gre/modules/Services.jsm");
15 Cu.import("resource://gre/modules/AppsUtils.jsm");
16 Cu.import("resource://gre/modules/NetUtil.jsm");
18 let Namespace = CC('@mozilla.org/network/application-cache-namespace;1',
19                    'nsIApplicationCacheNamespace',
20                    'init');
21 let makeFile = CC('@mozilla.org/file/local;1',
22                 'nsIFile',
23                 'initWithPath');
24 let MutableArray = CC('@mozilla.org/array;1', 'nsIMutableArray');
26 let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
28 const nsICacheStorage = Ci.nsICacheStorage;
29 const nsIApplicationCache = Ci.nsIApplicationCache;
30 const applicationCacheService =
31   Cc['@mozilla.org/network/application-cache-service;1']
32     .getService(Ci.nsIApplicationCacheService);
35 function debug(aMsg) {
36   //dump("-*-*- OfflineCacheInstaller.jsm : " + aMsg + "\n");
40 function enableOfflineCacheForApp(origin, appId) {
41   let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
42                     origin, appId, false);
43   Services.perms.addFromPrincipal(principal, 'offline-app',
44                                   Ci.nsIPermissionManager.ALLOW_ACTION);
45   // Prevent cache from being evicted:
46   Services.perms.addFromPrincipal(principal, 'pin-app',
47                                   Ci.nsIPermissionManager.ALLOW_ACTION);
51 function storeCache(applicationCache, url, file, itemType, metadata) {
52   let storage =
53     Services.cache2.appCacheStorage(LoadContextInfo.default, applicationCache);
54   let uri = Services.io.newURI(url, null, null);
55   let nowGMT = new Date().toGMTString();
56   metadata = metadata || {};
57   metadata.lastFetched = metadata.lastFetched || nowGMT;
58   metadata.lastModified = metadata.lastModified || nowGMT;
59   storage.asyncOpenURI(uri, "", nsICacheStorage.OPEN_TRUNCATE, {
60     onCacheEntryAvailable:
61       function (cacheEntry, isNew, appCache, result) {
62         cacheEntry.setMetaDataElement("request-method", "GET");
63         cacheEntry.setMetaDataElement("response-head",
64           "HTTP/1.1 200 OK\r\n" +
65           "Date: " + metadata.lastFetched + "\r\n" +
66           "Last-Modified: " + metadata.lastModified + "\r\n" +
67           "Cache-Control: no-cache\r\n");
69         let outputStream = cacheEntry.openOutputStream(0);
71         // Input-Output stream machinery in order to push nsIFile content into
72         // cache
73         let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
74                             .createInstance(Ci.nsIFileInputStream);
75         inputStream.init(file, 1, -1, null);
76         let bufferedOutputStream =
77           Cc["@mozilla.org/network/buffered-output-stream;1"]
78             .createInstance(Ci.nsIBufferedOutputStream);
79         bufferedOutputStream.init(outputStream, 1024);
80         bufferedOutputStream.writeFrom(inputStream, inputStream.available());
81         bufferedOutputStream.flush();
82         bufferedOutputStream.close();
83         inputStream.close();
85         cacheEntry.setExpirationTime(0);
86         cacheEntry.markValid();
87         debug (file.path + " -> " + url + " (" + itemType + ")");
88         applicationCache.markEntry(url, itemType);
89         cacheEntry.close();
90       }
91   });
94 function readFile(aFile, aCallback) {
95   let channel = NetUtil.newChannel(aFile);
96   channel.contentType = "plain/text";
97   NetUtil.asyncFetch(channel, function(aStream, aResult) {
98     if (!Components.isSuccessCode(aResult)) {
99       Cu.reportError("OfflineCacheInstaller: Could not read file " + aFile.path);
100       if (aCallback)
101         aCallback(null);
102       return;
103     }
105     // Obtain a converter to read from a UTF-8 encoded input stream.
106     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
107                       .createInstance(Ci.nsIScriptableUnicodeConverter);
108     converter.charset = "UTF-8";
110     let data = NetUtil.readInputStreamToString(aStream,
111                                                aStream.available());
112     aCallback(converter.ConvertToUnicode(data));
113   });
116 function parseCacheLine(app, urls, line) {
117   try {
118     let url = Services.io.newURI(line, null, app.origin);
119     urls.push(url.spec);
120   } catch(e) {
121     throw new Error('Unable to parse cache line: ' + line + '(' + e + ')');
122   }
125 function parseFallbackLine(app, urls, namespaces, fallbacks, line) {
126   let split = line.split(/[ \t]+/);
127   if (split.length != 2) {
128     throw new Error('Should be made of two URLs seperated with spaces')
129   }
130   let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_FALLBACK;
131   let [ namespace, fallback ] = split;
133   // Prepend webapp origin in case of absolute path
134   try {
135     namespace = Services.io.newURI(namespace, null, app.origin).spec;
136     fallback = Services.io.newURI(fallback, null, app.origin).spec;
137   } catch(e) {
138     throw new Error('Unable to parse fallback line: ' + line + '(' + e + ')');
139   }
141   namespaces.push([type, namespace, fallback]);
142   fallbacks.push(fallback);
143   urls.push(fallback);
146 function parseNetworkLine(namespaces, line) {
147   let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS;
148   if (line[0] == '*' && (line.length == 1 || line[1] == ' '
149                                           || line[1] == '\t')) {
150     namespaces.push([type, '', '']);
151   } else {
152     namespaces.push([type, namespace, '']);
153   }
156 function parseAppCache(app, path, content) {
157   let lines = content.split(/\r?\n/);
159   let urls = [];
160   let namespaces = [];
161   let fallbacks = [];
163   let currentSection = 'CACHE';
164   for (let i = 0; i < lines.length; i++) {
165     let line = lines[i];
167     // Ignore comments
168     if (/^#/.test(line) || !line.length)
169       continue;
171     // Process section headers
172     if (line == 'CACHE MANIFEST')
173       continue;
174     if (line == 'CACHE:') {
175       currentSection = 'CACHE';
176       continue;
177     } else if (line == 'NETWORK:') {
178       currentSection = 'NETWORK';
179       continue;
180     } else if (line == 'FALLBACK:') {
181       currentSection = 'FALLBACK';
182       continue;
183     }
185     // Process cache, network and fallback rules
186     try {
187       if (currentSection == 'CACHE') {
188         parseCacheLine(app, urls, line);
189       } else if (currentSection == 'NETWORK') {
190         parseNetworkLine(namespaces, line);
191       } else if (currentSection == 'FALLBACK') {
192         parseFallbackLine(app, urls, namespaces, fallbacks, line);
193       }
194     } catch(e) {
195       throw new Error('Invalid ' + currentSection + ' line in appcache ' +
196                       'manifest:\n' + e.message +
197                       '\nFrom: ' + path +
198                       '\nLine ' + i + ': ' + line);
199     }
200   }
202   return {
203     urls: urls,
204     namespaces: namespaces,
205     fallbacks: fallbacks
206   };
209 function installCache(app) {
210   if (!app.cachePath) {
211     return;
212   }
214   let cacheDir = makeFile(app.cachePath);
215   cacheDir.append(app.appId);
217   let resourcesMetadata = cacheDir.clone();
218   resourcesMetadata.append('resources_metadata.json');
220   cacheDir.append('cache');
221   if (!cacheDir.exists())
222     return;
224   let cacheManifest = cacheDir.clone();
225   cacheManifest.append('manifest.appcache');
226   if (!cacheManifest.exists())
227     return;
229   // If the build has been correctly configured, this should not happen!
230   // If we install the cache anyway, it won't be updateable. If we don't install
231   // it, the application won't be useable offline.
232   let metadataLoaded;
233   if (!resourcesMetadata.exists()) {
234     // Not debug, since this is something that should be logged always!
235     dump("OfflineCacheInstaller: App " + app.appId + " does have an app cache" +
236          " but does not have a resources_metadata.json file!");
237     metadataLoaded = Promise.resolve({});
238   } else {
239     metadataLoaded = new Promise(
240       (resolve, reject) =>
241         readFile(resourcesMetadata, content => resolve(JSON.parse(content))));
242   }
244   metadataLoaded.then(function(metadata) {
245     enableOfflineCacheForApp(app.origin, app.localId);
247     // Get the url for the manifest.
248     let appcacheURL = app.appcache_path;
250     // The group ID contains application id and 'f' for not being hosted in
251     // a browser element, but a mozbrowser iframe.
252     // See netwerk/cache/nsDiskCacheDeviceSQL.cpp: AppendJARIdentifier
253     let groupID = appcacheURL + '#' + app.localId+ '+f';
254     let applicationCache = applicationCacheService.createApplicationCache(groupID);
255     applicationCache.activate();
257     readFile(cacheManifest, function readAppCache(content) {
258       let entries = parseAppCache(app, cacheManifest.path, content);
260       entries.urls.forEach(function processCachedFile(url) {
261         // Get this nsIFile from cache folder for this URL
262         // We have absolute urls, so remove the origin part to locate the
263         // files.
264         let path = url.replace(app.origin.spec, '');
265         let file = cacheDir.clone();
266         let paths = path.split('/');
267         paths.forEach(file.append);
269         if (!file.exists()) {
270           let msg = 'File ' + file.path + ' exists in the manifest but does ' +
271                     'not points to a real file.';
272           throw new Error(msg);
273         }
275         let itemType = nsIApplicationCache.ITEM_EXPLICIT;
276         if (entries.fallbacks.indexOf(url) > -1) {
277           debug('add fallback: ' + url + '\n');
278           itemType |= nsIApplicationCache.ITEM_FALLBACK;
279         }
280         storeCache(applicationCache, url, file, itemType, metadata[path]);
281       });
283       let array = new MutableArray();
284       entries.namespaces.forEach(function processNamespace([type, spec, data]) {
285         debug('add namespace: ' + type + ' - ' + spec + ' - ' + data + '\n');
286         array.appendElement(new Namespace(type, spec, data), false);
287       });
288       applicationCache.addNamespaces(array);
290       storeCache(applicationCache, appcacheURL, cacheManifest,
291                  nsIApplicationCache.ITEM_MANIFEST);
292     });
293   });
297 // Public API
299 this.OfflineCacheInstaller = {
300   installCache: installCache