1//  Copyright 2009 Google Inc.
2//
3//  Licensed under the Apache License, Version 2.0 (the "License");
4//  you may not use this file except in compliance with the License.
5//  You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14
15
16//  PopupManager is a library to facilitate integration with OpenID
17//  identity providers (OP)s that support a pop-up authentication interface.
18//  To create a popup window, you first construct a popupOpener customized
19//  for your site and a particular identity provider, E.g.:
20//
21//  var googleOpener = popupManager.createOpener(openidParams);
22//
23//  where 'openidParams' are customized for Google in this instance.
24//  (typically you just change the openidpoint, the version number
25//  (the openid.ns parameter) and the extensions based on what
26//  the OP supports.
27//  OpenID libraries can often discover these properties
28//  automatically from the location of an XRD document.
29//
30//  Then, you can either directly call
31//  googleOpener.popup(width, height), where 'width' and 'height' are your choices
32//  for popup size, or you can display a button 'Sign in with Google' and set the
33//..'onclick' handler of the button to googleOpener.popup()
34
35var popupManager = {};
36
37// Library constants
38
39popupManager.constants = {
40  'darkCover' : 'popupManager_darkCover_div',
41  'darkCoverStyle' : ['position:absolute;',
42                      'top:0px;',
43                      'left:0px;',
44                      'padding-right:0px;',
45                      'padding-bottom:0px;',
46                      'background-color:#000000;',
47                      'opacity:0.5;', //standard-compliant browsers
48                      '-moz-opacity:0.5;',           // old Mozilla
49                      'filter:alpha(opacity=0.5);',  // IE
50                      'z-index:10000;',
51                      'width:100%;',
52                      'height:100%;'
53                      ].join(''),
54  'openidSpec' : {
55     'identifier_select' : 'http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select',
56     'namespace2' : 'http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0'
57  } };
58
59// Computes the size of the window contents. Returns a pair of
60// coordinates [width, height] which can be [0, 0] if it was not possible
61// to compute the values.
62popupManager.getWindowInnerSize = function() {
63  var width = 0;
64  var height = 0;
65  var elem = null;
66  if ('innerWidth' in window) {
67    // For non-IE
68    width = window.innerWidth;
69    height = window.innerHeight;
70  } else {
71    // For IE,
72    if (('BackCompat' === window.document.compatMode)
73        && ('body' in window.document)) {
74        elem = window.document.body;
75    } else if ('documentElement' in window.document) {
76      elem = window.document.documentElement;
77    }
78    if (elem !== null) {
79      width = elem.offsetWidth;
80      height = elem.offsetHeight;
81    }
82  }
83  return [width, height];
84};
85
86// Computes the coordinates of the parent window.
87// Gets the coordinates of the parent frame
88popupManager.getParentCoords = function() {
89  var width = 0;
90  var height = 0;
91  if ('screenLeft' in window) {
92    // IE-compatible variants
93    width = window.screenLeft;
94    height = window.screenTop;
95  } else if ('screenX' in window) {
96    // Firefox-compatible
97    width = window.screenX;
98    height = window.screenY;
99  }
100  return [width, height];
101};
102
103// Computes the coordinates of the new window, so as to center it
104// over the parent frame
105popupManager.getCenteredCoords = function(width, height) {
106   var parentSize = this.getWindowInnerSize();
107   var parentPos = this.getParentCoords();
108   var xPos = parentPos[0] +
109       Math.max(0, Math.floor((parentSize[0] - width) / 2));
110   var yPos = parentPos[1] +
111       Math.max(0, Math.floor((parentSize[1] - height) / 2));
112   return [xPos, yPos];
113};
114
115//  A utility class, implements an onOpenHandler that darkens the screen
116//  by overlaying it with a semi-transparent black layer. To use, ensure that
117//  no screen element has a z-index at or above 10000.
118//  This layer will be suppressed automatically after the screen closes.
119//
120//  Note: If you want to perform other operations before opening the popup, but
121//  also would like the screen to darken, you can define a custom handler
122//  as such:
123//  var myOnOpenHandler = function(inputs) {
124//    .. do something
125//    popupManager.darkenScreen();
126//    .. something else
127//  };
128//  Then you pass myOnOpenHandler as input to the opener, as in:
129//  var openidParams = {};
130//  openidParams.onOpenHandler = myOnOpenHandler;
131//  ... other customizations
132//  var myOpener = popupManager.createOpener(openidParams);
133popupManager.darkenScreen = function() {
134  var darkCover = window.document.getElementById(window.popupManager.constants['darkCover']);
135  if (!darkCover) {
136    darkCover = window.document.createElement('div');
137    darkCover['id'] = window.popupManager.constants['darkCover'];
138    darkCover.setAttribute('style', window.popupManager.constants['darkCoverStyle']);
139    window.document.body.appendChild(darkCover);
140  }
141  darkCover.style.visibility = 'visible';
142};
143
144//  Returns a an object that can open a popup window customized for an OP & RP.
145//  to use you call var opener = popupManager.cretePopupOpener(openidParams);
146//  and then you can assign the 'onclick' handler of a button to
147//  opener.popup(width, height), where width and height are the values of the popup size;
148//
149//  To use it, you would typically have code such as:
150//  var myLoginCheckFunction = ...  some AJAXy call or page refresh operation
151//  that will cause the user to see the logged-in experience in the current page.
152//  var openidParams = { realm : 'openid.realm', returnToUrl : 'openid.return_to',
153//  opEndpoint : 'openid.op_endpoint', onCloseHandler : myLoginCheckFunction,
154//  shouldEncodeUrls : 'true' (default) or 'false', extensions : myOpenIDExtensions };
155//
156//  Here extensions include any OpenID extensions that you support. For instance,
157//  if you support Attribute Exchange v.1.0, you can say:
158//  (Example for attribute exchange request for email and name,
159//  assuming that shouldEncodeUrls = 'true':)
160//  var myOpenIDExtensions = {
161//      'openid.ax.ns' : 'http://openid.net/srv/ax/1.0',
162//      'openid.ax.type.email' : 'http://axschema.org/contact/email',
163//      'openid.ax.type.name1' : 'http://axschema.org/namePerson/first',
164//      'openid.ax.type.name2' : 'http://axschema.org/namePerson/last',
165//      'openid.ax.required' : 'email,name1,name2' };
166//  Note that the 'ui' namespace is reserved by this library for the OpenID
167//  UI extension, and that the mode 'popup' is automatically applied.
168//  If you wish to make use of the 'language' feature of the OpenID UI extension
169//  simply add the following entry (example assumes the language requested
170//  is Swiss French:
171//  var my OpenIDExtensions = {
172//    ... // other extension parameters
173//    'openid.ui.language' : 'fr_CH',
174//    ... };
175popupManager.createPopupOpener = (function(openidParams) {
176  var interval_ = null;
177  var popupWindow_ = null;
178  var that = this;
179  var shouldEscape_ = ('shouldEncodeUrls' in openidParams) ? openidParams.shouldEncodeUrls : true;
180  var encodeIfRequested_ = function(url) {
181    return (shouldEscape_ ? encodeURIComponent(url) : url);
182  };
183  var identifier_ = ('identifier' in openidParams) ? encodeIfRequested_(openidParams.identifier) :
184      this.constants.openidSpec.identifier_select;
185  var identity_ = ('identity' in openidParams) ? encodeIfRequested_(openidParams.identity) :
186      this.constants.openidSpec.identifier_select;
187  var openidNs_ = ('namespace' in openidParams) ? encodeIfRequested_(openidParams.namespace) :
188      this.constants.openidSpec.namespace2;
189  var onOpenHandler_ = (('onOpenHandler' in openidParams) &&
190      ('function' === typeof(openidParams.onOpenHandler))) ?
191          openidParams.onOpenHandler : this.darkenScreen;
192  var onCloseHandler_ = (('onCloseHandler' in openidParams) &&
193      ('function' === typeof(openidParams.onCloseHandler))) ?
194          openidParams.onCloseHandler : null;
195  var returnToUrl_ = ('returnToUrl' in openidParams) ? openidParams.returnToUrl : null;
196  var realm_ = ('realm' in openidParams) ? openidParams.realm : null;
197  var endpoint_ = ('opEndpoint' in openidParams) ? openidParams.opEndpoint : null;
198  var extensions_ = ('extensions' in openidParams) ? openidParams.extensions : null;
199
200  // processes key value pairs, escaping any input;
201  var keyValueConcat_ = function(keyValuePairs) {
202    var result = "";
203    for (key in keyValuePairs) {
204      result += ['&', key, '=', encodeIfRequested_(keyValuePairs[key])].join('');
205    }
206    return result;
207  };
208
209  //Assembles the OpenID request from customizable parameters
210  var buildUrlToOpen_ = function() {
211    var connector = '&';
212    var encodedUrl = null;
213    var urlToOpen = null;
214    if ((null === endpoint_) || (null === returnToUrl_)) {
215      return;
216    }
217    if (endpoint_.indexOf('?') === -1) {
218      connector = '?';
219    }
220    encodedUrl = encodeIfRequested_(returnToUrl_);
221    urlToOpen = [ endpoint_, connector,
222        'openid.ns=', openidNs_,
223        '&openid.mode=checkid_setup',
224        '&openid.claimed_id=', identifier_,
225        '&openid.identity=', identity_,
226        '&openid.return_to=', encodedUrl ].join('');
227    if (realm_ !== null) {
228      urlToOpen += "&openid.realm=" + encodeIfRequested_(realm_);
229    }
230    if (extensions_ !== null) {
231      urlToOpen += keyValueConcat_(extensions_);
232    }
233    urlToOpen += '&openid.ns.ui=' + encodeURIComponent(
234        'http://specs.openid.net/extensions/ui/1.0');
235    urlToOpen += '&openid.ui.mode=popup';
236    return urlToOpen;
237  };
238
239  // Tests that the popup window has closed
240  var isPopupClosed_ = function() {
241    return (!popupWindow_ || popupWindow_.closed);
242  };
243
244  // Check to perform at each execution of the timed loop. It also triggers
245  // the action that follows the closing of the popup
246  var waitForPopupClose_ = function() {
247    if (isPopupClosed_()) {
248      popupWindow_ = null;
249      var darkCover = window.document.getElementById(window.popupManager.constants['darkCover']);
250      if (darkCover) {
251        darkCover.style.visibility = 'hidden';
252      }
253      if (onCloseHandler_ !== null) {
254        onCloseHandler_();
255      }
256      if ((null !== interval_)) {
257        window.clearInterval(interval_);
258        interval_ = null;
259      }
260    }
261  };
262
263  return {
264    // Function that opens the window.
265    popup: function(width, height) {
266      var urlToOpen = buildUrlToOpen_();
267      if (onOpenHandler_ !== null) {
268        onOpenHandler_();
269      }
270      var coordinates = that.getCenteredCoords(width, height);
271      popupWindow_ = window.open(urlToOpen, "",
272          "width=" + width + ",height=" + height +
273          ",status=1,location=1,resizable=yes" +
274          ",left=" + coordinates[0] +",top=" + coordinates[1]);
275      interval_ = window.setInterval(waitForPopupClose_, 80);
276      return true;
277    }
278  };
279});
280