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/. */
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',
21 let makeFile = CC('@mozilla.org/file/local;1',
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) {
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
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();
85 cacheEntry.setExpirationTime(0);
86 cacheEntry.markValid();
87 debug (file.path + " -> " + url + " (" + itemType + ")");
88 applicationCache.markEntry(url, itemType);
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);
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));
116 function parseCacheLine(app, urls, line) {
118 let url = Services.io.newURI(line, null, app.origin);
121 throw new Error('Unable to parse cache line: ' + line + '(' + e + ')');
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')
130 let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_FALLBACK;
131 let [ namespace, fallback ] = split;
133 // Prepend webapp origin in case of absolute path
135 namespace = Services.io.newURI(namespace, null, app.origin).spec;
136 fallback = Services.io.newURI(fallback, null, app.origin).spec;
138 throw new Error('Unable to parse fallback line: ' + line + '(' + e + ')');
141 namespaces.push([type, namespace, fallback]);
142 fallbacks.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, '', '']);
152 namespaces.push([type, namespace, '']);
156 function parseAppCache(app, path, content) {
157 let lines = content.split(/\r?\n/);
163 let currentSection = 'CACHE';
164 for (let i = 0; i < lines.length; i++) {
168 if (/^#/.test(line) || !line.length)
171 // Process section headers
172 if (line == 'CACHE MANIFEST')
174 if (line == 'CACHE:') {
175 currentSection = 'CACHE';
177 } else if (line == 'NETWORK:') {
178 currentSection = 'NETWORK';
180 } else if (line == 'FALLBACK:') {
181 currentSection = 'FALLBACK';
185 // Process cache, network and fallback rules
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);
195 throw new Error('Invalid ' + currentSection + ' line in appcache ' +
196 'manifest:\n' + e.message +
198 '\nLine ' + i + ': ' + line);
204 namespaces: namespaces,
209 function installCache(app) {
210 if (!app.cachePath) {
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())
224 let cacheManifest = cacheDir.clone();
225 cacheManifest.append('manifest.appcache');
226 if (!cacheManifest.exists())
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.
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({});
239 metadataLoaded = new Promise(
241 readFile(resourcesMetadata, content => resolve(JSON.parse(content))));
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
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);
275 let itemType = nsIApplicationCache.ITEM_EXPLICIT;
276 if (entries.fallbacks.indexOf(url) > -1) {
277 debug('add fallback: ' + url + '\n');
278 itemType |= nsIApplicationCache.ITEM_FALLBACK;
280 storeCache(applicationCache, url, file, itemType, metadata[path]);
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);
288 applicationCache.addNamespaces(array);
290 storeCache(applicationCache, appcacheURL, cacheManifest,
291 nsIApplicationCache.ITEM_MANIFEST);
299 this.OfflineCacheInstaller = {
300 installCache: installCache