diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index adc94b5d48f..d48980d8635 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -816,7 +816,7 @@ var AlertsHelper = { this._listeners[uid] = listener; let app = DOMApplicationRegistry.getAppByManifestURL(listener.manifestURL); - DOMApplicationRegistry.getManifestFor(app.manifestURL, function(manifest) { + DOMApplicationRegistry.getManifestFor(app.manifestURL).then((manifest) => { let helper = new ManifestHelper(manifest, app.origin); let getNotificationURLFor = function(messages) { if (!messages) @@ -873,7 +873,7 @@ var AlertsHelper = { // If we have a manifest URL, get the icon and title from the manifest // to prevent spoofing. let app = DOMApplicationRegistry.getAppByManifestURL(manifestUrl); - DOMApplicationRegistry.getManifestFor(manifestUrl, function(aManifest) { + DOMApplicationRegistry.getManifestFor(manifestUrl).then((aManifest) => { let helper = new ManifestHelper(aManifest, app.origin); send(helper.name, helper.iconURLForSize(128)); }); @@ -972,7 +972,7 @@ var WebappsHelper = { switch(topic) { case "webapps-launch": - DOMApplicationRegistry.getManifestFor(json.manifestURL, function(aManifest) { + DOMApplicationRegistry.getManifestFor(json.manifestURL).then((aManifest) => { if (!aManifest) return; diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 824ebddbcc7..cea3eda870a 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -12,7 +12,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 1fb60999250..0d13dbfdbf1 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 824ebddbcc7..cea3eda870a 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -12,7 +12,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 622de6aa0d6..b0de69e6861 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "6ee0334bda22b4848d99e3a44e29eab3de815f52", + "revision": "18bd82325a82f5b9a3a4b976e213515cd3e5866b", "repo_path": "/integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index cc80701aea0..97a212e1142 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 5dbf627a872..e0477b31603 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -10,7 +10,7 @@ - + diff --git a/b2g/config/inari/sources.xml b/b2g/config/inari/sources.xml index 9f6a2ea833b..b91d60ca92f 100644 --- a/b2g/config/inari/sources.xml +++ b/b2g/config/inari/sources.xml @@ -12,7 +12,7 @@ - + diff --git a/b2g/config/leo/sources.xml b/b2g/config/leo/sources.xml index 6cc70cce37d..a3bbf36cce6 100644 --- a/b2g/config/leo/sources.xml +++ b/b2g/config/leo/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/b2g/config/mako/sources.xml b/b2g/config/mako/sources.xml index 82dcda84b2d..63dc9c0d9a6 100644 --- a/b2g/config/mako/sources.xml +++ b/b2g/config/mako/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index a17e19e0317..c478353d9d2 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/dom/apps/src/Webapps.js b/dom/apps/src/Webapps.js index 2601cbc8789..fca1ee8d023 100644 --- a/dom/apps/src/Webapps.js +++ b/dom/apps/src/Webapps.js @@ -508,6 +508,49 @@ WebappsApplication.prototype = { }.bind(this)); }, + addReceipt: function(receipt) { + let request = this.createRequest(); + + this.addMessageListeners(["Webapps:AddReceipt:Return:OK", + "Webapps:AddReceipt:Return:KO"]); + + cpmm.sendAsyncMessage("Webapps:AddReceipt", { manifestURL: this.manifestURL, + receipt: receipt, + oid: this._id, + requestID: this.getRequestId(request) }); + + return request; + }, + + removeReceipt: function(receipt) { + let request = this.createRequest(); + + this.addMessageListeners(["Webapps:RemoveReceipt:Return:OK", + "Webapps:RemoveReceipt:Return:KO"]); + + cpmm.sendAsyncMessage("Webapps:RemoveReceipt", { manifestURL: this.manifestURL, + receipt: receipt, + oid: this._id, + requestID: this.getRequestId(request) }); + + return request; + }, + + replaceReceipt: function(oldReceipt, newReceipt) { + let request = this.createRequest(); + + this.addMessageListeners(["Webapps:ReplaceReceipt:Return:OK", + "Webapps:ReplaceReceipt:Return:KO"]); + + cpmm.sendAsyncMessage("Webapps:ReplaceReceipt", { manifestURL: this.manifestURL, + newReceipt: newReceipt, + oldReceipt: oldReceipt, + oid: this._id, + requestID: this.getRequestId(request) }); + + return request; + }, + uninit: function() { this._onprogress = null; cpmm.sendAsyncMessage("Webapps:UnregisterForMessages", [ @@ -638,6 +681,39 @@ WebappsApplication.prototype = { }); req.resolve(connections); break; + case "Webapps:AddReceipt:Return:OK": + this.removeMessageListeners(["Webapps:AddReceipt:Return:OK", + "Webapps:AddReceipt:Return:KO"]); + this.receipts = msg.receipts; + Services.DOMRequest.fireSuccess(req, null); + break; + case "Webapps:AddReceipt:Return:KO": + this.removeMessageListeners(["Webapps:AddReceipt:Return:OK", + "Webapps:AddReceipt:Return:KO"]); + Services.DOMRequest.fireError(req, msg.error); + break; + case "Webapps:RemoveReceipt:Return:OK": + this.removeMessageListeners(["Webapps:RemoveReceipt:Return:OK", + "Webapps:RemoveReceipt:Return:KO"]); + this.receipts = msg.receipts; + Services.DOMRequest.fireSuccess(req, null); + break; + case "Webapps:RemoveReceipt:Return:KO": + this.removeMessageListeners(["Webapps:RemoveReceipt:Return:OK", + "Webapps:RemoveReceipt:Return:KO"]); + Services.DOMRequest.fireError(req, msg.error); + break; + case "Webapps:ReplaceReceipt:Return:OK": + this.removeMessageListeners(["Webapps:ReplaceReceipt:Return:OK", + "Webapps:ReplaceReceipt:Return:KO"]); + this.receipts = msg.receipts; + Services.DOMRequest.fireSuccess(req, null); + break; + case "Webapps:ReplaceReceipt:Return:KO": + this.removeMessageListeners(["Webapps:ReplaceReceipt:Return:OK", + "Webapps:ReplaceReceipt:Return:KO"]); + Services.DOMRequest.fireError(req, msg.error); + break; } }, diff --git a/dom/apps/src/Webapps.jsm b/dom/apps/src/Webapps.jsm index 0f447224886..fd3d854f419 100755 --- a/dom/apps/src/Webapps.jsm +++ b/dom/apps/src/Webapps.jsm @@ -128,6 +128,7 @@ XPCOMUtils.defineLazyGetter(this, "updateSvc", function() { const STORE_ID_PENDING_PREFIX = "#unknownID#"; this.DOMApplicationRegistry = { + // Path to the webapps.json file where we store the registry data. appsFile: null, webapps: { }, children: [ ], @@ -143,7 +144,8 @@ this.DOMApplicationRegistry = { "Webapps:UnregisterForMessages", "Webapps:CancelDownload", "Webapps:CheckForUpdate", "Webapps:Download", "Webapps:ApplyDownload", - "Webapps:Install:Return:Ack", + "Webapps:Install:Return:Ack", "Webapps:AddReceipt", + "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt", "child-process-shutdown"]; this.frameMessages = ["Webapps:ClearBrowserData"]; @@ -160,87 +162,82 @@ this.DOMApplicationRegistry = { AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this)); this.appsFile = FileUtils.getFile(DIRECTORY_NAME, - ["webapps", "webapps.json"], true); + ["webapps", "webapps.json"], true).path; this.loadAndUpdateApps(); }, // loads the current registry, that could be empty on first run. - // aNext() is called after we load the current webapps list. - loadCurrentRegistry: function loadCurrentRegistry(aNext) { - let file = FileUtils.getFile(DIRECTORY_NAME, ["webapps", "webapps.json"], false); - if (file && file.exists()) { - this._loadJSONAsync(file, (function loadRegistry(aData) { - if (aData) { - this.webapps = aData; - let appDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], false); - for (let id in this.webapps) { - let app = this.webapps[id]; - if (!app) { - delete this.webapps[id]; - continue; - } + loadCurrentRegistry: function() { + return this._loadJSONAsync(this.appsFile).then((aData) => { + if (!aData) { + return; + } - app.id = id; - - // Make sure we have a localId - if (app.localId === undefined) { - app.localId = this._nextLocalId(); - } - - if (app.basePath === undefined) { - app.basePath = appDir.path; - } - - // Default to removable apps. - if (app.removable === undefined) { - app.removable = true; - } - - // Default to a non privileged status. - if (app.appStatus === undefined) { - app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED; - } - - // Default to NO_APP_ID and not in browser. - if (app.installerAppId === undefined) { - app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID; - } - if (app.installerIsBrowser === undefined) { - app.installerIsBrowser = false; - } - - // Default installState to "installed", and reset if we shutdown - // during an update. - if (app.installState === undefined || - app.installState === "updating") { - app.installState = "installed"; - } - - // Default storeId to "" and storeVersion to 0 - if (this.webapps[id].storeId === undefined) { - this.webapps[id].storeId = ""; - } - if (this.webapps[id].storeVersion === undefined) { - this.webapps[id].storeVersion = 0; - } - - // Default role to "". - if (this.webapps[id].role === undefined) { - this.webapps[id].role = ""; - } - - // At startup we can't be downloading, and the $TMP directory - // will be empty so we can't just apply a staged update. - app.downloading = false; - app.readyToApplyDownload = false; - }; + this.webapps = aData; + let appDir = OS.Path.dirname(this.appsFile); + for (let id in this.webapps) { + let app = this.webapps[id]; + if (!app) { + delete this.webapps[id]; + continue; } - aNext(); - }).bind(this)); - } else { - aNext(); - } + + app.id = id; + + // Make sure we have a localId + if (app.localId === undefined) { + app.localId = this._nextLocalId(); + } + + if (app.basePath === undefined) { + app.basePath = appDir; + } + + // Default to removable apps. + if (app.removable === undefined) { + app.removable = true; + } + + // Default to a non privileged status. + if (app.appStatus === undefined) { + app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED; + } + + // Default to NO_APP_ID and not in browser. + if (app.installerAppId === undefined) { + app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID; + } + if (app.installerIsBrowser === undefined) { + app.installerIsBrowser = false; + } + + // Default installState to "installed", and reset if we shutdown + // during an update. + if (app.installState === undefined || + app.installState === "updating") { + app.installState = "installed"; + } + + // Default storeId to "" and storeVersion to 0 + if (this.webapps[id].storeId === undefined) { + this.webapps[id].storeId = ""; + } + if (this.webapps[id].storeVersion === undefined) { + this.webapps[id].storeVersion = 0; + } + + // Default role to "". + if (this.webapps[id].role === undefined) { + this.webapps[id].role = ""; + } + + // At startup we can't be downloading, and the $TMP directory + // will be empty so we can't just apply a staged update. + app.downloading = false; + app.readyToApplyDownload = false; + } + }); }, // Notify we are starting with registering apps. @@ -273,7 +270,7 @@ this.DOMApplicationRegistry = { }, // Registers all the activities and system messages. - registerAppsHandlers: function registerAppsHandlers(aRunUpdate) { + registerAppsHandlers: function(aRunUpdate) { this.notifyAppsRegistryStart(); let ids = []; for (let id in this.webapps) { @@ -285,8 +282,8 @@ this.DOMApplicationRegistry = { // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on // _processManifestForIds so as to not reading the manifests // twice - this._readManifests(ids, (function readCSPs(aResults) { - aResults.forEach(function registerManifest(aResult) { + this._readManifests(ids).then((aResults) => { + aResults.forEach((aResult) => { if (!aResult.manifest) { // If we can't load the manifest, we probably have a corrupted // registry. We delete the app since we can't do anything with it. @@ -299,8 +296,8 @@ this.DOMApplicationRegistry = { if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { app.redirects = this.sanitizeRedirects(aResult.redirects); } - }, this); - }).bind(this)); + }); + }); // Nothing else to do but notifying we're ready. this.notifyAppsRegistryReady(); @@ -313,14 +310,14 @@ this.DOMApplicationRegistry = { } // Create or Update the DataStore for this app - this._readManifests([{ id: aId }], (function(aResult) { + this._readManifests([{ id: aId }]).then((aResult) => { let app = this.webapps[aId]; this.updateDataStore(app.localId, app.origin, app.manifestURL, aResult[0].manifest, app.appStatus); - }).bind(this)); + }); }, - updatePermissionsForApp: function updatePermissionsForApp(aId) { + updatePermissionsForApp: function(aId) { if (!this.webapps[aId]) { return; } @@ -329,7 +326,7 @@ this.DOMApplicationRegistry = { // to cleanup the old ones if needed. // TODO It's not clear what this should do when there are multiple profiles. if (supportUseCurrentProfile()) { - this._readManifests([{ id: aId }], (function(aResult) { + this._readManifests([{ id: aId }]).then((aResult) => { let data = aResult[0]; PermissionsInstaller.installPermissions({ manifest: data.manifest, @@ -338,13 +335,13 @@ this.DOMApplicationRegistry = { }, true, function() { debug("Error installing permissions for " + aId); }); - }).bind(this)); + }); } }, - updateOfflineCacheForApp: function updateOfflineCacheForApp(aId) { + updateOfflineCacheForApp: function(aId) { let app = this.webapps[aId]; - this._readManifests([{ id: aId }], function(aResult) { + this._readManifests([{ id: aId }]).then((aResult) => { let manifest = new ManifestHelper(aResult[0].manifest, app.origin); OfflineCacheInstaller.installCache({ cachePath: app.cachePath, @@ -413,8 +410,7 @@ this.DOMApplicationRegistry = { app.installState = "installed"; app.cachePath = app.basePath; - app.basePath = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true) - .path; + app.basePath = OS.Path.dirname(this.appsFile); if (!isPackage) { return; @@ -484,63 +480,62 @@ this.DOMApplicationRegistry = { // new core apps registry. // c. for all apps in the new core registry, install them if they are not // yet in the current registry, and run installPermissions() - installSystemApps: function installSystemApps(aNext) { - let file; - try { - file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false); - } catch(e) { } + installSystemApps: function() { + return Task.spawn(function() { + let file; + try { + file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false); + } catch(e) { } + + if (!file || !file.exists()) { + return; + } - if (file && file.exists()) { // a - this._loadJSONAsync(file, (function loadCoreRegistry(aData) { - if (!aData) { - aNext(); - return; - } + let data = yield this._loadJSONAsync(file.path); + if (!data) { + return; + } - // b : core apps are not removable. - for (let id in this.webapps) { - if (id in aData || this.webapps[id].removable) - continue; - // Remove the permissions, cookies and private data for this app. - let localId = this.webapps[id].localId; - let permMgr = Cc["@mozilla.org/permissionmanager;1"] - .getService(Ci.nsIPermissionManager); - permMgr.removePermissionsForApp(localId, false); - Services.cookies.removeCookiesForApp(localId, false); - this._clearPrivateData(localId, false); - delete this.webapps[id]; - } + // b : core apps are not removable. + for (let id in this.webapps) { + if (id in data || this.webapps[id].removable) + continue; + // Remove the permissions, cookies and private data for this app. + let localId = this.webapps[id].localId; + let permMgr = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + permMgr.removePermissionsForApp(localId, false); + Services.cookies.removeCookiesForApp(localId, false); + this._clearPrivateData(localId, false); + delete this.webapps[id]; + } - let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); - // c - for (let id in aData) { - // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org) - // Use that property to check if they are new or not. - if (!(id in this.webapps)) { - this.webapps[id] = aData[id]; - this.webapps[id].basePath = appDir.path; + let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); + // c + for (let id in data) { + // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org) + // Use that property to check if they are new or not. + if (!(id in this.webapps)) { + this.webapps[id] = data[id]; + this.webapps[id].basePath = appDir.path; - this.webapps[id].id = id; + this.webapps[id].id = id; - // Create a new localId. - this.webapps[id].localId = this._nextLocalId(); + // Create a new localId. + this.webapps[id].localId = this._nextLocalId(); - // Core apps are not removable. - if (this.webapps[id].removable === undefined) { - this.webapps[id].removable = false; - } + // Core apps are not removable. + if (this.webapps[id].removable === undefined) { + this.webapps[id].removable = false; } } - aNext(); - }).bind(this)); - } else { - aNext(); - } + } + }.bind(this)).then(null, Cu.reportError); }, #ifdef MOZ_WIDGET_GONK - fixIndexedDb: function fixIndexedDb() { + fixIndexedDb: function() { debug("Fixing indexedDb folder names"); let idbDir = FileUtils.getDir("indexedDBPDir", ["indexedDB"]); @@ -567,17 +562,23 @@ this.DOMApplicationRegistry = { }, #endif - loadAndUpdateApps: function loadAndUpdateApps() { - let runUpdate = AppsUtils.isFirstRun(Services.prefs); + loadAndUpdateApps: function() { + return Task.spawn(function() { + let runUpdate = AppsUtils.isFirstRun(Services.prefs); #ifdef MOZ_WIDGET_GONK - if (runUpdate) { - this.fixIndexedDb(); - } + if (runUpdate) { + this.fixIndexedDb(); + } #endif - let onAppsLoaded = (function onAppsLoaded() { + yield this.loadCurrentRegistry(); + if (runUpdate) { +#ifdef MOZ_WIDGET_GONK + yield this.installSystemApps(); +#endif + // At first run, install preloaded apps and set up their permissions. for (let id in this.webapps) { this.installPreinstalledApp(id); @@ -599,19 +600,7 @@ this.DOMApplicationRegistry = { } this.registerAppsHandlers(runUpdate); - }).bind(this); - - this.loadCurrentRegistry((function() { -#ifdef MOZ_WIDGET_GONK - // if first run, merge the system apps. - if (runUpdate) - this.installSystemApps(onAppsLoaded); - else - onAppsLoaded(); -#else - onAppsLoaded(); -#endif - }).bind(this)); + }.bind(this)).then(null, Cu.reportError); }, updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) { @@ -929,9 +918,9 @@ this.DOMApplicationRegistry = { }, _processManifestForIds: function(aIds, aRunUpdate) { - this._readManifests(aIds, (function registerManifests(aResults) { + this._readManifests(aIds).then((aResults) => { let appsToRegister = []; - aResults.forEach(function registerManifest(aResult) { + aResults.forEach((aResult) => { let app = this.webapps[aResult.id]; let manifest = aResult.manifest; if (!manifest) { @@ -949,9 +938,9 @@ this.DOMApplicationRegistry = { this._registerSystemMessages(manifest, app); this._registerInterAppConnections(manifest, app); appsToRegister.push({ manifest: manifest, app: app }); - }, this); + }); this._registerActivitiesForApps(appsToRegister, aRunUpdate); - }).bind(this)); + }); }, observe: function(aSubject, aTopic, aData) { @@ -968,45 +957,46 @@ this.DOMApplicationRegistry = { } }, - _loadJSONAsync: function(aFile, aCallback) { + _loadJSONAsync: function(aPath) { + let deferred = Promise.defer(); + try { - let channel = NetUtil.newChannel(aFile); + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(aPath); + let channel = NetUtil.newChannel(file); channel.contentType = "application/json"; NetUtil.asyncFetch(channel, function(aStream, aResult) { if (!Components.isSuccessCode(aResult)) { Cu.reportError("DOMApplicationRegistry: Could not read from json file " - + aFile.path); - if (aCallback) - aCallback(null); - return; + + aPath); + deferred.resolve(null); } - // Read json file into a string - let data = null; try { // Obtain a converter to read from a UTF-8 encoded input stream. let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; - data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream, + // Read json file into a string + let data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream, aStream.available()) || "")); aStream.close(); - if (aCallback) - aCallback(data); + + deferred.resolve(data); } catch (ex) { Cu.reportError("DOMApplicationRegistry: Could not parse JSON: " + - aFile.path + " " + ex + "\n" + ex.stack); - if (aCallback) - aCallback(null); + aPath + " " + ex + "\n" + ex.stack); + deferred.resolve(null); } }); } catch (ex) { Cu.reportError("DOMApplicationRegistry: Could not read from " + - aFile.path + " : " + ex + "\n" + ex.stack); - if (aCallback) - aCallback(null); + aPath + " : " + ex + "\n" + ex.stack); + deferred.resolve(null); } + + return deferred.promise; }, addMessageListener: function(aMsgNames, aApp, aMm) { @@ -1186,6 +1176,15 @@ this.DOMApplicationRegistry = { case "Webapps:Install:Return:Ack": this.onInstallSuccessAck(msg.manifestURL); break; + case "Webapps:AddReceipt": + this.addReceipt(msg, mm); + break; + case "Webapps:RemoveReceipt": + this.removeReceipt(msg, mm); + break; + case "Webapps:ReplaceReceipt": + this.replaceReceipt(msg, mm); + break; } }, @@ -1215,23 +1214,13 @@ this.DOMApplicationRegistry = { return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); }, - _writeFile: function _writeFile(aFile, aData, aCallback) { - debug("Saving " + aFile.path); - // Initialize the file output stream. - let ostream = FileUtils.openSafeFileOutputStream(aFile); + _writeFile: function(aPath, aData) { + debug("Saving " + aPath); - // Obtain a converter to convert our data to a UTF-8 encoded input stream. - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - // Asynchronously copy the data to the file. - let istream = converter.convertToInputStream(aData); - NetUtil.asyncCopy(istream, ostream, function(rc) { - if (aCallback) { - aCallback(); - } - }); + return OS.File.writeAtomic(aPath, + new TextEncoder().encode(aData), + { tmpPath: aPath + ".tmp" }) + .then(null, Cu.reportError); }, doLaunch: function (aData, aMm) { @@ -1305,7 +1294,7 @@ this.DOMApplicationRegistry = { return; } - this._saveApps((function() { + this._saveApps().then(() => { this.broadcastMessage("Webapps:UpdateState", { app: { progress: 0, @@ -1319,7 +1308,7 @@ this.DOMApplicationRegistry = { eventType: "downloaderror", manifestURL: app.manifestURL }); - }).bind(this)); + }); AppDownloadManager.remove(aManifestURL); }, @@ -1371,7 +1360,7 @@ this.DOMApplicationRegistry = { if (!file.exists()) { // This is a hosted app, let's check if it has an appcache // and download it. - this._readManifests([{ id: id }], (function readManifest(aResults) { + this._readManifests([{ id: id }]).then((aResults) => { let jsonManifest = aResults[0].manifest; let manifest = new ManifestHelper(jsonManifest, app.origin); @@ -1383,24 +1372,24 @@ this.DOMApplicationRegistry = { // downloaded event. debug("No appcache found, sending 'downloaded' for " + aManifestURL); app.downloadAvailable = false; - DOMApplicationRegistry._saveApps(function() { - DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { + this._saveApps().then(() => { + this.broadcastMessage("Webapps:UpdateState", { app: app, manifest: jsonManifest, manifestURL: aManifestURL }); - DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { + this.broadcastMessage("Webapps:FireEvent", { eventType: "downloadsuccess", manifestURL: aManifestURL }); }); } - }).bind(this)); + }); return; } - this._loadJSONAsync(file, (function(aJSON) { + this._loadJSONAsync(file.path).then((aJSON) => { if (!aJSON) { debug("startDownload: No update manifest found at " + file.path + " " + aManifestURL); @@ -1416,14 +1405,10 @@ this.DOMApplicationRegistry = { }, isUpdate).then(function([aId, aManifest]) { // Success! Keep the zip in of TmpD, we'll move it out when // applyDownload() will be called. - let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true); - // Save the manifest in TmpD also - let manFile = tmpDir.clone(); - manFile.append("manifest.webapp"); - DOMApplicationRegistry._writeFile(manFile, - JSON.stringify(aManifest), - function() { }); + let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", aId, + "manifest.webapp"); + DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest)); app = DOMApplicationRegistry.webapps[aId]; // Set state and fire events. @@ -1431,7 +1416,7 @@ this.DOMApplicationRegistry = { app.downloadAvailable = false; app.readyToApplyDownload = true; app.updateTime = Date.now(); - DOMApplicationRegistry._saveApps(function() { + DOMApplicationRegistry._saveApps().then(() => { DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { app: app, manifestURL: aManifestURL @@ -1446,7 +1431,7 @@ this.DOMApplicationRegistry = { } }); }); - }).bind(this)); + }); }, applyDownload: function applyDownload(aManifestURL) { @@ -1458,7 +1443,7 @@ this.DOMApplicationRegistry = { } // We need to get the old manifest to unregister web activities. - this.getManifestFor(aManifestURL, (function(aOldManifest) { + this.getManifestFor(aManifestURL).then((aOldManifest) => { // Move the application.zip and manifest.webapp files out of TmpD let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true); let manFile = tmpDir.clone(); @@ -1496,7 +1481,7 @@ this.DOMApplicationRegistry = { Services.obs.notifyObservers(zipFile, "flush-cache-entry", null); // Get the manifest, and set properties. - this.getManifestFor(aManifestURL, (function(aData) { + this.getManifestFor(aManifestURL).then((aData) => { app.downloading = false; app.downloadAvailable = false; app.downloadSize = 0; @@ -1513,7 +1498,7 @@ this.DOMApplicationRegistry = { delete app.retryingDownload; - this._saveApps((function() { + this._saveApps().then(() => { // Update the handlers and permissions for this app. this.updateAppHandlers(aOldManifest, aData, app); if (supportUseCurrentProfile()) { @@ -1534,9 +1519,9 @@ this.DOMApplicationRegistry = { eventType: "downloadapplied", manifestURL: app.manifestURL }); - }).bind(this)); - }).bind(this)); - }).bind(this)); + }); + }); + }); }, startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) { @@ -1561,7 +1546,7 @@ this.DOMApplicationRegistry = { // starting the app download/update. aApp.downloading = true; aApp.progress = 0; - DOMApplicationRegistry._saveApps((function() { + DOMApplicationRegistry._saveApps().then(() => { DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { app: { downloading: true, @@ -1585,7 +1570,7 @@ this.DOMApplicationRegistry = { cacheUpdate.addObserver(new AppcacheObserver(aApp), false); - }).bind(this)); + }); }, // Returns the MD5 hash of the manifest. @@ -1637,10 +1622,9 @@ this.DOMApplicationRegistry = { } // Store the new update manifest. - let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); - let manFile = dir.clone(); - manFile.append("staged-update.webapp"); - this._writeFile(manFile, JSON.stringify(aManifest), function() { }); + let dir = this._getAppDir(id).path; + let manFile = OS.Path.join(dir, "staged-update.webapp"); + this._writeFile(manFile, JSON.stringify(aManifest)); let manifest = new ManifestHelper(aManifest, app.manifestURL); // A package is available: set downloadAvailable to fire the matching @@ -1648,12 +1632,12 @@ this.DOMApplicationRegistry = { app.downloadAvailable = true; app.downloadSize = manifest.size; app.updateManifest = aManifest; - DOMApplicationRegistry._saveApps(function() { - DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { + this._saveApps().then(() => { + this.broadcastMessage("Webapps:UpdateState", { app: app, manifestURL: app.manifestURL }); - DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { + this.broadcastMessage("Webapps:FireEvent", { eventType: "downloadavailable", manifestURL: app.manifestURL, requestID: aData.requestID @@ -1682,10 +1666,9 @@ this.DOMApplicationRegistry = { this.updateAppHandlers(aOldManifest, aNewManifest, app); // Store the new manifest. - let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); - let manFile = dir.clone(); - manFile.append("manifest.webapp"); - this._writeFile(manFile, JSON.stringify(aNewManifest), function() { }); + let dir = this._getAppDir(id).path; + let manFile = OS.Path.join(dir, "manifest.webapp"); + this._writeFile(manFile, JSON.stringify(aNewManifest)); manifest = new ManifestHelper(aNewManifest, app.origin); if (supportUseCurrentProfile()) { @@ -1710,7 +1693,7 @@ this.DOMApplicationRegistry = { // Update the registry. this.webapps[id] = app; - this._saveApps(function() { + this._saveApps().then(() => { let reg = DOMApplicationRegistry; if (!manifest.appcache_path) { reg.broadcastMessage("Webapps:UpdateState", { @@ -1734,7 +1717,7 @@ this.DOMApplicationRegistry = { aTopic == "offline-cache-update-available" ? "downloadavailable" : "downloadapplied"; app.downloadAvailable = (eventType == "downloadavailable"); - reg._saveApps(function() { + reg._saveApps().then(() => { reg.broadcastMessage("Webapps:UpdateState", { app: app, manifest: app.manifest, @@ -1795,7 +1778,7 @@ this.DOMApplicationRegistry = { } // We need the manifest to check if we have an appcache. - this._readManifests([{ id: id }], function(aResult) { + this._readManifests([{ id: id }]).then((aResult) => { let manifest = aResult[0].manifest; if (!manifest.appcache_path) { aData.error = "NOT_UPDATABLE"; @@ -1812,12 +1795,12 @@ this.DOMApplicationRegistry = { app.manifestURL + " - event is " + aTopic); if (aTopic == "offline-cache-update-available") { app.downloadAvailable = true; - this._saveApps(function() { - DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { + this._saveApps().then(() => { + this.broadcastMessage("Webapps:UpdateState", { app: app, manifestURL: app.manifestURL }); - DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { + this.broadcastMessage("Webapps:FireEvent", { eventType: "downloadavailable", manifestURL: app.manifestURL, requestID: aData.requestID @@ -1878,7 +1861,7 @@ this.DOMApplicationRegistry = { if (oldHash != hash) { updatePackagedApp.call(this, manifest); } else { - this._saveApps(function() { + this._saveApps().then(() => { // Like if we got a 304, just send a 'downloadapplied' // or downloadavailable event. let eventType = app.downloadAvailable ? "downloadavailable" @@ -1905,7 +1888,7 @@ this.DOMApplicationRegistry = { // The manifest has not changed. if (isPackage) { app.lastCheckedUpdate = Date.now(); - this._saveApps(function() { + this._saveApps().then(() => { // If the app is a packaged app, we just send a 'downloadapplied' // or downloadavailable event. let eventType = app.downloadAvailable ? "downloadavailable" @@ -1959,7 +1942,7 @@ this.DOMApplicationRegistry = { } // Read the current app manifest file - this._readManifests([{ id: id }], (function(aResult) { + this._readManifests([{ id: id }]).then((aResult) => { let extraHeaders = []; #ifdef MOZ_WIDGET_GONK let pingManifestURL; @@ -1975,7 +1958,7 @@ this.DOMApplicationRegistry = { } #endif doRequest.call(this, aResult[0].manifest, extraHeaders); - }).bind(this)); + }); }, // Creates a nsILoadContext object with a given appId and isBrowser flag. @@ -2312,8 +2295,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } appObject.localId = aLocalId; - appObject.basePath = - FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true).path; + appObject.basePath = OS.Path.dirname(this.appsFile); appObject.name = aManifest.name; appObject.csp = aManifest.csp || ""; appObject.role = aManifest.role || ""; @@ -2325,13 +2307,13 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, _writeManifestFile: function(aId, aIsPackage, aJsonManifest) { debug("_writeManifestFile"); - let dir = this._getAppDir(aId); - let manFile = dir.clone(); // For packaged apps, keep the update manifest distinct from the app manifest. let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp"; - manFile.append(manifestName); - this._writeFile(manFile, JSON.stringify(aJsonManifest), function() { }); + + let dir = this._getAppDir(aId).path; + let manFile = OS.Path.join(dir, manifestName); + this._writeFile(manFile, JSON.stringify(aJsonManifest)); }, confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) { @@ -2404,7 +2386,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, // We notify about the successful installation via mgmt.oninstall and the // corresponging DOMRequest.onsuccess event as soon as the app is properly // saved in the registry. - this._saveApps((function() { + this._saveApps().then(() => { this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject }); if (aData.isPackage && aData.autoInstall) { // Skip directly to onInstallSuccessAck, since there isn't @@ -2419,7 +2401,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } Services.obs.notifyObservers(null, "webapps-installed", JSON.stringify({ manifestURL: app.manifestURL })); - }).bind(this)); + }); if (!aData.isPackage) { this.updateAppHandlers(null, app.manifest, app); @@ -2489,14 +2471,13 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } catch(e) { } // Save the manifest - let manFile = dir.clone(); - manFile.append("manifest.webapp"); - this._writeFile(manFile, JSON.stringify(aManifest), function() { }); + let manFile = OS.Path.join(dir.path, "manifest.webapp"); + this._writeFile(manFile, JSON.stringify(aManifest)); // Set state and fire events. app.installState = "installed"; app.downloading = false; app.downloadAvailable = false; - this._saveApps((function() { + this._saveApps().then(() => { this.updateAppHandlers(null, aManifest, aNewApp); this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp }); Services.obs.notifyObservers(null, "webapps-installed", @@ -2526,7 +2507,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, if (aInstallSuccessCallback) { aInstallSuccessCallback(aManifest, zipFile.path); } - }).bind(this)); + }); }, _nextLocalId: function() { @@ -2554,9 +2535,8 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, return uuidGenerator.generateUUID().toString(); }, - _saveApps: function(aCallback) { - this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2), - aCallback); + _saveApps: function() { + return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2)); }, /** @@ -2565,43 +2545,38 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, _manifestCache: {}, - _readManifests: function(aData, aFinalCallback, aIndex) { - if (!aData.length) { - aFinalCallback(aData); - return; - } + _readManifests: function(aData) { + return Task.spawn(function*() { + if (!aData.length) { + return aData; + } - let index = aIndex || 0; - let id = aData[index].id; + for (let elem of aData) { + let id = elem.id; - // Use the cached manifest instead of reading the file again from disk. - if (id in this._manifestCache) { - aData[index].manifest = this._manifestCache[id]; - if (index == aData.length - 1) - aFinalCallback(aData); - else - this._readManifests(aData, aFinalCallback, index + 1); - return; - } + if (!this._manifestCache[id]) { + // the manifest file used to be named manifest.json, so fallback on this. + let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath() + ? "coreAppsDir" : DIRECTORY_NAME; - // the manifest file used to be named manifest.json, so fallback on this. - let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath() - ? "coreAppsDir" : DIRECTORY_NAME; - let file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.webapp"], true); - if (!file.exists()) { - file = FileUtils.getFile(baseDir, ["webapps", id, "update.webapp"], true); - } - if (!file.exists()) { - file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.json"], true); - } + let file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.webapp"], true); - this._loadJSONAsync(file, (function(aJSON) { - aData[index].manifest = this._manifestCache[id] = aJSON; - if (index == aData.length - 1) - aFinalCallback(aData); - else - this._readManifests(aData, aFinalCallback, index + 1); - }).bind(this)); + if (!file.exists()) { + file = FileUtils.getFile(baseDir, ["webapps", id, "update.webapp"], true); + } + + if (!file.exists()) { + file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.json"], true); + } + + this._manifestCache[id] = yield this._loadJSONAsync(file.path); + } + + elem.manifest = this._manifestCache[id]; + } + + return aData; + }.bind(this)).then(null, Cu.reportError); }, downloadPackage: function(aManifest, aNewApp, aIsUpdate, aOnSuccess) { @@ -2959,7 +2934,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } // Save the updated registry, and cleanup the tmp directory. - this._saveApps((function() { + this._saveApps().then(() => { this.broadcastMessage("Webapps:UpdateState", { app: aOldApp, manifestURL: aNewApp.manifestURL @@ -2968,7 +2943,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, manifestURL: aNewApp.manifestURL, eventType: ["downloadsuccess", "downloadapplied"] }); - }).bind(this)); + }); let file = FileUtils.getFile("TmpD", ["webapps", aId], false); if (file && file.exists()) { file.remove(true); @@ -3342,7 +3317,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, delete aOldApp.staged; } - this._saveApps((function() { + this._saveApps().then(() => { this.broadcastMessage("Webapps:UpdateState", { app: aOldApp, error: aError, @@ -3352,7 +3327,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, eventType: "downloaderror", manifestURL: aNewApp.manifestURL }); - }).bind(this)); + }); AppDownloadManager.remove(aNewApp.manifestURL); }, @@ -3405,9 +3380,9 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(appClone)); if (supportSystemMessages()) { - this._readManifests([{ id: id }], (function unregisterManifest(aResult) { + this._readManifests([{ id: id }]).then((aResult) => { this._unregisterActivities(aResult[0].manifest, app); - }).bind(this)); + }); } let dir = this._getAppDir(id); @@ -3417,7 +3392,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, delete this.webapps[id]; - this._saveApps((function() { + this._saveApps().then(() => { this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", appClone); // Catch exception on callback call to ensure notifying observers after try { @@ -3429,7 +3404,7 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, ex + "\n" + ex.stack); } this.broadcastMessage("Webapps:RemoveApp", { id: id }); - }).bind(this)); + }); }, getSelf: function(aData, aMm) { @@ -3459,11 +3434,11 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, return; } - this._readManifests(tmp, (function(aResult) { + this._readManifests(tmp).then((aResult) => { for (let i = 0; i < aResult.length; i++) aData.apps[i].manifest = aResult[i].manifest; aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); - }).bind(this)); + }); }, checkInstalled: function(aData, aMm) { @@ -3479,13 +3454,13 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } } - this._readManifests(tmp, (function(aResult) { + this._readManifests(tmp).then((aResult) => { for (let i = 0; i < aResult.length; i++) { aData.app.manifest = aResult[i].manifest; break; } aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", aData); - }).bind(this)); + }); }, getInstalled: function(aData, aMm) { @@ -3500,11 +3475,11 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } } - this._readManifests(tmp, (function(aResult) { + this._readManifests(tmp).then((aResult) => { for (let i = 0; i < aResult.length; i++) aData.apps[i].manifest = aResult[i].manifest; aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", aData); - }).bind(this)); + }); }, getNotInstalled: function(aData, aMm) { @@ -3518,11 +3493,11 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, } } - this._readManifests(tmp, (function(aResult) { + this._readManifests(tmp).then((aResult) => { for (let i = 0; i < aResult.length; i++) aData.apps[i].manifest = aResult[i].manifest; aMm.sendAsyncMessage("Webapps:GetNotInstalled:Return:OK", aData); - }).bind(this)); + }); }, doGetAll: function(aData, aMm) { @@ -3546,26 +3521,209 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, tmp.push({ id: id }); } - this._readManifests(tmp, (function(aResult) { + this._readManifests(tmp).then((aResult) => { for (let i = 0; i < aResult.length; i++) apps[i].manifest = aResult[i].manifest; aCallback(apps); - }).bind(this)); + }); }, - getManifestFor: function(aManifestURL, aCallback) { - if (!aCallback) - return; + /* Check if |data| is actually a receipt */ + isReceipt: function(data) { + try { + // The receipt data shouldn't be too big (allow up to 1 MiB of data) + const MAX_RECEIPT_SIZE = 1048576; - let id = this._appIdForManifestURL(aManifestURL); - let app = this.webapps[id]; - if (!id || (app.installState == "pending" && !app.retryingDownload)) { - aCallback(null); + if (data.length > MAX_RECEIPT_SIZE) { + return "RECEIPT_TOO_BIG"; + } + + // Marketplace receipts are JWK + "~" + JWT + // Other receipts may contain only the JWT + let receiptParts = data.split('~'); + let jwtData = null; + if (receiptParts.length == 2) { + jwtData = receiptParts[1]; + } else { + jwtData = receiptParts[0]; + } + + let segments = jwtData.split('.'); + if (segments.length != 3) { + return "INVALID_SEGMENTS_NUMBER"; + } + + // We need to translate the base64 alphabet used in JWT to our base64 alphabet + // before calling atob. + let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+') + .replace(/_/g, '/'))); + if (!decodedReceipt) { + return "INVALID_RECEIPT_ENCODING"; + } + + // Required values for a receipt + if (!decodedReceipt.typ) { + return "RECEIPT_TYPE_REQUIRED"; + } + if (!decodedReceipt.product) { + return "RECEIPT_PRODUCT_REQUIRED"; + } + if (!decodedReceipt.user) { + return "RECEIPT_USER_REQUIRED"; + } + if (!decodedReceipt.iss) { + return "RECEIPT_ISS_REQUIRED"; + } + if (!decodedReceipt.nbf) { + return "RECEIPT_NBF_REQUIRED"; + } + if (!decodedReceipt.iat) { + return "RECEIPT_IAT_REQUIRED"; + } + + let allowedTypes = [ "purchase-receipt", "developer-receipt", + "reviewer-receipt", "test-receipt" ]; + if (allowedTypes.indexOf(decodedReceipt.typ) < 0) { + return "RECEIPT_TYPE_UNSUPPORTED"; + } + } catch (e) { + return "RECEIPT_ERROR"; + } + + return null; + }, + + addReceipt: function(aData, aMm) { + debug("addReceipt " + aData.manifestURL); + + let receipt = aData.receipt; + + if (!receipt) { + aData.error = "INVALID_PARAMETERS"; + aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); return; } - this._readManifests([{ id: id }], function(aResult) { - aCallback(aResult[0].manifest); + let error = this.isReceipt(receipt); + if (error) { + aData.error = error; + aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); + return; + } + + let id = this._appIdForManifestURL(aData.manifestURL); + let app = this.webapps[id]; + + if (!app.receipts) { + app.receipts = []; + } else if (app.receipts.length > 500) { + aData.error = "TOO_MANY_RECEIPTS"; + aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); + return; + } + + let index = app.receipts.indexOf(receipt); + if (index >= 0) { + aData.error = "RECEIPT_ALREADY_EXISTS"; + aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); + return; + } + + app.receipts.push(receipt); + + this._saveApps().then(() => { + aData.receipts = app.receipts; + aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData); + }); + }, + + removeReceipt: function(aData, aMm) { + debug("removeReceipt " + aData.manifestURL); + + let receipt = aData.receipt; + + if (!receipt) { + aData.error = "INVALID_PARAMETERS"; + aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); + return; + } + + let id = this._appIdForManifestURL(aData.manifestURL); + let app = this.webapps[id]; + + if (!app.receipts) { + aData.error = "NO_SUCH_RECEIPT"; + aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); + return; + } + + let index = app.receipts.indexOf(receipt); + if (index == -1) { + aData.error = "NO_SUCH_RECEIPT"; + aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); + return; + } + + app.receipts.splice(index, 1); + + this._saveApps().then(() => { + aData.receipts = app.receipts; + aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData); + }); + }, + + replaceReceipt: function(aData, aMm) { + debug("replaceReceipt " + aData.manifestURL); + + let oldReceipt = aData.oldReceipt; + let newReceipt = aData.newReceipt; + + if (!oldReceipt || !newReceipt) { + aData.error = "INVALID_PARAMETERS"; + aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); + return; + } + + let error = this.isReceipt(newReceipt); + if (error) { + aData.error = error; + aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); + return; + } + + let id = this._appIdForManifestURL(aData.manifestURL); + let app = this.webapps[id]; + + if (!app.receipts) { + aData.error = "NO_SUCH_RECEIPT"; + aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); + return; + } + + let oldIndex = app.receipts.indexOf(oldReceipt); + if (oldIndex == -1) { + aData.error = "NO_SUCH_RECEIPT"; + aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); + return; + } + + app.receipts[oldIndex] = newReceipt; + + this._saveApps().then(() => { + aData.receipts = app.receipts; + aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData); + }); + }, + + getManifestFor: function(aManifestURL) { + let id = this._appIdForManifestURL(aManifestURL); + let app = this.webapps[id]; + if (!id || (app.installState == "pending" && !app.retryingDownload)) { + return Promise.resolve(null); + } + + return this._readManifests([{ id: id }]).then((aResult) => { + return aResult[0].manifest; }); }, @@ -3599,8 +3757,8 @@ onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, return AppsUtils.getCoreAppsBasePath(); }, - getWebAppsBasePath: function getWebAppsBasePath() { - return FileUtils.getDir(DIRECTORY_NAME, ["webapps"], false).path; + getWebAppsBasePath: function() { + return OS.Path.dirname(this.appsFile); }, _isLaunchable: function(aApp) { diff --git a/dom/apps/tests/mochitest.ini b/dom/apps/tests/mochitest.ini index 8f04c4188b7..45a251a25ee 100644 --- a/dom/apps/tests/mochitest.ini +++ b/dom/apps/tests/mochitest.ini @@ -15,3 +15,4 @@ support-files = [test_packaged_app_install.html] [test_packaged_app_update.html] [test_uninstall_errors.html] +[test_receipt_operations.html] diff --git a/dom/apps/tests/test_receipt_operations.html b/dom/apps/tests/test_receipt_operations.html new file mode 100644 index 00000000000..fdc460476df --- /dev/null +++ b/dom/apps/tests/test_receipt_operations.html @@ -0,0 +1,250 @@ + + + + + Test for Bug {757226} Implement mozApps app.replaceReceipt + + + + + + +Mozilla Bug {757226} +

+ +
+
+
+ + \ No newline at end of file diff --git a/dom/devicestorage/nsDeviceStorage.cpp b/dom/devicestorage/nsDeviceStorage.cpp index 4485dec7cdc..b4bf85e1ff3 100644 --- a/dom/devicestorage/nsDeviceStorage.cpp +++ b/dom/devicestorage/nsDeviceStorage.cpp @@ -894,6 +894,16 @@ DeviceStorageFile::AppendRelativePath(const nsAString& aPath) { if (!mFile) { return; } + if (!IsSafePath(aPath)) { + // All of the APIs (in the child) do checks to verify that the path is + // valid and return PERMISSION_DENIED if a non-safe path is entered. + // This check is done in the parent and prevents a compromised + // child from bypassing the check. It shouldn't be possible for this + // code path to be taken with a non-compromised child. + NS_WARNING("Unsafe path detected - ignoring"); + NS_WARNING(NS_LossyConvertUTF16toASCII(aPath).get()); + return; + } #if defined(XP_WIN) // replace forward slashes with backslashes, // since nsLocalFileWin chokes on them diff --git a/dom/interfaces/apps/nsIDOMApplicationRegistry.idl b/dom/interfaces/apps/nsIDOMApplicationRegistry.idl index f53dba271cf..b6088ecfe6f 100644 --- a/dom/interfaces/apps/nsIDOMApplicationRegistry.idl +++ b/dom/interfaces/apps/nsIDOMApplicationRegistry.idl @@ -7,7 +7,7 @@ interface nsIDOMDOMRequest; -[scriptable, uuid(4081390c-08cf-11e3-9200-b3c0a8744b20)] +[scriptable, uuid(f8cb08ed-588e-465f-b2b3-a4b0afde711a)] interface mozIDOMApplication : nsISupports { readonly attribute jsval manifest; @@ -100,6 +100,11 @@ interface mozIDOMApplication : nsISupports [optional] in jsval rules); // nsISupports is a Promise. nsISupports getConnections(); // nsISupports is a Promise. + + /* Receipts handling functions */ + nsIDOMDOMRequest addReceipt(in DOMString receipt); + nsIDOMDOMRequest removeReceipt(in DOMString receipt); + nsIDOMDOMRequest replaceReceipt(in DOMString oldReceipt, in DOMString newReceipt); }; [scriptable, uuid(cf742022-5ba3-11e2-868f-03310341b006)] diff --git a/toolkit/devtools/server/actors/webapps.js b/toolkit/devtools/server/actors/webapps.js index 283a8edfc30..b565bf44742 100644 --- a/toolkit/devtools/server/actors/webapps.js +++ b/toolkit/devtools/server/actors/webapps.js @@ -165,12 +165,12 @@ WebappsActor.prototype = { reg.webapps[aId] = aApp; reg.updatePermissionsForApp(aId); - reg._readManifests([{ id: aId }], function(aResult) { + reg._readManifests([{ id: aId }]).then((aResult) => { let manifest = aResult[0].manifest; aApp.name = manifest.name; reg.updateAppHandlers(null, manifest, aApp); - reg._saveApps(function() { + reg._saveApps().then(() => { aApp.manifest = manifest; // Needed to evict manifest cache on content side @@ -264,17 +264,8 @@ WebappsActor.prototype = { if (aManifest) { return promise.resolve(aManifest); } else { - let deferred = promise.defer(); - let manFile = aDir.clone(); - manFile.append("manifest.webapp"); - DOMApplicationRegistry._loadJSONAsync(manFile, function(aManifest) { - if (!aManifest) { - deferred.reject("Error parsing manifest.webapp."); - } else { - deferred.resolve(aManifest); - } - }); - return deferred.promise; + let manFile = OS.Path.join(aDir.path, "manifest.webapp"); + return AppsUtils.loadJSONAsync(manFile); } } function checkSideloading(aManifest) { @@ -285,13 +276,10 @@ WebappsActor.prototype = { // The destination directory for this app. let installDir = DOMApplicationRegistry._getAppDir(aId); if (aManifest) { - let deferred = promise.defer(); - let manFile = installDir.clone(); - manFile.append("manifest.webapp"); - DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest), function () { - deferred.resolve(aAppType); + let manFile = OS.Path.join(installDir.path, "manifest.webapp"); + return DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest)).then(() => { + return aAppType; }); - return deferred.promise; } else { let manFile = aDir.clone(); manFile.append("manifest.webapp"); @@ -304,21 +292,16 @@ WebappsActor.prototype = { return { metadata: aMetadata, appType: aAppType }; } // Read the origin and manifest url from metadata.json - let deferred = promise.defer(); - let metaFile = aDir.clone(); - metaFile.append("metadata.json"); - DOMApplicationRegistry._loadJSONAsync(metaFile, function(aMetadata) { + let metaFile = OS.Path.join(aDir.path, "metadata.json"); + return AppsUtils.loadJSONAsync(metaFile).then((aMetadata) => { if (!aMetadata) { - deferred.reject("Error parsing metadata.json."); - return; + throw("Error parsing metadata.json."); } if (!aMetadata.origin) { - deferred.reject("Missing 'origin' property in metadata.json"); - return; + throw("Missing 'origin' property in metadata.json"); } - deferred.resolve({ metadata: aMetadata, appType: aAppType }); + return { metadata: aMetadata, appType: aAppType }; }); - return deferred.promise; } let runnable = { run: function run() { @@ -645,7 +628,7 @@ WebappsActor.prototype = { let reg = DOMApplicationRegistry; let id = reg._appIdForManifestURL(aManifestURL); - reg._readManifests([{ id: id }], function (aResults) { + reg._readManifests([{ id: id }]).then((aResults) => { deferred.resolve(aResults[0].manifest); });