mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
cd079d2bf9
Backed out changeset b5abe23a4ea5 (bug 1048048) Backed out changeset 4f91b10e8be0 (bug 1048048) Backed out changeset 450d4a13c90e (bug 1048048) Backed out changeset 6a727c40eb68 (bug 1048048) Backed out changeset 88c2333ff745 (bug 1048048) Backed out changeset 740ab1ecd079 (bug 1048048) Backed out changeset 02c6d6aef163 (bug 1048048)
479 lines
14 KiB
JavaScript
479 lines
14 KiB
JavaScript
/* 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 = ["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 DEFAULT_ICON_URL = "chrome://global/skin/icons/webapps-64.png";
|
|
|
|
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 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
|
|
* CommonNativeApp.call(this, ...) from the platform-specific
|
|
* constructor.
|
|
*
|
|
* @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 CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) {
|
|
// Set the name property of the app object, otherwise
|
|
// WebappOSUtils::getUniqueName won't work.
|
|
aApp.name = aManifest.name;
|
|
this.uniqueName = WebappOSUtils.getUniqueName(aApp);
|
|
|
|
let localeManifest =
|
|
new ManifestHelper(aManifest, aApp.origin, aApp.manifestURL);
|
|
|
|
this.appLocalizedName = localeManifest.name;
|
|
this.appNameAsFilename = stripStringForFilename(aApp.name);
|
|
|
|
if (aApp.updateManifest) {
|
|
this.isPackaged = true;
|
|
}
|
|
|
|
this.categories = aCategories.slice(0);
|
|
|
|
this.registryDir = aRegistryDir || OS.Constants.Path.profileDir;
|
|
|
|
this._dryRun = false;
|
|
try {
|
|
if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
|
|
this._dryRun = true;
|
|
}
|
|
} catch (ex) {}
|
|
}
|
|
|
|
CommonNativeApp.prototype = {
|
|
uniqueName: null,
|
|
appLocalizedName: 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 aManifest {Object} the manifest data provided by the web app
|
|
*
|
|
*/
|
|
_setData: function(aApp, aManifest) {
|
|
let manifest = new ManifestHelper(aManifest, aApp.origin, aApp.manifestURL);
|
|
let origin = Services.io.newURI(aApp.origin, null, null);
|
|
|
|
#ifdef XP_WIN
|
|
let biggestIconURL = manifest.biggestIconURL(v => v <= 256);
|
|
#else
|
|
let biggestIconURL = manifest.biggestIconURL();
|
|
#endif
|
|
|
|
this.iconURI = Services.io.newURI(biggestIconURL || DEFAULT_ICON_URL, null,
|
|
null);
|
|
|
|
if (manifest.developer) {
|
|
if (manifest.developer.name) {
|
|
let devName = manifest.developer.name.substr(0, 128);
|
|
if (devName) {
|
|
this.developerName = devName;
|
|
}
|
|
}
|
|
|
|
if (manifest.developer.url) {
|
|
this.developerUrl = manifest.developer.url;
|
|
}
|
|
}
|
|
|
|
if (manifest.description) {
|
|
let firstLine = manifest.description.split("\n")[0];
|
|
let shortDesc = firstLine.length <= 256
|
|
? firstLine
|
|
: firstLine.substr(0, 253) + "…";
|
|
this.shortDescription = shortDesc;
|
|
} else {
|
|
this.shortDescription = this.appLocalizedName;
|
|
}
|
|
|
|
if (manifest.version) {
|
|
this.version = manifest.version;
|
|
}
|
|
|
|
this.webappJson = {
|
|
// The app registry is the Firefox profile from which the app
|
|
// was installed.
|
|
"registryDir": this.registryDir,
|
|
"app": {
|
|
"manifest": aManifest,
|
|
"origin": aApp.origin,
|
|
"manifestURL": aApp.manifestURL,
|
|
"installOrigin": aApp.installOrigin,
|
|
"categories": this.categories,
|
|
"receipts": aApp.receipts,
|
|
"installTime": aApp.installTime,
|
|
}
|
|
};
|
|
|
|
if (aApp.etag) {
|
|
this.webappJson.app.etag = aApp.etag;
|
|
}
|
|
|
|
if (aApp.packageEtag) {
|
|
this.webappJson.app.packageEtag = aApp.packageEtag;
|
|
}
|
|
|
|
if (aApp.updateManifest) {
|
|
this.webappJson.app.updateManifest = aApp.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(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(aTmpDir,
|
|
this.zipFile));
|
|
|
|
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, 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, 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.
|
|
*/
|
|
createProfile: function() {
|
|
if (this._dryRun) {
|
|
return null;
|
|
}
|
|
|
|
let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].
|
|
getService(Ci.nsIToolkitProfileService);
|
|
|
|
try {
|
|
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
|
|
|
|
#include WinNativeApp.js
|
|
|
|
#elifdef XP_MACOSX
|
|
|
|
#include MacNativeApp.js
|
|
|
|
#elifdef XP_UNIX
|
|
|
|
#include LinuxNativeApp.js
|
|
|
|
#endif
|
|
|
|
/* Helper Functions */
|
|
|
|
/**
|
|
* Async write a data string into a file
|
|
*
|
|
* @param aPath the path to the file to write to
|
|
* @param aData a string with the data to be written
|
|
*/
|
|
function writeToFile(aPath, aData) {
|
|
return Task.spawn(function() {
|
|
let data = new TextEncoder().encode(aData);
|
|
|
|
let file;
|
|
try {
|
|
file = yield OS.File.open(aPath, { truncate: true, write: true },
|
|
{ unixMode: PERMS_FILE });
|
|
yield file.write(data);
|
|
} finally {
|
|
yield file.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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]
|
|
let stripFrontRE = new RegExp("^\\W*", "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
|
|
* @param aName string with the filename (minus the extension) desired
|
|
* @param aExtension string with the file extension, including the dot
|
|
|
|
* @return file name or null if folder is unwritable or unique name
|
|
* was not available
|
|
*/
|
|
function getAvailableFileName(aPathSet, aName, aExtension) {
|
|
return Task.spawn(function*() {
|
|
let name = aName + aExtension;
|
|
|
|
function checkUnique(aName) {
|
|
return Task.spawn(function*() {
|
|
for (let path of aPathSet) {
|
|
if (yield OS.File.exists(OS.Path.join(path, aName))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
if (yield checkUnique(name)) {
|
|
return name;
|
|
}
|
|
|
|
// If we're here, the plain name wasn't enough. Let's try modifying the name
|
|
// by adding "(" + num + ")".
|
|
for (let i = 2; i < 100; i++) {
|
|
name = aName + " (" + i + ")" + aExtension;
|
|
|
|
if (yield checkUnique(name)) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
throw "No available filename";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Attempts to remove files or directories.
|
|
*
|
|
* @param aPaths An array with paths to files to remove
|
|
*/
|
|
function removeFiles(aPaths) {
|
|
for (let path of aPaths) {
|
|
let file = getFile(path);
|
|
|
|
try {
|
|
if (file.exists()) {
|
|
file.followLinks = false;
|
|
file.remove(true);
|
|
}
|
|
} catch(ex) {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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 {
|
|
entry.moveTo(destDir, entry.leafName);
|
|
}
|
|
}
|
|
|
|
// The source directory is now empty, remove it.
|
|
yield OS.File.removeEmptyDir(srcPath);
|
|
}
|
|
|
|
function escapeXML(aStr) {
|
|
return aStr.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
// Helper to create a nsIFile from a set of path components
|
|
function getFile() {
|
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
|
file.initWithPath(OS.Path.join.apply(OS.Path, arguments));
|
|
return file;
|
|
}
|
|
|
|
// Download an icon using either a temp file or a pipe.
|
|
function downloadIcon(aIconURI) {
|
|
let deferred = Promise.defer();
|
|
|
|
let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
|
let mimeType;
|
|
try {
|
|
let tIndex = aIconURI.path.indexOf(";");
|
|
if("data" == aIconURI.scheme && tIndex != -1) {
|
|
mimeType = aIconURI.path.substring(0, tIndex);
|
|
} else {
|
|
mimeType = mimeService.getTypeFromURI(aIconURI);
|
|
}
|
|
} catch(e) {
|
|
deferred.reject("Failed to determine icon MIME type: " + e);
|
|
return deferred.promise;
|
|
}
|
|
|
|
function onIconDownloaded(aStatusCode, aIcon) {
|
|
if (Components.isSuccessCode(aStatusCode)) {
|
|
deferred.resolve([ mimeType, aIcon ]);
|
|
} else {
|
|
deferred.reject("Failure downloading icon: " + aStatusCode);
|
|
}
|
|
}
|
|
|
|
try {
|
|
#ifdef XP_MACOSX
|
|
let downloadObserver = {
|
|
onDownloadComplete: function(downloader, request, cx, aStatus, file) {
|
|
onIconDownloaded(aStatus, file);
|
|
}
|
|
};
|
|
|
|
let tmpIcon = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
|
tmpIcon.append("tmpicon." + mimeService.getPrimaryExtension(mimeType, ""));
|
|
tmpIcon.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
|
|
|
|
let listener = Cc["@mozilla.org/network/downloader;1"]
|
|
.createInstance(Ci.nsIDownloader);
|
|
listener.init(downloadObserver, tmpIcon);
|
|
#else
|
|
let pipe = Cc["@mozilla.org/pipe;1"]
|
|
.createInstance(Ci.nsIPipe);
|
|
pipe.init(true, true, 0, 0xffffffff, null);
|
|
|
|
let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
|
|
.createInstance(Ci.nsISimpleStreamListener);
|
|
listener.init(pipe.outputStream, {
|
|
onStartRequest: function() {},
|
|
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
|
pipe.outputStream.close();
|
|
onIconDownloaded(aStatusCode, pipe.inputStream);
|
|
}
|
|
});
|
|
#endif
|
|
|
|
// If not fetching an icon from chrome:// then we should create a
|
|
// NoAppCodeBasePrincipal. Note, that we are still in the process of
|
|
// installing the app, hence app.origin is not available yet and
|
|
// therefore we can not call getAppCodebasePrincipal.
|
|
let principal =
|
|
aIconURI.schemeIs("chrome") ?
|
|
Services.scriptSecurityManager.getSystemPrincipal() :
|
|
Services.scriptSecurityManager.createCodebasePrincipal(aIconURI, {});
|
|
|
|
let channel = NetUtil.newChannel({
|
|
uri: aIconURI,
|
|
loadingPrincipal: principal,
|
|
contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE});
|
|
let { BadCertHandler } = Cu.import("resource://gre/modules/CertUtils.jsm", {});
|
|
// Pass true to avoid optional redirect-cert-checking behavior.
|
|
channel.notificationCallbacks = new BadCertHandler(true);
|
|
|
|
channel.asyncOpen(listener, null);
|
|
} catch(e) {
|
|
deferred.reject("Failure initiating download of icon: " + e);
|
|
}
|
|
|
|
return deferred.promise;
|
|
}
|