diff --git a/modules/gapi/config.xml b/modules/gapi/config.xml new file mode 100644 index 000000000..acc350fbd --- /dev/null +++ b/modules/gapi/config.xml @@ -0,0 +1,12 @@ + + + gapi + + + + + + 1 + 0 + + \ No newline at end of file diff --git a/modules/gapi/gapi.php b/modules/gapi/gapi.php new file mode 100644 index 000000000..31e5276a6 --- /dev/null +++ b/modules/gapi/gapi.php @@ -0,0 +1,407 @@ + +* @copyright 2007-2013 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +if (!defined('_PS_VERSION_')) + exit; + +class Gapi extends Module +{ + public function __construct($type = null) + { + $this->name = 'gapi'; + $this->tab = 'administration'; + $this->version = 0.9; + $this->author = 'PrestaShop'; + $this->need_instance = 0; + + parent::__construct(); + + $this->displayName = $this->l('Google Analytics API'); + } + + public function getContent() + { + // You can switch to the 1.3 API by replacing the following function call by $this->api_1_3_getContent() + return $this->api_3_0_getContent(); + } + + public function api_3_0_authenticate() + { + // https://developers.google.com/accounts/docs/OAuth2WebServer + $params = array( + 'response_type' => 'code', + 'client_id' => Configuration::get('PS_GAPI30_CLIENT_ID_TMP'), + 'scope' => 'https://www.googleapis.com/auth/analytics.readonly', + 'redirect_uri' => Tools::getShopDomain(true, false).__PS_BASE_URI__.'modules/'.$this->name.'/oauth2callback.php', + 'state' => $this->context->employee->id.'-'.Tools::encrypt($this->context->employee->id.Configuration::get('PS_GAPI30_CLIENT_ID_TMP')), + 'approval_prompt' => 'force', + 'access_type' => 'offline' + ); + Tools::redirectLink('https://accounts.google.com/o/oauth2/auth?'.http_build_query($params)); + } + + public function api_3_0_refreshtoken() + { + $params = array( + 'client_id' => Configuration::get('PS_GAPI30_CLIENT_ID'), + 'client_secret' => Configuration::get('PS_GAPI30_CLIENT_SECRET'), + 'grant_type' => 'authorization_code' + ); + + // https://developers.google.com/accounts/docs/OAuth2WebServer#offline + if (Configuration::get('PS_GAPI30_REFRESH_TOKEN')) + $params['refresh_token'] = Configuration::get('PS_GAPI30_REFRESH_TOKEN'); + else + { + $params['code'] = Configuration::get('PS_GAPI30_AUTHORIZATION_CODE'); + $params['redirect_uri'] = Tools::getShopDomain(true, false).__PS_BASE_URI__.'modules/'.$this->name.'/oauth2callback.php'; + } + + $stream_context = stream_context_create(array( + 'http' => array( + 'method'=> 'POST', + 'content' => http_build_query($params), + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'timeout' => 5, + ) + )); + + if (!$response_json = Tools::file_get_contents('https://accounts.google.com/o/oauth2/token', false, $stream_context)) + return false; + + $response = Tools::jsonDecode($response_json, true); + Configuration::updateValue('PS_GAPI30_ACCESS_TOKEN', $response['access_token']); + Configuration::updateValue('PS_GAPI30_TOKEN_EXPIRATION', time() + (int)$response['expires_in']); + if (isset($response['refresh_token'])) + Configuration::updateValue('PS_GAPI30_REFRESH_TOKEN', $response['refresh_token']); + return true; + } + + public function api_3_0_getContent() + { + $html = ''; + if (Tools::isSubmit('PS_GAPI30_CLIENT_ID')) + { + Configuration::updateValue('PS_GAPI30_REQUEST_URI_TMP', dirname($_SERVER['REQUEST_URI']).'/'.AdminController::$currentIndex.'&configure='.$this->name.'&token='.Tools::getAdminTokenLite('AdminModules')); + Configuration::updateValue('PS_GAPI30_CLIENT_ID_TMP', trim(Tools::getValue('PS_GAPI30_CLIENT_ID'))); + Configuration::updateValue('PS_GAPI30_CLIENT_SECRET_TMP', trim(Tools::getValue('PS_GAPI30_CLIENT_SECRET'))); + Configuration::updateValue('PS_GAPI30_PROFILE_TMP', trim(Tools::getValue('PS_GAPI30_PROFILE'))); + // This will redirect the user to Google API authentication page + $this->api_3_0_authenticate(); + } + elseif (Tools::getValue('oauth2callback') == 'error') + $html .= $this->displayError('Google API: Access denied'); + elseif (Tools::getValue('oauth2callback') == 'undefined') + $html .= $this->displayError('Something wrong happened with Google API authorization'); + elseif (Tools::getValue('oauth2callback') == 'success') + { + if ($this->api_3_0_refreshtoken()) + $html .= $this->displayConfirmation('Google API Authorization granted'); + else + $html .= $this->displayError('Google API Authorization granted but access token cannot be retrieved'); + } + + if (Configuration::get('PS_GAPI30_CLIENT_ID') && Configuration::get('PS_GAPI30_CLIENT_SECRET') && Configuration::get('PS_GAPI30_PROFILE')) + { + $result_test = $this->api_3_0_requestReportData('', 'ga:visits,ga:uniquePageviews', date('Y-m-d', strtotime('-1 day')), date('Y-m-d', strtotime('-1 day')), null, null, 1, 1); + if (!$result_test) + $html .= $this->displayError('Cannot retrieve test results'); + else + $html .= $this->displayConfirmation(sprintf($this->l('Yesterday, your store received the visit of %d people for a total of %d unique page views.'), $result_test[0]['metrics']['visits'], $result_test[0]['metrics']['uniquePageviews'])); + } + + $helper = new HelperOptions($this); + $helper->id = $this->id; + $helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->module = $this; + + $fields_options = array( + 'general' => array( + 'title' => $this->l('Google Analytics API'), + 'icon' => $this->_path.'logo.png', + 'fields' => $fields = array( + 'PS_GAPI30_CLIENT_ID' => array( + 'title' => $this->l('Client ID'), + 'validation' => 'isEmail', + 'type' => 'text' + ), + 'PS_GAPI30_CLIENT_SECRET' => array( + 'title' => $this->l('Client Secret'), + 'validation' => 'isNothing', + 'type' => 'text' + ), + 'PS_GAPI30_PROFILE' => array( + 'title' => $this->l('Profile'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'desc' => sprintf($this->l('You can find your profile ID in the address bar of your browser while accessing Analytics report: %s.'), ''.$this->l('see screenshot').'') + ) + ), + 'submit' => array('title' => $this->l('Authenticate')), + ) + ); + + return $html.$helper->generateOptions($fields_options); + } + + public function api_3_0_oauth2callback() + { + if (!Tools::getValue('state')) + die ('token missing'); + $state = explode('-', Tools::getValue('state')); + if (count($state) != 2) + die ('token malformed'); + if ($state[1] != Tools::encrypt($state[0].Configuration::get('PS_GAPI30_CLIENT_ID_TMP'))) + die ('token not valid'); + + $oauth2callback = 'undefined'; + $url = Configuration::get('PS_GAPI30_REQUEST_URI_TMP'); + if (Tools::getValue('error')) + $oauth2callback = 'error'; + elseif (Tools::getValue('code')) + { + Configuration::updateValue('PS_GAPI30_CLIENT_ID', Configuration::get('PS_GAPI30_CLIENT_ID_TMP')); + Configuration::updateValue('PS_GAPI30_CLIENT_SECRET', Configuration::get('PS_GAPI30_CLIENT_SECRET_TMP')); + Configuration::updateValue('PS_GAPI30_PROFILE', Configuration::get('PS_GAPI30_PROFILE_TMP')); + Configuration::updateValue('PS_GAPI30_AUTHORIZATION_CODE', Tools::getValue('code')); + $oauth2callback = 'success'; + } + + Configuration::deleteByName('PS_GAPI30_CLIENT_ID_TMP'); + Configuration::deleteByName('PS_GAPI30_CLIENT_SECRET_TMP'); + Configuration::deleteByName('PS_GAPI30_PROFILE_TMP'); + Configuration::deleteByName('PS_GAPI30_REQUEST_URI_TMP'); + Configuration::deleteByName('PS_GAPI30_REFRESH_TOKEN'); + + Tools::redirectAdmin($url.'&oauth2callback='.$oauth2callback); + } + + // https://developers.google.com/analytics/devguides/reporting/core/dimsmets + // requestReportData('ga:country', 'ga:visits', '2013-08-25', '2013-08-25', null, null, 1, 1000); + protected function api_3_0_requestReportData($dimensions, $metrics, $date_from, $date_to, $sort = null, $filters = null, $start = 1, $limit = 30) + { + if (Configuration::get('PS_GAPI30_TOKEN_EXPIRATION') < time() + 30 && !$this->api_3_0_refreshtoken()) + return false; + $bearer = Configuration::get('PS_GAPI30_ACCESS_TOKEN'); + + $params = array( + 'ids' => 'ga:'.Configuration::get('PS_GAPI30_PROFILE'), + 'dimensions' => $dimensions, + 'metrics' => $metrics, + 'sort' => $sort ? $sort : $metrics, + 'start-date' => $date_from, + 'end-date' => $date_to, + 'start-index' => $start, + 'max-results' => $limit, + ); + if ($filters !== null) + $params['filters'] = $filters; + $content = str_replace('&', '&', urldecode(http_build_query($params))); + + $stream_context = stream_context_create(array( + 'http' => array( + 'method'=> 'GET', + 'header' => 'Authorization: Bearer '.$bearer."\r\n", + 'timeout' => 5, + ) + )); + if (!$response_json = Tools::file_get_contents('https://www.googleapis.com/analytics/v3/data/ga?'.$content, false, $stream_context)) + return false; + + // https://developers.google.com/analytics/devguides/reporting/core/v3/reference + $response = Tools::jsonDecode($response_json, true); + + $result = array(); + foreach ($response['rows'] as $row) + { + $metrics = array(); + $dimensions = array(); + foreach($row as $key => $value) + if ($response['columnHeaders'][$key]['columnType'] == 'DIMENSION') + $dimensions[str_replace('ga:', '', $response['columnHeaders'][$key]['name'])] = $value; + elseif ($response['columnHeaders'][$key]['columnType'] == 'METRIC') + $metrics[str_replace('ga:', '', $response['columnHeaders'][$key]['name'])] = $value; + $result[] = array('metrics' => $metrics, 'dimensions' => $dimensions); + } + return $result; + } + + public function api_1_3_getContent() + { + $html = ''; + if (Tools::isSubmit('PS_GAPI13_EMAIL')) + { + if ($this->api_1_3_authenticate(Tools::getValue('PS_GAPI13_EMAIL'), Tools::getValue('PS_GAPI13_PASSWORD'))) + { + Configuration::updateValue('PS_GAPI13_EMAIL', Tools::getValue('PS_GAPI13_EMAIL')); + Configuration::updateValue('PS_GAPI13_PASSWORD', Tools::getValue('PS_GAPI13_PASSWORD')); + Configuration::updateValue('PS_GAPI13_PROFILE', Tools::getValue('PS_GAPI13_PROFILE')); + } + else + $html .= $this->displayError($this->l('Authentication failed')); + } + + if (Configuration::get('PS_GAPI13_EMAIL') && Configuration::get('PS_GAPI13_PASSWORD') && Configuration::get('PS_GAPI13_PROFILE')) + { + $result_test = $this->api_3_0_requestReportData('', 'ga:visits,ga:uniquePageviews', date('Y-m-d', strtotime('-1 day')), date('Y-m-d', strtotime('-1 day')), null, null, 1, 1); + if (!$result_test) + $html .= $this->displayError('Cannot retrieve test results'); + else + $html .= $this->displayConfirmation(sprintf($this->l('Yesterday, your store received the visit of %d people for a total of %d unique page views.'), $result_test[0]['metrics']['visits'], $result_test[0]['metrics']['uniquePageviews'])); + } + + $helper = new HelperOptions($this); + $helper->id = $this->id; + $helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->module = $this; + + $fields_options = array( + 'general' => array( + 'title' => $this->l('Google Analytics API'), + 'icon' => $this->_path.'logo.png', + 'fields' => $fields = array( + 'PS_GAPI13_EMAIL' => array( + 'title' => $this->l('Email'), + 'validation' => 'isEmail', + 'type' => 'text' + ), + 'PS_GAPI13_PASSWORD' => array( + 'title' => $this->l('Password'), + 'validation' => 'isNothing', + 'type' => 'password' + ), + 'PS_GAPI13_PROFILE' => array( + 'title' => $this->l('Profile'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'desc' => sprintf($this->l('You can find your profile ID in the address bar of your browser while accessing Analytics report: %s.'), ''.$this->l('see screenshot').'') + ) + ), + 'submit' => array('title' => $this->l('Authenticate'), 'class' => 'button'), + ) + ); + + return $html.$helper->generateOptions($fields_options); + } + + protected function api_1_3_authenticate($email, $password) + { + $stream_context = stream_context_create(array( + 'http' => array( + 'method'=> 'POST', + 'content' => 'accountType=GOOGLE&Email='.urlencode($email).'&Passwd='.urlencode($password).'&source=GAPI-1.3&service=analytics', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'timeout' => 5, + ) + )); + + if (!$response = Tools::file_get_contents('https://www.google.com/accounts/ClientLogin', false, $stream_context)) + return false; + + parse_str(str_replace(array("\n", "\r\n"), '&', $response), $response_array); + if (!is_array($response_array) || !isset($response_array['Auth']) || empty($response_array['Auth'])) + return false; + + $this->auth_token = $response_array['Auth']; + return true; + } + + // requestReportData('ga:country', 'ga:visits', '2013-08-25', '2013-08-25', null, null, 1, 1000); + protected function api_1_3_requestReportData($dimensions, $metrics, $date_from, $date_to, $sort = null, $filters = null, $start = 1, $limit = 30) + { + if (!$this->api_1_3_authenticate(Configuration::get('PS_GAPI13_EMAIL'), Configuration::get('PS_GAPI13_PASSWORD'))) + return false; + + $params = array( + 'ids' => 'ga:'.Configuration::get('PS_GAPI13_PROFILE'), + 'dimensions' => $dimensions, + 'metrics' => $metrics, + 'sort' => $sort ? $sort : $metrics, + 'start-date' => $date_from, + 'end-date' => $date_to, + 'start-index' => $start, + 'max-results' => $limit, + ); + if ($filters !== null) + $params['filters'] = $filters; + $content = str_replace('&', '&', urldecode(http_build_query($params))); + + $stream_context = stream_context_create(array( + 'http' => array( + 'method'=> 'GET', + 'header' => "Authorization: GoogleLogin auth=".$this->auth_token."\r\n", + 'timeout' => 5, + ) + )); + if (!$response = Tools::file_get_contents('https://www.google.com/analytics/feeds/data?'.$content, false, $stream_context)) + return false; + + $xml = simplexml_load_string($response); + + /* Meta data not useful at this time */ + /* + $report_root_parameters = array(); + $report_aggregate_metrics = array(); + $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); + foreach($google_results->aggregates->metric as $aggregate_metric) + { + $key = str_replace('ga:', '', $aggregate_metric->attributes()->name); + $metric_value = strval($aggregate_metric->attributes()->value); + if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value)) + $report_aggregate_metrics[$key] = floatval($metric_value); + else + $report_aggregate_metrics[$key] = intval($metric_value); + } + */ + + $result = array(); + foreach($xml->entry as $entry) + { + $metrics = array(); + foreach ($entry->children('http://schemas.google.com/analytics/2009')->metric as $metric) + { + $key = str_replace('ga:','',$metric->attributes()->name); + $metric_value = strval($metric->attributes()->value); + if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/',$metric_value)) + $metrics[$key] = floatval($metric_value); + else + $metrics[$key] = intval($metric_value); + } + + $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); + + $result[] = array('metrics' => $metrics, 'dimensions' => $dimensions); + } + + return $result; + } +} \ No newline at end of file diff --git a/modules/gapi/index.php b/modules/gapi/index.php new file mode 100644 index 000000000..05972f21b --- /dev/null +++ b/modules/gapi/index.php @@ -0,0 +1,33 @@ + +* @copyright 2007-2013 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); +header("Location: ../"); +exit; \ No newline at end of file diff --git a/modules/gapi/logo.gif b/modules/gapi/logo.gif new file mode 100644 index 000000000..73538897c Binary files /dev/null and b/modules/gapi/logo.gif differ diff --git a/modules/gapi/logo.png b/modules/gapi/logo.png new file mode 100644 index 000000000..5e6d77799 Binary files /dev/null and b/modules/gapi/logo.png differ diff --git a/modules/gapi/oauth2callback.php b/modules/gapi/oauth2callback.php new file mode 100644 index 000000000..397759e6e --- /dev/null +++ b/modules/gapi/oauth2callback.php @@ -0,0 +1,7 @@ +api_3_0_oauth2callback(); diff --git a/modules/gapi/screenshots/Google API - 01 - Start.png b/modules/gapi/screenshots/Google API - 01 - Start.png new file mode 100644 index 000000000..012d078c7 Binary files /dev/null and b/modules/gapi/screenshots/Google API - 01 - Start.png differ diff --git a/modules/gapi/screenshots/Google API - 02 - Services.png b/modules/gapi/screenshots/Google API - 02 - Services.png new file mode 100644 index 000000000..5b45d4494 Binary files /dev/null and b/modules/gapi/screenshots/Google API - 02 - Services.png differ diff --git a/modules/gapi/screenshots/Google API - 03 - Terms.png b/modules/gapi/screenshots/Google API - 03 - Terms.png new file mode 100644 index 000000000..a6c889e4f Binary files /dev/null and b/modules/gapi/screenshots/Google API - 03 - Terms.png differ diff --git a/modules/gapi/screenshots/Google API - 04 - Terms.png b/modules/gapi/screenshots/Google API - 04 - Terms.png new file mode 100644 index 000000000..c2a9c4582 Binary files /dev/null and b/modules/gapi/screenshots/Google API - 04 - Terms.png differ diff --git a/modules/gapi/screenshots/Google API - 05 - Services OK.png b/modules/gapi/screenshots/Google API - 05 - Services OK.png new file mode 100644 index 000000000..158634aba Binary files /dev/null and b/modules/gapi/screenshots/Google API - 05 - Services OK.png differ diff --git a/modules/gapi/screenshots/Google API - 06 - API Access.png b/modules/gapi/screenshots/Google API - 06 - API Access.png new file mode 100644 index 000000000..f7a8235ea Binary files /dev/null and b/modules/gapi/screenshots/Google API - 06 - API Access.png differ diff --git a/modules/gapi/screenshots/Google API - 07 - Create Client ID.png b/modules/gapi/screenshots/Google API - 07 - Create Client ID.png new file mode 100644 index 000000000..1936f57cc Binary files /dev/null and b/modules/gapi/screenshots/Google API - 07 - Create Client ID.png differ diff --git a/modules/gapi/screenshots/Google API - 08 - Create Client ID.png b/modules/gapi/screenshots/Google API - 08 - Create Client ID.png new file mode 100644 index 000000000..d263b30ba Binary files /dev/null and b/modules/gapi/screenshots/Google API - 08 - Create Client ID.png differ diff --git a/modules/gapi/screenshots/Google API - 09 - API Access created.png b/modules/gapi/screenshots/Google API - 09 - API Access created.png new file mode 100644 index 000000000..570f7cacb Binary files /dev/null and b/modules/gapi/screenshots/Google API - 09 - API Access created.png differ diff --git a/modules/gapi/screenshots/Google API - 10 - Profile ID.png b/modules/gapi/screenshots/Google API - 10 - Profile ID.png new file mode 100644 index 000000000..baa3d9e28 Binary files /dev/null and b/modules/gapi/screenshots/Google API - 10 - Profile ID.png differ