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