Bug 1728955: part 8) Refactor `DisplayErrCode` in Windows' `nsClipboard`. r=masayuki
[gecko.git] / uriloader / exthandler / HandlerService.js
blob84bffc89e8209376bae2a99937ae570006c2b9e9
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"
7 );
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(
19   this,
20   "FileUtils",
21   "resource://gre/modules/FileUtils.jsm"
23 ChromeUtils.defineModuleGetter(
24   this,
25   "JSONFile",
26   "resource://gre/modules/JSONFile.jsm"
29 XPCOMUtils.defineLazyServiceGetter(
30   this,
31   "gExternalProtocolService",
32   "@mozilla.org/uriloader/external-protocol-service;1",
33   "nsIExternalProtocolService"
35 XPCOMUtils.defineLazyServiceGetter(
36   this,
37   "gMIMEService",
38   "@mozilla.org/mime;1",
39   "nsIMIMEService"
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",
51     "nsIHandlerService",
52     "nsIObserver",
53   ]),
55   __store: null,
56   get _store() {
57     if (!this.__store) {
58       this.__store = new JSONFile({
59         path: PathUtils.join(
60           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
61           "handlers.json"
62         ),
63         dataPostProcessor: this._dataPostProcessor.bind(this),
64       });
65     }
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();
70     return this.__store;
71   },
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");
83     }
84   },
86   _dataPostProcessor(data) {
87     return data.defaultHandlersVersion
88       ? data
89       : {
90           defaultHandlersVersion: {},
91           mimeTypes: {},
92           schemes: {},
93         };
94   },
96   /**
97    * Injects new default protocol handlers if the version in the preferences is
98    * newer than the one in the data store.
99    */
100   _injectDefaultProtocolHandlersIfNeeded() {
101     let prefsDefaultHandlersVersion;
102     try {
103       prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
104         "gecko.handlerService.defaultHandlersVersion",
105         Ci.nsIPrefLocalizedString
106       );
107     } catch (ex) {
108       if (
109         ex instanceof Components.Exception &&
110         ex.result == Cr.NS_ERROR_UNEXPECTED
111       ) {
112         // This platform does not have any default protocol handlers configured.
113         return;
114       }
115       throw ex;
116     }
118     try {
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[
127           locale
128         ] = prefsDefaultHandlersVersion;
129         // Now save the result:
130         this._store.saveSoon();
131       }
132     } catch (ex) {
133       Cu.reportError(ex);
134     }
135   },
137   _injectDefaultProtocolHandlers() {
138     let schemesPrefBranch = Services.prefs.getBranch(
139       "gecko.handlerService.schemes."
140     );
141     let schemePrefList = schemesPrefBranch.getChildList("");
143     let schemes = {};
145     // read all the scheme prefs into a hash
146     for (let schemePrefName of schemePrefList) {
147       let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
149       try {
150         let attrData = schemesPrefBranch.getComplexValue(
151           schemePrefName,
152           Ci.nsIPrefLocalizedString
153         ).data;
154         if (!(scheme in schemes)) {
155           schemes[scheme] = {};
156         }
158         if (!(handlerNumber in schemes[scheme])) {
159           schemes[scheme][handlerNumber] = {};
160         }
162         schemes[scheme][handlerNumber][attribute] = attrData;
163       } catch (ex) {}
164     }
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) {
173         if (
174           !obj.uriTemplate ||
175           !obj.uriTemplate.startsWith("https://") ||
176           !obj.uriTemplate.toLowerCase().includes("%s")
177         ) {
178           delete handlerObject[key];
179         } else {
180           validHandlers++;
181         }
182       }
183       if (!validHandlers) {
184         delete schemes[scheme];
185       }
186     }
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.
205           stubEntry: true,
206           // The first item in the list is the preferred handler, and
207           // there isn't one, so we fill in null:
208           handlers: [null],
209         };
210         this._store.data.schemes[scheme] = existingSchemeInfo;
211       }
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);
221         }
222       }
223     }
224   },
226   /**
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.
229    *
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.
235    */
236   _migrateProtocolHandlersIfNeeded() {
237     const kMigrations = {
238       "30boxes": () => {
239         const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
240         let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
241           "webcal"
242         );
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);
250             if (
251               app instanceof Ci.nsIWebHandlerApp &&
252               k30BoxesRegex.test(app.uriTemplate)
253             ) {
254               shouldStore = true;
255               handlers.removeElementAt(i);
256             }
257           }
258           // Then remove as a preferred handler.
259           if (webcalHandler.preferredApplicationHandler) {
260             let app = webcalHandler.preferredApplicationHandler;
261             if (
262               app instanceof Ci.nsIWebHandlerApp &&
263               k30BoxesRegex.test(app.uriTemplate)
264             ) {
265               webcalHandler.preferredApplicationHandler = null;
266               shouldStore = true;
267             }
268           }
269           // Then store, if we changed anything.
270           if (shouldStore) {
271             this.store(webcalHandler);
272           }
273         }
274       },
275       // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
276       "secure-mail": () => {
277         const kSubstitutions = new Map([
278           [
279             "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
280             "https://mail.yahoo.co.jp/compose/?To=%s",
281           ],
282           [
283             "http://www.inbox.lv/rfc2368/?value=%s",
284             "https://mail.inbox.lv/compose?to=%s",
285           ],
286           [
287             "http://poczta.interia.pl/mh/?mailto=%s",
288             "https://poczta.interia.pl/mh/?mailto=%s",
289           ],
290           [
291             "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
292             "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
293           ],
294         ]);
296         function maybeReplaceURL(app) {
297           if (app instanceof Ci.nsIWebHandlerApp) {
298             let { uriTemplate } = app;
299             let sub = kSubstitutions.get(uriTemplate);
300             if (sub) {
301               app.uriTemplate = sub;
302               return true;
303             }
304           }
305           return false;
306         }
307         let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
308           "mailto"
309         );
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
317             // logical or.
318             shouldStore |= maybeReplaceURL(app);
319           }
320           // Then check the preferred handler.
321           if (mailHandler.preferredApplicationHandler) {
322             shouldStore |= maybeReplaceURL(
323               mailHandler.preferredApplicationHandler
324             );
325           }
326           // Then store, if we changed anything. Note that store() handles
327           // duplicates, so we don't have to.
328           if (shouldStore) {
329             this.store(mailHandler);
330           }
331         }
332       },
333     };
334     let migrationsToRun = Services.prefs.getCharPref(
335       "browser.handlers.migrations",
336       ""
337     );
338     migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
339     for (let migration of migrationsToRun) {
340       migration.trim();
341       try {
342         kMigrations[migration]();
343       } catch (ex) {
344         Cu.reportError(ex);
345       }
346     }
348     if (migrationsToRun.length) {
349       Services.prefs.clearUserPref("browser.handlers.migrations");
350     }
351   },
353   _onDBChange() {
354     return (async () => {
355       if (this.__store) {
356         await this.__store.finalize();
357       }
358       this.__store = null;
359       this.__storeInitialized = false;
360     })().catch(Cu.reportError);
361   },
363   // nsIObserver
364   observe(subject, topic, data) {
365     if (topic != "handlersvc-json-replace") {
366       return;
367     }
368     let promise = this._onDBChange();
369     promise.then(() => {
370       Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
371     });
372   },
374   // nsIHandlerService
375   asyncInit() {
376     if (!this.__store) {
377       this.__store = new JSONFile({
378         path: PathUtils.join(
379           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
380           "handlers.json"
381         ),
382         dataPostProcessor: this._dataPostProcessor.bind(this),
383       });
384       this.__store
385         .load()
386         .then(() => {
387           // __store can be null if we called _onDBChange in the mean time.
388           if (this.__store) {
389             this._ensureStoreInitialized();
390           }
391         })
392         .catch(Cu.reportError);
393     }
394   },
396   // nsIHandlerService
397   enumerate() {
398     let handlers = Cc["@mozilla.org/array;1"].createInstance(
399       Ci.nsIMutableArray
400     );
401     for (let type of Object.keys(this._store.data.mimeTypes)) {
402       let handler = gMIMEService.getFromTypeAndExtension(type, null);
403       handlers.appendElement(handler);
404     }
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.
409       //
410       // Note: our caller still needs to yield periodically when iterating
411       // the enumerator and accessing handler properties to avoid monopolizing
412       // the main thread.
413       //
414       let handler = new Proxy(
415         {
416           QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
417           type,
418           get _handlerInfo() {
419             delete this._handlerInfo;
420             return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
421               type
422             ));
423           },
424         },
425         {
426           get(target, name) {
427             return target[name] || target._handlerInfo[name];
428           },
429           set(target, name, value) {
430             target._handlerInfo[name] = value;
431           },
432         }
433       );
434       handlers.appendElement(handler);
435     }
436     return handlers.enumerate(Ci.nsIHandlerInfo);
437   },
439   // nsIHandlerService
440   store(handlerInfo) {
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;
449     }
451     // Only a limited number of preferredAction values is allowed.
452     if (
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"
459         ))
460     ) {
461       storedHandlerInfo.action = handlerInfo.preferredAction;
462     } else {
463       storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp;
464     }
466     if (handlerInfo.alwaysAskBeforeHandling) {
467       storedHandlerInfo.ask = true;
468     } else {
469       delete storedHandlerInfo.ask;
470     }
472     // Build a list of unique nsIHandlerInfo instances to process later.
473     let handlers = [];
474     if (handlerInfo.preferredApplicationHandler) {
475       handlers.push(handlerInfo.preferredApplicationHandler);
476     }
477     for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
478       Ci.nsIHandlerApp
479     )) {
480       // If the caller stored duplicate handlers, we save them only once.
481       if (!handlers.some(h => h.equals(handler))) {
482         handlers.push(handler);
483       }
484     }
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))
491       .filter(h => h);
492     if (serializableHandlers.length) {
493       if (!handlerInfo.preferredApplicationHandler) {
494         serializableHandlers.unshift(null);
495       }
496       storedHandlerInfo.handlers = serializableHandlers;
497     } else {
498       delete storedHandlerInfo.handlers;
499     }
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);
508         }
509       }
510       if (extensions.length) {
511         storedHandlerInfo.extensions = extensions;
512       } else {
513         delete storedHandlerInfo.extensions;
514       }
515     }
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);
526     }
527   },
529   // nsIHandlerService
530   fillHandlerInfo(handlerInfo, overrideType) {
531     let type = overrideType || handlerInfo.type;
532     let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
533       type
534     ];
535     if (!storedHandlerInfo) {
536       throw new Components.Exception(
537         "handlerSvc fillHandlerInfo: don't know this type",
538         Cr.NS_ERROR_NOT_AVAILABLE
539       );
540     }
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.
545     if (!isStub) {
546       handlerInfo.preferredAction = storedHandlerInfo.action;
547       handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
548     } else {
549       // If we've got a stub, ensure the defaults are still set:
550       gExternalProtocolService.setProtocolHandlerDefaults(
551         handlerInfo,
552         handlerInfo.hasDefaultHandler
553       );
554       if (
555         handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
556         handlerInfo.alwaysAskBeforeHandling
557       ) {
558         // `store` will default to `useHelperApp` because `alwaysAsk` is
559         // not one of the 3 recognized options; for compatibility, do
560         // the same here.
561         handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
562       }
563     }
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);
573       }
574     } else if (this._mockedHandler) {
575       this._insertMockedHandler(handlerInfo);
576     }
577   },
579   /**
580    * Private method to inject stored handler information into an nsIHandlerInfo
581    * instance.
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)
587    */
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 || {});
594       if (isFirstItem) {
595         isFirstItem = false;
596         // Do not overwrite the preferred app unless that's allowed
597         if (!keepPreferredApp) {
598           handlerInfo.preferredApplicationHandler = handlerApp;
599         }
600       }
601       if (handlerApp) {
602         handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
603       }
604     }
605   },
607   /**
608    * @param handler
609    *        A nsIHandlerApp handler app
610    * @returns  Serializable representation of a handler app object.
611    */
612   handlerAppToSerializable(handler) {
613     if (handler instanceof Ci.nsILocalHandlerApp) {
614       return {
615         name: handler.name,
616         path: handler.executable.path,
617       };
618     } else if (handler instanceof Ci.nsIWebHandlerApp) {
619       return {
620         name: handler.name,
621         uriTemplate: handler.uriTemplate,
622       };
623     } else if (handler instanceof Ci.nsIDBusHandlerApp) {
624       return {
625         name: handler.name,
626         service: handler.service,
627         method: handler.method,
628         objectPath: handler.objectPath,
629         dBusInterface: handler.dBusInterface,
630       };
631     } else if (handler instanceof Ci.nsIGIOMimeApp) {
632       return {
633         name: handler.name,
634         command: handler.command,
635       };
636     }
637     // If the handler is an unknown handler type, return null.
638     // Android default application handler is the case.
639     return null;
640   },
642   /**
643    * @param handlerObj
644    *        Serializable representation of a handler object.
645    * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
646    */
647   handlerAppFromSerializable(handlerObj) {
648     let handlerApp;
649     if ("path" in handlerObj) {
650       try {
651         let file = new FileUtils.File(handlerObj.path);
652         if (!file.exists()) {
653           return null;
654         }
655         handlerApp = Cc[
656           "@mozilla.org/uriloader/local-handler-app;1"
657         ].createInstance(Ci.nsILocalHandlerApp);
658         handlerApp.executable = file;
659       } catch (ex) {
660         return null;
661       }
662     } else if ("uriTemplate" in handlerObj) {
663       handlerApp = Cc[
664         "@mozilla.org/uriloader/web-handler-app;1"
665       ].createInstance(Ci.nsIWebHandlerApp);
666       handlerApp.uriTemplate = handlerObj.uriTemplate;
667     } else if ("service" in handlerObj) {
668       handlerApp = Cc[
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) {
676       try {
677         handlerApp = Cc["@mozilla.org/gio-service;1"]
678           .getService(Ci.nsIGIOService)
679           .createAppFromCommand(handlerObj.command, handlerObj.name);
680       } catch (ex) {
681         return null;
682       }
683     } else {
684       return null;
685     }
687     handlerApp.name = handlerObj.name;
688     return handlerApp;
689   },
691   /**
692    * The function returns a reference to the "mimeTypes" or "schemes" object
693    * based on which type of handlerInfo is provided.
694    */
695   _getHandlerListByHandlerInfoType(handlerInfo) {
696     return this._isMIMEInfo(handlerInfo)
697       ? this._store.data.mimeTypes
698       : this._store.data.schemes;
699   },
701   /**
702    * Determines whether an nsIHandlerInfo instance represents a MIME type.
703    */
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.
708     return (
709       handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
710     );
711   },
713   // nsIHandlerService
714   exists(handlerInfo) {
715     return (
716       handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
717     );
718   },
720   // nsIHandlerService
721   remove(handlerInfo) {
722     delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
723     this._store.saveSoon();
724   },
726   // nsIHandlerService
727   getTypeFromExtension(fileExtension) {
728     let extension = fileExtension.toLowerCase();
729     let mimeTypes = this._store.data.mimeTypes;
730     for (let type of Object.keys(mimeTypes)) {
731       if (
732         mimeTypes[type].extensions &&
733         mimeTypes[type].extensions.includes(extension)
734       ) {
735         return type;
736       }
737     }
738     return "";
739   },
741   _mockedHandler: null,
742   _mockedProtocol: null,
744   _insertMockedHandler(handlerInfo) {
745     if (handlerInfo.type == this._mockedProtocol) {
746       handlerInfo.preferredApplicationHandler = this._mockedHandler;
747       handlerInfo.possibleApplicationHandlers.insertElementAt(
748         this._mockedHandler,
749         0
750       );
751     }
752   },
754   // test-only: mock the handler instance for a particular protocol/scheme
755   mockProtocolHandler(protocol) {
756     if (!protocol) {
757       this._mockedProtocol = null;
758       this._mockedHandler = null;
759       return;
760     }
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");
766       },
767       name: "Mocked handler",
768       detailedDescription: "Mocked handler for tests",
769       equals(x) {
770         return x == this;
771       },
772       get executable() {
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");
779           return f;
780         }
781         return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
782       },
783       parameterCount: 0,
784       clearParameters() {},
785       appendParameter() {},
786       getParameter() {},
787       parameterExists() {
788         return false;
789       },
790     };
791   },
794 this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);