Bug 1687263: part 4) Defer and in some cases avoid removing spellchecking-ranges...
[gecko.git] / uriloader / exthandler / HandlerService.js
blobfba269fda48611828db0bb6d66fdf528f5867c33
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 { XPCOMUtils } = ChromeUtils.import(
9   "resource://gre/modules/XPCOMUtils.jsm"
11 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
13 const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
15 ChromeUtils.defineModuleGetter(
16   this,
17   "FileUtils",
18   "resource://gre/modules/FileUtils.jsm"
20 ChromeUtils.defineModuleGetter(
21   this,
22   "JSONFile",
23   "resource://gre/modules/JSONFile.jsm"
26 XPCOMUtils.defineLazyServiceGetter(
27   this,
28   "gExternalProtocolService",
29   "@mozilla.org/uriloader/external-protocol-service;1",
30   "nsIExternalProtocolService"
32 XPCOMUtils.defineLazyServiceGetter(
33   this,
34   "gMIMEService",
35   "@mozilla.org/mime;1",
36   "nsIMIMEService"
39 function HandlerService() {
40   // Observe handlersvc-json-replace so we can switch to the datasource
41   Services.obs.addObserver(this, "handlersvc-json-replace", true);
44 HandlerService.prototype = {
45   classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"),
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 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         };
91   },
93   /**
94    * Injects new default protocol handlers if the version in the preferences is
95    * newer than the one in the data store.
96    */
97   _injectDefaultProtocolHandlersIfNeeded() {
98     let prefsDefaultHandlersVersion;
99     try {
100       prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
101         "gecko.handlerService.defaultHandlersVersion",
102         Ci.nsIPrefLocalizedString
103       );
104     } catch (ex) {
105       if (
106         ex instanceof Components.Exception &&
107         ex.result == Cr.NS_ERROR_UNEXPECTED
108       ) {
109         // This platform does not have any default protocol handlers configured.
110         return;
111       }
112       throw ex;
113     }
115     try {
116       prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data);
117       let locale = Services.locale.appLocaleAsBCP47;
119       let defaultHandlersVersion =
120         this._store.data.defaultHandlersVersion[locale] || 0;
121       if (defaultHandlersVersion < prefsDefaultHandlersVersion) {
122         this._injectDefaultProtocolHandlers();
123         this._store.data.defaultHandlersVersion[
124           locale
125         ] = prefsDefaultHandlersVersion;
126         // Now save the result:
127         this._store.saveSoon();
128       }
129     } catch (ex) {
130       Cu.reportError(ex);
131     }
132   },
134   _injectDefaultProtocolHandlers() {
135     let schemesPrefBranch = Services.prefs.getBranch(
136       "gecko.handlerService.schemes."
137     );
138     let schemePrefList = schemesPrefBranch.getChildList("");
140     let schemes = {};
142     // read all the scheme prefs into a hash
143     for (let schemePrefName of schemePrefList) {
144       let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
146       try {
147         let attrData = schemesPrefBranch.getComplexValue(
148           schemePrefName,
149           Ci.nsIPrefLocalizedString
150         ).data;
151         if (!(scheme in schemes)) {
152           schemes[scheme] = {};
153         }
155         if (!(handlerNumber in schemes[scheme])) {
156           schemes[scheme][handlerNumber] = {};
157         }
159         schemes[scheme][handlerNumber][attribute] = attrData;
160       } catch (ex) {}
161     }
163     // Now drop any entries without a uriTemplate, or with a broken one.
164     // The Array.from calls ensure we can safely delete things without
165     // affecting the iterator.
166     for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) {
167       let handlers = Array.from(Object.entries(handlerObject));
168       let validHandlers = 0;
169       for (let [key, obj] of handlers) {
170         if (
171           !obj.uriTemplate ||
172           !obj.uriTemplate.startsWith("https://") ||
173           !obj.uriTemplate.toLowerCase().includes("%s")
174         ) {
175           delete handlerObject[key];
176         } else {
177           validHandlers++;
178         }
179       }
180       if (!validHandlers) {
181         delete schemes[scheme];
182       }
183     }
185     // Now, we're going to cheat. Terribly. The idiologically correct way
186     // of implementing the following bit of code would be to fetch the
187     // handler info objects from the protocol service, manipulate those,
188     // and then store each of them.
189     // However, that's expensive. It causes us to talk to the OS about
190     // default apps, which causes the OS to go hit the disk.
191     // All we're trying to do is insert some web apps into the list. We
192     // don't care what's already in the file, we just want to do the
193     // equivalent of appending into the database. So let's just go do that:
194     for (let scheme of Object.keys(schemes)) {
195       let existingSchemeInfo = this._store.data.schemes[scheme];
196       if (!existingSchemeInfo) {
197         // Haven't seen this scheme before. Default to asking which app the
198         // user wants to use:
199         existingSchemeInfo = {
200           // Signal to future readers that we didn't ask the OS anything.
201           // When the entry is first used, get the info from the OS.
202           stubEntry: true,
203           // The first item in the list is the preferred handler, and
204           // there isn't one, so we fill in null:
205           handlers: [null],
206         };
207         this._store.data.schemes[scheme] = existingSchemeInfo;
208       }
209       let { handlers } = existingSchemeInfo;
210       for (let handlerNumber of Object.keys(schemes[scheme])) {
211         let newHandler = schemes[scheme][handlerNumber];
212         // If there is already a handler registered with the same template
213         // URL, ignore the new one:
214         let matchingTemplate = handler =>
215           handler && handler.uriTemplate == newHandler.uriTemplate;
216         if (!handlers.some(matchingTemplate)) {
217           handlers.push(newHandler);
218         }
219       }
220     }
221   },
223   /**
224    * Execute any migrations. Migrations are defined here for any changes or removals for
225    * existing handlers. Additions are still handled via the localized prefs infrastructure.
226    *
227    * This depends on the browser.handlers.migrations pref being set by migrateUI in
228    * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
229    * This is a comma-separated list of identifiers of migrations that need running.
230    * This avoids both re-running older migrations and keeping an additional
231    * pref around permanently.
232    */
233   _migrateProtocolHandlersIfNeeded() {
234     const kMigrations = {
235       "30boxes": () => {
236         const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
237         let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
238           "webcal"
239         );
240         if (this.exists(webcalHandler)) {
241           this.fillHandlerInfo(webcalHandler, "");
242           let shouldStore = false;
243           // First remove 30boxes from possible handlers.
244           let handlers = webcalHandler.possibleApplicationHandlers;
245           for (let i = handlers.length - 1; i >= 0; i--) {
246             let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
247             if (
248               app instanceof Ci.nsIWebHandlerApp &&
249               k30BoxesRegex.test(app.uriTemplate)
250             ) {
251               shouldStore = true;
252               handlers.removeElementAt(i);
253             }
254           }
255           // Then remove as a preferred handler.
256           if (webcalHandler.preferredApplicationHandler) {
257             let app = webcalHandler.preferredApplicationHandler;
258             if (
259               app instanceof Ci.nsIWebHandlerApp &&
260               k30BoxesRegex.test(app.uriTemplate)
261             ) {
262               webcalHandler.preferredApplicationHandler = null;
263               shouldStore = true;
264             }
265           }
266           // Then store, if we changed anything.
267           if (shouldStore) {
268             this.store(webcalHandler);
269           }
270         }
271       },
272       // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
273       "secure-mail": () => {
274         const kSubstitutions = new Map([
275           [
276             "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
277             "https://mail.yahoo.co.jp/compose/?To=%s",
278           ],
279           [
280             "http://www.inbox.lv/rfc2368/?value=%s",
281             "https://mail.inbox.lv/compose?to=%s",
282           ],
283           [
284             "http://poczta.interia.pl/mh/?mailto=%s",
285             "https://poczta.interia.pl/mh/?mailto=%s",
286           ],
287           [
288             "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
289             "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
290           ],
291         ]);
293         function maybeReplaceURL(app) {
294           if (app instanceof Ci.nsIWebHandlerApp) {
295             let { uriTemplate } = app;
296             let sub = kSubstitutions.get(uriTemplate);
297             if (sub) {
298               app.uriTemplate = sub;
299               return true;
300             }
301           }
302           return false;
303         }
304         let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
305           "mailto"
306         );
307         if (this.exists(mailHandler)) {
308           this.fillHandlerInfo(mailHandler, "");
309           let handlers = mailHandler.possibleApplicationHandlers;
310           let shouldStore = false;
311           for (let i = handlers.length - 1; i >= 0; i--) {
312             let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
313             // Note: will evaluate the RHS because it's a binary rather than
314             // logical or.
315             shouldStore |= maybeReplaceURL(app);
316           }
317           // Then check the preferred handler.
318           if (mailHandler.preferredApplicationHandler) {
319             shouldStore |= maybeReplaceURL(
320               mailHandler.preferredApplicationHandler
321             );
322           }
323           // Then store, if we changed anything. Note that store() handles
324           // duplicates, so we don't have to.
325           if (shouldStore) {
326             this.store(mailHandler);
327           }
328         }
329       },
330     };
331     let migrationsToRun = Services.prefs.getCharPref(
332       "browser.handlers.migrations",
333       ""
334     );
335     migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
336     for (let migration of migrationsToRun) {
337       migration.trim();
338       try {
339         kMigrations[migration]();
340       } catch (ex) {
341         Cu.reportError(ex);
342       }
343     }
345     if (migrationsToRun.length) {
346       Services.prefs.clearUserPref("browser.handlers.migrations");
347     }
348   },
350   _onDBChange() {
351     return (async () => {
352       if (this.__store) {
353         await this.__store.finalize();
354       }
355       this.__store = null;
356       this.__storeInitialized = false;
357     })().catch(Cu.reportError);
358   },
360   // nsIObserver
361   observe(subject, topic, data) {
362     if (topic != "handlersvc-json-replace") {
363       return;
364     }
365     let promise = this._onDBChange();
366     promise.then(() => {
367       Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
368     });
369   },
371   // nsIHandlerService
372   asyncInit() {
373     if (!this.__store) {
374       this.__store = new JSONFile({
375         path: PathUtils.join(
376           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
377           "handlers.json"
378         ),
379         dataPostProcessor: this._dataPostProcessor.bind(this),
380       });
381       this.__store
382         .load()
383         .then(() => {
384           // __store can be null if we called _onDBChange in the mean time.
385           if (this.__store) {
386             this._ensureStoreInitialized();
387           }
388         })
389         .catch(Cu.reportError);
390     }
391   },
393   // nsIHandlerService
394   enumerate() {
395     let handlers = Cc["@mozilla.org/array;1"].createInstance(
396       Ci.nsIMutableArray
397     );
398     for (let type of Object.keys(this._store.data.mimeTypes)) {
399       let handler = gMIMEService.getFromTypeAndExtension(type, null);
400       handlers.appendElement(handler);
401     }
402     for (let type of Object.keys(this._store.data.schemes)) {
403       // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
404       // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
405       // until one of its properties is accessed.
406       //
407       // Note: our caller still needs to yield periodically when iterating
408       // the enumerator and accessing handler properties to avoid monopolizing
409       // the main thread.
410       //
411       let handler = new Proxy(
412         {
413           QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
414           type,
415           get _handlerInfo() {
416             delete this._handlerInfo;
417             return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
418               type
419             ));
420           },
421         },
422         {
423           get(target, name) {
424             return target[name] || target._handlerInfo[name];
425           },
426           set(target, name, value) {
427             target._handlerInfo[name] = value;
428           },
429         }
430       );
431       handlers.appendElement(handler);
432     }
433     return handlers.enumerate(Ci.nsIHandlerInfo);
434   },
436   // nsIHandlerService
437   store(handlerInfo) {
438     let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
440     // Retrieve an existing entry if present, instead of creating a new one, so
441     // that we preserve unknown properties for forward compatibility.
442     let storedHandlerInfo = handlerList[handlerInfo.type];
443     if (!storedHandlerInfo) {
444       storedHandlerInfo = {};
445       handlerList[handlerInfo.type] = storedHandlerInfo;
446     }
448     // Only a limited number of preferredAction values is allowed.
449     if (
450       handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk ||
451       handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault ||
452       handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally
453     ) {
454       storedHandlerInfo.action = handlerInfo.preferredAction;
455     } else {
456       storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp;
457     }
459     if (handlerInfo.alwaysAskBeforeHandling) {
460       storedHandlerInfo.ask = true;
461     } else {
462       delete storedHandlerInfo.ask;
463     }
465     // Build a list of unique nsIHandlerInfo instances to process later.
466     let handlers = [];
467     if (handlerInfo.preferredApplicationHandler) {
468       handlers.push(handlerInfo.preferredApplicationHandler);
469     }
470     for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
471       Ci.nsIHandlerApp
472     )) {
473       // If the caller stored duplicate handlers, we save them only once.
474       if (!handlers.some(h => h.equals(handler))) {
475         handlers.push(handler);
476       }
477     }
479     // If any of the nsIHandlerInfo instances cannot be serialized, it is not
480     // included in the final list. The first element is always the preferred
481     // handler, or null if there is none.
482     let serializableHandlers = handlers
483       .map(h => this.handlerAppToSerializable(h))
484       .filter(h => h);
485     if (serializableHandlers.length) {
486       if (!handlerInfo.preferredApplicationHandler) {
487         serializableHandlers.unshift(null);
488       }
489       storedHandlerInfo.handlers = serializableHandlers;
490     } else {
491       delete storedHandlerInfo.handlers;
492     }
494     if (this._isMIMEInfo(handlerInfo)) {
495       let extensions = storedHandlerInfo.extensions || [];
496       for (let extension of handlerInfo.getFileExtensions()) {
497         extension = extension.toLowerCase();
498         // If the caller stored duplicate extensions, we save them only once.
499         if (!extensions.includes(extension)) {
500           extensions.push(extension);
501         }
502       }
503       if (extensions.length) {
504         storedHandlerInfo.extensions = extensions;
505       } else {
506         delete storedHandlerInfo.extensions;
507       }
508     }
510     // If we're saving *anything*, it stops being a stub:
511     delete storedHandlerInfo.stubEntry;
513     this._store.saveSoon();
515     // Now notify PDF.js. This is hacky, but a lot better than expecting all
516     // the consumers to do it...
517     if (handlerInfo.type == "application/pdf") {
518       Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
519     }
520   },
522   // nsIHandlerService
523   fillHandlerInfo(handlerInfo, overrideType) {
524     let type = overrideType || handlerInfo.type;
525     let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
526       type
527     ];
528     if (!storedHandlerInfo) {
529       throw new Components.Exception(
530         "handlerSvc fillHandlerInfo: don't know this type",
531         Cr.NS_ERROR_NOT_AVAILABLE
532       );
533     }
535     let isStub = !!storedHandlerInfo.stubEntry;
536     // In the normal case, this is not a stub, so we can just read stored info
537     // and write to the handlerInfo object we were passed.
538     if (!isStub) {
539       handlerInfo.preferredAction = storedHandlerInfo.action;
540       handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
541     } else {
542       // If we've got a stub, ensure the defaults are still set:
543       gExternalProtocolService.setProtocolHandlerDefaults(
544         handlerInfo,
545         handlerInfo.hasDefaultHandler
546       );
547       if (
548         handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
549         handlerInfo.alwaysAskBeforeHandling
550       ) {
551         // `store` will default to `useHelperApp` because `alwaysAsk` is
552         // not one of the 3 recognized options; for compatibility, do
553         // the same here.
554         handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
555       }
556     }
557     // If it *is* a stub, don't override alwaysAskBeforeHandling or the
558     // preferred actions. Instead, just append the stored handlers, without
559     // overriding the preferred app, and then schedule a task to store proper
560     // info for this handler.
561     this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
563     if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
564       for (let extension of storedHandlerInfo.extensions) {
565         handlerInfo.appendExtension(extension);
566       }
567     }
568   },
570   /**
571    * Private method to inject stored handler information into an nsIHandlerInfo
572    * instance.
573    * @param handlerInfo           the nsIHandlerInfo instance to write to
574    * @param storedHandlers        the stored handlers
575    * @param keepPreferredApp      whether to keep the handlerInfo's
576    *                              preferredApplicationHandler or override it
577    *                              (default: false, ie override it)
578    */
579   _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
580     // If the first item is not null, it is also the preferred handler. Since
581     // we cannot modify the stored array, use a boolean to keep track of this.
582     let isFirstItem = true;
583     for (let handler of storedHandlers || [null]) {
584       let handlerApp = this.handlerAppFromSerializable(handler || {});
585       if (isFirstItem) {
586         isFirstItem = false;
587         // Do not overwrite the preferred app unless that's allowed
588         if (!keepPreferredApp) {
589           handlerInfo.preferredApplicationHandler = handlerApp;
590         }
591       }
592       if (handlerApp) {
593         handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
594       }
595     }
596   },
598   /**
599    * @param handler
600    *        A nsIHandlerApp handler app
601    * @returns  Serializable representation of a handler app object.
602    */
603   handlerAppToSerializable(handler) {
604     if (handler instanceof Ci.nsILocalHandlerApp) {
605       return {
606         name: handler.name,
607         path: handler.executable.path,
608       };
609     } else if (handler instanceof Ci.nsIWebHandlerApp) {
610       return {
611         name: handler.name,
612         uriTemplate: handler.uriTemplate,
613       };
614     } else if (handler instanceof Ci.nsIDBusHandlerApp) {
615       return {
616         name: handler.name,
617         service: handler.service,
618         method: handler.method,
619         objectPath: handler.objectPath,
620         dBusInterface: handler.dBusInterface,
621       };
622     } else if (handler instanceof Ci.nsIGIOMimeApp) {
623       return {
624         name: handler.name,
625         command: handler.command,
626       };
627     }
628     // If the handler is an unknown handler type, return null.
629     // Android default application handler is the case.
630     return null;
631   },
633   /**
634    * @param handlerObj
635    *        Serializable representation of a handler object.
636    * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
637    */
638   handlerAppFromSerializable(handlerObj) {
639     let handlerApp;
640     if ("path" in handlerObj) {
641       try {
642         let file = new FileUtils.File(handlerObj.path);
643         if (!file.exists()) {
644           return null;
645         }
646         handlerApp = Cc[
647           "@mozilla.org/uriloader/local-handler-app;1"
648         ].createInstance(Ci.nsILocalHandlerApp);
649         handlerApp.executable = file;
650       } catch (ex) {
651         return null;
652       }
653     } else if ("uriTemplate" in handlerObj) {
654       handlerApp = Cc[
655         "@mozilla.org/uriloader/web-handler-app;1"
656       ].createInstance(Ci.nsIWebHandlerApp);
657       handlerApp.uriTemplate = handlerObj.uriTemplate;
658     } else if ("service" in handlerObj) {
659       handlerApp = Cc[
660         "@mozilla.org/uriloader/dbus-handler-app;1"
661       ].createInstance(Ci.nsIDBusHandlerApp);
662       handlerApp.service = handlerObj.service;
663       handlerApp.method = handlerObj.method;
664       handlerApp.objectPath = handlerObj.objectPath;
665       handlerApp.dBusInterface = handlerObj.dBusInterface;
666     } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
667       try {
668         handlerApp = Cc["@mozilla.org/gio-service;1"]
669           .getService(Ci.nsIGIOService)
670           .createAppFromCommand(handlerObj.command, handlerObj.name);
671       } catch (ex) {
672         return null;
673       }
674     } else {
675       return null;
676     }
678     handlerApp.name = handlerObj.name;
679     return handlerApp;
680   },
682   /**
683    * The function returns a reference to the "mimeTypes" or "schemes" object
684    * based on which type of handlerInfo is provided.
685    */
686   _getHandlerListByHandlerInfoType(handlerInfo) {
687     return this._isMIMEInfo(handlerInfo)
688       ? this._store.data.mimeTypes
689       : this._store.data.schemes;
690   },
692   /**
693    * Determines whether an nsIHandlerInfo instance represents a MIME type.
694    */
695   _isMIMEInfo(handlerInfo) {
696     // We cannot rely only on the instanceof check because on Android both MIME
697     // types and protocols are instances of nsIMIMEInfo. We still do the check
698     // so that properties of nsIMIMEInfo become available to the callers.
699     return (
700       handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
701     );
702   },
704   // nsIHandlerService
705   exists(handlerInfo) {
706     return (
707       handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
708     );
709   },
711   // nsIHandlerService
712   remove(handlerInfo) {
713     delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
714     this._store.saveSoon();
715   },
717   // nsIHandlerService
718   getTypeFromExtension(fileExtension) {
719     let extension = fileExtension.toLowerCase();
720     let mimeTypes = this._store.data.mimeTypes;
721     for (let type of Object.keys(mimeTypes)) {
722       if (
723         mimeTypes[type].extensions &&
724         mimeTypes[type].extensions.includes(extension)
725       ) {
726         return type;
727       }
728     }
729     return "";
730   },
733 this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);