Bug 1732409 let fake:true getUserMedia() parameter override loopback prefs r=jib
[gecko.git] / uriloader / exthandler / HandlerService.js
blobc05ee64eb1b0fc72fd681fc354151f8e312ab47b
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"
28 const { Integration } = ChromeUtils.import(
29   "resource://gre/modules/Integration.jsm"
32 /* global DownloadIntegration */
33 Integration.downloads.defineModuleGetter(
34   this,
35   "DownloadIntegration",
36   "resource://gre/modules/DownloadIntegration.jsm"
39 XPCOMUtils.defineLazyServiceGetter(
40   this,
41   "gExternalProtocolService",
42   "@mozilla.org/uriloader/external-protocol-service;1",
43   "nsIExternalProtocolService"
45 XPCOMUtils.defineLazyServiceGetter(
46   this,
47   "gMIMEService",
48   "@mozilla.org/mime;1",
49   "nsIMIMEService"
52 function HandlerService() {
53   // Observe handlersvc-json-replace so we can switch to the datasource
54   Services.obs.addObserver(this, "handlersvc-json-replace", true);
57 HandlerService.prototype = {
58   classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"),
59   QueryInterface: ChromeUtils.generateQI([
60     "nsISupportsWeakReference",
61     "nsIHandlerService",
62     "nsIObserver",
63   ]),
65   __store: null,
66   get _store() {
67     if (!this.__store) {
68       this.__store = new JSONFile({
69         path: PathUtils.join(
70           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
71           "handlers.json"
72         ),
73         dataPostProcessor: this._dataPostProcessor.bind(this),
74       });
75     }
77     // Always call this even if this.__store was set, since it may have been
78     // set by asyncInit, which might not have completed yet.
79     this._ensureStoreInitialized();
80     return this.__store;
81   },
83   __storeInitialized: false,
84   _ensureStoreInitialized() {
85     if (!this.__storeInitialized) {
86       this.__storeInitialized = true;
87       this.__store.ensureDataReady();
89       this._injectDefaultProtocolHandlersIfNeeded();
90       this._migrateProtocolHandlersIfNeeded();
92       Services.obs.notifyObservers(null, "handlersvc-store-initialized");
94       // Bug 1736924: run migration for browser.download.improvements_to_download_panel if applicable.
95       // Since we need DownloadsViewInternally to verify mimetypes, we run this after
96       // DownloadsViewInternally is registered via the 'handlersvc-store-initialized' notification.
97       this._migrateDownloadsImprovementsIfNeeded();
98     }
99   },
101   _dataPostProcessor(data) {
102     return data.defaultHandlersVersion
103       ? data
104       : {
105           defaultHandlersVersion: {},
106           mimeTypes: {},
107           schemes: {},
108           isDownloadsImprovementsAlreadyMigrated: false,
109         };
110   },
112   /**
113    * Injects new default protocol handlers if the version in the preferences is
114    * newer than the one in the data store.
115    */
116   _injectDefaultProtocolHandlersIfNeeded() {
117     let prefsDefaultHandlersVersion;
118     try {
119       prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
120         "gecko.handlerService.defaultHandlersVersion",
121         Ci.nsIPrefLocalizedString
122       );
123     } catch (ex) {
124       if (
125         ex instanceof Components.Exception &&
126         ex.result == Cr.NS_ERROR_UNEXPECTED
127       ) {
128         // This platform does not have any default protocol handlers configured.
129         return;
130       }
131       throw ex;
132     }
134     try {
135       prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data);
136       let locale = Services.locale.appLocaleAsBCP47;
138       let defaultHandlersVersion =
139         this._store.data.defaultHandlersVersion[locale] || 0;
140       if (defaultHandlersVersion < prefsDefaultHandlersVersion) {
141         this._injectDefaultProtocolHandlers();
142         this._store.data.defaultHandlersVersion[
143           locale
144         ] = prefsDefaultHandlersVersion;
145         // Now save the result:
146         this._store.saveSoon();
147       }
148     } catch (ex) {
149       Cu.reportError(ex);
150     }
151   },
153   _injectDefaultProtocolHandlers() {
154     let schemesPrefBranch = Services.prefs.getBranch(
155       "gecko.handlerService.schemes."
156     );
157     let schemePrefList = schemesPrefBranch.getChildList("");
159     let schemes = {};
161     // read all the scheme prefs into a hash
162     for (let schemePrefName of schemePrefList) {
163       let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
165       try {
166         let attrData = schemesPrefBranch.getComplexValue(
167           schemePrefName,
168           Ci.nsIPrefLocalizedString
169         ).data;
170         if (!(scheme in schemes)) {
171           schemes[scheme] = {};
172         }
174         if (!(handlerNumber in schemes[scheme])) {
175           schemes[scheme][handlerNumber] = {};
176         }
178         schemes[scheme][handlerNumber][attribute] = attrData;
179       } catch (ex) {}
180     }
182     // Now drop any entries without a uriTemplate, or with a broken one.
183     // The Array.from calls ensure we can safely delete things without
184     // affecting the iterator.
185     for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) {
186       let handlers = Array.from(Object.entries(handlerObject));
187       let validHandlers = 0;
188       for (let [key, obj] of handlers) {
189         if (
190           !obj.uriTemplate ||
191           !obj.uriTemplate.startsWith("https://") ||
192           !obj.uriTemplate.toLowerCase().includes("%s")
193         ) {
194           delete handlerObject[key];
195         } else {
196           validHandlers++;
197         }
198       }
199       if (!validHandlers) {
200         delete schemes[scheme];
201       }
202     }
204     // Now, we're going to cheat. Terribly. The idiologically correct way
205     // of implementing the following bit of code would be to fetch the
206     // handler info objects from the protocol service, manipulate those,
207     // and then store each of them.
208     // However, that's expensive. It causes us to talk to the OS about
209     // default apps, which causes the OS to go hit the disk.
210     // All we're trying to do is insert some web apps into the list. We
211     // don't care what's already in the file, we just want to do the
212     // equivalent of appending into the database. So let's just go do that:
213     for (let scheme of Object.keys(schemes)) {
214       let existingSchemeInfo = this._store.data.schemes[scheme];
215       if (!existingSchemeInfo) {
216         // Haven't seen this scheme before. Default to asking which app the
217         // user wants to use:
218         existingSchemeInfo = {
219           // Signal to future readers that we didn't ask the OS anything.
220           // When the entry is first used, get the info from the OS.
221           stubEntry: true,
222           // The first item in the list is the preferred handler, and
223           // there isn't one, so we fill in null:
224           handlers: [null],
225         };
226         this._store.data.schemes[scheme] = existingSchemeInfo;
227       }
228       let { handlers } = existingSchemeInfo;
229       for (let handlerNumber of Object.keys(schemes[scheme])) {
230         let newHandler = schemes[scheme][handlerNumber];
231         // If there is already a handler registered with the same template
232         // URL, ignore the new one:
233         let matchingTemplate = handler =>
234           handler && handler.uriTemplate == newHandler.uriTemplate;
235         if (!handlers.some(matchingTemplate)) {
236           handlers.push(newHandler);
237         }
238       }
239     }
240   },
242   /**
243    * Execute any migrations. Migrations are defined here for any changes or removals for
244    * existing handlers. Additions are still handled via the localized prefs infrastructure.
245    *
246    * This depends on the browser.handlers.migrations pref being set by migrateUI in
247    * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
248    * This is a comma-separated list of identifiers of migrations that need running.
249    * This avoids both re-running older migrations and keeping an additional
250    * pref around permanently.
251    */
252   _migrateProtocolHandlersIfNeeded() {
253     const kMigrations = {
254       "30boxes": () => {
255         const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
256         let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
257           "webcal"
258         );
259         if (this.exists(webcalHandler)) {
260           this.fillHandlerInfo(webcalHandler, "");
261           let shouldStore = false;
262           // First remove 30boxes from possible handlers.
263           let handlers = webcalHandler.possibleApplicationHandlers;
264           for (let i = handlers.length - 1; i >= 0; i--) {
265             let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
266             if (
267               app instanceof Ci.nsIWebHandlerApp &&
268               k30BoxesRegex.test(app.uriTemplate)
269             ) {
270               shouldStore = true;
271               handlers.removeElementAt(i);
272             }
273           }
274           // Then remove as a preferred handler.
275           if (webcalHandler.preferredApplicationHandler) {
276             let app = webcalHandler.preferredApplicationHandler;
277             if (
278               app instanceof Ci.nsIWebHandlerApp &&
279               k30BoxesRegex.test(app.uriTemplate)
280             ) {
281               webcalHandler.preferredApplicationHandler = null;
282               shouldStore = true;
283             }
284           }
285           // Then store, if we changed anything.
286           if (shouldStore) {
287             this.store(webcalHandler);
288           }
289         }
290       },
291       // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
292       "secure-mail": () => {
293         const kSubstitutions = new Map([
294           [
295             "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
296             "https://mail.yahoo.co.jp/compose/?To=%s",
297           ],
298           [
299             "http://www.inbox.lv/rfc2368/?value=%s",
300             "https://mail.inbox.lv/compose?to=%s",
301           ],
302           [
303             "http://poczta.interia.pl/mh/?mailto=%s",
304             "https://poczta.interia.pl/mh/?mailto=%s",
305           ],
306           [
307             "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
308             "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
309           ],
310         ]);
312         function maybeReplaceURL(app) {
313           if (app instanceof Ci.nsIWebHandlerApp) {
314             let { uriTemplate } = app;
315             let sub = kSubstitutions.get(uriTemplate);
316             if (sub) {
317               app.uriTemplate = sub;
318               return true;
319             }
320           }
321           return false;
322         }
323         let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
324           "mailto"
325         );
326         if (this.exists(mailHandler)) {
327           this.fillHandlerInfo(mailHandler, "");
328           let handlers = mailHandler.possibleApplicationHandlers;
329           let shouldStore = false;
330           for (let i = handlers.length - 1; i >= 0; i--) {
331             let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
332             // Note: will evaluate the RHS because it's a binary rather than
333             // logical or.
334             shouldStore |= maybeReplaceURL(app);
335           }
336           // Then check the preferred handler.
337           if (mailHandler.preferredApplicationHandler) {
338             shouldStore |= maybeReplaceURL(
339               mailHandler.preferredApplicationHandler
340             );
341           }
342           // Then store, if we changed anything. Note that store() handles
343           // duplicates, so we don't have to.
344           if (shouldStore) {
345             this.store(mailHandler);
346           }
347         }
348       },
349     };
350     let migrationsToRun = Services.prefs.getCharPref(
351       "browser.handlers.migrations",
352       ""
353     );
354     migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
355     for (let migration of migrationsToRun) {
356       migration.trim();
357       try {
358         kMigrations[migration]();
359       } catch (ex) {
360         Cu.reportError(ex);
361       }
362     }
364     if (migrationsToRun.length) {
365       Services.prefs.clearUserPref("browser.handlers.migrations");
366     }
367   },
369   _onDBChange() {
370     return (async () => {
371       if (this.__store) {
372         await this.__store.finalize();
373       }
374       this.__store = null;
375       this.__storeInitialized = false;
376     })().catch(Cu.reportError);
377   },
379   // nsIObserver
380   observe(subject, topic, data) {
381     if (topic != "handlersvc-json-replace") {
382       return;
383     }
384     let promise = this._onDBChange();
385     promise.then(() => {
386       Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
387     });
388   },
390   // nsIHandlerService
391   asyncInit() {
392     if (!this.__store) {
393       this.__store = new JSONFile({
394         path: PathUtils.join(
395           Services.dirsvc.get("ProfD", Ci.nsIFile).path,
396           "handlers.json"
397         ),
398         dataPostProcessor: this._dataPostProcessor.bind(this),
399       });
400       this.__store
401         .load()
402         .then(() => {
403           // __store can be null if we called _onDBChange in the mean time.
404           if (this.__store) {
405             this._ensureStoreInitialized();
406           }
407         })
408         .catch(Cu.reportError);
409     }
410   },
412   /**
413    * Update already existing handlers for non-internal mimetypes to have prefs set from alwaysAsk
414    * to saveToDisk. However, if reading an internal mimetype and set to alwaysAsk, update to use handleInternally.
415    * This migration is needed since browser.download.improvements_to_download_panel does not
416    * override user preferences if preferredAction = alwaysAsk. By doing so, we can ensure that file prompt
417    * behaviours remain consistent for most files.
418    *
419    * See Bug 1736924 for more information.
420    */
421   _migrateDownloadsImprovementsIfNeeded() {
422     // Migrate if the preference is enabled AND if the migration has never been run before.
423     // Otherwise, we risk overwriting preferences for existing profiles!
424     if (
425       Services.prefs.getBoolPref(
426         "browser.download.improvements_to_download_panel"
427       ) &&
428       !Services.policies.getActivePolicies()?.Handlers &&
429       !this._store.data.isDownloadsImprovementsAlreadyMigrated
430     ) {
431       for (let [type, mimeInfo] of Object.entries(this._store.data.mimeTypes)) {
432         let isViewableInternally = DownloadIntegration.shouldViewDownloadInternally(
433           type
434         );
435         let isAskOnly = mimeInfo && mimeInfo.ask;
437         if (isAskOnly) {
438           if (isViewableInternally) {
439             mimeInfo.action = Ci.nsIHandlerInfo.handleInternally;
440           } else {
441             mimeInfo.action = Ci.nsIHandlerInfo.saveToDisk;
442           }
444           // Sets alwaysAskBeforeHandling to false. Needed to ensure that:
445           // preferredAction appears as expected in preferences table; and
446           // downloads behaviour is updated to never show UCT window.
447           mimeInfo.ask = false;
448         }
449       }
451       this._store.data.isDownloadsImprovementsAlreadyMigrated = true;
452       this._store.saveSoon();
453     }
454   },
456   // nsIHandlerService
457   enumerate() {
458     let handlers = Cc["@mozilla.org/array;1"].createInstance(
459       Ci.nsIMutableArray
460     );
461     for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
462       let primaryExtension = typeInfo.extensions?.[0] ?? null;
463       let handler = gMIMEService.getFromTypeAndExtension(
464         type,
465         primaryExtension
466       );
467       handlers.appendElement(handler);
468     }
469     for (let type of Object.keys(this._store.data.schemes)) {
470       // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
471       // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
472       // until one of its properties is accessed.
473       //
474       // Note: our caller still needs to yield periodically when iterating
475       // the enumerator and accessing handler properties to avoid monopolizing
476       // the main thread.
477       //
478       let handler = new Proxy(
479         {
480           QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
481           type,
482           get _handlerInfo() {
483             delete this._handlerInfo;
484             return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
485               type
486             ));
487           },
488         },
489         {
490           get(target, name) {
491             return target[name] || target._handlerInfo[name];
492           },
493           set(target, name, value) {
494             target._handlerInfo[name] = value;
495           },
496         }
497       );
498       handlers.appendElement(handler);
499     }
500     return handlers.enumerate(Ci.nsIHandlerInfo);
501   },
503   // nsIHandlerService
504   store(handlerInfo) {
505     let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
507     // Retrieve an existing entry if present, instead of creating a new one, so
508     // that we preserve unknown properties for forward compatibility.
509     let storedHandlerInfo = handlerList[handlerInfo.type];
510     if (!storedHandlerInfo) {
511       storedHandlerInfo = {};
512       handlerList[handlerInfo.type] = storedHandlerInfo;
513     }
515     // Only a limited number of preferredAction values is allowed.
516     if (
517       handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk ||
518       handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault ||
519       handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally ||
520       // For files (ie mimetype rather than protocol handling info), ensure
521       // we can store the "always ask" state, too:
522       (handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
523         this._isMIMEInfo(handlerInfo) &&
524         Services.prefs.getBoolPref(
525           "browser.download.improvements_to_download_panel"
526         ))
527     ) {
528       storedHandlerInfo.action = handlerInfo.preferredAction;
529     } else {
530       storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp;
531     }
533     if (handlerInfo.alwaysAskBeforeHandling) {
534       storedHandlerInfo.ask = true;
535     } else {
536       delete storedHandlerInfo.ask;
537     }
539     // Build a list of unique nsIHandlerInfo instances to process later.
540     let handlers = [];
541     if (handlerInfo.preferredApplicationHandler) {
542       handlers.push(handlerInfo.preferredApplicationHandler);
543     }
544     for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
545       Ci.nsIHandlerApp
546     )) {
547       // If the caller stored duplicate handlers, we save them only once.
548       if (!handlers.some(h => h.equals(handler))) {
549         handlers.push(handler);
550       }
551     }
553     // If any of the nsIHandlerInfo instances cannot be serialized, it is not
554     // included in the final list. The first element is always the preferred
555     // handler, or null if there is none.
556     let serializableHandlers = handlers
557       .map(h => this.handlerAppToSerializable(h))
558       .filter(h => h);
559     if (serializableHandlers.length) {
560       if (!handlerInfo.preferredApplicationHandler) {
561         serializableHandlers.unshift(null);
562       }
563       storedHandlerInfo.handlers = serializableHandlers;
564     } else {
565       delete storedHandlerInfo.handlers;
566     }
568     if (this._isMIMEInfo(handlerInfo)) {
569       let extensions = storedHandlerInfo.extensions || [];
570       for (let extension of handlerInfo.getFileExtensions()) {
571         extension = extension.toLowerCase();
572         // If the caller stored duplicate extensions, we save them only once.
573         if (!extensions.includes(extension)) {
574           extensions.push(extension);
575         }
576       }
577       if (extensions.length) {
578         storedHandlerInfo.extensions = extensions;
579       } else {
580         delete storedHandlerInfo.extensions;
581       }
582     }
584     // If we're saving *anything*, it stops being a stub:
585     delete storedHandlerInfo.stubEntry;
587     this._store.saveSoon();
589     // Now notify PDF.js. This is hacky, but a lot better than expecting all
590     // the consumers to do it...
591     if (handlerInfo.type == "application/pdf") {
592       Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
593     }
594   },
596   // nsIHandlerService
597   fillHandlerInfo(handlerInfo, overrideType) {
598     let type = overrideType || handlerInfo.type;
599     let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
600       type
601     ];
602     if (!storedHandlerInfo) {
603       throw new Components.Exception(
604         "handlerSvc fillHandlerInfo: don't know this type",
605         Cr.NS_ERROR_NOT_AVAILABLE
606       );
607     }
609     let isStub = !!storedHandlerInfo.stubEntry;
610     // In the normal case, this is not a stub, so we can just read stored info
611     // and write to the handlerInfo object we were passed.
612     if (!isStub) {
613       handlerInfo.preferredAction = storedHandlerInfo.action;
614       handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
615     } else {
616       // If we've got a stub, ensure the defaults are still set:
617       gExternalProtocolService.setProtocolHandlerDefaults(
618         handlerInfo,
619         handlerInfo.hasDefaultHandler
620       );
621       if (
622         handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
623         handlerInfo.alwaysAskBeforeHandling
624       ) {
625         // `store` will default to `useHelperApp` because `alwaysAsk` is
626         // not one of the 3 recognized options; for compatibility, do
627         // the same here.
628         handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
629       }
630     }
631     // If it *is* a stub, don't override alwaysAskBeforeHandling or the
632     // preferred actions. Instead, just append the stored handlers, without
633     // overriding the preferred app, and then schedule a task to store proper
634     // info for this handler.
635     this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
637     if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
638       for (let extension of storedHandlerInfo.extensions) {
639         handlerInfo.appendExtension(extension);
640       }
641     } else if (this._mockedHandler) {
642       this._insertMockedHandler(handlerInfo);
643     }
644   },
646   /**
647    * Private method to inject stored handler information into an nsIHandlerInfo
648    * instance.
649    * @param handlerInfo           the nsIHandlerInfo instance to write to
650    * @param storedHandlers        the stored handlers
651    * @param keepPreferredApp      whether to keep the handlerInfo's
652    *                              preferredApplicationHandler or override it
653    *                              (default: false, ie override it)
654    */
655   _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
656     // If the first item is not null, it is also the preferred handler. Since
657     // we cannot modify the stored array, use a boolean to keep track of this.
658     let isFirstItem = true;
659     for (let handler of storedHandlers || [null]) {
660       let handlerApp = this.handlerAppFromSerializable(handler || {});
661       if (isFirstItem) {
662         isFirstItem = false;
663         // Do not overwrite the preferred app unless that's allowed
664         if (!keepPreferredApp) {
665           handlerInfo.preferredApplicationHandler = handlerApp;
666         }
667       }
668       if (handlerApp) {
669         handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
670       }
671     }
672   },
674   /**
675    * @param handler
676    *        A nsIHandlerApp handler app
677    * @returns  Serializable representation of a handler app object.
678    */
679   handlerAppToSerializable(handler) {
680     if (handler instanceof Ci.nsILocalHandlerApp) {
681       return {
682         name: handler.name,
683         path: handler.executable.path,
684       };
685     } else if (handler instanceof Ci.nsIWebHandlerApp) {
686       return {
687         name: handler.name,
688         uriTemplate: handler.uriTemplate,
689       };
690     } else if (handler instanceof Ci.nsIDBusHandlerApp) {
691       return {
692         name: handler.name,
693         service: handler.service,
694         method: handler.method,
695         objectPath: handler.objectPath,
696         dBusInterface: handler.dBusInterface,
697       };
698     } else if (handler instanceof Ci.nsIGIOMimeApp) {
699       return {
700         name: handler.name,
701         command: handler.command,
702       };
703     }
704     // If the handler is an unknown handler type, return null.
705     // Android default application handler is the case.
706     return null;
707   },
709   /**
710    * @param handlerObj
711    *        Serializable representation of a handler object.
712    * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
713    */
714   handlerAppFromSerializable(handlerObj) {
715     let handlerApp;
716     if ("path" in handlerObj) {
717       try {
718         let file = new FileUtils.File(handlerObj.path);
719         if (!file.exists()) {
720           return null;
721         }
722         handlerApp = Cc[
723           "@mozilla.org/uriloader/local-handler-app;1"
724         ].createInstance(Ci.nsILocalHandlerApp);
725         handlerApp.executable = file;
726       } catch (ex) {
727         return null;
728       }
729     } else if ("uriTemplate" in handlerObj) {
730       handlerApp = Cc[
731         "@mozilla.org/uriloader/web-handler-app;1"
732       ].createInstance(Ci.nsIWebHandlerApp);
733       handlerApp.uriTemplate = handlerObj.uriTemplate;
734     } else if ("service" in handlerObj) {
735       handlerApp = Cc[
736         "@mozilla.org/uriloader/dbus-handler-app;1"
737       ].createInstance(Ci.nsIDBusHandlerApp);
738       handlerApp.service = handlerObj.service;
739       handlerApp.method = handlerObj.method;
740       handlerApp.objectPath = handlerObj.objectPath;
741       handlerApp.dBusInterface = handlerObj.dBusInterface;
742     } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
743       try {
744         handlerApp = Cc["@mozilla.org/gio-service;1"]
745           .getService(Ci.nsIGIOService)
746           .createAppFromCommand(handlerObj.command, handlerObj.name);
747       } catch (ex) {
748         return null;
749       }
750     } else {
751       return null;
752     }
754     handlerApp.name = handlerObj.name;
755     return handlerApp;
756   },
758   /**
759    * The function returns a reference to the "mimeTypes" or "schemes" object
760    * based on which type of handlerInfo is provided.
761    */
762   _getHandlerListByHandlerInfoType(handlerInfo) {
763     return this._isMIMEInfo(handlerInfo)
764       ? this._store.data.mimeTypes
765       : this._store.data.schemes;
766   },
768   /**
769    * Determines whether an nsIHandlerInfo instance represents a MIME type.
770    */
771   _isMIMEInfo(handlerInfo) {
772     // We cannot rely only on the instanceof check because on Android both MIME
773     // types and protocols are instances of nsIMIMEInfo. We still do the check
774     // so that properties of nsIMIMEInfo become available to the callers.
775     return (
776       handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
777     );
778   },
780   // nsIHandlerService
781   exists(handlerInfo) {
782     return (
783       handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
784     );
785   },
787   // nsIHandlerService
788   remove(handlerInfo) {
789     delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
790     this._store.saveSoon();
791   },
793   // nsIHandlerService
794   getTypeFromExtension(fileExtension) {
795     let extension = fileExtension.toLowerCase();
796     let mimeTypes = this._store.data.mimeTypes;
797     for (let type of Object.keys(mimeTypes)) {
798       if (
799         mimeTypes[type].extensions &&
800         mimeTypes[type].extensions.includes(extension)
801       ) {
802         return type;
803       }
804     }
805     return "";
806   },
808   _mockedHandler: null,
809   _mockedProtocol: null,
811   _insertMockedHandler(handlerInfo) {
812     if (handlerInfo.type == this._mockedProtocol) {
813       handlerInfo.preferredApplicationHandler = this._mockedHandler;
814       handlerInfo.possibleApplicationHandlers.insertElementAt(
815         this._mockedHandler,
816         0
817       );
818     }
819   },
821   // test-only: mock the handler instance for a particular protocol/scheme
822   mockProtocolHandler(protocol) {
823     if (!protocol) {
824       this._mockedProtocol = null;
825       this._mockedHandler = null;
826       return;
827     }
828     this._mockedProtocol = protocol;
829     this._mockedHandler = {
830       QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
831       launchWithURI(uri, context) {
832         Services.obs.notifyObservers(uri, "mocked-protocol-handler");
833       },
834       name: "Mocked handler",
835       detailedDescription: "Mocked handler for tests",
836       equals(x) {
837         return x == this;
838       },
839       get executable() {
840         if (AppConstants.platform == "macosx") {
841           // We need an app path that isn't us, nor in our app bundle, and
842           // Apple no longer allows us to read the default-shipped apps
843           // in /Applications/ - except for Safari, it would appear!
844           let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
845           f.initWithPath("/Applications/Safari.app");
846           return f;
847         }
848         return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
849       },
850       parameterCount: 0,
851       clearParameters() {},
852       appendParameter() {},
853       getParameter() {},
854       parameterExists() {
855         return false;
856       },
857     };
858   },
861 this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);