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"
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(
18 "resource://gre/modules/FileUtils.jsm"
20 ChromeUtils.defineModuleGetter(
23 "resource://gre/modules/JSONFile.jsm"
26 XPCOMUtils.defineLazyServiceGetter(
28 "gExternalProtocolService",
29 "@mozilla.org/uriloader/external-protocol-service;1",
30 "nsIExternalProtocolService"
32 XPCOMUtils.defineLazyServiceGetter(
35 "@mozilla.org/mime;1",
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",
55 this.__store = new JSONFile({
57 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
60 dataPostProcessor: this._dataPostProcessor.bind(this),
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();
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");
83 _dataPostProcessor(data) {
84 return data.defaultHandlersVersion
87 defaultHandlersVersion: {},
94 * Injects new default protocol handlers if the version in the preferences is
95 * newer than the one in the data store.
97 _injectDefaultProtocolHandlersIfNeeded() {
98 let prefsDefaultHandlersVersion;
100 prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
101 "gecko.handlerService.defaultHandlersVersion",
102 Ci.nsIPrefLocalizedString
106 ex instanceof Components.Exception &&
107 ex.result == Cr.NS_ERROR_UNEXPECTED
109 // This platform does not have any default protocol handlers configured.
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[
125 ] = prefsDefaultHandlersVersion;
126 // Now save the result:
127 this._store.saveSoon();
134 _injectDefaultProtocolHandlers() {
135 let schemesPrefBranch = Services.prefs.getBranch(
136 "gecko.handlerService.schemes."
138 let schemePrefList = schemesPrefBranch.getChildList("");
142 // read all the scheme prefs into a hash
143 for (let schemePrefName of schemePrefList) {
144 let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
147 let attrData = schemesPrefBranch.getComplexValue(
149 Ci.nsIPrefLocalizedString
151 if (!(scheme in schemes)) {
152 schemes[scheme] = {};
155 if (!(handlerNumber in schemes[scheme])) {
156 schemes[scheme][handlerNumber] = {};
159 schemes[scheme][handlerNumber][attribute] = attrData;
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) {
172 !obj.uriTemplate.startsWith("https://") ||
173 !obj.uriTemplate.toLowerCase().includes("%s")
175 delete handlerObject[key];
180 if (!validHandlers) {
181 delete schemes[scheme];
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.
203 // The first item in the list is the preferred handler, and
204 // there isn't one, so we fill in null:
207 this._store.data.schemes[scheme] = existingSchemeInfo;
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);
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.
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.
233 _migrateProtocolHandlersIfNeeded() {
234 const kMigrations = {
236 const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
237 let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo(
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);
248 app instanceof Ci.nsIWebHandlerApp &&
249 k30BoxesRegex.test(app.uriTemplate)
252 handlers.removeElementAt(i);
255 // Then remove as a preferred handler.
256 if (webcalHandler.preferredApplicationHandler) {
257 let app = webcalHandler.preferredApplicationHandler;
259 app instanceof Ci.nsIWebHandlerApp &&
260 k30BoxesRegex.test(app.uriTemplate)
262 webcalHandler.preferredApplicationHandler = null;
266 // Then store, if we changed anything.
268 this.store(webcalHandler);
272 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
273 "secure-mail": () => {
274 const kSubstitutions = new Map([
276 "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
277 "https://mail.yahoo.co.jp/compose/?To=%s",
280 "http://www.inbox.lv/rfc2368/?value=%s",
281 "https://mail.inbox.lv/compose?to=%s",
284 "http://poczta.interia.pl/mh/?mailto=%s",
285 "https://poczta.interia.pl/mh/?mailto=%s",
288 "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
289 "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
293 function maybeReplaceURL(app) {
294 if (app instanceof Ci.nsIWebHandlerApp) {
295 let { uriTemplate } = app;
296 let sub = kSubstitutions.get(uriTemplate);
298 app.uriTemplate = sub;
304 let mailHandler = gExternalProtocolService.getProtocolHandlerInfo(
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
315 shouldStore |= maybeReplaceURL(app);
317 // Then check the preferred handler.
318 if (mailHandler.preferredApplicationHandler) {
319 shouldStore |= maybeReplaceURL(
320 mailHandler.preferredApplicationHandler
323 // Then store, if we changed anything. Note that store() handles
324 // duplicates, so we don't have to.
326 this.store(mailHandler);
331 let migrationsToRun = Services.prefs.getCharPref(
332 "browser.handlers.migrations",
335 migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
336 for (let migration of migrationsToRun) {
339 kMigrations[migration]();
345 if (migrationsToRun.length) {
346 Services.prefs.clearUserPref("browser.handlers.migrations");
351 return (async () => {
353 await this.__store.finalize();
356 this.__storeInitialized = false;
357 })().catch(Cu.reportError);
361 observe(subject, topic, data) {
362 if (topic != "handlersvc-json-replace") {
365 let promise = this._onDBChange();
367 Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
374 this.__store = new JSONFile({
375 path: PathUtils.join(
376 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
379 dataPostProcessor: this._dataPostProcessor.bind(this),
384 // __store can be null if we called _onDBChange in the mean time.
386 this._ensureStoreInitialized();
389 .catch(Cu.reportError);
395 let handlers = Cc["@mozilla.org/array;1"].createInstance(
398 for (let type of Object.keys(this._store.data.mimeTypes)) {
399 let handler = gMIMEService.getFromTypeAndExtension(type, null);
400 handlers.appendElement(handler);
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.
407 // Note: our caller still needs to yield periodically when iterating
408 // the enumerator and accessing handler properties to avoid monopolizing
411 let handler = new Proxy(
413 QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
416 delete this._handlerInfo;
417 return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo(
424 return target[name] || target._handlerInfo[name];
426 set(target, name, value) {
427 target._handlerInfo[name] = value;
431 handlers.appendElement(handler);
433 return handlers.enumerate(Ci.nsIHandlerInfo);
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;
448 // Only a limited number of preferredAction values is allowed.
450 handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk ||
451 handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault ||
452 handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally
454 storedHandlerInfo.action = handlerInfo.preferredAction;
456 storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp;
459 if (handlerInfo.alwaysAskBeforeHandling) {
460 storedHandlerInfo.ask = true;
462 delete storedHandlerInfo.ask;
465 // Build a list of unique nsIHandlerInfo instances to process later.
467 if (handlerInfo.preferredApplicationHandler) {
468 handlers.push(handlerInfo.preferredApplicationHandler);
470 for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
473 // If the caller stored duplicate handlers, we save them only once.
474 if (!handlers.some(h => h.equals(handler))) {
475 handlers.push(handler);
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))
485 if (serializableHandlers.length) {
486 if (!handlerInfo.preferredApplicationHandler) {
487 serializableHandlers.unshift(null);
489 storedHandlerInfo.handlers = serializableHandlers;
491 delete storedHandlerInfo.handlers;
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);
503 if (extensions.length) {
504 storedHandlerInfo.extensions = extensions;
506 delete storedHandlerInfo.extensions;
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);
523 fillHandlerInfo(handlerInfo, overrideType) {
524 let type = overrideType || handlerInfo.type;
525 let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[
528 if (!storedHandlerInfo) {
529 throw new Components.Exception(
530 "handlerSvc fillHandlerInfo: don't know this type",
531 Cr.NS_ERROR_NOT_AVAILABLE
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.
539 handlerInfo.preferredAction = storedHandlerInfo.action;
540 handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
542 // If we've got a stub, ensure the defaults are still set:
543 gExternalProtocolService.setProtocolHandlerDefaults(
545 handlerInfo.hasDefaultHandler
548 handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk &&
549 handlerInfo.alwaysAskBeforeHandling
551 // `store` will default to `useHelperApp` because `alwaysAsk` is
552 // not one of the 3 recognized options; for compatibility, do
554 handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
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);
571 * Private method to inject stored handler information into an nsIHandlerInfo
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)
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 || {});
587 // Do not overwrite the preferred app unless that's allowed
588 if (!keepPreferredApp) {
589 handlerInfo.preferredApplicationHandler = handlerApp;
593 handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
600 * A nsIHandlerApp handler app
601 * @returns Serializable representation of a handler app object.
603 handlerAppToSerializable(handler) {
604 if (handler instanceof Ci.nsILocalHandlerApp) {
607 path: handler.executable.path,
609 } else if (handler instanceof Ci.nsIWebHandlerApp) {
612 uriTemplate: handler.uriTemplate,
614 } else if (handler instanceof Ci.nsIDBusHandlerApp) {
617 service: handler.service,
618 method: handler.method,
619 objectPath: handler.objectPath,
620 dBusInterface: handler.dBusInterface,
622 } else if (handler instanceof Ci.nsIGIOMimeApp) {
625 command: handler.command,
628 // If the handler is an unknown handler type, return null.
629 // Android default application handler is the case.
635 * Serializable representation of a handler object.
636 * @returns {nsIHandlerApp} the handler app, if any; otherwise null
638 handlerAppFromSerializable(handlerObj) {
640 if ("path" in handlerObj) {
642 let file = new FileUtils.File(handlerObj.path);
643 if (!file.exists()) {
647 "@mozilla.org/uriloader/local-handler-app;1"
648 ].createInstance(Ci.nsILocalHandlerApp);
649 handlerApp.executable = file;
653 } else if ("uriTemplate" in handlerObj) {
655 "@mozilla.org/uriloader/web-handler-app;1"
656 ].createInstance(Ci.nsIWebHandlerApp);
657 handlerApp.uriTemplate = handlerObj.uriTemplate;
658 } else if ("service" in handlerObj) {
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) {
668 handlerApp = Cc["@mozilla.org/gio-service;1"]
669 .getService(Ci.nsIGIOService)
670 .createAppFromCommand(handlerObj.command, handlerObj.name);
678 handlerApp.name = handlerObj.name;
683 * The function returns a reference to the "mimeTypes" or "schemes" object
684 * based on which type of handlerInfo is provided.
686 _getHandlerListByHandlerInfoType(handlerInfo) {
687 return this._isMIMEInfo(handlerInfo)
688 ? this._store.data.mimeTypes
689 : this._store.data.schemes;
693 * Determines whether an nsIHandlerInfo instance represents a MIME type.
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.
700 handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
705 exists(handlerInfo) {
707 handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
712 remove(handlerInfo) {
713 delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
714 this._store.saveSoon();
718 getTypeFromExtension(fileExtension) {
719 let extension = fileExtension.toLowerCase();
720 let mimeTypes = this._store.data.mimeTypes;
721 for (let type of Object.keys(mimeTypes)) {
723 mimeTypes[type].extensions &&
724 mimeTypes[type].extensions.includes(extension)
733 this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);