no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / uriloader / exthandler / ExtHandlerService.sys.mjs
blobd7f3c9b9e0460afb15947dc03d84eea62658e4df
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";
8 const {
9   saveToDisk,
10   alwaysAsk,
11   useHelperApp,
12   handleInternally,
13   useSystemDefault,
14 } = Ci.nsIHandlerInfo;
16 const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
18 const lazy = {};
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",
25 });
27 XPCOMUtils.defineLazyServiceGetter(
28   lazy,
29   "externalProtocolService",
30   "@mozilla.org/uriloader/external-protocol-service;1",
31   "nsIExternalProtocolService"
33 XPCOMUtils.defineLazyServiceGetter(
34   lazy,
35   "MIMEService",
36   "@mozilla.org/mime;1",
37   "nsIMIMEService"
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",
48     "nsIHandlerService",
49     "nsIObserver",
50   ]),
52   __store: null,
53   get _store() {
54     if (!this.__store) {
55       this.__store = new lazy.JSONFile({
56         path: PathUtils.join(
57           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
58           "handlers.json"
59         ),
60         dataPostProcessor: this._dataPostProcessor.bind(this),
61       });
62     }
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();
67     return this.__store;
68   },
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");
80     }
81   },
83   _dataPostProcessor(data) {
84     return data.defaultHandlersVersion
85       ? data
86       : {
87           defaultHandlersVersion: {},
88           mimeTypes: {},
89           schemes: {},
90           isDownloadsImprovementsAlreadyMigrated: false,
91         };
92   },
94   /**
95    * Injects new default protocol handlers if the version in the preferences is
96    * newer than the one in the data store.
97    */
98   _injectDefaultProtocolHandlersIfNeeded() {
99     try {
100       let defaultHandlersVersion = Services.prefs.getIntPref(
101         "gecko.handlerService.defaultHandlersVersion",
102         0
103       );
104       if (defaultHandlersVersion < lazy.kHandlerListVersion) {
105         this._injectDefaultProtocolHandlers();
106         Services.prefs.setIntPref(
107           "gecko.handlerService.defaultHandlersVersion",
108           lazy.kHandlerListVersion
109         );
110         // Now save the result:
111         this._store.saveSoon();
112       }
113     } catch (ex) {
114       console.error(ex);
115     }
116   },
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];
127       }
128     }
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.
142         continue;
143       }
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.
152           stubEntry: true,
153           // The first item in the list is the preferred handler, and
154           // there isn't one, so we fill in null:
155           handlers: [null],
156         };
157         this._store.data.schemes[scheme] = existingSchemeInfo;
158       }
159       let { handlers } = existingSchemeInfo;
160       for (let newHandler of localeHandlers.schemes[scheme].handlers) {
161         if (!newHandler.uriTemplate) {
162           console.error(
163             `Ignoring protocol handler for ${scheme} without a uriTemplate!`
164           );
165           continue;
166         }
167         if (!newHandler.uriTemplate.startsWith("https://")) {
168           console.error(
169             `Ignoring protocol handler for ${scheme} with insecure template URL ${newHandler.uriTemplate}.`
170           );
171           continue;
172         }
173         if (!newHandler.uriTemplate.toLowerCase().includes("%s")) {
174           console.error(
175             `Ignoring protocol handler for ${scheme} with invalid template URL ${newHandler.uriTemplate}.`
176           );
177           continue;
178         }
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);
185         }
186       }
187     }
188   },
190   /**
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.
193    *
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.
199    */
200   _migrateProtocolHandlersIfNeeded() {
201     const kMigrations = {
202       "30boxes": () => {
203         const k30BoxesRegex =
204           /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
205         let webcalHandler =
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);
214             if (
215               app instanceof Ci.nsIWebHandlerApp &&
216               k30BoxesRegex.test(app.uriTemplate)
217             ) {
218               shouldStore = true;
219               handlers.removeElementAt(i);
220             }
221           }
222           // Then remove as a preferred handler.
223           if (webcalHandler.preferredApplicationHandler) {
224             let app = webcalHandler.preferredApplicationHandler;
225             if (
226               app instanceof Ci.nsIWebHandlerApp &&
227               k30BoxesRegex.test(app.uriTemplate)
228             ) {
229               webcalHandler.preferredApplicationHandler = null;
230               shouldStore = true;
231             }
232           }
233           // Then store, if we changed anything.
234           if (shouldStore) {
235             this.store(webcalHandler);
236           }
237         }
238       },
239       // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
240       "secure-mail": () => {
241         const kSubstitutions = new Map([
242           [
243             "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
244             "https://mail.yahoo.co.jp/compose/?To=%s",
245           ],
246           [
247             "http://www.inbox.lv/rfc2368/?value=%s",
248             "https://mail.inbox.lv/compose?to=%s",
249           ],
250           [
251             "http://poczta.interia.pl/mh/?mailto=%s",
252             "https://poczta.interia.pl/mh/?mailto=%s",
253           ],
254           [
255             "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
256             "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
257           ],
258         ]);
260         function maybeReplaceURL(app) {
261           if (app instanceof Ci.nsIWebHandlerApp) {
262             let { uriTemplate } = app;
263             let sub = kSubstitutions.get(uriTemplate);
264             if (sub) {
265               app.uriTemplate = sub;
266               return true;
267             }
268           }
269           return false;
270         }
271         let mailHandler =
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
280             // logical or.
281             shouldStore |= maybeReplaceURL(app);
282           }
283           // Then check the preferred handler.
284           if (mailHandler.preferredApplicationHandler) {
285             shouldStore |= maybeReplaceURL(
286               mailHandler.preferredApplicationHandler
287             );
288           }
289           // Then store, if we changed anything. Note that store() handles
290           // duplicates, so we don't have to.
291           if (shouldStore) {
292             this.store(mailHandler);
293           }
294         }
295       },
296     };
297     let migrationsToRun = Services.prefs.getCharPref(
298       "browser.handlers.migrations",
299       ""
300     );
301     migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
302     for (let migration of migrationsToRun) {
303       migration.trim();
304       try {
305         kMigrations[migration]();
306       } catch (ex) {
307         console.error(ex);
308       }
309     }
311     if (migrationsToRun.length) {
312       Services.prefs.clearUserPref("browser.handlers.migrations");
313     }
314   },
316   _onDBChange() {
317     return (async () => {
318       if (this.__store) {
319         await this.__store.finalize();
320       }
321       this.__store = null;
322       this.__storeInitialized = false;
323     })().catch(console.error);
324   },
326   // nsIObserver
327   observe(subject, topic) {
328     if (topic != "handlersvc-json-replace") {
329       return;
330     }
331     let promise = this._onDBChange();
332     promise.then(() => {
333       Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
334     });
335   },
337   // nsIHandlerService
338   asyncInit() {
339     if (!this.__store) {
340       this.__store = new lazy.JSONFile({
341         path: PathUtils.join(
342           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
343           "handlers.json"
344         ),
345         dataPostProcessor: this._dataPostProcessor.bind(this),
346       });
347       this.__store
348         .load()
349         .then(() => {
350           // __store can be null if we called _onDBChange in the mean time.
351           if (this.__store) {
352             this._ensureStoreInitialized();
353           }
354         })
355         .catch(console.error);
356     }
357   },
359   // nsIHandlerService
360   enumerate() {
361     let handlers = Cc["@mozilla.org/array;1"].createInstance(
362       Ci.nsIMutableArray
363     );
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(
367         type,
368         primaryExtension
369       );
370       handlers.appendElement(handler);
371     }
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.
376       //
377       // Note: our caller still needs to yield periodically when iterating
378       // the enumerator and accessing handler properties to avoid monopolizing
379       // the main thread.
380       //
381       let handler = new Proxy(
382         {
383           QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
384           type,
385           get _handlerInfo() {
386             delete this._handlerInfo;
387             return (this._handlerInfo =
388               lazy.externalProtocolService.getProtocolHandlerInfo(type));
389           },
390         },
391         {
392           get(target, name) {
393             return target[name] || target._handlerInfo[name];
394           },
395           set(target, name, value) {
396             target._handlerInfo[name] = value;
397           },
398         }
399       );
400       handlers.appendElement(handler);
401     }
402     return handlers.enumerate(Ci.nsIHandlerInfo);
403   },
405   // nsIHandlerService
406   store(handlerInfo) {
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;
415     }
417     // Only a limited number of preferredAction values is allowed.
418     if (
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))
426     ) {
427       storedHandlerInfo.action = handlerInfo.preferredAction;
428     } else {
429       storedHandlerInfo.action = useHelperApp;
430     }
432     if (handlerInfo.alwaysAskBeforeHandling) {
433       storedHandlerInfo.ask = true;
434     } else {
435       delete storedHandlerInfo.ask;
436     }
438     // Build a list of unique nsIHandlerInfo instances to process later.
439     let handlers = [];
440     if (handlerInfo.preferredApplicationHandler) {
441       handlers.push(handlerInfo.preferredApplicationHandler);
442     }
443     for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
444       Ci.nsIHandlerApp
445     )) {
446       // If the caller stored duplicate handlers, we save them only once.
447       if (!handlers.some(h => h.equals(handler))) {
448         handlers.push(handler);
449       }
450     }
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))
457       .filter(h => h);
458     if (serializableHandlers.length) {
459       if (!handlerInfo.preferredApplicationHandler) {
460         serializableHandlers.unshift(null);
461       }
462       storedHandlerInfo.handlers = serializableHandlers;
463     } else {
464       delete storedHandlerInfo.handlers;
465     }
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);
474         }
475       }
476       if (extensions.length) {
477         storedHandlerInfo.extensions = extensions;
478       } else {
479         delete storedHandlerInfo.extensions;
480       }
481     }
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);
492     }
493   },
495   // nsIHandlerService
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
504       );
505     }
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.
510     if (!isStub) {
511       handlerInfo.preferredAction = storedHandlerInfo.action;
512       handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
513     } else {
514       // If we've got a stub, ensure the defaults are still set:
515       lazy.externalProtocolService.setProtocolHandlerDefaults(
516         handlerInfo,
517         handlerInfo.hasDefaultHandler
518       );
519       if (
520         handlerInfo.preferredAction == alwaysAsk &&
521         handlerInfo.alwaysAskBeforeHandling
522       ) {
523         // `store` will default to `useHelperApp` because `alwaysAsk` is
524         // not one of the 3 recognized options; for compatibility, do
525         // the same here.
526         handlerInfo.preferredAction = useHelperApp;
527       }
528     }
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);
538       }
539     } else if (this._mockedHandler) {
540       this._insertMockedHandler(handlerInfo);
541     }
542   },
544   /**
545    * Private method to inject stored handler information into an nsIHandlerInfo
546    * instance.
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)
552    */
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 || {});
559       if (isFirstItem) {
560         isFirstItem = false;
561         // Do not overwrite the preferred app unless that's allowed
562         if (!keepPreferredApp) {
563           handlerInfo.preferredApplicationHandler = handlerApp;
564         }
565       }
566       if (handlerApp) {
567         handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
568       }
569     }
570   },
572   /**
573    * @param handler
574    *        A nsIHandlerApp handler app
575    * @returns  Serializable representation of a handler app object.
576    */
577   handlerAppToSerializable(handler) {
578     if (handler instanceof Ci.nsILocalHandlerApp) {
579       return {
580         name: handler.name,
581         path: handler.executable.path,
582       };
583     } else if (handler instanceof Ci.nsIWebHandlerApp) {
584       return {
585         name: handler.name,
586         uriTemplate: handler.uriTemplate,
587       };
588     } else if (handler instanceof Ci.nsIDBusHandlerApp) {
589       return {
590         name: handler.name,
591         service: handler.service,
592         method: handler.method,
593         objectPath: handler.objectPath,
594         dBusInterface: handler.dBusInterface,
595       };
596     } else if (handler instanceof Ci.nsIGIOMimeApp) {
597       return {
598         name: handler.name,
599         command: handler.command,
600       };
601     }
602     // If the handler is an unknown handler type, return null.
603     // Android default application handler is the case.
604     return null;
605   },
607   /**
608    * @param handlerObj
609    *        Serializable representation of a handler object.
610    * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
611    */
612   handlerAppFromSerializable(handlerObj) {
613     let handlerApp;
614     if ("path" in handlerObj) {
615       try {
616         let file = new lazy.FileUtils.File(handlerObj.path);
617         if (!file.exists()) {
618           return null;
619         }
620         handlerApp = Cc[
621           "@mozilla.org/uriloader/local-handler-app;1"
622         ].createInstance(Ci.nsILocalHandlerApp);
623         handlerApp.executable = file;
624       } catch (ex) {
625         return null;
626       }
627     } else if ("uriTemplate" in handlerObj) {
628       handlerApp = Cc[
629         "@mozilla.org/uriloader/web-handler-app;1"
630       ].createInstance(Ci.nsIWebHandlerApp);
631       handlerApp.uriTemplate = handlerObj.uriTemplate;
632     } else if ("service" in handlerObj) {
633       handlerApp = Cc[
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) {
641       try {
642         handlerApp = Cc["@mozilla.org/gio-service;1"]
643           .getService(Ci.nsIGIOService)
644           .createAppFromCommand(handlerObj.command, handlerObj.name);
645       } catch (ex) {
646         return null;
647       }
648     } else {
649       return null;
650     }
652     handlerApp.name = handlerObj.name;
653     return handlerApp;
654   },
656   /**
657    * The function returns a reference to the "mimeTypes" or "schemes" object
658    * based on which type of handlerInfo is provided.
659    */
660   _getHandlerListByHandlerInfoType(handlerInfo) {
661     return this._isMIMEInfo(handlerInfo)
662       ? this._store.data.mimeTypes
663       : this._store.data.schemes;
664   },
666   /**
667    * Determines whether an nsIHandlerInfo instance represents a MIME type.
668    */
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.
673     return (
674       handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
675     );
676   },
678   // nsIHandlerService
679   exists(handlerInfo) {
680     return (
681       handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
682     );
683   },
685   // nsIHandlerService
686   remove(handlerInfo) {
687     delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
688     this._store.saveSoon();
689   },
691   // nsIHandlerService
692   getTypeFromExtension(fileExtension) {
693     let extension = fileExtension.toLowerCase();
694     let mimeTypes = this._store.data.mimeTypes;
695     for (let type of Object.keys(mimeTypes)) {
696       if (
697         mimeTypes[type].extensions &&
698         mimeTypes[type].extensions.includes(extension)
699       ) {
700         return type;
701       }
702     }
703     return "";
704   },
706   _mockedHandler: null,
707   _mockedProtocol: null,
709   _insertMockedHandler(handlerInfo) {
710     if (handlerInfo.type == this._mockedProtocol) {
711       handlerInfo.preferredApplicationHandler = this._mockedHandler;
712       handlerInfo.possibleApplicationHandlers.insertElementAt(
713         this._mockedHandler,
714         0
715       );
716     }
717   },
719   // test-only: mock the handler instance for a particular protocol/scheme
720   mockProtocolHandler(protocol) {
721     if (!protocol) {
722       this._mockedProtocol = null;
723       this._mockedHandler = null;
724       return;
725     }
726     this._mockedProtocol = protocol;
727     this._mockedHandler = {
728       QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
729       launchWithURI(uri) {
730         Services.obs.notifyObservers(uri, "mocked-protocol-handler");
731       },
732       name: "Mocked handler",
733       detailedDescription: "Mocked handler for tests",
734       equals(x) {
735         return x == this;
736       },
737       get executable() {
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");
744           return f;
745         }
746         return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
747       },
748       parameterCount: 0,
749       clearParameters() {},
750       appendParameter() {},
751       getParameter() {},
752       parameterExists() {
753         return false;
754       },
755     };
756   },