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 : */
8 var Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
10 var EXPORTED_SYMBOLS = ["BrowserElementPromptService"];
12 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
15 // dump("BrowserElementPromptService - " + msg + "\n");
18 function BrowserElementPrompt(win, browserElementChild) {
20 this._browserElementChild = browserElementChild;
23 BrowserElementPrompt.prototype = {
24 QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
27 this._browserElementChild.showModalPrompt(this._win, {
31 returnValue: undefined,
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);
41 confirm(title, text) {
42 return this._browserElementChild.showModalPrompt(this._win, {
43 promptType: "confirm",
46 returnValue: undefined,
50 confirmCheck(title, text, checkMsg, checkState) {
51 return this.confirm(title, text);
54 // Each button is described by an object with the following schema
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'.
61 // Expected result from embedder:
63 // int button, // Index of the button that user pressed.
64 // boolean checked, // True if the check box is checked.
76 let buttonProperties = this._buildConfirmExButtonProperties(
82 let defaultReturnValue = { selectedButton: buttonProperties.defaultButton };
84 defaultReturnValue.checked = checkState.value;
86 let ret = this._browserElementChild.showModalPrompt(this._win, {
87 promptType: "custom-prompt",
90 defaultButton: buttonProperties.defaultButton,
91 buttons: buttonProperties.buttons,
92 showCheckbox: !!checkMsg,
93 checkboxMessage: checkMsg,
94 checkboxCheckedByDefault: !!checkState.value,
95 returnValue: defaultReturnValue,
98 checkState.value = ret.checked;
100 return buttonProperties.indexToButtonNumberMap[ret.selectedButton];
103 prompt(title, text, value, checkMsg, checkState) {
104 let rv = this._browserElementChild.showModalPrompt(this._win, {
105 promptType: "prompt",
108 initialValue: value.value,
114 // nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt,
115 // and false if the user pressed "Cancel".
117 // BrowserElementChild returns null for "Cancel" and returns the string the
118 // user entered otherwise.
122 promptUsernameAndPassword(
130 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
133 promptPassword(title, text, password, checkMsg, checkState) {
134 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
137 select(title, text, aSelectList, aOutSelection) {
138 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
141 _buildConfirmExButtonProperties(
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: [],
155 let defaultButton = 0; // Default to Button 0.
156 if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
158 } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
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,
169 function buildButton(buttonTitle, buttonNumber) {
171 let buttonPosition = buttonPositions[buttonNumber];
172 let mask = 0xff * buttonPosition; // 8 bit mask
173 let titleType = (buttonFlags & mask) / buttonPosition;
175 ret.messageType = "builtin";
177 case Ci.nsIPrompt.BUTTON_TITLE_OK:
180 case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
181 ret.message = "cancel";
183 case Ci.nsIPrompt.BUTTON_TITLE_YES:
186 case Ci.nsIPrompt.BUTTON_TITLE_NO:
189 case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
190 ret.message = "save";
192 case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
193 ret.message = "dontsave";
195 case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
196 ret.message = "revert";
198 case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
199 ret.message = buttonTitle;
200 ret.messageType = "custom";
203 // This button is not shown.
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;
214 r.indexToButtonNumberMap.push(buttonNumber);
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
223 if (r.defaultButton === -1) {
224 throw new Components.Exception(
225 "Default button won't be shown",
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);
243 asyncPromptAuth: function asyncPromptAuth(
250 debug("asyncPromptAuth");
252 // The cases that we don't support now.
254 authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
255 authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
257 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
260 let frame = this._getFrameFromChannel(channel);
262 debug("Cannot get frame, asyncPromptAuth fail");
263 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
266 let browserElementParent = BrowserElementPromptService.getBrowserElementParentForFrame(
270 if (!browserElementParent) {
271 debug("Failed to load browser element parent.");
272 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
276 QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
280 this.callback.onAuthCancelled(this.context, false);
281 this.callback = null;
286 let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
287 let hashKey = level + "|" + hostname + "|" + httpRealm;
288 let asyncPrompt = this._asyncPrompts[hashKey];
290 asyncPrompt.consumers.push(consumer);
295 consumers: [consumer],
300 browserElementParent,
303 this._asyncPrompts[hashKey] = asyncPrompt;
304 this._doAsyncPrompt();
308 // Utilities for nsIAuthPrompt2 ----------------
311 _asyncPromptInProgress: new WeakMap(),
313 // Find the key of a prompt whose browser element parent does not have
314 // async prompt in progress.
316 for (let key in this._asyncPrompts) {
317 let prompt = this._asyncPrompts[key];
318 if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
324 // Didn't find an available prompt, so just return.
329 let prompt = this._asyncPrompts[hashKey];
331 this._asyncPromptInProgress.set(prompt.browserElementParent, true);
332 prompt.inProgress = true;
335 let callback = function(ok, username, password) {
337 "Async auth callback is called, ok = " + ok + ", username = " + username
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
348 let flags = prompt.authInfo.flags;
350 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
351 // Domain is separated from username by a backslash
352 let idx = username.indexOf("\\");
354 prompt.authInfo.username = username;
356 prompt.authInfo.domain = username.substring(0, idx);
357 prompt.authInfo.username = username.substring(idx + 1);
360 prompt.authInfo.username = username;
365 prompt.authInfo.password = password;
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.
377 debug("Ok, calling onAuthAvailable to finish auth");
378 consumer.callback.onAuthAvailable(
383 debug("Cancelled, calling onAuthCancelled to finish auth.");
384 consumer.callback.onAuthCancelled(consumer.context, true);
387 /* Throw away exceptions caused by callback */
391 // Process the next prompt, if one is pending.
392 self._doAsyncPrompt();
397 // Call promptAuth of browserElementParent, to show the prompt.
398 prompt.browserElementParent.promptAuth(
399 self._createAuthDetail(prompt.channel, prompt.authInfo),
405 Services.tm.dispatchToMainThread(runnable);
408 _getFrameFromChannel(channel) {
409 let loadContext = channel.notificationCallbacks.getInterface(
412 return loadContext.topFrameElement;
415 _createAuthDetail(channel, authInfo) {
416 let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
419 path: channel.URI.pathQueryRef,
421 username: authInfo.username,
422 isProxy: !!(authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY),
423 isOnlyPassword: !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD),
427 // The code is taken from nsLoginManagerPrompter.js, with slight
428 // modification for parameter name consistency here.
429 _getAuthTarget(channel, authInfo) {
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");
439 let info = channel.proxyInfo;
441 throw new Error("proxy auth needs nsIProxyInfo");
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(
451 idnService.convertUTF8toACE(info.host) +
454 realm = authInfo.realm;
459 return [hostname, realm];
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;
472 return [hostname, realm];
476 * Strip out things like userPass and path for display.
478 _getFormattedHostname(uri) {
479 return uri.scheme + "://" + uri.hostPort;
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);
494 return this._oldImpl.promptAuth(channel, level, authInfo);
497 asyncPromptAuth(channel, callback, context, level, authInfo) {
498 if (this._canGetParentElement(channel)) {
499 return this._browserElementImpl.asyncPromptAuth(
507 return this._oldImpl.asyncPromptAuth(
516 _canGetParentElement(channel) {
518 let context = channel.notificationCallbacks.getInterface(
521 let frame = context.topFrameElement;
526 if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame)) {
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() {
547 return Services.prefs.getBoolPref("browser.prompt.allowNative");
549 // This properity is default to true.
554 _getNativePromptIfAllowed(win, iid, err) {
555 if (this._mayUseNativePrompt()) {
556 return this._wrapped.getPrompt(win, iid);
559 // Not allowed, throw an exception.
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
568 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
572 iid.number != Ci.nsIPrompt.number &&
573 iid.number != Ci.nsIAuthPrompt2.number
576 "We don't recognize the requested IID (" +
587 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
590 // Try to find a BrowserElementChild for the window.
591 let browserElementChild = BrowserElementPromptService.getBrowserElementChildForWindow(
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
602 return new BrowserElementAuthPrompt().QueryInterface(iid);
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);
616 // Falling back is not allowed, so we don't need wrap the
617 // BrowserElementPrompt.
618 return new BrowserElementAuthPrompt().QueryInterface(iid);
621 if (!browserElementChild) {
623 "We can't find a browserElementChild for " + win + ", " + win.location
625 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
628 debug("Returning wrapped getPrompt for " + win);
629 return new BrowserElementPrompt(win, browserElementChild).QueryInterface(
635 var BrowserElementPromptService = {
636 QueryInterface: ChromeUtils.generateQI([
638 "nsISupportsWeakReference",
644 if (this._initialized) {
648 this._initialized = true;
649 this._browserElementParentMap = new WeakMap();
651 Services.obs.addObserver(
653 "outer-window-destroyed",
654 /* ownsWeak = */ true
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!");
668 var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
669 var newInstance = new BrowserElementPromptFactory(oldInstance);
672 createInstance(outer, iid) {
674 throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
676 return newInstance.QueryInterface(iid);
681 "BrowserElementPromptService's prompter;1 wrapper",
686 debug("Done installing new prompt factory.");
689 _getOuterWindowID(win) {
690 return win.docShell.outerWindowID;
693 _browserElementChildMap: {},
694 mapWindowToBrowserElementChild(win, browserElementChild) {
695 this._browserElementChildMap[
696 this._getOuterWindowID(win)
697 ] = browserElementChild;
699 unmapWindowToBrowserElementChild(win) {
700 delete this._browserElementChildMap[this._getOuterWindowID(win)];
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)];
710 mapFrameToBrowserElementParent(frame, browserElementParent) {
711 this._browserElementParentMap.set(frame, browserElementParent);
714 getBrowserElementParentForFrame(frame) {
715 return this._browserElementParentMap.get(frame);
718 _observeOuterWindowDestroyed(outerWindowID) {
719 let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
720 debug("observeOuterWindowDestroyed " + id);
721 delete this._browserElementChildMap[outerWindowID.data];
724 observe(subject, topic, data) {
726 case "outer-window-destroyed":
727 this._observeOuterWindowDestroyed(subject);
730 debug("Observed unexpected topic " + topic);
735 BrowserElementPromptService._init();