Add a tryOAuth2() callback in the data loader iframe.

The idea is that if we aren't authorized to get some jsonp data (missing an
auth header, cookie, etc), the data provider can call this callback and
attempt to get an authorization key for the service.  authtest.json is an
example of how this works (the example doesn't actually validate the auth=
parameter, it just checks to make sure it's there, and if not, tries to get a
Google OAuth2 token for a random Google API).
diff --git a/authtest.json b/authtest.json
new file mode 100644
index 0000000..1c42e48
--- /dev/null
+++ b/authtest.json
@@ -0,0 +1,7 @@
+console.debug('in authtest.json', [jsonp_url]);
+if (jsonp_url.indexOf('auth=') >= 0) {
+  jsonp([['ok'], [true]]);
+} else {
+  tryOAuth2('https://accounts.google.com/o/oauth2/auth' +
+            '?scope=https://www.googleapis.com/auth/spreadsheets.readonly');
+}
\ No newline at end of file
diff --git a/render.js b/render.js
index 0defc6d..82a1eef 100644
--- a/render.js
+++ b/render.js
@@ -1097,6 +1097,13 @@
   }
 
 
+  function checkUrlSafety(url) {
+    if (/[<>"''"]/.exec(url)) {
+      throw new Error("unsafe url detected. encoded=" + encodedURI(url));
+    }
+  }
+
+
   function getUrlData(url, success_func, error_func) {
     // some services expect callback=, some expect jsonp=, so supply both
     var plus = 'callback=jsonp&jsonp=jsonp';
@@ -1112,6 +1119,7 @@
     } else {
       nurl = url + '?' + plus;
     }
+    console.debug('fetching data url:', nurl);
 
     var iframe = document.createElement('iframe');
     iframe.style.display = 'none';
@@ -1120,6 +1128,46 @@
     // the default jsonp callback
     iframe.contentWindow.jsonp = success_func;
 
+    // a callback the jsonp can execute if oauth2 authentication is needed
+    iframe.contentWindow.tryOAuth2 = function(oauth2_url) {
+      var hostpart = urlMinusPath(oauth2_url);
+      var oauth_appends = {
+        'https://accounts.google.com':
+            'client_id=41470923181.apps.googleusercontent.com'
+        // (If you register afterquery with any other API providers, add the
+        //  app ids here.  app client_id fields are not secret in oauth2;
+        //  there's a client_secret, but it's not needed in pure javascript.)
+      }
+      var plus = [oauth_appends[hostpart]];
+      if (plus) {
+        plus += '&response_type=token';
+        plus += '&state=' +
+            encodeURIComponent(
+                'url=' + encodeURIComponent(url) +
+                '&continue=' + encodeURIComponent(window.top.location));
+        plus += '&redirect_uri=' +
+            encodeURIComponent(window.location.origin + '/oauth2callback');
+        var want_url;
+        if (oauth2_url.indexOf('?') >= 0) {
+          want_url = oauth2_url + '&' + plus;
+        } else {
+          want_url = oauth2_url + '?' + plus;
+        }
+        console.debug('oauth2 redirect:', want_url);
+        checkUrlSafety(want_url);
+        document.write('Click here to ' +
+                       '<a target="_top" ' +
+                       '  href="' + want_url +
+                       '">authorize the data source</a>.');
+      } else {
+        console.debug('no oauth2 service known for host', hostpart);
+        document.write("Data source requires authorization, but I don't " +
+                       "know how to oauth2 authorize urls from <b>" +
+                       encodeURI(hostpart) +
+                       "</b> - sorry.");
+      }
+    }
+
     // some services are hardcoded to use the gviz callback, so supply that too
     iframe.contentWindow.google = {
       visualization: {
@@ -1133,6 +1181,8 @@
       error(null, message + ' url=' + xurl + ' line=' + lineno);
     };
 
+    iframe.contentWindow.jsonp_url = nurl;
+
     //TODO(apenwarr): change the domain/origin attribute of the iframe.
     //  That way the script won't be able to affect us, no matter how badly
     //  behaved it might be.  That's important so they can't access our
@@ -1174,6 +1224,7 @@
       extractRegexp: extractRegexp,
       fillNullsWithZero: fillNullsWithZero,
       urlMinusPath: urlMinusPath,
+      checkUrlSafety: checkUrlSafety,
       gridFromData: gridFromData
     },
     render: wrap(_run)
diff --git a/setauth.html b/setauth.html
index e6d5737..776cde7 100644
--- a/setauth.html
+++ b/setauth.html
@@ -9,11 +9,23 @@
     (window.location.search || '?') + '&' + window.location.hash.substr(1));
 
 var token = args.get('access_token') || args.get('auth');
-var url = args.get('state') || args.get('url');
+var state = args.get('state');
+console.debug('state is:', state);
+var stateargs = afterquery.internal.parseArgs('?' + (state || ''));
+var url = args.get('url') || stateargs.get('url');
+var continue_url = args.get('continue') || stateargs.get('continue');
 var hostpart = url ? afterquery.internal.urlMinusPath(url) : null;
 
 if (token && hostpart) {
   localStorage[['auth', hostpart]] = token;
+  document.write('Authorization successful.')
+} else {
+  document.write('Authorization was rejected.');
+}
+if (continue_url) {
+  afterquery.internal.checkUrlSafety(continue_url);
+  document.write(
+      ' <a href="' + continue_url + '">Continue >></a>');
 }
 
 </script>