|
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 |