dom/apps/src/OfflineCacheInstaller.jsm

changeset 2
7e26c7da4463
equal deleted inserted replaced
-1:000000000000 0:190180458b27
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/. */
4
5 "use strict";
6
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10 const CC = Components.Constructor;
11
12 this.EXPORTED_SYMBOLS = ["OfflineCacheInstaller"];
13
14 Cu.import("resource://gre/modules/Services.jsm");
15 Cu.import("resource://gre/modules/AppsUtils.jsm");
16 Cu.import("resource://gre/modules/NetUtil.jsm");
17
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');
25
26 const nsICache = Ci.nsICache;
27 const nsIApplicationCache = Ci.nsIApplicationCache;
28 const applicationCacheService =
29 Cc['@mozilla.org/network/application-cache-service;1']
30 .getService(Ci.nsIApplicationCacheService);
31
32
33 function debug(aMsg) {
34 //dump("-*-*- OfflineCacheInstaller.jsm : " + aMsg + "\n");
35 }
36
37
38 function enableOfflineCacheForApp(origin, appId) {
39 let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
40 origin, appId, false);
41 Services.perms.addFromPrincipal(principal, 'offline-app',
42 Ci.nsIPermissionManager.ALLOW_ACTION);
43 // Prevent cache from being evicted:
44 Services.perms.addFromPrincipal(principal, 'pin-app',
45 Ci.nsIPermissionManager.ALLOW_ACTION);
46 }
47
48
49 function storeCache(applicationCache, url, file, itemType) {
50 let session = Services.cache.createSession(applicationCache.clientID,
51 nsICache.STORE_OFFLINE, true);
52 session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, {
53 onCacheEntryAvailable: function (cacheEntry, accessGranted, status) {
54 cacheEntry.setMetaDataElement('request-method', 'GET');
55 cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n');
56
57 let outputStream = cacheEntry.openOutputStream(0);
58
59 // Input-Output stream machinery in order to push nsIFile content into cache
60 let inputStream = Cc['@mozilla.org/network/file-input-stream;1']
61 .createInstance(Ci.nsIFileInputStream);
62 inputStream.init(file, 1, -1, null);
63 let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1']
64 .createInstance(Ci.nsIBufferedOutputStream);
65 bufferedOutputStream.init(outputStream, 1024);
66 bufferedOutputStream.writeFrom(inputStream, inputStream.available());
67 bufferedOutputStream.flush();
68 bufferedOutputStream.close();
69 inputStream.close();
70
71 cacheEntry.markValid();
72 debug (file.path + ' -> ' + url + ' (' + itemType + ')');
73 applicationCache.markEntry(url, itemType);
74 cacheEntry.close();
75 }
76 });
77 }
78
79 function readFile(aFile, aCallback) {
80 let channel = NetUtil.newChannel(aFile);
81 channel.contentType = "plain/text";
82 NetUtil.asyncFetch(channel, function(aStream, aResult) {
83 if (!Components.isSuccessCode(aResult)) {
84 Cu.reportError("OfflineCacheInstaller: Could not read file " + aFile.path);
85 if (aCallback)
86 aCallback(null);
87 return;
88 }
89
90 // Obtain a converter to read from a UTF-8 encoded input stream.
91 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
92 .createInstance(Ci.nsIScriptableUnicodeConverter);
93 converter.charset = "UTF-8";
94
95 let data = NetUtil.readInputStreamToString(aStream,
96 aStream.available());
97 aCallback(converter.ConvertToUnicode(data));
98 });
99 }
100
101 function parseCacheLine(app, urls, line) {
102 try {
103 let url = Services.io.newURI(line, null, app.origin);
104 urls.push(url.spec);
105 } catch(e) {
106 throw new Error('Unable to parse cache line: ' + line + '(' + e + ')');
107 }
108 }
109
110 function parseFallbackLine(app, urls, namespaces, fallbacks, line) {
111 let split = line.split(/[ \t]+/);
112 if (split.length != 2) {
113 throw new Error('Should be made of two URLs seperated with spaces')
114 }
115 let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_FALLBACK;
116 let [ namespace, fallback ] = split;
117
118 // Prepend webapp origin in case of absolute path
119 try {
120 namespace = Services.io.newURI(namespace, null, app.origin).spec;
121 fallback = Services.io.newURI(fallback, null, app.origin).spec;
122 } catch(e) {
123 throw new Error('Unable to parse fallback line: ' + line + '(' + e + ')');
124 }
125
126 namespaces.push([type, namespace, fallback]);
127 fallbacks.push(fallback);
128 urls.push(fallback);
129 }
130
131 function parseNetworkLine(namespaces, line) {
132 let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS;
133 if (line[0] == '*' && (line.length == 1 || line[1] == ' '
134 || line[1] == '\t')) {
135 namespaces.push([type, '', '']);
136 } else {
137 namespaces.push([type, namespace, '']);
138 }
139 }
140
141 function parseAppCache(app, path, content) {
142 let lines = content.split(/\r?\n/);
143
144 let urls = [];
145 let namespaces = [];
146 let fallbacks = [];
147
148 let currentSection = 'CACHE';
149 for (let i = 0; i < lines.length; i++) {
150 let line = lines[i];
151
152 // Ignore comments
153 if (/^#/.test(line) || !line.length)
154 continue;
155
156 // Process section headers
157 if (line == 'CACHE MANIFEST')
158 continue;
159 if (line == 'CACHE:') {
160 currentSection = 'CACHE';
161 continue;
162 } else if (line == 'NETWORK:') {
163 currentSection = 'NETWORK';
164 continue;
165 } else if (line == 'FALLBACK:') {
166 currentSection = 'FALLBACK';
167 continue;
168 }
169
170 // Process cache, network and fallback rules
171 try {
172 if (currentSection == 'CACHE') {
173 parseCacheLine(app, urls, line);
174 } else if (currentSection == 'NETWORK') {
175 parseNetworkLine(namespaces, line);
176 } else if (currentSection == 'FALLBACK') {
177 parseFallbackLine(app, urls, namespaces, fallbacks, line);
178 }
179 } catch(e) {
180 throw new Error('Invalid ' + currentSection + ' line in appcache ' +
181 'manifest:\n' + e.message +
182 '\nFrom: ' + path +
183 '\nLine ' + i + ': ' + line);
184 }
185 }
186
187 return {
188 urls: urls,
189 namespaces: namespaces,
190 fallbacks: fallbacks
191 };
192 }
193
194 function installCache(app) {
195 if (!app.cachePath) {
196 return;
197 }
198
199 let cacheDir = makeFile(app.cachePath)
200 cacheDir.append(app.appId);
201 cacheDir.append('cache');
202 if (!cacheDir.exists())
203 return;
204
205 let cacheManifest = cacheDir.clone();
206 cacheManifest.append('manifest.appcache');
207 if (!cacheManifest.exists())
208 return;
209
210 enableOfflineCacheForApp(app.origin, app.localId);
211
212 // Get the url for the manifest.
213 let appcacheURL = app.appcache_path;
214
215 // The group ID contains application id and 'f' for not being hosted in
216 // a browser element, but a mozbrowser iframe.
217 // See netwerk/cache/nsDiskCacheDeviceSQL.cpp: AppendJARIdentifier
218 let groupID = appcacheURL + '#' + app.localId+ '+f';
219 let applicationCache = applicationCacheService.createApplicationCache(groupID);
220 applicationCache.activate();
221
222 readFile(cacheManifest, function readAppCache(content) {
223 let entries = parseAppCache(app, cacheManifest.path, content);
224
225 entries.urls.forEach(function processCachedFile(url) {
226 // Get this nsIFile from cache folder for this URL
227 // We have absolute urls, so remove the origin part to locate the
228 // files.
229 let path = url.replace(app.origin.spec, '');
230 let file = cacheDir.clone();
231 let paths = path.split('/');
232 paths.forEach(file.append);
233
234 if (!file.exists()) {
235 let msg = 'File ' + file.path + ' exists in the manifest but does ' +
236 'not points to a real file.';
237 throw new Error(msg);
238 }
239
240 let itemType = nsIApplicationCache.ITEM_EXPLICIT;
241 storeCache(applicationCache, url, file, itemType);
242 });
243
244 let array = new MutableArray();
245 entries.namespaces.forEach(function processNamespace([type, spec, data]) {
246 debug('add namespace: ' + type + ' - ' + spec + ' - ' + data + '\n');
247 array.appendElement(new Namespace(type, spec, data), false);
248 });
249 applicationCache.addNamespaces(array);
250
251 entries.fallbacks.forEach(function processFallback(url) {
252 debug('add fallback: ' + url + '\n');
253 let type = nsIApplicationCache.ITEM_FALLBACK;
254 applicationCache.markEntry(url, type);
255 });
256
257 storeCache(applicationCache, appcacheURL, cacheManifest,
258 nsIApplicationCache.ITEM_MANIFEST);
259 });
260 }
261
262
263 // Public API
264
265 this.OfflineCacheInstaller = {
266 installCache: installCache
267 };
268

mercurial