summaryrefslogtreecommitdiffstats
path: root/uriloader/exthandler/ExtHandlerService.sys.mjs
blob: 85cff866b2c4b0a6e63a3af950da277f8d8b2bad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const {
  saveToDisk,
  alwaysAsk,
  useHelperApp,
  handleInternally,
  useSystemDefault,
} = Ci.nsIHandlerInfo;

const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs",
  kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
});
import { Integration } from "resource://gre/modules/Integration.sys.mjs";

Integration.downloads.defineESModuleGetter(
  lazy,
  "DownloadIntegration",
  "resource://gre/modules/DownloadIntegration.sys.mjs"
);

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "externalProtocolService",
  "@mozilla.org/uriloader/external-protocol-service;1",
  "nsIExternalProtocolService"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "MIMEService",
  "@mozilla.org/mime;1",
  "nsIMIMEService"
);

export function HandlerService() {
  // Observe handlersvc-json-replace so we can switch to the datasource
  Services.obs.addObserver(this, "handlersvc-json-replace", true);
}

HandlerService.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsISupportsWeakReference",
    "nsIHandlerService",
    "nsIObserver",
  ]),

  __store: null,
  get _store() {
    if (!this.__store) {
      this.__store = new lazy.JSONFile({
        path: PathUtils.join(
          Services.dirsvc.get("ProfD", Ci.nsIFile).path,
          "handlers.json"
        ),
        dataPostProcessor: this._dataPostProcessor.bind(this),
      });
    }

    // Always call this even if this.__store was set, since it may have been
    // set by asyncInit, which might not have completed yet.
    this._ensureStoreInitialized();
    return this.__store;
  },

  __storeInitialized: false,
  _ensureStoreInitialized() {
    if (!this.__storeInitialized) {
      this.__storeInitialized = true;
      this.__store.ensureDataReady();

      this._injectDefaultProtocolHandlersIfNeeded();
      this._migrateProtocolHandlersIfNeeded();

      Services.obs.notifyObservers(null, "handlersvc-store-initialized");

      // Bug 1736924: run migration for browser.download.improvements_to_download_panel if applicable.
      // Since we need DownloadsViewInternally to verify mimetypes, we run this after
      // DownloadsViewInternally is registered via the 'handlersvc-store-initialized' notification.
      this._migrateDownloadsImprovementsIfNeeded();
      this._migrateSVGXMLIfNeeded();
    }
  },

  _dataPostProcessor(data) {
    return data.defaultHandlersVersion
      ? data
      : {
          defaultHandlersVersion: {},
          mimeTypes: {},
          schemes: {},
          isDownloadsImprovementsAlreadyMigrated: false,
        };
  },

  /**
   * Injects new default protocol handlers if the version in the preferences is
   * newer than the one in the data store.
   */
  _injectDefaultProtocolHandlersIfNeeded() {
    try {
      let defaultHandlersVersion = Services.prefs.getIntPref(
        "gecko.handlerService.defaultHandlersVersion",
        0
      );
      if (defaultHandlersVersion < lazy.kHandlerListVersion) {
        this._injectDefaultProtocolHandlers();
        Services.prefs.setIntPref(
          "gecko.handlerService.defaultHandlersVersion",
          lazy.kHandlerListVersion
        );
        // Now save the result:
        this._store.saveSoon();
      }
    } catch (ex) {
      console.error(ex);
    }
  },

  _injectDefaultProtocolHandlers() {
    let locale = Services.locale.appLocaleAsBCP47;

    // Initialize handlers to default and update based on locale.
    let localeHandlers = lazy.kHandlerList.default;
    if (lazy.kHandlerList[locale]) {
      for (let scheme in lazy.kHandlerList[locale].schemes) {
        localeHandlers.schemes[scheme] =
          lazy.kHandlerList[locale].schemes[scheme];
      }
    }

    // Now, we're going to cheat. Terribly. The idiologically correct way
    // of implementing the following bit of code would be to fetch the
    // handler info objects from the protocol service, manipulate those,
    // and then store each of them.
    // However, that's expensive. It causes us to talk to the OS about
    // default apps, which causes the OS to go hit the disk.
    // All we're trying to do is insert some web apps into the list. We
    // don't care what's already in the file, we just want to do the
    // equivalent of appending into the database. So let's just go do that:
    for (let scheme of Object.keys(localeHandlers.schemes)) {
      if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") {
        // Thunderbird IS a mailto handler, it doesn't need handlers added.
        continue;
      }

      let existingSchemeInfo = this._store.data.schemes[scheme];
      if (!existingSchemeInfo) {
        // Haven't seen this scheme before. Default to asking which app the
        // user wants to use:
        existingSchemeInfo = {
          // Signal to future readers that we didn't ask the OS anything.
          // When the entry is first used, get the info from the OS.
          stubEntry: true,
          // The first item in the list is the preferred handler, and
          // there isn't one, so we fill in null:
          handlers: [null],
        };
        this._store.data.schemes[scheme] = existingSchemeInfo;
      }
      let { handlers } = existingSchemeInfo;
      for (let newHandler of localeHandlers.schemes[scheme].handlers) {
        // If there is already a handler registered with the same template
        // URL, ignore the new one:
        let matchingTemplate = handler =>
          handler && handler.uriTemplate == newHandler.uriTemplate;
        if (!handlers.some(matchingTemplate)) {
          handlers.push(newHandler);
        }
      }
    }
  },

  /**
   * Execute any migrations. Migrations are defined here for any changes or removals for
   * existing handlers. Additions are still handled via the localized prefs infrastructure.
   *
   * This depends on the browser.handlers.migrations pref being set by migrateUI in
   * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
   * This is a comma-separated list of identifiers of migrations that need running.
   * This avoids both re-running older migrations and keeping an additional
   * pref around permanently.
   */
  _migrateProtocolHandlersIfNeeded() {
    const kMigrations = {
      "30boxes": () => {
        const k30BoxesRegex =
          /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
        let webcalHandler =
          lazy.externalProtocolService.getProtocolHandlerInfo("webcal");
        if (this.exists(webcalHandler)) {
          this.fillHandlerInfo(webcalHandler, "");
          let shouldStore = false;
          // First remove 30boxes from possible handlers.
          let handlers = webcalHandler.possibleApplicationHandlers;
          for (let i = handlers.length - 1; i >= 0; i--) {
            let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
            if (
              app instanceof Ci.nsIWebHandlerApp &&
              k30BoxesRegex.test(app.uriTemplate)
            ) {
              shouldStore = true;
              handlers.removeElementAt(i);
            }
          }
          // Then remove as a preferred handler.
          if (webcalHandler.preferredApplicationHandler) {
            let app = webcalHandler.preferredApplicationHandler;
            if (
              app instanceof Ci.nsIWebHandlerApp &&
              k30BoxesRegex.test(app.uriTemplate)
            ) {
              webcalHandler.preferredApplicationHandler = null;
              shouldStore = true;
            }
          }
          // Then store, if we changed anything.
          if (shouldStore) {
            this.store(webcalHandler);
          }
        }
      },
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
      "secure-mail": () => {
        const kSubstitutions = new Map([
          [
            "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
            "https://mail.yahoo.co.jp/compose/?To=%s",
          ],
          [
            "http://www.inbox.lv/rfc2368/?value=%s",
            "https://mail.inbox.lv/compose?to=%s",
          ],
          [
            "http://poczta.interia.pl/mh/?mailto=%s",
            "https://poczta.interia.pl/mh/?mailto=%s",
          ],
          [
            "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
            "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
          ],
        ]);

        function maybeReplaceURL(app) {
          if (app instanceof Ci.nsIWebHandlerApp) {
            let { uriTemplate } = app;
            let sub = kSubstitutions.get(uriTemplate);
            if (sub) {
              app.uriTemplate = sub;
              return true;
            }
          }
          return false;
        }
        let mailHandler =
          lazy.externalProtocolService.getProtocolHandlerInfo("mailto");
        if (this.exists(mailHandler)) {
          this.fillHandlerInfo(mailHandler, "");
          let handlers = mailHandler.possibleApplicationHandlers;
          let shouldStore = false;
          for (let i = handlers.length - 1; i >= 0; i--) {
            let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
            // Note: will evaluate the RHS because it's a binary rather than
            // logical or.
            shouldStore |= maybeReplaceURL(app);
          }
          // Then check the preferred handler.
          if (mailHandler.preferredApplicationHandler) {
            shouldStore |= maybeReplaceURL(
              mailHandler.preferredApplicationHandler
            );
          }
          // Then store, if we changed anything. Note that store() handles
          // duplicates, so we don't have to.
          if (shouldStore) {
            this.store(mailHandler);
          }
        }
      },
    };
    let migrationsToRun = Services.prefs.getCharPref(
      "browser.handlers.migrations",
      ""
    );
    migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
    for (let migration of migrationsToRun) {
      migration.trim();
      try {
        kMigrations[migration]();
      } catch (ex) {
        console.error(ex);
      }
    }

    if (migrationsToRun.length) {
      Services.prefs.clearUserPref("browser.handlers.migrations");
    }
  },

  _onDBChange() {
    return (async () => {
      if (this.__store) {
        await this.__store.finalize();
      }
      this.__store = null;
      this.__storeInitialized = false;
    })().catch(console.error);
  },

  // nsIObserver
  observe(subject, topic, data) {
    if (topic != "handlersvc-json-replace") {
      return;
    }
    let promise = this._onDBChange();
    promise.then(() => {
      Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
    });
  },

  // nsIHandlerService
  asyncInit() {
    if (!this.__store) {
      this.__store = new lazy.JSONFile({
        path: PathUtils.join(
          Services.dirsvc.get("ProfD", Ci.nsIFile).path,
          "handlers.json"
        ),
        dataPostProcessor: this._dataPostProcessor.bind(this),
      });
      this.__store
        .load()
        .then(() => {
          // __store can be null if we called _onDBChange in the mean time.
          if (this.__store) {
            this._ensureStoreInitialized();
          }
        })
        .catch(console.error);
    }
  },

  /**
   * Update already existing handlers for non-internal mimetypes to have prefs set from alwaysAsk
   * to saveToDisk. However, if reading an internal mimetype and set to alwaysAsk, update to use handleInternally.
   * This migration is needed since browser.download.improvements_to_download_panel does not
   * override user preferences if preferredAction = alwaysAsk. By doing so, we can ensure that file prompt
   * behaviours remain consistent for most files.
   *
   * See Bug 1736924 for more information.
   */
  _noInternalHandlingDefault: new Set([
    "text/xml",
    "application/xml",
    "image/svg+xml",
  ]),
  _migrateDownloadsImprovementsIfNeeded() {
    // Migrate if the migration has never been run before.
    // Otherwise, we risk overwriting preferences for existing profiles!
    if (
      Services.prefs.getBoolPref(
        "browser.download.improvements_to_download_panel",
        true
      ) &&
      !Services.policies?.getActivePolicies()?.Handlers &&
      !this._store.data.isDownloadsImprovementsAlreadyMigrated &&
      AppConstants.MOZ_APP_NAME != "thunderbird"
    ) {
      for (let [type, mimeInfo] of Object.entries(this._store.data.mimeTypes)) {
        let isViewableInternally =
          lazy.DownloadIntegration.shouldViewDownloadInternally(type) &&
          !this._noInternalHandlingDefault.has(type);
        let isAskOnly = mimeInfo && mimeInfo.ask;

        if (isAskOnly) {
          if (isViewableInternally) {
            mimeInfo.action = handleInternally;
          } else {
            mimeInfo.action = saveToDisk;
          }

          // Sets alwaysAskBeforeHandling to false. Needed to ensure that:
          // preferredAction appears as expected in preferences table; and
          // downloads behaviour is updated to never show UCT window.
          mimeInfo.ask = false;
        }
      }

      this._store.data.isDownloadsImprovementsAlreadyMigrated = true;
      this._store.saveSoon();
    }
  },

  _migrateSVGXMLIfNeeded() {
    // Migrate if the migration has never been run before.
    // We need to make sure we only run this once.
    if (
      Services.prefs.getBoolPref(
        "browser.download.improvements_to_download_panel",
        true
      ) &&
      !Services.policies?.getActivePolicies()?.Handlers &&
      !this._store.data.isSVGXMLAlreadyMigrated
    ) {
      for (let type of this._noInternalHandlingDefault) {
        if (Object.hasOwn(this._store.data.mimeTypes, type)) {
          let mimeInfo = this._store.data.mimeTypes[type];
          if (!mimeInfo.ask && mimeInfo.action == handleInternally) {
            mimeInfo.action = saveToDisk;
          }
        }
      }

      this._store.data.isSVGXMLAlreadyMigrated = true;
      this._store.saveSoon();
    }
  },

  // nsIHandlerService
  enumerate() {
    let handlers = Cc["@mozilla.org/array;1"].createInstance(
      Ci.nsIMutableArray
    );
    for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
      let primaryExtension = typeInfo.extensions?.[0] ?? null;
      let handler = lazy.MIMEService.getFromTypeAndExtension(
        type,
        primaryExtension
      );
      handlers.appendElement(handler);
    }
    for (let type of Object.keys(this._store.data.schemes)) {
      // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
      // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
      // until one of its properties is accessed.
      //
      // Note: our caller still needs to yield periodically when iterating
      // the enumerator and accessing handler properties to avoid monopolizing
      // the main thread.
      //
      let handler = new Proxy(
        {
          QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
          type,
          get _handlerInfo() {
            delete this._handlerInfo;
            return (this._handlerInfo =
              lazy.externalProtocolService.getProtocolHandlerInfo(type));
          },
        },
        {
          get(target, name) {
            return target[name] || target._handlerInfo[name];
          },
          set(target, name, value) {
            target._handlerInfo[name] = value;
          },
        }
      );
      handlers.appendElement(handler);
    }
    return handlers.enumerate(Ci.nsIHandlerInfo);
  },

  // nsIHandlerService
  store(handlerInfo) {
    let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);

    // Retrieve an existing entry if present, instead of creating a new one, so
    // that we preserve unknown properties for forward compatibility.
    let storedHandlerInfo = handlerList[handlerInfo.type];
    if (!storedHandlerInfo) {
      storedHandlerInfo = {};
      handlerList[handlerInfo.type] = storedHandlerInfo;
    }

    // Only a limited number of preferredAction values is allowed.
    if (
      handlerInfo.preferredAction == saveToDisk ||
      handlerInfo.preferredAction == useSystemDefault ||
      handlerInfo.preferredAction == handleInternally ||
      // For files (ie mimetype rather than protocol handling info), ensure
      // we can store the "always ask" state, too:
      (handlerInfo.preferredAction == alwaysAsk &&
        this._isMIMEInfo(handlerInfo) &&
        Services.prefs.getBoolPref(
          "browser.download.improvements_to_download_panel",
          true
        ))
    ) {
      storedHandlerInfo.action = handlerInfo.preferredAction;
    } else {
      storedHandlerInfo.action = useHelperApp;
    }

    if (handlerInfo.alwaysAskBeforeHandling) {
      storedHandlerInfo.ask = true;
    } else {
      delete storedHandlerInfo.ask;
    }

    // Build a list of unique nsIHandlerInfo instances to process later.
    let handlers = [];
    if (handlerInfo.preferredApplicationHandler) {
      handlers.push(handlerInfo.preferredApplicationHandler);
    }
    for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
      Ci.nsIHandlerApp
    )) {
      // If the caller stored duplicate handlers, we save them only once.
      if (!handlers.some(h => h.equals(handler))) {
        handlers.push(handler);
      }
    }

    // If any of the nsIHandlerInfo instances cannot be serialized, it is not
    // included in the final list. The first element is always the preferred
    // handler, or null if there is none.
    let serializableHandlers = handlers
      .map(h => this.handlerAppToSerializable(h))
      .filter(h => h);
    if (serializableHandlers.length) {
      if (!handlerInfo.preferredApplicationHandler) {
        serializableHandlers.unshift(null);
      }
      storedHandlerInfo.handlers = serializableHandlers;
    } else {
      delete storedHandlerInfo.handlers;
    }

    if (this._isMIMEInfo(handlerInfo)) {
      let extensions = storedHandlerInfo.extensions || [];
      for (let extension of handlerInfo.getFileExtensions()) {
        extension = extension.toLowerCase();
        // If the caller stored duplicate extensions, we save them only once.
        if (!extensions.includes(extension)) {
          extensions.push(extension);
        }
      }
      if (extensions.length) {
        storedHandlerInfo.extensions = extensions;
      } else {
        delete storedHandlerInfo.extensions;
      }
    }

    // If we're saving *anything*, it stops being a stub:
    delete storedHandlerInfo.stubEntry;

    this._store.saveSoon();

    // Now notify PDF.js. This is hacky, but a lot better than expecting all
    // the consumers to do it...
    if (handlerInfo.type == "application/pdf") {
      Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
    }
  },

  // nsIHandlerService
  fillHandlerInfo(handlerInfo, overrideType) {
    let type = overrideType || handlerInfo.type;
    let storedHandlerInfo =
      this._getHandlerListByHandlerInfoType(handlerInfo)[type];
    if (!storedHandlerInfo) {
      throw new Components.Exception(
        "handlerSvc fillHandlerInfo: don't know this type",
        Cr.NS_ERROR_NOT_AVAILABLE
      );
    }

    let isStub = !!storedHandlerInfo.stubEntry;
    // In the normal case, this is not a stub, so we can just read stored info
    // and write to the handlerInfo object we were passed.
    if (!isStub) {
      handlerInfo.preferredAction = storedHandlerInfo.action;
      handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
    } else {
      // If we've got a stub, ensure the defaults are still set:
      lazy.externalProtocolService.setProtocolHandlerDefaults(
        handlerInfo,
        handlerInfo.hasDefaultHandler
      );
      if (
        handlerInfo.preferredAction == alwaysAsk &&
        handlerInfo.alwaysAskBeforeHandling
      ) {
        // `store` will default to `useHelperApp` because `alwaysAsk` is
        // not one of the 3 recognized options; for compatibility, do
        // the same here.
        handlerInfo.preferredAction = useHelperApp;
      }
    }
    // If it *is* a stub, don't override alwaysAskBeforeHandling or the
    // preferred actions. Instead, just append the stored handlers, without
    // overriding the preferred app, and then schedule a task to store proper
    // info for this handler.
    this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);

    if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
      for (let extension of storedHandlerInfo.extensions) {
        handlerInfo.appendExtension(extension);
      }
    } else if (this._mockedHandler) {
      this._insertMockedHandler(handlerInfo);
    }
  },

  /**
   * Private method to inject stored handler information into an nsIHandlerInfo
   * instance.
   * @param handlerInfo           the nsIHandlerInfo instance to write to
   * @param storedHandlers        the stored handlers
   * @param keepPreferredApp      whether to keep the handlerInfo's
   *                              preferredApplicationHandler or override it
   *                              (default: false, ie override it)
   */
  _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
    // If the first item is not null, it is also the preferred handler. Since
    // we cannot modify the stored array, use a boolean to keep track of this.
    let isFirstItem = true;
    for (let handler of storedHandlers || [null]) {
      let handlerApp = this.handlerAppFromSerializable(handler || {});
      if (isFirstItem) {
        isFirstItem = false;
        // Do not overwrite the preferred app unless that's allowed
        if (!keepPreferredApp) {
          handlerInfo.preferredApplicationHandler = handlerApp;
        }
      }
      if (handlerApp) {
        handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
      }
    }
  },

  /**
   * @param handler
   *        A nsIHandlerApp handler app
   * @returns  Serializable representation of a handler app object.
   */
  handlerAppToSerializable(handler) {
    if (handler instanceof Ci.nsILocalHandlerApp) {
      return {
        name: handler.name,
        path: handler.executable.path,
      };
    } else if (handler instanceof Ci.nsIWebHandlerApp) {
      return {
        name: handler.name,
        uriTemplate: handler.uriTemplate,
      };
    } else if (handler instanceof Ci.nsIDBusHandlerApp) {
      return {
        name: handler.name,
        service: handler.service,
        method: handler.method,
        objectPath: handler.objectPath,
        dBusInterface: handler.dBusInterface,
      };
    } else if (handler instanceof Ci.nsIGIOMimeApp) {
      return {
        name: handler.name,
        command: handler.command,
      };
    }
    // If the handler is an unknown handler type, return null.
    // Android default application handler is the case.
    return null;
  },

  /**
   * @param handlerObj
   *        Serializable representation of a handler object.
   * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
   */
  handlerAppFromSerializable(handlerObj) {
    let handlerApp;
    if ("path" in handlerObj) {
      try {
        let file = new lazy.FileUtils.File(handlerObj.path);
        if (!file.exists()) {
          return null;
        }
        handlerApp = Cc[
          "@mozilla.org/uriloader/local-handler-app;1"
        ].createInstance(Ci.nsILocalHandlerApp);
        handlerApp.executable = file;
      } catch (ex) {
        return null;
      }
    } else if ("uriTemplate" in handlerObj) {
      handlerApp = Cc[
        "@mozilla.org/uriloader/web-handler-app;1"
      ].createInstance(Ci.nsIWebHandlerApp);
      handlerApp.uriTemplate = handlerObj.uriTemplate;
    } else if ("service" in handlerObj) {
      handlerApp = Cc[
        "@mozilla.org/uriloader/dbus-handler-app;1"
      ].createInstance(Ci.nsIDBusHandlerApp);
      handlerApp.service = handlerObj.service;
      handlerApp.method = handlerObj.method;
      handlerApp.objectPath = handlerObj.objectPath;
      handlerApp.dBusInterface = handlerObj.dBusInterface;
    } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
      try {
        handlerApp = Cc["@mozilla.org/gio-service;1"]
          .getService(Ci.nsIGIOService)
          .createAppFromCommand(handlerObj.command, handlerObj.name);
      } catch (ex) {
        return null;
      }
    } else {
      return null;
    }

    handlerApp.name = handlerObj.name;
    return handlerApp;
  },

  /**
   * The function returns a reference to the "mimeTypes" or "schemes" object
   * based on which type of handlerInfo is provided.
   */
  _getHandlerListByHandlerInfoType(handlerInfo) {
    return this._isMIMEInfo(handlerInfo)
      ? this._store.data.mimeTypes
      : this._store.data.schemes;
  },

  /**
   * Determines whether an nsIHandlerInfo instance represents a MIME type.
   */
  _isMIMEInfo(handlerInfo) {
    // We cannot rely only on the instanceof check because on Android both MIME
    // types and protocols are instances of nsIMIMEInfo. We still do the check
    // so that properties of nsIMIMEInfo become available to the callers.
    return (
      handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
    );
  },

  // nsIHandlerService
  exists(handlerInfo) {
    return (
      handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
    );
  },

  // nsIHandlerService
  remove(handlerInfo) {
    delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
    this._store.saveSoon();
  },

  // nsIHandlerService
  getTypeFromExtension(fileExtension) {
    let extension = fileExtension.toLowerCase();
    let mimeTypes = this._store.data.mimeTypes;
    for (let type of Object.keys(mimeTypes)) {
      if (
        mimeTypes[type].extensions &&
        mimeTypes[type].extensions.includes(extension)
      ) {
        return type;
      }
    }
    return "";
  },

  _mockedHandler: null,
  _mockedProtocol: null,

  _insertMockedHandler(handlerInfo) {
    if (handlerInfo.type == this._mockedProtocol) {
      handlerInfo.preferredApplicationHandler = this._mockedHandler;
      handlerInfo.possibleApplicationHandlers.insertElementAt(
        this._mockedHandler,
        0
      );
    }
  },

  // test-only: mock the handler instance for a particular protocol/scheme
  mockProtocolHandler(protocol) {
    if (!protocol) {
      this._mockedProtocol = null;
      this._mockedHandler = null;
      return;
    }
    this._mockedProtocol = protocol;
    this._mockedHandler = {
      QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
      launchWithURI(uri, context) {
        Services.obs.notifyObservers(uri, "mocked-protocol-handler");
      },
      name: "Mocked handler",
      detailedDescription: "Mocked handler for tests",
      equals(x) {
        return x == this;
      },
      get executable() {
        if (AppConstants.platform == "macosx") {
          // We need an app path that isn't us, nor in our app bundle, and
          // Apple no longer allows us to read the default-shipped apps
          // in /Applications/ - except for Safari, it would appear!
          let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
          f.initWithPath("/Applications/Safari.app");
          return f;
        }
        return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
      },
      parameterCount: 0,
      clearParameters() {},
      appendParameter() {},
      getParameter() {},
      parameterExists() {
        return false;
      },
    };
  },
};