diff options
Diffstat (limited to '')
-rw-r--r-- | uriloader/exthandler/HandlerService.js | 675 |
1 files changed, 675 insertions, 0 deletions
diff --git a/uriloader/exthandler/HandlerService.js b/uriloader/exthandler/HandlerService.js new file mode 100644 index 0000000000..085ddab31c --- /dev/null +++ b/uriloader/exthandler/HandlerService.js @@ -0,0 +1,675 @@ +/* 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 { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + +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"); + } + }, + + _dataPostProcessor(data) { + return data.defaultHandlersVersion + ? data + : { + defaultHandlersVersion: {}, + mimeTypes: {}, + schemes: {}, + }; + }, + + /** + * Injects new default protocol handlers if the version in the preferences is + * newer than the one in the data store. + */ + _injectDefaultProtocolHandlersIfNeeded() { + let prefsDefaultHandlersVersion; + try { + prefsDefaultHandlersVersion = Services.prefs.getComplexValue( + "gecko.handlerService.defaultHandlersVersion", + Ci.nsIPrefLocalizedString + ); + } catch (ex) { + if ( + ex instanceof Components.Exception && + ex.result == Cr.NS_ERROR_UNEXPECTED + ) { + // This platform does not have any default protocol handlers configured. + return; + } + throw ex; + } + + try { + prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data); + let locale = Services.locale.appLocaleAsBCP47; + + let defaultHandlersVersion = + this._store.data.defaultHandlersVersion[locale] || 0; + if (defaultHandlersVersion < prefsDefaultHandlersVersion) { + this._injectDefaultProtocolHandlers(); + this._store.data.defaultHandlersVersion[ + locale + ] = prefsDefaultHandlersVersion; + // Now save the result: + this._store.saveSoon(); + } + } catch (ex) { + Cu.reportError(ex); + } + }, + + _injectDefaultProtocolHandlers() { + let schemesPrefBranch = Services.prefs.getBranch( + "gecko.handlerService.schemes." + ); + let schemePrefList = schemesPrefBranch.getChildList(""); + + let schemes = {}; + + // read all the scheme prefs into a hash + for (let schemePrefName of schemePrefList) { + let [scheme, handlerNumber, attribute] = schemePrefName.split("."); + + try { + let attrData = schemesPrefBranch.getComplexValue( + schemePrefName, + Ci.nsIPrefLocalizedString + ).data; + if (!(scheme in schemes)) { + schemes[scheme] = {}; + } + + if (!(handlerNumber in schemes[scheme])) { + schemes[scheme][handlerNumber] = {}; + } + + schemes[scheme][handlerNumber][attribute] = attrData; + } catch (ex) {} + } + + // Now drop any entries without a uriTemplate, or with a broken one. + // The Array.from calls ensure we can safely delete things without + // affecting the iterator. + for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) { + let handlers = Array.from(Object.entries(handlerObject)); + let validHandlers = 0; + for (let [key, obj] of handlers) { + if ( + !obj.uriTemplate || + !obj.uriTemplate.startsWith("https://") || + !obj.uriTemplate.toLowerCase().includes("%s") + ) { + delete handlerObject[key]; + } else { + validHandlers++; + } + } + if (!validHandlers) { + delete 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(schemes)) { + 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 handlerNumber of Object.keys(schemes[scheme])) { + let newHandler = schemes[scheme][handlerNumber]; + // 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); + } + } + }, + }; + 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); + } + }, + + // nsIHandlerService + enumerate() { + let handlers = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + for (let type of Object.keys(this._store.data.mimeTypes)) { + let handler = gMIMEService.getFromTypeAndExtension(type, null); + 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 == Ci.nsIHandlerInfo.saveToDisk || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally + ) { + storedHandlerInfo.action = handlerInfo.preferredAction; + } else { + storedHandlerInfo.action = Ci.nsIHandlerInfo.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 == Ci.nsIHandlerInfo.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 = Ci.nsIHandlerInfo.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); + } + } + }, + + /** + * 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 ""; + }, +}; + +this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]); |