Bumping manifests a=b2g-bump
[gecko.git] / dom / browser-element / BrowserElementPromptService.jsm
blob9830dd079e65385541a6e303b6a42d954285c0e8
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 let Cu = Components.utils;
9 let Ci = Components.interfaces;
10 let Cc = Components.classes;
11 let Cr = Components.results;
12 let Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
14 this.EXPORTED_SYMBOLS = ["BrowserElementPromptService"];
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
17 Cu.import("resource://gre/modules/Services.jsm");
19 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
20 const BROWSER_FRAMES_ENABLED_PREF = "dom.mozBrowserFramesEnabled";
22 function debug(msg) {
23   //dump("BrowserElementPromptService - " + msg + "\n");
26 function BrowserElementPrompt(win, browserElementChild) {
27   this._win = win;
28   this._browserElementChild = browserElementChild;
31 BrowserElementPrompt.prototype = {
32   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
34   alert: function(title, text) {
35     this._browserElementChild.showModalPrompt(
36       this._win, {promptType: "alert", title: title, message: text, returnValue: undefined});
37   },
39   alertCheck: function(title, text, checkMsg, checkState) {
40     // Treat this like a normal alert() call, ignoring the checkState.  The
41     // front-end can do its own suppression of the alert() if it wants.
42     this.alert(title, text);
43   },
45   confirm: function(title, text) {
46     return this._browserElementChild.showModalPrompt(
47       this._win, {promptType: "confirm", title: title, message: text, returnValue: undefined});
48   },
50   confirmCheck: function(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: function(title, text, buttonFlags, button0Title, button1Title,
67                       button2Title, checkMsg, checkState) {
68     let buttonProperties = this._buildConfirmExButtonProperties(buttonFlags,
69                                                                 button0Title,
70                                                                 button1Title,
71                                                                 button2Title);
72     let defaultReturnValue = { selectedButton: buttonProperties.defaultButton };
73     if (checkMsg) {
74       defaultReturnValue.checked = checkState.value;
75     }
76     let ret = this._browserElementChild.showModalPrompt(
77       this._win,
78       {
79         promptType: "custom-prompt",
80         title: title,
81         message: text,
82         defaultButton: buttonProperties.defaultButton,
83         buttons: buttonProperties.buttons,
84         showCheckbox: !!checkMsg,
85         checkboxMessage: checkMsg,
86         checkboxCheckedByDefault: !!checkState.value,
87         returnValue: defaultReturnValue
88       }
89     );
90     if (checkMsg) {
91       checkState.value = ret.checked;
92     }
93     return buttonProperties.indexToButtonNumberMap[ret.selectedButton];
94   },
96   prompt: function(title, text, value, checkMsg, checkState) {
97     let rv = this._browserElementChild.showModalPrompt(
98       this._win,
99       { promptType: "prompt",
100         title: title,
101         message: text,
102         initialValue: value.value,
103         returnValue: null });
105     value.value = rv;
107     // nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt,
108     // and false if the user pressed "Cancel".
109     //
110     // BrowserElementChild returns null for "Cancel" and returns the string the
111     // user entered otherwise.
112     return rv !== null;
113   },
115   promptUsernameAndPassword: function(title, text, username, password, checkMsg, checkState) {
116     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
117   },
119   promptPassword: function(title, text, password, checkMsg, checkState) {
120     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
121   },
123   select: function(title, text, aCount, aSelectList, aOutSelection) {
124     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
125   },
127   _buildConfirmExButtonProperties: function(buttonFlags, button0Title,
128                                             button1Title, button2Title) {
129     let r = {
130       defaultButton: -1,
131       buttons: [],
132       // This map is for translating array index to the button number that
133       // is recognized by Gecko. This shouldn't be exposed to embedder.
134       indexToButtonNumberMap: []
135     };
137     let defaultButton = 0;  // Default to Button 0.
138     if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
139       defaultButton = 1;
140     } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
141       defaultButton = 2;
142     }
144     // Properties of each button.
145     let buttonPositions = [
146       Ci.nsIPrompt.BUTTON_POS_0,
147       Ci.nsIPrompt.BUTTON_POS_1,
148       Ci.nsIPrompt.BUTTON_POS_2
149     ];
151     function buildButton(buttonTitle, buttonNumber) {
152       let ret = {};
153       let buttonPosition = buttonPositions[buttonNumber];
154       let mask = 0xff * buttonPosition;  // 8 bit mask
155       let titleType = (buttonFlags & mask) / buttonPosition;
157       ret.messageType = 'builtin';
158       switch(titleType) {
159       case Ci.nsIPrompt.BUTTON_TITLE_OK:
160         ret.message = 'ok';
161         break;
162       case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
163         ret.message = 'cancel';
164         break;
165       case Ci.nsIPrompt.BUTTON_TITLE_YES:
166         ret.message = 'yes';
167         break;
168       case Ci.nsIPrompt.BUTTON_TITLE_NO:
169         ret.message = 'no';
170         break;
171       case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
172         ret.message = 'save';
173         break;
174       case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
175         ret.message = 'dontsave';
176         break;
177       case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
178         ret.message = 'revert';
179         break;
180       case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
181         ret.message = buttonTitle;
182         ret.messageType = 'custom';
183         break;
184       default:
185         // This button is not shown.
186         return;
187       }
189       // If this is the default button, set r.defaultButton to
190       // the index of this button in the array. This value is going to be
191       // exposed to the embedder.
192       if (defaultButton === buttonNumber) {
193         r.defaultButton = r.buttons.length;
194       }
195       r.buttons.push(ret);
196       r.indexToButtonNumberMap.push(buttonNumber);
197     }
199     buildButton(button0Title, 0);
200     buildButton(button1Title, 1);
201     buildButton(button2Title, 2);
203     // If defaultButton is still -1 here, it means the default button won't
204     // be shown.
205     if (r.defaultButton === -1) {
206       throw new Components.Exception("Default button won't be shown",
207                                      Cr.NS_ERROR_FAILURE);
208     }
210     return r;
211   },
215 function BrowserElementAuthPrompt() {
218 BrowserElementAuthPrompt.prototype = {
219   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
221   promptAuth: function promptAuth(channel, level, authInfo) {
222     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
223   },
225   asyncPromptAuth: function asyncPromptAuth(channel, callback, context, level, authInfo) {
226     debug("asyncPromptAuth");
228     // The cases that we don't support now.
229     if ((authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) &&
230         (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)) {
231       throw Cr.NS_ERROR_FAILURE;
232     }
234     let frame = this._getFrameFromChannel(channel);
235     if (!frame) {
236       debug("Cannot get frame, asyncPromptAuth fail");
237       throw Cr.NS_ERROR_FAILURE;
238     }
240     let browserElementParent =
241       BrowserElementPromptService.getBrowserElementParentForFrame(frame);
243     if (!browserElementParent) {
244       debug("Failed to load browser element parent.");
245       throw Cr.NS_ERROR_FAILURE;
246     }
248     let consumer = {
249       QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
250       callback: callback,
251       context: context,
252       cancel: function() {
253         this.callback.onAuthCancelled(this.context, false);
254         this.callback = null;
255         this.context = null;
256       }
257     };
259     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
260     let hashKey = level + "|" + hostname + "|" + httpRealm;
261     let asyncPrompt = this._asyncPrompts[hashKey];
262     if (asyncPrompt) {
263       asyncPrompt.consumers.push(consumer);
264       return consumer;
265     }
267     asyncPrompt = {
268       consumers: [consumer],
269       channel: channel,
270       authInfo: authInfo,
271       level: level,
272       inProgress: false,
273       browserElementParent: browserElementParent
274     };
276     this._asyncPrompts[hashKey] = asyncPrompt;
277     this._doAsyncPrompt();
278     return consumer;
279   },
281   // Utilities for nsIAuthPrompt2 ----------------
283   _asyncPrompts: {},
284   _asyncPromptInProgress: new WeakMap(),
285   _doAsyncPrompt: function() {
286     // Find the key of a prompt whose browser element parent does not have
287     // async prompt in progress.
288     let hashKey = null;
289     for (let key in this._asyncPrompts) {
290       let prompt = this._asyncPrompts[key];
291       if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
292         hashKey = key;
293         break;
294       }
295     }
297     // Didn't find an available prompt, so just return.
298     if (!hashKey)
299       return;
301     let prompt = this._asyncPrompts[hashKey];
302     let [hostname, httpRealm] = this._getAuthTarget(prompt.channel,
303                                                     prompt.authInfo);
305     this._asyncPromptInProgress.set(prompt.browserElementParent, true);
306     prompt.inProgress = true;
308     let self = this;
309     let callback = function(ok, username, password) {
310       debug("Async auth callback is called, ok = " +
311             ok + ", username = " + username);
313       // Here we got the username and password provided by embedder, or
314       // ok = false if the prompt was cancelled by embedder.
315       delete self._asyncPrompts[hashKey];
316       prompt.inProgress = false;
317       self._asyncPromptInProgress.delete(prompt.browserElementParent);
319       // Fill authentication information with username and password provided
320       // by user.
321       let flags = prompt.authInfo.flags;
322       if (username) {
323         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
324           // Domain is separated from username by a backslash
325           let idx = username.indexOf("\\");
326           if (idx == -1) {
327             prompt.authInfo.username = username;
328           } else {
329             prompt.authInfo.domain   = username.substring(0, idx);
330             prompt.authInfo.username = username.substring(idx + 1);
331           }
332         } else {
333           prompt.authInfo.username = username;
334         }
335       }
337       if (password) {
338         prompt.authInfo.password = password;
339       }
341       for each (let consumer in prompt.consumers) {
342         if (!consumer.callback) {
343           // Not having a callback means that consumer didn't provide it
344           // or canceled the notification.
345           continue;
346         }
348         try {
349           if (ok) {
350             debug("Ok, calling onAuthAvailable to finish auth");
351             consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
352           } else {
353             debug("Cancelled, calling onAuthCancelled to finish auth.");
354             consumer.callback.onAuthCancelled(consumer.context, true);
355           }
356         } catch (e) { /* Throw away exceptions caused by callback */ }
357       }
359       // Process the next prompt, if one is pending.
360       self._doAsyncPrompt();
361     };
363     let runnable = {
364       run: function() {
365         // Call promptAuth of browserElementParent, to show the prompt.
366         prompt.browserElementParent.promptAuth(
367           self._createAuthDetail(prompt.channel, prompt.authInfo),
368           callback);
369       }
370     }
372     Services.tm.currentThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
373   },
375   _getFrameFromChannel: function(channel) {
376     let loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
377     return loadContext.topFrameElement;
378   },
380   _createAuthDetail: function(channel, authInfo) {
381     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
382     return {
383       host:             hostname,
384       realm:            httpRealm,
385       username:         authInfo.username,
386       isOnlyPassword:   !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
387     };
388   },
390   _getAuthTarget : function (channel, authInfo) {
391     let hostname = this._getFormattedHostname(channel.URI);
393     // If a HTTP WWW-Authenticate header specified a realm, that value
394     // will be available here. If it wasn't set or wasn't HTTP, we'll use
395     // the formatted hostname instead.
396     let realm = authInfo.realm;
397     if (!realm)
398       realm = hostname;
400     return [hostname, realm];
401   },
403   _getFormattedHostname : function(uri) {
404     let scheme = uri.scheme;
405     let hostname = scheme + "://" + uri.host;
407     // If the URI explicitly specified a port, only include it when
408     // it's not the default. (We never want "http://foo.com:80")
409     let port = uri.port;
410     if (port != -1) {
411       let handler = Services.io.getProtocolHandler(scheme);
412       if (port != handler.defaultPort)
413         hostname += ":" + port;
414     }
415     return hostname;
416   }
420 function AuthPromptWrapper(oldImpl, browserElementImpl) {
421   this._oldImpl = oldImpl;
422   this._browserElementImpl = browserElementImpl;
425 AuthPromptWrapper.prototype = {
426   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
427   promptAuth: function(channel, level, authInfo) {
428     if (this._canGetParentElement(channel)) {
429       return this._browserElementImpl.promptAuth(channel, level, authInfo);
430     } else {
431       return this._oldImpl.promptAuth(channel, level, authInfo);
432     }
433   },
435   asyncPromptAuth: function(channel, callback, context, level, authInfo) {
436     if (this._canGetParentElement(channel)) {
437       return this._browserElementImpl.asyncPromptAuth(channel, callback, context, level, authInfo);
438     } else {
439       return this._oldImpl.asyncPromptAuth(channel, callback, context, level, authInfo);
440     }
441   },
443   _canGetParentElement: function(channel) {
444     try {
445       let context = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
446       let frame = context.topFrameElement;
447       if (!frame) {
448         // This function returns a boolean value
449         return !!context.nestedFrameId;
450       }
452       if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame))
453         return false;
455       return true;
456     } catch (e) {
457       return false;
458     }
459   }
462 function BrowserElementPromptFactory(toWrap) {
463   this._wrapped = toWrap;
466 BrowserElementPromptFactory.prototype = {
467   classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"),
468   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
470   _mayUseNativePrompt: function() {
471     try {
472       return Services.prefs.getBoolPref("browser.prompt.allowNative");
473     } catch (e) {
474       // This properity is default to true.
475       return true;
476     }
477   },
479   _getNativePromptIfAllowed: function(win, iid, err) {
480     if (this._mayUseNativePrompt())
481       return this._wrapped.getPrompt(win, iid);
482     else {
483       // Not allowed, throw an exception.
484       throw err;
485     }
486   },
488   getPrompt: function(win, iid) {
489     // It is possible for some object to get a prompt without passing
490     // valid reference of window, like nsNSSComponent. In such case, we
491     // should just fall back to the native prompt service
492     if (!win)
493       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
495     if (iid.number != Ci.nsIPrompt.number &&
496         iid.number != Ci.nsIAuthPrompt2.number) {
497       debug("We don't recognize the requested IID (" + iid + ", " +
498             "allowed IID: " +
499             "nsIPrompt=" + Ci.nsIPrompt + ", " +
500             "nsIAuthPrompt2=" + Ci.nsIAuthPrompt2 + ")");
501       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
502     }
504     // Try to find a BrowserElementChild for the window.
505     let browserElementChild =
506       BrowserElementPromptService.getBrowserElementChildForWindow(win);
508     if (iid.number === Ci.nsIAuthPrompt2.number) {
509       debug("Caller requests an instance of nsIAuthPrompt2.");
511       if (browserElementChild) {
512         // If we are able to get a BrowserElementChild, it means that
513         // the auth prompt is for a mozbrowser. Therefore we don't need to
514         // fall back.
515         return new BrowserElementAuthPrompt().QueryInterface(iid);
516       }
518       // Because nsIAuthPrompt2 is called in parent process. If caller
519       // wants nsIAuthPrompt2 and we cannot get BrowserElementchild,
520       // it doesn't mean that we should fallback. It is possible that we can
521       // get the BrowserElementParent from nsIChannel that passed to
522       // functions of nsIAuthPrompt2.
523       if (this._mayUseNativePrompt()) {
524         return new AuthPromptWrapper(
525             this._wrapped.getPrompt(win, iid),
526             new BrowserElementAuthPrompt().QueryInterface(iid))
527           .QueryInterface(iid);
528       } else {
529         // Falling back is not allowed, so we don't need wrap the
530         // BrowserElementPrompt.
531         return new BrowserElementAuthPrompt().QueryInterface(iid);
532       }
533     }
535     if (!browserElementChild) {
536       debug("We can't find a browserElementChild for " +
537             win + ", " + win.location);
538       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
539     }
541     debug("Returning wrapped getPrompt for " + win);
542     return new BrowserElementPrompt(win, browserElementChild)
543                                    .QueryInterface(iid);
544   }
547 this.BrowserElementPromptService = {
548   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
549                                          Ci.nsISupportsWeakReference]),
551   _initialized: false,
553   _init: function() {
554     if (this._initialized) {
555       return;
556     }
558     // If the pref is disabled, do nothing except wait for the pref to change.
559     if (!this._browserFramesPrefEnabled()) {
560       var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
561       prefs.addObserver(BROWSER_FRAMES_ENABLED_PREF, this, /* ownsWeak = */ true);
562       return;
563     }
565     this._initialized = true;
566     this._browserElementParentMap = new WeakMap();
568     var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
569     os.addObserver(this, "outer-window-destroyed", /* ownsWeak = */ true);
571     // Wrap the existing @mozilla.org/prompter;1 implementation.
572     var contractID = "@mozilla.org/prompter;1";
573     var oldCID = Cm.contractIDToCID(contractID);
574     var newCID = BrowserElementPromptFactory.prototype.classID;
575     var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory);
577     if (oldCID == newCID) {
578       debug("WARNING: Wrapped prompt factory is already installed!");
579       return;
580     }
582     Cm.unregisterFactory(oldCID, oldFactory);
584     var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
585     var newInstance = new BrowserElementPromptFactory(oldInstance);
587     var newFactory = {
588       createInstance: function(outer, iid) {
589         if (outer != null) {
590           throw Cr.NS_ERROR_NO_AGGREGATION;
591         }
592         return newInstance.QueryInterface(iid);
593       }
594     };
595     Cm.registerFactory(newCID,
596                        "BrowserElementPromptService's prompter;1 wrapper",
597                        contractID, newFactory);
599     debug("Done installing new prompt factory.");
600   },
602   _getOuterWindowID: function(win) {
603     return win.QueryInterface(Ci.nsIInterfaceRequestor)
604               .getInterface(Ci.nsIDOMWindowUtils)
605               .outerWindowID;
606   },
608   _browserElementChildMap: {},
609   mapWindowToBrowserElementChild: function(win, browserElementChild) {
610     this._browserElementChildMap[this._getOuterWindowID(win)] = browserElementChild;
611   },
613   getBrowserElementChildForWindow: function(win) {
614     // We only have a mapping for <iframe mozbrowser>s, not their inner
615     // <iframes>, so we look up win.top below.  window.top (when called from
616     // script) respects <iframe mozbrowser> boundaries.
617     return this._browserElementChildMap[this._getOuterWindowID(win.top)];
618   },
620   mapFrameToBrowserElementParent: function(frame, browserElementParent) {
621     this._browserElementParentMap.set(frame, browserElementParent);
622   },
624   getBrowserElementParentForFrame: function(frame) {
625     return this._browserElementParentMap.get(frame);
626   },
628   _observeOuterWindowDestroyed: function(outerWindowID) {
629     let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
630     debug("observeOuterWindowDestroyed " + id);
631     delete this._browserElementChildMap[outerWindowID.data];
632   },
634   _browserFramesPrefEnabled: function() {
635     var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
636     try {
637       return prefs.getBoolPref(BROWSER_FRAMES_ENABLED_PREF);
638     }
639     catch(e) {
640       return false;
641     }
642   },
644   observe: function(subject, topic, data) {
645     switch(topic) {
646     case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
647       if (data == BROWSER_FRAMES_ENABLED_PREF) {
648         this._init();
649       }
650       break;
651     case "outer-window-destroyed":
652       this._observeOuterWindowDestroyed(subject);
653       break;
654     default:
655       debug("Observed unexpected topic " + topic);
656     }
657   }
660 BrowserElementPromptService._init();