зеркало из https://github.com/mozilla/gecko-dev.git
852 строки
28 KiB
JavaScript
852 строки
28 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/. */
|
|
|
|
const { ComponentUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/ComponentUtils.jsm"
|
|
);
|
|
const { AppConstants } = ChromeUtils.import(
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
const {
|
|
saveToDisk,
|
|
alwaysAsk,
|
|
useHelperApp,
|
|
handleInternally,
|
|
useSystemDefault,
|
|
} = Ci.nsIHandlerInfo;
|
|
|
|
const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"JSONFile",
|
|
"resource://gre/modules/JSONFile.jsm"
|
|
);
|
|
const { Integration } = ChromeUtils.import(
|
|
"resource://gre/modules/Integration.jsm"
|
|
);
|
|
|
|
/* global DownloadIntegration */
|
|
Integration.downloads.defineModuleGetter(
|
|
this,
|
|
"DownloadIntegration",
|
|
"resource://gre/modules/DownloadIntegration.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"gExternalProtocolService",
|
|
"@mozilla.org/uriloader/external-protocol-service;1",
|
|
"nsIExternalProtocolService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"gMIMEService",
|
|
"@mozilla.org/mime;1",
|
|
"nsIMIMEService"
|
|
);
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
kHandlerList: "resource://gre/modules/handlers/HandlerList.jsm",
|
|
kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.jsm",
|
|
});
|
|
|
|
function HandlerService() {
|
|
// Observe handlersvc-json-replace so we can switch to the datasource
|
|
Services.obs.addObserver(this, "handlersvc-json-replace", true);
|
|
}
|
|
|
|
HandlerService.prototype = {
|
|
classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"),
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsISupportsWeakReference",
|
|
"nsIHandlerService",
|
|
"nsIObserver",
|
|
]),
|
|
|
|
__store: null,
|
|
get _store() {
|
|
if (!this.__store) {
|
|
this.__store = new JSONFile({
|
|
path: PathUtils.join(
|
|
Services.dirsvc.get("ProfD", Ci.nsIFile).path,
|
|
"handlers.json"
|
|
),
|
|
dataPostProcessor: this._dataPostProcessor.bind(this),
|
|
});
|
|
}
|
|
|
|
// Always call this even if this.__store was set, since it may have been
|
|
// set by asyncInit, which might not have completed yet.
|
|
this._ensureStoreInitialized();
|
|
return this.__store;
|
|
},
|
|
|
|
__storeInitialized: false,
|
|
_ensureStoreInitialized() {
|
|
if (!this.__storeInitialized) {
|
|
this.__storeInitialized = true;
|
|
this.__store.ensureDataReady();
|
|
|
|
this._injectDefaultProtocolHandlersIfNeeded();
|
|
this._migrateProtocolHandlersIfNeeded();
|
|
|
|
Services.obs.notifyObservers(null, "handlersvc-store-initialized");
|
|
|
|
// Bug 1736924: run migration for browser.download.improvements_to_download_panel if applicable.
|
|
// Since we need DownloadsViewInternally to verify mimetypes, we run this after
|
|
// DownloadsViewInternally is registered via the 'handlersvc-store-initialized' notification.
|
|
this._migrateDownloadsImprovementsIfNeeded();
|
|
this._migrateSVGXMLIfNeeded();
|
|
}
|
|
},
|
|
|
|
_dataPostProcessor(data) {
|
|
return data.defaultHandlersVersion
|
|
? data
|
|
: {
|
|
defaultHandlersVersion: {},
|
|
mimeTypes: {},
|
|
schemes: {},
|
|
isDownloadsImprovementsAlreadyMigrated: false,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Injects new default protocol handlers if the version in the preferences is
|
|
* newer than the one in the data store.
|
|
*/
|
|
_injectDefaultProtocolHandlersIfNeeded() {
|
|
try {
|
|
let defaultHandlersVersion = Services.prefs.getIntPref(
|
|
"gecko.handlerService.defaultHandlersVersion",
|
|
0
|
|
);
|
|
if (defaultHandlersVersion < kHandlerListVersion) {
|
|
this._injectDefaultProtocolHandlers();
|
|
Services.prefs.setIntPref(
|
|
"gecko.handlerService.defaultHandlersVersion",
|
|
kHandlerListVersion
|
|
);
|
|
// Now save the result:
|
|
this._store.saveSoon();
|
|
}
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
},
|
|
|
|
_injectDefaultProtocolHandlers() {
|
|
let locale = Services.locale.appLocaleAsBCP47;
|
|
|
|
// Initialize handlers to default and update based on locale.
|
|
let localeHandlers = kHandlerList.default;
|
|
if (kHandlerList[locale]) {
|
|
for (let scheme in kHandlerList[locale].schemes) {
|
|
localeHandlers.schemes[scheme] = kHandlerList[locale].schemes[scheme];
|
|
}
|
|
}
|
|
|
|
// Now, we're going to cheat. Terribly. The idiologically correct way
|
|
// of implementing the following bit of code would be to fetch the
|
|
// handler info objects from the protocol service, manipulate those,
|
|
// and then store each of them.
|
|
// However, that's expensive. It causes us to talk to the OS about
|
|
// default apps, which causes the OS to go hit the disk.
|
|
// All we're trying to do is insert some web apps into the list. We
|
|
// don't care what's already in the file, we just want to do the
|
|
// equivalent of appending into the database. So let's just go do that:
|
|
for (let scheme of Object.keys(localeHandlers.schemes)) {
|
|
if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") {
|
|
// Thunderbird IS a mailto handler, it doesn't need handlers added.
|
|
continue;
|
|
}
|
|
|
|
let existingSchemeInfo = this._store.data.schemes[scheme];
|
|
if (!existingSchemeInfo) {
|
|
// Haven't seen this scheme before. Default to asking which app the
|
|
// user wants to use:
|
|
existingSchemeInfo = {
|
|
// Signal to future readers that we didn't ask the OS anything.
|
|
// When the entry is first used, get the info from the OS.
|
|
stubEntry: true,
|
|
// The first item in the list is the preferred handler, and
|
|
// there isn't one, so we fill in null:
|
|
handlers: [null],
|
|
};
|
|
this._store.data.schemes[scheme] = existingSchemeInfo;
|
|
}
|
|
let { handlers } = existingSchemeInfo;
|
|
for (let newHandler of localeHandlers.schemes[scheme].handlers) {
|
|
// If there is already a handler registered with the same template
|
|
// URL, ignore the new one:
|
|
let matchingTemplate = handler =>
|
|
handler && handler.uriTemplate == newHandler.uriTemplate;
|
|
if (!handlers.some(matchingTemplate)) {
|
|
handlers.push(newHandler);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Execute any migrations. Migrations are defined here for any changes or removals for
|
|
* existing handlers. Additions are still handled via the localized prefs infrastructure.
|
|
*
|
|
* This depends on the browser.handlers.migrations pref being set by migrateUI in
|
|
* nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
|
|
* This is a comma-separated list of identifiers of migrations that need running.
|
|
* This avoids both re-running older migrations and keeping an additional
|
|
* pref around permanently.
|
|
*/
|
|
_migrateProtocolHandlersIfNeeded() {
|
|
const kMigrations = {
|
|
"30boxes": () => {
|
|
const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
|
|
let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
|
|
"webcal"
|
|
);
|
|
if (this.exists(webcalHandler)) {
|
|
this.fillHandlerInfo(webcalHandler, "");
|
|
let shouldStore = false;
|
|
// First remove 30boxes from possible handlers.
|
|
let handlers = webcalHandler.possibleApplicationHandlers;
|
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
|
|
if (
|
|
app instanceof Ci.nsIWebHandlerApp &&
|
|
k30BoxesRegex.test(app.uriTemplate)
|
|
) {
|
|
shouldStore = true;
|
|
handlers.removeElementAt(i);
|
|
}
|
|
}
|
|
// Then remove as a preferred handler.
|
|
if (webcalHandler.preferredApplicationHandler) {
|
|
let app = webcalHandler.preferredApplicationHandler;
|
|
if (
|
|
app instanceof Ci.nsIWebHandlerApp &&
|
|
k30BoxesRegex.test(app.uriTemplate)
|
|
) {
|
|
webcalHandler.preferredApplicationHandler = null;
|
|
shouldStore = true;
|
|
}
|
|
}
|
|
// Then store, if we changed anything.
|
|
if (shouldStore) {
|
|
this.store(webcalHandler);
|
|
}
|
|
}
|
|
},
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
|
|
"secure-mail": () => {
|
|
const kSubstitutions = new Map([
|
|
[
|
|
"http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
|
|
"https://mail.yahoo.co.jp/compose/?To=%s",
|
|
],
|
|
[
|
|
"http://www.inbox.lv/rfc2368/?value=%s",
|
|
"https://mail.inbox.lv/compose?to=%s",
|
|
],
|
|
[
|
|
"http://poczta.interia.pl/mh/?mailto=%s",
|
|
"https://poczta.interia.pl/mh/?mailto=%s",
|
|
],
|
|
[
|
|
"http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
|
|
"https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
|
|
],
|
|
]);
|
|
|
|
function maybeReplaceURL(app) {
|
|
if (app instanceof Ci.nsIWebHandlerApp) {
|
|
let { uriTemplate } = app;
|
|
let sub = kSubstitutions.get(uriTemplate);
|
|
if (sub) {
|
|
app.uriTemplate = sub;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
|
|
"mailto"
|
|
);
|
|
if (this.exists(mailHandler)) {
|
|
this.fillHandlerInfo(mailHandler, "");
|
|
let handlers = mailHandler.possibleApplicationHandlers;
|
|
let shouldStore = false;
|
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
|
|
// Note: will evaluate the RHS because it's a binary rather than
|
|
// logical or.
|
|
shouldStore |= maybeReplaceURL(app);
|
|
}
|
|
// Then check the preferred handler.
|
|
if (mailHandler.preferredApplicationHandler) {
|
|
shouldStore |= maybeReplaceURL(
|
|
mailHandler.preferredApplicationHandler
|
|
);
|
|
}
|
|
// Then store, if we changed anything. Note that store() handles
|
|
// duplicates, so we don't have to.
|
|
if (shouldStore) {
|
|
this.store(mailHandler);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
let migrationsToRun = Services.prefs.getCharPref(
|
|
"browser.handlers.migrations",
|
|
""
|
|
);
|
|
migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
|
|
for (let migration of migrationsToRun) {
|
|
migration.trim();
|
|
try {
|
|
kMigrations[migration]();
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
if (migrationsToRun.length) {
|
|
Services.prefs.clearUserPref("browser.handlers.migrations");
|
|
}
|
|
},
|
|
|
|
_onDBChange() {
|
|
return (async () => {
|
|
if (this.__store) {
|
|
await this.__store.finalize();
|
|
}
|
|
this.__store = null;
|
|
this.__storeInitialized = false;
|
|
})().catch(Cu.reportError);
|
|
},
|
|
|
|
// nsIObserver
|
|
observe(subject, topic, data) {
|
|
if (topic != "handlersvc-json-replace") {
|
|
return;
|
|
}
|
|
let promise = this._onDBChange();
|
|
promise.then(() => {
|
|
Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
|
|
});
|
|
},
|
|
|
|
// nsIHandlerService
|
|
asyncInit() {
|
|
if (!this.__store) {
|
|
this.__store = new JSONFile({
|
|
path: PathUtils.join(
|
|
Services.dirsvc.get("ProfD", Ci.nsIFile).path,
|
|
"handlers.json"
|
|
),
|
|
dataPostProcessor: this._dataPostProcessor.bind(this),
|
|
});
|
|
this.__store
|
|
.load()
|
|
.then(() => {
|
|
// __store can be null if we called _onDBChange in the mean time.
|
|
if (this.__store) {
|
|
this._ensureStoreInitialized();
|
|
}
|
|
})
|
|
.catch(Cu.reportError);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update already existing handlers for non-internal mimetypes to have prefs set from alwaysAsk
|
|
* to saveToDisk. However, if reading an internal mimetype and set to alwaysAsk, update to use handleInternally.
|
|
* This migration is needed since browser.download.improvements_to_download_panel does not
|
|
* override user preferences if preferredAction = alwaysAsk. By doing so, we can ensure that file prompt
|
|
* behaviours remain consistent for most files.
|
|
*
|
|
* See Bug 1736924 for more information.
|
|
*/
|
|
_noInternalHandlingDefault: new Set([
|
|
"text/xml",
|
|
"application/xml",
|
|
"image/svg+xml",
|
|
]),
|
|
_migrateDownloadsImprovementsIfNeeded() {
|
|
// Migrate if the preference is enabled AND if the migration has never been run before.
|
|
// Otherwise, we risk overwriting preferences for existing profiles!
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"browser.download.improvements_to_download_panel"
|
|
) &&
|
|
!Services.policies.getActivePolicies()?.Handlers &&
|
|
!this._store.data.isDownloadsImprovementsAlreadyMigrated &&
|
|
AppConstants.MOZ_APP_NAME != "thunderbird"
|
|
) {
|
|
for (let [type, mimeInfo] of Object.entries(this._store.data.mimeTypes)) {
|
|
let isViewableInternally =
|
|
DownloadIntegration.shouldViewDownloadInternally(type) &&
|
|
!this._noInternalHandlingDefault.has(type);
|
|
let isAskOnly = mimeInfo && mimeInfo.ask;
|
|
|
|
if (isAskOnly) {
|
|
if (isViewableInternally) {
|
|
mimeInfo.action = handleInternally;
|
|
} else {
|
|
mimeInfo.action = saveToDisk;
|
|
}
|
|
|
|
// Sets alwaysAskBeforeHandling to false. Needed to ensure that:
|
|
// preferredAction appears as expected in preferences table; and
|
|
// downloads behaviour is updated to never show UCT window.
|
|
mimeInfo.ask = false;
|
|
}
|
|
}
|
|
|
|
this._store.data.isDownloadsImprovementsAlreadyMigrated = true;
|
|
this._store.saveSoon();
|
|
}
|
|
},
|
|
|
|
_migrateSVGXMLIfNeeded() {
|
|
// Migrate if the preference is enabled AND if the migration has never been run before.
|
|
// We need to make sure we only run this once.
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"browser.download.improvements_to_download_panel"
|
|
) &&
|
|
!Services.policies.getActivePolicies()?.Handlers &&
|
|
!this._store.data.isSVGXMLAlreadyMigrated
|
|
) {
|
|
for (let type of this._noInternalHandlingDefault) {
|
|
if (Object.hasOwn(this._store.data.mimeTypes, type)) {
|
|
let mimeInfo = this._store.data.mimeTypes[type];
|
|
if (!mimeInfo.ask && mimeInfo.action == handleInternally) {
|
|
mimeInfo.action = saveToDisk;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._store.data.isSVGXMLAlreadyMigrated = true;
|
|
this._store.saveSoon();
|
|
}
|
|
},
|
|
|
|
// nsIHandlerService
|
|
enumerate() {
|
|
let handlers = Cc["@mozilla.org/array;1"].createInstance(
|
|
Ci.nsIMutableArray
|
|
);
|
|
for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
|
|
let primaryExtension = typeInfo.extensions?.[0] ?? null;
|
|
let handler = gMIMEService.getFromTypeAndExtension(
|
|
type,
|
|
primaryExtension
|
|
);
|
|
handlers.appendElement(handler);
|
|
}
|
|
for (let type of Object.keys(this._store.data.schemes)) {
|
|
// nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
|
|
// on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
|
|
// until one of its properties is accessed.
|
|
//
|
|
// Note: our caller still needs to yield periodically when iterating
|
|
// the enumerator and accessing handler properties to avoid monopolizing
|
|
// the main thread.
|
|
//
|
|
let handler = new Proxy(
|
|
{
|
|
QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
|
|
type,
|
|
get _handlerInfo() {
|
|
delete this._handlerInfo;
|
|
return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
|
|
type
|
|
));
|
|
},
|
|
},
|
|
{
|
|
get(target, name) {
|
|
return target[name] || target._handlerInfo[name];
|
|
},
|
|
set(target, name, value) {
|
|
target._handlerInfo[name] = value;
|
|
},
|
|
}
|
|
);
|
|
handlers.appendElement(handler);
|
|
}
|
|
return handlers.enumerate(Ci.nsIHandlerInfo);
|
|
},
|
|
|
|
// nsIHandlerService
|
|
store(handlerInfo) {
|
|
let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
|
|
|
|
// Retrieve an existing entry if present, instead of creating a new one, so
|
|
// that we preserve unknown properties for forward compatibility.
|
|
let storedHandlerInfo = handlerList[handlerInfo.type];
|
|
if (!storedHandlerInfo) {
|
|
storedHandlerInfo = {};
|
|
handlerList[handlerInfo.type] = storedHandlerInfo;
|
|
}
|
|
|
|
// Only a limited number of preferredAction values is allowed.
|
|
if (
|
|
handlerInfo.preferredAction == saveToDisk ||
|
|
handlerInfo.preferredAction == useSystemDefault ||
|
|
handlerInfo.preferredAction == handleInternally ||
|
|
// For files (ie mimetype rather than protocol handling info), ensure
|
|
// we can store the "always ask" state, too:
|
|
(handlerInfo.preferredAction == alwaysAsk &&
|
|
this._isMIMEInfo(handlerInfo) &&
|
|
Services.prefs.getBoolPref(
|
|
"browser.download.improvements_to_download_panel"
|
|
))
|
|
) {
|
|
storedHandlerInfo.action = handlerInfo.preferredAction;
|
|
} else {
|
|
storedHandlerInfo.action = useHelperApp;
|
|
}
|
|
|
|
if (handlerInfo.alwaysAskBeforeHandling) {
|
|
storedHandlerInfo.ask = true;
|
|
} else {
|
|
delete storedHandlerInfo.ask;
|
|
}
|
|
|
|
// Build a list of unique nsIHandlerInfo instances to process later.
|
|
let handlers = [];
|
|
if (handlerInfo.preferredApplicationHandler) {
|
|
handlers.push(handlerInfo.preferredApplicationHandler);
|
|
}
|
|
for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
|
|
Ci.nsIHandlerApp
|
|
)) {
|
|
// If the caller stored duplicate handlers, we save them only once.
|
|
if (!handlers.some(h => h.equals(handler))) {
|
|
handlers.push(handler);
|
|
}
|
|
}
|
|
|
|
// If any of the nsIHandlerInfo instances cannot be serialized, it is not
|
|
// included in the final list. The first element is always the preferred
|
|
// handler, or null if there is none.
|
|
let serializableHandlers = handlers
|
|
.map(h => this.handlerAppToSerializable(h))
|
|
.filter(h => h);
|
|
if (serializableHandlers.length) {
|
|
if (!handlerInfo.preferredApplicationHandler) {
|
|
serializableHandlers.unshift(null);
|
|
}
|
|
storedHandlerInfo.handlers = serializableHandlers;
|
|
} else {
|
|
delete storedHandlerInfo.handlers;
|
|
}
|
|
|
|
if (this._isMIMEInfo(handlerInfo)) {
|
|
let extensions = storedHandlerInfo.extensions || [];
|
|
for (let extension of handlerInfo.getFileExtensions()) {
|
|
extension = extension.toLowerCase();
|
|
// If the caller stored duplicate extensions, we save them only once.
|
|
if (!extensions.includes(extension)) {
|
|
extensions.push(extension);
|
|
}
|
|
}
|
|
if (extensions.length) {
|
|
storedHandlerInfo.extensions = extensions;
|
|
} else {
|
|
delete storedHandlerInfo.extensions;
|
|
}
|
|
}
|
|
|
|
// If we're saving *anything*, it stops being a stub:
|
|
delete storedHandlerInfo.stubEntry;
|
|
|
|
this._store.saveSoon();
|
|
|
|
// Now notify PDF.js. This is hacky, but a lot better than expecting all
|
|
// the consumers to do it...
|
|
if (handlerInfo.type == "application/pdf") {
|
|
Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
|
|
}
|
|
},
|
|
|
|
// nsIHandlerService
|
|
fillHandlerInfo(handlerInfo, overrideType) {
|
|
let type = overrideType || handlerInfo.type;
|
|
let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
|
|
type
|
|
];
|
|
if (!storedHandlerInfo) {
|
|
throw new Components.Exception(
|
|
"handlerSvc fillHandlerInfo: don't know this type",
|
|
Cr.NS_ERROR_NOT_AVAILABLE
|
|
);
|
|
}
|
|
|
|
let isStub = !!storedHandlerInfo.stubEntry;
|
|
// In the normal case, this is not a stub, so we can just read stored info
|
|
// and write to the handlerInfo object we were passed.
|
|
if (!isStub) {
|
|
handlerInfo.preferredAction = storedHandlerInfo.action;
|
|
handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
|
|
} else {
|
|
// If we've got a stub, ensure the defaults are still set:
|
|
gExternalProtocolService.setProtocolHandlerDefaults(
|
|
handlerInfo,
|
|
handlerInfo.hasDefaultHandler
|
|
);
|
|
if (
|
|
handlerInfo.preferredAction == alwaysAsk &&
|
|
handlerInfo.alwaysAskBeforeHandling
|
|
) {
|
|
// `store` will default to `useHelperApp` because `alwaysAsk` is
|
|
// not one of the 3 recognized options; for compatibility, do
|
|
// the same here.
|
|
handlerInfo.preferredAction = useHelperApp;
|
|
}
|
|
}
|
|
// If it *is* a stub, don't override alwaysAskBeforeHandling or the
|
|
// preferred actions. Instead, just append the stored handlers, without
|
|
// overriding the preferred app, and then schedule a task to store proper
|
|
// info for this handler.
|
|
this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
|
|
|
|
if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
|
|
for (let extension of storedHandlerInfo.extensions) {
|
|
handlerInfo.appendExtension(extension);
|
|
}
|
|
} else if (this._mockedHandler) {
|
|
this._insertMockedHandler(handlerInfo);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Private method to inject stored handler information into an nsIHandlerInfo
|
|
* instance.
|
|
* @param handlerInfo the nsIHandlerInfo instance to write to
|
|
* @param storedHandlers the stored handlers
|
|
* @param keepPreferredApp whether to keep the handlerInfo's
|
|
* preferredApplicationHandler or override it
|
|
* (default: false, ie override it)
|
|
*/
|
|
_appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
|
|
// If the first item is not null, it is also the preferred handler. Since
|
|
// we cannot modify the stored array, use a boolean to keep track of this.
|
|
let isFirstItem = true;
|
|
for (let handler of storedHandlers || [null]) {
|
|
let handlerApp = this.handlerAppFromSerializable(handler || {});
|
|
if (isFirstItem) {
|
|
isFirstItem = false;
|
|
// Do not overwrite the preferred app unless that's allowed
|
|
if (!keepPreferredApp) {
|
|
handlerInfo.preferredApplicationHandler = handlerApp;
|
|
}
|
|
}
|
|
if (handlerApp) {
|
|
handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param handler
|
|
* A nsIHandlerApp handler app
|
|
* @returns Serializable representation of a handler app object.
|
|
*/
|
|
handlerAppToSerializable(handler) {
|
|
if (handler instanceof Ci.nsILocalHandlerApp) {
|
|
return {
|
|
name: handler.name,
|
|
path: handler.executable.path,
|
|
};
|
|
} else if (handler instanceof Ci.nsIWebHandlerApp) {
|
|
return {
|
|
name: handler.name,
|
|
uriTemplate: handler.uriTemplate,
|
|
};
|
|
} else if (handler instanceof Ci.nsIDBusHandlerApp) {
|
|
return {
|
|
name: handler.name,
|
|
service: handler.service,
|
|
method: handler.method,
|
|
objectPath: handler.objectPath,
|
|
dBusInterface: handler.dBusInterface,
|
|
};
|
|
} else if (handler instanceof Ci.nsIGIOMimeApp) {
|
|
return {
|
|
name: handler.name,
|
|
command: handler.command,
|
|
};
|
|
}
|
|
// If the handler is an unknown handler type, return null.
|
|
// Android default application handler is the case.
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* @param handlerObj
|
|
* Serializable representation of a handler object.
|
|
* @returns {nsIHandlerApp} the handler app, if any; otherwise null
|
|
*/
|
|
handlerAppFromSerializable(handlerObj) {
|
|
let handlerApp;
|
|
if ("path" in handlerObj) {
|
|
try {
|
|
let file = new FileUtils.File(handlerObj.path);
|
|
if (!file.exists()) {
|
|
return null;
|
|
}
|
|
handlerApp = Cc[
|
|
"@mozilla.org/uriloader/local-handler-app;1"
|
|
].createInstance(Ci.nsILocalHandlerApp);
|
|
handlerApp.executable = file;
|
|
} catch (ex) {
|
|
return null;
|
|
}
|
|
} else if ("uriTemplate" in handlerObj) {
|
|
handlerApp = Cc[
|
|
"@mozilla.org/uriloader/web-handler-app;1"
|
|
].createInstance(Ci.nsIWebHandlerApp);
|
|
handlerApp.uriTemplate = handlerObj.uriTemplate;
|
|
} else if ("service" in handlerObj) {
|
|
handlerApp = Cc[
|
|
"@mozilla.org/uriloader/dbus-handler-app;1"
|
|
].createInstance(Ci.nsIDBusHandlerApp);
|
|
handlerApp.service = handlerObj.service;
|
|
handlerApp.method = handlerObj.method;
|
|
handlerApp.objectPath = handlerObj.objectPath;
|
|
handlerApp.dBusInterface = handlerObj.dBusInterface;
|
|
} else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
|
|
try {
|
|
handlerApp = Cc["@mozilla.org/gio-service;1"]
|
|
.getService(Ci.nsIGIOService)
|
|
.createAppFromCommand(handlerObj.command, handlerObj.name);
|
|
} catch (ex) {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
handlerApp.name = handlerObj.name;
|
|
return handlerApp;
|
|
},
|
|
|
|
/**
|
|
* The function returns a reference to the "mimeTypes" or "schemes" object
|
|
* based on which type of handlerInfo is provided.
|
|
*/
|
|
_getHandlerListByHandlerInfoType(handlerInfo) {
|
|
return this._isMIMEInfo(handlerInfo)
|
|
? this._store.data.mimeTypes
|
|
: this._store.data.schemes;
|
|
},
|
|
|
|
/**
|
|
* Determines whether an nsIHandlerInfo instance represents a MIME type.
|
|
*/
|
|
_isMIMEInfo(handlerInfo) {
|
|
// We cannot rely only on the instanceof check because on Android both MIME
|
|
// types and protocols are instances of nsIMIMEInfo. We still do the check
|
|
// so that properties of nsIMIMEInfo become available to the callers.
|
|
return (
|
|
handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
|
|
);
|
|
},
|
|
|
|
// nsIHandlerService
|
|
exists(handlerInfo) {
|
|
return (
|
|
handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
|
|
);
|
|
},
|
|
|
|
// nsIHandlerService
|
|
remove(handlerInfo) {
|
|
delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
|
|
this._store.saveSoon();
|
|
},
|
|
|
|
// nsIHandlerService
|
|
getTypeFromExtension(fileExtension) {
|
|
let extension = fileExtension.toLowerCase();
|
|
let mimeTypes = this._store.data.mimeTypes;
|
|
for (let type of Object.keys(mimeTypes)) {
|
|
if (
|
|
mimeTypes[type].extensions &&
|
|
mimeTypes[type].extensions.includes(extension)
|
|
) {
|
|
return type;
|
|
}
|
|
}
|
|
return "";
|
|
},
|
|
|
|
_mockedHandler: null,
|
|
_mockedProtocol: null,
|
|
|
|
_insertMockedHandler(handlerInfo) {
|
|
if (handlerInfo.type == this._mockedProtocol) {
|
|
handlerInfo.preferredApplicationHandler = this._mockedHandler;
|
|
handlerInfo.possibleApplicationHandlers.insertElementAt(
|
|
this._mockedHandler,
|
|
0
|
|
);
|
|
}
|
|
},
|
|
|
|
// test-only: mock the handler instance for a particular protocol/scheme
|
|
mockProtocolHandler(protocol) {
|
|
if (!protocol) {
|
|
this._mockedProtocol = null;
|
|
this._mockedHandler = null;
|
|
return;
|
|
}
|
|
this._mockedProtocol = protocol;
|
|
this._mockedHandler = {
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
|
|
launchWithURI(uri, context) {
|
|
Services.obs.notifyObservers(uri, "mocked-protocol-handler");
|
|
},
|
|
name: "Mocked handler",
|
|
detailedDescription: "Mocked handler for tests",
|
|
equals(x) {
|
|
return x == this;
|
|
},
|
|
get executable() {
|
|
if (AppConstants.platform == "macosx") {
|
|
// We need an app path that isn't us, nor in our app bundle, and
|
|
// Apple no longer allows us to read the default-shipped apps
|
|
// in /Applications/ - except for Safari, it would appear!
|
|
let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
|
f.initWithPath("/Applications/Safari.app");
|
|
return f;
|
|
}
|
|
return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
|
},
|
|
parameterCount: 0,
|
|
clearParameters() {},
|
|
appendParameter() {},
|
|
getParameter() {},
|
|
parameterExists() {
|
|
return false;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);
|