/* 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 Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; const CLASS_MIMEINFO = "mimetype"; const CLASS_PROTOCOLINFO = "scheme"; // namespace prefix const NC_NS = "http://home.netscape.com/NC-rdf#"; // the most recent default handlers that have been injected. Note that // this is used to construct an RDF resource, which needs to have NC_NS // prepended, since that hasn't been done yet const DEFAULT_HANDLERS_VERSION = "defaultHandlersVersion"; // type list properties const NC_MIME_TYPES = NC_NS + "MIME-types"; const NC_PROTOCOL_SCHEMES = NC_NS + "Protocol-Schemes"; // content type ("type") properties // nsIHandlerInfo::type const NC_VALUE = NC_NS + "value"; const NC_DESCRIPTION = NC_NS + "description"; // additional extensions const NC_FILE_EXTENSIONS = NC_NS + "fileExtensions"; // references nsIHandlerInfo record const NC_HANDLER_INFO = NC_NS + "handlerProp"; // handler info ("info") properties // nsIHandlerInfo::preferredAction const NC_SAVE_TO_DISK = NC_NS + "saveToDisk"; const NC_HANDLE_INTERNALLY = NC_NS + "handleInternal"; const NC_USE_SYSTEM_DEFAULT = NC_NS + "useSystemDefault"; // nsIHandlerInfo::alwaysAskBeforeHandling const NC_ALWAYS_ASK = NC_NS + "alwaysAsk"; // references nsIHandlerApp records const NC_PREFERRED_APP = NC_NS + "externalApplication"; const NC_POSSIBLE_APP = NC_NS + "possibleApplication"; // handler app ("handler") properties // nsIHandlerApp::name const NC_PRETTY_NAME = NC_NS + "prettyName"; // nsILocalHandlerApp::executable const NC_PATH = NC_NS + "path"; // nsIWebHandlerApp::uriTemplate const NC_URI_TEMPLATE = NC_NS + "uriTemplate"; // nsIDBusHandlerApp::service const NC_SERVICE = NC_NS + "service"; // nsIDBusHandlerApp::method const NC_METHOD = NC_NS + "method"; // nsIDBusHandlerApp::objectPath const NC_OBJPATH = NC_NS + "objectPath"; // nsIDBusHandlerApp::dbusInterface const NC_INTERFACE = NC_NS + "dBusInterface"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function HandlerService() { this._init(); } const HandlerServiceFactory = { _instance: null, createInstance: function (outer, iid) { if (this._instance) return this._instance; let processType = Cc["@mozilla.org/xre/runtime;1"]. getService(Ci.nsIXULRuntime).processType; if (processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) return Cr.NS_ERROR_NOT_IMPLEMENTED; return (this._instance = new HandlerService()); } }; HandlerService.prototype = { //**************************************************************************// // XPCOM Plumbing classID: Components.ID("{32314cc8-22f7-4f7f-a645-1a45453ba6a6}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIHandlerService]), _xpcom_factory: HandlerServiceFactory, //**************************************************************************// // Initialization & Destruction _init: function HS__init() { // Observe profile-before-change so we can switch to the datasource // in the new profile when the user changes profiles. this._observerSvc.addObserver(this, "profile-before-change", false); // Observe xpcom-shutdown so we can remove these observers // when the application shuts down. this._observerSvc.addObserver(this, "xpcom-shutdown", false); // Observe profile-do-change so that non-default profiles get upgraded too this._observerSvc.addObserver(this, "profile-do-change", false); // do any necessary updating of the datastore this._updateDB(); }, _updateDB: function HS__updateDB() { try { var defaultHandlersVersion = this._datastoreDefaultHandlersVersion; } catch(ex) { // accessing the datastore failed, we can't update anything return; } try { // if we don't have the current version of the default prefs for // this locale, inject any new default handers into the datastore if (defaultHandlersVersion < this._prefsDefaultHandlersVersion) { // set the new version first so that if we recurse we don't // call _injectNewDefaults several times this._datastoreDefaultHandlersVersion = this._prefsDefaultHandlersVersion; this._injectNewDefaults(); } } catch (ex) { // if injecting the defaults failed, set the version back to the // previous value this._datastoreDefaultHandlersVersion = defaultHandlersVersion; } }, get _currentLocale() { var chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]. getService(Ci.nsIXULChromeRegistry); var currentLocale = chromeRegistry.getSelectedLocale("global"); return currentLocale; }, _destroy: function HS__destroy() { this._observerSvc.removeObserver(this, "profile-before-change"); this._observerSvc.removeObserver(this, "xpcom-shutdown"); this._observerSvc.removeObserver(this, "profile-do-change"); // XXX Should we also null references to all the services that get stored // by our memoizing getters in the Convenience Getters section? }, _onProfileChange: function HS__onProfileChange() { // Lose our reference to the datasource so we reacquire it // from the new profile the next time we need it. this.__ds = null; }, _isInHandlerArray: function HS__isInHandlerArray(aArray, aHandler) { var enumerator = aArray.enumerate(); while (enumerator.hasMoreElements()) { let handler = enumerator.getNext(); handler.QueryInterface(Ci.nsIHandlerApp); if (handler.equals(aHandler)) return true; } return false; }, // note that this applies to the current locale only get _datastoreDefaultHandlersVersion() { var version = this._getValue("urn:root", NC_NS + this._currentLocale + "_" + DEFAULT_HANDLERS_VERSION); return version ? version : -1; }, set _datastoreDefaultHandlersVersion(aNewVersion) { return this._setLiteral("urn:root", NC_NS + this._currentLocale + "_" + DEFAULT_HANDLERS_VERSION, aNewVersion); }, get _prefsDefaultHandlersVersion() { // get handler service pref branch var prefSvc = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); var handlerSvcBranch = prefSvc.getBranch("gecko.handlerService."); // get the version of the preferences for this locale return Number(handlerSvcBranch. getComplexValue("defaultHandlersVersion", Ci.nsIPrefLocalizedString).data); }, _injectNewDefaults: function HS__injectNewDefaults() { // get handler service pref branch var prefSvc = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); let schemesPrefBranch = prefSvc.getBranch("gecko.handlerService.schemes."); let schemePrefList = schemesPrefBranch.getChildList(""); var schemes = {}; // read all the scheme prefs into a hash for (var schemePrefName of schemePrefList) { let [scheme, handlerNumber, attribute] = schemePrefName.split("."); try { var 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) {} } let protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. getService(Ci.nsIExternalProtocolService); for (var scheme in schemes) { // This clause is essentially a reimplementation of // nsIExternalProtocolHandlerService.getProtocolHandlerInfo(). // Necessary because calling that from here would make XPConnect barf // when getService tried to re-enter the constructor for this // service. let osDefaultHandlerFound = {}; let protoInfo = protoSvc.getProtocolHandlerInfoFromOS(scheme, osDefaultHandlerFound); if (this.exists(protoInfo)) this.fillHandlerInfo(protoInfo, null); else protoSvc.setProtocolHandlerDefaults(protoInfo, osDefaultHandlerFound.value); // cache the possible handlers to avoid extra xpconnect traversals. let possibleHandlers = protoInfo.possibleApplicationHandlers; for (let handlerNumber in schemes[scheme]) { let handlerPrefs = schemes[scheme][handlerNumber]; let handlerApp = Cc["@mozilla.org/uriloader/web-handler-app;1"]. createInstance(Ci.nsIWebHandlerApp); handlerApp.uriTemplate = handlerPrefs.uriTemplate; handlerApp.name = handlerPrefs.name; if (!this._isInHandlerArray(possibleHandlers, handlerApp)) { possibleHandlers.appendElement(handlerApp, false); } } this.store(protoInfo); } }, //**************************************************************************// // nsIObserver observe: function HS__observe(subject, topic, data) { switch(topic) { case "profile-before-change": this._onProfileChange(); break; case "xpcom-shutdown": this._destroy(); break; case "profile-do-change": this._updateDB(); break; } }, //**************************************************************************// // nsIHandlerService enumerate: function HS_enumerate() { var handlers = Cc["@mozilla.org/array;1"]. createInstance(Ci.nsIMutableArray); this._appendHandlers(handlers, CLASS_MIMEINFO); this._appendHandlers(handlers, CLASS_PROTOCOLINFO); return handlers.enumerate(); }, fillHandlerInfo: function HS_fillHandlerInfo(aHandlerInfo, aOverrideType) { var type = aOverrideType || aHandlerInfo.type; var typeID = this._getTypeID(this._getClass(aHandlerInfo), type); // Determine whether or not information about this handler is available // in the datastore by looking for its "value" property, which stores its // type and should always be present. if (!this._hasValue(typeID, NC_VALUE)) throw Cr.NS_ERROR_NOT_AVAILABLE; // Retrieve the human-readable description of the type. if (this._hasValue(typeID, NC_DESCRIPTION)) aHandlerInfo.description = this._getValue(typeID, NC_DESCRIPTION); // Note: for historical reasons, we don't actually check that the type // record has a "handlerProp" property referencing the info record. It's // unclear whether or not we should start doing this check; perhaps some // legacy datasources don't have such references. var infoID = this._getInfoID(this._getClass(aHandlerInfo), type); aHandlerInfo.preferredAction = this._retrievePreferredAction(infoID); var preferredHandlerID = this._getPreferredHandlerID(this._getClass(aHandlerInfo), type); // Retrieve the preferred handler. // Note: for historical reasons, we don't actually check that the info // record has an "externalApplication" property referencing the preferred // handler record. It's unclear whether or not we should start doing // this check; perhaps some legacy datasources don't have such references. aHandlerInfo.preferredApplicationHandler = this._retrieveHandlerApp(preferredHandlerID); // Fill the array of possible handlers with the ones in the datastore. this._fillPossibleHandlers(infoID, aHandlerInfo.possibleApplicationHandlers, aHandlerInfo.preferredApplicationHandler); // If we have an "always ask" flag stored in the RDF, always use its // value. Otherwise, use the default value stored in the pref service. var alwaysAsk; if (this._hasValue(infoID, NC_ALWAYS_ASK)) { alwaysAsk = (this._getValue(infoID, NC_ALWAYS_ASK) != "false"); } else { var prefSvc = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); var prefBranch = prefSvc.getBranch("network.protocol-handler."); try { alwaysAsk = prefBranch.getBoolPref("warn-external." + type); } catch (e) { // will throw if pref didn't exist. try { alwaysAsk = prefBranch.getBoolPref("warn-external-default"); } catch (e) { // Nothing to tell us what to do, so be paranoid and prompt. alwaysAsk = true; } } } aHandlerInfo.alwaysAskBeforeHandling = alwaysAsk; // If the object represents a MIME type handler, then also retrieve // any file extensions. if (aHandlerInfo instanceof Ci.nsIMIMEInfo) for (let fileExtension of this._retrieveFileExtensions(typeID)) aHandlerInfo.appendExtension(fileExtension); }, store: function HS_store(aHandlerInfo) { // FIXME: when we switch from RDF to something with transactions (like // SQLite), enclose the following changes in a transaction so they all // get rolled back if any of them fail and we don't leave the datastore // in an inconsistent state. this._ensureRecordsForType(aHandlerInfo); this._storePreferredAction(aHandlerInfo); this._storePreferredHandler(aHandlerInfo); this._storePossibleHandlers(aHandlerInfo); this._storeAlwaysAsk(aHandlerInfo); this._storeExtensions(aHandlerInfo); // Write the changes to the database immediately so we don't lose them // if the application crashes. if (this._ds instanceof Ci.nsIRDFRemoteDataSource) this._ds.Flush(); }, exists: function HS_exists(aHandlerInfo) { var found; try { var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); found = this._hasLiteralAssertion(typeID, NC_VALUE, aHandlerInfo.type); } catch (e) { // If the RDF threw (eg, corrupt file), treat as nonexistent. found = false; } return found; }, remove: function HS_remove(aHandlerInfo) { var preferredHandlerID = this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); this._removeAssertions(preferredHandlerID); var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); // Get a list of possible handlers. After we have removed the info record, // we'll check if any other info records reference these handlers, and we'll // remove the handler records that aren't referenced by other info records. var possibleHandlerIDs = []; var possibleHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP); while (possibleHandlerTargets.hasMoreElements()) { let possibleHandlerTarget = possibleHandlerTargets.getNext(); // Note: possibleHandlerTarget should always be an nsIRDFResource. // The conditional is just here in case of a corrupt RDF datasource. if (possibleHandlerTarget instanceof Ci.nsIRDFResource) possibleHandlerIDs.push(possibleHandlerTarget.ValueUTF8); } // Remove the info record. this._removeAssertions(infoID); // Now that we've removed the info record, remove any possible handlers // that aren't referenced by other info records. for (let possibleHandlerID of possibleHandlerIDs) if (!this._existsResourceTarget(NC_POSSIBLE_APP, possibleHandlerID)) this._removeAssertions(possibleHandlerID); var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); this._removeAssertions(typeID); // Now that there's no longer a handler for this type, remove the type // from the list of types for which there are known handlers. var typeList = this._ensureAndGetTypeList(this._getClass(aHandlerInfo)); var type = this._rdf.GetResource(typeID); var typeIndex = typeList.IndexOf(type); if (typeIndex != -1) typeList.RemoveElementAt(typeIndex, true); // Write the changes to the database immediately so we don't lose them // if the application crashes. // XXX If we're removing a bunch of handlers at once, will flushing // after every removal cause a significant performance hit? if (this._ds instanceof Ci.nsIRDFRemoteDataSource) this._ds.Flush(); }, getTypeFromExtension: function HS_getTypeFromExtension(aFileExtension) { var fileExtension = aFileExtension.toLowerCase(); var typeID; // See bug 1100069 for why we want to fail gracefully and silently here. try { this._ds; } catch (ex) { Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE; return; } if (this._existsLiteralTarget(NC_FILE_EXTENSIONS, fileExtension)) typeID = this._getSourceForLiteral(NC_FILE_EXTENSIONS, fileExtension); if (typeID && this._hasValue(typeID, NC_VALUE)) { let type = this._getValue(typeID, NC_VALUE); if (type == "") throw Cr.NS_ERROR_FAILURE; return type; } return ""; }, //**************************************************************************// // Retrieval Methods /** * Retrieve the preferred action for the info record with the given ID. * * @param aInfoID {string} the info record ID * * @returns {integer} the preferred action enumeration value */ _retrievePreferredAction: function HS__retrievePreferredAction(aInfoID) { if (this._getValue(aInfoID, NC_SAVE_TO_DISK) == "true") return Ci.nsIHandlerInfo.saveToDisk; if (this._getValue(aInfoID, NC_USE_SYSTEM_DEFAULT) == "true") return Ci.nsIHandlerInfo.useSystemDefault; if (this._getValue(aInfoID, NC_HANDLE_INTERNALLY) == "true") return Ci.nsIHandlerInfo.handleInternally; return Ci.nsIHandlerInfo.useHelperApp; }, /** * Fill an array of possible handlers with the handlers for the given info ID. * * @param aInfoID {string} the ID of the info record * @param aPossibleHandlers {nsIMutableArray} the array of possible handlers * @param aPreferredHandler {nsIHandlerApp} the preferred handler, if any */ _fillPossibleHandlers: function HS__fillPossibleHandlers(aInfoID, aPossibleHandlers, aPreferredHandler) { // The set of possible handlers should include the preferred handler, // but legacy datastores (from before we added possible handlers) won't // include the preferred handler, so check if it's included as we build // the list of handlers, and, if it's not included, add it to the list. if (aPreferredHandler) aPossibleHandlers.appendElement(aPreferredHandler, false); var possibleHandlerTargets = this._getTargets(aInfoID, NC_POSSIBLE_APP); while (possibleHandlerTargets.hasMoreElements()) { let possibleHandlerTarget = possibleHandlerTargets.getNext(); if (!(possibleHandlerTarget instanceof Ci.nsIRDFResource)) continue; let possibleHandlerID = possibleHandlerTarget.ValueUTF8; let possibleHandler = this._retrieveHandlerApp(possibleHandlerID); if (possibleHandler && (!aPreferredHandler || !possibleHandler.equals(aPreferredHandler))) aPossibleHandlers.appendElement(possibleHandler, false); } }, /** * Retrieve the handler app object with the given ID. * * @param aHandlerAppID {string} the ID of the handler app to retrieve * * @returns {nsIHandlerApp} the handler app, if any; otherwise null */ _retrieveHandlerApp: function HS__retrieveHandlerApp(aHandlerAppID) { var handlerApp; // If it has a path, it's a local handler; otherwise, it's a web handler. if (this._hasValue(aHandlerAppID, NC_PATH)) { let executable = this._getFileWithPath(this._getValue(aHandlerAppID, NC_PATH)); if (!executable) return null; handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. createInstance(Ci.nsILocalHandlerApp); handlerApp.executable = executable; } else if (this._hasValue(aHandlerAppID, NC_URI_TEMPLATE)) { let uriTemplate = this._getValue(aHandlerAppID, NC_URI_TEMPLATE); if (!uriTemplate) return null; handlerApp = Cc["@mozilla.org/uriloader/web-handler-app;1"]. createInstance(Ci.nsIWebHandlerApp); handlerApp.uriTemplate = uriTemplate; } else if (this._hasValue(aHandlerAppID, NC_SERVICE)) { let service = this._getValue(aHandlerAppID, NC_SERVICE); if (!service) return null; let method = this._getValue(aHandlerAppID, NC_METHOD); if (!method) return null; let objpath = this._getValue(aHandlerAppID, NC_OBJPATH); if (!objpath) return null; let iface = this._getValue(aHandlerAppID, NC_INTERFACE); if (!iface) return null; handlerApp = Cc["@mozilla.org/uriloader/dbus-handler-app;1"]. createInstance(Ci.nsIDBusHandlerApp); handlerApp.service = service; handlerApp.method = method; handlerApp.objectPath = objpath; handlerApp.dBusInterface = iface; } else return null; handlerApp.name = this._getValue(aHandlerAppID, NC_PRETTY_NAME); return handlerApp; }, /* * Retrieve file extensions, if any, for the MIME type with the given type ID. * * @param aTypeID {string} the type record ID */ _retrieveFileExtensions: function HS__retrieveFileExtensions(aTypeID) { var fileExtensions = []; var fileExtensionTargets = this._getTargets(aTypeID, NC_FILE_EXTENSIONS); while (fileExtensionTargets.hasMoreElements()) { let fileExtensionTarget = fileExtensionTargets.getNext(); if (fileExtensionTarget instanceof Ci.nsIRDFLiteral && fileExtensionTarget.Value != "") fileExtensions.push(fileExtensionTarget.Value); } return fileExtensions; }, /** * Get the file with the given path. This is not as simple as merely * initializing a local file object with the path, because the path might be * relative to the current process directory, in which case we have to * construct a path starting from that directory. * * @param aPath {string} a path to a file * * @returns {nsILocalFile} the file, or null if the file does not exist */ _getFileWithPath: function HS__getFileWithPath(aPath) { var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); try { file.initWithPath(aPath); if (file.exists()) return file; } catch(ex) { // Note: for historical reasons, we don't actually check to see // if the exception is NS_ERROR_FILE_UNRECOGNIZED_PATH, which is what // nsILocalFile::initWithPath throws when a path is relative. file = this._dirSvc.get("XCurProcD", Ci.nsIFile); try { file.append(aPath); if (file.exists()) return file; } catch(ex) {} } return null; }, //**************************************************************************// // Storage Methods _storePreferredAction: function HS__storePreferredAction(aHandlerInfo) { var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); switch(aHandlerInfo.preferredAction) { case Ci.nsIHandlerInfo.saveToDisk: this._setLiteral(infoID, NC_SAVE_TO_DISK, "true"); this._removeTarget(infoID, NC_HANDLE_INTERNALLY); this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); break; case Ci.nsIHandlerInfo.handleInternally: this._setLiteral(infoID, NC_HANDLE_INTERNALLY, "true"); this._removeTarget(infoID, NC_SAVE_TO_DISK); this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); break; case Ci.nsIHandlerInfo.useSystemDefault: this._setLiteral(infoID, NC_USE_SYSTEM_DEFAULT, "true"); this._removeTarget(infoID, NC_SAVE_TO_DISK); this._removeTarget(infoID, NC_HANDLE_INTERNALLY); break; // This value is indicated in the datastore either by the absence of // the three properties or by setting them all "false". Of these two // options, the former seems preferable, because it reduces the size // of the RDF file and thus the amount of stuff we have to parse. case Ci.nsIHandlerInfo.useHelperApp: default: this._removeTarget(infoID, NC_SAVE_TO_DISK); this._removeTarget(infoID, NC_HANDLE_INTERNALLY); this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); break; } }, _storePreferredHandler: function HS__storePreferredHandler(aHandlerInfo) { var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); var handlerID = this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); var handler = aHandlerInfo.preferredApplicationHandler; if (handler) { this._storeHandlerApp(handlerID, handler); // Make this app be the preferred app for the handler info. // // Note: nsExternalHelperAppService::FillContentHandlerProperties ignores // this setting and instead identifies the preferred app as the resource // whose URI follows the pattern urn:<class>:externalApplication:<type>. // But the old downloadactions.js code used to set this property, so just // in case there is still some code somewhere that relies on its presence, // we set it here. this._setResource(infoID, NC_PREFERRED_APP, handlerID); } else { // There isn't a preferred handler. Remove the existing record for it, // if any. this._removeTarget(infoID, NC_PREFERRED_APP); this._removeAssertions(handlerID); } }, /** * Store the list of possible handler apps for the content type represented * by the given handler info object. * * @param aHandlerInfo {nsIHandlerInfo} the handler info object */ _storePossibleHandlers: function HS__storePossibleHandlers(aHandlerInfo) { var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); // First, retrieve the set of handler apps currently stored for the type, // keeping track of their IDs in a hash that we'll use to determine which // ones are no longer valid and should be removed. var currentHandlerApps = {}; var currentHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP); while (currentHandlerTargets.hasMoreElements()) { let handlerApp = currentHandlerTargets.getNext(); if (handlerApp instanceof Ci.nsIRDFResource) { let handlerAppID = handlerApp.ValueUTF8; currentHandlerApps[handlerAppID] = true; } } // Next, store any new handler apps. var newHandlerApps = aHandlerInfo.possibleApplicationHandlers.enumerate(); while (newHandlerApps.hasMoreElements()) { let handlerApp = newHandlerApps.getNext().QueryInterface(Ci.nsIHandlerApp); let handlerAppID = this._getPossibleHandlerAppID(handlerApp); if (!this._hasResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID)) { this._storeHandlerApp(handlerAppID, handlerApp); this._addResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID); } delete currentHandlerApps[handlerAppID]; } // Finally, remove any old handler apps that aren't being used anymore, // and if those handler apps aren't being used by any other type either, // then completely remove their record from the datastore so we don't // leave it clogged up with information about handler apps we don't care // about anymore. for (let handlerAppID in currentHandlerApps) { this._removeResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID); if (!this._existsResourceTarget(NC_POSSIBLE_APP, handlerAppID)) this._removeAssertions(handlerAppID); } }, /** * Store the given handler app. * * Note: the reason this method takes the ID of the handler app in a param * is that the ID is different than it usually is when the handler app * in question is a preferred handler app, so this method can't just derive * the ID of the handler app by calling _getPossibleHandlerAppID, its callers * have to do that for it. * * @param aHandlerAppID {string} the ID of the handler app to store * @param aHandlerApp {nsIHandlerApp} the handler app to store */ _storeHandlerApp: function HS__storeHandlerApp(aHandlerAppID, aHandlerApp) { aHandlerApp.QueryInterface(Ci.nsIHandlerApp); this._setLiteral(aHandlerAppID, NC_PRETTY_NAME, aHandlerApp.name); // In the case of the preferred handler, the handler ID could have been // used to refer to a different kind of handler in the past (i.e. either // a local hander or a web handler), so if the new handler is a local // handler, then we remove any web handler properties and vice versa. // This is unnecessary but harmless for possible handlers. if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { this._setLiteral(aHandlerAppID, NC_PATH, aHandlerApp.executable.path); this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE); this._removeTarget(aHandlerAppID, NC_METHOD); this._removeTarget(aHandlerAppID, NC_SERVICE); this._removeTarget(aHandlerAppID, NC_OBJPATH); this._removeTarget(aHandlerAppID, NC_INTERFACE); } else if(aHandlerApp instanceof Ci.nsIWebHandlerApp){ aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp); this._setLiteral(aHandlerAppID, NC_URI_TEMPLATE, aHandlerApp.uriTemplate); this._removeTarget(aHandlerAppID, NC_PATH); this._removeTarget(aHandlerAppID, NC_METHOD); this._removeTarget(aHandlerAppID, NC_SERVICE); this._removeTarget(aHandlerAppID, NC_OBJPATH); this._removeTarget(aHandlerAppID, NC_INTERFACE); } else if(aHandlerApp instanceof Ci.nsIDBusHandlerApp){ aHandlerApp.QueryInterface(Ci.nsIDBusHandlerApp); this._setLiteral(aHandlerAppID, NC_SERVICE, aHandlerApp.service); this._setLiteral(aHandlerAppID, NC_METHOD, aHandlerApp.method); this._setLiteral(aHandlerAppID, NC_OBJPATH, aHandlerApp.objectPath); this._setLiteral(aHandlerAppID, NC_INTERFACE, aHandlerApp.dBusInterface); this._removeTarget(aHandlerAppID, NC_PATH); this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE); } else { throw "unknown handler type"; } }, _storeAlwaysAsk: function HS__storeAlwaysAsk(aHandlerInfo) { var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); this._setLiteral(infoID, NC_ALWAYS_ASK, aHandlerInfo.alwaysAskBeforeHandling ? "true" : "false"); }, _storeExtensions: function HS__storeExtensions(aHandlerInfo) { if (aHandlerInfo instanceof Ci.nsIMIMEInfo) { var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); var extEnum = aHandlerInfo.getFileExtensions(); while (extEnum.hasMore()) { let ext = extEnum.getNext().toLowerCase(); if (!this._hasLiteralAssertion(typeID, NC_FILE_EXTENSIONS, ext)) { this._setLiteral(typeID, NC_FILE_EXTENSIONS, ext); } } } }, //**************************************************************************// // Convenience Getters // Observer Service __observerSvc: null, get _observerSvc() { if (!this.__observerSvc) this.__observerSvc = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); return this.__observerSvc; }, // Directory Service __dirSvc: null, get _dirSvc() { if (!this.__dirSvc) this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); return this.__dirSvc; }, // MIME Service __mimeSvc: null, get _mimeSvc() { if (!this.__mimeSvc) this.__mimeSvc = Cc["@mozilla.org/mime;1"]. getService(Ci.nsIMIMEService); return this.__mimeSvc; }, // Protocol Service __protocolSvc: null, get _protocolSvc() { if (!this.__protocolSvc) this.__protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. getService(Ci.nsIExternalProtocolService); return this.__protocolSvc; }, // RDF Service __rdf: null, get _rdf() { if (!this.__rdf) this.__rdf = Cc["@mozilla.org/rdf/rdf-service;1"]. getService(Ci.nsIRDFService); return this.__rdf; }, // RDF Container Utils __containerUtils: null, get _containerUtils() { if (!this.__containerUtils) this.__containerUtils = Cc["@mozilla.org/rdf/container-utils;1"]. getService(Ci.nsIRDFContainerUtils); return this.__containerUtils; }, // RDF datasource containing content handling config (i.e. mimeTypes.rdf) __ds: null, get _ds() { if (!this.__ds) { var file = this._dirSvc.get("UMimTyp", Ci.nsIFile); // FIXME: make this a memoizing getter if we use it anywhere else. var ioService = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); var fileHandler = ioService.getProtocolHandler("file"). QueryInterface(Ci.nsIFileProtocolHandler); this.__ds = this._rdf.GetDataSourceBlocking(fileHandler.getURLSpecFromFile(file)); } return this.__ds; }, //**************************************************************************// // Datastore Utils /** * Get the string identifying whether this is a MIME or a protocol handler. * This string is used in the URI IDs of various RDF properties. * * @param aHandlerInfo {nsIHandlerInfo} the handler for which to get the class * * @returns {string} the class */ _getClass: function HS__getClass(aHandlerInfo) { if (aHandlerInfo instanceof Ci.nsIMIMEInfo) return CLASS_MIMEINFO; else return CLASS_PROTOCOLINFO; }, /** * Return the unique identifier for a content type record, which stores * the value field plus a reference to the content type's handler info record. * * |urn:<class>:<type>| * * XXX: should this be a property of nsIHandlerInfo? * * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) * @param aType {string} the type (a MIME type or protocol scheme) * * @returns {string} the ID */ _getTypeID: function HS__getTypeID(aClass, aType) { return "urn:" + aClass + ":" + aType; }, /** * Return the unique identifier for a handler info record, which stores * the preferredAction and alwaysAsk fields plus a reference to the preferred * handler app. Roughly equivalent to the nsIHandlerInfo interface. * * |urn:<class>:handler:<type>| * * FIXME: the type info record should be merged into the type record, * since there's a one to one relationship between them, and this record * merely stores additional attributes of a content type. * * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) * @param aType {string} the type (a MIME type or protocol scheme) * * @returns {string} the ID */ _getInfoID: function HS__getInfoID(aClass, aType) { return "urn:" + aClass + ":handler:" + aType; }, /** * Return the unique identifier for a preferred handler record, which stores * information about the preferred handler for a given content type, including * its human-readable name and the path to its executable (for a local app) * or its URI template (for a web app). * * |urn:<class>:externalApplication:<type>| * * XXX: should this be a property of nsIHandlerApp? * * FIXME: this should be an arbitrary ID, and we should retrieve it from * the datastore for a given content type via the NC:ExternalApplication * property rather than looking for a specific ID, so a handler doesn't * have to change IDs when it goes from being a possible handler to being * the preferred one (once we support possible handlers). * * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) * @param aType {string} the type (a MIME type or protocol scheme) * * @returns {string} the ID */ _getPreferredHandlerID: function HS__getPreferredHandlerID(aClass, aType) { return "urn:" + aClass + ":externalApplication:" + aType; }, /** * Return the unique identifier for a handler app record, which stores * information about a possible handler for one or more content types, * including its human-readable name and the path to its executable (for a * local app) or its URI template (for a web app). * * Note: handler app IDs for preferred handlers are different. For those, * see the _getPreferredHandlerID method. * * @param aHandlerApp {nsIHandlerApp} the handler app object */ _getPossibleHandlerAppID: function HS__getPossibleHandlerAppID(aHandlerApp) { var handlerAppID = "urn:handler:"; if (aHandlerApp instanceof Ci.nsILocalHandlerApp) handlerAppID += "local:" + aHandlerApp.executable.path; else if(aHandlerApp instanceof Ci.nsIWebHandlerApp){ aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp); handlerAppID += "web:" + aHandlerApp.uriTemplate; } else if(aHandlerApp instanceof Ci.nsIDBusHandlerApp){ aHandlerApp.QueryInterface(Ci.nsIDBusHandlerApp); handlerAppID += "dbus:" + aHandlerApp.service + " " + aHandlerApp.method + " " + aHandlerApp.uriTemplate; }else{ throw "unknown handler type"; } return handlerAppID; }, /** * Get the list of types for the given class, creating the list if it doesn't * already exist. The class can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO * (i.e. the result of a call to _getClass). * * |urn:<class>s| * |urn:<class>s:root| * * @param aClass {string} the class for which to retrieve a list of types * * @returns {nsIRDFContainer} the list of types */ _ensureAndGetTypeList: function HS__ensureAndGetTypeList(aClass) { var source = this._rdf.GetResource("urn:" + aClass + "s"); var property = this._rdf.GetResource(aClass == CLASS_MIMEINFO ? NC_MIME_TYPES : NC_PROTOCOL_SCHEMES); var target = this._rdf.GetResource("urn:" + aClass + "s:root"); // Make sure we have an arc from the source to the target. if (!this._ds.HasAssertion(source, property, target, true)) this._ds.Assert(source, property, target, true); // Make sure the target is a container. if (!this._containerUtils.IsContainer(this._ds, target)) this._containerUtils.MakeSeq(this._ds, target); // Get the type list as an RDF container. var typeList = Cc["@mozilla.org/rdf/container;1"]. createInstance(Ci.nsIRDFContainer); typeList.Init(this._ds, target); return typeList; }, /** * Make sure there are records in the datasource for the given content type * by creating them if they don't already exist. We have to do this before * storing any specific data, because we can't assume the presence * of the records (the nsIHandlerInfo object might have been created * from the OS), and the records have to all be there in order for the helper * app service to properly construct an nsIHandlerInfo object for the type. * * Based on old downloadactions.js::_ensureMIMERegistryEntry. * * @param aHandlerInfo {nsIHandlerInfo} the type to make sure has a record */ _ensureRecordsForType: function HS__ensureRecordsForType(aHandlerInfo) { // Get the list of types. var typeList = this._ensureAndGetTypeList(this._getClass(aHandlerInfo)); // If there's already a record in the datastore for this type, then we // don't need to do anything more. var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); var type = this._rdf.GetResource(typeID); if (typeList.IndexOf(type) != -1) return; // Create a basic type record for this type. typeList.AppendElement(type); this._setLiteral(typeID, NC_VALUE, aHandlerInfo.type); // Create a basic info record for this type. var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); this._setLiteral(infoID, NC_ALWAYS_ASK, "false"); this._setResource(typeID, NC_HANDLER_INFO, infoID); // XXX Shouldn't we set preferredAction to useSystemDefault? // That's what it is if there's no record in the datastore; why should it // change to useHelperApp just because we add a record to the datastore? // Create a basic preferred handler record for this type. // XXX Not sure this is necessary, since preferred handlers are optional, // and nsExternalHelperAppService::FillHandlerInfoForTypeFromDS doesn't seem // to require the record , but downloadactions.js::_ensureMIMERegistryEntry // used to create it, so we'll do the same. var preferredHandlerID = this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); this._setLiteral(preferredHandlerID, NC_PATH, ""); this._setResource(infoID, NC_PREFERRED_APP, preferredHandlerID); }, /** * Append known handlers of the given class to the given array. The class * can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO. * * @param aHandlers {array} the array of handlers to append to * @param aClass {string} the class for which to append handlers */ _appendHandlers: function HS__appendHandlers(aHandlers, aClass) { var typeList = this._ensureAndGetTypeList(aClass); var enumerator = typeList.GetElements(); while (enumerator.hasMoreElements()) { var element = enumerator.getNext(); // This should never happen. If it does, that means our datasource // is corrupted with type list entries that point to literal values // instead of resources. If it does happen, let's just do our best // to recover by ignoring this entry and moving on to the next one. if (!(element instanceof Ci.nsIRDFResource)) continue; // Get the value of the element's NC:value property, which contains // the MIME type or scheme for which we're retrieving a handler info. var type = this._getValue(element.ValueUTF8, NC_VALUE); if (!type) continue; var handler; if (typeList.Resource.ValueUTF8 == "urn:mimetypes:root") handler = this._mimeSvc.getFromTypeAndExtension(type, null); else handler = this._protocolSvc.getProtocolHandlerInfo(type); aHandlers.appendElement(handler, false); } }, /** * Whether or not a property of an RDF source has a value. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @returns {boolean} whether or not the property has a value */ _hasValue: function HS__hasValue(sourceURI, propertyURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); return this._ds.hasArcOut(source, property); }, /** * Get the value of a property of an RDF source. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @returns {string} the value of the property */ _getValue: function HS__getValue(sourceURI, propertyURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._ds.GetTarget(source, property, true); if (!target) return null; if (target instanceof Ci.nsIRDFResource) return target.ValueUTF8; if (target instanceof Ci.nsIRDFLiteral) return target.Value; return null; }, /** * Get all targets for the property of an RDF source. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * * @returns {nsISimpleEnumerator} an enumerator of targets */ _getTargets: function HS__getTargets(sourceURI, propertyURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); return this._ds.GetTargets(source, property, true); }, /** * Set a property of an RDF source to a literal value. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param value {string} the literal value */ _setLiteral: function HS__setLiteral(sourceURI, propertyURI, value) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetLiteral(value); this._setTarget(source, property, target); }, /** * Set a property of an RDF source to a resource target. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param targetURI {string} the URI of the target */ _setResource: function HS__setResource(sourceURI, propertyURI, targetURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetResource(targetURI); this._setTarget(source, property, target); }, /** * Assert an arc into the RDF datasource if there is no arc with the given * source and property; otherwise, if there is already an existing arc, * change it to point to the given target. _setLiteral and _setResource * call this after converting their string arguments into resources * and literals, and most callers should call one of those two methods * instead of this one. * * @param source {nsIRDFResource} the source * @param property {nsIRDFResource} the property * @param target {nsIRDFNode} the target */ _setTarget: function HS__setTarget(source, property, target) { if (this._ds.hasArcOut(source, property)) { var oldTarget = this._ds.GetTarget(source, property, true); this._ds.Change(source, property, oldTarget, target); } else this._ds.Assert(source, property, target, true); }, /** * Assert that a property of an RDF source has a resource target. * * The difference between this method and _setResource is that this one adds * an assertion even if one already exists, which allows its callers to make * sets of assertions (i.e. to set a property to multiple targets). * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param targetURI {string} the URI of the target */ _addResourceAssertion: function HS__addResourceAssertion(sourceURI, propertyURI, targetURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetResource(targetURI); this._ds.Assert(source, property, target, true); }, /** * Remove an assertion with a resource target. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param targetURI {string} the URI of the target */ _removeResourceAssertion: function HS__removeResourceAssertion(sourceURI, propertyURI, targetURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetResource(targetURI); this._ds.Unassert(source, property, target); }, /** * Whether or not a property of an RDF source has a given resource target. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param targetURI {string} the URI of the target * * @returns {boolean} whether or not there is such an assertion */ _hasResourceAssertion: function HS__hasResourceAssertion(sourceURI, propertyURI, targetURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetResource(targetURI); return this._ds.HasAssertion(source, property, target, true); }, /** * Whether or not a property of an RDF source has a given literal value. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property * @param value {string} the literal value * * @returns {boolean} whether or not there is such an assertion */ _hasLiteralAssertion: function HS__hasLiteralAssertion(sourceURI, propertyURI, value) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetLiteral(value); return this._ds.HasAssertion(source, property, target, true); }, /** * Whether or not there is an RDF source that has the given property set to * the given literal value. * * @param propertyURI {string} the URI of the property * @param value {string} the literal value * * @returns {boolean} whether or not there is a source */ _existsLiteralTarget: function HS__existsLiteralTarget(propertyURI, value) { var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetLiteral(value); return this._ds.hasArcIn(target, property); }, /** * Get the source for a property set to a given literal value. * * @param propertyURI {string} the URI of the property * @param value {string} the literal value */ _getSourceForLiteral: function HS__getSourceForLiteral(propertyURI, value) { var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetLiteral(value); var source = this._ds.GetSource(property, target, true); if (source) return source.ValueUTF8; return null; }, /** * Whether or not there is an RDF source that has the given property set to * the given resource target. * * @param propertyURI {string} the URI of the property * @param targetURI {string} the URI of the target * * @returns {boolean} whether or not there is a source */ _existsResourceTarget: function HS__existsResourceTarget(propertyURI, targetURI) { var property = this._rdf.GetResource(propertyURI); var target = this._rdf.GetResource(targetURI); return this._ds.hasArcIn(target, property); }, /** * Remove a property of an RDF source. * * @param sourceURI {string} the URI of the source * @param propertyURI {string} the URI of the property */ _removeTarget: function HS__removeTarget(sourceURI, propertyURI) { var source = this._rdf.GetResource(sourceURI); var property = this._rdf.GetResource(propertyURI); if (this._ds.hasArcOut(source, property)) { var target = this._ds.GetTarget(source, property, true); this._ds.Unassert(source, property, target); } }, /** * Remove all assertions about a given RDF source. * * Note: not recursive. If some assertions point to other resources, * and you want to remove assertions about those resources too, you need * to do so manually. * * @param sourceURI {string} the URI of the source */ _removeAssertions: function HS__removeAssertions(sourceURI) { var source = this._rdf.GetResource(sourceURI); var properties = this._ds.ArcLabelsOut(source); while (properties.hasMoreElements()) { let property = properties.getNext(); let targets = this._ds.GetTargets(source, property, true); while (targets.hasMoreElements()) { let target = targets.getNext(); this._ds.Unassert(source, property, target); } } } }; //****************************************************************************// // More XPCOM Plumbing this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HandlerService]);