forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D66128 --HG-- extra : moz-landing-system : lando
908 lines
24 KiB
JavaScript
908 lines
24 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/. */
|
|
|
|
/**
|
|
* A Site Specific Browser intends to allow the user to navigate through the
|
|
* chosen site in the SSB UI. Any attempt to load something outside the site
|
|
* should be loaded in a normal browser. In order to achieve this we have to use
|
|
* various APIs to listen for attempts to load new content and take appropriate
|
|
* action. Often this requires returning synchronous responses to method calls
|
|
* in content processes and will require data about the SSB in order to respond
|
|
* correctly. Here we implement an architecture to support that:
|
|
*
|
|
* In the main process the SiteSpecificBrowser class implements all the
|
|
* functionality involved with managing an SSB. All content processes can
|
|
* synchronously retrieve a matching SiteSpecificBrowserBase that has enough
|
|
* data about the SSB in order to be able to respond to load requests
|
|
* synchronously. To support this we give every SSB a unique ID (UUID based)
|
|
* and the appropriate data is shared via sharedData. Once created the ID can be
|
|
* used to retrieve the SiteSpecificBrowser instance in the main process or
|
|
* SiteSpecificBrowserBase instance in any content process.
|
|
*/
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"SiteSpecificBrowserService",
|
|
"SiteSpecificBrowserBase",
|
|
"SiteSpecificBrowser",
|
|
"SSBCommandLineHandler",
|
|
];
|
|
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm",
|
|
ManifestProcessor: "resource://gre/modules/ManifestProcessor.jsm",
|
|
KeyValueService: "resource://gre/modules/kvstore.jsm",
|
|
OS: "resource://gre/modules/osfile.jsm",
|
|
ImageTools: "resource:///modules/ssb/ImageTools.jsm",
|
|
AppConstants: "resource://gre/modules/AppConstants.jsm",
|
|
});
|
|
|
|
if (AppConstants.platform == "win") {
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"WindowsSupport",
|
|
"resource:///modules/ssb/WindowsSupport.jsm"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A schema version for the SSB data stored in the kvstore.
|
|
*
|
|
* Version 1 has the `manifest` and `config` properties.
|
|
*/
|
|
const DATA_VERSION = 1;
|
|
|
|
/**
|
|
* The prefix used for SSB ids in the store.
|
|
*/
|
|
const SSB_STORE_PREFIX = "ssb:";
|
|
|
|
/**
|
|
* A prefix that will sort immediately after any SSB ids in the store.
|
|
*/
|
|
const SSB_STORE_LAST = "ssb;";
|
|
|
|
function uuid() {
|
|
return Cc["@mozilla.org/uuid-generator;1"]
|
|
.getService(Ci.nsIUUIDGenerator)
|
|
.generateUUID()
|
|
.toString();
|
|
}
|
|
|
|
const sharedDataKey = id => `SiteSpecificBrowserBase:${id}`;
|
|
const storeKey = id => SSB_STORE_PREFIX + id;
|
|
|
|
/**
|
|
* Builds a lookup table for all the icons in order of size.
|
|
*/
|
|
function buildIconList(icons) {
|
|
let iconList = [];
|
|
|
|
for (let icon of icons) {
|
|
for (let sizeSpec of icon.sizes) {
|
|
let size =
|
|
sizeSpec == "any" ? Number.MAX_SAFE_INTEGER : parseInt(sizeSpec);
|
|
|
|
iconList.push({
|
|
icon,
|
|
size,
|
|
});
|
|
}
|
|
}
|
|
|
|
iconList.sort((a, b) => {
|
|
// Given that we're using MAX_SAFE_INTEGER adding a value to that would
|
|
// overflow and give odd behaviour. And we're using numbers supplied by a
|
|
// website so just compare for safety.
|
|
if (a.size < b.size) {
|
|
return -1;
|
|
}
|
|
|
|
if (a.size > b.size) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
return iconList;
|
|
}
|
|
|
|
const IS_MAIN_PROCESS =
|
|
Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT;
|
|
|
|
/**
|
|
* Tests whether an app manifest's scope includes the given URI.
|
|
*
|
|
* @param {nsIURI} scope the manifest's scope.
|
|
* @param {nsIURI} uri the URI to test.
|
|
* @returns {boolean} true if the uri is included in the scope.
|
|
*/
|
|
function scopeIncludes(scope, uri) {
|
|
// https://w3c.github.io/manifest/#dfn-within-scope
|
|
if (scope.prePath != uri.prePath) {
|
|
return false;
|
|
}
|
|
|
|
return uri.filePath.startsWith(scope.filePath);
|
|
}
|
|
|
|
/**
|
|
* Generates a basic app manifest for a URI.
|
|
*
|
|
* @param {nsIURI} uri the start URI for the site.
|
|
* @return {Manifest} an app manifest.
|
|
*/
|
|
function manifestForURI(uri) {
|
|
try {
|
|
let manifestURI = Services.io.newURI("/manifest.json", null, uri);
|
|
return ManifestProcessor.process({
|
|
jsonText: "{}",
|
|
manifestURL: manifestURI.spec,
|
|
docURL: uri.spec,
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to generate a SSB manifest for ${uri.spec}.`, e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an IconResource from the LinkHandler data.
|
|
*
|
|
* @param {object} iconData the data from the LinkHandler actor.
|
|
* @return {Promise<IconResource>} an icon resource.
|
|
*/
|
|
async function getIconResource(iconData) {
|
|
// This should be a data url so no network traffic.
|
|
let imageData = await ImageTools.loadImage(
|
|
Services.io.newURI(iconData.iconURL)
|
|
);
|
|
if (imageData.container.type == Ci.imgIContainer.TYPE_VECTOR) {
|
|
return {
|
|
src: iconData.iconURL,
|
|
purpose: ["any"],
|
|
type: imageData.type,
|
|
sizes: ["any"],
|
|
};
|
|
}
|
|
|
|
// TODO: For ico files we should find all the available sizes: Bug 1604285.
|
|
|
|
return {
|
|
src: iconData.iconURL,
|
|
purpose: ["any"],
|
|
type: imageData.type,
|
|
sizes: [`${imageData.container.width}x${imageData.container.height}`],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generates an app manifest for a site loaded in a browser element.
|
|
*
|
|
* @param {Element} browser the browser element the site is loaded in.
|
|
* @return {Promise<Manifest>} an app manifest.
|
|
*/
|
|
async function buildManifestForBrowser(browser) {
|
|
let manifest = null;
|
|
try {
|
|
manifest = await ManifestObtainer.browserObtainManifest(browser);
|
|
} catch (e) {
|
|
// We can function without a valid manifest.
|
|
console.error(e);
|
|
}
|
|
|
|
// Reject the manifest if its scope doesn't include the current document.
|
|
if (
|
|
!manifest ||
|
|
!scopeIncludes(Services.io.newURI(manifest.scope), browser.currentURI)
|
|
) {
|
|
manifest = manifestForURI(browser.currentURI);
|
|
}
|
|
|
|
// Cache all the icons as data URIs since we can need access to them when
|
|
// the website is not loaded.
|
|
manifest.icons = (
|
|
await Promise.all(
|
|
manifest.icons.map(async icon => {
|
|
if (icon.src.startsWith("data:")) {
|
|
return icon;
|
|
}
|
|
|
|
let actor = browser.browsingContext.currentWindowGlobal.getActor(
|
|
"SiteSpecificBrowser"
|
|
);
|
|
try {
|
|
icon.src = await actor.sendQuery("LoadIcon", icon.src);
|
|
} catch (e) {
|
|
// Bad icon, drop it from the list.
|
|
return null;
|
|
}
|
|
|
|
return icon;
|
|
})
|
|
)
|
|
).filter(icon => icon);
|
|
|
|
// If the site provided no icons then try to use the normal page icons.
|
|
if (!manifest.icons.length) {
|
|
let linkHandler = browser.browsingContext.currentWindowGlobal.getActor(
|
|
"LinkHandler"
|
|
);
|
|
|
|
for (let icon of [linkHandler.icon, linkHandler.richIcon]) {
|
|
if (!icon) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
manifest.icons.push(await getIconResource(icon));
|
|
} catch (e) {
|
|
console.warn(`Failed to load icon resource ${icon.originalURL}`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* Maintains an ID -> SSB mapping in the main process. Content processes should
|
|
* use sharedData to get a SiteSpecificBrowserBase.
|
|
*
|
|
* We do not currently expire data from here so once created an SSB instance
|
|
* lives for the lifetime of the application. The expectation is that the
|
|
* numbers of different SSBs used will be low and the memory use will also
|
|
* be low.
|
|
*/
|
|
const SSBMap = new Map();
|
|
|
|
/**
|
|
* The base contains the data about an SSB instance needed in content processes.
|
|
*
|
|
* The only data needed currently is site's `scope` which is just a URI.
|
|
*/
|
|
class SiteSpecificBrowserBase {
|
|
/**
|
|
* Creates a new SiteSpecificBrowserBase. Generally should only be called by
|
|
* code within this module.
|
|
*
|
|
* @param {nsIURI} scope the scope for the SSB.
|
|
*/
|
|
constructor(scope) {
|
|
this._scope = scope;
|
|
}
|
|
|
|
/**
|
|
* Gets the SiteSpecifcBrowserBase for an ID. If this is the main process this
|
|
* will instead return the SiteSpecificBrowser instance itself but generally
|
|
* don't call this from the main process.
|
|
*
|
|
* The returned object is not "live" and will not be updated with any
|
|
* configuration changes from the main process so do not cache this, get it
|
|
* when needed and then discard.
|
|
*
|
|
* @param {string} id the SSB ID.
|
|
* @return {SiteSpecificBrowserBase|null} the instance if it exists.
|
|
*/
|
|
static get(id) {
|
|
if (IS_MAIN_PROCESS) {
|
|
return SiteSpecificBrowser.get(id);
|
|
}
|
|
|
|
let key = sharedDataKey(id);
|
|
if (!Services.cpmm.sharedData.has(key)) {
|
|
return null;
|
|
}
|
|
|
|
let scope = Services.io.newURI(Services.cpmm.sharedData.get(key));
|
|
return new SiteSpecificBrowserBase(scope);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the given URI is considered to be a part of this SSB or not.
|
|
* Any URIs that return false should be loaded in a normal browser.
|
|
*
|
|
* @param {nsIURI} uri the URI to check.
|
|
* @return {boolean} whether this SSB can load the URI.
|
|
*/
|
|
canLoad(uri) {
|
|
// Always allow loading about:blank as it is the initial page for iframes.
|
|
if (uri.spec == "about:blank") {
|
|
return true;
|
|
}
|
|
|
|
return scopeIncludes(this._scope, uri);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The SSB instance used in the main process.
|
|
*
|
|
* We maintain three pieces of data for an SSB:
|
|
*
|
|
* First is the string UUID for identification purposes.
|
|
*
|
|
* Second is an app manifest (https://w3c.github.io/manifest/). If the site does
|
|
* not provide one a basic one will be automatically generated. The intent is to
|
|
* never modify this such that it can be updated from the site when needed
|
|
* without blowing away any configuration changes a user might want to make to
|
|
* the SSB itself.
|
|
*
|
|
* Thirdly there is the SSB configuration. This includes internal data, user
|
|
* overrides for the app manifest and custom SSB extensions to the app manifest.
|
|
*
|
|
* We pass data based on these down to the SiteSpecificBrowserBase in this and
|
|
* other processes (via `_updateSharedData`).
|
|
*/
|
|
class SiteSpecificBrowser extends SiteSpecificBrowserBase {
|
|
/**
|
|
* Creates a new SiteSpecificBrowser. Generally should only be called by
|
|
* code within this module.
|
|
*
|
|
* @param {string} id the SSB's unique ID.
|
|
* @param {Manifest} manifest the app manifest for the SSB.
|
|
* @param {object?} config the SSB configuration settings.
|
|
*/
|
|
constructor(id, manifest, config = {}) {
|
|
if (!IS_MAIN_PROCESS) {
|
|
throw new Error(
|
|
"SiteSpecificBrowser instances are only available in the main process."
|
|
);
|
|
}
|
|
|
|
super(Services.io.newURI(manifest.scope));
|
|
this._id = id;
|
|
this._manifest = manifest;
|
|
this._config = Object.assign(
|
|
{
|
|
needsUpdate: true,
|
|
persisted: false,
|
|
},
|
|
config
|
|
);
|
|
|
|
// Cache the SSB for retrieval.
|
|
SSBMap.set(id, this);
|
|
|
|
this._updateSharedData();
|
|
}
|
|
|
|
/**
|
|
* Loads the SiteSpecificBrowser for the given ID.
|
|
*
|
|
* @param {string} id the SSB's unique ID.
|
|
* @param {object?} data the data to deserialize from. Do not use externally.
|
|
* @return {Promise<SiteSpecificBrowser?>} the instance if it exists.
|
|
*/
|
|
static async load(id, data = null) {
|
|
if (!IS_MAIN_PROCESS) {
|
|
throw new Error(
|
|
"SiteSpecificBrowser instances are only available in the main process."
|
|
);
|
|
}
|
|
|
|
if (SSBMap.has(id)) {
|
|
return SSBMap.get(id);
|
|
}
|
|
|
|
if (!data) {
|
|
let kvstore = await SiteSpecificBrowserService.getKVStore();
|
|
data = await kvstore.get(storeKey(id), null);
|
|
}
|
|
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
let parsed = JSON.parse(data);
|
|
parsed.config.persisted = true;
|
|
return new SiteSpecificBrowser(id, parsed.manifest, parsed.config);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the SiteSpecifcBrowser for an ID. Can only be called from the main
|
|
* process.
|
|
*
|
|
* @param {string} id the SSB ID.
|
|
* @return {SiteSpecificBrowser|null} the instance if it exists.
|
|
*/
|
|
static get(id) {
|
|
if (!IS_MAIN_PROCESS) {
|
|
throw new Error(
|
|
"SiteSpecificBrowser instances are only available in the main process."
|
|
);
|
|
}
|
|
|
|
return SSBMap.get(id);
|
|
}
|
|
|
|
/**
|
|
* Creates an SSB from a parsed app manifest.
|
|
*
|
|
* @param {Manifest} manifest the app manifest for the site.
|
|
* @return {Promise<SiteSpecificBrowser>} the generated SSB.
|
|
*/
|
|
static async createFromManifest(manifest) {
|
|
if (!SiteSpecificBrowserService.isEnabled) {
|
|
throw new Error("Site specific browsing is disabled.");
|
|
}
|
|
|
|
if (!manifest.scope.startsWith("https:")) {
|
|
throw new Error(
|
|
"Site specific browsers can only be opened for secure sites."
|
|
);
|
|
}
|
|
|
|
return new SiteSpecificBrowser(uuid(), manifest, { needsUpdate: false });
|
|
}
|
|
|
|
/**
|
|
* Creates an SSB from a site loaded in a browser element.
|
|
*
|
|
* @param {Element} browser the browser element the site is loaded in.
|
|
* @return {Promise<SiteSpecificBrowser>} the generated SSB.
|
|
*/
|
|
static async createFromBrowser(browser) {
|
|
if (!SiteSpecificBrowserService.isEnabled) {
|
|
throw new Error("Site specific browsing is disabled.");
|
|
}
|
|
|
|
if (!browser.currentURI.schemeIs("https")) {
|
|
throw new Error(
|
|
"Site specific browsers can only be opened for secure sites."
|
|
);
|
|
}
|
|
|
|
let manifest = await buildManifestForBrowser(browser);
|
|
let ssb = await SiteSpecificBrowser.createFromManifest(manifest);
|
|
|
|
if (!manifest.name) {
|
|
ssb.name = browser.contentTitle;
|
|
}
|
|
return ssb;
|
|
}
|
|
|
|
/**
|
|
* Creates an SSB from a sURI.
|
|
*
|
|
* @param {nsIURI} uri the uri to generate from.
|
|
* @return {SiteSpecificBrowser} the generated SSB.
|
|
*/
|
|
static createFromURI(uri) {
|
|
if (!SiteSpecificBrowserService.isEnabled) {
|
|
throw new Error("Site specific browsing is disabled.");
|
|
}
|
|
|
|
if (!uri.schemeIs("https")) {
|
|
throw new Error(
|
|
"Site specific browsers can only be opened for secure sites."
|
|
);
|
|
}
|
|
|
|
return new SiteSpecificBrowser(uuid(), manifestForURI(uri));
|
|
}
|
|
|
|
/**
|
|
* Caches the data needed by content processes.
|
|
*/
|
|
_updateSharedData() {
|
|
Services.ppmm.sharedData.set(sharedDataKey(this.id), this._scope.spec);
|
|
Services.ppmm.sharedData.flush();
|
|
}
|
|
|
|
/**
|
|
* Persists the data to the store if needed. When a change in configuration
|
|
* has occured call this.
|
|
*/
|
|
async _maybeSave() {
|
|
// If this SSB is persisted then update it in the data store.
|
|
if (this._config.persisted) {
|
|
let data = {
|
|
manifest: this._manifest,
|
|
config: this._config,
|
|
};
|
|
|
|
let kvstore = await SiteSpecificBrowserService.getKVStore();
|
|
await kvstore.put(storeKey(this.id), JSON.stringify(data));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Installs this SiteSpecificBrowser such that it exists for future instances
|
|
* of the application and will appear in lists of installed SSBs.
|
|
*/
|
|
async install() {
|
|
if (this._config.persisted) {
|
|
return;
|
|
}
|
|
|
|
this._config.persisted = true;
|
|
await this._maybeSave();
|
|
|
|
if (AppConstants.platform == "win") {
|
|
await WindowsSupport.install(this);
|
|
}
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"site-specific-browser-install",
|
|
this.id
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Uninstalls this SiteSpecificBrowser. Undoes eveerything above. The SSB is
|
|
* still usable afterwards.
|
|
*/
|
|
async uninstall() {
|
|
if (!this._config.persisted) {
|
|
return;
|
|
}
|
|
|
|
if (AppConstants.platform == "win") {
|
|
await WindowsSupport.uninstall(this);
|
|
}
|
|
|
|
this._config.persisted = false;
|
|
let kvstore = await SiteSpecificBrowserService.getKVStore();
|
|
await kvstore.delete(storeKey(this.id));
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"site-specific-browser-uninstall",
|
|
this.id
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The SSB's ID.
|
|
*/
|
|
get id() {
|
|
return this._id;
|
|
}
|
|
|
|
get name() {
|
|
if (this._config.name) {
|
|
return this._config.name;
|
|
}
|
|
|
|
if (this._manifest.name) {
|
|
return this._manifest.name;
|
|
}
|
|
|
|
return this.startURI.host;
|
|
}
|
|
|
|
set name(val) {
|
|
this._config.name = val;
|
|
this._maybeSave();
|
|
}
|
|
|
|
/**
|
|
* The default URI to load.
|
|
*/
|
|
get startURI() {
|
|
return Services.io.newURI(this._manifest.start_url);
|
|
}
|
|
|
|
/**
|
|
* Whether this SSB needs to be checked for an updated manifest.
|
|
*/
|
|
get needsUpdate() {
|
|
return this._config.needsUpdate;
|
|
}
|
|
|
|
/**
|
|
* Gets the best icon for the requested size. It may not be the exact size
|
|
* requested.
|
|
*
|
|
* Finds the smallest icon that is larger than the requested size. If no such
|
|
* icon exists returns the largest icon available. Returns null only if there
|
|
* are no icons at all.
|
|
*
|
|
* @param {Number} size the size of the desired icon in pixels.
|
|
* @return {IconResource} the icon resource for the icon.
|
|
*/
|
|
getIcon(size) {
|
|
if (!this._iconSizes) {
|
|
this._iconSizes = buildIconList(this._manifest.icons);
|
|
}
|
|
|
|
if (!this._iconSizes.length) {
|
|
return null;
|
|
}
|
|
|
|
let i = 0;
|
|
while (i < this._iconSizes.length && this._iconSizes[i].size < size) {
|
|
i++;
|
|
}
|
|
|
|
return i < this._iconSizes.length
|
|
? this._iconSizes[i].icon
|
|
: this._iconSizes[this._iconSizes.length - 1].icon;
|
|
}
|
|
|
|
/**
|
|
* Gets the best icon for the requested size. If there isn't a perfect match
|
|
* the closest match will be scaled.
|
|
*
|
|
* @param {Number} size the size of the desired icon in pixels.
|
|
* @return {string|null} a data URI for the icon.
|
|
*/
|
|
async getScaledIcon(size) {
|
|
let icon = this.getIcon(size);
|
|
if (!icon) {
|
|
return null;
|
|
}
|
|
|
|
let { container } = await ImageTools.loadImage(
|
|
Services.io.newURI(icon.src)
|
|
);
|
|
return ImageTools.scaleImage(container, size, size);
|
|
}
|
|
|
|
/**
|
|
* Updates this SSB from a new app manifest.
|
|
*
|
|
* @param {Manifest} manifest the new app manifest.
|
|
*/
|
|
async updateFromManifest(manifest) {
|
|
this._manifest = manifest;
|
|
this._iconSizes = null;
|
|
this._scope = Services.io.newURI(this._manifest.scope);
|
|
this._config.needsUpdate = false;
|
|
|
|
this._updateSharedData();
|
|
await this._maybeSave();
|
|
}
|
|
|
|
/**
|
|
* Updates this SSB from the site loaded in the browser element.
|
|
*
|
|
* @param {Element} browser the browser element.
|
|
*/
|
|
async updateFromBrowser(browser) {
|
|
let manifest = await buildManifestForBrowser(browser);
|
|
await this.updateFromManifest(manifest);
|
|
}
|
|
|
|
/**
|
|
* Launches a SSB by opening the necessary UI.
|
|
*
|
|
* @param {nsIURI?} the initial URI to load. If not provided uses the default.
|
|
*/
|
|
launch(uri = null) {
|
|
let sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
|
|
|
let idstr = Cc["@mozilla.org/supports-string;1"].createInstance(
|
|
Ci.nsISupportsString
|
|
);
|
|
idstr.data = this.id;
|
|
sa.appendElement(idstr);
|
|
|
|
if (uri) {
|
|
let uristr = Cc["@mozilla.org/supports-string;1"].createInstance(
|
|
Ci.nsISupportsString
|
|
);
|
|
uristr.data = uri.spec;
|
|
sa.appendElement(uristr);
|
|
}
|
|
|
|
let win = Services.ww.openWindow(
|
|
null,
|
|
"chrome://browser/content/ssb/ssb.html",
|
|
"_blank",
|
|
"chrome,dialog=no,all",
|
|
sa
|
|
);
|
|
|
|
if (Services.appinfo.OS == "WINNT") {
|
|
WindowsSupport.applyOSIntegration(this, win);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the KV store for SSBs. Should always resolve with a store even if that
|
|
* means wiping whatever is currently on disk because it was unreadable.
|
|
*/
|
|
async function loadKVStore() {
|
|
let dir = OS.Path.join(OS.Constants.Path.profileDir, "ssb");
|
|
|
|
/**
|
|
* Creates an empty store. Called when we know there is an empty directory.
|
|
*/
|
|
async function createStore() {
|
|
await OS.File.makeDir(dir);
|
|
let kvstore = await KeyValueService.getOrCreate(dir, "ssb");
|
|
await kvstore.put(
|
|
"_meta",
|
|
JSON.stringify({
|
|
version: DATA_VERSION,
|
|
})
|
|
);
|
|
|
|
return kvstore;
|
|
}
|
|
|
|
// First see if anything exists.
|
|
try {
|
|
let info = await OS.File.stat(dir);
|
|
|
|
if (!info.isDir) {
|
|
await OS.File.remove(dir, { ignoreAbsent: true });
|
|
return createStore();
|
|
}
|
|
} catch (e) {
|
|
if (e.becauseNoSuchFile) {
|
|
return createStore();
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
// Something exists, try to load it.
|
|
try {
|
|
let kvstore = await KeyValueService.getOrCreate(dir, "ssb");
|
|
|
|
let meta = await kvstore.get("_meta", null);
|
|
if (meta) {
|
|
let data = JSON.parse(meta);
|
|
if (data.version == DATA_VERSION) {
|
|
return kvstore;
|
|
}
|
|
console.error(`SSB store is an unexpected version ${data.version}`);
|
|
} else {
|
|
console.error("SSB store was missing meta data.");
|
|
}
|
|
|
|
// We don't know how to handle this database, re-initialize it.
|
|
await kvstore.clear();
|
|
await kvstore.put(
|
|
"_meta",
|
|
JSON.stringify({
|
|
version: DATA_VERSION,
|
|
})
|
|
);
|
|
|
|
return kvstore;
|
|
} catch (e) {
|
|
console.error(e);
|
|
|
|
// Something is very wrong. Wipe all our data and start again.
|
|
await OS.File.removeDir(dir);
|
|
return createStore();
|
|
}
|
|
}
|
|
|
|
const SiteSpecificBrowserService = {
|
|
kvstore: null,
|
|
|
|
/**
|
|
* Returns a promise that resolves to the KV store for SSBs.
|
|
*/
|
|
getKVStore() {
|
|
if (!this.kvstore) {
|
|
this.kvstore = loadKVStore();
|
|
}
|
|
|
|
return this.kvstore;
|
|
},
|
|
|
|
/**
|
|
* Checks if OS integration is enabled. This will affect whether installs and
|
|
* uninstalls have effects on the OS itself amongst other things. Generally
|
|
* only disabled for testing.
|
|
*/
|
|
get useOSIntegration() {
|
|
if (Services.appinfo.OS != "WINNT") {
|
|
return false;
|
|
}
|
|
|
|
return Services.prefs.getBoolPref("browser.ssb.osintegration", true);
|
|
},
|
|
|
|
/**
|
|
* Returns a promise that resolves to an array of all of the installed SSBs.
|
|
*/
|
|
async list() {
|
|
let kvstore = await this.getKVStore();
|
|
let list = await kvstore.enumerate(SSB_STORE_PREFIX, SSB_STORE_LAST);
|
|
return Promise.all(
|
|
Array.from(list).map(({ key: id, value: data }) =>
|
|
SiteSpecificBrowser.load(id.substring(SSB_STORE_PREFIX.length), data)
|
|
)
|
|
);
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
SiteSpecificBrowserService,
|
|
"isEnabled",
|
|
"browser.ssb.enabled",
|
|
false
|
|
);
|
|
|
|
async function startSSB(id) {
|
|
// Loading the SSB is async. Until that completes and launches we will
|
|
// be without an open window and the platform will not continue startup
|
|
// in that case. Flag that a window is coming.
|
|
Services.startup.enterLastWindowClosingSurvivalArea();
|
|
|
|
// Whatever happens we must exitLastWindowClosingSurvivalArea when done.
|
|
try {
|
|
let ssb = await SiteSpecificBrowser.load(id);
|
|
if (ssb) {
|
|
ssb.launch();
|
|
} else {
|
|
dump(`No SSB installed as ID ${id}\n`);
|
|
}
|
|
} finally {
|
|
Services.startup.exitLastWindowClosingSurvivalArea();
|
|
}
|
|
}
|
|
|
|
class SSBCommandLineHandler {
|
|
/* nsICommandLineHandler */
|
|
handle(cmdLine) {
|
|
if (!SiteSpecificBrowserService.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
let site = cmdLine.handleFlagWithParam("ssb", false);
|
|
if (site) {
|
|
cmdLine.preventDefault = true;
|
|
|
|
try {
|
|
let fixupInfo = Services.uriFixup.getFixupURIInfo(
|
|
site,
|
|
Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
|
|
);
|
|
|
|
let uri = fixupInfo.preferredURI;
|
|
if (!uri) {
|
|
dump(`Unable to parse '${site}' as a URI.\n`);
|
|
return;
|
|
}
|
|
|
|
if (fixupInfo.fixupChangedProtocol && uri.schemeIs("http")) {
|
|
uri = uri
|
|
.mutate()
|
|
.setScheme("https")
|
|
.finalize();
|
|
}
|
|
let ssb = SiteSpecificBrowser.createFromURI(uri);
|
|
ssb.launch();
|
|
} catch (e) {
|
|
dump(`Unable to parse '${site}' as a URI: ${e}\n`);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
let id = cmdLine.handleFlagWithParam("start-ssb", false);
|
|
if (id) {
|
|
cmdLine.preventDefault = true;
|
|
|
|
startSSB(id);
|
|
}
|
|
}
|
|
|
|
get helpInfo() {
|
|
return " --ssb <uri> Open a site specific browser for <uri>.\n";
|
|
}
|
|
}
|
|
|
|
SSBCommandLineHandler.prototype.QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsICommandLineHandler,
|
|
]);
|