- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / common / extensions / docs / examples / extensions / gdocs / chrome_ex_oauth.js
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6  * Constructor - no need to invoke directly, call initBackgroundPage instead.
7  * @constructor
8  * @param {String} url_request_token The OAuth request token URL.
9  * @param {String} url_auth_token The OAuth authorize token URL.
10  * @param {String} url_access_token The OAuth access token URL.
11  * @param {String} consumer_key The OAuth consumer key.
12  * @param {String} consumer_secret The OAuth consumer secret.
13  * @param {String} oauth_scope The OAuth scope parameter.
14  * @param {Object} opt_args Optional arguments.  Recognized parameters:
15  *     "app_name" {String} Name of the current application
16  *     "callback_page" {String} If you renamed chrome_ex_oauth.html, the name
17  *          this file was renamed to.
18  */
19 function ChromeExOAuth(url_request_token, url_auth_token, url_access_token,
20                        consumer_key, consumer_secret, oauth_scope, opt_args) {
21   this.url_request_token = url_request_token;
22   this.url_auth_token = url_auth_token;
23   this.url_access_token = url_access_token;
24   this.consumer_key = consumer_key;
25   this.consumer_secret = consumer_secret;
26   this.oauth_scope = oauth_scope;
27   this.app_name = opt_args && opt_args['app_name'] ||
28       "ChromeExOAuth Library";
29   this.key_token = "oauth_token";
30   this.key_token_secret = "oauth_token_secret";
31   this.callback_page = opt_args && opt_args['callback_page'] ||
32       "chrome_ex_oauth.html";
33   this.auth_params = {};
34   if (opt_args && opt_args['auth_params']) {
35     for (key in opt_args['auth_params']) {
36       if (opt_args['auth_params'].hasOwnProperty(key)) {
37         this.auth_params[key] = opt_args['auth_params'][key];
38       }
39     }
40   }
41 };
42
43 /*******************************************************************************
44  * PUBLIC API METHODS
45  * Call these from your background page.
46  ******************************************************************************/
47
48 /**
49  * Initializes the OAuth helper from the background page.  You must call this
50  * before attempting to make any OAuth calls.
51  * @param {Object} oauth_config Configuration parameters in a JavaScript object.
52  *     The following parameters are recognized:
53  *         "request_url" {String} OAuth request token URL.
54  *         "authorize_url" {String} OAuth authorize token URL.
55  *         "access_url" {String} OAuth access token URL.
56  *         "consumer_key" {String} OAuth consumer key.
57  *         "consumer_secret" {String} OAuth consumer secret.
58  *         "scope" {String} OAuth access scope.
59  *         "app_name" {String} Application name.
60  *         "auth_params" {Object} Additional parameters to pass to the
61  *             Authorization token URL.  For an example, 'hd', 'hl', 'btmpl':
62  *             http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
63  * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
64  */
65 ChromeExOAuth.initBackgroundPage = function(oauth_config) {
66   window.chromeExOAuthConfig = oauth_config;
67   window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config);
68   window.chromeExOAuthRedirectStarted = false;
69   window.chromeExOAuthRequestingAccess = false;
70
71   var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page);
72   var tabs = {};
73   chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
74     if (changeInfo.url &&
75         changeInfo.url.substr(0, url_match.length) === url_match &&
76         changeInfo.url != tabs[tabId] &&
77         window.chromeExOAuthRequestingAccess == false) {
78       chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) {
79         tabs[tab.id] = tab.url;
80         chrome.tabs.remove(tabId);
81       });
82     }
83   });
84
85   return window.chromeExOAuth;
86 };
87
88 /**
89  * Authorizes the current user with the configued API.  You must call this
90  * before calling sendSignedRequest.
91  * @param {Function} callback A function to call once an access token has
92  *     been obtained.  This callback will be passed the following arguments:
93  *         token {String} The OAuth access token.
94  *         secret {String} The OAuth access token secret.
95  */
96 ChromeExOAuth.prototype.authorize = function(callback) {
97   if (this.hasToken()) {
98     callback(this.getToken(), this.getTokenSecret());
99   } else {
100     window.chromeExOAuthOnAuthorize = function(token, secret) {
101       callback(token, secret);
102     };
103     chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) });
104   }
105 };
106
107 /**
108  * Clears any OAuth tokens stored for this configuration.  Effectively a
109  * "logout" of the configured OAuth API.
110  */
111 ChromeExOAuth.prototype.clearTokens = function() {
112   delete localStorage[this.key_token + encodeURI(this.oauth_scope)];
113   delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
114 };
115
116 /**
117  * Returns whether a token is currently stored for this configuration.
118  * Effectively a check to see whether the current user is "logged in" to
119  * the configured OAuth API.
120  * @return {Boolean} True if an access token exists.
121  */
122 ChromeExOAuth.prototype.hasToken = function() {
123   return !!this.getToken();
124 };
125
126 /**
127  * Makes an OAuth-signed HTTP request with the currently authorized tokens.
128  * @param {String} url The URL to send the request to.  Querystring parameters
129  *     should be omitted.
130  * @param {Function} callback A function to be called once the request is
131  *     completed.  This callback will be passed the following arguments:
132  *         responseText {String} The text response.
133  *         xhr {XMLHttpRequest} The XMLHttpRequest object which was used to
134  *             send the request.  Useful if you need to check response status
135  *             code, etc.
136  * @param {Object} opt_params Additional parameters to configure the request.
137  *     The following parameters are accepted:
138  *         "method" {String} The HTTP method to use.  Defaults to "GET".
139  *         "body" {String} A request body to send.  Defaults to null.
140  *         "parameters" {Object} Query parameters to include in the request.
141  *         "headers" {Object} Additional headers to include in the request.
142  */
143 ChromeExOAuth.prototype.sendSignedRequest = function(url, callback,
144                                                      opt_params) {
145   var method = opt_params && opt_params['method'] || 'GET';
146   var body = opt_params && opt_params['body'] || null;
147   var params = opt_params && opt_params['parameters'] || {};
148   var headers = opt_params && opt_params['headers'] || {};
149
150   var signedUrl = this.signURL(url, method, params);
151
152   ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) {
153     if (xhr.readyState == 4) {
154       callback(xhr.responseText, xhr);
155     }
156   });
157 };
158
159 /**
160  * Adds the required OAuth parameters to the given url and returns the
161  * result.  Useful if you need a signed url but don't want to make an XHR
162  * request.
163  * @param {String} method The http method to use.
164  * @param {String} url The base url of the resource you are querying.
165  * @param {Object} opt_params Query parameters to include in the request.
166  * @return {String} The base url plus any query params plus any OAuth params.
167  */
168 ChromeExOAuth.prototype.signURL = function(url, method, opt_params) {
169   var token = this.getToken();
170   var secret = this.getTokenSecret();
171   if (!token || !secret) {
172     throw new Error("No oauth token or token secret");
173   }
174
175   var params = opt_params || {};
176
177   var result = OAuthSimple().sign({
178     action : method,
179     path : url,
180     parameters : params,
181     signatures: {
182       consumer_key : this.consumer_key,
183       shared_secret : this.consumer_secret,
184       oauth_secret : secret,
185       oauth_token: token
186     }
187   });
188
189   return result.signed_url;
190 };
191
192 /**
193  * Generates the Authorization header based on the oauth parameters.
194  * @param {String} url The base url of the resource you are querying.
195  * @param {Object} opt_params Query parameters to include in the request.
196  * @return {String} An Authorization header containing the oauth_* params.
197  */
198 ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method,
199                                                           opt_params) {
200   var token = this.getToken();
201   var secret = this.getTokenSecret();
202   if (!token || !secret) {
203     throw new Error("No oauth token or token secret");
204   }
205
206   var params = opt_params || {};
207
208   return OAuthSimple().getHeaderString({
209     action: method,
210     path : url,
211     parameters : params,
212     signatures: {
213       consumer_key : this.consumer_key,
214       shared_secret : this.consumer_secret,
215       oauth_secret : secret,
216       oauth_token: token
217     }
218   });
219 };
220
221 /*******************************************************************************
222  * PRIVATE API METHODS
223  * Used by the library.  There should be no need to call these methods directly.
224  ******************************************************************************/
225
226 /**
227  * Creates a new ChromeExOAuth object from the supplied configuration object.
228  * @param {Object} oauth_config Configuration parameters in a JavaScript object.
229  *     The following parameters are recognized:
230  *         "request_url" {String} OAuth request token URL.
231  *         "authorize_url" {String} OAuth authorize token URL.
232  *         "access_url" {String} OAuth access token URL.
233  *         "consumer_key" {String} OAuth consumer key.
234  *         "consumer_secret" {String} OAuth consumer secret.
235  *         "scope" {String} OAuth access scope.
236  *         "app_name" {String} Application name.
237  *         "auth_params" {Object} Additional parameters to pass to the
238  *             Authorization token URL.  For an example, 'hd', 'hl', 'btmpl':
239  *             http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
240  * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
241  */
242 ChromeExOAuth.fromConfig = function(oauth_config) {
243   return new ChromeExOAuth(
244     oauth_config['request_url'],
245     oauth_config['authorize_url'],
246     oauth_config['access_url'],
247     oauth_config['consumer_key'],
248     oauth_config['consumer_secret'],
249     oauth_config['scope'],
250     {
251       'app_name' : oauth_config['app_name'],
252       'auth_params' : oauth_config['auth_params']
253     }
254   );
255 };
256
257 /**
258  * Initializes chrome_ex_oauth.html and redirects the page if needed to start
259  * the OAuth flow.  Once an access token is obtained, this function closes
260  * chrome_ex_oauth.html.
261  */
262 ChromeExOAuth.initCallbackPage = function() {
263   var background_page = chrome.extension.getBackgroundPage();
264   var oauth_config = background_page.chromeExOAuthConfig;
265   var oauth = ChromeExOAuth.fromConfig(oauth_config);
266   background_page.chromeExOAuthRedirectStarted = true;
267   oauth.initOAuthFlow(function (token, secret) {
268     background_page.chromeExOAuthOnAuthorize(token, secret);
269     background_page.chromeExOAuthRedirectStarted = false;
270     chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
271       chrome.tabs.remove(tabs[0].id);
272     });
273   });
274 };
275
276 /**
277  * Sends an HTTP request.  Convenience wrapper for XMLHttpRequest calls.
278  * @param {String} method The HTTP method to use.
279  * @param {String} url The URL to send the request to.
280  * @param {Object} headers Optional request headers in key/value format.
281  * @param {String} body Optional body content.
282  * @param {Function} callback Function to call when the XMLHttpRequest's
283  *     ready state changes.  See documentation for XMLHttpRequest's
284  *     onreadystatechange handler for more information.
285  */
286 ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) {
287   var xhr = new XMLHttpRequest();
288   xhr.onreadystatechange = function(data) {
289     callback(xhr, data);
290   }
291   xhr.open(method, url, true);
292   if (headers) {
293     for (var header in headers) {
294       if (headers.hasOwnProperty(header)) {
295         xhr.setRequestHeader(header, headers[header]);
296       }
297     }
298   }
299   xhr.send(body);
300 };
301
302 /**
303  * Decodes a URL-encoded string into key/value pairs.
304  * @param {String} encoded An URL-encoded string.
305  * @return {Object} An object representing the decoded key/value pairs found
306  *     in the encoded string.
307  */
308 ChromeExOAuth.formDecode = function(encoded) {
309   var params = encoded.split("&");
310   var decoded = {};
311   for (var i = 0, param; param = params[i]; i++) {
312     var keyval = param.split("=");
313     if (keyval.length == 2) {
314       var key = ChromeExOAuth.fromRfc3986(keyval[0]);
315       var val = ChromeExOAuth.fromRfc3986(keyval[1]);
316       decoded[key] = val;
317     }
318   }
319   return decoded;
320 };
321
322 /**
323  * Returns the current window's querystring decoded into key/value pairs.
324  * @return {Object} A object representing any key/value pairs found in the
325  *     current window's querystring.
326  */
327 ChromeExOAuth.getQueryStringParams = function() {
328   var urlparts = window.location.href.split("?");
329   if (urlparts.length >= 2) {
330     var querystring = urlparts.slice(1).join("?");
331     return ChromeExOAuth.formDecode(querystring);
332   }
333   return {};
334 };
335
336 /**
337  * Binds a function call to a specific object.  This function will also take
338  * a variable number of additional arguments which will be prepended to the
339  * arguments passed to the bound function when it is called.
340  * @param {Function} func The function to bind.
341  * @param {Object} obj The object to bind to the function's "this".
342  * @return {Function} A closure that will call the bound function.
343  */
344 ChromeExOAuth.bind = function(func, obj) {
345   var newargs = Array.prototype.slice.call(arguments).slice(2);
346   return function() {
347     var combinedargs = newargs.concat(Array.prototype.slice.call(arguments));
348     func.apply(obj, combinedargs);
349   };
350 };
351
352 /**
353  * Encodes a value according to the RFC3986 specification.
354  * @param {String} val The string to encode.
355  */
356 ChromeExOAuth.toRfc3986 = function(val){
357    return encodeURIComponent(val)
358        .replace(/\!/g, "%21")
359        .replace(/\*/g, "%2A")
360        .replace(/'/g, "%27")
361        .replace(/\(/g, "%28")
362        .replace(/\)/g, "%29");
363 };
364
365 /**
366  * Decodes a string that has been encoded according to RFC3986.
367  * @param {String} val The string to decode.
368  */
369 ChromeExOAuth.fromRfc3986 = function(val){
370   var tmp = val
371       .replace(/%21/g, "!")
372       .replace(/%2A/g, "*")
373       .replace(/%27/g, "'")
374       .replace(/%28/g, "(")
375       .replace(/%29/g, ")");
376    return decodeURIComponent(tmp);
377 };
378
379 /**
380  * Adds a key/value parameter to the supplied URL.
381  * @param {String} url An URL which may or may not contain querystring values.
382  * @param {String} key A key
383  * @param {String} value A value
384  * @return {String} The URL with URL-encoded versions of the key and value
385  *     appended, prefixing them with "&" or "?" as needed.
386  */
387 ChromeExOAuth.addURLParam = function(url, key, value) {
388   var sep = (url.indexOf('?') >= 0) ? "&" : "?";
389   return url + sep +
390          ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value);
391 };
392
393 /**
394  * Stores an OAuth token for the configured scope.
395  * @param {String} token The token to store.
396  */
397 ChromeExOAuth.prototype.setToken = function(token) {
398   localStorage[this.key_token + encodeURI(this.oauth_scope)] = token;
399 };
400
401 /**
402  * Retrieves any stored token for the configured scope.
403  * @return {String} The stored token.
404  */
405 ChromeExOAuth.prototype.getToken = function() {
406   return localStorage[this.key_token + encodeURI(this.oauth_scope)];
407 };
408
409 /**
410  * Stores an OAuth token secret for the configured scope.
411  * @param {String} secret The secret to store.
412  */
413 ChromeExOAuth.prototype.setTokenSecret = function(secret) {
414   localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret;
415 };
416
417 /**
418  * Retrieves any stored secret for the configured scope.
419  * @return {String} The stored secret.
420  */
421 ChromeExOAuth.prototype.getTokenSecret = function() {
422   return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
423 };
424
425 /**
426  * Starts an OAuth authorization flow for the current page.  If a token exists,
427  * no redirect is needed and the supplied callback is called immediately.
428  * If this method detects that a redirect has finished, it grabs the
429  * appropriate OAuth parameters from the URL and attempts to retrieve an
430  * access token.  If no token exists and no redirect has happened, then
431  * an access token is requested and the page is ultimately redirected.
432  * @param {Function} callback The function to call once the flow has finished.
433  *     This callback will be passed the following arguments:
434  *         token {String} The OAuth access token.
435  *         secret {String} The OAuth access token secret.
436  */
437 ChromeExOAuth.prototype.initOAuthFlow = function(callback) {
438   if (!this.hasToken()) {
439     var params = ChromeExOAuth.getQueryStringParams();
440     if (params['chromeexoauthcallback'] == 'true') {
441       var oauth_token = params['oauth_token'];
442       var oauth_verifier = params['oauth_verifier']
443       this.getAccessToken(oauth_token, oauth_verifier, callback);
444     } else {
445       var request_params = {
446         'url_callback_param' : 'chromeexoauthcallback'
447       }
448       this.getRequestToken(function(url) {
449         window.location.href = url;
450       }, request_params);
451     }
452   } else {
453     callback(this.getToken(), this.getTokenSecret());
454   }
455 };
456
457 /**
458  * Requests an OAuth request token.
459  * @param {Function} callback Function to call once the authorize URL is
460  *     calculated.  This callback will be passed the following arguments:
461  *         url {String} The URL the user must be redirected to in order to
462  *             approve the token.
463  * @param {Object} opt_args Optional arguments.  The following parameters
464  *     are accepted:
465  *         "url_callback" {String} The URL the OAuth provider will redirect to.
466  *         "url_callback_param" {String} A parameter to include in the callback
467  *             URL in order to indicate to this library that a redirect has
468  *             taken place.
469  */
470 ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) {
471   if (typeof callback !== "function") {
472     throw new Error("Specified callback must be a function.");
473   }
474   var url = opt_args && opt_args['url_callback'] ||
475             window && window.top && window.top.location &&
476             window.top.location.href;
477
478   var url_param = opt_args && opt_args['url_callback_param'] ||
479                   "chromeexoauthcallback";
480   var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true");
481
482   var result = OAuthSimple().sign({
483     path : this.url_request_token,
484     parameters: {
485       "xoauth_displayname" : this.app_name,
486       "scope" : this.oauth_scope,
487       "oauth_callback" : url_callback
488     },
489     signatures: {
490       consumer_key : this.consumer_key,
491       shared_secret : this.consumer_secret
492     }
493   });
494   var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback);
495   ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
496 };
497
498 /**
499  * Called when a request token has been returned.  Stores the request token
500  * secret for later use and sends the authorization url to the supplied
501  * callback (for redirecting the user).
502  * @param {Function} callback Function to call once the authorize URL is
503  *     calculated.  This callback will be passed the following arguments:
504  *         url {String} The URL the user must be redirected to in order to
505  *             approve the token.
506  * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
507  *     request token.
508  */
509 ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) {
510   if (xhr.readyState == 4) {
511     if (xhr.status == 200) {
512       var params = ChromeExOAuth.formDecode(xhr.responseText);
513       var token = params['oauth_token'];
514       this.setTokenSecret(params['oauth_token_secret']);
515       var url = ChromeExOAuth.addURLParam(this.url_auth_token,
516                                           "oauth_token", token);
517       for (var key in this.auth_params) {
518         if (this.auth_params.hasOwnProperty(key)) {
519           url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]);
520         }
521       }
522       callback(url);
523     } else {
524       throw new Error("Fetching request token failed. Status " + xhr.status);
525     }
526   }
527 };
528
529 /**
530  * Requests an OAuth access token.
531  * @param {String} oauth_token The OAuth request token.
532  * @param {String} oauth_verifier The OAuth token verifier.
533  * @param {Function} callback The function to call once the token is obtained.
534  *     This callback will be passed the following arguments:
535  *         token {String} The OAuth access token.
536  *         secret {String} The OAuth access token secret.
537  */
538 ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier,
539                                                   callback) {
540   if (typeof callback !== "function") {
541     throw new Error("Specified callback must be a function.");
542   }
543   var bg = chrome.extension.getBackgroundPage();
544   if (bg.chromeExOAuthRequestingAccess == false) {
545     bg.chromeExOAuthRequestingAccess = true;
546
547     var result = OAuthSimple().sign({
548       path : this.url_access_token,
549       parameters: {
550         "oauth_token" : oauth_token,
551         "oauth_verifier" : oauth_verifier
552       },
553       signatures: {
554         consumer_key : this.consumer_key,
555         shared_secret : this.consumer_secret,
556         oauth_secret : this.getTokenSecret(this.oauth_scope)
557       }
558     });
559
560     var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback);
561     ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
562   }
563 };
564
565 /**
566  * Called when an access token has been returned.  Stores the access token and
567  * access token secret for later use and sends them to the supplied callback.
568  * @param {Function} callback The function to call once the token is obtained.
569  *     This callback will be passed the following arguments:
570  *         token {String} The OAuth access token.
571  *         secret {String} The OAuth access token secret.
572  * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
573  *     access token.
574  */
575 ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) {
576   if (xhr.readyState == 4) {
577     var bg = chrome.extension.getBackgroundPage();
578     if (xhr.status == 200) {
579       var params = ChromeExOAuth.formDecode(xhr.responseText);
580       var token = params["oauth_token"];
581       var secret = params["oauth_token_secret"];
582       this.setToken(token);
583       this.setTokenSecret(secret);
584       bg.chromeExOAuthRequestingAccess = false;
585       callback(token, secret);
586     } else {
587       bg.chromeExOAuthRequestingAccess = false;
588       throw new Error("Fetching access token failed with status " + xhr.status);
589     }
590   }
591 };
592