Bug 1776444 [wpt PR 34582] - Revert "Add TimedHTMLParserBudget to fieldtrial_testing_...
[gecko.git] / dom / browser-element / BrowserElementPromptService.jsm
blob628de0730ed6c19371fe03015af7a20e3d4f8dc9
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 /* vim: set ft=javascript : */
6 "use strict";
8 var Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
10 var EXPORTED_SYMBOLS = ["BrowserElementPromptService"];
12 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 function debug(msg) {
15   // dump("BrowserElementPromptService - " + msg + "\n");
18 function BrowserElementPrompt(win, browserElementChild) {
19   this._win = win;
20   this._browserElementChild = browserElementChild;
23 BrowserElementPrompt.prototype = {
24   QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
26   alert(title, text) {
27     this._browserElementChild.showModalPrompt(this._win, {
28       promptType: "alert",
29       title,
30       message: text,
31       returnValue: undefined,
32     });
33   },
35   alertCheck(title, text, checkMsg, checkState) {
36     // Treat this like a normal alert() call, ignoring the checkState.  The
37     // front-end can do its own suppression of the alert() if it wants.
38     this.alert(title, text);
39   },
41   confirm(title, text) {
42     return this._browserElementChild.showModalPrompt(this._win, {
43       promptType: "confirm",
44       title,
45       message: text,
46       returnValue: undefined,
47     });
48   },
50   confirmCheck(title, text, checkMsg, checkState) {
51     return this.confirm(title, text);
52   },
54   // Each button is described by an object with the following schema
55   // {
56   //   string messageType,  // 'builtin' or 'custom'
57   //   string message, // 'ok', 'cancel', 'yes', 'no', 'save', 'dontsave',
58   //                   // 'revert' or a string from caller if messageType was 'custom'.
59   // }
60   //
61   // Expected result from embedder:
62   // {
63   //   int button, // Index of the button that user pressed.
64   //   boolean checked, // True if the check box is checked.
65   // }
66   confirmEx(
67     title,
68     text,
69     buttonFlags,
70     button0Title,
71     button1Title,
72     button2Title,
73     checkMsg,
74     checkState
75   ) {
76     let buttonProperties = this._buildConfirmExButtonProperties(
77       buttonFlags,
78       button0Title,
79       button1Title,
80       button2Title
81     );
82     let defaultReturnValue = { selectedButton: buttonProperties.defaultButton };
83     if (checkMsg) {
84       defaultReturnValue.checked = checkState.value;
85     }
86     let ret = this._browserElementChild.showModalPrompt(this._win, {
87       promptType: "custom-prompt",
88       title,
89       message: text,
90       defaultButton: buttonProperties.defaultButton,
91       buttons: buttonProperties.buttons,
92       showCheckbox: !!checkMsg,
93       checkboxMessage: checkMsg,
94       checkboxCheckedByDefault: !!checkState.value,
95       returnValue: defaultReturnValue,
96     });
97     if (checkMsg) {
98       checkState.value = ret.checked;
99     }
100     return buttonProperties.indexToButtonNumberMap[ret.selectedButton];
101   },
103   prompt(title, text, value, checkMsg, checkState) {
104     let rv = this._browserElementChild.showModalPrompt(this._win, {
105       promptType: "prompt",
106       title,
107       message: text,
108       initialValue: value.value,
109       returnValue: null,
110     });
112     value.value = rv;
114     // nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt,
115     // and false if the user pressed "Cancel".
116     //
117     // BrowserElementChild returns null for "Cancel" and returns the string the
118     // user entered otherwise.
119     return rv !== null;
120   },
122   promptUsernameAndPassword(title, text, username, password) {
123     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
124   },
126   promptPassword(title, text, password) {
127     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
128   },
130   select(title, text, aSelectList, aOutSelection) {
131     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
132   },
134   _buildConfirmExButtonProperties(
135     buttonFlags,
136     button0Title,
137     button1Title,
138     button2Title
139   ) {
140     let r = {
141       defaultButton: -1,
142       buttons: [],
143       // This map is for translating array index to the button number that
144       // is recognized by Gecko. This shouldn't be exposed to embedder.
145       indexToButtonNumberMap: [],
146     };
148     let defaultButton = 0; // Default to Button 0.
149     if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
150       defaultButton = 1;
151     } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
152       defaultButton = 2;
153     }
155     // Properties of each button.
156     let buttonPositions = [
157       Ci.nsIPrompt.BUTTON_POS_0,
158       Ci.nsIPrompt.BUTTON_POS_1,
159       Ci.nsIPrompt.BUTTON_POS_2,
160     ];
162     function buildButton(buttonTitle, buttonNumber) {
163       let ret = {};
164       let buttonPosition = buttonPositions[buttonNumber];
165       let mask = 0xff * buttonPosition; // 8 bit mask
166       let titleType = (buttonFlags & mask) / buttonPosition;
168       ret.messageType = "builtin";
169       switch (titleType) {
170         case Ci.nsIPrompt.BUTTON_TITLE_OK:
171           ret.message = "ok";
172           break;
173         case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
174           ret.message = "cancel";
175           break;
176         case Ci.nsIPrompt.BUTTON_TITLE_YES:
177           ret.message = "yes";
178           break;
179         case Ci.nsIPrompt.BUTTON_TITLE_NO:
180           ret.message = "no";
181           break;
182         case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
183           ret.message = "save";
184           break;
185         case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
186           ret.message = "dontsave";
187           break;
188         case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
189           ret.message = "revert";
190           break;
191         case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
192           ret.message = buttonTitle;
193           ret.messageType = "custom";
194           break;
195         default:
196           // This button is not shown.
197           return;
198       }
200       // If this is the default button, set r.defaultButton to
201       // the index of this button in the array. This value is going to be
202       // exposed to the embedder.
203       if (defaultButton === buttonNumber) {
204         r.defaultButton = r.buttons.length;
205       }
206       r.buttons.push(ret);
207       r.indexToButtonNumberMap.push(buttonNumber);
208     }
210     buildButton(button0Title, 0);
211     buildButton(button1Title, 1);
212     buildButton(button2Title, 2);
214     // If defaultButton is still -1 here, it means the default button won't
215     // be shown.
216     if (r.defaultButton === -1) {
217       throw new Components.Exception(
218         "Default button won't be shown",
219         Cr.NS_ERROR_FAILURE
220       );
221     }
223     return r;
224   },
227 function BrowserElementAuthPrompt() {}
229 BrowserElementAuthPrompt.prototype = {
230   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
232   promptAuth: function promptAuth(channel, level, authInfo) {
233     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
234   },
236   asyncPromptAuth: function asyncPromptAuth(
237     channel,
238     callback,
239     context,
240     level,
241     authInfo
242   ) {
243     debug("asyncPromptAuth");
245     // The cases that we don't support now.
246     if (
247       authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
248       authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
249     ) {
250       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
251     }
253     let frame = this._getFrameFromChannel(channel);
254     if (!frame) {
255       debug("Cannot get frame, asyncPromptAuth fail");
256       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
257     }
259     let browserElementParent = BrowserElementPromptService.getBrowserElementParentForFrame(
260       frame
261     );
263     if (!browserElementParent) {
264       debug("Failed to load browser element parent.");
265       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
266     }
268     let consumer = {
269       QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
270       callback,
271       context,
272       cancel() {
273         this.callback.onAuthCancelled(this.context, false);
274         this.callback = null;
275         this.context = null;
276       },
277     };
279     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
280     let hashKey = level + "|" + hostname + "|" + httpRealm;
281     let asyncPrompt = this._asyncPrompts[hashKey];
282     if (asyncPrompt) {
283       asyncPrompt.consumers.push(consumer);
284       return consumer;
285     }
287     asyncPrompt = {
288       consumers: [consumer],
289       channel,
290       authInfo,
291       level,
292       inProgress: false,
293       browserElementParent,
294     };
296     this._asyncPrompts[hashKey] = asyncPrompt;
297     this._doAsyncPrompt();
298     return consumer;
299   },
301   // Utilities for nsIAuthPrompt2 ----------------
303   _asyncPrompts: {},
304   _asyncPromptInProgress: new WeakMap(),
305   _doAsyncPrompt() {
306     // Find the key of a prompt whose browser element parent does not have
307     // async prompt in progress.
308     let hashKey = null;
309     for (let key in this._asyncPrompts) {
310       let prompt = this._asyncPrompts[key];
311       if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
312         hashKey = key;
313         break;
314       }
315     }
317     // Didn't find an available prompt, so just return.
318     if (!hashKey) {
319       return;
320     }
322     let prompt = this._asyncPrompts[hashKey];
324     this._asyncPromptInProgress.set(prompt.browserElementParent, true);
325     prompt.inProgress = true;
327     let self = this;
328     let callback = function(ok, username, password) {
329       debug(
330         "Async auth callback is called, ok = " + ok + ", username = " + username
331       );
333       // Here we got the username and password provided by embedder, or
334       // ok = false if the prompt was cancelled by embedder.
335       delete self._asyncPrompts[hashKey];
336       prompt.inProgress = false;
337       self._asyncPromptInProgress.delete(prompt.browserElementParent);
339       // Fill authentication information with username and password provided
340       // by user.
341       let flags = prompt.authInfo.flags;
342       if (username) {
343         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
344           // Domain is separated from username by a backslash
345           let idx = username.indexOf("\\");
346           if (idx == -1) {
347             prompt.authInfo.username = username;
348           } else {
349             prompt.authInfo.domain = username.substring(0, idx);
350             prompt.authInfo.username = username.substring(idx + 1);
351           }
352         } else {
353           prompt.authInfo.username = username;
354         }
355       }
357       if (password) {
358         prompt.authInfo.password = password;
359       }
361       for (let consumer of prompt.consumers) {
362         if (!consumer.callback) {
363           // Not having a callback means that consumer didn't provide it
364           // or canceled the notification.
365           continue;
366         }
368         try {
369           if (ok) {
370             debug("Ok, calling onAuthAvailable to finish auth");
371             consumer.callback.onAuthAvailable(
372               consumer.context,
373               prompt.authInfo
374             );
375           } else {
376             debug("Cancelled, calling onAuthCancelled to finish auth.");
377             consumer.callback.onAuthCancelled(consumer.context, true);
378           }
379         } catch (e) {
380           /* Throw away exceptions caused by callback */
381         }
382       }
384       // Process the next prompt, if one is pending.
385       self._doAsyncPrompt();
386     };
388     let runnable = {
389       run() {
390         // Call promptAuth of browserElementParent, to show the prompt.
391         prompt.browserElementParent.promptAuth(
392           self._createAuthDetail(prompt.channel, prompt.authInfo),
393           callback
394         );
395       },
396     };
398     Services.tm.dispatchToMainThread(runnable);
399   },
401   _getFrameFromChannel(channel) {
402     let loadContext = channel.notificationCallbacks.getInterface(
403       Ci.nsILoadContext
404     );
405     return loadContext.topFrameElement;
406   },
408   _createAuthDetail(channel, authInfo) {
409     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
410     return {
411       host: hostname,
412       path: channel.URI.pathQueryRef,
413       realm: httpRealm,
414       username: authInfo.username,
415       isProxy: !!(authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY),
416       isOnlyPassword: !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD),
417     };
418   },
420   // The code is taken from nsLoginManagerPrompter.js, with slight
421   // modification for parameter name consistency here.
422   _getAuthTarget(channel, authInfo) {
423     let hostname, realm;
425     // If our proxy is demanding authentication, don't use the
426     // channel's actual destination.
427     if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
428       if (!(channel instanceof Ci.nsIProxiedChannel)) {
429         throw new Error("proxy auth needs nsIProxiedChannel");
430       }
432       let info = channel.proxyInfo;
433       if (!info) {
434         throw new Error("proxy auth needs nsIProxyInfo");
435       }
437       // Proxies don't have a scheme, but we'll use "moz-proxy://"
438       // so that it's more obvious what the login is for.
439       var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
440         Ci.nsIIDNService
441       );
442       hostname =
443         "moz-proxy://" +
444         idnService.convertUTF8toACE(info.host) +
445         ":" +
446         info.port;
447       realm = authInfo.realm;
448       if (!realm) {
449         realm = hostname;
450       }
452       return [hostname, realm];
453     }
455     hostname = this._getFormattedHostname(channel.URI);
457     // If a HTTP WWW-Authenticate header specified a realm, that value
458     // will be available here. If it wasn't set or wasn't HTTP, we'll use
459     // the formatted hostname instead.
460     realm = authInfo.realm;
461     if (!realm) {
462       realm = hostname;
463     }
465     return [hostname, realm];
466   },
468   /**
469    * Strip out things like userPass and path for display.
470    */
471   _getFormattedHostname(uri) {
472     return uri.scheme + "://" + uri.hostPort;
473   },
476 function AuthPromptWrapper(oldImpl, browserElementImpl) {
477   this._oldImpl = oldImpl;
478   this._browserElementImpl = browserElementImpl;
481 AuthPromptWrapper.prototype = {
482   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
483   promptAuth(channel, level, authInfo) {
484     if (this._canGetParentElement(channel)) {
485       return this._browserElementImpl.promptAuth(channel, level, authInfo);
486     }
487     return this._oldImpl.promptAuth(channel, level, authInfo);
488   },
490   asyncPromptAuth(channel, callback, context, level, authInfo) {
491     if (this._canGetParentElement(channel)) {
492       return this._browserElementImpl.asyncPromptAuth(
493         channel,
494         callback,
495         context,
496         level,
497         authInfo
498       );
499     }
500     return this._oldImpl.asyncPromptAuth(
501       channel,
502       callback,
503       context,
504       level,
505       authInfo
506     );
507   },
509   _canGetParentElement(channel) {
510     try {
511       let context = channel.notificationCallbacks.getInterface(
512         Ci.nsILoadContext
513       );
514       let frame = context.topFrameElement;
515       if (!frame) {
516         return false;
517       }
519       if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame)) {
520         return false;
521       }
523       return true;
524     } catch (e) {
525       return false;
526     }
527   },
530 function BrowserElementPromptFactory(toWrap) {
531   this._wrapped = toWrap;
534 BrowserElementPromptFactory.prototype = {
535   classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"),
536   QueryInterface: ChromeUtils.generateQI(["nsIPromptFactory"]),
538   _mayUseNativePrompt() {
539     try {
540       return Services.prefs.getBoolPref("browser.prompt.allowNative");
541     } catch (e) {
542       // This properity is default to true.
543       return true;
544     }
545   },
547   _getNativePromptIfAllowed(win, iid, err) {
548     if (this._mayUseNativePrompt()) {
549       return this._wrapped.getPrompt(win, iid);
550     }
552     // Not allowed, throw an exception.
553     throw err;
554   },
556   getPrompt(win, iid) {
557     // It is possible for some object to get a prompt without passing
558     // valid reference of window, like nsNSSComponent. In such case, we
559     // should just fall back to the native prompt service
560     if (!win) {
561       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
562     }
564     if (
565       iid.number != Ci.nsIPrompt.number &&
566       iid.number != Ci.nsIAuthPrompt2.number
567     ) {
568       debug(
569         "We don't recognize the requested IID (" +
570           iid +
571           ", " +
572           "allowed IID: " +
573           "nsIPrompt=" +
574           Ci.nsIPrompt +
575           ", " +
576           "nsIAuthPrompt2=" +
577           Ci.nsIAuthPrompt2 +
578           ")"
579       );
580       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
581     }
583     // Try to find a BrowserElementChild for the window.
584     let browserElementChild = BrowserElementPromptService.getBrowserElementChildForWindow(
585       win
586     );
588     if (iid.number === Ci.nsIAuthPrompt2.number) {
589       debug("Caller requests an instance of nsIAuthPrompt2.");
591       if (browserElementChild) {
592         // If we are able to get a BrowserElementChild, it means that
593         // the auth prompt is for a mozbrowser. Therefore we don't need to
594         // fall back.
595         return new BrowserElementAuthPrompt().QueryInterface(iid);
596       }
598       // Because nsIAuthPrompt2 is called in parent process. If caller
599       // wants nsIAuthPrompt2 and we cannot get BrowserElementchild,
600       // it doesn't mean that we should fallback. It is possible that we can
601       // get the BrowserElementParent from nsIChannel that passed to
602       // functions of nsIAuthPrompt2.
603       if (this._mayUseNativePrompt()) {
604         return new AuthPromptWrapper(
605           this._wrapped.getPrompt(win, iid),
606           new BrowserElementAuthPrompt().QueryInterface(iid)
607         ).QueryInterface(iid);
608       }
609       // Falling back is not allowed, so we don't need wrap the
610       // BrowserElementPrompt.
611       return new BrowserElementAuthPrompt().QueryInterface(iid);
612     }
614     if (!browserElementChild) {
615       debug(
616         "We can't find a browserElementChild for " + win + ", " + win.location
617       );
618       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
619     }
621     debug("Returning wrapped getPrompt for " + win);
622     return new BrowserElementPrompt(win, browserElementChild).QueryInterface(
623       iid
624     );
625   },
628 var BrowserElementPromptService = {
629   QueryInterface: ChromeUtils.generateQI([
630     "nsIObserver",
631     "nsISupportsWeakReference",
632   ]),
634   _initialized: false,
636   _init() {
637     if (this._initialized) {
638       return;
639     }
641     this._initialized = true;
642     this._browserElementParentMap = new WeakMap();
644     Services.obs.addObserver(
645       this,
646       "outer-window-destroyed",
647       /* ownsWeak = */ true
648     );
650     // Wrap the existing @mozilla.org/prompter;1 implementation.
651     var contractID = "@mozilla.org/prompter;1";
652     var oldCID = Cm.contractIDToCID(contractID);
653     var newCID = BrowserElementPromptFactory.prototype.classID;
654     var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory);
656     if (oldCID == newCID) {
657       debug("WARNING: Wrapped prompt factory is already installed!");
658       return;
659     }
661     var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
662     var newInstance = new BrowserElementPromptFactory(oldInstance);
664     var newFactory = {
665       createInstance(iid) {
666         return newInstance.QueryInterface(iid);
667       },
668     };
669     Cm.registerFactory(
670       newCID,
671       "BrowserElementPromptService's prompter;1 wrapper",
672       contractID,
673       newFactory
674     );
676     debug("Done installing new prompt factory.");
677   },
679   _getOuterWindowID(win) {
680     return win.docShell.outerWindowID;
681   },
683   _browserElementChildMap: {},
684   mapWindowToBrowserElementChild(win, browserElementChild) {
685     this._browserElementChildMap[
686       this._getOuterWindowID(win)
687     ] = browserElementChild;
688   },
689   unmapWindowToBrowserElementChild(win) {
690     delete this._browserElementChildMap[this._getOuterWindowID(win)];
691   },
693   getBrowserElementChildForWindow(win) {
694     // We only have a mapping for <iframe mozbrowser>s, not their inner
695     // <iframes>, so we look up win.top below.  window.top (when called from
696     // script) respects <iframe mozbrowser> boundaries.
697     return this._browserElementChildMap[this._getOuterWindowID(win.top)];
698   },
700   mapFrameToBrowserElementParent(frame, browserElementParent) {
701     this._browserElementParentMap.set(frame, browserElementParent);
702   },
704   getBrowserElementParentForFrame(frame) {
705     return this._browserElementParentMap.get(frame);
706   },
708   _observeOuterWindowDestroyed(outerWindowID) {
709     let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
710     debug("observeOuterWindowDestroyed " + id);
711     delete this._browserElementChildMap[outerWindowID.data];
712   },
714   observe(subject, topic, data) {
715     switch (topic) {
716       case "outer-window-destroyed":
717         this._observeOuterWindowDestroyed(subject);
718         break;
719       default:
720         debug("Observed unexpected topic " + topic);
721     }
722   },
725 BrowserElementPromptService._init();