Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const USER_LIB_DIR = OS.Constants.Path.macUserLibDir; |
michael@0 | 6 | const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir; |
michael@0 | 7 | |
michael@0 | 8 | /** |
michael@0 | 9 | * Constructor for the Mac native app shell |
michael@0 | 10 | * |
michael@0 | 11 | * @param aApp {Object} the app object provided to the install function |
michael@0 | 12 | * @param aManifest {Object} the manifest data provided by the web app |
michael@0 | 13 | * @param aCategories {Array} array of app categories |
michael@0 | 14 | * @param aRegistryDir {String} (optional) path to the registry |
michael@0 | 15 | */ |
michael@0 | 16 | function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { |
michael@0 | 17 | CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); |
michael@0 | 18 | |
michael@0 | 19 | // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash |
michael@0 | 20 | this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support", |
michael@0 | 21 | this.uniqueName); |
michael@0 | 22 | this.configJson = "webapp.json"; |
michael@0 | 23 | |
michael@0 | 24 | this.contentsDir = "Contents"; |
michael@0 | 25 | this.macOSDir = OS.Path.join(this.contentsDir, "MacOS"); |
michael@0 | 26 | this.resourcesDir = OS.Path.join(this.contentsDir, "Resources"); |
michael@0 | 27 | this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns"); |
michael@0 | 28 | this.zipFile = OS.Path.join(this.resourcesDir, "application.zip"); |
michael@0 | 29 | } |
michael@0 | 30 | |
michael@0 | 31 | NativeApp.prototype = { |
michael@0 | 32 | __proto__: CommonNativeApp.prototype, |
michael@0 | 33 | /* |
michael@0 | 34 | * The _rootInstallDir property is the path of the directory where we install |
michael@0 | 35 | * apps. In production code, it's "/Applications". In tests, it's |
michael@0 | 36 | * "~/Applications" because on build machines we don't have enough privileges |
michael@0 | 37 | * to write to the global "/Applications" directory. |
michael@0 | 38 | */ |
michael@0 | 39 | _rootInstallDir: LOCAL_APP_DIR, |
michael@0 | 40 | |
michael@0 | 41 | /** |
michael@0 | 42 | * Creates a native installation of the web app in the OS |
michael@0 | 43 | * |
michael@0 | 44 | * @param aManifest {Object} the manifest data provided by the web app |
michael@0 | 45 | * @param aZipPath {String} path to the zip file for packaged apps (undefined |
michael@0 | 46 | * for hosted apps) |
michael@0 | 47 | */ |
michael@0 | 48 | install: Task.async(function*(aManifest, aZipPath) { |
michael@0 | 49 | if (this._dryRun) { |
michael@0 | 50 | return; |
michael@0 | 51 | } |
michael@0 | 52 | |
michael@0 | 53 | // If the application is already installed, this is a reinstallation. |
michael@0 | 54 | if (WebappOSUtils.getInstallPath(this.app)) { |
michael@0 | 55 | return yield this.prepareUpdate(aManifest, aZipPath); |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | this._setData(aManifest); |
michael@0 | 59 | |
michael@0 | 60 | let localAppDir = getFile(this._rootInstallDir); |
michael@0 | 61 | if (!localAppDir.isWritable()) { |
michael@0 | 62 | throw("Not enough privileges to install apps"); |
michael@0 | 63 | } |
michael@0 | 64 | |
michael@0 | 65 | let destinationName = yield getAvailableFileName([ this._rootInstallDir ], |
michael@0 | 66 | this.appNameAsFilename, |
michael@0 | 67 | ".app"); |
michael@0 | 68 | |
michael@0 | 69 | let installDir = OS.Path.join(this._rootInstallDir, destinationName); |
michael@0 | 70 | |
michael@0 | 71 | let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app"); |
michael@0 | 72 | dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
michael@0 | 73 | let tmpDir = dir.path; |
michael@0 | 74 | |
michael@0 | 75 | try { |
michael@0 | 76 | yield this._createDirectoryStructure(tmpDir); |
michael@0 | 77 | this._copyPrebuiltFiles(tmpDir); |
michael@0 | 78 | yield this._createConfigFiles(tmpDir); |
michael@0 | 79 | |
michael@0 | 80 | if (aZipPath) { |
michael@0 | 81 | yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | yield this._getIcon(tmpDir); |
michael@0 | 85 | } catch (ex) { |
michael@0 | 86 | yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
michael@0 | 87 | throw ex; |
michael@0 | 88 | } |
michael@0 | 89 | |
michael@0 | 90 | this._removeInstallation(true, installDir); |
michael@0 | 91 | |
michael@0 | 92 | try { |
michael@0 | 93 | // Move the temp installation directory to the /Applications directory |
michael@0 | 94 | yield this._applyTempInstallation(tmpDir, installDir); |
michael@0 | 95 | } catch (ex) { |
michael@0 | 96 | this._removeInstallation(false, installDir); |
michael@0 | 97 | yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
michael@0 | 98 | throw ex; |
michael@0 | 99 | } |
michael@0 | 100 | }), |
michael@0 | 101 | |
michael@0 | 102 | /** |
michael@0 | 103 | * Creates an update in a temporary directory to be applied later. |
michael@0 | 104 | * |
michael@0 | 105 | * @param aManifest {Object} the manifest data provided by the web app |
michael@0 | 106 | * @param aZipPath {String} path to the zip file for packaged apps (undefined |
michael@0 | 107 | * for hosted apps) |
michael@0 | 108 | */ |
michael@0 | 109 | prepareUpdate: Task.async(function*(aManifest, aZipPath) { |
michael@0 | 110 | if (this._dryRun) { |
michael@0 | 111 | return; |
michael@0 | 112 | } |
michael@0 | 113 | |
michael@0 | 114 | this._setData(aManifest); |
michael@0 | 115 | |
michael@0 | 116 | let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app); |
michael@0 | 117 | if (!installDir) { |
michael@0 | 118 | throw ERR_NOT_INSTALLED; |
michael@0 | 119 | } |
michael@0 | 120 | |
michael@0 | 121 | if (this.uniqueName != oldUniqueName) { |
michael@0 | 122 | // Bug 919799: If the app is still in the registry, migrate its data to |
michael@0 | 123 | // the new format. |
michael@0 | 124 | throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; |
michael@0 | 125 | } |
michael@0 | 126 | |
michael@0 | 127 | let updateDir = OS.Path.join(installDir, "update"); |
michael@0 | 128 | yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
michael@0 | 129 | yield OS.File.makeDir(updateDir); |
michael@0 | 130 | |
michael@0 | 131 | try { |
michael@0 | 132 | yield this._createDirectoryStructure(updateDir); |
michael@0 | 133 | this._copyPrebuiltFiles(updateDir); |
michael@0 | 134 | yield this._createConfigFiles(updateDir); |
michael@0 | 135 | |
michael@0 | 136 | if (aZipPath) { |
michael@0 | 137 | yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); |
michael@0 | 138 | } |
michael@0 | 139 | |
michael@0 | 140 | yield this._getIcon(updateDir); |
michael@0 | 141 | } catch (ex) { |
michael@0 | 142 | yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
michael@0 | 143 | throw ex; |
michael@0 | 144 | } |
michael@0 | 145 | }), |
michael@0 | 146 | |
michael@0 | 147 | /** |
michael@0 | 148 | * Applies an update. |
michael@0 | 149 | */ |
michael@0 | 150 | applyUpdate: Task.async(function*() { |
michael@0 | 151 | if (this._dryRun) { |
michael@0 | 152 | return; |
michael@0 | 153 | } |
michael@0 | 154 | |
michael@0 | 155 | let installDir = WebappOSUtils.getInstallPath(this.app); |
michael@0 | 156 | let updateDir = OS.Path.join(installDir, "update"); |
michael@0 | 157 | |
michael@0 | 158 | let backupDir = yield this._backupInstallation(installDir); |
michael@0 | 159 | |
michael@0 | 160 | try { |
michael@0 | 161 | // Move the update directory to the /Applications directory |
michael@0 | 162 | yield this._applyTempInstallation(updateDir, installDir); |
michael@0 | 163 | } catch (ex) { |
michael@0 | 164 | yield this._restoreInstallation(backupDir, installDir); |
michael@0 | 165 | throw ex; |
michael@0 | 166 | } finally { |
michael@0 | 167 | yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
michael@0 | 168 | yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
michael@0 | 169 | } |
michael@0 | 170 | }), |
michael@0 | 171 | |
michael@0 | 172 | _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { |
michael@0 | 173 | yield OS.File.move(OS.Path.join(aTmpDir, this.configJson), |
michael@0 | 174 | OS.Path.join(this.appProfileDir, this.configJson)); |
michael@0 | 175 | |
michael@0 | 176 | yield moveDirectory(aTmpDir, aInstallDir); |
michael@0 | 177 | }), |
michael@0 | 178 | |
michael@0 | 179 | _removeInstallation: function(keepProfile, aInstallDir) { |
michael@0 | 180 | let filesToRemove = [ aInstallDir ]; |
michael@0 | 181 | |
michael@0 | 182 | if (!keepProfile) { |
michael@0 | 183 | filesToRemove.push(this.appProfileDir); |
michael@0 | 184 | } |
michael@0 | 185 | |
michael@0 | 186 | removeFiles(filesToRemove); |
michael@0 | 187 | }, |
michael@0 | 188 | |
michael@0 | 189 | _backupInstallation: Task.async(function*(aInstallDir) { |
michael@0 | 190 | let backupDir = OS.Path.join(aInstallDir, "backup"); |
michael@0 | 191 | yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
michael@0 | 192 | yield OS.File.makeDir(backupDir); |
michael@0 | 193 | |
michael@0 | 194 | yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir), |
michael@0 | 195 | backupDir); |
michael@0 | 196 | yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson), |
michael@0 | 197 | OS.Path.join(backupDir, this.configJson)); |
michael@0 | 198 | |
michael@0 | 199 | return backupDir; |
michael@0 | 200 | }), |
michael@0 | 201 | |
michael@0 | 202 | _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) { |
michael@0 | 203 | yield OS.File.move(OS.Path.join(aBackupDir, this.configJson), |
michael@0 | 204 | OS.Path.join(this.appProfileDir, this.configJson)); |
michael@0 | 205 | yield moveDirectory(aBackupDir, |
michael@0 | 206 | OS.Path.join(aInstallDir, this.contentsDir)); |
michael@0 | 207 | }), |
michael@0 | 208 | |
michael@0 | 209 | _createDirectoryStructure: Task.async(function*(aDir) { |
michael@0 | 210 | yield OS.File.makeDir(this.appProfileDir, |
michael@0 | 211 | { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
michael@0 | 212 | |
michael@0 | 213 | yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir), |
michael@0 | 214 | { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
michael@0 | 215 | |
michael@0 | 216 | yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir), |
michael@0 | 217 | { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
michael@0 | 218 | |
michael@0 | 219 | yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir), |
michael@0 | 220 | { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
michael@0 | 221 | }), |
michael@0 | 222 | |
michael@0 | 223 | _copyPrebuiltFiles: function(aDir) { |
michael@0 | 224 | let destDir = getFile(aDir, this.macOSDir); |
michael@0 | 225 | let stub = getFile(this.runtimeFolder, "webapprt-stub"); |
michael@0 | 226 | stub.copyTo(destDir, "webapprt"); |
michael@0 | 227 | }, |
michael@0 | 228 | |
michael@0 | 229 | _createConfigFiles: function(aDir) { |
michael@0 | 230 | // ${ProfileDir}/webapp.json |
michael@0 | 231 | yield writeToFile(OS.Path.join(aDir, this.configJson), |
michael@0 | 232 | JSON.stringify(this.webappJson)); |
michael@0 | 233 | |
michael@0 | 234 | // ${InstallDir}/Contents/MacOS/webapp.ini |
michael@0 | 235 | let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini"); |
michael@0 | 236 | |
michael@0 | 237 | let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. |
michael@0 | 238 | getService(Ci.nsIINIParserFactory). |
michael@0 | 239 | createINIParser(applicationINI). |
michael@0 | 240 | QueryInterface(Ci.nsIINIParserWriter); |
michael@0 | 241 | writer.setString("Webapp", "Name", this.appName); |
michael@0 | 242 | writer.setString("Webapp", "Profile", this.uniqueName); |
michael@0 | 243 | writer.writeFile(); |
michael@0 | 244 | applicationINI.permissions = PERMS_FILE; |
michael@0 | 245 | |
michael@0 | 246 | // ${InstallDir}/Contents/Info.plist |
michael@0 | 247 | let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\ |
michael@0 | 248 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\ |
michael@0 | 249 | <plist version="1.0">\n\ |
michael@0 | 250 | <dict>\n\ |
michael@0 | 251 | <key>CFBundleDevelopmentRegion</key>\n\ |
michael@0 | 252 | <string>English</string>\n\ |
michael@0 | 253 | <key>CFBundleDisplayName</key>\n\ |
michael@0 | 254 | <string>' + escapeXML(this.appName) + '</string>\n\ |
michael@0 | 255 | <key>CFBundleExecutable</key>\n\ |
michael@0 | 256 | <string>webapprt</string>\n\ |
michael@0 | 257 | <key>CFBundleIconFile</key>\n\ |
michael@0 | 258 | <string>appicon</string>\n\ |
michael@0 | 259 | <key>CFBundleIdentifier</key>\n\ |
michael@0 | 260 | <string>' + escapeXML(this.uniqueName) + '</string>\n\ |
michael@0 | 261 | <key>CFBundleInfoDictionaryVersion</key>\n\ |
michael@0 | 262 | <string>6.0</string>\n\ |
michael@0 | 263 | <key>CFBundleName</key>\n\ |
michael@0 | 264 | <string>' + escapeXML(this.appName) + '</string>\n\ |
michael@0 | 265 | <key>CFBundlePackageType</key>\n\ |
michael@0 | 266 | <string>APPL</string>\n\ |
michael@0 | 267 | <key>CFBundleVersion</key>\n\ |
michael@0 | 268 | <string>0</string>\n\ |
michael@0 | 269 | <key>NSHighResolutionCapable</key>\n\ |
michael@0 | 270 | <true/>\n\ |
michael@0 | 271 | <key>NSPrincipalClass</key>\n\ |
michael@0 | 272 | <string>GeckoNSApplication</string>\n\ |
michael@0 | 273 | <key>FirefoxBinary</key>\n\ |
michael@0 | 274 | #expand <string>__MOZ_MACBUNDLE_ID__</string>\n\ |
michael@0 | 275 | </dict>\n\ |
michael@0 | 276 | </plist>'; |
michael@0 | 277 | |
michael@0 | 278 | yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"), |
michael@0 | 279 | infoPListContent); |
michael@0 | 280 | }, |
michael@0 | 281 | |
michael@0 | 282 | /** |
michael@0 | 283 | * Process the icon from the imageStream as retrieved from |
michael@0 | 284 | * the URL by getIconForApp(). This will bundle the icon to the |
michael@0 | 285 | * app package at Contents/Resources/appicon.icns. |
michael@0 | 286 | * |
michael@0 | 287 | * @param aMimeType the icon mimetype |
michael@0 | 288 | * @param aImageStream the stream for the image data |
michael@0 | 289 | * @param aDir the directory where the icon should be stored |
michael@0 | 290 | */ |
michael@0 | 291 | _processIcon: function(aMimeType, aIcon, aDir) { |
michael@0 | 292 | let deferred = Promise.defer(); |
michael@0 | 293 | |
michael@0 | 294 | function conversionDone(aSubject, aTopic) { |
michael@0 | 295 | if (aTopic == "process-finished") { |
michael@0 | 296 | deferred.resolve(); |
michael@0 | 297 | } else { |
michael@0 | 298 | deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue); |
michael@0 | 299 | } |
michael@0 | 300 | } |
michael@0 | 301 | |
michael@0 | 302 | let process = Cc["@mozilla.org/process/util;1"]. |
michael@0 | 303 | createInstance(Ci.nsIProcess); |
michael@0 | 304 | let sipsFile = getFile("/usr/bin/sips"); |
michael@0 | 305 | |
michael@0 | 306 | process.init(sipsFile); |
michael@0 | 307 | process.runAsync(["-s", "format", "icns", |
michael@0 | 308 | aIcon.path, |
michael@0 | 309 | "--out", OS.Path.join(aDir, this.iconFile), |
michael@0 | 310 | "-z", "128", "128"], |
michael@0 | 311 | 9, conversionDone); |
michael@0 | 312 | |
michael@0 | 313 | return deferred.promise; |
michael@0 | 314 | } |
michael@0 | 315 | } |