1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/apps/src/OfflineCacheInstaller.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,268 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cu = Components.utils; 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 +const CC = Components.Constructor; 1.14 + 1.15 +this.EXPORTED_SYMBOLS = ["OfflineCacheInstaller"]; 1.16 + 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.19 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.20 + 1.21 +let Namespace = CC('@mozilla.org/network/application-cache-namespace;1', 1.22 + 'nsIApplicationCacheNamespace', 1.23 + 'init'); 1.24 +let makeFile = CC('@mozilla.org/file/local;1', 1.25 + 'nsIFile', 1.26 + 'initWithPath'); 1.27 +let MutableArray = CC('@mozilla.org/array;1', 'nsIMutableArray'); 1.28 + 1.29 +const nsICache = Ci.nsICache; 1.30 +const nsIApplicationCache = Ci.nsIApplicationCache; 1.31 +const applicationCacheService = 1.32 + Cc['@mozilla.org/network/application-cache-service;1'] 1.33 + .getService(Ci.nsIApplicationCacheService); 1.34 + 1.35 + 1.36 +function debug(aMsg) { 1.37 + //dump("-*-*- OfflineCacheInstaller.jsm : " + aMsg + "\n"); 1.38 +} 1.39 + 1.40 + 1.41 +function enableOfflineCacheForApp(origin, appId) { 1.42 + let principal = Services.scriptSecurityManager.getAppCodebasePrincipal( 1.43 + origin, appId, false); 1.44 + Services.perms.addFromPrincipal(principal, 'offline-app', 1.45 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.46 + // Prevent cache from being evicted: 1.47 + Services.perms.addFromPrincipal(principal, 'pin-app', 1.48 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.49 +} 1.50 + 1.51 + 1.52 +function storeCache(applicationCache, url, file, itemType) { 1.53 + let session = Services.cache.createSession(applicationCache.clientID, 1.54 + nsICache.STORE_OFFLINE, true); 1.55 + session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, { 1.56 + onCacheEntryAvailable: function (cacheEntry, accessGranted, status) { 1.57 + cacheEntry.setMetaDataElement('request-method', 'GET'); 1.58 + cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n'); 1.59 + 1.60 + let outputStream = cacheEntry.openOutputStream(0); 1.61 + 1.62 + // Input-Output stream machinery in order to push nsIFile content into cache 1.63 + let inputStream = Cc['@mozilla.org/network/file-input-stream;1'] 1.64 + .createInstance(Ci.nsIFileInputStream); 1.65 + inputStream.init(file, 1, -1, null); 1.66 + let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1'] 1.67 + .createInstance(Ci.nsIBufferedOutputStream); 1.68 + bufferedOutputStream.init(outputStream, 1024); 1.69 + bufferedOutputStream.writeFrom(inputStream, inputStream.available()); 1.70 + bufferedOutputStream.flush(); 1.71 + bufferedOutputStream.close(); 1.72 + inputStream.close(); 1.73 + 1.74 + cacheEntry.markValid(); 1.75 + debug (file.path + ' -> ' + url + ' (' + itemType + ')'); 1.76 + applicationCache.markEntry(url, itemType); 1.77 + cacheEntry.close(); 1.78 + } 1.79 + }); 1.80 +} 1.81 + 1.82 +function readFile(aFile, aCallback) { 1.83 + let channel = NetUtil.newChannel(aFile); 1.84 + channel.contentType = "plain/text"; 1.85 + NetUtil.asyncFetch(channel, function(aStream, aResult) { 1.86 + if (!Components.isSuccessCode(aResult)) { 1.87 + Cu.reportError("OfflineCacheInstaller: Could not read file " + aFile.path); 1.88 + if (aCallback) 1.89 + aCallback(null); 1.90 + return; 1.91 + } 1.92 + 1.93 + // Obtain a converter to read from a UTF-8 encoded input stream. 1.94 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.95 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.96 + converter.charset = "UTF-8"; 1.97 + 1.98 + let data = NetUtil.readInputStreamToString(aStream, 1.99 + aStream.available()); 1.100 + aCallback(converter.ConvertToUnicode(data)); 1.101 + }); 1.102 +} 1.103 + 1.104 +function parseCacheLine(app, urls, line) { 1.105 + try { 1.106 + let url = Services.io.newURI(line, null, app.origin); 1.107 + urls.push(url.spec); 1.108 + } catch(e) { 1.109 + throw new Error('Unable to parse cache line: ' + line + '(' + e + ')'); 1.110 + } 1.111 +} 1.112 + 1.113 +function parseFallbackLine(app, urls, namespaces, fallbacks, line) { 1.114 + let split = line.split(/[ \t]+/); 1.115 + if (split.length != 2) { 1.116 + throw new Error('Should be made of two URLs seperated with spaces') 1.117 + } 1.118 + let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_FALLBACK; 1.119 + let [ namespace, fallback ] = split; 1.120 + 1.121 + // Prepend webapp origin in case of absolute path 1.122 + try { 1.123 + namespace = Services.io.newURI(namespace, null, app.origin).spec; 1.124 + fallback = Services.io.newURI(fallback, null, app.origin).spec; 1.125 + } catch(e) { 1.126 + throw new Error('Unable to parse fallback line: ' + line + '(' + e + ')'); 1.127 + } 1.128 + 1.129 + namespaces.push([type, namespace, fallback]); 1.130 + fallbacks.push(fallback); 1.131 + urls.push(fallback); 1.132 +} 1.133 + 1.134 +function parseNetworkLine(namespaces, line) { 1.135 + let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS; 1.136 + if (line[0] == '*' && (line.length == 1 || line[1] == ' ' 1.137 + || line[1] == '\t')) { 1.138 + namespaces.push([type, '', '']); 1.139 + } else { 1.140 + namespaces.push([type, namespace, '']); 1.141 + } 1.142 +} 1.143 + 1.144 +function parseAppCache(app, path, content) { 1.145 + let lines = content.split(/\r?\n/); 1.146 + 1.147 + let urls = []; 1.148 + let namespaces = []; 1.149 + let fallbacks = []; 1.150 + 1.151 + let currentSection = 'CACHE'; 1.152 + for (let i = 0; i < lines.length; i++) { 1.153 + let line = lines[i]; 1.154 + 1.155 + // Ignore comments 1.156 + if (/^#/.test(line) || !line.length) 1.157 + continue; 1.158 + 1.159 + // Process section headers 1.160 + if (line == 'CACHE MANIFEST') 1.161 + continue; 1.162 + if (line == 'CACHE:') { 1.163 + currentSection = 'CACHE'; 1.164 + continue; 1.165 + } else if (line == 'NETWORK:') { 1.166 + currentSection = 'NETWORK'; 1.167 + continue; 1.168 + } else if (line == 'FALLBACK:') { 1.169 + currentSection = 'FALLBACK'; 1.170 + continue; 1.171 + } 1.172 + 1.173 + // Process cache, network and fallback rules 1.174 + try { 1.175 + if (currentSection == 'CACHE') { 1.176 + parseCacheLine(app, urls, line); 1.177 + } else if (currentSection == 'NETWORK') { 1.178 + parseNetworkLine(namespaces, line); 1.179 + } else if (currentSection == 'FALLBACK') { 1.180 + parseFallbackLine(app, urls, namespaces, fallbacks, line); 1.181 + } 1.182 + } catch(e) { 1.183 + throw new Error('Invalid ' + currentSection + ' line in appcache ' + 1.184 + 'manifest:\n' + e.message + 1.185 + '\nFrom: ' + path + 1.186 + '\nLine ' + i + ': ' + line); 1.187 + } 1.188 + } 1.189 + 1.190 + return { 1.191 + urls: urls, 1.192 + namespaces: namespaces, 1.193 + fallbacks: fallbacks 1.194 + }; 1.195 +} 1.196 + 1.197 +function installCache(app) { 1.198 + if (!app.cachePath) { 1.199 + return; 1.200 + } 1.201 + 1.202 + let cacheDir = makeFile(app.cachePath) 1.203 + cacheDir.append(app.appId); 1.204 + cacheDir.append('cache'); 1.205 + if (!cacheDir.exists()) 1.206 + return; 1.207 + 1.208 + let cacheManifest = cacheDir.clone(); 1.209 + cacheManifest.append('manifest.appcache'); 1.210 + if (!cacheManifest.exists()) 1.211 + return; 1.212 + 1.213 + enableOfflineCacheForApp(app.origin, app.localId); 1.214 + 1.215 + // Get the url for the manifest. 1.216 + let appcacheURL = app.appcache_path; 1.217 + 1.218 + // The group ID contains application id and 'f' for not being hosted in 1.219 + // a browser element, but a mozbrowser iframe. 1.220 + // See netwerk/cache/nsDiskCacheDeviceSQL.cpp: AppendJARIdentifier 1.221 + let groupID = appcacheURL + '#' + app.localId+ '+f'; 1.222 + let applicationCache = applicationCacheService.createApplicationCache(groupID); 1.223 + applicationCache.activate(); 1.224 + 1.225 + readFile(cacheManifest, function readAppCache(content) { 1.226 + let entries = parseAppCache(app, cacheManifest.path, content); 1.227 + 1.228 + entries.urls.forEach(function processCachedFile(url) { 1.229 + // Get this nsIFile from cache folder for this URL 1.230 + // We have absolute urls, so remove the origin part to locate the 1.231 + // files. 1.232 + let path = url.replace(app.origin.spec, ''); 1.233 + let file = cacheDir.clone(); 1.234 + let paths = path.split('/'); 1.235 + paths.forEach(file.append); 1.236 + 1.237 + if (!file.exists()) { 1.238 + let msg = 'File ' + file.path + ' exists in the manifest but does ' + 1.239 + 'not points to a real file.'; 1.240 + throw new Error(msg); 1.241 + } 1.242 + 1.243 + let itemType = nsIApplicationCache.ITEM_EXPLICIT; 1.244 + storeCache(applicationCache, url, file, itemType); 1.245 + }); 1.246 + 1.247 + let array = new MutableArray(); 1.248 + entries.namespaces.forEach(function processNamespace([type, spec, data]) { 1.249 + debug('add namespace: ' + type + ' - ' + spec + ' - ' + data + '\n'); 1.250 + array.appendElement(new Namespace(type, spec, data), false); 1.251 + }); 1.252 + applicationCache.addNamespaces(array); 1.253 + 1.254 + entries.fallbacks.forEach(function processFallback(url) { 1.255 + debug('add fallback: ' + url + '\n'); 1.256 + let type = nsIApplicationCache.ITEM_FALLBACK; 1.257 + applicationCache.markEntry(url, type); 1.258 + }); 1.259 + 1.260 + storeCache(applicationCache, appcacheURL, cacheManifest, 1.261 + nsIApplicationCache.ITEM_MANIFEST); 1.262 + }); 1.263 +} 1.264 + 1.265 + 1.266 +// Public API 1.267 + 1.268 +this.OfflineCacheInstaller = { 1.269 + installCache: installCache 1.270 +}; 1.271 +