ga_reports-8.x-1.0/ga_reports.module
ga_reports.module
<?php
/**
* @file
* Drupal Module: Google Analytics Reports Module.
*/
use Drupal\Component\Serialization\Json;
use Drupal\ga_reports\GaReportsApiFeed;
use Drupal\Core\Cache\CacheableRedirectResponse;
use Drupal\Core\Url;
use Drupal\ga_reports\Component\Render\GoogleAnalyticsJavaScriptSnippet;
/**
* @file
* Front-end interfaces that use the Google Analytics Reports API module.
*/
/**
* Implements hook_ga_reports_field_import_alter().
*/
function ga_reports_field_import_alter(&$field) {
// Change data type for Date field.
if ($field['id'] == 'date') {
$field['attributes']['dataType'] = 'date';
}
}
/**
* Implements hook_ga_reports_reported_data_alter().
*/
function ga_reports_reported_data_alter(&$name, &$value) {
// Get all Google Analytics fields.
$fields = ga_reports_get_fields();
// Date and time datatypes should not have the digits after the zero.
if ((isset($fields[$name])) && (in_array($fields[$name]->data_type, ['date', 'time']))) {
$value = round($value);
}
switch ($name) {
case 'userType':
$value = ($value == 'New Visitor') ? t('New Visitor') : t('Returning Visitor');
break;
case 'date':
$value = strtotime($value);
break;
case 'yearMonth':
$value = strtotime($value . '01');
break;
case 'userGender':
$value = ($value == 'male') ? t('Male') : t('Female');
break;
}
}
/**
* List of Google Analytics dimensions and metrics.
*
* @return array
* An associative array containing list of Google Analytics column objects.
* Each object is associative array containing:
* - gid: The primary identifier for a column.
* - type: The type of column.
* - data_type: The type of data this column represents.
* - column_group: The dimensions/metrics group the column belongs to.
* - ui_name: The name/label of the column used in user interfaces (UI).
* - description: The full description of the column.
* - calculation: This shows how the metric is calculated.
*/
function ga_reports_get_fields() {
$fields = &drupal_static(__FUNCTION__);
// todo: fetch data from cache.
if (!isset($fields)) {
$fields = \Drupal::database()->select('ga_reports_fields', 'g')
->fields('g')
->execute()
->fetchAllAssoc('gaid');
}
return $fields;
}
/**
* Determines if a field is custom or not.
*/
function ga_reports_is_custom($field) {
return preg_match('/XX/', $field) ? TRUE : FALSE;
}
/**
* Converts a base custom field name and number into a specific field name.
*/
function ga_reports_custom_to_variable_field($field, $number) {
return preg_replace('/XX/', $number, $field);
}
/**
* Converts a specific field name into a base custom field name.
*/
function ga_reports_variable_to_custom_field($field) {
return preg_replace('/\d+/', 'XX', $field);
}
/**
* Instantiate a new GaReportsApiFeed object.
*
* @return object
* GaReportsApiFeed object to authorize access and request data
* from the Google Analytics Core Reporting API.
*/
function ga_reports_gafeed() {
$config = \Drupal::configFactory()->getEditable('ga_reports_api.settings');
// If the access token is still valid, return an authenticated
// GaReportsApiFeed.
$access_token = $config->get('access_token');
if ($access_token && time() < $config->get('expires_at')) {
return new GaReportsApiFeed($access_token);
}
else {
// If the site has an access token and refresh token, but the access
// token has expired, authenticate the user with the refresh token.
$refresh_token = $config->get('refresh_token');
if ($refresh_token) {
try {
$ga_reports_feed = new GaReportsApiFeed();
$ga_reports_feed->refreshToken($config->get('client_id'), $config->get('client_secret'), $refresh_token);
$config
->set('access_token', $ga_reports_feed->accessToken)
->set('expires_at', $ga_reports_feed->expiresAt)
->save();
return $ga_reports_feed;
}
catch (\Exception $e) {
drupal_set_message(t('There was an authentication error. Message: @message.', ['@message' => $e->getMessage()]), 'error', FALSE);
\Drupal::logger('ga_reports_api')->error('There was an authentication error. Message: @message.', ['@message' => $e->getMessage()]);
return NULL;
}
}
else {
// If there is no access token or refresh token and client is returned
// to the config page with an access code, complete the authentication.
if (isset($_GET['code'])) {
try {
$ga_reports_feed = new GaReportsApiFeed();
$redirect_uri = $config->get('redirect_uri');
$ga_reports_feed->finishAuthentication($config->get('client_id'), $config->get('client_secret'), $redirect_uri);
$config
->set('access_token', $ga_reports_feed->accessToken)
->set('expires_at', $ga_reports_feed->expiresAt)
->set('refresh_token', $ga_reports_feed->refreshToken)
->clear('redirect_uri')
->save();
drupal_set_message(t('You have been successfully authenticated.'));
$response = new CacheableRedirectResponse(Url::fromUri($redirect_uri)->toString());
$response->send();
}
catch (Exception $e) {
drupal_set_message(t('There was an authentication error. Message: @message.', ['@message' => $e->getMessage()]), 'error', FALSE);
\Drupal::logger('ga_reports_api')->error('There was an authentication error. Message: @message.', ['@message' => $e->getMessage()]);
return NULL;
}
}
}
}
}
/**
* Request report data.
*
* @array $params
* An associative array containing:
* - profile_id: required
* [default=variable_get('ga_reports_profile_id')].
* - metrics: required.
* - dimensions: optional [default=none].
* - sort_metric: optional [default=none].
* - filters: optional [default=none].
* - segment: optional [default=none].
* - start_date: optional [default=2005-01-01].
* - end_date: optional [default=today].
* - start_index: optional [default=1].
* - max_results: optional [default=10,000].
* @array $cache_options
* An optional associative array containing:
* - cid: optional [default=md5 hash].
* - expire: optional [default=CACHE_TEMPORARY].
* - refresh: optional [default=FALSE].
*
* @return object
* GaReportsApiFeed object to authorize access and request data
* from the Google Analytics Core Reporting API after reporting data.
*/
function ga_reports_report_data($params = [], $cache_options = []) {
$config = \Drupal::config('ga_reports_api.settings');
if (isset($params['profile_id'])) {
$params['profile_id'] = 'ga:' . $params['profile_id'];
}
else {
$params['profile_id'] = 'ga:' . $config->get('profile_id');
}
$ga_feed = ga_reports_gafeed();
if ($ga_feed) {
$ga_feed->queryReportFeed($params, $cache_options);
return $ga_feed;
}
else {
drupal_set_message(t('There was an authentication error. Please check your Google Analytics API settings and try again.'), 'error', FALSE);
\Drupal::logger('ga_reports_api')->error('There was an authentication error. Please check your Google Analytics API settings and try again.');
return ['error' => TRUE];
}
}
/**
* Programmatically revoke token.
*/
function ga_reports_revoke() {
$ga_feed = ga_reports_gafeed();
$ga_feed->revokeToken();
$config = \Drupal::configFactory()->getEditable('ga_reports_api.settings');
// Delete module variables.
$config
->clear('access_token')
->clear('client_id')
->clear('client_secret')
->clear('default_page')
->clear('expires_at')
->clear('profile_id')
->clear('redirect_uri')
->clear('refresh_token')
->save();
}
/**
* Sets the expiry timestamp for cached queries.
*
* Default is 3 days.
*
* @return int
* The UNIX timestamp to expire the query at.
*/
function ga_reports_cache_time() {
return time() + \Drupal::config('ga_reports_api.settings')->get('cache_length');
}
/**
* Google Analytics reports profiles for current authorized user.
*
* @return arraynull
* An associative array containing:
* - options: list of current available profiles.
* - profile_id: current default profile id.
* - current_profile: current default profile object.
*/
function ga_reports_profiles_list() {
$config = \Drupal::configFactory()->getEditable('ga_reports_api.settings');
$account = ga_reports_gafeed();
if (($account) && ($account->isAuthenticated())) {
$web_properties = NULL;
$web_properties_obj = $account->queryWebProperties();
if (isset($web_properties_obj->results->items)) {
$web_properties = $web_properties_obj->results->items;
}
$profiles_obj = $account->queryProfiles();
$profiles = [];
if (isset($profiles_obj->results->items)) {
$profiles = $profiles_obj->results->items;
}
$options = [];
$profile_id = $config->get('profile_id');
$config_ga = \Drupal::config('ga.settings');
$ga_account = $config_ga->get('account') ? $config_ga->get('account') : NULL;
$set_default = FALSE;
// Add optgroups for each web property.
if (!empty($profiles)) {
foreach ($profiles as $profile) {
$web_property = NULL;
foreach ($web_properties as $web_property_value) {
if ($web_property_value->id == $profile->webPropertyId) {
$web_property = $web_property_value;
break;
}
}
$options[$web_property->name][$profile->id] = $profile->name . ' (' . $profile->id . ')';
// Find current site in the account list.
if (empty($profile_id)) {
// If Google Analytics module is enabled check it first.
if (isset($ga_account) && ($ga_account == $profile->webPropertyId)) {
$profile_id = $profile->id;
$set_default = TRUE;
}
// Rough attempt to see if the current site is in the account list.
elseif (parse_url($web_property->websiteUrl, PHP_URL_HOST) == $_SERVER['HTTP_HOST']) {
$profile_id = $profile->id;
$set_default = TRUE;
}
}
}
}
// If no profile ID is set yet, set the first profile in the list.
if (empty($profile_id)) {
if (count($options)) {
$profile_id = key($options[key($options)]);
$set_default = TRUE;
}
}
if ($set_default) {
$config
->set('profile_id', $profile_id)
->save();
}
$current_profile = NULL;
// Load current profile object.
foreach ($profiles as $profile) {
if ($profile->id == $profile_id) {
$current_profile = $profile;
$config
->set('default_page', isset($current_profile->defaultPage) ? '/' . $current_profile->defaultPage : '/')
->save();
break;
}
}
$return = [
'options' => $options,
'profile_id' => $profile_id,
'current_profile' => $current_profile,
];
return $return;
}
}
/**
* Implements hook_page_attachments().
*
* Insert JavaScript to the appropriate scope/region of the page.
*/
function ga_reports_page_attachments(array &$page) {
// Get page http status code for visibility filtering.
$config = \Drupal::config('ga_reports_api.settings');
$id = $config->get('account');
// 1. Check if the GA account number has a valid value.
// 2. Track page views based on visibility value.
// 3. Check if we should track the currently active user's role.
// 4. Ignore pages visibility filter for 404 or 403 status codes.
if (preg_match('/^UA-\d+-\d+$/', $id)) {
// Add messages tracking.
$message_events = '';
if ($message_types = $config->get('track.messages')) {
$message_types = array_values(array_filter($message_types));
$status_heading = [
'status' => t('Status message'),
'warning' => t('Warning message'),
'error' => t('Error message'),
];
foreach (drupal_get_messages(NULL, FALSE) as $type => $messages) {
// Track only the selected message types.
if (in_array($type, $message_types)) {
foreach ($messages as $message) {
// @todo: Track as exceptions?
$message_events .= 'ga("send", "event", ' . Json::encode(t('Messages')) . ', ' . Json::encode($status_heading[$type]) . ', ' . Json::encode(strip_tags($message)) . ');';
}
}
}
}
// Build tracker code.
$script = '(function(i,s,o,g,r,a,m){';
$script .= 'i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){';
$script .= '(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),';
$script .= 'm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)';
$script .= '})(window,document,"script",';
$debug = '';
// Which version of the tracking library should be used?
$library_tracker_url = 'https://www.google-analytics.com/' . ($debug ? 'analytics_debug.js' : 'analytics.js');
// Should a local cached copy of analytics.js be used?
if ($config->get('cache') && $url = _ga_reports_cache($library_tracker_url)) {
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed.
$query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
$script .= '"' . $url . $query_string . '"';
}
else {
$script .= '"' . $library_tracker_url . '"';
}
$script .= ',"ga");';
// Build the create only fields list.
$create_only_fields = ['cookieDomain' => 'auto'];
// Domain tracking type.
global $cookie_domain;
$domain_mode = $config->get('domain_mode');
$googleanalytics_adsense_script = '';
// Per RFC 2109, cookie domains must contain at least one dot other than the
// first. For hosts such as 'localhost' or IP Addresses we don't set a
// cookie domain.
if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
$create_only_fields = array_merge($create_only_fields, ['cookieDomain' => $cookie_domain]);
$googleanalytics_adsense_script .= 'window.google_analytics_domain_name = ' . Json::encode($cookie_domain) . ';';
}
elseif ($domain_mode == 2) {
// Cross Domain tracking. 'autoLinker' need to be enabled in 'create'.
$create_only_fields = array_merge($create_only_fields, ['allowLinker' => TRUE]);
$googleanalytics_adsense_script .= 'window.google_analytics_domain_name = "none";';
}
// Create a tracker.
$script .= 'ga("create", ' . Json::encode($id) . ', ' . Json::encode($create_only_fields) . ');';
// Prepare Adsense tracking.
$googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . Json::encode($id) . ';';
$page['#attached']['html_head'][] = [
[
'#tag' => 'script',
'#value' => new GoogleAnalyticsJavaScriptSnippet($script),
],
'google_analytics_tracking_script',
];
}
}
/**
* Download/Synchronize/Cache tracking code file locally.
*
* @string $location
* The full URL to the external javascript file.
* @bool $synchronize
* Synchronize to local cache if remote file has changed.
*
* @return mixed
* The path to the local javascript file on success, boolean FALSE on failure.
*/
function _ga_reports_cache($location, $synchronize = FALSE) {
$path = 'public://google_analytics';
$file_destination = $path . '/' . basename($location);
if (!file_exists($file_destination) || $synchronize) {
// Download the latest tracking code.
try {
$data = (string) \Drupal::httpClient()
->get($location)
->getBody();
if (file_exists($file_destination)) {
// Synchronize tracking code and and replace local file if outdated.
$data_hash_local = Crypt::hashBase64(file_get_contents($file_destination));
$data_hash_remote = Crypt::hashBase64($data);
// Check that the files directory is writable.
if ($data_hash_local != $data_hash_remote && file_prepare_directory($path)) {
// Save updated tracking code file to disk.
file_unmanaged_save_data($data, $file_destination, FILE_EXISTS_REPLACE);
// Based on Drupal Core class AssetDumper.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
}
\Drupal::logger('google_analytics')->info('Locally cached tracking code file has been updated.');
// Change query-strings on css/js files to enforce reload for all
// users.
_drupal_flush_css_js();
}
}
else {
// Check that the files directory is writable.
if (file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
// There is no need to flush JS here as core refreshes JS caches
// automatically, if new files are added.
file_unmanaged_save_data($data, $file_destination, FILE_EXISTS_REPLACE);
// Based on Drupal Core class AssetDumper.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
}
\Drupal::logger('google_analytics')->info('Locally cached tracking code file has been saved.');
// Return the local JS file path.
return file_url_transform_relative(file_create_url($file_destination));
}
}
}
catch (RequestException $exception) {
watchdog_exception('google_analytics', $exception);
}
}
else {
// Return the local JS file path.
return file_url_transform_relative(file_create_url($file_destination));
}
}
