Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / components / protocolhandler / WebProtocolHandlerRegistrar.sys.mjs
blob927685c83c4f4a41755c59626eb1cf91c533c8f7
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties";
8 export function WebProtocolHandlerRegistrar() {}
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
14 });
16 ChromeUtils.defineLazyGetter(lazy, "log", () => {
17   let { ConsoleAPI } = ChromeUtils.importESModule(
18     "resource://gre/modules/Console.sys.mjs"
19   );
20   let consoleOptions = {
21     // tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
22     // detailed messages during development. See LOG_LEVELS in Console.sys.mjs
23     // for details.
24     maxLogLevel: "warning",
25     maxLogLevelPref: "browser.protocolhandler.loglevel",
26     prefix: "WebProtocolHandlerRegistrar.sys.mjs",
27   };
28   return new ConsoleAPI(consoleOptions);
29 });
31 WebProtocolHandlerRegistrar.prototype = {
32   get stringBundle() {
33     let sb = Services.strings.createBundle(STRING_BUNDLE_URI);
34     delete WebProtocolHandlerRegistrar.prototype.stringBundle;
35     return (WebProtocolHandlerRegistrar.prototype.stringBundle = sb);
36   },
38   _getFormattedString(key, params) {
39     return this.stringBundle.formatStringFromName(key, params);
40   },
42   _getString(key) {
43     return this.stringBundle.GetStringFromName(key);
44   },
46   /**
47    * See nsIWebProtocolHandlerRegistrar
48    */
49   removeProtocolHandler(aProtocol, aURITemplate) {
50     let eps = Cc[
51       "@mozilla.org/uriloader/external-protocol-service;1"
52     ].getService(Ci.nsIExternalProtocolService);
53     let handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
54     let handlers = handlerInfo.possibleApplicationHandlers;
55     for (let i = 0; i < handlers.length; i++) {
56       try {
57         // We only want to test web handlers
58         let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
59         if (handler.uriTemplate == aURITemplate) {
60           handlers.removeElementAt(i);
61           let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
62             Ci.nsIHandlerService
63           );
64           hs.store(handlerInfo);
65           return;
66         }
67       } catch (e) {
68         /* it wasn't a web handler */
69       }
70     }
71   },
73   /**
74    * Determines if a web handler is already registered.
75    *
76    * @param {string} aProtocol
77    *        The scheme of the web handler we are checking for.
78    * @param {string} aURITemplate
79    *        The URI template that the handler uses to handle the protocol.
80    * @returns {boolean} true if it is already registered, false otherwise.
81    */
82   _protocolHandlerRegistered(aProtocol, aURITemplate) {
83     let eps = Cc[
84       "@mozilla.org/uriloader/external-protocol-service;1"
85     ].getService(Ci.nsIExternalProtocolService);
86     let handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
87     let handlers = handlerInfo.possibleApplicationHandlers;
88     for (let i = 0; i < handlers.length; i++) {
89       try {
90         // We only want to test web handlers
91         let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
92         if (handler.uriTemplate == aURITemplate) {
93           return true;
94         }
95       } catch (e) {
96         /* it wasn't a web handler */
97         lazy.log.debug("No protocolHandler registered, because: " + e.message);
98       }
99     }
100     return false;
101   },
103   /**
104    * Private method to return the installHash, which is important for app
105    * registration on OS level. Without it apps cannot be default/handler apps
106    * under Windows. Because this value is used to check if its possible to reset
107    * the default and to actually set it as well, this function puts the
108    * acquisition of the installHash in one place in the hopes that check and set
109    * conditions will never deviate.
110    *
111    * @returns {string} installHash
112    */
113   _getInstallHash() {
114     const xreDirProvider = Cc[
115       "@mozilla.org/xre/directory-provider;1"
116     ].getService(Ci.nsIXREDirProvider);
117     return xreDirProvider.getInstallHash();
118   },
120   /**
121    * Private method to determine if we can set a new OS default for a certain
122    * protocol.
123    *
124    * @param {string} protocol name, e.g. mailto (without ://)
125    * @returns {boolean}
126    */
127   _canSetOSDefault(protocol) {
128     // can be toggled off individually if necessary...
129     if (!lazy.NimbusFeatures.mailto.getVariable("dualPrompt.os")) {
130       lazy.log.debug("_canSetOSDefault: false: mailto rollout deactivated.");
131       return false;
132     }
134     // this preferences saves that the user has dismissed the bar before...
135     if (!Services.prefs.getBoolPref("browser.mailto.prompt.os", true)) {
136       lazy.log.debug("_canSetOSDefault: false: prompt dismissed before.");
137       return false;
138     }
140     // an installHash is required for the association with a scheme handler
141     if ("" == this._getInstallHash()) {
142       lazy.log.debug("_canSetOSDefault: false: no installation hash.");
143       return false;
144     }
146     // check if we are already the protocolhandler...
147     let shellService = Cc[
148       "@mozilla.org/browser/shell-service;1"
149     ].createInstance(Ci.nsIWindowsShellService);
151     if (shellService.isDefaultHandlerFor(protocol)) {
152       lazy.log.debug("_canSetOSDefault: false: is already default handler.");
153       return false;
154     }
156     return true;
157   },
159   /**
160    * Private method to reset the OS default for a certain protocol/uri scheme.
161    * We basically ignore, that setDefaultExtensionHandlersUserChoice can fail
162    * when the installHash is wrong or cannot be determined.
163    *
164    * @param {string} protocol name, e.g. mailto (without ://)
165    * @returns {boolean}
166    */
167   _setOSDefault(protocol) {
168     try {
169       let defaultAgent = Cc["@mozilla.org/default-agent;1"].createInstance(
170         Ci.nsIDefaultAgent
171       );
172       defaultAgent.setDefaultExtensionHandlersUserChoice(
173         this._getInstallHash(),
174         [protocol, "FirefoxURL"]
175       );
176       return true;
177     } catch (e) {
178       // TODO: why could not we just add the installHash and promote the running
179       // install to be a properly installed one?
180       lazy.log.debug(
181         "Could not set Firefox as default application for " +
182           protocol +
183           ", because: " +
184           e.message
185       );
186     }
187     return false;
188   },
190   /**
191    * Private method to set the default uri to handle a certain protocol. This
192    * automates in a way what a user can do in settings under applications,
193    * where different 'actions' can be chosen for different 'content types'.
194    *
195    * @param {string} protocol
196    * @param {handler} handler
197    */
198   _setLocalDefault(protocol, handler) {
199     let eps = Cc[
200       "@mozilla.org/uriloader/external-protocol-service;1"
201     ].getService(Ci.nsIExternalProtocolService);
203     let handlerInfo = eps.getProtocolHandlerInfo(protocol);
204     handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; // this is IMPORTANT!
205     handlerInfo.preferredApplicationHandler = handler;
206     handlerInfo.alwaysAskBeforeHandling = false;
207     let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
208       Ci.nsIHandlerService
209     );
210     hs.store(handlerInfo);
211   },
213   /**
214    * Private method to set the default uri to handle a certain protocol. This
215    * automates in a way what a user can do in settings under applications,
216    * where different 'actions' can be chosen for different 'content types'.
217    *
218    * @param {string} protocol - e.g. 'mailto', so again without ://
219    * @param {string} name - the protocol associated 'Action'
220    * @param {string} uri - the uri (compare 'use other...' in the preferences)
221    * @returns {handler} handler - either the existing one or a newly created
222    */
223   _addLocal(protocol, name, uri) {
224     let eps = Cc[
225       "@mozilla.org/uriloader/external-protocol-service;1"
226     ].getService(Ci.nsIExternalProtocolService);
228     let phi = eps.getProtocolHandlerInfo(protocol);
229     // not adding duplicates and bail out with the existing entry
230     for (let h of phi.possibleApplicationHandlers.enumerate()) {
231       if (h.uriTemplate == uri) {
232         return h;
233       }
234     }
236     let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
237       Ci.nsIWebHandlerApp
238     );
239     handler.name = name;
240     handler.uriTemplate = uri;
242     let handlerInfo = eps.getProtocolHandlerInfo(protocol);
243     handlerInfo.possibleApplicationHandlers.appendElement(handler);
245     // Since the user has agreed to add a new handler, chances are good
246     // that the next time they see a handler of this type, they're going
247     // to want to use it.  Reset the handlerInfo to ask before the next
248     // use.
249     handlerInfo.alwaysAskBeforeHandling = true;
251     let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
252       Ci.nsIHandlerService
253     );
254     hs.store(handlerInfo);
256     return handler;
257   },
259   /**
260    * See nsIWebProtocolHandlerRegistrar
261    */
262   registerProtocolHandler(
263     aProtocol,
264     aURI,
265     aTitle,
266     aDocumentURI,
267     aBrowserOrWindow
268   ) {
269     // first mitigation: check if the API call comes from another domain
270     aProtocol = (aProtocol || "").toLowerCase();
271     if (!aURI || !aDocumentURI) {
272       return;
273     }
275     let browser = aBrowserOrWindow; // This is the e10s case.
276     if (aBrowserOrWindow instanceof Ci.nsIDOMWindow) {
277       // In the non-e10s case, grab the browser off the same-process window.
278       let rootDocShell = aBrowserOrWindow.docShell.sameTypeRootTreeItem;
279       browser = rootDocShell.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
280     }
282     let browserWindow = browser.ownerGlobal;
283     try {
284       browserWindow.navigator.checkProtocolHandlerAllowed(
285         aProtocol,
286         aURI,
287         aDocumentURI
288       );
289     } catch (ex) {
290       // We should have already shown the user an error.
291       return;
292     }
293     if (lazy.NimbusFeatures.mailto.getVariable("dualPrompt")) {
294       if ("mailto" === aProtocol) {
295         this._askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle);
296         return;
297       }
298     }
300     // If the protocol handler is already registered, just return early.
301     if (this._protocolHandlerRegistered(aProtocol, aURI.spec)) {
302       return;
303     }
305     // Now Ask the user and provide the proper callback
306     let message = this._getFormattedString("addProtocolHandlerMessage", [
307       aURI.host,
308       aProtocol,
309     ]);
311     let notificationIcon = aURI.prePath + "/favicon.ico";
312     let notificationValue = "Protocol Registration: " + aProtocol;
313     let addButton = {
314       label: this._getString("addProtocolHandlerAddButton"),
315       accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"),
316       protocolInfo: { protocol: aProtocol, uri: aURI.spec, name: aTitle },
318       callback(aNotification, aButtonInfo) {
319         let protocol = aButtonInfo.protocolInfo.protocol;
320         let name = aButtonInfo.protocolInfo.name;
322         let handler = Cc[
323           "@mozilla.org/uriloader/web-handler-app;1"
324         ].createInstance(Ci.nsIWebHandlerApp);
325         handler.name = name;
326         handler.uriTemplate = aButtonInfo.protocolInfo.uri;
328         let eps = Cc[
329           "@mozilla.org/uriloader/external-protocol-service;1"
330         ].getService(Ci.nsIExternalProtocolService);
331         let handlerInfo = eps.getProtocolHandlerInfo(protocol);
332         handlerInfo.possibleApplicationHandlers.appendElement(handler);
334         // Since the user has agreed to add a new handler, chances are good
335         // that the next time they see a handler of this type, they're going
336         // to want to use it.  Reset the handlerInfo to ask before the next
337         // use.
338         handlerInfo.alwaysAskBeforeHandling = true;
340         let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
341           Ci.nsIHandlerService
342         );
343         hs.store(handlerInfo);
344       },
345     };
347     let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
349     // check if the notification box is already shown
350     if (notificationBox.getNotificationWithValue(notificationValue)) {
351       return;
352     }
354     notificationBox.appendNotification(
355       notificationValue,
356       {
357         label: message,
358         image: notificationIcon,
359         priority: notificationBox.PRIORITY_INFO_LOW,
360       },
361       [addButton]
362     );
363   },
365   /*
366    * Special implementation for mailto
367    *
368    * @param {string} browser
369    * @param {string} aProtocol
370    * @param {string} aURI
371    * @param {string} aTitle
372    */
373   async _askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle) {
374     // shortcut for Localization
375     let l10n = new Localization([
376       "branding/brand.ftl",
377       "browser/webProtocolHandler.ftl",
378     ]);
379     let [
380       msg_os_box,
381       msg_os_yes_confirm,
382       msg_os_yes,
383       msg_os_no,
384       msg_box,
385       msg_yes_confirm,
386       msg_yes,
387       msg_no,
388     ] = await l10n.formatValues([
389       { id: "protocolhandler-mailto-os-handler-notificationbox" },
390       { id: "protocolhandler-mailto-os-handler-yes-confirm" },
391       { id: "protocolhandler-mailto-os-handler-yes-button" },
392       { id: "protocolhandler-mailto-os-handler-no-button" },
393       {
394         id: "protocolhandler-mailto-handler-notificationbox-always",
395         args: { url: aURI.prePath },
396       },
397       {
398         id: "protocolhandler-mailto-handler-yes-confirm",
399         args: { url: aURI.prePath },
400       },
401       { id: "protocolhandler-mailto-handler-yes-button" },
402       { id: "protocolhandler-mailto-handler-no-button" },
403     ]);
405     // First prompt:
406     // Only shown if there is a realistic chance that we can really set the OS
407     // default and can also be disabled with a preference or experiement
408     if (this._canSetOSDefault(aProtocol)) {
409       // Only show if not already set and if we have been properly installed
410       let notificationId = "OS Protocol Registration: " + aProtocol;
411       let osDefaultNotificationBox = browser
412         .getTabBrowser()
413         .getNotificationBox(browser);
414       if (!osDefaultNotificationBox.getNotificationWithValue(notificationId)) {
415         osDefaultNotificationBox.appendNotification(
416           notificationId,
417           {
418             label: msg_os_box,
419             priority: osDefaultNotificationBox.PRIORITY_INFO_LOW,
420           },
421           [
422             {
423               label: msg_os_yes,
424               callback: () => {
425                 this._setOSDefault(aProtocol);
426                 Glean.protocolhandlerMailto.promptClicked.set_os_default.add();
427                 osDefaultNotificationBox.appendNotification(
428                   notificationId,
429                   {
430                     label: msg_os_yes_confirm,
431                     priority: osDefaultNotificationBox.PRIORITY_INFO_LOW,
432                   },
433                   []
434                 );
435                 return false;
436               },
437             },
438             {
439               label: msg_os_no,
440               callback: () => {
441                 Services.prefs.setBoolPref("browser.mailto.prompt.os", false);
442                 Glean.protocolhandlerMailto.promptClicked.dismiss_os_default.add();
443                 return false;
444               },
445             },
446           ]
447         );
449         Glean.protocolhandlerMailto.handlerPromptShown.os_default.add();
450       }
451     }
453     // Second prompt:
454     // Only shown if the protocol handler is not already registered
455     if (!this._protocolHandlerRegistered(aProtocol, aURI.spec)) {
456       let notificationId = "Protocol Registration: " + aProtocol;
457       let FxDefaultNotificationBox = browser
458         .getTabBrowser()
459         .getNotificationBox(browser);
460       if (!FxDefaultNotificationBox.getNotificationWithValue(notificationId)) {
461         FxDefaultNotificationBox.appendNotification(
462           notificationId,
463           {
464             label: msg_box,
465             priority: FxDefaultNotificationBox.PRIORITY_INFO_LOW,
466           },
467           [
468             {
469               label: msg_yes,
470               callback: () => {
471                 this._setLocalDefault(
472                   aProtocol,
473                   this._addLocal(aProtocol, aTitle, aURI.spec)
474                 );
475                 Glean.protocolhandlerMailto.promptClicked.set_local_default.add();
476                 FxDefaultNotificationBox.appendNotification(
477                   notificationId,
478                   {
479                     label: msg_yes_confirm,
480                     priority: FxDefaultNotificationBox.PRIORITY_INFO_LOW,
481                   },
482                   []
483                 );
484                 return false;
485               },
486             },
487             {
488               label: msg_no,
489               callback: () => {
490                 Glean.protocolhandlerMailto.promptClicked.dismiss_local_default.add();
491                 return false;
492               },
493             },
494           ]
495         );
497         Glean.protocolhandlerMailto.handlerPromptShown.fx_default.add();
498       }
499     }
500   },
502   /**
503    * See nsISupports
504    */
505   QueryInterface: ChromeUtils.generateQI(["nsIWebProtocolHandlerRegistrar"]),