1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const { ComponentUtils } = ChromeUtils.import(
6 "resource://gre/modules/ComponentUtils.jsm"
8 const { AppConstants } = ChromeUtils.import(
9 "resource://gre/modules/AppConstants.jsm"
11 const { XPCOMUtils } = ChromeUtils.import(
12 "resource://gre/modules/XPCOMUtils.jsm"
14 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
16 const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
18 ChromeUtils.defineModuleGetter(
21 "resource://gre/modules/FileUtils.jsm"
23 ChromeUtils.defineModuleGetter(
26 "resource://gre/modules/JSONFile.jsm"
29 XPCOMUtils.defineLazyServiceGetter(
31 "gExternalProtocolService",
32 "@mozilla.org/uriloader/external-protocol-service;1",
33 "nsIExternalProtocolService"
35 XPCOMUtils.defineLazyServiceGetter(
38 "@mozilla.org/mime;1",
42 function HandlerService() {
43 // Observe handlersvc-json-replace so we can switch to the datasource
44 Services.obs.addObserver(this, "handlersvc-json-replace", true);
47 HandlerService.prototype = {
48 classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"),
49 QueryInterface: ChromeUtils.generateQI([
50 "nsISupportsWeakReference",
58 this.__store = new JSONFile({
60 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
63 dataPostProcessor: this._dataPostProcessor.bind(this),
67 // Always call this even if this.__store was set, since it may have been
68 // set by asyncInit, which might not have completed yet.
69 this._ensureStoreInitialized();
73 __storeInitialized: false,
74 _ensureStoreInitialized() {
75 if (!this.__storeInitialized) {
76 this.__storeInitialized = true;
77 this.__store.ensureDataReady();
79 this._injectDefaultProtocolHandlersIfNeeded();
80 this._migrateProtocolHandlersIfNeeded();
82 Services.obs.notifyObservers(null, "handlersvc-store-initialized");
86 _dataPostProcessor(data) {
87 return data.defaultHandlersVersion
90 defaultHandlersVersion: {},
97 * Injects new default protocol handlers if the version in the preferences is
98 * newer than the one in the data store.
100 _injectDefaultProtocolHandlersIfNeeded() {
101 let prefsDefaultHandlersVersion;
103 prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
104 "gecko.handlerService.defaultHandlersVersion",
105 Ci.nsIPrefLocalizedString
109 ex instanceof Components.Exception &&
110 ex.result == Cr.NS_ERROR_UNEXPECTED
112 // This platform does not have any default protocol handlers configured.
119 prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data);
120 let locale = Services.locale.appLocaleAsBCP47;
122 let defaultHandlersVersion =
123 this._store.data.defaultHandlersVersion[locale] || 0;
124 if (defaultHandlersVersion < prefsDefaultHandlersVersion) {
125 this._injectDefaultProtocolHandlers();
126 this._store.data.defaultHandlersVersion[
128 ] = prefsDefaultHandlersVersion;
129 // Now save the result:
130 this._store.saveSoon();
137 _injectDefaultProtocolHandlers() {
138 let schemesPrefBranch = Services.prefs.getBranch(
139 "gecko.handlerService.schemes."
141 let schemePrefList = schemesPrefBranch.getChildList("");
145 // read all the scheme prefs into a hash
146 for (let schemePrefName of schemePrefList) {
147 let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
150 let attrData = schemesPrefBranch.getComplexValue(
152 Ci.nsIPrefLocalizedString
154 if (!(scheme in schemes)) {
155 schemes[scheme] = {};
158 if (!(handlerNumber in schemes[scheme])) {
159 schemes[scheme][handlerNumber] = {};
162 schemes[scheme][handlerNumber][attribute] = attrData;
166 // Now drop any entries without a uriTemplate, or with a broken one.
167 // The Array.from calls ensure we can safely delete things without
168 // affecting the iterator.
169 for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) {
170 let handlers = Array.from(Object.entries(handlerObject));
171 let validHandlers = 0;
172 for (let [key, obj] of handlers) {
175 !obj.uriTemplate.startsWith("https://") ||
176 !obj.uriTemplate.toLowerCase().includes("%s")
178 delete handlerObject[key];
183 if (!validHandlers) {
184 delete schemes[scheme];
188 // Now, we're going to cheat. Terribly. The idiologically correct way
189 // of implementing the following bit of code would be to fetch the
190 // handler info objects from the protocol service, manipulate those,
191 // and then store each of them.
192 // However, that's expensive. It causes us to talk to the OS about
193 // default apps, which causes the OS to go hit the disk.
194 // All we're trying to do is insert some web apps into the list. We
195 // don't care what's already in the file, we just want to do the
196 // equivalent of appending into the database. So let's just go do that:
197 for (let scheme of Object.keys(schemes)) {
198 let existingSchemeInfo = this._store.data.schemes[scheme];
199 if (!existingSchemeInfo) {
200 // Haven't seen this scheme before. Default to asking which app the
201 // user wants to use:
202 existingSchemeInfo = {
203 // Signal to future readers that we didn't ask the OS anything.
204 // When the entry is first used, get the info from the OS.
206 // The first item in the list is the preferred handler, and
207 // there isn't one, so we fill in null:
210 this._store.data.schemes[scheme] = existingSchemeInfo;
212 let { handlers } = existingSchemeInfo;
213 for (let handlerNumber of Object.keys(schemes[scheme])) {
214 let newHandler = schemes[scheme][handlerNumber];
215 // If there is already a handler registered with the same template
216 // URL, ignore the new one:
217 let matchingTemplate = handler =>
218 handler && handler.uriTemplate == newHandler.uriTemplate;
219 if (!handlers.some(matchingTemplate)) {
220 handlers.push(newHandler);
227 * Execute any migrations. Migrations are defined here for any changes or removals for
228 * existing handlers. Additions are still handled via the localized prefs infrastructure.
230 * This depends on the browser.handlers.migrations pref being set by migrateUI in
231 * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
232 * This is a comma-separated list of identifiers of migrations that need running.
233 * This avoids both re-running older migrations and keeping an additional
234 * pref around permanently.
236 _migrateProtocolHandlersIfNeeded() {
237 const kMigrations = {
239 const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
240 let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
243 if (this.exists(webcalHandler)) {
244 this.fillHandlerInfo(webcalHandler, "");
245 let shouldStore = false;
246 // First remove 30boxes from possible handlers.
247 let handlers = webcalHandler.possibleApplicationHandlers;
248 for (let i = handlers.length - 1; i >= 0; i--) {
249 let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
251 app instanceof Ci.nsIWebHandlerApp &&
252 k30BoxesRegex.test(app.uriTemplate)
255 handlers.removeElementAt(i);
258 // Then remove as a preferred handler.
259 if (webcalHandler.preferredApplicationHandler) {
260 let app = webcalHandler.preferredApplicationHandler;
262 app instanceof Ci.nsIWebHandlerApp &&
263 k30BoxesRegex.test(app.uriTemplate)
265 webcalHandler.preferredApplicationHandler = null;
269 // Then store, if we changed anything.
271 this.store(webcalHandler);
275 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
276 "secure-mail": () => {
277 const kSubstitutions = new Map([
279 "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
280 "https://mail.yahoo.co.jp/compose/?To=%s",
283 "http://www.inbox.lv/rfc2368/?value=%s",
284 "https://mail.inbox.lv/compose?to=%s",
287 "http://poczta.interia.pl/mh/?mailto=%s",
288 "https://poczta.interia.pl/mh/?mailto=%s",
291 "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
292 "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
296 function maybeReplaceURL(app) {
297 if (app instanceof Ci.nsIWebHandlerApp) {
298 let { uriTemplate } = app;
299 let sub = kSubstitutions.get(uriTemplate);
301 app.uriTemplate = sub;
307 let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
310 if (this.exists(mailHandler)) {
311 this.fillHandlerInfo(mailHandler, "");
312 let handlers = mailHandler.possibleApplicationHandlers;
313 let shouldStore = false;
314 for (let i = handlers.length - 1; i >= 0; i--) {
315 let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
316 // Note: will evaluate the RHS because it's a binary rather than
318 shouldStore |= maybeReplaceURL(app);
320 // Then check the preferred handler.
321 if (mailHandler.preferredApplicationHandler) {
322 shouldStore |= maybeReplaceURL(
323 mailHandler.preferredApplicationHandler
326 // Then store, if we changed anything. Note that store() handles
327 // duplicates, so we don't have to.
329 this.store(mailHandler);
334 let migrationsToRun = Services.prefs.getCharPref(
335 "browser.handlers.migrations",
338 migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
339 for (let migration of migrationsToRun) {
342 kMigrations[migration]();
348 if (migrationsToRun.length) {
349 Services.prefs.clearUserPref("browser.handlers.migrations");
354 return (async () => {
356 await this.__store.finalize();
359 this.__storeInitialized = false;
360 })().catch(Cu.reportError);
364 observe(subject, topic, data) {
365 if (topic != "handlersvc-json-replace") {
368 let promise = this._onDBChange();
370 Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
377 this.__store = new JSONFile({
378 path: PathUtils.join(
379 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
382 dataPostProcessor: this._dataPostProcessor.bind(this),
387 // __store can be null if we called _onDBChange in the mean time.
389 this._ensureStoreInitialized();
392 .catch(Cu.reportError);
398 let handlers = Cc["@mozilla.org/array;1"].createInstance(
401 for (let type of Object.keys(this._store.data.mimeTypes)) {
402 let handler = gMIMEService.getFromTypeAndExtension(type, null);
403 handlers.appendElement(handler);
405 for (let type of Object.keys(this._store.data.schemes)) {
406 // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
407 // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
408 // until one of its properties is accessed.
410 // Note: our caller still needs to yield periodically when iterating
411 // the enumerator and accessing handler properties to avoid monopolizing
414 let handler = new Proxy(
416 QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
419 delete this._handlerInfo;
420 return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
427 return target[name] || target._handlerInfo[name];
429 set(target, name, value) {
430 target._handlerInfo[name] = value;
434 handlers.appendElement(handler);
436 return handlers.enumerate(Ci.nsIHandlerInfo);
441 let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
443 // Retrieve an existing entry if present, instead of creating a new one, so
444 // that we preserve unknown properties for forward compatibility.
445 let storedHandlerInfo = handlerList[handlerInfo.type];
446 if (!storedHandlerInfo) {
447 storedHandlerInfo = {};
448 handlerList[handlerInfo.type] = storedHandlerInfo;
451 // Only a limited number of preferredAction values is allowed.
453 handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk ||
454 handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault ||
455 handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally ||
456 (handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
457 Services.prefs.getBoolPref(
458 "browser.download.improvements_to_download_panel"
461 storedHandlerInfo.action = handlerInfo.preferredAction;
463 storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp;
466 if (handlerInfo.alwaysAskBeforeHandling) {
467 storedHandlerInfo.ask = true;
469 delete storedHandlerInfo.ask;
472 // Build a list of unique nsIHandlerInfo instances to process later.
474 if (handlerInfo.preferredApplicationHandler) {
475 handlers.push(handlerInfo.preferredApplicationHandler);
477 for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
480 // If the caller stored duplicate handlers, we save them only once.
481 if (!handlers.some(h => h.equals(handler))) {
482 handlers.push(handler);
486 // If any of the nsIHandlerInfo instances cannot be serialized, it is not
487 // included in the final list. The first element is always the preferred
488 // handler, or null if there is none.
489 let serializableHandlers = handlers
490 .map(h => this.handlerAppToSerializable(h))
492 if (serializableHandlers.length) {
493 if (!handlerInfo.preferredApplicationHandler) {
494 serializableHandlers.unshift(null);
496 storedHandlerInfo.handlers = serializableHandlers;
498 delete storedHandlerInfo.handlers;
501 if (this._isMIMEInfo(handlerInfo)) {
502 let extensions = storedHandlerInfo.extensions || [];
503 for (let extension of handlerInfo.getFileExtensions()) {
504 extension = extension.toLowerCase();
505 // If the caller stored duplicate extensions, we save them only once.
506 if (!extensions.includes(extension)) {
507 extensions.push(extension);
510 if (extensions.length) {
511 storedHandlerInfo.extensions = extensions;
513 delete storedHandlerInfo.extensions;
517 // If we're saving *anything*, it stops being a stub:
518 delete storedHandlerInfo.stubEntry;
520 this._store.saveSoon();
522 // Now notify PDF.js. This is hacky, but a lot better than expecting all
523 // the consumers to do it...
524 if (handlerInfo.type == "application/pdf") {
525 Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
530 fillHandlerInfo(handlerInfo, overrideType) {
531 let type = overrideType || handlerInfo.type;
532 let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
535 if (!storedHandlerInfo) {
536 throw new Components.Exception(
537 "handlerSvc fillHandlerInfo: don't know this type",
538 Cr.NS_ERROR_NOT_AVAILABLE
542 let isStub = !!storedHandlerInfo.stubEntry;
543 // In the normal case, this is not a stub, so we can just read stored info
544 // and write to the handlerInfo object we were passed.
546 handlerInfo.preferredAction = storedHandlerInfo.action;
547 handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
549 // If we've got a stub, ensure the defaults are still set:
550 gExternalProtocolService.setProtocolHandlerDefaults(
552 handlerInfo.hasDefaultHandler
555 handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
556 handlerInfo.alwaysAskBeforeHandling
558 // `store` will default to `useHelperApp` because `alwaysAsk` is
559 // not one of the 3 recognized options; for compatibility, do
561 handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
564 // If it *is* a stub, don't override alwaysAskBeforeHandling or the
565 // preferred actions. Instead, just append the stored handlers, without
566 // overriding the preferred app, and then schedule a task to store proper
567 // info for this handler.
568 this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
570 if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
571 for (let extension of storedHandlerInfo.extensions) {
572 handlerInfo.appendExtension(extension);
574 } else if (this._mockedHandler) {
575 this._insertMockedHandler(handlerInfo);
580 * Private method to inject stored handler information into an nsIHandlerInfo
582 * @param handlerInfo the nsIHandlerInfo instance to write to
583 * @param storedHandlers the stored handlers
584 * @param keepPreferredApp whether to keep the handlerInfo's
585 * preferredApplicationHandler or override it
586 * (default: false, ie override it)
588 _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
589 // If the first item is not null, it is also the preferred handler. Since
590 // we cannot modify the stored array, use a boolean to keep track of this.
591 let isFirstItem = true;
592 for (let handler of storedHandlers || [null]) {
593 let handlerApp = this.handlerAppFromSerializable(handler || {});
596 // Do not overwrite the preferred app unless that's allowed
597 if (!keepPreferredApp) {
598 handlerInfo.preferredApplicationHandler = handlerApp;
602 handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
609 * A nsIHandlerApp handler app
610 * @returns Serializable representation of a handler app object.
612 handlerAppToSerializable(handler) {
613 if (handler instanceof Ci.nsILocalHandlerApp) {
616 path: handler.executable.path,
618 } else if (handler instanceof Ci.nsIWebHandlerApp) {
621 uriTemplate: handler.uriTemplate,
623 } else if (handler instanceof Ci.nsIDBusHandlerApp) {
626 service: handler.service,
627 method: handler.method,
628 objectPath: handler.objectPath,
629 dBusInterface: handler.dBusInterface,
631 } else if (handler instanceof Ci.nsIGIOMimeApp) {
634 command: handler.command,
637 // If the handler is an unknown handler type, return null.
638 // Android default application handler is the case.
644 * Serializable representation of a handler object.
645 * @returns {nsIHandlerApp} the handler app, if any; otherwise null
647 handlerAppFromSerializable(handlerObj) {
649 if ("path" in handlerObj) {
651 let file = new FileUtils.File(handlerObj.path);
652 if (!file.exists()) {
656 "@mozilla.org/uriloader/local-handler-app;1"
657 ].createInstance(Ci.nsILocalHandlerApp);
658 handlerApp.executable = file;
662 } else if ("uriTemplate" in handlerObj) {
664 "@mozilla.org/uriloader/web-handler-app;1"
665 ].createInstance(Ci.nsIWebHandlerApp);
666 handlerApp.uriTemplate = handlerObj.uriTemplate;
667 } else if ("service" in handlerObj) {
669 "@mozilla.org/uriloader/dbus-handler-app;1"
670 ].createInstance(Ci.nsIDBusHandlerApp);
671 handlerApp.service = handlerObj.service;
672 handlerApp.method = handlerObj.method;
673 handlerApp.objectPath = handlerObj.objectPath;
674 handlerApp.dBusInterface = handlerObj.dBusInterface;
675 } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
677 handlerApp = Cc["@mozilla.org/gio-service;1"]
678 .getService(Ci.nsIGIOService)
679 .createAppFromCommand(handlerObj.command, handlerObj.name);
687 handlerApp.name = handlerObj.name;
692 * The function returns a reference to the "mimeTypes" or "schemes" object
693 * based on which type of handlerInfo is provided.
695 _getHandlerListByHandlerInfoType(handlerInfo) {
696 return this._isMIMEInfo(handlerInfo)
697 ? this._store.data.mimeTypes
698 : this._store.data.schemes;
702 * Determines whether an nsIHandlerInfo instance represents a MIME type.
704 _isMIMEInfo(handlerInfo) {
705 // We cannot rely only on the instanceof check because on Android both MIME
706 // types and protocols are instances of nsIMIMEInfo. We still do the check
707 // so that properties of nsIMIMEInfo become available to the callers.
709 handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
714 exists(handlerInfo) {
716 handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
721 remove(handlerInfo) {
722 delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
723 this._store.saveSoon();
727 getTypeFromExtension(fileExtension) {
728 let extension = fileExtension.toLowerCase();
729 let mimeTypes = this._store.data.mimeTypes;
730 for (let type of Object.keys(mimeTypes)) {
732 mimeTypes[type].extensions &&
733 mimeTypes[type].extensions.includes(extension)
741 _mockedHandler: null,
742 _mockedProtocol: null,
744 _insertMockedHandler(handlerInfo) {
745 if (handlerInfo.type == this._mockedProtocol) {
746 handlerInfo.preferredApplicationHandler = this._mockedHandler;
747 handlerInfo.possibleApplicationHandlers.insertElementAt(
754 // test-only: mock the handler instance for a particular protocol/scheme
755 mockProtocolHandler(protocol) {
757 this._mockedProtocol = null;
758 this._mockedHandler = null;
761 this._mockedProtocol = protocol;
762 this._mockedHandler = {
763 QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
764 launchWithURI(uri, context) {
765 Services.obs.notifyObservers(uri, "mocked-protocol-handler");
767 name: "Mocked handler",
768 detailedDescription: "Mocked handler for tests",
773 if (AppConstants.platform == "macosx") {
774 // We need an app path that isn't us, nor in our app bundle, and
775 // Apple no longer allows us to read the default-shipped apps
776 // in /Applications/ - except for Safari, it would appear!
777 let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
778 f.initWithPath("/Applications/Safari.app");
781 return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
784 clearParameters() {},
785 appendParameter() {},
794 this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);