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');