michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const CC = Components.Constructor; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["OfflineCacheInstaller"]; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/AppsUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: let Namespace = CC('@mozilla.org/network/application-cache-namespace;1', michael@0: 'nsIApplicationCacheNamespace', michael@0: 'init'); michael@0: let makeFile = CC('@mozilla.org/file/local;1', michael@0: 'nsIFile', michael@0: 'initWithPath'); michael@0: let MutableArray = CC('@mozilla.org/array;1', 'nsIMutableArray'); michael@0: michael@0: const nsICache = Ci.nsICache; michael@0: const nsIApplicationCache = Ci.nsIApplicationCache; michael@0: const applicationCacheService = michael@0: Cc['@mozilla.org/network/application-cache-service;1'] michael@0: .getService(Ci.nsIApplicationCacheService); michael@0: michael@0: michael@0: function debug(aMsg) { michael@0: //dump("-*-*- OfflineCacheInstaller.jsm : " + aMsg + "\n"); michael@0: } michael@0: michael@0: michael@0: function enableOfflineCacheForApp(origin, appId) { michael@0: let principal = Services.scriptSecurityManager.getAppCodebasePrincipal( michael@0: origin, appId, false); michael@0: Services.perms.addFromPrincipal(principal, 'offline-app', michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: // Prevent cache from being evicted: michael@0: Services.perms.addFromPrincipal(principal, 'pin-app', michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: } michael@0: michael@0: michael@0: function storeCache(applicationCache, url, file, itemType) { michael@0: let session = Services.cache.createSession(applicationCache.clientID, michael@0: nsICache.STORE_OFFLINE, true); michael@0: session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, { michael@0: onCacheEntryAvailable: function (cacheEntry, accessGranted, status) { michael@0: cacheEntry.setMetaDataElement('request-method', 'GET'); michael@0: cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n'); michael@0: michael@0: let outputStream = cacheEntry.openOutputStream(0); michael@0: michael@0: // Input-Output stream machinery in order to push nsIFile content into cache michael@0: let inputStream = Cc['@mozilla.org/network/file-input-stream;1'] michael@0: .createInstance(Ci.nsIFileInputStream); michael@0: inputStream.init(file, 1, -1, null); michael@0: let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1'] michael@0: .createInstance(Ci.nsIBufferedOutputStream); michael@0: bufferedOutputStream.init(outputStream, 1024); michael@0: bufferedOutputStream.writeFrom(inputStream, inputStream.available()); michael@0: bufferedOutputStream.flush(); michael@0: bufferedOutputStream.close(); michael@0: inputStream.close(); michael@0: michael@0: cacheEntry.markValid(); michael@0: debug (file.path + ' -> ' + url + ' (' + itemType + ')'); michael@0: applicationCache.markEntry(url, itemType); michael@0: cacheEntry.close(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: function readFile(aFile, aCallback) { michael@0: let channel = NetUtil.newChannel(aFile); michael@0: channel.contentType = "plain/text"; michael@0: NetUtil.asyncFetch(channel, function(aStream, aResult) { michael@0: if (!Components.isSuccessCode(aResult)) { michael@0: Cu.reportError("OfflineCacheInstaller: Could not read file " + aFile.path); michael@0: if (aCallback) michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: // Obtain a converter to read from a UTF-8 encoded input stream. michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: michael@0: let data = NetUtil.readInputStreamToString(aStream, michael@0: aStream.available()); michael@0: aCallback(converter.ConvertToUnicode(data)); michael@0: }); michael@0: } michael@0: michael@0: function parseCacheLine(app, urls, line) { michael@0: try { michael@0: let url = Services.io.newURI(line, null, app.origin); michael@0: urls.push(url.spec); michael@0: } catch(e) { michael@0: throw new Error('Unable to parse cache line: ' + line + '(' + e + ')'); michael@0: } michael@0: } michael@0: michael@0: function parseFallbackLine(app, urls, namespaces, fallbacks, line) { michael@0: let split = line.split(/[ \t]+/); michael@0: if (split.length != 2) { michael@0: throw new Error('Should be made of two URLs seperated with spaces') michael@0: } michael@0: let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_FALLBACK; michael@0: let [ namespace, fallback ] = split; michael@0: michael@0: // Prepend webapp origin in case of absolute path michael@0: try { michael@0: namespace = Services.io.newURI(namespace, null, app.origin).spec; michael@0: fallback = Services.io.newURI(fallback, null, app.origin).spec; michael@0: } catch(e) { michael@0: throw new Error('Unable to parse fallback line: ' + line + '(' + e + ')'); michael@0: } michael@0: michael@0: namespaces.push([type, namespace, fallback]); michael@0: fallbacks.push(fallback); michael@0: urls.push(fallback); michael@0: } michael@0: michael@0: function parseNetworkLine(namespaces, line) { michael@0: let type = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS; michael@0: if (line[0] == '*' && (line.length == 1 || line[1] == ' ' michael@0: || line[1] == '\t')) { michael@0: namespaces.push([type, '', '']); michael@0: } else { michael@0: namespaces.push([type, namespace, '']); michael@0: } michael@0: } michael@0: michael@0: function parseAppCache(app, path, content) { michael@0: let lines = content.split(/\r?\n/); michael@0: michael@0: let urls = []; michael@0: let namespaces = []; michael@0: let fallbacks = []; michael@0: michael@0: let currentSection = 'CACHE'; michael@0: for (let i = 0; i < lines.length; i++) { michael@0: let line = lines[i]; michael@0: michael@0: // Ignore comments michael@0: if (/^#/.test(line) || !line.length) michael@0: continue; michael@0: michael@0: // Process section headers michael@0: if (line == 'CACHE MANIFEST') michael@0: continue; michael@0: if (line == 'CACHE:') { michael@0: currentSection = 'CACHE'; michael@0: continue; michael@0: } else if (line == 'NETWORK:') { michael@0: currentSection = 'NETWORK'; michael@0: continue; michael@0: } else if (line == 'FALLBACK:') { michael@0: currentSection = 'FALLBACK'; michael@0: continue; michael@0: } michael@0: michael@0: // Process cache, network and fallback rules michael@0: try { michael@0: if (currentSection == 'CACHE') { michael@0: parseCacheLine(app, urls, line); michael@0: } else if (currentSection == 'NETWORK') { michael@0: parseNetworkLine(namespaces, line); michael@0: } else if (currentSection == 'FALLBACK') { michael@0: parseFallbackLine(app, urls, namespaces, fallbacks, line); michael@0: } michael@0: } catch(e) { michael@0: throw new Error('Invalid ' + currentSection + ' line in appcache ' + michael@0: 'manifest:\n' + e.message + michael@0: '\nFrom: ' + path + michael@0: '\nLine ' + i + ': ' + line); michael@0: } michael@0: } michael@0: michael@0: return { michael@0: urls: urls, michael@0: namespaces: namespaces, michael@0: fallbacks: fallbacks michael@0: }; michael@0: } michael@0: michael@0: function installCache(app) { michael@0: if (!app.cachePath) { michael@0: return; michael@0: } michael@0: michael@0: let cacheDir = makeFile(app.cachePath) michael@0: cacheDir.append(app.appId); michael@0: cacheDir.append('cache'); michael@0: if (!cacheDir.exists()) michael@0: return; michael@0: michael@0: let cacheManifest = cacheDir.clone(); michael@0: cacheManifest.append('manifest.appcache'); michael@0: if (!cacheManifest.exists()) michael@0: return; michael@0: michael@0: enableOfflineCacheForApp(app.origin, app.localId); michael@0: michael@0: // Get the url for the manifest. michael@0: let appcacheURL = app.appcache_path; michael@0: michael@0: // The group ID contains application id and 'f' for not being hosted in michael@0: // a browser element, but a mozbrowser iframe. michael@0: // See netwerk/cache/nsDiskCacheDeviceSQL.cpp: AppendJARIdentifier michael@0: let groupID = appcacheURL + '#' + app.localId+ '+f'; michael@0: let applicationCache = applicationCacheService.createApplicationCache(groupID); michael@0: applicationCache.activate(); michael@0: michael@0: readFile(cacheManifest, function readAppCache(content) { michael@0: let entries = parseAppCache(app, cacheManifest.path, content); michael@0: michael@0: entries.urls.forEach(function processCachedFile(url) { michael@0: // Get this nsIFile from cache folder for this URL michael@0: // We have absolute urls, so remove the origin part to locate the michael@0: // files. michael@0: let path = url.replace(app.origin.spec, ''); michael@0: let file = cacheDir.clone(); michael@0: let paths = path.split('/'); michael@0: paths.forEach(file.append); michael@0: michael@0: if (!file.exists()) { michael@0: let msg = 'File ' + file.path + ' exists in the manifest but does ' + michael@0: 'not points to a real file.'; michael@0: throw new Error(msg); michael@0: } michael@0: michael@0: let itemType = nsIApplicationCache.ITEM_EXPLICIT; michael@0: storeCache(applicationCache, url, file, itemType); michael@0: }); michael@0: michael@0: let array = new MutableArray(); michael@0: entries.namespaces.forEach(function processNamespace([type, spec, data]) { michael@0: debug('add namespace: ' + type + ' - ' + spec + ' - ' + data + '\n'); michael@0: array.appendElement(new Namespace(type, spec, data), false); michael@0: }); michael@0: applicationCache.addNamespaces(array); michael@0: michael@0: entries.fallbacks.forEach(function processFallback(url) { michael@0: debug('add fallback: ' + url + '\n'); michael@0: let type = nsIApplicationCache.ITEM_FALLBACK; michael@0: applicationCache.markEntry(url, type); michael@0: }); michael@0: michael@0: storeCache(applicationCache, appcacheURL, cacheManifest, michael@0: nsIApplicationCache.ITEM_MANIFEST); michael@0: }); michael@0: } michael@0: michael@0: michael@0: // Public API michael@0: michael@0: this.OfflineCacheInstaller = { michael@0: installCache: installCache michael@0: }; michael@0: