Author: beaton
Date: Tue Feb 17 16:50:03 2009
New Revision: 745142

URL: http://svn.apache.org/viewvc?rev=745142&view=rev
Log:
Implement oauth popup as a feature so code is easier to share.

This resolves SHINDIG-747.  There are minor differences between
what I've implemented and what the spec says, so I've sent in
a patch to the spec as well.

This also creates a mock window object that supports
window.setInterval, window.open, and window.close.



Added:
    incubator/shindig/trunk/features/mocks/window.js
    incubator/shindig/trunk/features/oauthpopup/
    incubator/shindig/trunk/features/oauthpopup/feature.xml
    incubator/shindig/trunk/features/oauthpopup/oauthpopup-test.js
    incubator/shindig/trunk/features/oauthpopup/oauthpopup.js
Removed:
    incubator/shindig/trunk/javascript/samplecontainer/examples/popup.js
Modified:
    incubator/shindig/trunk/features/features.txt
    incubator/shindig/trunk/features/mocks/env.js
    incubator/shindig/trunk/features/pom.xml
    incubator/shindig/trunk/javascript/samplecontainer/examples/oauth.xml

Modified: incubator/shindig/trunk/features/features.txt
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/features.txt?rev=745142&r1=745141&r2=745142&view=diff
==============================================================================
--- incubator/shindig/trunk/features/features.txt (original)
+++ incubator/shindig/trunk/features/features.txt Tue Feb 17 16:50:03 2009
@@ -9,6 +9,7 @@
 features/flash/feature.xml
 features/locked-domain/feature.xml
 features/minimessage/feature.xml
+features/oauthpopup/feature.xml
 features/opensocial-0.6/feature.xml
 features/opensocial-0.7/feature.xml
 features/opensocial-0.8/feature.xml

Modified: incubator/shindig/trunk/features/mocks/env.js
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/mocks/env.js?rev=745142&r1=745141&r2=745142&view=diff
==============================================================================
--- incubator/shindig/trunk/features/mocks/env.js (original)
+++ incubator/shindig/trunk/features/mocks/env.js Tue Feb 17 16:50:03 2009
@@ -30,5 +30,5 @@
   }
 };
 
+// See mocks.FakeWindow if you need something more full featured.
 var window = {};
-

Added: incubator/shindig/trunk/features/mocks/window.js
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/mocks/window.js?rev=745142&view=auto
==============================================================================
--- incubator/shindig/trunk/features/mocks/window.js (added)
+++ incubator/shindig/trunk/features/mocks/window.js Tue Feb 17 16:50:03 2009
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Provides a simulated window object.  Recommended usage is to reset
+ * the global window object in each test case that uses the window.
+ *
+ * Example:
+ * 
+ * ExampleTest.prototype.testSomething = function() {
+ *   window = new mocks.FakeWindow();
+ *   // Test things
+ * };
+ */
+
+var mocks = mocks || {};
+
+/**
+ * @constructor
+ * @description creates a new fake window object.
+ * @param {String} url Destination url.
+ * @param {String} target Name of window
+ * @param {String} options Options for window, such as size.
+ */
+mocks.FakeWindow = function(url, target, options) {
+  // Properties passed to window.open
+  this.url_ = url;
+  this.target_ = target;
+  this.options_ = options;
+
+  // Whether the window has been closed.
+  this.closed = false;
+
+  // Event handling.  Events array is always sorted in order of ascending
+  // execution time.
+  this.now_ = 1000000;
+  this.events_ = [];
+  this.nextEventId_ = 1000;
+};
+
+/**
+ * Replacement for window.open.
+ */
+mocks.FakeWindow.prototype.open = function(url, target, options) {
+  return new mocks.FakeWindow(url, target, options);
+};
+
+/**
+ * Replacement for window.close.
+ */
+mocks.FakeWindow.prototype.close = function() {
+  this.closed = true;
+};
+
+/**
+ * Replacement for window.setInterval
+ */
+mocks.FakeWindow.prototype.setInterval = function(callback, millis) {
+  var event = {
+    id: this.nextEventId_,
+    when: this.now_ += millis,
+    interval: millis,
+    callback: callback
+  };
+  this.events_.push(event);
+  this.sortEvents_();
+  ++this.nextEventId_;
+  return event.id;
+};
+
+mocks.FakeWindow.prototype.sortEvents_ = function(event) {
+  this.events_.sort(function (a, b) {
+    return a.when - b.when;
+  });
+};
+
+/**
+ * Replacement for window.clearInterval
+ */
+mocks.FakeWindow.prototype.clearInterval = function(id) {
+  // Removes a single event by copying everything but that event.
+  var remaining = [];
+  for (var i = 0; i < this.events_.length; ++i) {
+    e = this.events_[i];
+    if (e.id !== id) {
+      remaining.push(e);
+    }
+  }
+  if (this.events_.length === remaining.length) {
+    throw 'window.clearInterval failed, no event with id ' + id;
+  }
+  this.events_ = remaining;
+};
+
+/**
+ * Moves the clock forward, running any associated events.
+ */
+mocks.FakeWindow.prototype.incrementTime = function(millis) {
+  if (this.active) {
+    throw 'recursive invocation of window.incrementTime.  Cut that out';
+  }
+  this.active = true;
+
+  // Each iteration bumps the time just enough to run a single event, or
+  // else ends the loop.
+  var finish = this.now_ + millis;
+  do {
+    var ranEvent = false;
+    if (this.events_.length > 0) {
+      var e = this.events_[0];
+      if (e.when <= finish) {
+        this.now_ = e.when;
+        e.when += e.interval;
+        this.sortEvents_();
+        // Deliberately let exceptions propagate, it's probably a bug if
+        // a timer throws an exception.
+        e.callback();
+        ranEvent = true;
+      }
+    }
+  } while (ranEvent);
+  this.now_ = finish;
+  this.active = false;
+};

Added: incubator/shindig/trunk/features/oauthpopup/feature.xml
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/oauthpopup/feature.xml?rev=745142&view=auto
==============================================================================
--- incubator/shindig/trunk/features/oauthpopup/feature.xml (added)
+++ incubator/shindig/trunk/features/oauthpopup/feature.xml Tue Feb 17 16:50:03 
2009
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+-->
+<feature>
+  <name>oauthpopup</name>
+  <gadget>
+    <script src="oauthpopup.js"/>
+  </gadget>
+</feature>

Added: incubator/shindig/trunk/features/oauthpopup/oauthpopup-test.js
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/oauthpopup/oauthpopup-test.js?rev=745142&view=auto
==============================================================================
--- incubator/shindig/trunk/features/oauthpopup/oauthpopup-test.js (added)
+++ incubator/shindig/trunk/features/oauthpopup/oauthpopup-test.js Tue Feb 17 
16:50:03 2009
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+var gadgets = gadgets || {};
+
+function PopupTest(name) {
+  TestCase.call(this, name);
+};
+PopupTest.inherits(TestCase);
+
+PopupTest.prototype.setUp = function() {
+  this.oldWindow = window;
+  window = new mocks.FakeWindow();
+};
+
+PopupTest.prototype.tearDown = function() {
+  window = this.oldWindow;
+};
+
+PopupTest.prototype.testPopup = function() {
+  var opened = false;
+  var open = function() {
+    opened = true;
+  };
+  var closed = false;
+  var close = function() {
+    closed = true;
+  };
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+  this.assertNull('Window opened prematurely', popup.win_);
+  this.assertFalse('Opener callback was called', opened);
+
+  // Open the window
+  var ranDefaultAction = openerOnClick();
+  this.assertTrue('Window not opened', opened);
+  this.assertFalse('Ran browser default action on open', ranDefaultAction);
+  this.assertNotNull('Window was null', popup.win_);
+  this.assertEquals('Url incorrect', 'destination', popup.win_.url_);
+  this.assertEquals('Target incorrect', '_blank', popup.win_.target_);
+  this.assertEquals('Options incorrect', 'options', popup.win_.options_);
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertFalse('closer callback called early', closed);
+
+  // User or site closes window
+  popup.win_.close();
+  window.incrementTime(100);
+  this.assertTrue('Closer callback not called', closed);
+};
+
+PopupTest.prototype.testPopup_userClick = function() {
+  var opened = false;
+  var open = function() {
+    opened = true;
+  };
+  var closed = false;
+  var close = function() {
+    closed = true;
+  };
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+
+  // Open the window
+  openerOnClick();
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertFalse('closer callback called early', closed);
+
+  // User clicks link
+  var ranDefaultAction = closerOnClick();
+  this.assertFalse(ranDefaultAction);
+  this.assertTrue('Closer callback not called', closed);
+};
+
+PopupTest.prototype.testTimerCancelled = function() {
+  var open = function() {};
+  var closeCount = 0;
+  var close = function() {
+    ++closeCount;
+  };
+
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+
+  // Open the window
+  openerOnClick();
+
+  // Close the window
+  popup.win_.close();
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertEquals('Wrong number of calls to close', 1, closeCount);
+  window.incrementTime(1000);
+  this.assertEquals('timer not cancelled', 1, closeCount);
+};

Added: incubator/shindig/trunk/features/oauthpopup/oauthpopup.js
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/oauthpopup/oauthpopup.js?rev=745142&view=auto
==============================================================================
--- incubator/shindig/trunk/features/oauthpopup/oauthpopup.js (added)
+++ incubator/shindig/trunk/features/oauthpopup/oauthpopup.js Tue Feb 17 
16:50:03 2009
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview API to assist with management of the OAuth popup window.
+ */
+
+/**
+ * @private
+ * @constructor
+ */
+var gadgets = gadgets || {};
+
+/**
+ * @private
+ * @constructor
+ */
+gadgets.oauth = gadgets.oauth || {};
+
+/**
+ * @class OAuth popup window manager.
+ *
+ * <p>
+ * Expected usage:
+ * </p>
+ *
+ * <ol>
+ * <li>
+ * <p>
+ * Gadget attempts to fetch OAuth data for the user and discovers that
+ * approval is needed.  The gadget creates two new UI elements:
+ * </p>
+ * <ul>
+ *   <li>
+ *      a "personalize this gadget" button or link.
+ *   </li>
+ *   <li>
+ *      a "personalization done" button or link, which is initially hidden.
+ *   </li>
+ * </ul>
+ * <p>
+ * The "personalization done" button may be unnecessary.  The popup window
+ * manager will attempt to detect when the window closes.  However, the 
+ * "personalization done" button should still be displayed to handle cases
+ * where the popup manager is unable to detect that a window has closed.  This
+ * allows the user to signal approval manually.
+ * </p>
+ * </li>
+ *
+ * <li>
+ * Gadget creates a popup object and associates event handlers with the UI
+ * elements:
+ *
+ * <pre>
+ *    // Called when the user opens the popup window.
+ *    var onOpen = function() {
+ *      $("personalizeDone").style.display = "block"
+ *    }
+ *    // Called when the user closes the popup window.
+ *    var onClose = function() {
+ *      $("personalizeDone").style.display = "none"
+ *      fetchData();
+ *    }
+ *    var popup = new gadgets.oauth.Popup(
+ *        response.oauthApprovalUrl,
+ *        "height=300,width=200",
+ *        onOpen,
+ *        onClose
+ *    );
+ *
+ *    personalizeButton.onclick = popup.createOpenerOnClick();
+ *    personalizeDoneButton.onclick = popup.createApprovedOnClick();
+ * </pre>
+ * </li>
+ *
+ * <li>
+ * <p>
+ * When the user clicks the personalization button/link, a window is opened
+ * to the approval URL.  The onOpen function is called to notify the gadget
+ * that the window was opened.
+ * </p>
+ * </li>
+ *
+ * <li>
+ * <p>
+ * When the window is closed, the popup manager calls the onClose function
+ * and the gadget attempts to fetch the user's data.
+ * </p>
+ * </li>
+ * </ol>
+ *
+ * @constructor
+ *
+ * @description used to create a new OAuth popup window manager.
+ *
+ * @param {String} destination Target URL for the popup window.
+ * @param {String} windowOptions Options for window.open, used to specify
+ *     look and feel of the window.
+ * @param {function} openCallback Function to call when the window is opened.
+ * @param {function} closeCallback Function to call when the window is closed.
+ */
+gadgets.oauth.Popup = function(destination, windowOptions, openCallback,
+    closeCallback) {
+  this.destination_ = destination;
+  this.windowOptions_ = windowOptions;
+  this.openCallback_ = openCallback;
+  this.closeCallback_ = closeCallback;
+  this.win_ = null;
+};
+
+/**
+ * @return an onclick handler for the "open the approval window" link
+ * @type function
+ */
+gadgets.oauth.Popup.prototype.createOpenerOnClick = function() {
+  var self = this;
+  return function() {
+    self.onClick_();
+  };
+};
+
+/**
+ * Called when the user clicks to open the popup window.
+ * 
+ * @returns false to prevent the default action for the click.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.onClick_ = function() {
+  // If a popup blocker blocks the window, we do nothing.  The user will
+  // need to approve the popup, then click again to open the window.
+  // Note that because we don't call window.open until the user has clicked
+  // something the popup blockers *should* let us through.
+  this.win_ = window.open(this.destination_, "_blank", this.windowOptions_);
+  if (this.win_) {
+    // Poll every 100ms to check if the window has been closed
+    var self = this;
+    var closure = function() {
+      self.checkClosed_(); 
+    };
+    this.timer_ = window.setInterval(closure, 100);
+    this.openCallback_();
+  }
+  return false;
+};
+
+/**
+ * Called at intervals to check whether the window has closed.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.checkClosed_ = function() {
+  if ((!this.win_) || this.win_.closed) {
+    this.win_ = null;
+    this.handleApproval_();
+  }
+};
+
+/**
+ * Called when we recieve an indication the user has approved access, either
+ * because they closed the popup window or clicked an "I've approved" button.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.handleApproval_ = function() {
+  if (this.timer_) {
+    window.clearInterval(this.timer_);
+    this.timer_ = null;
+  }
+  if (this.win_) {
+    this.win_.close();
+    this.win_ = null;
+  }
+  this.closeCallback_();
+  return false;
+};
+
+/**
+ * @return an onclick handler for the "I've approved" link.  This may not
+ * ever be called.  If we successfully detect that the window was closed,
+ * this link is unnecessary.
+ * @type function
+ */
+gadgets.oauth.Popup.prototype.createApprovedOnClick = function() {
+  var self = this;
+  return function() {
+    self.handleApproval_();
+  };
+};

Modified: incubator/shindig/trunk/features/pom.xml
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/pom.xml?rev=745142&r1=745141&r2=745142&view=diff
==============================================================================
--- incubator/shindig/trunk/features/pom.xml (original)
+++ incubator/shindig/trunk/features/pom.xml Tue Feb 17 16:50:03 2009
@@ -70,6 +70,7 @@
               <sourceDirectory>${basedir}</sourceDirectory>
               <sources>
                 <source>mocks/env.js</source>
+                <source>mocks/window.js</source>
                 <source>mocks/xhr.js</source>
                 <source>core/config.js</source>
                 <source>core/json.js</source>
@@ -103,6 +104,7 @@
                 <source>opensocial-base/jsonperson.js</source>
                 <source>opensocial-rest/restfulcontainer.js</source>
                 <source>opensocial-jsonrpc/jsonrpccontainer.js</source>
+                <source>oauthpopup/oauthpopup.js</source>
               </sources>
               <testSourceDirectory>${basedir}</testSourceDirectory>
               <testSuites>

Modified: incubator/shindig/trunk/javascript/samplecontainer/examples/oauth.xml
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/javascript/samplecontainer/examples/oauth.xml?rev=745142&r1=745141&r2=745142&view=diff
==============================================================================
--- incubator/shindig/trunk/javascript/samplecontainer/examples/oauth.xml 
(original)
+++ incubator/shindig/trunk/javascript/samplecontainer/examples/oauth.xml Tue 
Feb 17 16:50:03 2009
@@ -8,6 +8,7 @@
         <Authorization 
url="http://localhost:9090/oauth-provider/authorize?oauth_callback=http://localhost:8080/gadgets/oauthcallback";
 />
       </Service>
     </OAuth>
+    <Require feature="oauthpopup" />
     <Preload authz="oauth" href="http://localhost:9090/oauth-provider/echo"; />
   </ModulePrefs>
   <Content type="html">
@@ -35,8 +36,6 @@
       once you've approved access to your data.
     </div>
 
-    <script 
src="http://localhost:8080/gadgets/files/samplecontainer/examples/popup.js";></script>
-
     <script type="text/javascript">
       function $(x) {
         return document.getElementById(x);
@@ -66,14 +65,15 @@
           gadgets.io.MethodType.GET;
 
         gadgets.io.makeRequest(url, function (response) {
-          var popup = null;
           if (response.oauthApprovalUrl) {
-            popup = shindig.oauth.popup({
-              destination: response.oauthApprovalUrl,
-              windowOptions: null,
-              onOpen: function() { showOneSection('waiting'); },
-              onClose: function() { fetchData(); }
-            });
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
             $('personalize').onclick = popup.createOpenerOnClick();
             $('approvaldone').onclick = popup.createApprovedOnClick();
             showOneSection('approval');


Reply via email to