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(title, text, username, password) {
123 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
126 promptPassword(title, text, password) {
127 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
130 select(title, text, aSelectList, aOutSelection) {
131 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
134 _buildConfirmExButtonProperties(
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: [],
148 let defaultButton = 0; // Default to Button 0.
149 if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
151 } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
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,
162 function buildButton(buttonTitle, buttonNumber) {
164 let buttonPosition = buttonPositions[buttonNumber];
165 let mask = 0xff * buttonPosition; // 8 bit mask
166 let titleType = (buttonFlags & mask) / buttonPosition;
168 ret.messageType = "builtin";
170 case Ci.nsIPrompt.BUTTON_TITLE_OK:
173 case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
174 ret.message = "cancel";
176 case Ci.nsIPrompt.BUTTON_TITLE_YES:
179 case Ci.nsIPrompt.BUTTON_TITLE_NO:
182 case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
183 ret.message = "save";
185 case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
186 ret.message = "dontsave";
188 case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
189 ret.message = "revert";
191 case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
192 ret.message = buttonTitle;
193 ret.messageType = "custom";
196 // This button is not shown.
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;
207 r.indexToButtonNumberMap.push(buttonNumber);
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
216 if (r.defaultButton === -1) {
217 throw new Components.Exception(
218 "Default button won't be shown",
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);
236 asyncPromptAuth: function asyncPromptAuth(
243 debug("asyncPromptAuth");
245 // The cases that we don't support now.
247 authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
248 authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
250 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
253 let frame = this._getFrameFromChannel(channel);
255 debug("Cannot get frame, asyncPromptAuth fail");
256 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
259 let browserElementParent = BrowserElementPromptService.getBrowserElementParentForFrame(
263 if (!browserElementParent) {
264 debug("Failed to load browser element parent.");
265 throw Components.Exception("", Cr.NS_ERROR_FAILURE);
269 QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
273 this.callback.onAuthCancelled(this.context, false);
274 this.callback = null;
279 let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
280 let hashKey = level + "|" + hostname + "|" + httpRealm;
281 let asyncPrompt = this._asyncPrompts[hashKey];
283 asyncPrompt.consumers.push(consumer);
288 consumers: [consumer],
293 browserElementParent,
296 this._asyncPrompts[hashKey] = asyncPrompt;
297 this._doAsyncPrompt();
301 // Utilities for nsIAuthPrompt2 ----------------
304 _asyncPromptInProgress: new WeakMap(),
306 // Find the key of a prompt whose browser element parent does not have
307 // async prompt in progress.
309 for (let key in this._asyncPrompts) {
310 let prompt = this._asyncPrompts[key];
311 if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
317 // Didn't find an available prompt, so just return.
322 let prompt = this._asyncPrompts[hashKey];
324 this._asyncPromptInProgress.set(prompt.browserElementParent, true);
325 prompt.inProgress = true;
328 let callback = function(ok, username, password) {
330 "Async auth callback is called, ok = " + ok + ", username = " + username
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
341 let flags = prompt.authInfo.flags;
343 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
344 // Domain is separated from username by a backslash
345 let idx = username.indexOf("\\");
347 prompt.authInfo.username = username;
349 prompt.authInfo.domain = username.substring(0, idx);
350 prompt.authInfo.username = username.substring(idx + 1);
353 prompt.authInfo.username = username;
358 prompt.authInfo.password = password;
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.
370 debug("Ok, calling onAuthAvailable to finish auth");
371 consumer.callback.onAuthAvailable(
376 debug("Cancelled, calling onAuthCancelled to finish auth.");
377 consumer.callback.onAuthCancelled(consumer.context, true);
380 /* Throw away exceptions caused by callback */
384 // Process the next prompt, if one is pending.
385 self._doAsyncPrompt();
390 // Call promptAuth of browserElementParent, to show the prompt.
391 prompt.browserElementParent.promptAuth(
392 self._createAuthDetail(prompt.channel, prompt.authInfo),
398 Services.tm.dispatchToMainThread(runnable);
401 _getFrameFromChannel(channel) {
402 let loadContext = channel.notificationCallbacks.getInterface(
405 return loadContext.topFrameElement;
408 _createAuthDetail(channel, authInfo) {
409 let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
412 path: channel.URI.pathQueryRef,
414 username: authInfo.username,
415 isProxy: !!(authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY),
416 isOnlyPassword: !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD),
420 // The code is taken from nsLoginManagerPrompter.js, with slight
421 // modification for parameter name consistency here.
422 _getAuthTarget(channel, authInfo) {
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");
432 let info = channel.proxyInfo;
434 throw new Error("proxy auth needs nsIProxyInfo");
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(
444 idnService.convertUTF8toACE(info.host) +
447 realm = authInfo.realm;
452 return [hostname, realm];
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;
465 return [hostname, realm];
469 * Strip out things like userPass and path for display.
471 _getFormattedHostname(uri) {
472 return uri.scheme + "://" + uri.hostPort;
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);
487 return this._oldImpl.promptAuth(channel, level, authInfo);
490 asyncPromptAuth(channel, callback, context, level, authInfo) {
491 if (this._canGetParentElement(channel)) {
492 return this._browserElementImpl.asyncPromptAuth(
500 return this._oldImpl.asyncPromptAuth(
509 _canGetParentElement(channel) {
511 let context = channel.notificationCallbacks.getInterface(
514 let frame = context.topFrameElement;
519 if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame)) {
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() {
540 return Services.prefs.getBoolPref("browser.prompt.allowNative");
542 // This properity is default to true.
547 _getNativePromptIfAllowed(win, iid, err) {
548 if (this._mayUseNativePrompt()) {
549 return this._wrapped.getPrompt(win, iid);
552 // Not allowed, throw an exception.
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
561 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
565 iid.number != Ci.nsIPrompt.number &&
566 iid.number != Ci.nsIAuthPrompt2.number
569 "We don't recognize the requested IID (" +
580 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
583 // Try to find a BrowserElementChild for the window.
584 let browserElementChild = BrowserElementPromptService.getBrowserElementChildForWindow(
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
595 return new BrowserElementAuthPrompt().QueryInterface(iid);
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);
609 // Falling back is not allowed, so we don't need wrap the
610 // BrowserElementPrompt.
611 return new BrowserElementAuthPrompt().QueryInterface(iid);
614 if (!browserElementChild) {
616 "We can't find a browserElementChild for " + win + ", " + win.location
618 return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
621 debug("Returning wrapped getPrompt for " + win);
622 return new BrowserElementPrompt(win, browserElementChild).QueryInterface(
628 var BrowserElementPromptService = {
629 QueryInterface: ChromeUtils.generateQI([
631 "nsISupportsWeakReference",
637 if (this._initialized) {
641 this._initialized = true;
642 this._browserElementParentMap = new WeakMap();
644 Services.obs.addObserver(
646 "outer-window-destroyed",
647 /* ownsWeak = */ true
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!");
661 var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
662 var newInstance = new BrowserElementPromptFactory(oldInstance);
665 createInstance(outer, iid) {
667 throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
669 return newInstance.QueryInterface(iid);
674 "BrowserElementPromptService's prompter;1 wrapper",
679 debug("Done installing new prompt factory.");
682 _getOuterWindowID(win) {
683 return win.docShell.outerWindowID;
686 _browserElementChildMap: {},
687 mapWindowToBrowserElementChild(win, browserElementChild) {
688 this._browserElementChildMap[
689 this._getOuterWindowID(win)
690 ] = browserElementChild;
692 unmapWindowToBrowserElementChild(win) {
693 delete this._browserElementChildMap[this._getOuterWindowID(win)];
696 getBrowserElementChildForWindow(win) {
697 // We only have a mapping for <iframe mozbrowser>s, not their inner
698 // <iframes>, so we look up win.top below. window.top (when called from
699 // script) respects <iframe mozbrowser> boundaries.
700 return this._browserElementChildMap[this._getOuterWindowID(win.top)];
703 mapFrameToBrowserElementParent(frame, browserElementParent) {
704 this._browserElementParentMap.set(frame, browserElementParent);
707 getBrowserElementParentForFrame(frame) {
708 return this._browserElementParentMap.get(frame);
711 _observeOuterWindowDestroyed(outerWindowID) {
712 let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
713 debug("observeOuterWindowDestroyed " + id);
714 delete this._browserElementChildMap[outerWindowID.data];
717 observe(subject, topic, data) {
719 case "outer-window-destroyed":
720 this._observeOuterWindowDestroyed(subject);
723 debug("Observed unexpected topic " + topic);
728 BrowserElementPromptService._init();