Bug 1688354 [wpt PR 27298] - Treat 'rem' as an absolute unit for font size, a=testonly
[gecko.git] / dom / browser-element / BrowserElementPromptService.jsm
blob12189efd43745503e4e531d5eee9202481744170
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(
123     title,
124     text,
125     username,
126     password,
127     checkMsg,
128     checkState
129   ) {
130     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
131   },
133   promptPassword(title, text, password, checkMsg, checkState) {
134     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
135   },
137   select(title, text, aSelectList, aOutSelection) {
138     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
139   },
141   _buildConfirmExButtonProperties(
142     buttonFlags,
143     button0Title,
144     button1Title,
145     button2Title
146   ) {
147     let r = {
148       defaultButton: -1,
149       buttons: [],
150       // This map is for translating array index to the button number that
151       // is recognized by Gecko. This shouldn't be exposed to embedder.
152       indexToButtonNumberMap: [],
153     };
155     let defaultButton = 0; // Default to Button 0.
156     if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
157       defaultButton = 1;
158     } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
159       defaultButton = 2;
160     }
162     // Properties of each button.
163     let buttonPositions = [
164       Ci.nsIPrompt.BUTTON_POS_0,
165       Ci.nsIPrompt.BUTTON_POS_1,
166       Ci.nsIPrompt.BUTTON_POS_2,
167     ];
169     function buildButton(buttonTitle, buttonNumber) {
170       let ret = {};
171       let buttonPosition = buttonPositions[buttonNumber];
172       let mask = 0xff * buttonPosition; // 8 bit mask
173       let titleType = (buttonFlags & mask) / buttonPosition;
175       ret.messageType = "builtin";
176       switch (titleType) {
177         case Ci.nsIPrompt.BUTTON_TITLE_OK:
178           ret.message = "ok";
179           break;
180         case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
181           ret.message = "cancel";
182           break;
183         case Ci.nsIPrompt.BUTTON_TITLE_YES:
184           ret.message = "yes";
185           break;
186         case Ci.nsIPrompt.BUTTON_TITLE_NO:
187           ret.message = "no";
188           break;
189         case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
190           ret.message = "save";
191           break;
192         case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
193           ret.message = "dontsave";
194           break;
195         case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
196           ret.message = "revert";
197           break;
198         case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
199           ret.message = buttonTitle;
200           ret.messageType = "custom";
201           break;
202         default:
203           // This button is not shown.
204           return;
205       }
207       // If this is the default button, set r.defaultButton to
208       // the index of this button in the array. This value is going to be
209       // exposed to the embedder.
210       if (defaultButton === buttonNumber) {
211         r.defaultButton = r.buttons.length;
212       }
213       r.buttons.push(ret);
214       r.indexToButtonNumberMap.push(buttonNumber);
215     }
217     buildButton(button0Title, 0);
218     buildButton(button1Title, 1);
219     buildButton(button2Title, 2);
221     // If defaultButton is still -1 here, it means the default button won't
222     // be shown.
223     if (r.defaultButton === -1) {
224       throw new Components.Exception(
225         "Default button won't be shown",
226         Cr.NS_ERROR_FAILURE
227       );
228     }
230     return r;
231   },
234 function BrowserElementAuthPrompt() {}
236 BrowserElementAuthPrompt.prototype = {
237   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
239   promptAuth: function promptAuth(channel, level, authInfo) {
240     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
241   },
243   asyncPromptAuth: function asyncPromptAuth(
244     channel,
245     callback,
246     context,
247     level,
248     authInfo
249   ) {
250     debug("asyncPromptAuth");
252     // The cases that we don't support now.
253     if (
254       authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
255       authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
256     ) {
257       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
258     }
260     let frame = this._getFrameFromChannel(channel);
261     if (!frame) {
262       debug("Cannot get frame, asyncPromptAuth fail");
263       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
264     }
266     let browserElementParent = BrowserElementPromptService.getBrowserElementParentForFrame(
267       frame
268     );
270     if (!browserElementParent) {
271       debug("Failed to load browser element parent.");
272       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
273     }
275     let consumer = {
276       QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
277       callback,
278       context,
279       cancel() {
280         this.callback.onAuthCancelled(this.context, false);
281         this.callback = null;
282         this.context = null;
283       },
284     };
286     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
287     let hashKey = level + "|" + hostname + "|" + httpRealm;
288     let asyncPrompt = this._asyncPrompts[hashKey];
289     if (asyncPrompt) {
290       asyncPrompt.consumers.push(consumer);
291       return consumer;
292     }
294     asyncPrompt = {
295       consumers: [consumer],
296       channel,
297       authInfo,
298       level,
299       inProgress: false,
300       browserElementParent,
301     };
303     this._asyncPrompts[hashKey] = asyncPrompt;
304     this._doAsyncPrompt();
305     return consumer;
306   },
308   // Utilities for nsIAuthPrompt2 ----------------
310   _asyncPrompts: {},
311   _asyncPromptInProgress: new WeakMap(),
312   _doAsyncPrompt() {
313     // Find the key of a prompt whose browser element parent does not have
314     // async prompt in progress.
315     let hashKey = null;
316     for (let key in this._asyncPrompts) {
317       let prompt = this._asyncPrompts[key];
318       if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
319         hashKey = key;
320         break;
321       }
322     }
324     // Didn't find an available prompt, so just return.
325     if (!hashKey) {
326       return;
327     }
329     let prompt = this._asyncPrompts[hashKey];
331     this._asyncPromptInProgress.set(prompt.browserElementParent, true);
332     prompt.inProgress = true;
334     let self = this;
335     let callback = function(ok, username, password) {
336       debug(
337         "Async auth callback is called, ok = " + ok + ", username = " + username
338       );
340       // Here we got the username and password provided by embedder, or
341       // ok = false if the prompt was cancelled by embedder.
342       delete self._asyncPrompts[hashKey];
343       prompt.inProgress = false;
344       self._asyncPromptInProgress.delete(prompt.browserElementParent);
346       // Fill authentication information with username and password provided
347       // by user.
348       let flags = prompt.authInfo.flags;
349       if (username) {
350         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
351           // Domain is separated from username by a backslash
352           let idx = username.indexOf("\\");
353           if (idx == -1) {
354             prompt.authInfo.username = username;
355           } else {
356             prompt.authInfo.domain = username.substring(0, idx);
357             prompt.authInfo.username = username.substring(idx + 1);
358           }
359         } else {
360           prompt.authInfo.username = username;
361         }
362       }
364       if (password) {
365         prompt.authInfo.password = password;
366       }
368       for (let consumer of prompt.consumers) {
369         if (!consumer.callback) {
370           // Not having a callback means that consumer didn't provide it
371           // or canceled the notification.
372           continue;
373         }
375         try {
376           if (ok) {
377             debug("Ok, calling onAuthAvailable to finish auth");
378             consumer.callback.onAuthAvailable(
379               consumer.context,
380               prompt.authInfo
381             );
382           } else {
383             debug("Cancelled, calling onAuthCancelled to finish auth.");
384             consumer.callback.onAuthCancelled(consumer.context, true);
385           }
386         } catch (e) {
387           /* Throw away exceptions caused by callback */
388         }
389       }
391       // Process the next prompt, if one is pending.
392       self._doAsyncPrompt();
393     };
395     let runnable = {
396       run() {
397         // Call promptAuth of browserElementParent, to show the prompt.
398         prompt.browserElementParent.promptAuth(
399           self._createAuthDetail(prompt.channel, prompt.authInfo),
400           callback
401         );
402       },
403     };
405     Services.tm.dispatchToMainThread(runnable);
406   },
408   _getFrameFromChannel(channel) {
409     let loadContext = channel.notificationCallbacks.getInterface(
410       Ci.nsILoadContext
411     );
412     return loadContext.topFrameElement;
413   },
415   _createAuthDetail(channel, authInfo) {
416     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
417     return {
418       host: hostname,
419       path: channel.URI.pathQueryRef,
420       realm: httpRealm,
421       username: authInfo.username,
422       isProxy: !!(authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY),
423       isOnlyPassword: !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD),
424     };
425   },
427   // The code is taken from nsLoginManagerPrompter.js, with slight
428   // modification for parameter name consistency here.
429   _getAuthTarget(channel, authInfo) {
430     let hostname, realm;
432     // If our proxy is demanding authentication, don't use the
433     // channel's actual destination.
434     if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
435       if (!(channel instanceof Ci.nsIProxiedChannel)) {
436         throw new Error("proxy auth needs nsIProxiedChannel");
437       }
439       let info = channel.proxyInfo;
440       if (!info) {
441         throw new Error("proxy auth needs nsIProxyInfo");
442       }
444       // Proxies don't have a scheme, but we'll use "moz-proxy://"
445       // so that it's more obvious what the login is for.
446       var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
447         Ci.nsIIDNService
448       );
449       hostname =
450         "moz-proxy://" +
451         idnService.convertUTF8toACE(info.host) +
452         ":" +
453         info.port;
454       realm = authInfo.realm;
455       if (!realm) {
456         realm = hostname;
457       }
459       return [hostname, realm];
460     }
462     hostname = this._getFormattedHostname(channel.URI);
464     // If a HTTP WWW-Authenticate header specified a realm, that value
465     // will be available here. If it wasn't set or wasn't HTTP, we'll use
466     // the formatted hostname instead.
467     realm = authInfo.realm;
468     if (!realm) {
469       realm = hostname;
470     }
472     return [hostname, realm];
473   },
475   /**
476    * Strip out things like userPass and path for display.
477    */
478   _getFormattedHostname(uri) {
479     return uri.scheme + "://" + uri.hostPort;
480   },
483 function AuthPromptWrapper(oldImpl, browserElementImpl) {
484   this._oldImpl = oldImpl;
485   this._browserElementImpl = browserElementImpl;
488 AuthPromptWrapper.prototype = {
489   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
490   promptAuth(channel, level, authInfo) {
491     if (this._canGetParentElement(channel)) {
492       return this._browserElementImpl.promptAuth(channel, level, authInfo);
493     }
494     return this._oldImpl.promptAuth(channel, level, authInfo);
495   },
497   asyncPromptAuth(channel, callback, context, level, authInfo) {
498     if (this._canGetParentElement(channel)) {
499       return this._browserElementImpl.asyncPromptAuth(
500         channel,
501         callback,
502         context,
503         level,
504         authInfo
505       );
506     }
507     return this._oldImpl.asyncPromptAuth(
508       channel,
509       callback,
510       context,
511       level,
512       authInfo
513     );
514   },
516   _canGetParentElement(channel) {
517     try {
518       let context = channel.notificationCallbacks.getInterface(
519         Ci.nsILoadContext
520       );
521       let frame = context.topFrameElement;
522       if (!frame) {
523         return false;
524       }
526       if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame)) {
527         return false;
528       }
530       return true;
531     } catch (e) {
532       return false;
533     }
534   },
537 function BrowserElementPromptFactory(toWrap) {
538   this._wrapped = toWrap;
541 BrowserElementPromptFactory.prototype = {
542   classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"),
543   QueryInterface: ChromeUtils.generateQI(["nsIPromptFactory"]),
545   _mayUseNativePrompt() {
546     try {
547       return Services.prefs.getBoolPref("browser.prompt.allowNative");
548     } catch (e) {
549       // This properity is default to true.
550       return true;
551     }
552   },
554   _getNativePromptIfAllowed(win, iid, err) {
555     if (this._mayUseNativePrompt()) {
556       return this._wrapped.getPrompt(win, iid);
557     }
559     // Not allowed, throw an exception.
560     throw err;
561   },
563   getPrompt(win, iid) {
564     // It is possible for some object to get a prompt without passing
565     // valid reference of window, like nsNSSComponent. In such case, we
566     // should just fall back to the native prompt service
567     if (!win) {
568       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
569     }
571     if (
572       iid.number != Ci.nsIPrompt.number &&
573       iid.number != Ci.nsIAuthPrompt2.number
574     ) {
575       debug(
576         "We don't recognize the requested IID (" +
577           iid +
578           ", " +
579           "allowed IID: " +
580           "nsIPrompt=" +
581           Ci.nsIPrompt +
582           ", " +
583           "nsIAuthPrompt2=" +
584           Ci.nsIAuthPrompt2 +
585           ")"
586       );
587       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
588     }
590     // Try to find a BrowserElementChild for the window.
591     let browserElementChild = BrowserElementPromptService.getBrowserElementChildForWindow(
592       win
593     );
595     if (iid.number === Ci.nsIAuthPrompt2.number) {
596       debug("Caller requests an instance of nsIAuthPrompt2.");
598       if (browserElementChild) {
599         // If we are able to get a BrowserElementChild, it means that
600         // the auth prompt is for a mozbrowser. Therefore we don't need to
601         // fall back.
602         return new BrowserElementAuthPrompt().QueryInterface(iid);
603       }
605       // Because nsIAuthPrompt2 is called in parent process. If caller
606       // wants nsIAuthPrompt2 and we cannot get BrowserElementchild,
607       // it doesn't mean that we should fallback. It is possible that we can
608       // get the BrowserElementParent from nsIChannel that passed to
609       // functions of nsIAuthPrompt2.
610       if (this._mayUseNativePrompt()) {
611         return new AuthPromptWrapper(
612           this._wrapped.getPrompt(win, iid),
613           new BrowserElementAuthPrompt().QueryInterface(iid)
614         ).QueryInterface(iid);
615       }
616       // Falling back is not allowed, so we don't need wrap the
617       // BrowserElementPrompt.
618       return new BrowserElementAuthPrompt().QueryInterface(iid);
619     }
621     if (!browserElementChild) {
622       debug(
623         "We can't find a browserElementChild for " + win + ", " + win.location
624       );
625       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
626     }
628     debug("Returning wrapped getPrompt for " + win);
629     return new BrowserElementPrompt(win, browserElementChild).QueryInterface(
630       iid
631     );
632   },
635 var BrowserElementPromptService = {
636   QueryInterface: ChromeUtils.generateQI([
637     "nsIObserver",
638     "nsISupportsWeakReference",
639   ]),
641   _initialized: false,
643   _init() {
644     if (this._initialized) {
645       return;
646     }
648     this._initialized = true;
649     this._browserElementParentMap = new WeakMap();
651     Services.obs.addObserver(
652       this,
653       "outer-window-destroyed",
654       /* ownsWeak = */ true
655     );
657     // Wrap the existing @mozilla.org/prompter;1 implementation.
658     var contractID = "@mozilla.org/prompter;1";
659     var oldCID = Cm.contractIDToCID(contractID);
660     var newCID = BrowserElementPromptFactory.prototype.classID;
661     var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory);
663     if (oldCID == newCID) {
664       debug("WARNING: Wrapped prompt factory is already installed!");
665       return;
666     }
668     var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
669     var newInstance = new BrowserElementPromptFactory(oldInstance);
671     var newFactory = {
672       createInstance(outer, iid) {
673         if (outer != null) {
674           throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
675         }
676         return newInstance.QueryInterface(iid);
677       },
678     };
679     Cm.registerFactory(
680       newCID,
681       "BrowserElementPromptService's prompter;1 wrapper",
682       contractID,
683       newFactory
684     );
686     debug("Done installing new prompt factory.");
687   },
689   _getOuterWindowID(win) {
690     return win.docShell.outerWindowID;
691   },
693   _browserElementChildMap: {},
694   mapWindowToBrowserElementChild(win, browserElementChild) {
695     this._browserElementChildMap[
696       this._getOuterWindowID(win)
697     ] = browserElementChild;
698   },
699   unmapWindowToBrowserElementChild(win) {
700     delete this._browserElementChildMap[this._getOuterWindowID(win)];
701   },
703   getBrowserElementChildForWindow(win) {
704     // We only have a mapping for <iframe mozbrowser>s, not their inner
705     // <iframes>, so we look up win.top below.  window.top (when called from
706     // script) respects <iframe mozbrowser> boundaries.
707     return this._browserElementChildMap[this._getOuterWindowID(win.top)];
708   },
710   mapFrameToBrowserElementParent(frame, browserElementParent) {
711     this._browserElementParentMap.set(frame, browserElementParent);
712   },
714   getBrowserElementParentForFrame(frame) {
715     return this._browserElementParentMap.get(frame);
716   },
718   _observeOuterWindowDestroyed(outerWindowID) {
719     let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
720     debug("observeOuterWindowDestroyed " + id);
721     delete this._browserElementChildMap[outerWindowID.data];
722   },
724   observe(subject, topic, data) {
725     switch (topic) {
726       case "outer-window-destroyed":
727         this._observeOuterWindowDestroyed(subject);
728         break;
729       default:
730         debug("Observed unexpected topic " + topic);
731     }
732   },
735 BrowserElementPromptService._init();