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)); } }