diff --git a/.DS_Store b/.DS_Store index 1adf149..d1ecf70 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/GAFeed.lib.inc b/GAFeed.lib.inc index 4352aa5..32a159d 100644 --- a/GAFeed.lib.inc +++ b/GAFeed.lib.inc @@ -13,6 +13,10 @@ class GAFeed { /* Methods require at least v2 */ const gaFeedVersion = 2; + const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'; + const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'; + const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'; + const SCOPE = 'https://www.googleapis.com/auth/analytics.readonly https://www.google.com/analytics/feeds/'; /* Response object */ public $response; @@ -38,117 +42,183 @@ class GAFeed { /* Domain of Data Feed API */ protected $host = 'www.google.com'; - /* Request header source */ - protected $source = 'drupal'; + /* OAuth access token */ + public $access_token; - /* Default is HMAC-SHA1 */ - protected $signatureMethod; + /* OAuth refresh token */ + public $refresh_token; - /* HMAC-SHA1 Consumer data */ - protected $consumer; - - /* OAuth token */ - protected $token; - - /* Google authorize callback verifier string */ - protected $verifier; + /* OAuth expiration time */ + public $expires_at; /** * Constructor for the GAFeed class */ - public function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) { - $this->signatureMethod = new OAuthSignatureMethod_HMAC_SHA1(); - $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); - - /* Allow developers the option of OAuth authentication without using this class's methods */ - if (!empty($oauth_token) && !empty($oauth_token_secret)) { - $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret); - } + public function __construct($token = NULL) { + $this->access_token = $token; } /** - * Set the verifier property + * Get the current page url + * + * @return String */ - public function setVerifier($verifier) { - $this->verifier = $verifier; - } - - /** - * Set the host property - */ - public function setHost($host) { - $this->host = $host; - } + public static function currentUrl() { + $https = $_SERVER['HTTPS'] == 'on'; + $url = $https ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + if ((!$https && $_SERVER['SERVER_PORT'] != '80') || + ($https && $_SERVER['SERVER_PORT'] != '443')) { + $url .= ':' . $_SERVER['SERVER_PORT']; + } - /** - * Set the queryPath property - */ - protected function setQueryPath($path) { - $this->queryPath = 'https://'. $this->host .'/'. $path; + return $url . $_SERVER['REQUEST_URI']; } - /** - * OAuth step #1: Fetch request token. - */ - public function getRequestToken() { - $this->setQueryPath('accounts/OAuthGetRequestToken'); - - /* xoauth_displayname is displayed on the Google Authentication page */ - $params = array( - 'scope' => 'https://www.google.com/analytics/feeds', - 'oauth_callback' => url('google-analytics-reports/oauth', array('absolute' => TRUE)), - 'xoauth_displayname' => t('Google Analytics Reports Drupal module'), - ); - - $this->query($this->queryPath, $params, 'GET', array('refresh' => TRUE)); - parse_str($this->response->data, $token); - $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); - return $token; + /** + * Create a URL to obtain user authorization. + * The authorization endpoint allows the user to first + * authenticate, and then grant/deny the access request. + * @param string $client_id + * @return string + */ + public function createAuthUrl($client_id, $redirect_uri) { + $params = array( + 'response_type=code', + 'redirect_uri=' . $redirect_uri, + 'client_id=' . urlencode($client_id), + 'scope=' . self::SCOPE, + 'access_type=offline', + 'approval_prompt=force' + ); + + $params = implode('&', $params); + return self::OAUTH2_AUTH_URL . "?$params"; + } + + /** + * Authenticate with the Google API + * + * @param String $client_id + * @param String $client_secret + * @param String $refresh_token + * @return GAFeed + */ + protected function fetchToken($client_id, $client_secret, $redirect_uri, $refresh_token=NULL) { + if ($refresh_token) { + $params = array( + 'client_id=' . $client_id, + 'client_secret=' . $client_secret, + 'refresh_token=' . $refresh_token, + 'grant_type=refresh_token', + ); + } + else { + $params = array( + 'code=' . $_GET['code'], + 'grant_type=authorization_code', + 'redirect_uri=' . $redirect_uri, + 'client_id=' . $client_id, + 'client_secret=' . $client_secret, + ); + } + + $data = implode('&', $params); + $this->response = drupal_http_request(self::OAUTH2_TOKEN_URI, array('Content-Type' => 'application/x-www-form-urlencoded'), 'POST', $data); + //var_dump($this->response); + if (substr($this->response->code, 0, 1) == '2') { + $decoded_response = json_decode($this->response->data, true); + $this->access_token = $decoded_response['access_token']; + $this->expires_at = time() + $decoded_response['expires_in']; + if (!$refresh_token) { + $this->refresh_token = $decoded_response['refresh_token']; + } + } + else { + $error_msg = 'Code: !code - Error: !message - Message: !details'; + $error_vars = array('!code' => $this->response->code, '!message' => $this->response->error, '!details' => strip_tags($this->response->data)); + $this->error = t($error_msg, $error_vars); + watchdog('google analytics reports', $error_msg, $error_vars, WATCHDOG_ERROR); + } + } + + /** + * Complete the authentication process. + * We got here after being redirected from a successful authorization grant. + * Fetch the access token + * + * @param String $client_id + * @param String $client_secret + */ + public function finishAuthentication($client_id, $client_secret, $redirect_uri) { + $this -> fetchToken($client_id, $client_secret, $redirect_uri); } - + /** - * OAuth step #2: Authorize request token. - */ - public function obtainAuthorization($token) { - $this->setQueryPath('accounts/OAuthAuthorizeToken'); - - /* hd is the best way of dealing with users with multiple domains verified with Google */ - $params = array( - 'oauth_token' => $token['oauth_token'], - 'hd' => variable_get('google_analytics_reports_hd', 'default'), - ); - - drupal_goto($this->queryPath, $params); + * Begin authentication by allowing the user to grant/deny access to the Google account + * + * @param String $client_id + */ + public function beginAuthentication($client_id, $redirect_uri) { + drupal_goto($this -> createAuthUrl($client_id, $redirect_uri)); } - /** - * OAuth step #3: Fetch access token. - */ - public function getAccessToken() { - $this->setQueryPath('accounts/OAuthGetAccessToken'); + /** + * Fetches a fresh access token with the given refresh token. + * @param String $client_id + * @param String $client_secret + * @param string $refresh_token + */ + public function refreshToken($client_id, $client_secret, $refresh_token) { + $this->refresh_token = $refresh_token; + $this -> fetchToken($client_id, $client_secret, '', $refresh_token); + } + + /** + * Revoke an OAuth2 access token or refresh token. This method will revoke the current access + * token, if a token isn't provided. + * @param string|NULL $token The token (access token or a refresh token) that should be revoked. + * @return boolean Returns True if the revocation was successful, otherwise False. + */ + public function revokeToken($token = NULL) { + if (!$token) { + $token = $this->refresh_token; + } + + $this->response = drupal_http_request(self::OAUTH2_REVOKE_URI, array('Content-Type' => 'application/x-www-form-urlencoded'), 'POST', "token=$token"); + + if ($this->response->code == 200) { + $this->access_token = NULL; + return true; + } + + return false; + } + + /** + * Generate authorization token header for all requests + * + * @return Array + */ + public function generateAuthHeader($token=NULL) { + if ($token == NULL) { + $token = $this->access_token; + } + return array('Authorization' => 'Bearer ' . $token); + } - $params = array( - 'oauth_verifier' => $this->verifier, - ); - - $this->query($this->queryPath, $params, 'GET', array('refresh' => TRUE)); - parse_str($this->response->data, $token); - $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); - return $token; - } /** - * Revoke OAuth token. - */ - public function revokeToken() { - $this->setQueryPath('accounts/AuthSubRevokeToken'); - $this->query($this->queryPath, array(), 'GET', array('refresh' => TRUE)); + * Set the queryPath property for all Data Export API requests + */ + protected function setQueryPath($path) { + $this->queryPath = 'https://'. $this->host .'/'. $path; } /** * Public query method for all Data Export API features. */ - public function query($path, $params = array(), $method = 'GET', $cache_options = array()) { + public function query($url, $params = array(), $method = 'GET', $headers, $cache_options) { $params_defaults = array( 'v' => self::gaFeedVersion, @@ -175,21 +245,12 @@ class GAFeed { $this->fromCache = TRUE; } else { - $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $this->queryPath, $params); - $request->sign_request($this->signatureMethod, $this->consumer, $this->token); - switch ($method) { - case 'GET': - $this->request($request->to_url()); - break; - case 'POST': - $this->request($request->get_normalized_http_url(), $request->get_parameters(), 'POST'); - break; - } + $this->request($url, $params, $headers); + } - /* Do not cache erroneous queries */ - if (empty($this->error)) { - cache_set($cache_options['cid'], $this->response, 'cache', $cache_options['expire']); - } + /* Do not cache erroneous queries */ + if (empty($this->error)) { + cache_set($cache_options['cid'], $this->response, 'cache', $cache_options['expire']); } return (empty($this->error)); @@ -198,7 +259,7 @@ class GAFeed { /** * Execute a query */ - protected function request($url, $params = array(), $method = 'GET') { + protected function request($url, $params = array(), $headers, $method = 'GET') { $data = ''; if (count($params) > 0) { if ($method == 'GET') { @@ -209,7 +270,6 @@ class GAFeed { } } - $headers = array(); $this->response = drupal_http_request($url, $headers, $method, $data); if ($this->response->code != '200') { // data is undefined if the connection failed. @@ -234,7 +294,7 @@ class GAFeed { ); $this->setQueryPath('analytics/feeds/accounts/default'); - if ($this->query($this->queryPath, $params, 'GET', $cache_options)) { + if ($this->query($this->queryPath, $params, 'GET', $this->generateAuthHeader(), $cache_options)) { $this->sanitizeAccount(); } } @@ -347,7 +407,7 @@ class GAFeed { $parameters['max-results'] = $params['max_results']; $this->setQueryPath('analytics/feeds/data'); - if ($this->query($this->queryPath, $parameters, 'GET', $cache_options)) { + if ($this->query($this->queryPath, $parameters, 'GET', $this->generateAuthHeader(), $cache_options)) { $this->sanitizeReport(); } } diff --git a/google_analytics_api.info b/google_analytics_api.info index 8927691..08237fc 100644 --- a/google_analytics_api.info +++ b/google_analytics_api.info @@ -2,7 +2,6 @@ name = "Google Analytics API" package = Statistics description = "API to access statistics from the Google Analytics Data Export API." core = 6.x -dependencies[] = oauth_common ; Information added by drupal.org packaging script on 2012-07-31 version = "6.x-1.2" core = "6.x" diff --git a/google_analytics_api.module b/google_analytics_api.module index 0ab7f1a..f2b81f9 100644 --- a/google_analytics_api.module +++ b/google_analytics_api.module @@ -42,15 +42,6 @@ function google_analytics_api_menu() { 'access arguments' => array('administer google analytics reports'), ); - /* OAuth callback from Google */ - $items['google-analytics-reports/oauth'] = array( - 'title' => 'Google Analytics Reports OAuth Callback', - 'access callback' => TRUE, - 'page callback' => 'drupal_get_form', - 'page arguments' => array('google_analytics_reports_oauth_callback'), - 'type' => MENU_CALLBACK, - 'file' => 'google_analytics_api.pages.inc', - ); return $items; } @@ -74,15 +65,61 @@ function google_analytics_api_theme() { /** - * Instantiate a new GAFeed object. - */ + * Instantiate a new authenticated GAFeed object or NULL if no authentication has taken place. +*/ function google_analytics_api_new_gafeed() { - module_load_include('inc', 'google_analytics_api', 'GAFeed.lib'); - $key = variable_get('google_analytics_reports_consumer_key', 'anonymous'); - $secret = variable_get('google_analytics_reports_consumer_secret', 'anonymous'); - $oauth_token = variable_get('google_analytics_reports_oauth_token', NULL); - $oauth_token_secret = variable_get('google_analytics_reports_oauth_token_secret', NULL); - return new GAFeed($key, $secret, $oauth_token, $oauth_token_secret); + module_load_include('inc', 'google_analytics_api', 'GAFeed.lib'); + if (variable_get('google_analytics_reports_access_token', NULL) && time() < variable_get('google_analytics_reports_expires_at', NULL)) { + // If the access token is still valid, return an authenticated GAFeed + return new GAFeed(variable_get('google_analytics_reports_access_token', NULL)); + } + else if (variable_get('google_analytics_reports_refresh_token', NULL)) { + // If the site has an access token and refresh token, but the access + // token has expired, authenticate the user with the refresh token + $client_id = variable_get('google_analytics_reports_client_id', NULL); + $client_secret = variable_get('google_analytics_reports_client_secret', NULL); + $refresh_token = variable_get('google_analytics_reports_refresh_token', NULL); + + try { + $GAFeed = new GAFeed(); + $GAFeed -> refreshToken($client_id, $client_secret, $refresh_token); + + variable_set("google_analytics_reports_access_token", $GAFeed->access_token); + variable_set("google_analytics_reports_expires_at", $GAFeed->expires_at); + return $GAFeed; + } + catch (Exception $e) { + drupal_set_message(t("There was an authentication error. Message: " . $e->getMessage()), 'error', FALSE); + return NULL; + } + } + else if (isset($_GET['code'])) { + // If there is no access token or refresh token and client is returned + // to the config page with an access code, complete the authentication + $client_id = variable_get('google_analytics_reports_client_id', NULL); + $client_secret = variable_get('google_analytics_reports_client_secret', NULL); + $redirect_uri = variable_get('google_analytics_reports_redirect_uri', NULL); + + try { + $GAFeed = new GAFeed(); + $GAFeed -> finishAuthentication($client_id, $client_secret, $redirect_uri); + + variable_set('google_analytics_reports_access_token', $GAFeed->access_token); + variable_set('google_analytics_reports_expires_at', $GAFeed->expires_at); + variable_set('google_analytics_reports_refresh_token', $GAFeed->refresh_token); + variable_del('google_analytics_reports_redirect_uri'); + + drupal_set_message(t("You have been successfully authenticated."), 'status', FALSE); + drupal_goto($redirect_uri); + } + catch (Exception $e) { + drupal_set_message(t("There was an authentication error. Message: " . $e->getMessage()), 'error', FALSE); + return NULL; + } + } + else { + return NULL; + } } /** @@ -134,23 +171,30 @@ function google_analytics_api_report_data($params = array(), $cache_options = ar $params += $params_defaults; $GAFeed = google_analytics_api_new_gafeed(); - $GAFeed->queryReportFeed($params, $cache_options); - - return $GAFeed; + if ($GAFeed) { + $GAFeed->queryReportFeed($params, $cache_options); + return $GAFeed; + } + else { + drupal_set_message(t("There was an authentication error. Please check your Google API settings and try again."), 'error', FALSE); + $error = array('error' => TRUE); + return $error; + } } /* * Programatically revoke token. */ function google_analytics_api_revoke() { - $GAFeed = google_analytics_api_new_gafeed(); - $GAFeed->revokeToken(); - variable_del('google_analytics_reports_profile_id'); - variable_del('google_analytics_reports_consumer_key'); - variable_del('google_analytics_reports_consumer_secret'); - variable_del('google_analytics_reports_oauth_token'); - variable_del('google_analytics_reports_oauth_token_secret'); - variable_del('google_analytics_reports_cache_length'); + google_analytics_api_new_gafeed()->revokeToken(); + variable_del("google_analytics_reports_client_id"); + variable_del("google_analytics_reports_client_secret"); + variable_del("google_analytics_reports_access_token"); + variable_del("google_analytics_reports_expires_at"); + variable_del("google_analytics_reports_refresh_token"); + variable_del("google_analytics_reports_profile_id"); + variable_del('google_analytics_reports_cache_length'); + variable_del('google_analytics_reports_redirect_uri'); } /** diff --git a/google_analytics_api.pages.inc b/google_analytics_api.pages.inc index 7284141..9fa7749 100644 --- a/google_analytics_api.pages.inc +++ b/google_analytics_api.pages.inc @@ -9,9 +9,15 @@ * Menu callback - admin form for OAuth and other settings. */ function google_analytics_api_admin() { + $form = array(); + $GAFeed = google_analytics_api_new_gafeed(); - $form = array(); - $account = google_analytics_api_account_data(); + if ($GAFeed) { + $account = google_analytics_api_account_data(); + } + else { + $account = NULL; + } /* If there is at least one profile */ if (!empty($account->results)) { @@ -95,12 +101,23 @@ function google_analytics_api_admin() { '#collapsible' => TRUE, '#collapsed' => FALSE, ); - $form['setup']['google_analytics_reports_hd'] = array( - '#type' => 'textfield', - '#title' => t('Google Apps for Business Domain (optional)'), - '#description' => t('Provide the domain name (example.com) if your domain is registered with Google Apps for Business. Otherwise, leave blank.'), - '#default_value' => variable_get('google_analytics_reports_hd', ''), - ); + $form['setup']['client_id'] = array( + '#type' => 'textfield', + '#title' => t('Client ID'), + '#default_value' => variable_get('google_analytics_reports_client_id', ''), + '#size' => 30, + '#description' => t('Client ID created for the app in the access tab of the ') . l('Google API Console', 'http://code.google.com/apis/console', array('attributes' => array('target' => '_blank'))), + '#weight' => -9, + ); + + $form['setup']['client_secret'] = array( + '#type' => 'textfield', + '#title' => t('Client Secret'), + '#default_value' => variable_get('google_analytics_reports_client_secret', ''), + '#size' => 30, + '#description' => t('Client Secret created for the app in the Google API Console'), + '#weight' => -8, + ); $form['setup']['setup_submit'] = array( '#type' => 'submit', '#value' => t('Start setup and authorize account'), @@ -110,38 +127,34 @@ function google_analytics_api_admin() { } /** - * Submit handler. Steps throuh the OAuth process, revokes tokens, saves profiles. - */ + * Submit handler. Begins the OAuth process, revokes tokens, saves profiles. +*/ function google_analytics_api_admin_submit($form, &$form_state) { - $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : ''; - cache_clear_all('GAFeed', 'cache', '*'); - switch ($op) { - case t('Start setup and authorize account'): - variable_set('google_analytics_reports_hd', $form_state['values']['google_analytics_reports_hd']); - /* Anonymous keys are a Google default */ - $key = variable_get('google_analytics_reports_consumer_key', 'anonymous'); - $secret = variable_get('google_analytics_reports_consumer_secret', 'anonymous'); - module_load_include('inc', 'google_analytics_api', 'GAFeed.lib'); - $GAFeed = new GAFeed($key, $secret); - - /* Step #1 of OAuth */ - $token = $GAFeed->getRequestToken(); - $_SESSION['google_analytics_reports_oauth']['token'] = $token; - $_SESSION['google_analytics_reports_oauth']['destination'] = $_GET['q']; - - /* Step #2 of OAuth */ - $GAFeed->obtainAuthorization($token); - break; - case t('Save settings'): - variable_set('google_analytics_reports_profile_id', $form_state['values']['google_analytics_reports_profile_id']); - variable_set('google_analytics_reports_cache_length', $form_state['values']['google_analytics_reports_cache_length']); - drupal_set_message(t('Settings have been saved successfully.')); - break; - case t('Revoke access token'): - google_analytics_api_revoke(); - drupal_set_message(t('Access token has been successfully revoked.')); - break; - } + $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : ''; + cache_clear_all('GAFeed', 'cache', '*'); + switch ($op) { + case t('Start setup and authorize account'): + $client_id = $form_state['values']['client_id']; + $client_secret = $form_state['values']['client_secret']; + $redirect_uri = GAFeed::currentUrl(); + variable_set('google_analytics_reports_client_id', $client_id); + variable_set('google_analytics_reports_client_secret', $client_secret); + variable_set('google_analytics_reports_redirect_uri', $redirect_uri); + + module_load_include('inc', 'google_analytics_api', 'GAFeed.lib'); + $GAFeed = new GAFeed(); + $GAFeed -> beginAuthentication($client_id, $redirect_uri); + break; + case t('Save settings'): + variable_set('google_analytics_reports_profile_id', $form_state['values']['google_analytics_reports_profile_id']); + variable_set('google_analytics_reports_cache_length', $form_state['values']['google_analytics_reports_cache_length']); + drupal_set_message(t('Settings have been saved successfully.')); + break; + case t('Revoke access token'): + google_analytics_api_revoke(); + drupal_set_message(t('Access token has been successfully revoked.')); + break; + } } /** diff --git a/google_analytics_reports/google_analytics_reports.pages.inc b/google_analytics_reports/google_analytics_reports.pages.inc index 97079b9..dc329e9 100644 --- a/google_analytics_reports/google_analytics_reports.pages.inc +++ b/google_analytics_reports/google_analytics_reports.pages.inc @@ -10,7 +10,7 @@ * @return An HTML summary of the site-wide statistics. */ function google_analytics_reports_summary_page() { - if (!variable_get('google_analytics_reports_oauth_token', FALSE)) { + if (!variable_get('google_analytics_reports_access_token', FALSE)) { drupal_set_message(t('You must authorize Drupal to use your Analytics account before you can view reports.', array('!url' => url('admin/settings/google-analytics-reports'))), 'warning'); return ' '; }