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