github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/clients/chrome/clip-it-good/chrome_ex_oauth.js (about)

     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   */
    21  function 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   */
    67  ChromeExOAuth.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   */
    98  ChromeExOAuth.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   */
   113  ChromeExOAuth.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   */
   124  ChromeExOAuth.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   */
   145  ChromeExOAuth.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   */
   170  ChromeExOAuth.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   */
   200  ChromeExOAuth.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   */
   244  ChromeExOAuth.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   */
   264  ChromeExOAuth.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   */
   288  ChromeExOAuth.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   */
   310  ChromeExOAuth.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   */
   329  ChromeExOAuth.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   */
   346  ChromeExOAuth.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   */
   358  ChromeExOAuth.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   */
   371  ChromeExOAuth.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   */
   389  ChromeExOAuth.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   */
   399  ChromeExOAuth.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   */
   407  ChromeExOAuth.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   */
   415  ChromeExOAuth.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   */
   423  ChromeExOAuth.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   */
   439  ChromeExOAuth.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   */
   472  ChromeExOAuth.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   */
   511  ChromeExOAuth.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   */
   540  ChromeExOAuth.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   */
   577  ChromeExOAuth.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  };