diff --git a/README.md b/README.md index fe6a362..7124c45 100644 --- a/README.md +++ b/README.md @@ -149,9 +149,12 @@ eg; ## Note on Google Analytics Panel -You need to add your Google Analytics config information to the project _config.php: -```php -DashboardGoogleAnalyticsPanel::set_account($email, $password, $profileID); +You need to add your Google Analytics config information to the project config.php: +```yaml +DashboardGoogleAnalyticsPanel: + email: [XXXXX]@developer.gserviceaccount.com + profile: 123456 + key_file_path: google_oauth.p12 ``` To locate your profile ID, visit the Google Analytics website, login and select the website. At the end of the URL will be fragment similar to this: ``` @@ -160,4 +163,6 @@ To locate your profile ID, visit the Google Analytics website, login and select ``` The 8 digits that follow the "p" are your profile ID. In the example above, this would be 12345678. -NOTE: To use the Google Analytics panel, you have to enable access for less secure apps in the account permissions section of [https://www.google.com/settings/security](https://www.google.com/settings/security). \ No newline at end of file +NOTE: To use the Google Analytics panel, you have to enable access for less secure apps in the account permissions section of [https://www.google.com/settings/security](https://www.google.com/settings/security). + +For more information about settting up a developer account and obtaining a key file, visit https://github.com/erebusnz/gapi-google-analytics-php-interface#instructions-for-setting-up-a-google-service-account-for-use-with-gapi \ No newline at end of file diff --git a/_config/config.yml b/_config/config.yml index 5f45ce1..f97074c 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -10,4 +10,8 @@ SiteConfig: [DashboardSiteConfig] GridFieldDetailForm_ItemRequest: extensions: - [DashboardItemEditForm] \ No newline at end of file + [DashboardItemEditForm] +DashboardGoogleAnalyticsPanel: + email: '' + profile: '' + key_file_path: google_oauth.p12 \ No newline at end of file diff --git a/code/panels/DashboardGoogleAnalyticsPanel.php b/code/panels/DashboardGoogleAnalyticsPanel.php index 77c60bf..d51010b 100644 --- a/code/panels/DashboardGoogleAnalyticsPanel.php +++ b/code/panels/DashboardGoogleAnalyticsPanel.php @@ -27,24 +27,17 @@ class DashboardGoogleAnalyticsPanel extends DashboardPanel { - protected static $gapi_email; - - - - protected static $gapi_password; - - - - protected static $gapi_profile; - - - /** * @var gapi A stored instantiation of the Google Analytics API object */ protected $gapi; + /** + * Stores any exceptions coming from GAPI for easy rendering on the template + * @var string + */ + protected $error; /** @@ -67,17 +60,10 @@ public static function seconds_to_minutes($seconds) { /** - * Sets the username and password for the Google Analytics account - * - * @param string The email address of the account - * @param string The password of the account - * @param string The profile ID of the account - * + * DEPRECATED: Sets the username and password for the Google Analytics account */ public static function set_account($email, $password, $profile) { - self::$gapi_email = $email; - self::$gapi_password = $password; - self::$gapi_profile = $profile; + throw new Exception("DashboardGoogleAnalyticsPanel::set_account() is no longer supported. Use the config settings 'email', 'profile', and 'key_file_path' in the DashboardGoogleAnalyticsPanel config. More info on oauth key: https://github.com/erebusnz/gapi-google-analytics-php-interface#instructions-for-setting-up-a-google-service-account-for-use-with-gapi"); } @@ -94,6 +80,10 @@ public function getLabel() { } + public function getError() { + return $this->error; + } + /** * Gets the title for this panel @@ -112,12 +102,17 @@ public function getDescription() { * * @return gapi */ - public function api() { + public function api() { if(!$this->gapi) { try { - $this->gapi = new gapi(self::$gapi_email, self::$gapi_password); + $this->gapi = new gapi( + self::config()->email, + Director::getAbsFile(self::config()->key_file_path) + ); } catch(Exception $e) { + $this->error = $e->getMessage(); + return $this->gapi; } } @@ -231,7 +226,14 @@ public function IsConnected() { * @return bool */ public function IsConfigured() { - return self::$gapi_email && self::$gapi_password && self::$gapi_profile; + $c = self::config(); + + return ( + $c->email && + $c->key_file_path && + $c->profile && + Director::fileExists($c->key_file_path) + ); } @@ -322,7 +324,7 @@ public function Chart() { if(!$this->isValid()) return false; try { $this->api()->requestReportData( - self::$gapi_profile, + self::config()->profile, array('date'), array('pageviews'), 'date', @@ -331,6 +333,8 @@ public function Chart() { ); } catch(Exception $e) { + $this->error = $e->getMessage(); + return false; } @@ -375,7 +379,7 @@ public function PageResults() { if(!$this->isValid()) return false; try { $this->api()->requestReportData( - self::$gapi_profile, + self::config()->profile, 'pagePath', array('pageviews', 'uniquePageviews', 'exitRate', 'avgTimeOnPage', 'entranceBounceRate'), null, @@ -384,6 +388,8 @@ public function PageResults() { ); } catch(Exception $e) { + $this->error = $e->getMessage(); + return false; } $set = ArrayList::create(array()); diff --git a/code/thirdparty/gapi.php b/code/thirdparty/gapi.php index 27c1888..c704301 100755 --- a/code/thirdparty/gapi.php +++ b/code/thirdparty/gapi.php @@ -20,79 +20,88 @@ * along with this program. If not, see . * * @author Stig Manning - * @version 1.3 + * @author Joel Kitching + * @author Cliff Gordon + * @version 2.0 * */ -class gapi -{ - const http_interface = 'auto'; //'auto': autodetect, 'curl' or 'fopen' - - const client_login_url = 'https://www.google.com/accounts/ClientLogin'; - const account_data_url = 'https://www.google.com/analytics/feeds/accounts/default'; - const report_data_url = 'https://www.google.com/analytics/feeds/data'; - const interface_name = 'GAPI-1.3'; +class gapi { + const account_data_url = 'https://www.googleapis.com/analytics/v3/management/accountSummaries'; + const report_data_url = 'https://www.googleapis.com/analytics/v3/data/ga'; + const interface_name = 'GAPI-2.0'; const dev_mode = false; - - private $auth_token = null; + + private $auth_method = null; private $account_entries = array(); - private $account_root_parameters = array(); private $report_aggregate_metrics = array(); private $report_root_parameters = array(); private $results = array(); - + /** - * Constructor function for all new gapi instances - * - * Set up authenticate with Google and get auth_token + * Constructor function for new gapi instances * - * @param String $email - * @param String $password - * @param String $token + * @param string $client_email Email of OAuth2 service account (XXXXX@developer.gserviceaccount.com) + * @param string $key_file Location/filename of .p12 key file + * @param string $delegate_email Optional email of account to impersonate * @return gapi */ - public function __construct($email, $password, $token=null) - { - if($token !== null) - { - $this->auth_token = $token; - } - else - { - $this->authenticateUser($email,$password); + public function __construct($client_email, $key_file, $delegate_email = null) { + if (version_compare(PHP_VERSION, '5.3.0') < 0) { + throw new Exception('GAPI: PHP version ' . PHP_VERSION . ' is below minimum required 5.3.0.'); } + $this->auth_method = new gapiOAuth2(); + $this->auth_method->fetchToken($client_email, $key_file, $delegate_email); } - + /** - * Return the auth token, used for storing the auth token in the user session + * Return the auth token string retrieved by Google * * @return String */ - public function getAuthToken() - { - return $this->auth_token; + public function getToken() { + return $this->auth_method->getToken(); } - + + /** + * Return the auth token information from the Google service + * + * @return Array + */ + public function getTokenInfo() { + return $this->auth_method->getTokenInfo(); + } + + /** + * Revoke the current auth token, rendering it invalid for future requests + * + * @return Boolean + */ + public function revokeToken() { + return $this->auth_method->revokeToken(); + } + /** * Request account data from Google Analytics * * @param Int $start_index OPTIONAL: Start index of results * @param Int $max_results OPTIONAL: Max results returned */ - public function requestAccountData($start_index=1, $max_results=20) - { - $response = $this->httpRequest(gapi::account_data_url, array('start-index'=>$start_index,'max-results'=>$max_results), null, $this->generateAuthHeader()); - - if(substr($response['code'],0,1) == '2') - { + public function requestAccountData($start_index=1, $max_results=1000) { + $get_variables = array( + 'start-index' => $start_index, + 'max-results' => $max_results, + ); + $url = new gapiRequest(gapi::account_data_url); + $response = $url->get($get_variables, $this->auth_method->generateAuthHeader()); + + if (substr($response['code'], 0, 1) == '2') { return $this->accountObjectMapper($response['body']); - } - else - { + } else { throw new Exception('GAPI: Failed to request account data. Error: "' . strip_tags($response['body']) . '"'); } } - + /** * Request report data from Google Analytics * @@ -110,111 +119,110 @@ public function requestAccountData($start_index=1, $max_results=20) * @param Int $start_index OPTIONAL: Start index of results * @param Int $max_results OPTIONAL: Max results returned */ - public function requestReportData($report_id, $dimensions, $metrics, $sort_metric=null, $filter=null, $start_date=null, $end_date=null, $start_index=1, $max_results=30) - { + public function requestReportData($report_id, $dimensions=null, $metrics, $sort_metric=null, $filter=null, $start_date=null, $end_date=null, $start_index=1, $max_results=10000) { $parameters = array('ids'=>'ga:' . $report_id); - - if(is_array($dimensions)) - { + + if (is_array($dimensions)) { $dimensions_string = ''; - foreach($dimensions as $dimesion) - { + foreach ($dimensions as $dimesion) { $dimensions_string .= ',ga:' . $dimesion; } - $parameters['dimensions'] = substr($dimensions_string,1); - } - else - { + $parameters['dimensions'] = substr($dimensions_string, 1); + } elseif ($dimensions !== null) { $parameters['dimensions'] = 'ga:'.$dimensions; } - if(is_array($metrics)) - { + if (is_array($metrics)) { $metrics_string = ''; - foreach($metrics as $metric) - { + foreach ($metrics as $metric) { $metrics_string .= ',ga:' . $metric; } - $parameters['metrics'] = substr($metrics_string,1); - } - else - { + $parameters['metrics'] = substr($metrics_string, 1); + } else { $parameters['metrics'] = 'ga:'.$metrics; } - - if($sort_metric==null&&isset($parameters['metrics'])) - { + + if ($sort_metric==null&&isset($parameters['metrics'])) { $parameters['sort'] = $parameters['metrics']; - } - elseif(is_array($sort_metric)) - { + } elseif (is_array($sort_metric)) { $sort_metric_string = ''; - - foreach($sort_metric as $sort_metric_value) - { + + foreach ($sort_metric as $sort_metric_value) { //Reverse sort - Thanks Nick Sullivan - if (substr($sort_metric_value, 0, 1) == "-") - { + if (substr($sort_metric_value, 0, 1) == "-") { $sort_metric_string .= ',-ga:' . substr($sort_metric_value, 1); // Descending } - else - { + else { $sort_metric_string .= ',ga:' . $sort_metric_value; // Ascending } } - + $parameters['sort'] = substr($sort_metric_string, 1); - } - else - { - if (substr($sort_metric, 0, 1) == "-") - { + } else { + if (substr($sort_metric, 0, 1) == "-") { $parameters['sort'] = '-ga:' . substr($sort_metric, 1); - } - else - { + } else { $parameters['sort'] = 'ga:' . $sort_metric; } } - - if($filter!=null) - { + + if ($filter!=null) { $filter = $this->processFilter($filter); - if($filter!==false) - { + if ($filter!==false) { $parameters['filters'] = $filter; } } - - if($start_date==null) - { - $start_date=date('Y-m-d',strtotime('1 month ago')); + + if ($start_date==null) { + // Use the day that Google Analytics was released (1 Jan 2005). + $start_date = '2005-01-01'; + } elseif (is_int($start_date)) { + // Perhaps we are receiving a Unix timestamp. + $start_date = date('Y-m-d', $start_date); } - + $parameters['start-date'] = $start_date; - if($end_date==null) - { - $end_date=date('Y-m-d'); + + if ($end_date==null) { + $end_date = date('Y-m-d'); + } elseif (is_int($end_date)) { + // Perhaps we are receiving a Unix timestamp. + $end_date = date('Y-m-d', $end_date); } - + $parameters['end-date'] = $end_date; - - + + $parameters['start-index'] = $start_index; $parameters['max-results'] = $max_results; - + $parameters['prettyprint'] = gapi::dev_mode ? 'true' : 'false'; - $response = $this->httpRequest(gapi::report_data_url, $parameters, null, $this->generateAuthHeader()); - + $url = new gapiRequest(gapi::report_data_url); + $response = $url->get($parameters, $this->auth_method->generateAuthHeader()); + //HTTP 2xx - if(substr($response['code'],0,1) == '2') - { + if (substr($response['code'], 0, 1) == '2') { return $this->reportObjectMapper($response['body']); + } else { + throw new Exception('GAPI: Failed to request report data. Error: "' . $this->cleanErrorResponse($response['body']) . '"'); + } + } + + /** + * Clean error message from Google API + * + * @param String $error Error message HTML or JSON from Google API + */ + private function cleanErrorResponse($error) { + if (strpos($error, ']*>[^<]*<\/(style|title|script)>/i', '', $error); + return trim(preg_replace('/\s+/', ' ', strip_tags($error))); } - else + else { - throw new Exception('GAPI: Failed to request report data. Error: "' . strip_tags($response['body']) . '"'); + $json = json_decode($error); + return isset($json->error->message) ? strval($json->error->message) : $error; } } @@ -225,341 +233,180 @@ public function requestReportData($report_id, $dimensions, $metrics, $sort_metri * @param String $filter * @return String Compatible filter string */ - protected function processFilter($filter) - { + protected function processFilter($filter) { $valid_operators = '(!~|=~|==|!=|>|<|>=|<=|=@|!@)'; - - $filter = preg_replace('/\s\s+/',' ',trim($filter)); //Clean duplicate whitespace - $filter = str_replace(array(',',';'),array('\,','\;'),$filter); //Escape Google Analytics reserved characters - $filter = preg_replace('/(&&\s*|\|\|\s*|^)([a-z]+)(\s*' . $valid_operators . ')/i','$1ga:$2$3',$filter); //Prefix ga: to metrics and dimensions - $filter = preg_replace('/[\'\"]/i','',$filter); //Clear invalid quote characters - $filter = preg_replace(array('/\s*&&\s*/','/\s*\|\|\s*/','/\s*' . $valid_operators . '\s*/'),array(';',',','$1'),$filter); //Clean up operators - - if(strlen($filter)>0) - { + + $filter = preg_replace('/\s\s+/', ' ', trim($filter)); //Clean duplicate whitespace + $filter = str_replace(array(',', ';'), array('\,', '\;'), $filter); //Escape Google Analytics reserved characters + $filter = preg_replace('/(&&\s*|\|\|\s*|^)([a-z0-9]+)(\s*' . $valid_operators . ')/i','$1ga:$2$3',$filter); //Prefix ga: to metrics and dimensions + $filter = preg_replace('/[\'\"]/i', '', $filter); //Clear invalid quote characters + $filter = preg_replace(array('/\s*&&\s*/','/\s*\|\|\s*/','/\s*' . $valid_operators . '\s*/'), array(';', ',', '$1'), $filter); //Clean up operators + + if (strlen($filter) > 0) { return urlencode($filter); } - else - { + else { return false; } } - + /** - * Report Account Mapper to convert the XML to array of useful PHP objects + * Report Account Mapper to convert the JSON to array of useful PHP objects * - * @param String $xml_string + * @param String $json_string * @return Array of gapiAccountEntry objects */ - protected function accountObjectMapper($xml_string) - { - $xml = simplexml_load_string($xml_string); - - $this->results = null; - + protected function accountObjectMapper($json_string) { + $json = json_decode($json_string, true); $results = array(); - $account_root_parameters = array(); - - //Load root parameters - - $account_root_parameters['updated'] = strval($xml->updated); - $account_root_parameters['generator'] = strval($xml->generator); - $account_root_parameters['generatorVersion'] = strval($xml->generator->attributes()); - - $open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/'); - - foreach($open_search_results as $key => $open_search_result) - { - $report_root_parameters[$key] = intval($open_search_result); - } - - $account_root_parameters['startDate'] = strval($google_results->startDate); - $account_root_parameters['endDate'] = strval($google_results->endDate); - - //Load result entries - - foreach($xml->entry as $entry) - { - $properties = array(); - foreach($entry->children('http://schemas.google.com/analytics/2009')->property as $property) - { - $properties[str_replace('ga:','',$property->attributes()->name)] = strval($property->attributes()->value); + + foreach ($json['items'] as $item) { + foreach ($item['webProperties'] as $property) { + if (isset($property['profiles'][0]['id'])) { + $property['ProfileId'] = $property['profiles'][0]['id']; + } + $results[] = new gapiAccountEntry($property); } - - $properties['title'] = strval($entry->title); - $properties['updated'] = strval($entry->updated); - - $results[] = new gapiAccountEntry($properties); } - - $this->account_root_parameters = $account_root_parameters; - $this->results = $results; - + + $this->account_entries = $results; + return $results; } - - + /** - * Report Object Mapper to convert the XML to array of useful PHP objects + * Report Object Mapper to convert the JSON to array of useful PHP objects * - * @param String $xml_string + * @param String $json_string * @return Array of gapiReportEntry objects */ - protected function reportObjectMapper($xml_string) - { - $xml = simplexml_load_string($xml_string); - + protected function reportObjectMapper($json_string) { + $json = json_decode($json_string, true); + $this->results = null; $results = array(); - - $report_root_parameters = array(); + $report_aggregate_metrics = array(); - + //Load root parameters - - $report_root_parameters['updated'] = strval($xml->updated); - $report_root_parameters['generator'] = strval($xml->generator); - $report_root_parameters['generatorVersion'] = strval($xml->generator->attributes()); - - $open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/'); - - foreach($open_search_results as $key => $open_search_result) - { - $report_root_parameters[$key] = intval($open_search_result); - } - - $google_results = $xml->children('http://schemas.google.com/analytics/2009'); - foreach($google_results->dataSource->property as $property_attributes) - { - $report_root_parameters[str_replace('ga:','',$property_attributes->attributes()->name)] = strval($property_attributes->attributes()->value); + // Start with elements from the root level of the JSON that aren't themselves arrays. + $report_root_parameters = array_filter($json, function($var){ + return !is_array($var); + }); + + // Get the items from the 'query' object, and rename them slightly. + foreach($json['query'] as $index => $value) { + $new_index = lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $index)))); + $report_root_parameters[$new_index] = $value; } - - $report_root_parameters['startDate'] = strval($google_results->startDate); - $report_root_parameters['endDate'] = strval($google_results->endDate); - + + // Now merge in the profileInfo, as this is also mostly useful. + array_merge($report_root_parameters, $json['profileInfo']); + //Load result aggregate metrics - - foreach($google_results->aggregates->metric as $aggregate_metric) - { - $metric_value = strval($aggregate_metric->attributes()->value); - + + foreach($json['totalsForAllResults'] as $index => $metric_value) { //Check for float, or value with scientific notation - if(preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/',$metric_value)) - { - $report_aggregate_metrics[str_replace('ga:','',$aggregate_metric->attributes()->name)] = floatval($metric_value); - } - else - { - $report_aggregate_metrics[str_replace('ga:','',$aggregate_metric->attributes()->name)] = intval($metric_value); + if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value)) { + $report_aggregate_metrics[str_replace('ga:', '', $index)] = floatval($metric_value); + } else { + $report_aggregate_metrics[str_replace('ga:', '', $index)] = intval($metric_value); } } - + //Load result entries - - foreach($xml->entry as $entry) - { - $metrics = array(); - foreach($entry->children('http://schemas.google.com/analytics/2009')->metric as $metric) - { - $metric_value = strval($metric->attributes()->value); - - //Check for float, or value with scientific notation - if(preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/',$metric_value)) - { - $metrics[str_replace('ga:','',$metric->attributes()->name)] = floatval($metric_value); - } - else - { - $metrics[str_replace('ga:','',$metric->attributes()->name)] = intval($metric_value); + if(isset($json['rows'])){ + foreach($json['rows'] as $row) { + $metrics = array(); + $dimensions = array(); + foreach($json['columnHeaders'] as $index => $header) { + switch($header['columnType']) { + case 'METRIC': + $metric_value = $row[$index]; + + //Check for float, or value with scientific notation + if(preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/',$metric_value)) { + $metrics[str_replace('ga:', '', $header['name'])] = floatval($metric_value); + } else { + $metrics[str_replace('ga:', '', $header['name'])] = intval($metric_value); + } + break; + case 'DIMENSION': + $dimensions[str_replace('ga:', '', $header['name'])] = strval($row[$index]); + break; + default: + throw new Exception("GAPI: Unrecognized columnType '{$header['columnType']}' for columnHeader '{$header['name']}'"); + } } + $results[] = new gapiReportEntry($metrics, $dimensions); } - - $dimensions = array(); - foreach($entry->children('http://schemas.google.com/analytics/2009')->dimension as $dimension) - { - $dimensions[str_replace('ga:','',$dimension->attributes()->name)] = strval($dimension->attributes()->value); - } - - $results[] = new gapiReportEntry($metrics,$dimensions); } - + $this->report_root_parameters = $report_root_parameters; $this->report_aggregate_metrics = $report_aggregate_metrics; $this->results = $results; - + return $results; } - + /** - * Authenticate Google Account with Google + * Get current analytics results * - * @param String $email - * @param String $password + * @return Array */ - protected function authenticateUser($email, $password) - { - $post_variables = array( - 'accountType' => 'GOOGLE', - 'Email' => $email, - 'Passwd' => $password, - 'source' => gapi::interface_name, - 'service' => 'analytics' - ); - - $response = $this->httpRequest(gapi::client_login_url,null,$post_variables); - - //Convert newline delimited variables into url format then import to array - parse_str(str_replace(array("\n","\r\n"),'&',$response['body']),$auth_token); - - if(substr($response['code'],0,1) != '2' || !is_array($auth_token) || empty($auth_token['Auth'])) - { - throw new Exception('GAPI: Failed to authenticate user. Error: "' . strip_tags($response['body']) . '"'); - } - - $this->auth_token = $auth_token['Auth']; + public function getResults() { + return is_array($this->results) ? $this->results : false; } - + /** - * Generate authentication token header for all requests + * Get current account data * * @return Array */ - protected function generateAuthHeader() - { - return array('Authorization: GoogleLogin auth=' . $this->auth_token); + public function getAccounts() { + return is_array($this->account_entries) ? $this->account_entries : false; } - + /** - * Perform http request - * + * Get an array of the metrics and the matching + * aggregate values for the current result * - * @param Array $get_variables - * @param Array $post_variables - * @param Array $headers - */ - protected function httpRequest($url, $get_variables=null, $post_variables=null, $headers=null) - { - $interface = gapi::http_interface; - - if(gapi::http_interface =='auto') - { - if(function_exists('curl_exec')) - { - $interface = 'curl'; - } - else - { - $interface = 'fopen'; - } - } - - if($interface == 'curl') - { - return $this->curlRequest($url, $get_variables, $post_variables, $headers); - } - elseif($interface == 'fopen') - { - return $this->fopenRequest($url, $get_variables, $post_variables, $headers); - } - else - { - throw new Exception('Invalid http interface defined. No such interface "' . gapi::http_interface . '"'); - } - } - - /** - * HTTP request using PHP CURL functions - * Requires curl library installed and configured for PHP - * - * @param Array $get_variables - * @param Array $post_variables - * @param Array $headers + * @return Array */ - private function curlRequest($url, $get_variables=null, $post_variables=null, $headers=null) - { - $ch = curl_init(); - - if(is_array($get_variables)) - { - $get_variables = '?' . str_replace('&','&',urldecode(http_build_query($get_variables))); - } - else - { - $get_variables = null; - } - - curl_setopt($ch, CURLOPT_URL, $url . $get_variables); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //CURL doesn't like google's cert - - if(is_array($post_variables)) - { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_variables); - } - - if(is_array($headers)) - { - curl_setopt($ch, CURLOPT_HTTPHEADER,$headers); - } - - $response = curl_exec($ch); - $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); - - curl_close($ch); - - return array('body'=>$response,'code'=>$code); + public function getMetrics() { + return $this->report_aggregate_metrics; } - + /** - * HTTP request using native PHP fopen function - * Requires PHP openSSL + * Call method to find a matching root parameter or + * aggregate metric to return * - * @param Array $get_variables - * @param Array $post_variables - * @param Array $headers + * @param $name String name of function called + * @return String + * @throws Exception if not a valid parameter or aggregate + * metric, or not a 'get' function */ - private function fopenRequest($url, $get_variables=null, $post_variables=null, $headers=null) - { - $http_options = array('method'=>'GET','timeout'=>3); - - if(is_array($headers)) - { - $headers = implode("\r\n",$headers) . "\r\n"; - } - else - { - $headers = ''; - } - - if(is_array($get_variables)) - { - $get_variables = '?' . str_replace('&','&',urldecode(http_build_query($get_variables))); - } - else - { - $get_variables = null; + public function __call($name, $parameters) { + if (!preg_match('/^get/', $name)) { + throw new Exception('No such function "' . $name . '"'); } - - if(is_array($post_variables)) - { - $post_variables = str_replace('&','&',urldecode(http_build_query($post_variables))); - $http_options['method'] = 'POST'; - $headers = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($post_variables) . "\r\n" . $headers; - $http_options['header'] = $headers; - $http_options['content'] = $post_variables; + + $name = preg_replace('/^get/', '', $name); + + $parameter_key = gapi::ArrayKeyExists($name, $this->report_root_parameters); + + if ($parameter_key) { + return $this->report_root_parameters[$parameter_key]; } - else - { - $post_variables = ''; - $http_options['header'] = $headers; + + $aggregate_metric_key = gapi::ArrayKeyExists($name, $this->report_aggregate_metrics); + + if ($aggregate_metric_key) { + return $this->report_aggregate_metrics[$aggregate_metric_key]; } - - $context = stream_context_create(array('http'=>$http_options)); - $response = @file_get_contents($url . $get_variables, null, $context); - - return array('body'=>$response!==false?$response:'Request failed, fopen provides no further information','code'=>$response!==false?'200':'400'); + + throw new Exception('No valid root parameter or aggregate metric called "' . $name . '"'); } /** @@ -570,135 +417,60 @@ private function fopenRequest($url, $get_variables=null, $post_variables=null, $ * @param Array $search * @return String Matching array key */ - public static function array_key_exists_nc($key, $search) - { - if (array_key_exists($key, $search)) - { + public static function ArrayKeyExists($key, $search) { + if (array_key_exists($key, $search)) { return $key; } - if (!(is_string($key) && is_array($search))) - { + if (!(is_string($key) && is_array($search))) { return false; } $key = strtolower($key); - foreach ($search as $k => $v) - { - if (strtolower($k) == $key) - { + foreach ($search as $k => $v) { + if (strtolower($k) == $key) { return $k; } } return false; } - - /** - * Get Results - * - * @return Array - */ - public function getResults() - { - if(is_array($this->results)) - { - return $this->results; - } - else - { - return; - } - } - - - /** - * Get an array of the metrics and the matchning - * aggregate values for the current result - * - * @return Array - */ - public function getMetrics() - { - return $this->report_aggregate_metrics; - } - - /** - * Call method to find a matching root parameter or - * aggregate metric to return - * - * @param $name String name of function called - * @return String - * @throws Exception if not a valid parameter or aggregate - * metric, or not a 'get' function - */ - public function __call($name,$parameters) - { - if(!preg_match('/^get/',$name)) - { - throw new Exception('No such function "' . $name . '"'); - } - - $name = preg_replace('/^get/','',$name); - - $parameter_key = gapi::array_key_exists_nc($name,$this->report_root_parameters); - - if($parameter_key) - { - return $this->report_root_parameters[$parameter_key]; - } - - $aggregate_metric_key = gapi::array_key_exists_nc($name,$this->report_aggregate_metrics); - - if($aggregate_metric_key) - { - return $this->report_aggregate_metrics[$aggregate_metric_key]; - } - - throw new Exception('No valid root parameter or aggregate metric called "' . $name . '"'); - } } /** - * Class gapiAccountEntry - * * Storage for individual gapi account entries * */ -class gapiAccountEntry -{ +class gapiAccountEntry { private $properties = array(); - - public function __construct($properties) - { + + /** + * Constructor function for all new gapiAccountEntry instances + * + * @param Array $properties + * @return gapiAccountEntry + */ + public function __construct($properties) { $this->properties = $properties; } - + /** * toString function to return the name of the account * * @return String */ - public function __toString() - { - if(isset($this->properties['title'])) - { - return $this->properties['title']; - } - else - { - return; - } + public function __toString() { + return isset($this->properties['name']) ? + $this->properties['name']: ''; } - + /** * Get an associative array of the properties * and the matching values for the current result * * @return Array */ - public function getProperties() - { + public function getProperties() { return $this->properties; } - + /** * Call method to find a matching parameter to return * @@ -706,116 +478,376 @@ public function getProperties() * @return String * @throws Exception if not a valid parameter, or not a 'get' function */ - public function __call($name,$parameters) - { - if(!preg_match('/^get/',$name)) - { + public function __call($name, $parameters) { + if (!preg_match('/^get/', $name)) { throw new Exception('No such function "' . $name . '"'); } - - $name = preg_replace('/^get/','',$name); - - $property_key = gapi::array_key_exists_nc($name,$this->properties); - - if($property_key) - { + + $name = preg_replace('/^get/', '', $name); + + $property_key = gapi::ArrayKeyExists($name, $this->properties); + + if ($property_key) { return $this->properties[$property_key]; } - + throw new Exception('No valid property called "' . $name . '"'); } } /** - * Class gapiReportEntry - * * Storage for individual gapi report entries * */ -class gapiReportEntry -{ +class gapiReportEntry { private $metrics = array(); private $dimensions = array(); - - public function __construct($metrics,$dimesions) - { + + /** + * Constructor function for all new gapiReportEntry instances + * + * @param Array $metrics + * @param Array $dimensions + * @return gapiReportEntry + */ + public function __construct($metrics, $dimensions) { $this->metrics = $metrics; - $this->dimensions = $dimesions; + $this->dimensions = $dimensions; } - + /** * toString function to return the name of the result - * this is a concatented string of the dimesions chosen + * this is a concatented string of the dimensions chosen * * For example: * 'Firefox 3.0.10' from browser and browserVersion * * @return String */ - public function __toString() - { - if(is_array($this->dimensions)) - { - return implode(' ',$this->dimensions); - } - else - { - return ''; - } + public function __toString() { + return is_array($this->dimensions) ? + implode(' ', $this->dimensions) : ''; } - + /** - * Get an associative array of the dimesions + * Get an associative array of the dimensions * and the matching values for the current result * * @return Array */ - public function getDimesions() - { + public function getDimensions() { return $this->dimensions; } - + /** * Get an array of the metrics and the matchning * values for the current result * * @return Array */ - public function getMetrics() - { + public function getMetrics() { return $this->metrics; } - + /** * Call method to find a matching metric or dimension to return * - * @param $name String name of function called + * @param String $name name of function called + * @param Array $parameters * @return String * @throws Exception if not a valid metric or dimensions, or not a 'get' function */ - public function __call($name,$parameters) - { - if(!preg_match('/^get/',$name)) - { + public function __call($name, $parameters) { + if (!preg_match('/^get/', $name)) { throw new Exception('No such function "' . $name . '"'); } - - $name = preg_replace('/^get/','',$name); - - $metric_key = gapi::array_key_exists_nc($name,$this->metrics); - - if($metric_key) - { + + $name = preg_replace('/^get/', '', $name); + + $metric_key = gapi::ArrayKeyExists($name, $this->metrics); + + if ($metric_key) { return $this->metrics[$metric_key]; } - - $dimension_key = gapi::array_key_exists_nc($name,$this->dimensions); - - if($dimension_key) - { + + $dimension_key = gapi::ArrayKeyExists($name, $this->dimensions); + + if ($dimension_key) { return $this->dimensions[$dimension_key]; } throw new Exception('No valid metric or dimesion called "' . $name . '"'); } +} + +/** + * OAuth2 Google API authentication + * + */ +class gapiOAuth2 { + const scope_url = 'https://www.googleapis.com/auth/analytics.readonly'; + const request_url = 'https://www.googleapis.com/oauth2/v3/token'; + const grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + const header_alg = 'RS256'; + const header_typ = 'JWT'; + + private function base64URLEncode($data) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private function base64URLDecode($data) { + return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT)); + } + + /** + * Authenticate Google Account with OAuth2 + * + * @param String $client_email + * @param String $key_file + * @param String $delegate_email + * @return String Authentication token + */ + public function fetchToken($client_email, $key_file, $delegate_email = null) { + $header = array( + "alg" => self::header_alg, + "typ" => self::header_typ, + ); + + $claimset = array( + "iss" => $client_email, + "scope" => self::scope_url, + "aud" => self::request_url, + "exp" => time() + (60 * 60), + "iat" => time(), + ); + + if(!empty($delegate_email)) { + $claimset["sub"] = $delegate_email; + } + + $data = $this->base64URLEncode(json_encode($header)) . '.' . $this->base64URLEncode(json_encode($claimset)); + + if (!file_exists($key_file)) { + if ( !file_exists(__DIR__ . DIRECTORY_SEPARATOR . $key_file) ) { + throw new Exception('GAPI: Failed load key file "' . $key_file . '". File could not be found.'); + } else { + $key_file = __DIR__ . DIRECTORY_SEPARATOR . $key_file; + } + } + + $key_data = file_get_contents($key_file); + + if (empty($key_data)) { + throw new Exception('GAPI: Failed load key file "' . $key_file . '". File could not be opened or is empty.'); + } + + openssl_pkcs12_read($key_data, $certs, 'notasecret'); + + if (!isset($certs['pkey'])) { + throw new Exception('GAPI: Failed load key file "' . $key_file . '". Unable to load pkcs12 check if correct p12 format.'); + } + + openssl_sign($data, $signature, openssl_pkey_get_private($certs['pkey']), "sha256"); + + $post_variables = array( + 'grant_type' => self::grant_type, + 'assertion' => $data . '.' . $this->base64URLEncode($signature), + ); + + $url = new gapiRequest(self::request_url); + $response = $url->post(null, $post_variables); + $auth_token = json_decode($response['body'], true); + + if (substr($response['code'], 0, 1) != '2' || !is_array($auth_token) || empty($auth_token['access_token'])) { + throw new Exception('GAPI: Failed to authenticate user. Error: "' . strip_tags($response['body']) . '"'); + } + + $this->auth_token = $auth_token['access_token']; + + return $this->auth_token; + } + + /** + * Return the auth token string retrieved from Google + * + * @return String + */ + public function getToken() { + return $this->auth_token; + } + + /** + * Generate authorization token header for all requests + * + * @param String $token + * @return Array + */ + public function generateAuthHeader($token=null) { + if ($token == null) + $token = $this->auth_token; + return array('Authorization' => 'Bearer ' . $token); + } +} + +/** + * Google Analytics API request + * + */ +class gapiRequest { + const http_interface = 'auto'; //'auto': autodetect, 'curl' or 'fopen' + const interface_name = gapi::interface_name; + + private $url = null; + + public function __construct($url) { + $this->url = $url; + } + + /** + * Return the URL to be requested, optionally adding GET variables + * + * @param Array $get_variables + * @return String + */ + public function getUrl($get_variables=null) { + if (is_array($get_variables)) { + $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&'))); + } else { + $get_variables = null; + } + + return $this->url . $get_variables; + } + + /** + * Perform http POST request + * + * + * @param Array $get_variables + * @param Array $post_variables + * @param Array $headers + */ + public function post($get_variables=null, $post_variables=null, $headers=null) { + return $this->request($get_variables, $post_variables, $headers); + } + + /** + * Perform http GET request + * + * + * @param Array $get_variables + * @param Array $headers + */ + public function get($get_variables=null, $headers=null) { + return $this->request($get_variables, null, $headers); + } + + /** + * Perform http request + * + * + * @param Array $get_variables + * @param Array $post_variables + * @param Array $headers + */ + public function request($get_variables=null, $post_variables=null, $headers=null) { + $interface = self::http_interface; + + if (self::http_interface == 'auto') + $interface = function_exists('curl_exec') ? 'curl' : 'fopen'; + + switch ($interface) { + case 'curl': + return $this->curlRequest($get_variables, $post_variables, $headers); + case 'fopen': + return $this->fopenRequest($get_variables, $post_variables, $headers); + default: + throw new Exception('Invalid http interface defined. No such interface "' . self::http_interface . '"'); + } + } + + /** + * HTTP request using PHP CURL functions + * Requires curl library installed and configured for PHP + * + * @param Array $get_variables + * @param Array $post_variables + * @param Array $headers + */ + private function curlRequest($get_variables=null, $post_variables=null, $headers=null) { + $ch = curl_init(); + + if (is_array($get_variables)) { + $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&'))); + } else { + $get_variables = null; + } + + curl_setopt($ch, CURLOPT_URL, $this->url . $get_variables); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //CURL doesn't like google's cert + + if (is_array($post_variables)) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_variables, '', '&')); + } + + if (is_array($headers)) { + $string_headers = array(); + foreach ($headers as $key => $value) { + $string_headers[] = "$key: $value"; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $string_headers); + } + + $response = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + return array('body' => $response, 'code' => $code); + } + + /** + * HTTP request using native PHP fopen function + * Requires PHP openSSL + * + * @param Array $get_variables + * @param Array $post_variables + * @param Array $headers + */ + private function fopenRequest($get_variables=null, $post_variables=null, $headers=null) { + $http_options = array('method'=>'GET', 'timeout'=>3); + + $string_headers = ''; + if (is_array($headers)) { + foreach ($headers as $key => $value) { + $string_headers .= "$key: $value\r\n"; + } + } + + if (is_array($get_variables)) { + $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&'))); + } + else { + $get_variables = null; + } + + if (is_array($post_variables)) { + $post_variables = str_replace('&', '&', urldecode(http_build_query($post_variables, '', '&'))); + $http_options['method'] = 'POST'; + $string_headers = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($post_variables) . "\r\n" . $string_headers; + $http_options['header'] = $string_headers; + $http_options['content'] = $post_variables; + } + else { + $post_variables = ''; + $http_options['header'] = $string_headers; + } + + $context = stream_context_create(array('http'=>$http_options)); + $response = @file_get_contents($this->url . $get_variables, null, $context); + + return array('body'=>$response!==false?$response:'Request failed, consider using php5-curl module for more information.', 'code'=>$response!==false?'200':'400'); + } } \ No newline at end of file diff --git a/templates/Includes/DashboardGoogleAnalyticsPanel.ss b/templates/Includes/DashboardGoogleAnalyticsPanel.ss index f07c1a6..0803978 100644 --- a/templates/Includes/DashboardGoogleAnalyticsPanel.ss +++ b/templates/Includes/DashboardGoogleAnalyticsPanel.ss @@ -1,5 +1,5 @@ <% if not IsConfigured %> - <% _t('Dashboard.NOGOOGLEACCOUNT','

You have not defined a Google Analytics account for this project. You can set this up by adding the following code to your project _config.php file:

DashboardGoogleAnalyticsPanel::set_account($email, $password, $profileID);

Where $email is your Google email, $password is your password, and $profileID is the profile ID of the account, found on the "Profile Settings" tab of the Google Analytics profile for this project.

') %> + <% _t('Dashboard.NOGOOGLEACCOUNT','

You have not defined a Google Analytics account for this project. You can set this up by setting the config settings email, profile, and key_file_path on DashboardGoogleAnalyticsPanel.

More information on key files

') %> <% else_if not IsConnected %> <% _t('Dashboard.INVALIDGOOGLEACCOUNT','

The account information you have entered for Google Analytics appears to be invalid. Please check the email and password combination and try again.

') %> <% else %> @@ -17,4 +17,8 @@ <% end_if %> +<% end_if %> + +<% if $Error %> +

$Error

<% end_if %> \ No newline at end of file