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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 } = Ci.nsIHandlerInfo;
16 const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
20 ChromeUtils.defineESModuleGetters(lazy, {
21 kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs",
22 kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.sys.mjs",
23 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
24 JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
27 XPCOMUtils.defineLazyServiceGetter(
29 "externalProtocolService",
30 "@mozilla.org/uriloader/external-protocol-service;1",
31 "nsIExternalProtocolService"
33 XPCOMUtils.defineLazyServiceGetter(
36 "@mozilla.org/mime;1",
40 export function HandlerService() {
41 // Observe handlersvc-json-replace so we can switch to the datasource
42 Services.obs.addObserver(this, "handlersvc-json-replace", true);
45 HandlerService.prototype = {
46 QueryInterface: ChromeUtils.generateQI([
47 "nsISupportsWeakReference",
55 this.__store = new lazy.JSONFile({
57 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
60 dataPostProcessor: this._dataPostProcessor.bind(this),
64 // Always call this even if this.__store was set, since it may have been
65 // set by asyncInit, which might not have completed yet.
66 this._ensureStoreInitialized();
70 __storeInitialized: false,
71 _ensureStoreInitialized() {
72 if (!this.__storeInitialized) {
73 this.__storeInitialized = true;
74 this.__store.ensureDataReady();
76 this._injectDefaultProtocolHandlersIfNeeded();
77 this._migrateProtocolHandlersIfNeeded();
79 Services.obs.notifyObservers(null, "handlersvc-store-initialized");
83 _dataPostProcessor(data) {
84 return data.defaultHandlersVersion
87 defaultHandlersVersion: {},
90 isDownloadsImprovementsAlreadyMigrated: false,
95 * Injects new default protocol handlers if the version in the preferences is
96 * newer than the one in the data store.
98 _injectDefaultProtocolHandlersIfNeeded() {
100 let defaultHandlersVersion = Services.prefs.getIntPref(
101 "gecko.handlerService.defaultHandlersVersion",
104 if (defaultHandlersVersion < lazy.kHandlerListVersion) {
105 this._injectDefaultProtocolHandlers();
106 Services.prefs.setIntPref(
107 "gecko.handlerService.defaultHandlersVersion",
108 lazy.kHandlerListVersion
110 // Now save the result:
111 this._store.saveSoon();
118 _injectDefaultProtocolHandlers() {
119 let locale = Services.locale.appLocaleAsBCP47;
121 // Initialize handlers to default and update based on locale.
122 let localeHandlers = lazy.kHandlerList.default;
123 if (lazy.kHandlerList[locale]) {
124 for (let scheme in lazy.kHandlerList[locale].schemes) {
125 localeHandlers.schemes[scheme] =
126 lazy.kHandlerList[locale].schemes[scheme];
130 // Now, we're going to cheat. Terribly. The idiologically correct way
131 // of implementing the following bit of code would be to fetch the
132 // handler info objects from the protocol service, manipulate those,
133 // and then store each of them.
134 // However, that's expensive. It causes us to talk to the OS about
135 // default apps, which causes the OS to go hit the disk.
136 // All we're trying to do is insert some web apps into the list. We
137 // don't care what's already in the file, we just want to do the
138 // equivalent of appending into the database. So let's just go do that:
139 for (let scheme of Object.keys(localeHandlers.schemes)) {
140 if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") {
141 // Thunderbird IS a mailto handler, it doesn't need handlers added.
145 let existingSchemeInfo = this._store.data.schemes[scheme];
146 if (!existingSchemeInfo) {
147 // Haven't seen this scheme before. Default to asking which app the
148 // user wants to use:
149 existingSchemeInfo = {
150 // Signal to future readers that we didn't ask the OS anything.
151 // When the entry is first used, get the info from the OS.
153 // The first item in the list is the preferred handler, and
154 // there isn't one, so we fill in null:
157 this._store.data.schemes[scheme] = existingSchemeInfo;
159 let { handlers } = existingSchemeInfo;
160 for (let newHandler of localeHandlers.schemes[scheme].handlers) {
161 if (!newHandler.uriTemplate) {
163 `Ignoring protocol handler for ${scheme} without a uriTemplate!`
167 if (!newHandler.uriTemplate.startsWith("https://")) {
169 `Ignoring protocol handler for ${scheme} with insecure template URL ${newHandler.uriTemplate}.`
173 if (!newHandler.uriTemplate.toLowerCase().includes("%s")) {
175 `Ignoring protocol handler for ${scheme} with invalid template URL ${newHandler.uriTemplate}.`
179 // If there is already a handler registered with the same template
180 // URL, ignore the new one:
181 let matchingTemplate = handler =>
182 handler && handler.uriTemplate == newHandler.uriTemplate;
183 if (!handlers.some(matchingTemplate)) {
184 handlers.push(newHandler);
191 * Execute any migrations. Migrations are defined here for any changes or removals for
192 * existing handlers. Additions are still handled via the localized prefs infrastructure.
194 * This depends on the browser.handlers.migrations pref being set by migrateUI in
195 * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
196 * This is a comma-separated list of identifiers of migrations that need running.
197 * This avoids both re-running older migrations and keeping an additional
198 * pref around permanently.
200 _migrateProtocolHandlersIfNeeded() {
201 const kMigrations = {
203 const k30BoxesRegex =
204 /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
206 lazy.externalProtocolService.getProtocolHandlerInfo("webcal");
207 if (this.exists(webcalHandler)) {
208 this.fillHandlerInfo(webcalHandler, "");
209 let shouldStore = false;
210 // First remove 30boxes from possible handlers.
211 let handlers = webcalHandler.possibleApplicationHandlers;
212 for (let i = handlers.length - 1; i >= 0; i--) {
213 let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
215 app instanceof Ci.nsIWebHandlerApp &&
216 k30BoxesRegex.test(app.uriTemplate)
219 handlers.removeElementAt(i);
222 // Then remove as a preferred handler.
223 if (webcalHandler.preferredApplicationHandler) {
224 let app = webcalHandler.preferredApplicationHandler;
226 app instanceof Ci.nsIWebHandlerApp &&
227 k30BoxesRegex.test(app.uriTemplate)
229 webcalHandler.preferredApplicationHandler = null;
233 // Then store, if we changed anything.
235 this.store(webcalHandler);
239 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
240 "secure-mail": () => {
241 const kSubstitutions = new Map([
243 "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
244 "https://mail.yahoo.co.jp/compose/?To=%s",
247 "http://www.inbox.lv/rfc2368/?value=%s",
248 "https://mail.inbox.lv/compose?to=%s",
251 "http://poczta.interia.pl/mh/?mailto=%s",
252 "https://poczta.interia.pl/mh/?mailto=%s",
255 "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
256 "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
260 function maybeReplaceURL(app) {
261 if (app instanceof Ci.nsIWebHandlerApp) {
262 let { uriTemplate } = app;
263 let sub = kSubstitutions.get(uriTemplate);
265 app.uriTemplate = sub;
272 lazy.externalProtocolService.getProtocolHandlerInfo("mailto");
273 if (this.exists(mailHandler)) {
274 this.fillHandlerInfo(mailHandler, "");
275 let handlers = mailHandler.possibleApplicationHandlers;
276 let shouldStore = false;
277 for (let i = handlers.length - 1; i >= 0; i--) {
278 let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
279 // Note: will evaluate the RHS because it's a binary rather than
281 shouldStore |= maybeReplaceURL(app);
283 // Then check the preferred handler.
284 if (mailHandler.preferredApplicationHandler) {
285 shouldStore |= maybeReplaceURL(
286 mailHandler.preferredApplicationHandler
289 // Then store, if we changed anything. Note that store() handles
290 // duplicates, so we don't have to.
292 this.store(mailHandler);
297 let migrationsToRun = Services.prefs.getCharPref(
298 "browser.handlers.migrations",
301 migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
302 for (let migration of migrationsToRun) {
305 kMigrations[migration]();
311 if (migrationsToRun.length) {
312 Services.prefs.clearUserPref("browser.handlers.migrations");
317 return (async () => {
319 await this.__store.finalize();
322 this.__storeInitialized = false;
323 })().catch(console.error);
327 observe(subject, topic) {
328 if (topic != "handlersvc-json-replace") {
331 let promise = this._onDBChange();
333 Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
340 this.__store = new lazy.JSONFile({
341 path: PathUtils.join(
342 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
345 dataPostProcessor: this._dataPostProcessor.bind(this),
350 // __store can be null if we called _onDBChange in the mean time.
352 this._ensureStoreInitialized();
355 .catch(console.error);
361 let handlers = Cc["@mozilla.org/array;1"].createInstance(
364 for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
365 let primaryExtension = typeInfo.extensions?.[0] ?? null;
366 let handler = lazy.MIMEService.getFromTypeAndExtension(
370 handlers.appendElement(handler);
372 for (let type of Object.keys(this._store.data.schemes)) {
373 // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
374 // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
375 // until one of its properties is accessed.
377 // Note: our caller still needs to yield periodically when iterating
378 // the enumerator and accessing handler properties to avoid monopolizing
381 let handler = new Proxy(
383 QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
386 delete this._handlerInfo;
387 return (this._handlerInfo =
388 lazy.externalProtocolService.getProtocolHandlerInfo(type));
393 return target[name] || target._handlerInfo[name];
395 set(target, name, value) {
396 target._handlerInfo[name] = value;
400 handlers.appendElement(handler);
402 return handlers.enumerate(Ci.nsIHandlerInfo);
407 let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
409 // Retrieve an existing entry if present, instead of creating a new one, so
410 // that we preserve unknown properties for forward compatibility.
411 let storedHandlerInfo = handlerList[handlerInfo.type];
412 if (!storedHandlerInfo) {
413 storedHandlerInfo = {};
414 handlerList[handlerInfo.type] = storedHandlerInfo;
417 // Only a limited number of preferredAction values is allowed.
419 handlerInfo.preferredAction == saveToDisk ||
420 handlerInfo.preferredAction == useSystemDefault ||
421 handlerInfo.preferredAction == handleInternally ||
422 // For files (ie mimetype rather than protocol handling info), ensure
423 // we can store the "always ask" state, too:
424 (handlerInfo.preferredAction == alwaysAsk &&
425 this._isMIMEInfo(handlerInfo))
427 storedHandlerInfo.action = handlerInfo.preferredAction;
429 storedHandlerInfo.action = useHelperApp;
432 if (handlerInfo.alwaysAskBeforeHandling) {
433 storedHandlerInfo.ask = true;
435 delete storedHandlerInfo.ask;
438 // Build a list of unique nsIHandlerInfo instances to process later.
440 if (handlerInfo.preferredApplicationHandler) {
441 handlers.push(handlerInfo.preferredApplicationHandler);
443 for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
446 // If the caller stored duplicate handlers, we save them only once.
447 if (!handlers.some(h => h.equals(handler))) {
448 handlers.push(handler);
452 // If any of the nsIHandlerInfo instances cannot be serialized, it is not
453 // included in the final list. The first element is always the preferred
454 // handler, or null if there is none.
455 let serializableHandlers = handlers
456 .map(h => this.handlerAppToSerializable(h))
458 if (serializableHandlers.length) {
459 if (!handlerInfo.preferredApplicationHandler) {
460 serializableHandlers.unshift(null);
462 storedHandlerInfo.handlers = serializableHandlers;
464 delete storedHandlerInfo.handlers;
467 if (this._isMIMEInfo(handlerInfo)) {
468 let extensions = storedHandlerInfo.extensions || [];
469 for (let extension of handlerInfo.getFileExtensions()) {
470 extension = extension.toLowerCase();
471 // If the caller stored duplicate extensions, we save them only once.
472 if (!extensions.includes(extension)) {
473 extensions.push(extension);
476 if (extensions.length) {
477 storedHandlerInfo.extensions = extensions;
479 delete storedHandlerInfo.extensions;
483 // If we're saving *anything*, it stops being a stub:
484 delete storedHandlerInfo.stubEntry;
486 this._store.saveSoon();
488 // Now notify PDF.js. This is hacky, but a lot better than expecting all
489 // the consumers to do it...
490 if (handlerInfo.type == "application/pdf") {
491 Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
496 fillHandlerInfo(handlerInfo, overrideType) {
497 let type = overrideType || handlerInfo.type;
498 let storedHandlerInfo =
499 this._getHandlerListByHandlerInfoType(handlerInfo)[type];
500 if (!storedHandlerInfo) {
501 throw new Components.Exception(
502 "handlerSvc fillHandlerInfo: don't know this type",
503 Cr.NS_ERROR_NOT_AVAILABLE
507 let isStub = !!storedHandlerInfo.stubEntry;
508 // In the normal case, this is not a stub, so we can just read stored info
509 // and write to the handlerInfo object we were passed.
511 handlerInfo.preferredAction = storedHandlerInfo.action;
512 handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
514 // If we've got a stub, ensure the defaults are still set:
515 lazy.externalProtocolService.setProtocolHandlerDefaults(
517 handlerInfo.hasDefaultHandler
520 handlerInfo.preferredAction == alwaysAsk &&
521 handlerInfo.alwaysAskBeforeHandling
523 // `store` will default to `useHelperApp` because `alwaysAsk` is
524 // not one of the 3 recognized options; for compatibility, do
526 handlerInfo.preferredAction = useHelperApp;
529 // If it *is* a stub, don't override alwaysAskBeforeHandling or the
530 // preferred actions. Instead, just append the stored handlers, without
531 // overriding the preferred app, and then schedule a task to store proper
532 // info for this handler.
533 this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
535 if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
536 for (let extension of storedHandlerInfo.extensions) {
537 handlerInfo.appendExtension(extension);
539 } else if (this._mockedHandler) {
540 this._insertMockedHandler(handlerInfo);
545 * Private method to inject stored handler information into an nsIHandlerInfo
547 * @param handlerInfo the nsIHandlerInfo instance to write to
548 * @param storedHandlers the stored handlers
549 * @param keepPreferredApp whether to keep the handlerInfo's
550 * preferredApplicationHandler or override it
551 * (default: false, ie override it)
553 _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
554 // If the first item is not null, it is also the preferred handler. Since
555 // we cannot modify the stored array, use a boolean to keep track of this.
556 let isFirstItem = true;
557 for (let handler of storedHandlers || [null]) {
558 let handlerApp = this.handlerAppFromSerializable(handler || {});
561 // Do not overwrite the preferred app unless that's allowed
562 if (!keepPreferredApp) {
563 handlerInfo.preferredApplicationHandler = handlerApp;
567 handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
574 * A nsIHandlerApp handler app
575 * @returns Serializable representation of a handler app object.
577 handlerAppToSerializable(handler) {
578 if (handler instanceof Ci.nsILocalHandlerApp) {
581 path: handler.executable.path,
583 } else if (handler instanceof Ci.nsIWebHandlerApp) {
586 uriTemplate: handler.uriTemplate,
588 } else if (handler instanceof Ci.nsIDBusHandlerApp) {
591 service: handler.service,
592 method: handler.method,
593 objectPath: handler.objectPath,
594 dBusInterface: handler.dBusInterface,
596 } else if (handler instanceof Ci.nsIGIOMimeApp) {
599 command: handler.command,
602 // If the handler is an unknown handler type, return null.
603 // Android default application handler is the case.
609 * Serializable representation of a handler object.
610 * @returns {nsIHandlerApp} the handler app, if any; otherwise null
612 handlerAppFromSerializable(handlerObj) {
614 if ("path" in handlerObj) {
616 let file = new lazy.FileUtils.File(handlerObj.path);
617 if (!file.exists()) {
621 "@mozilla.org/uriloader/local-handler-app;1"
622 ].createInstance(Ci.nsILocalHandlerApp);
623 handlerApp.executable = file;
627 } else if ("uriTemplate" in handlerObj) {
629 "@mozilla.org/uriloader/web-handler-app;1"
630 ].createInstance(Ci.nsIWebHandlerApp);
631 handlerApp.uriTemplate = handlerObj.uriTemplate;
632 } else if ("service" in handlerObj) {
634 "@mozilla.org/uriloader/dbus-handler-app;1"
635 ].createInstance(Ci.nsIDBusHandlerApp);
636 handlerApp.service = handlerObj.service;
637 handlerApp.method = handlerObj.method;
638 handlerApp.objectPath = handlerObj.objectPath;
639 handlerApp.dBusInterface = handlerObj.dBusInterface;
640 } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
642 handlerApp = Cc["@mozilla.org/gio-service;1"]
643 .getService(Ci.nsIGIOService)
644 .createAppFromCommand(handlerObj.command, handlerObj.name);
652 handlerApp.name = handlerObj.name;
657 * The function returns a reference to the "mimeTypes" or "schemes" object
658 * based on which type of handlerInfo is provided.
660 _getHandlerListByHandlerInfoType(handlerInfo) {
661 return this._isMIMEInfo(handlerInfo)
662 ? this._store.data.mimeTypes
663 : this._store.data.schemes;
667 * Determines whether an nsIHandlerInfo instance represents a MIME type.
669 _isMIMEInfo(handlerInfo) {
670 // We cannot rely only on the instanceof check because on Android both MIME
671 // types and protocols are instances of nsIMIMEInfo. We still do the check
672 // so that properties of nsIMIMEInfo become available to the callers.
674 handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
679 exists(handlerInfo) {
681 handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
686 remove(handlerInfo) {
687 delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
688 this._store.saveSoon();
692 getTypeFromExtension(fileExtension) {
693 let extension = fileExtension.toLowerCase();
694 let mimeTypes = this._store.data.mimeTypes;
695 for (let type of Object.keys(mimeTypes)) {
697 mimeTypes[type].extensions &&
698 mimeTypes[type].extensions.includes(extension)
706 _mockedHandler: null,
707 _mockedProtocol: null,
709 _insertMockedHandler(handlerInfo) {
710 if (handlerInfo.type == this._mockedProtocol) {
711 handlerInfo.preferredApplicationHandler = this._mockedHandler;
712 handlerInfo.possibleApplicationHandlers.insertElementAt(
719 // test-only: mock the handler instance for a particular protocol/scheme
720 mockProtocolHandler(protocol) {
722 this._mockedProtocol = null;
723 this._mockedHandler = null;
726 this._mockedProtocol = protocol;
727 this._mockedHandler = {
728 QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
730 Services.obs.notifyObservers(uri, "mocked-protocol-handler");
732 name: "Mocked handler",
733 detailedDescription: "Mocked handler for tests",
738 if (AppConstants.platform == "macosx") {
739 // We need an app path that isn't us, nor in our app bundle, and
740 // Apple no longer allows us to read the default-shipped apps
741 // in /Applications/ - except for Safari, it would appear!
742 let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
743 f.initWithPath("/Applications/Safari.app");
746 return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
749 clearParameters() {},
750 appendParameter() {},