author | Marco Castelluccio <mar.castelluccio@studenti.unina.it> |
Sat, 15 Mar 2014 14:37:37 -0700 | |
changeset 173794 | e9bff15a8d19bed540510b8230f71a1f10cb4a55 |
parent 173793 | 5a675f0c49fb20db22e4592d79ff635051ffe1cb |
child 173795 | c8c75db14c4ef7ab40df23e3c59bb028b0faf60c |
push id | 26420 |
push user | khuey@mozilla.com |
push date | Sun, 16 Mar 2014 00:40:14 +0000 |
treeherder | mozilla-central@e182de48f628 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | myk, fabrice |
bugs | 898647 |
milestone | 30.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -33,18 +33,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "webappsUI", - "resource:///modules/webappsUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", + "resource:///modules/WebappManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", "resource://gre/modules/PageThumbs.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader", @@ -463,17 +463,17 @@ BrowserGlue.prototype = { // prefs are applied in _onAppDefaults() this._distributionCustomizer.applyCustomizations(); // handle any UI migration this._migrateUI(); this._syncSearchEngines(); - webappsUI.init(); + WebappManager.init(); PageThumbs.init(); NewTabUtils.init(); BrowserNewTabPreloader.init(); SignInToWebsiteUX.init(); PdfJs.init(); #ifdef NIGHTLY_BUILD ShumwayUtils.init(); #endif @@ -649,17 +649,17 @@ BrowserGlue.prototype = { .getService(Ci.nsIAppStartup); appStartup.trackStartupCrashEnd(); } catch (e) { Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e); } BrowserNewTabPreloader.uninit(); CustomizationTabPreloader.uninit(); - webappsUI.uninit(); + WebappManager.uninit(); SignInToWebsiteUX.uninit(); webrtcUI.uninit(); }, // All initial windows have opened. _onWindowsRestored: function BG__onWindowsRestored() { // Show update notification, if needed. if (Services.prefs.prefHasUserValue("app.update.postupdate"))
--- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -793,17 +793,17 @@ bin/libfreebl_32int64_3.so @BINPATH@/webapprt/components/CommandLineHandler.js @BINPATH@/webapprt/components/ContentPermission.js @BINPATH@/webapprt/components/DirectoryProvider.js @BINPATH@/webapprt/components/PaymentUIGlue.js @BINPATH@/webapprt/components/components.manifest @BINPATH@/webapprt/defaults/preferences/prefs.js @BINPATH@/webapprt/modules/Startup.jsm @BINPATH@/webapprt/modules/WebappRT.jsm -@BINPATH@/webapprt/modules/WebappsHandler.jsm +@BINPATH@/webapprt/modules/WebappManager.jsm @BINPATH@/webapprt/modules/RemoteDebugger.jsm @BINPATH@/webapprt/modules/WebRTCHandler.jsm #endif #ifdef MOZ_METRO @BINPATH@/components/MetroUIUtils.js @BINPATH@/components/MetroUIUtils.manifest [metro]
rename from browser/modules/webappsUI.jsm rename to browser/modules/WebappManager.jsm --- a/browser/modules/webappsUI.jsm +++ b/browser/modules/WebappManager.jsm @@ -1,45 +1,45 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -this.EXPORTED_SYMBOLS = ["webappsUI"]; +this.EXPORTED_SYMBOLS = ["WebappManager"]; let Ci = Components.interfaces; let Cc = Components.classes; let Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Webapps.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); -Cu.import("resource://gre/modules/WebappsInstaller.jsm"); +Cu.import("resource://gre/modules/NativeApp.jsm"); Cu.import("resource://gre/modules/WebappOSUtils.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); -this.webappsUI = { +this.WebappManager = { // List of promises for in-progress installations installations: {}, - init: function webappsUI_init() { + init: function() { Services.obs.addObserver(this, "webapps-ask-install", false); Services.obs.addObserver(this, "webapps-launch", false); Services.obs.addObserver(this, "webapps-uninstall", false); cpmm.addMessageListener("Webapps:Install:Return:OK", this); cpmm.addMessageListener("Webapps:Install:Return:KO", this); cpmm.addMessageListener("Webapps:UpdateState", this); }, - uninit: function webappsUI_uninit() { + uninit: function() { Services.obs.removeObserver(this, "webapps-ask-install"); Services.obs.removeObserver(this, "webapps-launch"); Services.obs.removeObserver(this, "webapps-uninstall"); cpmm.removeMessageListener("Webapps:Install:Return:OK", this); cpmm.removeMessageListener("Webapps:Install:Return:KO", this); cpmm.removeMessageListener("Webapps:UpdateState", this); }, @@ -66,17 +66,17 @@ this.webappsUI = { if (!manifest.appcache_path) { this.installations[manifestURL].resolve(); } } else if (aMessage.name == "Webapps:Install:Return:KO") { this.installations[manifestURL].reject(data.error); } }, - observe: function webappsUI_observe(aSubject, aTopic, aData) { + observe: function(aSubject, aTopic, aData) { let data = JSON.parse(aData); data.mm = aSubject; switch(aTopic) { case "webapps-ask-install": let win = this._getWindowForId(data.oid); if (win && win.location.href == data.from) { this.doInstall(data, win); @@ -103,16 +103,18 @@ this.webappsUI = { .chromeEventHandler; let chromeDoc = browser.ownerDocument; let chromeWin = chromeDoc.defaultView; let popupProgressContent = chromeDoc.getElementById("webapps-install-progress-content"); let bundle = chromeWin.gNavigatorBundle; + let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest; + let notification; let mainAction = { label: bundle.getString("webapps.install"), accessKey: bundle.getString("webapps.install.accesskey"), callback: () => { notification.remove(); @@ -123,62 +125,60 @@ this.webappsUI = { "webapps-notification-icon"); let progressMeter = chromeDoc.createElement("progressmeter"); progressMeter.setAttribute("mode", "undetermined"); popupProgressContent.appendChild(progressMeter); let manifestURL = aData.app.manifestURL; - let cleanup = (ex) => { + let cleanup = () => { popupProgressContent.removeChild(progressMeter); delete this.installations[manifestURL]; if (Object.getOwnPropertyNames(this.installations).length == 0) { notification.remove(); } }; this.installations[manifestURL] = Promise.defer(); this.installations[manifestURL].promise.then(null, (error) => { Cu.reportError("Error installing webapp: " + error); cleanup(); }); - let app = WebappsInstaller.init(aData); - - if (app) { - let localDir = null; - if (app.appProfile) { - localDir = app.appProfile.localDir; - } - - DOMApplicationRegistry.confirmInstall(aData, localDir, - (aManifest, aZipPath) => { - Task.spawn(function() { - try { - yield WebappsInstaller.install(aData, aManifest, aZipPath); - yield this.installations[manifestURL].promise; - installationSuccessNotification(aData, app, bundle); - } catch (ex) { - Cu.reportError("Error installing webapp: " + ex); - // TODO: Notify user that the installation has failed - } finally { - cleanup(); - } - }.bind(this)); - }); - } else { + let nativeApp = new NativeApp(aData.app, jsonManifest, + aData.app.categories); + let localDir; + try { + localDir = nativeApp.createProfile(); + } catch (ex) { + Cu.reportError("Error installing webapp: " + ex); DOMApplicationRegistry.denyInstall(aData); cleanup(); + return; } + + DOMApplicationRegistry.confirmInstall(aData, localDir, + (aManifest, aZipPath) => Task.spawn((function*() { + try { + yield nativeApp.install(aManifest, aZipPath); + yield this.installations[manifestURL].promise; + notifyInstallSuccess(aData.app, nativeApp, bundle); + } catch (ex) { + Cu.reportError("Error installing webapp: " + ex); + // TODO: Notify user that the installation has failed + } finally { + cleanup(); + } + }).bind(this)) + ); } }; let requestingURI = chromeWin.makeURI(aData.from); - let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest; let manifest = new ManifestHelper(jsonManifest, aData.app.origin); let host; try { host = requestingURI.host; } catch(e) { host = requestingURI.spec; } @@ -190,27 +190,27 @@ this.webappsUI = { "webapps-install", message, "webapps-notification-icon", mainAction); } } -function installationSuccessNotification(aData, app, aBundle) { +function notifyInstallSuccess(aApp, aNativeApp, aBundle) { let launcher = { observe: function(aSubject, aTopic) { if (aTopic == "alertclickcallback") { - WebappOSUtils.launch(aData.app); + WebappOSUtils.launch(aApp); } } }; try { let notifier = Cc["@mozilla.org/alerts-service;1"]. getService(Ci.nsIAlertsService); - notifier.showAlertNotification(app.iconURI.spec, + notifier.showAlertNotification(aNativeApp.iconURI.spec, aBundle.getString("webapps.install.success"), - app.appNameAsFilename, + aNativeApp.appNameAsFilename, true, null, launcher); } catch (ex) {} }
--- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -15,17 +15,17 @@ EXTRA_JS_MODULES += [ 'Feeds.jsm', 'NetworkPrioritizer.jsm', 'offlineAppCache.jsm', 'SharedFrame.jsm', 'SignInToWebsite.jsm', 'SitePermissions.jsm', 'Social.jsm', 'TabCrashReporter.jsm', - 'webappsUI.jsm', + 'WebappManager.jsm', 'webrtcUI.jsm', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': EXTRA_JS_MODULES += [ 'WindowsJumpLists.jsm', 'WindowsPreviewPerTab.jsm', ]
--- a/dom/apps/src/AppsUtils.jsm +++ b/dom/apps/src/AppsUtils.jsm @@ -203,17 +203,17 @@ this.AppsUtils = { // so we can't use the 'removable' property for isCoreApp // Instead, we check if the app is installed under /system/b2g let isCoreApp = false; #ifdef MOZ_WIDGET_GONK isCoreApp = app.basePath == this.getCoreAppsBasePath(); #endif debug(app.basePath + " isCoreApp: " + isCoreApp); - return { "path": WebappOSUtils.getInstallPath(app), + return { "path": WebappOSUtils.getPackagePath(app), "isCoreApp": isCoreApp }; }, /** * Remove potential HTML tags from displayable fields in the manifest. * We check name, description, developer name, and permission description */ sanitizeManifest: function(aManifest) {
--- a/dom/apps/src/Webapps.jsm +++ b/dom/apps/src/Webapps.jsm @@ -72,16 +72,18 @@ function supportSystemMessages() { return Services.prefs.getBoolPref("dom.sysmsg.enabled"); } // Minimum delay between two progress events while downloading, in ms. const MIN_PROGRESS_EVENT_DELAY = 1500; const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org"; +const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser"; + XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { Cu.import("resource://gre/modules/NetUtil.jsm"); return NetUtil; }); XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); @@ -128,16 +130,17 @@ XPCOMUtils.defineLazyGetter(this, "updat const STORE_ID_PENDING_PREFIX = "#unknownID#"; this.DOMApplicationRegistry = { // Path to the webapps.json file where we store the registry data. appsFile: null, webapps: { }, children: [ ], allAppsLaunchable: false, + _updateHandlers: [ ], init: function() { this.messages = ["Webapps:Install", "Webapps:Uninstall", "Webapps:GetSelf", "Webapps:CheckInstalled", "Webapps:GetInstalled", "Webapps:GetNotInstalled", "Webapps:Launch", "Webapps:GetAll", "Webapps:InstallPackage", "Webapps:GetList", "Webapps:RegisterForMessages", @@ -1191,16 +1194,33 @@ this.DOMApplicationRegistry = { if (!(aMsgName in this.children)) { return; } this.children[aMsgName].forEach(function(mmRef) { mmRef.mm.sendAsyncMessage(aMsgName, aContent); }); }, + registerUpdateHandler: function(aHandler) { + this._updateHandlers.push(aHandler); + }, + + unregisterUpdateHandler: function(aHandler) { + let index = this._updateHandlers.indexOf(aHandler); + if (index != -1) { + this._updateHandlers.splice(index, 1); + } + }, + + notifyUpdateHandlers: function(aApp, aManifest, aZipPath) { + for (let updateHandler of this._updateHandlers) { + updateHandler(aApp, aManifest, aZipPath); + } + }, + _getAppDir: function(aId) { return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); }, _writeFile: function(aPath, aData) { debug("Saving " + aPath); return OS.File.writeAtomic(aPath, @@ -1482,16 +1502,23 @@ this.DOMApplicationRegistry = { delete app.staged; } delete app.retryingDownload; this._saveApps().then(() => { // Update the handlers and permissions for this app. this.updateAppHandlers(aOldManifest, aData, app); + + this._loadJSONAsync(staged.path).then((aUpdateManifest) => { + let appObject = AppsUtils.cloneAppObject(app); + appObject.updateManifest = aUpdateManifest; + this.notifyUpdateHandlers(appObject, aData, appFile.path); + }); + if (supportUseCurrentProfile()) { PermissionsInstaller.installPermissions( { manifest: aData, origin: app.origin, manifestURL: app.manifestURL }, true); } this.updateDataStore(this.webapps[id].localId, app.origin, @@ -1892,16 +1919,18 @@ this.DOMApplicationRegistry = { } aApp.manifest = aNewManifest || aOldManifest; let manifest; if (aNewManifest) { this.updateAppHandlers(aOldManifest, aNewManifest, aApp); + this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest); + // Store the new manifest. let dir = this._getAppDir(aId).path; let manFile = OS.Path.join(dir, "manifest.webapp"); this._writeFile(manFile, JSON.stringify(aNewManifest)); manifest = new ManifestHelper(aNewManifest, aApp.origin); if (supportUseCurrentProfile()) { // Update the permissions for this app. @@ -2664,17 +2693,17 @@ onInstallSuccessAck: function onInstallS aOnSuccess, this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate) ); }, _ensureSufficientStorage: function(aNewApp) { let deferred = Promise.defer(); - let navigator = Services.wm.getMostRecentWindow("navigator:browser") + let navigator = Services.wm.getMostRecentWindow(chromeWindowType) .navigator; let deviceStorage = null; if (navigator.getDeviceStorage) { deviceStorage = navigator.getDeviceStorage("apps"); } if (deviceStorage) {
--- a/dom/tests/browser/browser_webapps_permissions.js +++ b/dom/tests/browser/browser_webapps_permissions.js @@ -58,17 +58,17 @@ function test() { var browser = gBrowser.selectedBrowser; PopupNotifications.panel.addEventListener("popupshown", handlePopup, false); registerCleanupFunction(function () { gWindow = null; gBrowser.removeTab(tab); // The installation may have created a XUL alert window - // (see webappsUI.installationSuccessNotification). + // (see notifyInstallSuccess in WebappManager.jsm). // It need to be closed before the test finishes. var browsers = windowMediator.getEnumerator('alert:alert'); while (browsers.hasMoreElements()) { browsers.getNext().close(); } }); browser.addEventListener("DOMContentLoaded", function onLoad(event) {
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/LinuxNativeApp.js @@ -0,0 +1,335 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Constructor for the Linux native app shell + * + * @param aApp {Object} the app object provided to the install function + * @param aManifest {Object} the manifest data provided by the web app + * @param aCategories {Array} array of app categories + * @param aRegistryDir {String} (optional) path to the registry + */ +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { + CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); + + this.iconFile = "icon.png"; + this.webapprt = "webapprt-stub"; + this.configJson = "webapp.json"; + this.webappINI = "webapp.ini"; + this.zipFile = "application.zip"; + + this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ]; + if (this.isPackaged) { + this.backupFiles.push(this.zipFile); + } + + let xdg_data_home = Cc["@mozilla.org/process/environment;1"]. + getService(Ci.nsIEnvironment). + get("XDG_DATA_HOME"); + if (!xdg_data_home) { + xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share"); + } + + // The desktop file name is: "owa-" + sanitized app name + + // "-" + manifest url hash. + this.desktopINI = OS.Path.join(xdg_data_home, "applications", + "owa-" + this.uniqueName + ".desktop"); +} + +NativeApp.prototype = { + __proto__: CommonNativeApp.prototype, + + /** + * Creates a native installation of the web app in the OS + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + install: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + // If the application is already installed, this is a reinstallation. + if (WebappOSUtils.getInstallPath(this.app)) { + return yield this.prepareUpdate(aManifest, aZipPath); + } + + this._setData(aManifest); + + // The installation directory name is: sanitized app name + + // "-" + manifest url hash. + let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName); + + let dir = getFile(TMP_DIR, this.uniqueName); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + let tmpDir = dir.path; + + // Create the installation in a temporary directory. + try { + this._copyPrebuiltFiles(tmpDir); + yield this._createConfigFiles(tmpDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); + } + + yield this._getIcon(tmpDir); + } catch (ex) { + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + + // Apply the installation. + this._removeInstallation(true, installDir); + + try { + yield this._applyTempInstallation(tmpDir, installDir); + } catch (ex) { + this._removeInstallation(false, installDir); + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Creates an update in a temporary directory to be applied later. + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + prepareUpdate: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + this._setData(aManifest); + + let installDir = WebappOSUtils.getInstallPath(this.app); + if (!installDir) { + throw ERR_NOT_INSTALLED; + } + + let baseName = OS.Path.basename(installDir) + let oldUniqueName = baseName.substring(1, baseName.length); + if (this.uniqueName != oldUniqueName) { + // Bug 919799: If the app is still in the registry, migrate its data to + // the new format. + throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; + } + + let updateDir = OS.Path.join(installDir, "update"); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + yield OS.File.makeDir(updateDir); + + try { + yield this._createConfigFiles(updateDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); + } + + yield this._getIcon(updateDir); + } catch (ex) { + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Applies an update. + */ + applyUpdate: Task.async(function*() { + if (this._dryRun) { + return; + } + + let installDir = WebappOSUtils.getInstallPath(this.app); + let updateDir = OS.Path.join(installDir, "update"); + + let backupDir = yield this._backupInstallation(installDir); + + try { + yield this._applyTempInstallation(updateDir, installDir); + } catch (ex) { + yield this._restoreInstallation(backupDir, installDir); + throw ex; + } finally { + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + } + }), + + _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { + yield moveDirectory(aTmpDir, aInstallDir); + + this._createSystemFiles(aInstallDir); + }), + + _removeInstallation: function(keepProfile, aInstallDir) { + let filesToRemove = [this.desktopINI]; + + if (keepProfile) { + for (let filePath of this.backupFiles) { + filesToRemove.push(OS.Path.join(aInstallDir, filePath)); + } + + filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); + } else { + filesToRemove.push(aInstallDir); + } + + removeFiles(filesToRemove); + }, + + _backupInstallation: Task.async(function*(aInstallDir) { + let backupDir = OS.Path.join(aInstallDir, "backup"); + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.makeDir(backupDir); + + for (let filePath of this.backupFiles) { + yield OS.File.move(OS.Path.join(aInstallDir, filePath), + OS.Path.join(backupDir, filePath)); + } + + return backupDir; + }), + + _restoreInstallation: function(aBackupDir, aInstallDir) { + return moveDirectory(aBackupDir, aInstallDir); + }, + + _copyPrebuiltFiles: function(aDir) { + let destDir = getFile(aDir); + let stub = getFile(this.runtimeFolder, this.webapprt); + stub.copyTo(destDir, null); + }, + + /** + * Translate marketplace categories to freedesktop.org categories. + * + * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry + * + * @return an array of categories + */ + _translateCategories: function() { + let translations = { + "books": "Education;Literature", + "business": "Finance", + "education": "Education", + "entertainment": "Amusement", + "sports": "Sports", + "games": "Game", + "health-fitness": "MedicalSoftware", + "lifestyle": "Amusement", + "music": "Audio;Music", + "news-weather": "News", + "photo-video": "Video;AudioVideo;Photography", + "productivity": "Office", + "shopping": "Amusement", + "social": "Chat", + "travel": "Amusement", + "reference": "Science;Education;Documentation", + "maps-navigation": "Maps", + "utilities": "Utility" + }; + + // The trailing semicolon is needed as written in the freedesktop specification + let categories = ""; + for (let category of this.categories) { + let catLower = category.toLowerCase(); + if (catLower in translations) { + categories += translations[catLower] + ";"; + } + } + + return categories; + }, + + _createConfigFiles: function(aDir) { + // ${InstallDir}/webapp.json + yield writeToFile(OS.Path.join(aDir, this.configJson), + JSON.stringify(this.webappJson)); + + let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); + + // ${InstallDir}/webapp.ini + let webappINIfile = getFile(aDir, this.webappINI); + + let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(webappINIfile). + QueryInterface(Ci.nsIINIParserWriter); + writer.setString("Webapp", "Name", this.appName); + writer.setString("Webapp", "Profile", this.uniqueName); + writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1)); + writer.setString("WebappRT", "InstallDir", this.runtimeFolder); + writer.writeFile(); + }, + + _createSystemFiles: function(aInstallDir) { + let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); + + let webapprtPath = OS.Path.join(aInstallDir, this.webapprt); + + // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop + let desktopINIfile = getFile(this.desktopINI); + + let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(desktopINIfile). + QueryInterface(Ci.nsIINIParserWriter); + writer.setString("Desktop Entry", "Name", this.appName); + writer.setString("Desktop Entry", "Comment", this.shortDescription); + writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"'); + writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir, + this.iconFile)); + writer.setString("Desktop Entry", "Type", "Application"); + writer.setString("Desktop Entry", "Terminal", "false"); + + let categories = this._translateCategories(); + if (categories) + writer.setString("Desktop Entry", "Categories", categories); + + writer.setString("Desktop Entry", "Actions", "Uninstall;"); + writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label")); + writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove"); + + writer.writeFile(); + + desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR; + }, + + /** + * Process the icon from the imageStream as retrieved from + * the URL by getIconForApp(). + * + * @param aMimeType ahe icon mimetype + * @param aImageStream the stream for the image data + * @param aDir the directory where the icon should be stored + */ + _processIcon: function(aMimeType, aImageStream, aDir) { + let deferred = Promise.defer(); + + let imgTools = Cc["@mozilla.org/image/tools;1"]. + createInstance(Ci.imgITools); + + let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); + let iconStream = imgTools.encodeImage(imgContainer, "image/png"); + + let iconFile = getFile(aDir, this.iconFile); + let outputStream = FileUtils.openSafeFileOutputStream(iconFile); + NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { + if (Components.isSuccessCode(aResult)) { + deferred.resolve(); + } else { + deferred.reject("Failure copying icon: " + aResult); + } + }); + + return deferred.promise; + } +}
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/MacNativeApp.js @@ -0,0 +1,308 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const USER_LIB_DIR = OS.Constants.Path.macUserLibDir; +const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir; + +/** + * Constructor for the Mac native app shell + * + * @param aApp {Object} the app object provided to the install function + * @param aManifest {Object} the manifest data provided by the web app + * @param aCategories {Array} array of app categories + * @param aRegistryDir {String} (optional) path to the registry + */ +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { + CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); + + // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash + this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support", + this.uniqueName); + this.configJson = "webapp.json"; + + this.contentsDir = "Contents"; + this.macOSDir = OS.Path.join(this.contentsDir, "MacOS"); + this.resourcesDir = OS.Path.join(this.contentsDir, "Resources"); + this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns"); + this.zipFile = OS.Path.join(this.resourcesDir, "application.zip"); +} + +NativeApp.prototype = { + __proto__: CommonNativeApp.prototype, + + /** + * Creates a native installation of the web app in the OS + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + install: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + // If the application is already installed, this is a reinstallation. + if (WebappOSUtils.getInstallPath(this.app)) { + return yield this.prepareUpdate(aManifest, aZipPath); + } + + this._setData(aManifest); + + let localAppDir = getFile(LOCAL_APP_DIR); + if (!localAppDir.isWritable()) { + throw("Not enough privileges to install apps"); + } + + let destinationName = yield getAvailableFileName([ LOCAL_APP_DIR ], + this.appNameAsFilename, + ".app"); + + let installDir = OS.Path.join(LOCAL_APP_DIR, destinationName); + + let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app"); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + let tmpDir = dir.path; + + try { + yield this._createDirectoryStructure(tmpDir); + this._copyPrebuiltFiles(tmpDir); + yield this._createConfigFiles(tmpDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); + } + + yield this._getIcon(tmpDir); + } catch (ex) { + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + + this._removeInstallation(true, installDir); + + try { + // Move the temp installation directory to the /Applications directory + yield this._applyTempInstallation(tmpDir, installDir); + } catch (ex) { + this._removeInstallation(false, installDir); + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Creates an update in a temporary directory to be applied later. + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + prepareUpdate: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + this._setData(aManifest); + + let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app); + if (!installDir) { + throw ERR_NOT_INSTALLED; + } + + if (this.uniqueName != oldUniqueName) { + // Bug 919799: If the app is still in the registry, migrate its data to + // the new format. + throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; + } + + let updateDir = OS.Path.join(installDir, "update"); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + yield OS.File.makeDir(updateDir); + + try { + yield this._createDirectoryStructure(updateDir); + this._copyPrebuiltFiles(updateDir); + yield this._createConfigFiles(updateDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); + } + + yield this._getIcon(updateDir); + } catch (ex) { + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Applies an update. + */ + applyUpdate: Task.async(function*() { + if (this._dryRun) { + return; + } + + let installDir = WebappOSUtils.getInstallPath(this.app); + let updateDir = OS.Path.join(installDir, "update"); + + let backupDir = yield this._backupInstallation(installDir); + + try { + // Move the update directory to the /Applications directory + yield this._applyTempInstallation(updateDir, installDir); + } catch (ex) { + yield this._restoreInstallation(backupDir, installDir); + throw ex; + } finally { + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + } + }), + + _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { + yield OS.File.move(OS.Path.join(aTmpDir, this.configJson), + OS.Path.join(this.appProfileDir, this.configJson)); + + yield moveDirectory(aTmpDir, aInstallDir); + }), + + _removeInstallation: function(keepProfile, aInstallDir) { + let filesToRemove = [ aInstallDir ]; + + if (!keepProfile) { + filesToRemove.push(this.appProfileDir); + } + + removeFiles(filesToRemove); + }, + + _backupInstallation: Task.async(function*(aInstallDir) { + let backupDir = OS.Path.join(aInstallDir, "backup"); + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.makeDir(backupDir); + + yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir), + backupDir); + yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson), + OS.Path.join(backupDir, this.configJson)); + + return backupDir; + }), + + _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) { + yield OS.File.move(OS.Path.join(aBackupDir, this.configJson), + OS.Path.join(this.appProfileDir, this.configJson)); + yield moveDirectory(aBackupDir, + OS.Path.join(aInstallDir, this.contentsDir)); + }), + + _createDirectoryStructure: Task.async(function*(aDir) { + yield OS.File.makeDir(this.appProfileDir, + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + }), + + _copyPrebuiltFiles: function(aDir) { + let destDir = getFile(aDir, this.macOSDir); + let stub = getFile(this.runtimeFolder, "webapprt-stub"); + stub.copyTo(destDir, "webapprt"); + }, + + _createConfigFiles: function(aDir) { + // ${ProfileDir}/webapp.json + yield writeToFile(OS.Path.join(aDir, this.configJson), + JSON.stringify(this.webappJson)); + + // ${InstallDir}/Contents/MacOS/webapp.ini + let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini"); + + let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(applicationINI). + QueryInterface(Ci.nsIINIParserWriter); + writer.setString("Webapp", "Name", this.appName); + writer.setString("Webapp", "Profile", this.uniqueName); + writer.writeFile(); + applicationINI.permissions = PERMS_FILE; + + // ${InstallDir}/Contents/Info.plist + let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\ +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\ +<plist version="1.0">\n\ + <dict>\n\ + <key>CFBundleDevelopmentRegion</key>\n\ + <string>English</string>\n\ + <key>CFBundleDisplayName</key>\n\ + <string>' + escapeXML(this.appName) + '</string>\n\ + <key>CFBundleExecutable</key>\n\ + <string>webapprt</string>\n\ + <key>CFBundleIconFile</key>\n\ + <string>appicon</string>\n\ + <key>CFBundleIdentifier</key>\n\ + <string>' + escapeXML(this.uniqueName) + '</string>\n\ + <key>CFBundleInfoDictionaryVersion</key>\n\ + <string>6.0</string>\n\ + <key>CFBundleName</key>\n\ + <string>' + escapeXML(this.appName) + '</string>\n\ + <key>CFBundlePackageType</key>\n\ + <string>APPL</string>\n\ + <key>CFBundleVersion</key>\n\ + <string>0</string>\n\ + <key>NSHighResolutionCapable</key>\n\ + <true/>\n\ + <key>NSPrincipalClass</key>\n\ + <string>GeckoNSApplication</string>\n\ + <key>FirefoxBinary</key>\n\ +#expand <string>__MOZ_MACBUNDLE_ID__</string>\n\ + </dict>\n\ +</plist>'; + + yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"), + infoPListContent); + }, + + /** + * Process the icon from the imageStream as retrieved from + * the URL by getIconForApp(). This will bundle the icon to the + * app package at Contents/Resources/appicon.icns. + * + * @param aMimeType the icon mimetype + * @param aImageStream the stream for the image data + * @param aDir the directory where the icon should be stored + */ + _processIcon: function(aMimeType, aIcon, aDir) { + let deferred = Promise.defer(); + + function conversionDone(aSubject, aTopic) { + if (aTopic == "process-finished") { + deferred.resolve(); + } else { + deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue); + } + } + + let process = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + let sipsFile = getFile("/usr/bin/sips"); + + process.init(sipsFile); + process.runAsync(["-s", "format", "icns", + aIcon.path, + "--out", OS.Path.join(aDir, this.iconFile), + "-z", "128", "128"], + 9, conversionDone); + + return deferred.promise; + } +}
rename from toolkit/webapps/WebappsInstaller.jsm rename to toolkit/webapps/NativeApp.jsm --- a/toolkit/webapps/WebappsInstaller.jsm +++ b/toolkit/webapps/NativeApp.jsm @@ -1,158 +1,109 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -this.EXPORTED_SYMBOLS = ["WebappsInstaller"]; +this.EXPORTED_SYMBOLS = ["NativeApp"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; const Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/WebappOSUtils.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); +const ERR_NOT_INSTALLED = "The application isn't installed"; +const ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME = + "Updates for apps installed with the old naming scheme unsupported"; + // 0755 const PERMS_DIRECTORY = OS.Constants.libc.S_IRWXU | OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IXGRP | OS.Constants.libc.S_IROTH | OS.Constants.libc.S_IXOTH; // 0644 const PERMS_FILE = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR | OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IROTH; const DESKTOP_DIR = OS.Constants.Path.desktopDir; const HOME_DIR = OS.Constants.Path.homeDir; const TMP_DIR = OS.Constants.Path.tmpDir; -this.WebappsInstaller = { - shell: null, - - /** - * Initializes the app object that takes care of the installation - * and creates the profile directory for an application - * - * @param aData the data provided to the install function - * - * @returns NativeApp on success, null on error - */ - init: function(aData) { -#ifdef XP_WIN - this.shell = new WinNativeApp(aData); -#elifdef XP_MACOSX - this.shell = new MacNativeApp(aData); -#elifdef XP_UNIX - this.shell = new LinuxNativeApp(aData); -#else - return null; -#endif - - try { - if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) { - return this.shell; - } - } catch (ex) {} - - try { - this.shell.createAppProfile(); - } catch (ex) { - Cu.reportError("Error installing app: " + ex); - return null; - } - - return this.shell; - }, - - /** - * Creates a native installation of the web app in the OS - * - * @param aData the data provided to the install function - * @param aManifest the manifest data provided by the web app - * @param aZipPath path to the zip file for packaged apps (undefined for - * hosted apps) - */ - install: function(aData, aManifest, aZipPath) { - try { - if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) { - return Promise.resolve(); - } - } catch (ex) {} - - this.shell.init(aData, aManifest); - - return this.shell.install(aZipPath).then(() => { - let data = { - "installDir": this.shell.installDir, - "app": { - "manifest": aManifest, - "origin": aData.app.origin - } - }; - - Services.obs.notifyObservers(null, "webapp-installed", JSON.stringify(data)); - }); - } -} - /** * This function implements the common constructor for * the Windows, Mac and Linux native app shells. It sets * the app unique name. It's meant to be called as - * NativeApp.call(this, aData) from the platform-specific + * CommonNativeApp.call(this, ...) from the platform-specific * constructor. * - * @param aData the data object provided to the install function + * @param aApp {Object} the app object provided to the install function + * @param aManifest {Object} the manifest data provided by the web app + * @param aCategories {Array} array of app categories + * @param aRegistryDir {String} (optional) path to the registry * */ -function NativeApp(aData) { - let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest; - let manifest = new ManifestHelper(jsonManifest, aData.app.origin); +function CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) { + let manifest = new ManifestHelper(aManifest, aApp.origin); - aData.app.name = manifest.name; - this.uniqueName = WebappOSUtils.getUniqueName(aData.app); + aApp.name = manifest.name; + this.uniqueName = WebappOSUtils.getUniqueName(aApp); this.appName = sanitize(manifest.name); this.appNameAsFilename = stripStringForFilename(this.appName); + + if (aApp.updateManifest) { + this.isPackaged = true; + } + + this.categories = aCategories.slice(0); + + this.registryDir = aRegistryDir || OS.Constants.Path.profileDir; + + this.app = aApp; + + this._dryRun = false; + try { + if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) { + this._dryRun = true; + } + } catch (ex) {} } -NativeApp.prototype = { +CommonNativeApp.prototype = { uniqueName: null, appName: null, appNameAsFilename: null, iconURI: null, developerName: null, shortDescription: null, categories: null, webappJson: null, runtimeFolder: null, manifest: null, + registryDir: null, /** * This function reads and parses the data from the app * manifest and stores it in the NativeApp object. * - * @param aData the data object provided to the install function - * @param aManifest the manifest data provided by the web app + * @param aManifest {Object} the manifest data provided by the web app * */ - init: function(aData, aManifest) { - let app = this.app = aData.app; - let manifest = this.manifest = new ManifestHelper(aManifest, - app.origin); - - let origin = Services.io.newURI(app.origin, null, null); + _setData: function(aManifest) { + let manifest = new ManifestHelper(aManifest, this.app.origin); + let origin = Services.io.newURI(this.app.origin, null, null); let biggestIcon = getBiggestIconURL(manifest.icons); try { let iconURI = Services.io.newURI(biggestIcon, null, null); if (iconURI.scheme == "data") { this.iconURI = iconURI; } } catch (ex) {} @@ -182,905 +133,119 @@ NativeApp.prototype = { let shortDesc = firstLine.length <= 256 ? firstLine : firstLine.substr(0, 253) + "…"; this.shortDescription = sanitize(shortDesc); } else { this.shortDescription = this.appName; } - this.categories = app.categories.slice(0); + if (manifest.version) { + this.version = manifest.version; + } this.webappJson = { // The app registry is the Firefox profile from which the app // was installed. - "registryDir": OS.Constants.Path.profileDir, + "registryDir": this.registryDir, "app": { "manifest": aManifest, - "origin": app.origin, - "manifestURL": app.manifestURL, - "installOrigin": app.installOrigin, - "categories": app.categories, - "receipts": app.receipts, - "installTime": app.installTime, + "origin": this.app.origin, + "manifestURL": this.app.manifestURL, + "installOrigin": this.app.installOrigin, + "categories": this.categories, + "receipts": this.app.receipts, + "installTime": this.app.installTime, } }; - if (app.etag) { - this.webappJson.app.etag = app.etag; + if (this.app.etag) { + this.webappJson.app.etag = this.app.etag; } - if (app.packageEtag) { - this.webappJson.app.packageEtag = app.packageEtag; + if (this.app.packageEtag) { + this.webappJson.app.packageEtag = this.app.packageEtag; } - if (app.updateManifest) { - this.webappJson.app.updateManifest = app.updateManifest; + if (this.app.updateManifest) { + this.webappJson.app.updateManifest = this.app.updateManifest; } this.runtimeFolder = OS.Constants.Path.libDir; }, /** * This function retrieves the icon for an app. * If the retrieving fails, it uses the default chrome icon. */ - getIcon: function() { + _getIcon: function(aTmpDir) { try { // If the icon is in the zip package, we should modify the url // to point to the zip file (we can't use the app protocol yet // because the app isn't installed yet). if (this.iconURI.scheme == "app") { - let zipUrl = OS.Path.toFileURI(OS.Path.join(this.tmpInstallDir, + let zipUrl = OS.Path.toFileURI(OS.Path.join(aTmpDir, "application.zip")); let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath; this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath, null, null); } let [ mimeType, icon ] = yield downloadIcon(this.iconURI); - yield this.processIcon(mimeType, icon); + yield this._processIcon(mimeType, icon, aTmpDir); } catch(e) { Cu.reportError("Failure retrieving icon: " + e); let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null); let [ mimeType, icon ] = yield downloadIcon(iconURI); - yield this.processIcon(mimeType, icon); + yield this._processIcon(mimeType, icon, aTmpDir); // Set the iconURI property so that the user notification will have the // correct icon. this.iconURI = iconURI; } }, /** * Creates the profile to be used for this app. */ - createAppProfile: function() { - let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"] - .getService(Ci.nsIToolkitProfileService); + createProfile: function() { + if (this._dryRun) { + return null; + } + + let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]. + getService(Ci.nsIToolkitProfileService); try { - this.appProfile = profSvc.createDefaultProfileForApp(this.uniqueName, - null, null); - } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {} + let appProfile = profSvc.createDefaultProfileForApp(this.uniqueName, + null, null); + return appProfile.localDir; + } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) { + return null; + } }, }; #ifdef XP_WIN -const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir; -const APP_DATA_DIR = OS.Constants.Path.winAppDataDir; - -/************************************* - * Windows app installer - * - * The Windows installation process will generate the following files: - * - * ${FolderName} = sanitized app name + "-" + manifest url hash - * - * %APPDATA%/${FolderName} - * - webapp.ini - * - webapp.json - * - ${AppName}.exe - * - ${AppName}.lnk - * / uninstall - * - webapp-uninstaller.exe - * - shortcuts_log.ini - * - uninstall.log - * / chrome/icons/default/ - * - default.ico - * - * After the app runs for the first time, a profiles/ folder will also be - * created which will host the user profile for this app. - */ - -/** - * Constructor for the Windows native app shell - * - * @param aData the data object provided to the install function - */ -function WinNativeApp(aData) { - NativeApp.call(this, aData); - - if (aData.isPackage) { - this.size = aData.app.updateManifest.size / 1024; - this.isPackaged = true; - } - - let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); - - this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, ""); - if (this.appNameAsFilename == "") { - this.appNameAsFilename = "webapp"; - } - - this.webapprt = this.appNameAsFilename + ".exe"; - this.configJson = "webapp.json"; - this.webappINI = "webapp.ini"; - this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico"); - this.uninstallDir = "uninstall"; - this.uninstallerFile = OS.Path.join(this.uninstallDir, - "webapp-uninstaller.exe"); - this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini"); - - this.uninstallSubkeyStr = this.uniqueName; -} - -WinNativeApp.prototype = { - __proto__: NativeApp.prototype, - size: null, - - /** - * Install the app in the system - * - */ - install: function(aZipPath) { - return Task.spawn(function() { - yield this._getInstallDir(); - - try { - yield this._createDirectoryStructure(); - yield this._copyPrebuiltFiles(); - yield this._createConfigFiles(); - - if (aZipPath) { - yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir, - "application.zip")); - } - - yield this.getIcon(); - - // Remove previously installed app - this._removeInstallation(true); - } catch (ex) { - removeFiles([this.tmpInstallDir]); - throw(ex); - } - - try { - // On Windows, the webapprt executable can't be overwritten while it's - // running. - // As it takes care of updating itself, there's no need to update - // it here. - let filesToIgnore = [ this.webapprt ]; - yield moveDirectory(this.tmpInstallDir, this.installDir, filesToIgnore); - - this._createShortcutFiles(); - this._writeSystemKeys(); - } catch (ex) { - this._removeInstallation(false); - throw(ex); - } - }.bind(this)); - }, - - _getInstallDir: function() { - // The ${InstallDir} is: sanitized app name + "-" + manifest url hash - this.installDir = WebappOSUtils.getInstallPath(this.app); - if (this.installDir) { - if (this.uniqueName != OS.Path.basename(this.installDir)) { - // Bug 919799: If the app is still in the registry, migrate its data to - // the new format. - throw("Updates for apps installed with the old naming scheme unsupported"); - } - - let shortcutLogsINIfile = getFile(this.installDir, this.shortcutLogsINI); - // If it's a reinstallation (or an update) get the shortcut names - // from the shortcut_log.ini file - let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"] - .getService(Ci.nsIINIParserFactory) - .createINIParser(shortcutLogsINIfile); - this.shortcutName = parser.getString("STARTMENU", "Shortcut0"); - } else { - this.installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName); - - // Check in both directories to see if a shortcut with the same name - // already exists. - this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ], - this.appNameAsFilename, - ".lnk"); - } - }, - - /** - * Remove the current installation - */ - _removeInstallation: function(keepProfile) { - let uninstallKey; - try { - uninstallKey = Cc["@mozilla.org/windows-registry-key;1"] - .createInstance(Ci.nsIWindowsRegKey); - uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Microsoft\\Windows\\" + - "CurrentVersion\\Uninstall", - uninstallKey.ACCESS_WRITE); - if(uninstallKey.hasChild(this.uninstallSubkeyStr)) { - uninstallKey.removeChild(this.uninstallSubkeyStr); - } - } catch (e) { - } finally { - if(uninstallKey) - uninstallKey.close(); - } - - let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName), - OS.Path.join(PROGS_DIR, this.shortcutName) ]; - - if (keepProfile) { - [ this.iconPath, this.webapprt, this.configJson, - this.webappINI, this.uninstallDir ].forEach((filePath) => { - filesToRemove.push(OS.Path.join(this.installDir, filePath)); - }); - } else { - filesToRemove.push(this.installDir); - filesToRemove.push(this.tmpInstallDir); - } - - removeFiles(filesToRemove); - }, - - /** - * Creates the main directory structure. - */ - _createDirectoryStructure: function() { - let dir = getFile(TMP_DIR, this.uniqueName); - dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); - this.tmpInstallDir = dir.path; - - yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.uninstallDir), - { ignoreExisting: true }); - - // Recursively create the icon path's directory structure. - let path = this.tmpInstallDir; - let components = OS.Path.split(OS.Path.dirname(this.iconPath)).components; - for (let component of components) { - path = OS.Path.join(path, component); - yield OS.File.makeDir(path, { ignoreExisting: true }); - } - }, - - /** - * Copy the pre-built files into their destination folders. - */ - _copyPrebuiltFiles: function() { - yield OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"), - OS.Path.join(this.tmpInstallDir, this.webapprt)); - - yield OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"), - OS.Path.join(this.tmpInstallDir, this.uninstallerFile)); - }, - - /** - * Creates the configuration files into their destination folders. - */ - _createConfigFiles: function() { - // ${InstallDir}/webapp.json - yield writeToFile(OS.Path.join(this.tmpInstallDir, this.configJson), - JSON.stringify(this.webappJson)); - - let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"] - .getService(Ci.nsIINIParserFactory); - - // ${InstallDir}/webapp.ini - let webappINIfile = getFile(this.tmpInstallDir, this.webappINI); - - let writer = factory.createINIParser(webappINIfile) - .QueryInterface(Ci.nsIINIParserWriter); - writer.setString("Webapp", "Name", this.appName); - writer.setString("Webapp", "Profile", OS.Path.basename(this.installDir)); - writer.setString("Webapp", "Executable", this.appNameAsFilename); - writer.setString("WebappRT", "InstallDir", this.runtimeFolder); - writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); - - let shortcutLogsINIfile = getFile(this.tmpInstallDir, this.shortcutLogsINI); - - writer = factory.createINIParser(shortcutLogsINIfile) - .QueryInterface(Ci.nsIINIParserWriter); - writer.setString("STARTMENU", "Shortcut0", this.shortcutName); - writer.setString("DESKTOP", "Shortcut0", this.shortcutName); - writer.setString("TASKBAR", "Migrated", "true"); - writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); - - // ${UninstallDir}/uninstall.log - let uninstallContent = - "File: \\webapp.ini\r\n" + - "File: \\webapp.json\r\n" + - "File: \\webapprt.old\r\n" + - "File: \\chrome\\icons\\default\\default.ico"; - if (this.isPackaged) { - uninstallContent += "\r\nFile: \\application.zip"; - } - - yield writeToFile(OS.Path.join(this.tmpInstallDir, this.uninstallDir, - "uninstall.log"), - uninstallContent); - }, - - /** - * Writes the keys to the system registry that are necessary for the app operation - * and uninstall process. - */ - _writeSystemKeys: function() { - let parentKey; - let uninstallKey; - let subKey; - - try { - parentKey = Cc["@mozilla.org/windows-registry-key;1"] - .createInstance(Ci.nsIWindowsRegKey); - parentKey.open(parentKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", - parentKey.ACCESS_WRITE); - uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE) - subKey = uninstallKey.createChild(this.uninstallSubkeyStr, uninstallKey.ACCESS_WRITE); - - subKey.writeStringValue("DisplayName", this.appName); - - let uninstallerPath = OS.Path.join(this.installDir, - this.uninstallerFile); - - subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"'); - subKey.writeStringValue("InstallLocation", '"' + this.installDir + '"'); - subKey.writeStringValue("AppFilename", this.appNameAsFilename); - subKey.writeStringValue("DisplayIcon", OS.Path.join(this.installDir, - this.iconPath)); - - let date = new Date(); - let year = date.getYear().toString(); - let month = date.getMonth(); - if (month < 10) { - month = "0" + month; - } - let day = date.getDate(); - if (day < 10) { - day = "0" + day; - } - subKey.writeStringValue("InstallDate", year + month + day); - if (this.manifest.version) { - subKey.writeStringValue("DisplayVersion", this.manifest.version); - } - if (this.developerName) { - subKey.writeStringValue("Publisher", this.developerName); - } - subKey.writeStringValue("URLInfoAbout", this.developerUrl); - if (this.size) { - subKey.writeIntValue("EstimatedSize", this.size); - } - - subKey.writeIntValue("NoModify", 1); - subKey.writeIntValue("NoRepair", 1); - } catch(ex) { - throw(ex); - } finally { - if(subKey) subKey.close(); - if(uninstallKey) uninstallKey.close(); - if(parentKey) parentKey.close(); - } - }, - - /** - * Creates a shortcut file inside the app installation folder and makes - * two copies of it: one into the desktop and one into the start menu. - */ - _createShortcutFiles: function() { - let shortcut = getFile(this.installDir, this.shortcutName). - QueryInterface(Ci.nsILocalFileWin); - - /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args, - description, iconFile, iconIndex) */ - - shortcut.setShortcut(getFile(this.installDir, this.webapprt), - getFile(this.installDir), - null, - this.shortDescription, - getFile(this.installDir, this.iconPath), - 0); - - shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName); - shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName); - - shortcut.followLinks = false; - shortcut.remove(false); - }, - - /** - * Process the icon from the imageStream as retrieved from - * the URL by getIconForApp(). This will save the icon to the - * topwindow.ico file. - * - * @param aMimeType ahe icon mimetype - * @param aImageStream the stream for the image data - */ - processIcon: function(aMimeType, aImageStream) { - let deferred = Promise.defer(); - - let imgTools = Cc["@mozilla.org/image/tools;1"] - .createInstance(Ci.imgITools); - - let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); - let iconStream = imgTools.encodeImage(imgContainer, - "image/vnd.microsoft.icon", - "format=bmp;bpp=32"); - - let tmpIconFile = getFile(this.tmpInstallDir, this.iconPath); - - let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile); - NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { - if (Components.isSuccessCode(aResult)) { - deferred.resolve(); - } else { - deferred.reject("Failure copying icon: " + aResult); - } - }); - - return deferred.promise; - } -} +#include WinNativeApp.js #elifdef XP_MACOSX -const USER_LIB_DIR = OS.Constants.Path.macUserLibDir; -const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir; - -function MacNativeApp(aData) { - NativeApp.call(this, aData); - - let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); - this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, ""); - if (this.appNameAsFilename == "") { - this.appNameAsFilename = "Webapp"; - } - - // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash - this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support", - this.uniqueName); - - this.contentsDir = "Contents"; - this.macOSDir = OS.Path.join(this.contentsDir, "MacOS"); - this.resourcesDir = OS.Path.join(this.contentsDir, "Resources"); - this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns"); -} - -MacNativeApp.prototype = { - __proto__: NativeApp.prototype, - - install: function(aZipPath) { - return Task.spawn(function() { - yield this._getInstallDir(); - - try { - yield this._createDirectoryStructure(); - this._copyPrebuiltFiles(); - yield this._createConfigFiles(); - - if (aZipPath) { - yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir, - "application.zip")); - } - - yield this.getIcon(); - - // Remove previously installed app - this._removeInstallation(true); - } catch (ex) { - removeFiles([this.tmpInstallDir]); - throw(ex); - } - - try { - // Move the temp installation directory to the /Applications directory - yield moveDirectory(this.tmpInstallDir, this.installDir, []); - } catch (ex) { - this._removeInstallation(false); - throw(ex); - } - }.bind(this)); - }, - - _getInstallDir: function() { - let [ oldUniqueName, installPath ] = WebappOSUtils.getLaunchTarget(this.app); - if (installPath) { - this.installDir = installPath; - - if (this.uniqueName != oldUniqueName) { - // Bug 919799: If the app is still in the registry, migrate its data to - // the new format. - throw("Updates for apps installed with the old naming scheme unsupported"); - } - } else { - let localAppDir = getFile(LOCAL_APP_DIR); - if (!localAppDir.isWritable()) { - throw("Not enough privileges to install apps"); - } - - let destinationName = yield getAvailableFileName([ LOCAL_APP_DIR ], - this.appNameAsFilename, - ".app"); - - this.installDir = OS.Path.join(LOCAL_APP_DIR, destinationName); - } - }, - - _removeInstallation: function(keepProfile) { - let filesToRemove = [this.installDir]; - - if (!keepProfile) { - filesToRemove.push(this.appProfileDir); - } - - removeFiles(filesToRemove); - }, - - _createDirectoryStructure: function() { - let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app"); - dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); - this.tmpInstallDir = dir.path; - - yield OS.File.makeDir(this.appProfileDir, - { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); - - yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.contentsDir), - { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); - - yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.macOSDir), - { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); - - yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.resourcesDir), - { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); - }, - - _copyPrebuiltFiles: function() { - let destDir = getFile(this.tmpInstallDir, this.macOSDir); - let stub = getFile(this.runtimeFolder, "webapprt-stub"); - stub.copyTo(destDir, "webapprt"); - }, - - _createConfigFiles: function() { - // ${ProfileDir}/webapp.json - yield writeToFile(OS.Path.join(this.appProfileDir, "webapp.json"), - JSON.stringify(this.webappJson)); - - // ${InstallDir}/Contents/MacOS/webapp.ini - let applicationINI = getFile(this.tmpInstallDir, this.macOSDir, "webapp.ini"); - - let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"] - .getService(Ci.nsIINIParserFactory) - .createINIParser(applicationINI) - .QueryInterface(Ci.nsIINIParserWriter); - writer.setString("Webapp", "Name", this.appName); - writer.setString("Webapp", "Profile", OS.Path.basename(this.appProfileDir)); - writer.writeFile(); - applicationINI.permissions = PERMS_FILE; - - // ${InstallDir}/Contents/Info.plist - let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\ -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\ -<plist version="1.0">\n\ - <dict>\n\ - <key>CFBundleDevelopmentRegion</key>\n\ - <string>English</string>\n\ - <key>CFBundleDisplayName</key>\n\ - <string>' + escapeXML(this.appName) + '</string>\n\ - <key>CFBundleExecutable</key>\n\ - <string>webapprt</string>\n\ - <key>CFBundleIconFile</key>\n\ - <string>appicon</string>\n\ - <key>CFBundleIdentifier</key>\n\ - <string>' + escapeXML(this.uniqueName) + '</string>\n\ - <key>CFBundleInfoDictionaryVersion</key>\n\ - <string>6.0</string>\n\ - <key>CFBundleName</key>\n\ - <string>' + escapeXML(this.appName) + '</string>\n\ - <key>CFBundlePackageType</key>\n\ - <string>APPL</string>\n\ - <key>CFBundleVersion</key>\n\ - <string>0</string>\n\ - <key>NSHighResolutionCapable</key>\n\ - <true/>\n\ - <key>NSPrincipalClass</key>\n\ - <string>GeckoNSApplication</string>\n\ - <key>FirefoxBinary</key>\n\ -#expand <string>__MOZ_MACBUNDLE_ID__</string>\n\ - </dict>\n\ -</plist>'; - - yield writeToFile(OS.Path.join(this.tmpInstallDir, this.contentsDir, "Info.plist"), - infoPListContent); - }, - - /** - * Process the icon from the imageStream as retrieved from - * the URL by getIconForApp(). This will bundle the icon to the - * app package at Contents/Resources/appicon.icns. - * - * @param aMimeType the icon mimetype - * @param aImageStream the stream for the image data - */ - processIcon: function(aMimeType, aIcon) { - let deferred = Promise.defer(); - - function conversionDone(aSubject, aTopic) { - if (aTopic == "process-finished") { - deferred.resolve(); - } else { - deferred.reject("Failure converting icon."); - } - } - - let process = Cc["@mozilla.org/process/util;1"]. - createInstance(Ci.nsIProcess); - let sipsFile = getFile("/usr/bin/sips"); - - process.init(sipsFile); - process.runAsync(["-s", - "format", "icns", - aIcon.path, - "--out", OS.Path.join(this.tmpInstallDir, this.iconFile), - "-z", "128", "128"], - 9, conversionDone); - - return deferred.promise; - } - -} +#include MacNativeApp.js #elifdef XP_UNIX -function LinuxNativeApp(aData) { - NativeApp.call(this, aData); - - this.iconFile = "icon.png"; - this.webapprt = "webapprt-stub"; - this.configJson = "webapp.json"; - this.webappINI = "webapp.ini"; - - let xdg_data_home = Cc["@mozilla.org/process/environment;1"] - .getService(Ci.nsIEnvironment) - .get("XDG_DATA_HOME"); - if (!xdg_data_home) { - xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share"); - } - - this.desktopINI = OS.Path.join(xdg_data_home, "applications", - "owa-" + this.uniqueName + ".desktop"); -} - -LinuxNativeApp.prototype = { - __proto__: NativeApp.prototype, - - install: function(aZipPath) { - return Task.spawn(function() { - this._getInstallDir(); - - try { - this._createDirectoryStructure(); - this._copyPrebuiltFiles(); - yield this._createConfigFiles(); - - if (aZipPath) { - yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir, - "application.zip")); - } - - yield this.getIcon(); - - // Remove previously installed app - this._removeInstallation(true); - } catch (ex) { - removeFiles([this.tmpInstallDir]); - throw(ex); - } - - try { - yield moveDirectory(this.tmpInstallDir, this.installDir, []); - - this._createSystemFiles(); - } catch (ex) { - this._removeInstallation(false); - throw(ex); - } - }.bind(this)); - }, - - _getInstallDir: function() { - // The ${InstallDir} and desktop entry filename are: sanitized app name + - // "-" + manifest url hash - this.installDir = WebappOSUtils.getInstallPath(this.app); - if (this.installDir) { - let baseName = OS.Path.basename(this.installDir) - let oldUniqueName = baseName.substring(1, baseName.length); - if (this.uniqueName != oldUniqueName) { - // Bug 919799: If the app is still in the registry, migrate its data to - // the new format. - throw("Updates for apps installed with the old naming scheme unsupported"); - } - } else { - this.installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName); - } - }, - - _removeInstallation: function(keepProfile) { - let filesToRemove = [this.desktopINI]; - - if (keepProfile) { - [ this.iconFile, this.webapprt, - this.configJson, this.webappINI ].forEach((filePath) => { - filesToRemove.push(OS.Path.join(this.installDir, filePath)); - }); - } else { - filesToRemove.push(this.installDir); - filesToRemove.push(this.tmpInstallDir); - } - - removeFiles(filesToRemove); - }, - - _createDirectoryStructure: function() { - let dir = getFile(TMP_DIR, this.uniqueName); - dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); - this.tmpInstallDir = dir.path; - }, - - _copyPrebuiltFiles: function() { - let destDir = getFile(this.tmpInstallDir); - let stub = getFile(this.runtimeFolder, this.webapprt); - - stub.copyTo(destDir, null); - }, - - /** - * Translate marketplace categories to freedesktop.org categories. - * - * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry - * - * @return an array of categories - */ - _translateCategories: function() { - let translations = { - "books": "Education;Literature", - "business": "Finance", - "education": "Education", - "entertainment": "Amusement", - "sports": "Sports", - "games": "Game", - "health-fitness": "MedicalSoftware", - "lifestyle": "Amusement", - "music": "Audio;Music", - "news-weather": "News", - "photo-video": "Video;AudioVideo;Photography", - "productivity": "Office", - "shopping": "Amusement", - "social": "Chat", - "travel": "Amusement", - "reference": "Science;Education;Documentation", - "maps-navigation": "Maps", - "utilities": "Utility" - }; - - // The trailing semicolon is needed as written in the freedesktop specification - let categories = ""; - for (let category of this.categories) { - let catLower = category.toLowerCase(); - if (catLower in translations) { - categories += translations[catLower] + ";"; - } - } - - return categories; - }, - - _createConfigFiles: function() { - // ${InstallDir}/webapp.json - yield writeToFile(OS.Path.join(this.tmpInstallDir, this.configJson), - JSON.stringify(this.webappJson)); - - let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); - - // ${InstallDir}/webapp.ini - let webappINIfile = getFile(this.tmpInstallDir, this.webappINI); - - let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"] - .getService(Ci.nsIINIParserFactory) - .createINIParser(webappINIfile) - .QueryInterface(Ci.nsIINIParserWriter); - writer.setString("Webapp", "Name", this.appName); - writer.setString("Webapp", "Profile", this.uniqueName); - writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1)); - writer.setString("WebappRT", "InstallDir", this.runtimeFolder); - writer.writeFile(); - }, - - _createSystemFiles: function() { - let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties"); - - let webapprtPath = OS.Path.join(this.installDir, this.webapprt); - - // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop - let desktopINIfile = getFile(this.desktopINI); - - let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"] - .getService(Ci.nsIINIParserFactory) - .createINIParser(desktopINIfile) - .QueryInterface(Ci.nsIINIParserWriter); - writer.setString("Desktop Entry", "Name", this.appName); - writer.setString("Desktop Entry", "Comment", this.shortDescription); - writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"'); - writer.setString("Desktop Entry", "Icon", OS.Path.join(this.installDir, - this.iconFile)); - writer.setString("Desktop Entry", "Type", "Application"); - writer.setString("Desktop Entry", "Terminal", "false"); - - let categories = this._translateCategories(); - if (categories) - writer.setString("Desktop Entry", "Categories", categories); - - writer.setString("Desktop Entry", "Actions", "Uninstall;"); - writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label")); - writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove"); - - writer.writeFile(); - - desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR; - }, - - /** - * Process the icon from the imageStream as retrieved from - * the URL by getIconForApp(). - * - * @param aMimeType ahe icon mimetype - * @param aImageStream the stream for the image data - */ - processIcon: function(aMimeType, aImageStream) { - let deferred = Promise.defer(); - - let imgTools = Cc["@mozilla.org/image/tools;1"] - .createInstance(Ci.imgITools); - - let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); - let iconStream = imgTools.encodeImage(imgContainer, "image/png"); - - let iconFile = getFile(this.tmpInstallDir, this.iconFile); - let outputStream = FileUtils.openSafeFileOutputStream(iconFile); - NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { - if (Components.isSuccessCode(aResult)) { - deferred.resolve(); - } else { - deferred.reject("Failure copying icon: " + aResult); - } - }); - - return deferred.promise; - } -} +#include LinuxNativeApp.js #endif /* Helper Functions */ /** * Async write a data string into a file * @@ -1106,26 +271,39 @@ function writeToFile(aPath, aData) { * Removes unprintable characters from a string. */ function sanitize(aStr) { let unprintableRE = new RegExp("[\\x00-\\x1F\\x7F]" ,"gi"); return aStr.replace(unprintableRE, ""); } /** - * Strips all non-word characters from the beginning and end of a string + * Strips all non-word characters from the beginning and end of a string. + * Strips invalid characters from the string. + * */ function stripStringForFilename(aPossiblyBadFilenameString) { - //strip everything from the front up to the first [0-9a-zA-Z] + // Strip everything from the front up to the first [0-9a-zA-Z] + let stripFrontRE = new RegExp("^\\W*", "gi"); - let stripFrontRE = new RegExp("^\\W*","gi"); - let stripBackRE = new RegExp("\\s*$","gi"); + // Strip white space characters starting from the last [0-9a-zA-Z] + let stripBackRE = new RegExp("\\s*$", "gi"); + + // Strip invalid characters from the filename + let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi"); let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, ""); stripped = stripped.replace(stripBackRE, ""); + stripped = stripped.replace(filenameRE, ""); + + // If the filename ends up empty, let's call it "webapp". + if (stripped == "") { + stripped = "webapp"; + } + return stripped; } /** * Finds a unique name available in a folder (i.e., non-existent file) * * @param aPathSet a set of paths that represents the set of * directories where we want to write @@ -1187,40 +365,29 @@ function removeFiles(aPaths) { } } /** * Move (overwriting) the contents of one directory into another. * * @param srcPath A path to the source directory * @param destPath A path to the destination directory - * @param filesToIgnore An array of files. If one of those files can't be - * overwritten the function will not fail. */ -function moveDirectory(srcPath, destPath, filesToIgnore) { +function moveDirectory(srcPath, destPath) { let srcDir = getFile(srcPath); let destDir = getFile(destPath); let entries = srcDir.directoryEntries; let array = []; while (entries.hasMoreElements()) { let entry = entries.getNext().QueryInterface(Ci.nsIFile); - if (entry.isDirectory()) { yield moveDirectory(entry.path, OS.Path.join(destPath, entry.leafName)); } else { - try { - entry.moveTo(destDir, entry.leafName); - } catch (ex if ex.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) { - if (filesToIgnore.indexOf(entry.leafName) != -1) { - yield OS.File.remove(entry.path); - } else { - throw(ex); - } - } + entry.moveTo(destDir, entry.leafName); } } // The source directory is now empty, remove it. yield OS.File.removeEmptyDir(srcPath); } function escapeXML(aStr) {
--- a/toolkit/webapps/WebappOSUtils.jsm +++ b/toolkit/webapps/WebappOSUtils.jsm @@ -4,16 +4,17 @@ const Cc = Components.classes; const Ci = Components.interfaces; const CC = Components.Constructor; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); this.EXPORTED_SYMBOLS = ["WebappOSUtils"]; // Returns the MD5 hash of a string. function computeHash(aString) { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; @@ -199,16 +200,26 @@ this.WebappOSUtils = { return execFile.parent.path; #endif #endif // Anything unsupported, like Metro throw new Error("Unsupported apps platform"); }, + getPackagePath: function(aApp) { + let packagePath = this.getInstallPath(aApp); + +#ifdef XP_MACOSX + packagePath = OS.Path.join(packagePath, "Contents", "Resources"); +#endif + + return packagePath; + }, + launch: function(aApp) { let uniqueName = this.getUniqueName(aApp); #ifdef XP_WIN let initProcess = CC("@mozilla.org/process/util;1", "nsIProcess", "init"); let launchTarget = this.getLaunchTarget(aApp);
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/WinNativeApp.js @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir; +const APP_DATA_DIR = OS.Constants.Path.winAppDataDir; + +/************************************* + * Windows app installer + * + * The Windows installation process will generate the following files: + * + * ${FolderName} = sanitized app name + "-" + manifest url hash + * + * %APPDATA%/${FolderName} + * - webapp.ini + * - webapp.json + * - ${AppName}.exe + * - ${AppName}.lnk + * / uninstall + * - webapp-uninstaller.exe + * - shortcuts_log.ini + * - uninstall.log + * / chrome/icons/default/ + * - default.ico + * + * After the app runs for the first time, a profiles/ folder will also be + * created which will host the user profile for this app. + */ + +/** + * Constructor for the Windows native app shell + * + * @param aApp {Object} the app object provided to the install function + * @param aManifest {Object} the manifest data provided by the web app + * @param aCategories {Array} array of app categories + * @param aRegistryDir {String} (optional) path to the registry + */ +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { + CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); + + if (this.isPackaged) { + this.size = aApp.updateManifest.size / 1024; + } + + this.webapprt = this.appNameAsFilename + ".exe"; + this.configJson = "webapp.json"; + this.webappINI = "webapp.ini"; + this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico"); + this.uninstallDir = "uninstall"; + this.uninstallerFile = OS.Path.join(this.uninstallDir, + "webapp-uninstaller.exe"); + this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini"); + this.zipFile = "application.zip"; + + this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ]; + if (this.isPackaged) { + this.backupFiles.push(this.zipFile); + } + + this.uninstallSubkeyStr = this.uniqueName; +} + +NativeApp.prototype = { + __proto__: CommonNativeApp.prototype, + size: null, + + /** + * Creates a native installation of the web app in the OS + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + install: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + // If the application is already installed, this is a reinstallation. + if (WebappOSUtils.getInstallPath(this.app)) { + return yield this.prepareUpdate(aManifest, aZipPath); + } + + this._setData(aManifest); + + let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName); + + // Create a temporary installation directory. + let dir = getFile(TMP_DIR, this.uniqueName); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + let tmpDir = dir.path; + + // Perform the installation in the temp directory. + try { + yield this._createDirectoryStructure(tmpDir); + yield this._getShortcutName(installDir); + yield this._copyWebapprt(tmpDir); + yield this._copyUninstaller(tmpDir); + yield this._createConfigFiles(tmpDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); + } + + yield this._getIcon(tmpDir); + } catch (ex) { + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + + // Apply the installation. + this._removeInstallation(true, installDir); + + try { + yield this._applyTempInstallation(tmpDir, installDir); + } catch (ex) { + this._removeInstallation(false, installDir); + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Creates an update in a temporary directory to be applied later. + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + prepareUpdate: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + this._setData(aManifest); + + let installDir = WebappOSUtils.getInstallPath(this.app); + if (!installDir) { + throw ERR_NOT_INSTALLED; + } + + if (this.uniqueName != OS.Path.basename(installDir)) { + // Bug 919799: If the app is still in the registry, migrate its data to + // the new format. + throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; + } + + let updateDir = OS.Path.join(installDir, "update"); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + yield OS.File.makeDir(updateDir); + + // Perform the update in the "update" subdirectory. + try { + yield this._createDirectoryStructure(updateDir); + yield this._getShortcutName(installDir); + yield this._copyUninstaller(updateDir); + yield this._createConfigFiles(updateDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); + } + + yield this._getIcon(updateDir); + } catch (ex) { + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Applies an update. + */ + applyUpdate: Task.async(function*() { + if (this._dryRun) { + return; + } + + let installDir = WebappOSUtils.getInstallPath(this.app); + let updateDir = OS.Path.join(installDir, "update"); + + yield this._getShortcutName(installDir); + + let backupDir = yield this._backupInstallation(installDir); + + try { + yield this._applyTempInstallation(updateDir, installDir); + } catch (ex) { + yield this._restoreInstallation(backupDir, installDir); + throw ex; + } finally { + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + } + }), + + _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { + yield moveDirectory(aTmpDir, aInstallDir); + + this._createShortcutFiles(aInstallDir); + this._writeSystemKeys(aInstallDir); + }), + + _getShortcutName: Task.async(function*(aInstallDir) { + let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI); + + if (shortcutLogsINIfile.exists()) { + // If it's a reinstallation (or an update) get the shortcut names + // from the shortcut_log.ini file + let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(shortcutLogsINIfile); + this.shortcutName = parser.getString("STARTMENU", "Shortcut0"); + } else { + // Check in both directories to see if a shortcut with the same name + // already exists. + this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ], + this.appNameAsFilename, + ".lnk"); + } + }), + + /** + * Remove the current installation + */ + _removeInstallation: function(keepProfile, aInstallDir) { + let uninstallKey; + try { + uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Microsoft\\Windows\\" + + "CurrentVersion\\Uninstall", + uninstallKey.ACCESS_WRITE); + if (uninstallKey.hasChild(this.uninstallSubkeyStr)) { + uninstallKey.removeChild(this.uninstallSubkeyStr); + } + } catch (e) { + } finally { + if (uninstallKey) { + uninstallKey.close(); + } + } + + let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName), + OS.Path.join(PROGS_DIR, this.shortcutName) ]; + + if (keepProfile) { + for (let filePath of this.backupFiles) { + filesToRemove.push(OS.Path.join(aInstallDir, filePath)); + } + + filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt)); + } else { + filesToRemove.push(aInstallDir); + } + + removeFiles(filesToRemove); + }, + + _backupInstallation: Task.async(function*(aInstallDir) { + let backupDir = OS.Path.join(aInstallDir, "backup"); + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.makeDir(backupDir); + + for (let filePath of this.backupFiles) { + yield OS.File.move(OS.Path.join(aInstallDir, filePath), + OS.Path.join(backupDir, filePath)); + } + + return backupDir; + }), + + _restoreInstallation: function(aBackupDir, aInstallDir) { + return moveDirectory(aBackupDir, aInstallDir); + }, + + /** + * Creates the main directory structure. + */ + _createDirectoryStructure: Task.async(function*(aDir) { + yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir)); + + // Recursively create the icon path's directory structure. + let path = aDir; + let components = OS.Path.split(OS.Path.dirname(this.iconPath)).components; + for (let component of components) { + path = OS.Path.join(path, component); + yield OS.File.makeDir(path); + } + }), + + /** + * Copy the webrt executable into the installation directory. + */ + _copyWebapprt: function(aDir) { + return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"), + OS.Path.join(aDir, this.webapprt)); + }, + + /** + * Copy the uninstaller executable into the installation directory. + */ + _copyUninstaller: function(aDir) { + return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"), + OS.Path.join(aDir, this.uninstallerFile)); + }, + + /** + * Creates the configuration files into their destination folders. + */ + _createConfigFiles: function(aDir) { + // ${InstallDir}/webapp.json + yield writeToFile(OS.Path.join(aDir, this.configJson), + JSON.stringify(this.webappJson)); + + let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory); + + // ${InstallDir}/webapp.ini + let webappINIfile = getFile(aDir, this.webappINI); + + let writer = factory.createINIParser(webappINIfile) + .QueryInterface(Ci.nsIINIParserWriter); + writer.setString("Webapp", "Name", this.appName); + writer.setString("Webapp", "Profile", this.uniqueName); + writer.setString("Webapp", "Executable", this.appNameAsFilename); + writer.setString("WebappRT", "InstallDir", this.runtimeFolder); + writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); + + let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI); + + writer = factory.createINIParser(shortcutLogsINIfile) + .QueryInterface(Ci.nsIINIParserWriter); + writer.setString("STARTMENU", "Shortcut0", this.shortcutName); + writer.setString("DESKTOP", "Shortcut0", this.shortcutName); + writer.setString("TASKBAR", "Migrated", "true"); + writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16); + + // ${UninstallDir}/uninstall.log + let uninstallContent = + "File: \\webapp.ini\r\n" + + "File: \\webapp.json\r\n" + + "File: \\webapprt.old\r\n" + + "File: \\chrome\\icons\\default\\default.ico"; + if (this.isPackaged) { + uninstallContent += "\r\nFile: \\application.zip"; + } + + yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"), + uninstallContent); + }, + + /** + * Writes the keys to the system registry that are necessary for the app + * operation and uninstall process. + */ + _writeSystemKeys: function(aInstallDir) { + let parentKey; + let uninstallKey; + let subKey; + + try { + parentKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + parentKey.open(parentKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", + parentKey.ACCESS_WRITE); + uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE) + subKey = uninstallKey.createChild(this.uninstallSubkeyStr, + uninstallKey.ACCESS_WRITE); + + subKey.writeStringValue("DisplayName", this.appName); + + let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile); + + subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"'); + subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"'); + subKey.writeStringValue("AppFilename", this.appNameAsFilename); + subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir, + this.iconPath)); + + let date = new Date(); + let year = date.getYear().toString(); + let month = date.getMonth(); + if (month < 10) { + month = "0" + month; + } + let day = date.getDate(); + if (day < 10) { + day = "0" + day; + } + subKey.writeStringValue("InstallDate", year + month + day); + if (this.version) { + subKey.writeStringValue("DisplayVersion", this.version); + } + if (this.developerName) { + subKey.writeStringValue("Publisher", this.developerName); + } + subKey.writeStringValue("URLInfoAbout", this.developerUrl); + if (this.size) { + subKey.writeIntValue("EstimatedSize", this.size); + } + + subKey.writeIntValue("NoModify", 1); + subKey.writeIntValue("NoRepair", 1); + } catch(ex) { + throw ex; + } finally { + if(subKey) subKey.close(); + if(uninstallKey) uninstallKey.close(); + if(parentKey) parentKey.close(); + } + }, + + /** + * Creates a shortcut file inside the app installation folder and makes + * two copies of it: one into the desktop and one into the start menu. + */ + _createShortcutFiles: function(aInstallDir) { + let shortcut = getFile(aInstallDir, this.shortcutName). + QueryInterface(Ci.nsILocalFileWin); + + /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args, + description, iconFile, iconIndex) */ + + shortcut.setShortcut(getFile(aInstallDir, this.webapprt), + getFile(aInstallDir), + null, + this.shortDescription, + getFile(aInstallDir, this.iconPath), + 0); + + shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName); + shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName); + + shortcut.followLinks = false; + shortcut.remove(false); + }, + + /** + * Process the icon from the imageStream as retrieved from + * the URL by getIconForApp(). This will save the icon to the + * topwindow.ico file. + * + * @param aMimeType the icon mimetype + * @param aImageStream the stream for the image data + * @param aDir the directory where the icon should be stored + */ + _processIcon: function(aMimeType, aImageStream, aDir) { + let deferred = Promise.defer(); + + let imgTools = Cc["@mozilla.org/image/tools;1"]. + createInstance(Ci.imgITools); + + let imgContainer = imgTools.decodeImage(aImageStream, aMimeType); + let iconStream = imgTools.encodeImage(imgContainer, + "image/vnd.microsoft.icon", + "format=bmp;bpp=32"); + + let tmpIconFile = getFile(aDir, this.iconPath); + + let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile); + NetUtil.asyncCopy(iconStream, outputStream, function(aResult) { + if (Components.isSuccessCode(aResult)) { + deferred.resolve(); + } else { + deferred.reject("Failure copying icon: " + aResult); + } + }); + + return deferred.promise; + } +}
--- a/toolkit/webapps/moz.build +++ b/toolkit/webapps/moz.build @@ -1,13 +1,15 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_PP_JS_MODULES += [ + 'NativeApp.jsm', 'WebappOSUtils.jsm', - 'WebappsInstaller.jsm', ] +MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] + if CONFIG['MOZ_BUILD_APP'] == 'mobile/android': DEFINES['MOZ_FENNEC'] = True
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/tests/chrome.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + head.js + +[test_hosted.xul] +skip-if = os == "mac" +[test_packaged.xul] +skip-if = os == "mac"
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/tests/head.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +function checkFiles(files) { + return Task.spawn(function*() { + for (let file of files) { + if (!(yield OS.File.exists(file))) { + info("File doesn't exist: " + file); + return false; + } + } + + return true; + }); +} + +function checkDateHigherThan(files, date) { + return Task.spawn(function*() { + for (let file of files) { + if (!(yield OS.File.exists(file))) { + info("File doesn't exist: " + file); + return false; + } + + let stat = yield OS.File.stat(file); + if (!(stat.lastModificationDate > date)) { + info("File not newer: " + file); + return false; + } + } + + return true; + }); +} + +function wait(time) { + let deferred = Promise.defer(); + + setTimeout(function() { + deferred.resolve(); + }, time); + + return deferred.promise; +}
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/tests/test_hosted.xul @@ -0,0 +1,311 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=898647 +--> +<window title="Mozilla Bug 898647" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="head.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=898647" + target="_blank">Mozilla Bug 898647</a> + </body> + +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 898647 **/ + +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NativeApp.jsm"); +Cu.import("resource://gre/modules/WebappOSUtils.jsm"); + +let manifest = { + name: "Sample hosted app", +}; + +let app = { + name: "Sample hosted app", + manifestURL: "http://example.com/sample.manifest", + manifest: manifest, + origin: "http://example.com/", + categories: [], + installOrigin: "http://example.com/", + receipts: [], + installTime: Date.now(), +}; + +let profileDir; +let profilesIni; +let installPath; + +let installedFiles; +let tempUpdatedFiles; +let updatedFiles; + +let cleanup; + +if (navigator.platform.startsWith("Linux")) { + installPath = OS.Path.join(OS.Constants.Path.homeDir, "." + WebappOSUtils.getUniqueName(app)); + + let xdg_data_home = Cc["@mozilla.org/process/environment;1"]. + getService(Ci.nsIEnvironment). + get("XDG_DATA_HOME"); + if (!xdg_data_home) { + xdg_data_home = OS.Path.join(OS.Constants.Path.homeDir, ".local", "share"); + } + + let desktopINI = OS.Path.join(xdg_data_home, "applications", + "owa-" + WebappOSUtils.getUniqueName(app) + ".desktop"); + + installedFiles = [ + OS.Path.join(installPath, "icon.png"), + OS.Path.join(installPath, "webapprt-stub"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + desktopINI, + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "icon.png"), + OS.Path.join(installPath, "update", "webapp.json"), + OS.Path.join(installPath, "update", "webapp.ini"), + ]; + updatedFiles = [ + OS.Path.join(installPath, "icon.png"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + desktopINI, + ]; + + profilesIni = OS.Path.join(installPath, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.remove(desktopINI, { ignoreAbsent: true }); + }); + }; +} else if (navigator.platform.startsWith("Win")) { + installPath = OS.Path.join(OS.Constants.Path.winAppDataDir, WebappOSUtils.getUniqueName(app)); + + let desktopShortcut = OS.Path.join(OS.Constants.Path.desktopDir, "Sample hosted app.lnk"); + let startMenuShortcut = OS.Path.join(OS.Constants.Path.winStartMenuProgsDir, "Sample hosted app.lnk"); + + installedFiles = [ + OS.Path.join(installPath, "Sample hosted app.exe"), + OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "uninstall", "uninstall.log"), + OS.Path.join(installPath, "uninstall", "webapp-uninstaller.exe"), + desktopShortcut, + startMenuShortcut, + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "update", "webapp.json"), + OS.Path.join(installPath, "update", "webapp.ini"), + OS.Path.join(installPath, "update", "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "update", "uninstall", "uninstall.log"), + OS.Path.join(installPath, "update", "uninstall", "webapp-uninstaller.exe"), + ]; + updatedFiles = [ + OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "uninstall", "uninstall.log"), + desktopShortcut, + startMenuShortcut, + ]; + + profilesIni = OS.Path.join(installPath, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + let uninstallKey; + try { + uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + uninstallKey.ACCESS_WRITE); + if (uninstallKey.hasChild(WebappOSUtils.getUniqueName(app))) { + uninstallKey.removeChild(WebappOSUtils.getUniqueName(app)); + } + } catch (e) { + } finally { + if (uninstallKey) { + uninstallKey.close(); + } + } + + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.remove(desktopShortcut, { ignoreAbsent: true }); + yield OS.File.remove(startMenuShortcut, { ignoreAbsent: true }); + }); + }; +} else if (navigator.platform.startsWith("Mac")) { + installPath = OS.Path.join(OS.Constants.Path.macLocalApplicationsDir, "Sample hosted app.app"); + let appProfileDir = OS.Path.join(OS.Constants.Path.macUserLibDir, "Application Support", + WebappOSUtils.getUniqueName(app)); + + installedFiles = [ + OS.Path.join(installPath, "Contents", "Info.plist"), + OS.Path.join(installPath, "Contents", "MacOS", "webapprt"), + OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"), + OS.Path.join(appProfileDir, "webapp.json"), + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "Contents", "Info.plist"), + OS.Path.join(installPath, "update", "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "update", "Contents", "Resources", "appicon.icns"), + OS.Path.join(installPath, "update", "webapp.json") + ]; + updatedFiles = [ + OS.Path.join(installPath, "Contents", "Info.plist"), + OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"), + OS.Path.join(appProfileDir, "webapp.json"), + ]; + + profilesIni = OS.Path.join(appProfileDir, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.removeDir(appProfileDir, { ignoreAbsent: true }); + }); + }; +} + +let old_dry_run; +try { + old_dry_run = Services.prefs.getBoolPref("browser.mozApps.installer.dry_run"); +} catch (ex) {} + +Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", false); + +SimpleTest.registerCleanupFunction(function() { + if (old_dry_run === undefined) { + Services.prefs.clearUserPref("browser.mozApps.installer.dry_run"); + } else { + Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", old_dry_run); + } + + cleanup(); +}); + +Task.spawn(function() { + // Get to a clean state before the test + yield cleanup(); + + let nativeApp = new NativeApp(app, manifest, app.categories); + ok(nativeApp, "NativeApp object created"); + + info("Test update for an uninstalled application"); + try { + yield nativeApp.prepareUpdate(manifest); + ok(false, "Didn't thrown"); + } catch (ex) { + is(ex, "The application isn't installed", "Exception thrown"); + } + + profileDir = nativeApp.createProfile(); + ok(profileDir && profileDir.exists(), "Profile directory created"); + ok((yield OS.File.exists(profilesIni)), "profiles.ini file created"); + + // Install application + info("Test installation"); + yield nativeApp.install(manifest); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Files correctly written"); + + let stat = yield OS.File.stat(installPath); + let installTime = stat.lastModificationDate; + + // Wait one second, otherwise the last modification date is the same. + yield wait(1000); + + // Reinstall application + info("Test reinstallation"); + yield nativeApp.install(manifest); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory"); + + yield nativeApp.applyUpdate(); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed"); + ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher"); + + stat = yield OS.File.stat(installPath); + installTime = stat.lastModificationDate; + + // Wait one second, otherwise the last modification date is the same. + yield wait(1000); + + // Update application + info("Test update"); + yield nativeApp.prepareUpdate(manifest); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory"); + + yield nativeApp.applyUpdate(); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed"); + ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher"); + + SimpleTest.finish(); +}).then(null, function(e) { + ok(false, "Error during test: " + e); + SimpleTest.finish(); +}); + +]]> +</script> +</window>
new file mode 100644 --- /dev/null +++ b/toolkit/webapps/tests/test_packaged.xul @@ -0,0 +1,337 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=898647 +--> +<window title="Mozilla Bug 898647" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="head.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=898647" + target="_blank">Mozilla Bug 898647</a> + </body> + +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 898647 **/ + +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NativeApp.jsm"); +Cu.import("resource://gre/modules/WebappOSUtils.jsm"); + +let zipPath = OS.Path.join(OS.Constants.Path.profileDir, "sample.zip"); + +let manifest = { + name: "Sample packaged app", + version: "0.1a", + size: 777, + package_path: "/sample.zip", +}; + +let app = { + name: "Sample packaged app", + manifestURL: "http://example.com/sample.manifest", + manifest: manifest, + updateManifest: manifest, + origin: "http://example.com/", + categories: [], + installOrigin: "http://example.com/", + receipts: [], + installTime: Date.now(), +}; + +let profileDir; +let profilesIni; +let installPath; + +let installedFiles; +let tempUpdatedFiles; +let updatedFiles; + +let cleanup; + +if (navigator.platform.startsWith("Linux")) { + installPath = OS.Path.join(OS.Constants.Path.homeDir, "." + WebappOSUtils.getUniqueName(app)); + + let xdg_data_home = Cc["@mozilla.org/process/environment;1"]. + getService(Ci.nsIEnvironment). + get("XDG_DATA_HOME"); + if (!xdg_data_home) { + xdg_data_home = OS.Path.join(OS.Constants.Path.homeDir, ".local", "share"); + } + + let desktopINI = OS.Path.join(xdg_data_home, "applications", + "owa-" + WebappOSUtils.getUniqueName(app) + ".desktop"); + + installedFiles = [ + OS.Path.join(installPath, "icon.png"), + OS.Path.join(installPath, "webapprt-stub"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "application.zip"), + desktopINI, + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "icon.png"), + OS.Path.join(installPath, "update", "webapp.json"), + OS.Path.join(installPath, "update", "webapp.ini"), + OS.Path.join(installPath, "update", "application.zip"), + ]; + updatedFiles = [ + OS.Path.join(installPath, "icon.png"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "application.zip"), + desktopINI, + ]; + + profilesIni = OS.Path.join(installPath, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.remove(desktopINI, { ignoreAbsent: true }); + }); + }; +} else if (navigator.platform.startsWith("Win")) { + installPath = OS.Path.join(OS.Constants.Path.winAppDataDir, WebappOSUtils.getUniqueName(app)); + + let desktopShortcut = OS.Path.join(OS.Constants.Path.desktopDir, "Sample packaged app.lnk"); + let startMenuShortcut = OS.Path.join(OS.Constants.Path.winStartMenuProgsDir, "Sample packaged app.lnk"); + + installedFiles = [ + OS.Path.join(installPath, "Sample packaged app.exe"), + OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "application.zip"), + OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "uninstall", "uninstall.log"), + OS.Path.join(installPath, "uninstall", "webapp-uninstaller.exe"), + desktopShortcut, + startMenuShortcut, + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "update", "webapp.json"), + OS.Path.join(installPath, "update", "webapp.ini"), + OS.Path.join(installPath, "update", "application.zip"), + OS.Path.join(installPath, "update", "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "update", "uninstall", "uninstall.log"), + OS.Path.join(installPath, "update", "uninstall", "webapp-uninstaller.exe"), + ]; + updatedFiles = [ + OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"), + OS.Path.join(installPath, "webapp.json"), + OS.Path.join(installPath, "webapp.ini"), + OS.Path.join(installPath, "application.zip"), + OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"), + OS.Path.join(installPath, "uninstall", "uninstall.log"), + desktopShortcut, + startMenuShortcut, + ]; + + profilesIni = OS.Path.join(installPath, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + let uninstallKey; + try { + uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + uninstallKey.ACCESS_WRITE); + if (uninstallKey.hasChild(WebappOSUtils.getUniqueName(app))) { + uninstallKey.removeChild(WebappOSUtils.getUniqueName(app)); + } + } catch (e) { + } finally { + if (uninstallKey) { + uninstallKey.close(); + } + } + + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.remove(desktopShortcut, { ignoreAbsent: true }); + yield OS.File.remove(startMenuShortcut, { ignoreAbsent: true }); + }); + }; +} else if (navigator.platform.startsWith("Mac")) { + installPath = OS.Path.join(OS.Constants.Path.macLocalApplicationsDir, "Sample packaged app.app"); + let appProfileDir = OS.Path.join(OS.Constants.Path.macUserLibDir, "Application Support", + WebappOSUtils.getUniqueName(app)); + + installedFiles = [ + OS.Path.join(installPath, "Contents", "Info.plist"), + OS.Path.join(installPath, "Contents", "MacOS", "webapprt"), + OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"), + OS.Path.join(installPath, "Contents", "Resources", "application.zip"), + OS.Path.join(appProfileDir, "webapp.json"), + ]; + tempUpdatedFiles = [ + OS.Path.join(installPath, "update", "Contents", "Info.plist"), + OS.Path.join(installPath, "update", "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "update", "Contents", "Resources", "appicon.icns"), + OS.Path.join(installPath, "update", "Contents", "Resources", "application.zip"), + OS.Path.join(installPath, "update", "webapp.json"), + ]; + updatedFiles = [ + OS.Path.join(installPath, "Contents", "Info.plist"), + OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"), + OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"), + OS.Path.join(installPath, "Contents", "Resources", "application.zip"), + OS.Path.join(appProfileDir, "webapp.json"), + ]; + + profilesIni = OS.Path.join(appProfileDir, "profiles.ini"); + + cleanup = function() { + return Task.spawn(function*() { + if (profileDir) { + yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true }); + } + + yield OS.File.removeDir(installPath, { ignoreAbsent: true }); + + yield OS.File.removeDir(appProfileDir, { ignoreAbsent: true }); + }); + }; +} + +let old_dry_run; +try { + old_dry_run = Services.prefs.getBoolPref("browser.mozApps.installer.dry_run"); +} catch (ex) {} + +Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", false); + +SimpleTest.registerCleanupFunction(function() { + if (old_dry_run === undefined) { + Services.prefs.clearUserPref("browser.mozApps.installer.dry_run"); + } else { + Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", old_dry_run); + } + + cleanup(); +}); + +Task.spawn(function() { + // Get to a clean state before the test + yield cleanup(); + + let zipFile = yield OS.File.open(zipPath, { create: true }); + yield zipFile.close(); + + let nativeApp = new NativeApp(app, manifest, app.categories); + ok(nativeApp, "NativeApp object created"); + + info("Test update for an application that isn't installed"); + try { + yield nativeApp.prepareUpdate(manifest, zipPath); + ok(false, "Didn't thrown"); + } catch (ex) { + is(ex, "The application isn't installed", "Exception thrown"); + } + + profileDir = nativeApp.createProfile(); + ok(profileDir && profileDir.exists(), "Profile directory created"); + ok((yield OS.File.exists(profilesIni)), "profiles.ini file created"); + + // Install application + info("Test installation"); + yield nativeApp.install(manifest, zipPath); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Files correctly written"); + + let stat = yield OS.File.stat(installPath); + let installTime = stat.lastModificationDate; + + // Wait one second, otherwise the last modification date is the same. + yield wait(1000); + + // Reinstall application + info("Test reinstallation"); + + zipFile = yield OS.File.open(zipPath, { create: true }); + yield zipFile.close(); + + yield nativeApp.install(manifest, zipPath); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory"); + + yield nativeApp.applyUpdate(); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed"); + ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher"); + + stat = yield OS.File.stat(installPath); + installTime = stat.lastModificationDate; + + // Wait one second, otherwise the last modification date is the same. + yield wait(1000); + + // Update application + info("Test update"); + + zipFile = yield OS.File.open(zipPath, { create: true }); + yield zipFile.close(); + + yield nativeApp.prepareUpdate(manifest, zipPath); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory"); + + yield nativeApp.applyUpdate(); + while (!WebappOSUtils.isLaunchable(app)) { + yield wait(1000); + } + ok(true, "App launchable"); + ok((yield checkFiles(installedFiles)), "Installation not corrupted"); + ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed"); + ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher"); + + SimpleTest.finish(); +}).then(null, function(e) { + ok(false, "Error during test: " + e); + SimpleTest.finish(); +}); + +]]> +</script> +</window>
--- a/webapprt/Startup.jsm +++ b/webapprt/Startup.jsm @@ -8,27 +8,23 @@ * loaded. */ this.EXPORTED_SYMBOLS = ["startup"]; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); -// Initialize DOMApplicationRegistry by importing Webapps.jsm. -Cu.import("resource://gre/modules/Webapps.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); Cu.import("resource://gre/modules/PermissionsInstaller.jsm"); Cu.import('resource://gre/modules/Payment.jsm'); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); -// Initialize window-independent handling of webapps- notifications. -Cu.import("resource://webapprt/modules/WebappsHandler.jsm"); Cu.import("resource://webapprt/modules/WebappRT.jsm"); Cu.import("resource://webapprt/modules/WebRTCHandler.jsm"); const PROFILE_DIR = OS.Constants.Path.profileDir; function isFirstRunOrUpdate() { let savedBuildID = null; try { @@ -88,25 +84,37 @@ this.startup = function(window) { deferredWindowLoad.resolve(); } else { window.addEventListener("DOMContentLoaded", function onLoad() { window.removeEventListener("DOMContentLoaded", onLoad, false); deferredWindowLoad.resolve(); }); } + let appUpdated = false; + let updatePending = yield WebappRT.isUpdatePending(); + if (updatePending) { + appUpdated = yield WebappRT.applyUpdate(); + } + + yield WebappRT.loadConfig(); + + // Initialize DOMApplicationRegistry by importing Webapps.jsm. + Cu.import("resource://gre/modules/Webapps.jsm"); + // Initialize window-independent handling of webapps- notifications. + Cu.import("resource://webapprt/modules/WebappManager.jsm"); + // Wait for webapps registry loading. yield DOMApplicationRegistry.registryStarted; let manifestURL = WebappRT.config.app.manifestURL; if (manifestURL) { // On firstrun, set permissions to their default values. // When the webapp runtime is updated, update the permissions. - // TODO: Update the permissions when the application is updated. - if (isFirstRunOrUpdate(Services.prefs)) { + if (isFirstRunOrUpdate(Services.prefs) || appUpdated) { PermissionsInstaller.installPermissions(WebappRT.config.app, true); yield createBrandingFiles(); } } // Branding substitution let aliasFile = Components.classes["@mozilla.org/file/local;1"] .createInstance(Ci.nsIFile); @@ -130,10 +138,12 @@ this.startup = function(window) { if (WebappRT.config.app.manifest.fullscreen) { appBrowser.addEventListener("load", function onLoad() { appBrowser.removeEventListener("load", onLoad, true); appBrowser.contentDocument. documentElement.mozRequestFullScreen(); }, true); } + + WebappRT.startUpdateService(); }).then(null, Cu.reportError.bind(Cu)); }
rename from webapprt/WebappsHandler.jsm rename to webapprt/WebappManager.jsm --- a/webapprt/WebappsHandler.jsm +++ b/webapprt/WebappManager.jsm @@ -1,28 +1,29 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -this.EXPORTED_SYMBOLS = ["WebappsHandler"]; +this.EXPORTED_SYMBOLS = ["WebappManager"]; let Cc = Components.classes; let Ci = Components.interfaces; let Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Webapps.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); -Cu.import("resource://gre/modules/WebappsInstaller.jsm"); +Cu.import("resource://gre/modules/NativeApp.jsm"); Cu.import("resource://gre/modules/WebappOSUtils.jsm"); +Cu.import("resource://webapprt/modules/WebappRT.jsm"); -this.WebappsHandler = { +this.WebappManager = { observe: function(subject, topic, data) { data = JSON.parse(data); data.mm = subject; switch (topic) { case "webapps-ask-install": let chromeWin = Services.wm.getOuterWindowWithId(data.oid); if (chromeWin) @@ -32,16 +33,23 @@ this.WebappsHandler = { WebappOSUtils.launch(data); break; case "webapps-uninstall": WebappOSUtils.uninstall(data); break; } }, + update: function(aApp, aManifest, aZipPath) { + let nativeApp = new NativeApp(aApp, aManifest, + WebappRT.config.app.categories, + WebappRT.config.registryDir); + nativeApp.prepareUpdate(aManifest, aZipPath); + }, + doInstall: function(data, window) { let jsonManifest = data.isPackage ? data.app.updateManifest : data.app.manifest; let manifest = new ManifestHelper(jsonManifest, data.app.origin); let name = manifest.name; let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties"); let choice = Services.prompt.confirmEx( window, @@ -54,36 +62,39 @@ this.WebappsHandler = { bundle.GetStringFromName("webapps.install.install"), bundle.GetStringFromName("webapps.install.dontinstall"), null, null, {}); // Perform the install if the user allows it if (choice == 0) { - let shell = WebappsInstaller.init(data); - - if (shell) { - let localDir = null; - if (shell.appProfile) { - localDir = shell.appProfile.localDir; - } + let nativeApp = new NativeApp(data.app, jsonManifest, + WebappRT.config.app.categories, + WebappRT.config.registryDir); + let localDir; + try { + localDir = nativeApp.createProfile(); + } catch (ex) { + DOMApplicationRegistry.denyInstall(aData); + return; + } - DOMApplicationRegistry.confirmInstall(data, localDir, - function (aManifest) { - WebappsInstaller.install(data, aManifest); - } - ); - } else { - DOMApplicationRegistry.denyInstall(data); - } + DOMApplicationRegistry.confirmInstall(data, localDir, + function (aManifest, aZipPath) { + nativeApp.install(aManifest, aZipPath); + } + ); } else { DOMApplicationRegistry.denyInstall(data); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]) }; -Services.obs.addObserver(WebappsHandler, "webapps-ask-install", false); -Services.obs.addObserver(WebappsHandler, "webapps-launch", false); -Services.obs.addObserver(WebappsHandler, "webapps-uninstall", false); +Services.obs.addObserver(WebappManager, "webapps-ask-install", false); +Services.obs.addObserver(WebappManager, "webapps-launch", false); +Services.obs.addObserver(WebappManager, "webapps-uninstall", false); +Services.obs.addObserver(WebappManager, "webapps-update", false); + +DOMApplicationRegistry.registerUpdateHandler(WebappManager.update);
--- a/webapprt/WebappRT.jsm +++ b/webapprt/WebappRT.jsm @@ -10,48 +10,30 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, 'NativeApp', + 'resource://gre/modules/NativeApp.jsm'); + XPCOMUtils.defineLazyServiceGetter(this, "appsService", "@mozilla.org/AppsService;1", "nsIAppsService"); this.WebappRT = { - _config: null, - - get config() { - if (this._config) - return this._config; - - let webappFile = FileUtils.getFile("AppRegD", ["webapp.json"]); - - let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - inputStream.init(webappFile, -1, 0, Ci.nsIFileInputStream.CLOSE_ON_EOF); - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - let config = json.decodeFromStream(inputStream, webappFile.fileSize); - - return this._config = config; - }, - - // This exists to support test mode, which installs webapps after startup. - // Ideally we wouldn't have to have a setter, as tests can just delete - // the getter and then set the property. But the object to which they set it - // will have a reference to its global object, so our reference to it - // will leak that object (per bug 780674). The setter enables us to clone - // the new value so we don't actually retain a reference to it. - set config(newVal) { - this._config = JSON.parse(JSON.stringify(newVal)); - }, - get launchURI() { let manifest = this.localeManifest; return manifest.fullLaunchPath(); }, get localeManifest() { return new ManifestHelper(this.config.app.manifest, this.config.app.origin); @@ -60,9 +42,98 @@ this.WebappRT = { get appID() { let manifestURL = WebappRT.config.app.manifestURL; if (!manifestURL) { return Ci.nsIScriptSecurityManager.NO_APP_ID; } return appsService.getAppLocalIdByManifestURL(manifestURL); }, + + loadConfig: function() { + if (this.config) { + return; + } + + let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path, + "webapp.json"); + this.config = yield AppsUtils.loadJSONAsync(webappJson); + }, + + isUpdatePending: Task.async(function*() { + let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path, + "update", "webapp.json"); + + if (!(yield OS.File.exists(webappJson))) { + return false; + } + + return true; + }), + + applyUpdate: Task.async(function*() { + let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path, + "update", "webapp.json"); + let config = yield AppsUtils.loadJSONAsync(webappJson); + + let nativeApp = new NativeApp(config.app, config.app.manifest, + config.app.categories, + config.registryDir); + try { + yield nativeApp.applyUpdate(); + } catch (ex) { + return false; + } + + // The update has been applied successfully, the new config file + // is the config file that was in the update directory. + this.config = config; + + return true; + }), + + startUpdateService: function() { + let manifestURL = WebappRT.config.app.manifestURL; + // We used to install apps without storing their manifest URL. + // Now we can't update them. + if (!manifestURL) { + return; + } + + // Check for updates once a day. + let timerManager = Cc["@mozilla.org/updates/timer-manager;1"]. + getService(Ci.nsIUpdateTimerManager); + timerManager.registerTimer("updateTimer", () => { + let window = Services.wm.getMostRecentWindow("webapprt:webapp"); + window.navigator.mozApps.mgmt.getAll().onsuccess = function() { + let thisApp = null; + for (let app of this.result) { + if (app.manifestURL == manifestURL) { + thisApp = app; + break; + } + } + + // This shouldn't happen if the app is installed. + if (!thisApp) { + Cu.reportError("Couldn't find the app in the webapps registry"); + return; + } + + thisApp.ondownloadavailable = () => { + // Download available, download it! + thisApp.download(); + }; + + thisApp.ondownloadsuccess = () => { + // Update downloaded, apply it! + window.navigator.mozApps.mgmt.applyDownload(thisApp); + }; + + thisApp.ondownloadapplied = () => { + // Application updated, nothing to do. + }; + + thisApp.checkForUpdate(); + } + }, 24 * 60 * 60); + }, };
--- a/webapprt/moz.build +++ b/webapprt/moz.build @@ -19,18 +19,18 @@ EXTRA_COMPONENTS += [ 'ContentPermission.js', 'DirectoryProvider.js', 'PaymentUIGlue.js', ] EXTRA_JS_MODULES += [ 'RemoteDebugger.jsm', 'Startup.jsm', + 'WebappManager.jsm', 'WebappRT.jsm', - 'WebappsHandler.jsm', 'WebRTCHandler.jsm', ] MOCHITEST_WEBAPPRT_CHROME_MANIFESTS += ['test/chrome/webapprt.ini'] MOCHITEST_MANIFESTS += ['test/content/mochitest.ini'] # Place webapprt resources in a separate app dir DIST_SUBDIR = 'webapprt'