PluginProbe
Analytify – Google Analytics Dashboard For WordPress (GA4 analytics tracking) / trunk
Analytify – Google Analytics Dashboard For WordPress (GA4 analytics tracking) vtrunk
9.1.2 9.1.1 9.1.0 9.0.2 9.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 All 153 releases
← All changes | analytify-general.php +1088 -190 1.0.3trunk View file →
@@ -1,190 +1,1088 @@
1 -<?php
2 -
3 -/**
4 - * Base Class to use for the Add-ons
5 - * It will be used to extend the functionality of Analytify WordPress Plugin.
6 - */
7 -
8 - // Setting Global Values
9 - define( 'ANALYTIFY_LIB_PATH', dirname(__FILE__) . '/lib/' );
10 - define( 'ANALYTIFY_ID', 'wp-analytify-options' );
11 - define( 'ANALYTIFY_NICK', 'Analytify' );
12 - define( 'ANALYTIFY_ROOT_PATH', dirname(__FILE__) );
13 - define( 'ANALYTIFY_VERSION', '1.0.1');
14 - define( 'ANALYTIFY_TYPE', 'FREE');
15 - define( 'ANALYTIFY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
16 -
17 - define( 'ANALYTIFY_REDIRECT', 'urn:ietf:wg:oauth:2.0:oob' ); // This will redirect to window where we can copy Access code.
18 - define( 'ANALYTIFY_SCOPE', 'https://www.googleapis.com/auth/analytics' ); // readonly scope
19 -
20 - define( 'ANALYTIFY_STORE_URL', 'http://wp-analytify.com' );
21 - define( 'ANALYTIFY_PRODUCT_NAME', 'Analytify WordPress Plugin' );
22 -
23 -if (! class_exists( 'Analytify_General_FREE' ) ) {
24 -
25 - class Analytify_General_FREE {
26 -
27 - function __construct() {
28 -
29 - if (! class_exists('Analytify_Google_Client') ) {
30 -
31 - require_once ANALYTIFY_LIB_PATH . 'Google/Client.php';
32 - require_once ANALYTIFY_LIB_PATH . 'Google/Service/Analytics.php';
33 -
34 - }
35 -
36 - $this->client = new Analytify_Google_Client();
37 - $this->client->setApprovalPrompt( 'force' );
38 - $this->client->setAccessType( 'offline' );
39 - $this->client->setClientId( get_option('ANALYTIFY_CLIENTID'));
40 - $this->client->setClientSecret( get_option('ANALYTIFY_CLIENTSECRET') );
41 - $this->client->setRedirectUri( ANALYTIFY_REDIRECT );
42 - $this->client->setScopes( ANALYTIFY_SCOPE );
43 - $this->client->setDeveloperKey( get_option('ANALYTIFY_DEV_KEY') );
44 -
45 - try{
46 -
47 - $this->service = new Analytify_Google_Service_Analytics( $this->client );
48 -
49 - $this->pa_connect();
50 -
51 - }
52 - catch ( Analytify_Google_Service_Exception $e ) {
53 -
54 - }
55 -
56 - }
57 -
58 - /**
59 - * Connect with Google Analytics API and get authentication token and save it.
60 - */
61 -
62 - public function pa_connect() {
63 -
64 - $ga_google_authtoken = get_option('pa_google_token');
65 -
66 - if (! empty( $ga_google_authtoken )) {
67 -
68 - $this->client->setAccessToken( $ga_google_authtoken );
69 - }
70 - else{
71 -
72 - $authCode = get_option( 'post_analytics_token' );
73 -
74 - if ( empty( $authCode ) ) return false;
75 -
76 - try {
77 -
78 - $accessToken = $this->client->authenticate( $authCode );
79 - }
80 - catch ( Exception $e ) {
81 - return false;
82 - }
83 -
84 - if ( $accessToken ) {
85 -
86 - $this->client->setAccessToken( $accessToken );
87 -
88 - update_option( 'pa_google_token', $accessToken );
89 -
90 - return true;
91 - } //$accessToken
92 - else {
93 -
94 - return false;
95 - }
96 - }
97 -
98 - $this->token = json_decode($this->client->getAccessToken());
99 -
100 - return true;
101 - }
102 -
103 - /*
104 - * This function grabs the data from Google Analytics
105 - * For individual posts/pages.
106 - */
107 - public function pa_get_analytics( $metrics, $startDate, $endDate, $dimensions = false, $sort = false, $filter = false, $limit = false ) {
108 -
109 - try{
110 -
111 - $this->service = new Analytify_Google_Service_Analytics($this->client);
112 - $params = array();
113 -
114 - if ($dimensions){
115 - $params['dimensions'] = $dimensions;
116 - } //$dimensions
117 -
118 - if ($sort){
119 - $params['sort'] = $sort;
120 - } //$sort
121 -
122 - if ($filter){
123 - $params['filters'] = $filter;
124 - } //$filter
125 -
126 - if ($limit){
127 - $params['max-results'] = $limit;
128 - } //$limit
129 -
130 - $profile_id = get_option("pt_webprofile");
131 -
132 - if (!$profile_id){
133 - return false;
134 - }
135 -
136 - return $this->service->data_ga->get('ga:' . $profile_id, $startDate, $endDate, $metrics, $params);
137 - }
138 -
139 - catch ( Analytify_Google_Service_Exception $e ) {
140 -
141 - // Show error message only for logged in users.
142 - if ( is_user_logged_in() ) echo $e->getMessage();
143 -
144 - }
145 - }
146 -
147 - /*
148 - * This function grabs the data from Google Analytics
149 - * For dashboard.
150 - */
151 - public function pa_get_analytics_dashboard( $metrics, $startDate, $endDate, $dimensions = false, $sort = false, $filter = false, $limit = false ) {
152 -
153 - try{
154 -
155 - $this->service = new Analytify_Google_Service_Analytics( $this->client );
156 - $params = array();
157 -
158 - if ($dimensions){
159 - $params['dimensions'] = $dimensions;
160 - }
161 - if ($sort){
162 - $params['sort'] = $sort;
163 - }
164 - if ($filter){
165 - $params['filters'] = $filter;
166 - }
167 - if ($limit){
168 - $params['max-results'] = $limit;
169 - }
170 -
171 - $profile_id = get_option("pt_webprofile_dashboard");
172 - if (!$profile_id){
173 - return false;
174 - }
175 -
176 - return $this->service->data_ga->get('ga:' . $profile_id, $startDate, $endDate, $metrics, $params);
177 -
178 - }
179 -
180 - catch ( Analytify_Google_Service_Exception $e ) {
181 -
182 - // Show error message only for logged in users.
183 - if ( is_user_logged_in() ) echo $e->getMessage();
184 -
185 - }
186 - }
187 -
188 - }
189 -}
190 -?>
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +/**
3 + * Analytify General Class
4 + *
5 + * This is the core class that sets the foundation for the Analytify plugin.
6 + * It handles analytics wrappers, SDK calls to fetch data from Google Analytics,
7 + * and provides the base functionality for all other plugin components.
8 + *
9 + * @package WP_Analytify
10 + * @since 1.0.0
11 + * @version 8.0.0
12 + *
13 + * @author Analytify Team
14 + * @license GPL-2.0+
15 + *
16 + * @see https://analytify.io/
17 + * @see https://wordpress.org/plugins/wp-analytify/
18 + */
19 +
20 +
21 + // Include core classes first.
22 + require_once __DIR__ . '/classes/analytify-utils.php';
23 + require_once __DIR__ . '/classes/analytify-settings.php';
24 + require_once __DIR__ . '/classes/analytify-mp-ga4.php';
25 +
26 + // Include all trait files.
27 + require_once __DIR__ . '/inc/analytify-authentication.php';
28 + require_once __DIR__ . '/inc/analytify-ga4-core.php';
29 + require_once __DIR__ . '/inc/analytify-utilities.php';
30 + require_once __DIR__ . '/inc/analytify-navigation.php';
31 +
32 +if ( ! class_exists( 'Analytify_General' ) ) {
33 +
34 + /**
35 + * Analytify_General Class for Analytify.
36 + */
37 + class Analytify_General {
38 +
39 + // Use all the traits.
40 + use Analytify_Authentication;
41 + use Analytify_GA4_Core;
42 + use Analytify_General_Utilities;
43 + use Analytify_Navigation;
44 +
45 + /**
46 + * Plugin settings object.
47 + *
48 + * @var object
49 + */
50 + public $settings;
51 +
52 + /**
53 + * Google Analytics service object.
54 + *
55 + * @var object
56 + */
57 + public $service;
58 +
59 + /**
60 + * Google Analytics client object.
61 + *
62 + * @var object
63 + */
64 + public $client;
65 +
66 + /**
67 + * Authentication token.
68 + *
69 + * @var string
70 + */
71 + public $token;
72 +
73 + /**
74 + * State data for authentication.
75 + *
76 + * @var array
77 + */
78 + protected $state_data;
79 +
80 + /**
81 + * Transient timeout duration.
82 + *
83 + * @var int
84 + */
85 + protected $transient_timeout;
86 +
87 + /**
88 + * Load settings flag.
89 + *
90 + * @var bool
91 + */
92 + protected $load_settings;
93 +
94 + /**
95 + * Plugin base URL.
96 + *
97 + * @var string
98 + */
99 + protected $plugin_base;
100 +
101 + /**
102 + * Plugin settings base URL.
103 + *
104 + * @var string
105 + */
106 + protected $plugin_settings_base;
107 +
108 + /**
109 + * Cache timeout duration.
110 + *
111 + * @var int
112 + */
113 + protected $cache_timeout;
114 +
115 + /**
116 + * Exception data.
117 + *
118 + * @var mixed
119 + */
120 + private $exception;
121 +
122 + /**
123 + * GA4 exception data.
124 + *
125 + * @var mixed
126 + */
127 + private $ga4_exception;
128 +
129 + /**
130 + * Available modules.
131 + *
132 + * @var array
133 + */
134 + private $modules;
135 +
136 + /**
137 + * GA4 reporting flag.
138 + *
139 + * @var bool
140 + */
141 + protected $is_reporting_in_ga4;
142 +
143 + /**
144 + * User added client ID.
145 + *
146 + * @var string
147 + */
148 + private $user_client_id;
149 +
150 + /**
151 + * User added client secret.
152 + *
153 + * @var string
154 + */
155 + private $user_client_secret;
156 +
157 + /**
158 + * Authentication date format.
159 + *
160 + * @var string
161 + */
162 + protected $auth_date_format;
163 +
164 + /**
165 + * Google token data.
166 + *
167 + * @var array|false
168 + */
169 + protected $google_token;
170 +
171 + /**
172 + * GA4 streams data.
173 + *
174 + * @var array
175 + */
176 + protected $ga4_streams;
177 +
178 + /**
179 + * Constructor of analytify-general class.
180 + *
181 + * Initializes the core plugin settings, authentication data, and prepares
182 + * the environment for Google Analytics operations.
183 + *
184 + * @since 1.0.0
185 + */
186 + public function __construct() {
187 + // Set cache timeout to 12 hours (60 * 60 * 12 seconds).
188 + $this->transient_timeout = 60 * 60 * 12;
189 +
190 + // Define admin page URLs for navigation.
191 + $this->plugin_base = 'admin.php?page=analytify-dashboard';
192 + $this->plugin_settings_base = 'admin.php?page=analytify-settings';
193 +
194 + // Set authentication date format with timezone.
195 + $this->auth_date_format = gmdate( 'l jS F Y h:i:s A' ) . ' ' . date_default_timezone_get();
196 + // Sanitize page parameter for security.
197 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading URL parameter for display purposes
198 + $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
199 + if ( $current_page && strpos( $current_page, 'analytify-settings' ) === 0 ) {
200 + $this->exception = get_option( 'analytify_profile_exception' );
201 + $this->ga4_exception = get_option( 'analytify_ga4_exceptions' );
202 + }
203 + $this->modules = WPANALYTIFY_Utils::get_pro_modules();
204 + // Setup Settings.
205 + if ( class_exists( 'WP_Analytify_Settings' ) ) {
206 + $this->settings = new WP_Analytify_Settings();
207 + }
208 +
209 + $this->is_reporting_in_ga4 = 'ga4' === WPANALYTIFY_Utils::get_ga_mode() ? true : false;
210 +
211 + // Initialize connection on init hook to ensure themes are loaded.
212 + add_action( 'init', array( $this, 'init_connection' ) );
213 + }
214 +
215 + /**
216 + * Initialize connection to Google Analytics.
217 + *
218 + * This method is hooked to 'init' to ensure that themes (functions.php) are loaded
219 + * before the connection is attempted. This allows custom hooks to fire correctly.
220 + *
221 + * @since 7.1.4
222 + */
223 + public function init_connection() {
224 + if ( true === $this->is_reporting_in_ga4 ) {
225 + // Rankmath Instant Indexing addon Compatibility.
226 + // Sanitize page parameter for security.
227 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading URL parameter for display purposes
228 + $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
229 + if ( ( $current_page && 'instant-indexing' === $current_page ) || strpos( wp_get_referer(), 'instant-indexing' ) !== false ) {
230 + return;
231 + }
232 +
233 + if ( 'on' === $this->settings->get_option( 'user_advanced_keys', 'wp-analytify-advanced', '' ) ) {
234 + $this->user_client_id = $this->settings->get_option( 'client_id', 'wp-analytify-advanced' );
235 + $this->user_client_secret = $this->settings->get_option( 'client_secret', 'wp-analytify-advanced' );
236 + }
237 +
238 + try {
239 + $this->analytify_pa_connect_v2();
240 + } catch ( Exception $e ) {
241 + // Show error message only for logged in users.
242 + if ( current_user_can( 'manage_options' ) ) {
243 + // translators: Reset authentication error message.
244 + printf( esc_html__( '%1$s Oops, Try to %2$s Reset %3$s Authentication. %4$s %7$s %4$s %5$s Don\'t worry, This error message is only visible to Administrators. %6$s %2$s', 'wp-analytify' ), '<br /><br />', '<a href=' . esc_url( admin_url( 'admin.php?page=analytify-settings&tab=authentication' ) ) . 'title="Reset">', '</a>', '<br />', '<i>', '</i>', esc_textarea( $e->getMessage() ) );
245 + }
246 + }
247 + }
248 +
249 + // Set cache time directly since after_setup_theme has already fired before init.
250 + $this->set_cache_time();
251 +
252 + $this->analytify_set_tracking_mode();
253 + }
254 +
255 + /**
256 + * This function grabs the data from Google Analytics for individual posts/pages.
257 + *
258 + * @param string $metrics The metrics to retrieve.
259 + * @param string $start_date The start date for the report.
260 + * @param string $end_date The end date for the report.
261 + * @param boolean $dimensions Optional dimensions for the report.
262 + * @param boolean $sort Optional sorting for the report.
263 + * @param boolean $filter Optional filters for the report.
264 + * @param boolean $limit Optional limit for the report.
265 + * @param string $name Optional name for caching.
266 + * @return void
267 + */
268 + public function pa_get_analytics( $metrics, $start_date, $end_date, $dimensions = false, $sort = false, $filter = false, $limit = false, $name = '' ) {
269 +
270 + if ( $this->is_reporting_in_ga4 ) {
271 + return;
272 + }
273 +
274 + try {
275 + $this->service = new Analytify_Google_Service_Analytics( $this->client );
276 + $params = array();
277 +
278 + if ( $dimensions ) {
279 + $params['dimensions'] = $dimensions;
280 + }
281 +
282 + if ( $sort ) {
283 + $params['sort'] = $sort;
284 + }
285 +
286 + if ( $filter ) {
287 + $params['filters'] = $filter;
288 + }
289 +
290 + if ( $limit ) {
291 + $params['max-results'] = $limit;
292 + }
293 +
294 + $profile_id = $this->settings->get_option( 'profile_for_posts', 'wp-analytify-profile' );
295 +
296 + if ( ! $profile_id ) {
297 + return false;
298 + }
299 +
300 + $transient_key = 'analytify_transient_';
301 + $cache_result = get_transient( $transient_key . md5( $name . $profile_id . $start_date . $end_date . $filter ) );
302 +
303 + // Note: This hard coded setting should be removed in future versions.
304 +
305 + $is_custom_api = $this->settings->get_option( 'user_advanced_keys', 'wp-analytify-advanced' );
306 +
307 + if ( 'on' !== $is_custom_api ) {
308 + // If exception, return if the cache result else return the error.
309 + $exception = get_transient( 'analytify_quota_exception' );
310 + if ( $exception ) {
311 + return $this->tackle_exception( $exception, $cache_result );
312 + }
313 + }
314 +
315 + // If custom keys set. Fetch fresh result always.
316 + if ( 'on' === $is_custom_api || false === $cache_result ) {
317 + $result = $this->service->data_ga->get( 'ga:' . $profile_id, $start_date, $end_date, $metrics, $params );
318 + set_transient( $transient_key . md5( $name . $profile_id . $start_date . $end_date . $filter ), $result, $this->get_cache_time() );
319 + return $result;
320 +
321 + } else {
322 + return $cache_result;
323 + }
324 + } catch ( Analytify_Google_Service_Exception $e ) {
325 + // Show error message only for logged in users.
326 + if ( current_user_can( 'manage_options' ) ) {
327 + echo "<div class='wp_analytify_error_msg'>";
328 + // translators: Error message for logged in users.
329 + printf( esc_html__( '%1$s Oops, Something went wrong. %2$s %5$s %2$s %3$s Don\'t worry, This error message is only visible to Administrators. %4$s %2$s ', 'wp-analytify' ), '<br /><br />', '<br />', '<i>', '</i>', esc_html( $e->getMessage() ) );
330 + echo '</div>';
331 + }
332 + } catch ( Analytify_Google_Auth_Exception $e ) {
333 + // Show error message only for logged in users.
334 + if ( current_user_can( 'manage_options' ) ) {
335 + echo "<div class='wp_analytify_error_msg'>";
336 + // translators: Reset authentication error message.
337 + printf( esc_html__( '%1$s Oops, Try to %3$s Reset %4$s Authentication. %2$s %7$s %2$s %5$s Don\'t worry, This error message is only visible to Administrators. %6$s %2$s', 'wp-analytify' ), '<br /><br />', '<br />', '<a href=' . esc_url( admin_url( 'admin.php?page=analytify-settings&tab=authentication' ) ) . ' title="Reset">', '</a>', '<i>', '</i>', esc_textarea( $e->getMessage() ) );
338 + echo '</div>';
339 + }
340 + } catch ( Analytify_Google_IO_Exception $e ) {
341 + // Show error message only for logged in users.
342 + if ( current_user_can( 'manage_options' ) ) {
343 + echo "<div class='wp_analytify_error_msg'>";
344 + // translators: Error message.
345 + printf( esc_html__( '%1$s Oops! %2$s %5$s %2$s %3$s Don\'t worry, This error message is only visible to Administrators. %4$s %2$s', 'wp-analytify' ), '<br /><br />', '<br />', '<i>', '</i>', esc_html( $e->getMessage() ) );
346 + echo '</div>';
347 + }
348 + }
349 + }
350 +
351 + /**
352 + * Mock Function to resist GA3 removal conflicts.
353 + *
354 + * @param string $metrics The metrics to retrieve.
355 + * @param string $start_date The start date for the report.
356 + * @param string $end_date The end date for the report.
357 + * @param boolean $dimensions Optional dimensions for the report.
358 + * @param boolean $sort Optional sorting for the report.
359 + * @param boolean $filter Optional filters for the report.
360 + * @param boolean $limit Optional limit for the report.
361 + * @param string $name Optional name for caching.
362 + * @return null|false
363 + */
364 + public function pa_get_analytics_dashboard( $metrics, $start_date, $end_date, $dimensions = false, $sort = false, $filter = false, $limit = false, $name = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
365 + if ( $this->is_reporting_in_ga4 ) {
366 + return null;
367 + }
368 + return false;
369 + }
370 +
371 + /**
372 + * Mock Function to resist GA3 removal conflicts.
373 + *
374 + * @param string $metrics The metrics to retrieve.
375 + * @param string $start_date The start date for the report.
376 + * @param string $end_date The end date for the report.
377 + * @param boolean $dimensions Optional dimensions for the report.
378 + * @param boolean $sort Optional sorting for the report.
379 + * @param boolean $filter Optional filters for the report.
380 + * @param boolean $limit Optional limit for the report.
381 + * @param string $name Optional name for caching.
382 + * @return null|false
383 + */
384 + public function pa_get_analytics_dashboard_via_rest( $metrics, $start_date, $end_date, $dimensions = false, $sort = false, $filter = false, $limit = false, $name = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
385 + if ( $this->is_reporting_in_ga4 ) {
386 + return null;
387 + }
388 + return false;
389 + }
390 +
391 + /**
392 + * This function grabs the data from Google Analytics For dashboard.
393 + *
394 + * @param string $profile Google Analytic Profile Id.
395 + * @param string $metrics Metrics.
396 + * @param string $start_date Start date of stats.
397 + * @param string $end_date End date of stats.
398 + * @param string $dimensions Dimensions.
399 + * @param string $sort Sort.
400 + * @param string $filter Filter.
401 + * @param string $limit How many stats to show.
402 + *
403 + * @return array Return array of stats.
404 + */
405 + public function analytify_get_analytics( $profile, $metrics, $start_date, $end_date, $dimensions = false, $sort = false, $filter = false, $limit = false ) {
406 +
407 + if ( $this->is_reporting_in_ga4 ) {
408 + return null;
409 + }
410 + try {
411 + if ( class_exists( 'Analytify_Google_Service_Analytics' ) ) {
412 + $this->service = new Analytify_Google_Service_Analytics( $this->client );
413 + }
414 + $params = array();
415 +
416 + if ( $dimensions ) {
417 + $params['dimensions'] = $dimensions;
418 + }
419 + if ( $sort ) {
420 + $params['sort'] = $sort;
421 + }
422 + if ( $filter ) {
423 + $params['filters'] = $filter;
424 + }
425 + if ( $limit ) {
426 + $params['max-results'] = $limit;
427 + }
428 +
429 + if ( 'single' === $profile ) {
430 + $profile_id = $this->settings->get_option( 'profile_for_posts', 'wp-analytify-profile' );
431 + } else {
432 + $profile_id = $this->settings->get_option( 'profile_for_dashboard', 'wp-analytify-profile' );
433 + }
434 +
435 + if ( ! $profile_id ) {
436 + return false;
437 + }
438 +
439 + return $this->service->data_ga->get( 'ga:' . $profile_id, $start_date, $end_date, $metrics, $params );
440 + } catch ( Analytify_Google_Service_Exception $e ) {
441 + // Show error message only for logged in users.
442 + if ( current_user_can( 'manage_options' ) ) {
443 + // translators: Error message.
444 + printf( esc_html__( '%1$s Oops, Something went wrong. %2$s %5$s %2$s %3$s Don\'t worry, This error message is only visible to Administrators. %4$s %2$s ', 'wp-analytify' ), '<br /><br />', '<br />', '<i>', '</i>', esc_textarea( $e->getMessage() ) );
445 + }
446 + } catch ( Analytify_Google_Auth_Exception $e ) {
447 + // Show error message only for logged in users.
448 + if ( current_user_can( 'manage_options' ) ) {
449 + // translators: Error message.
450 + printf( esc_html__( '%1$s Oops, Try to %3$s Reset %4$s Authentication. %2$s %7$s %2$s %5$s Don\'t worry, This error message is only visible to Administrators. %6$s %2$s', 'wp-analytify' ), '<br /><br />', '<br />', '<a href=' . esc_url( admin_url( 'admin.php?page=analytify-settings&tab=authentication' ) ) . ' title="Reset">', '</a>', '<i>', '</i>', esc_textarea( $e->getMessage() ) );
451 + }
452 + } catch ( Analytify_Google_IO_Exception $e ) {
453 + // Show error message only for logged in users.
454 + if ( current_user_can( 'manage_options' ) ) {
455 + // translators: Error message.
456 + printf( esc_html__( '%1$s Oops! %2$s %5$s %2$s %3$s Don\'t worry, This error message is only visible to Administrators. %4$s %2$s', 'wp-analytify' ), '<br /><br />', '<br />', '<i>', '</i>', esc_html( $e->getMessage() ) );
457 + echo '</div>';
458 + }
459 + }
460 + }
461 +
462 + /**
463 + * Fetch reports from Google Analytics Data API.
464 + *
465 + * @param string $name 'test-report-name' Its the key used to store reports in transient as cache.
466 + * @param array $metrics Array of metrics to fetch.
467 + * @param array $date_range Date range for the report.
468 + * @param array $dimensions Array of dimensions.
469 + * @param array $order_by Sorting configuration.
470 + * @param array $filters Filter configuration.
471 + * @param integer array $limit Positive integer to limit report rows.
472 + * @param boolean $cached Whether to use cached results.
473 + *
474 + * @return array {
475 + * 'headers' => {
476 + * ...
477 + * },
478 + * 'rows' => {
479 + * ...
480 + * }
481 + * }
482 + * @version 7.0.1
483 + * @throws Exception When API request fails.
484 + */
485 + public function get_reports( $name, $metrics, $date_range, $dimensions = array(), $order_by = array(), $filters = array(), $limit = 0, $cached = true ) {
486 + $logger = function_exists( 'analytify_get_logger' ) ? analytify_get_logger() : null;
487 +
488 + $property_id = WPANALYTIFY_Utils::get_reporting_property();
489 +
490 + // Don't use cache if custom API keys are in use.
491 + if ( 'on' === $this->settings->get_option( 'user_advanced_keys', 'wp-analytify-advanced' ) ) {
492 + $cached = false;
493 + }
494 +
495 + // To override the caching.
496 + $cached = apply_filters( 'analytify_set_caching_to', $cached );
497 +
498 + if ( $cached ) {
499 + $cache_key = 'analytify_transient_' . md5( $name . $property_id . $date_range['start'] . $date_range['end'] );
500 + $report_cache = get_transient( $cache_key );
501 +
502 + if ( $report_cache ) {
503 + return $report_cache;
504 + }
505 + }
506 +
507 + $reports = array();
508 + $dimension_filters = array();
509 +
510 + // Default response array.
511 + $default_response = array(
512 + 'headers' => array(),
513 + 'rows' => array(),
514 + 'error' => array(),
515 + 'aggregations' => array(),
516 + );
517 +
518 + try {
519 + // Main request body for the report.
520 + $request_body = array(
521 + 'dateRanges' => array(
522 + array(
523 + 'startDate' => isset( $date_range['start'] ) ? $date_range['start'] : 'today',
524 + 'endDate' => isset( $date_range['end'] ) ? $date_range['end'] : 'today',
525 + ),
526 + ),
527 + 'metricAggregations' => array( 1 ), // TOTAL = 1; COUNT = 4; MINIMUM = 5; MAXIMUM = 6.
528 + );
529 +
530 + // Set metrics.
531 + if ( $metrics ) {
532 + $send_metrics = array();
533 + foreach ( $metrics as $value ) {
534 + $send_metrics[] = array( 'name' => $value );
535 + }
536 + $request_body['metrics'] = $send_metrics;
537 + }
538 +
539 + // Add dimensions.
540 + if ( $dimensions ) {
541 + $send_dimensions = array();
542 + foreach ( $dimensions as $value ) {
543 + $send_dimensions[] = array( 'name' => $value );
544 + }
545 + $request_body['dimensions'] = $send_dimensions;
546 + }
547 +
548 + // Order report by metric or dimension.
549 + if ( $order_by ) {
550 + $order_by_request = array();
551 + $is_desc = ( empty( $order_by['order'] ) || 'desc' !== $order_by['order'] ) ? false : true;
552 +
553 + if ( 'metric' === $order_by['type'] ) {
554 + $order_by_request = array(
555 + 'metric' => array(
556 + 'metric_name' => isset( $order_by['name'] ) ? $order_by['name'] : '',
557 + ),
558 + 'desc' => $is_desc,
559 + );
560 + } elseif ( 'dimension' === $order_by['type'] ) {
561 + $order_by_request = array(
562 + 'dimension' => array(
563 + 'dimension_name' => $order_by['name'],
564 + ),
565 + 'desc' => $is_desc,
566 + );
567 + }
568 +
569 + $request_body['orderBys'] = array( $order_by_request );
570 + }
571 +
572 + // Filters for the report.
573 + if ( $filters ) {
574 + $dimension_filters = array(); // Initialize an empty array for filters.
575 +
576 + foreach ( $filters['filters'] as $filter_data ) {
577 + if ( 'dimension' === $filter_data['type'] ) {
578 + if ( isset( $filter_data['not_expression'] ) && $filter_data['not_expression'] ) {
579 + // Handle 'not_expression' logic.
580 + $dimension_filters[] = array(
581 + 'not_expression' => array(
582 + 'filter' => array(
583 + 'field_name' => $filter_data['name'],
584 + 'string_filter' => array(
585 + 'match_type' => $filter_data['match_type'],
586 + 'value' => $filter_data['value'],
587 + 'case_sensitive' => true,
588 + ),
589 + ),
590 + ),
591 + );
592 + } else {
593 + // Standard dimension filter.
594 + $dimension_filters[] = array(
595 + 'filter' => array(
596 + 'field_name' => $filter_data['name'],
597 + 'string_filter' => array(
598 + 'match_type' => $filter_data['match_type'],
599 + 'value' => $filter_data['value'],
600 + 'case_sensitive' => true,
601 + ),
602 + ),
603 + );
604 + }
605 + } elseif ( 'metric' === $filter_data['type'] ) {
606 + // Note: Add metric filter handling here.
607 + // Currently no implementation for metric filters.
608 + // This is intentionally left empty for future implementation.
609 + // No action needed for metric filters at this time.
610 + // Skip metric filters without affecting dimension_filters array.
611 + continue;
612 + }
613 + }
614 +
615 + if ( $dimension_filters ) {
616 + $group_type = ( isset( $filters['logic'] ) && 'OR' === $filters['logic'] ) ? 'or_group' : 'and_group';
617 +
618 + $dimension_filter_construct = array(
619 + $group_type => array(
620 + 'expressions' => $dimension_filters,
621 + ),
622 + );
623 +
624 + $request_body['dimensionFilter'] = $dimension_filter_construct;
625 + }
626 + }
627 +
628 + // Set limit.
629 + if ( 0 < $limit ) {
630 + $request_body['limit'] = $limit;
631 + }
632 +
633 + // Get access token (this function should be implemented by you).
634 + $token = $this->analytify_get_google_token();
635 +
636 + // Validate that token is an array and has the expected structure.
637 + if ( ! is_array( $token ) || ! isset( $token['access_token'] ) ) {
638 + if ( $logger && method_exists( $logger, 'warning' ) ) {
639 + $logger->warning(
640 + 'Invalid or missing Google Analytics token in get_reports.',
641 + array(
642 + 'source' => 'get_reports',
643 + 'report_name' => $name,
644 + 'token_type' => gettype( $token ),
645 + 'has_access_token' => isset( $token['access_token'] ),
646 + )
647 + );
648 + }
649 + return array();
650 + }
651 +
652 + $access_token = $token['access_token'];
653 +
654 + // Prepare the cURL request URL for GA4 API.
655 + $url = 'https://analyticsdata.googleapis.com/v1beta/properties/' . $property_id . ':runReport';
656 +
657 + // Send the request using wp_remote_post.
658 + $response = wp_remote_post(
659 + $url,
660 + array(
661 + 'headers' => array(
662 + 'Authorization' => 'Bearer ' . $access_token,
663 + 'Content-Type' => 'application/json',
664 + ),
665 + 'body' => wp_json_encode( $request_body ),
666 + )
667 + );
668 +
669 + // Check for errors in the response.
670 + if ( is_wp_error( $response ) ) {
671 + throw new Exception( $response->get_error_message() );
672 + }
673 +
674 + // Parse the response body.
675 + $reports = json_decode( wp_remote_retrieve_body( $response ), true );
676 +
677 + // If the response doesn't contain rows, handle it accordingly.
678 + if ( ! isset( $reports['rows'] ) ) {
679 + return $default_response;
680 + }
681 + } catch ( \Throwable $th ) {
682 + if ( method_exists( $th, 'getStatus' ) && method_exists( $th, 'getBasicMessage' ) ) {
683 + $default_response['error'] = array(
684 + 'status' => $th->getStatus(),
685 + 'message' => $th->getBasicMessage(),
686 + );
687 + if ( $logger && method_exists( $logger, 'warning' ) ) {
688 + $logger->warning(
689 + 'Exception in get_reports API call.',
690 + array(
691 + 'source' => 'get_reports',
692 + 'report_name' => $name,
693 + 'status' => $th->getStatus(),
694 + 'message' => $th->getBasicMessage(),
695 + 'exception_type' => get_class( $th ),
696 + )
697 + );
698 + }
699 + } elseif ( method_exists( $th, 'getMessage' ) ) {
700 + $default_response['error'] = array(
701 + 'status' => 'Token Expired',
702 + 'message' => $th->getMessage(),
703 + );
704 + if ( $logger && method_exists( $logger, 'warning' ) ) {
705 + $logger->warning(
706 + 'Exception in get_reports API call - token expired.',
707 + array(
708 + 'source' => 'get_reports',
709 + 'report_name' => $name,
710 + 'message' => $th->getMessage(),
711 + 'exception_type' => get_class( $th ),
712 + )
713 + );
714 + }
715 + }
716 +
717 + return $default_response;
718 + }
719 +
720 + // Format the reports using your existing function.
721 + $formatted_reports = $this->analytify_format_ga_reports( $reports );
722 +
723 + if ( empty( $formatted_reports ) ) {
724 + return $default_response;
725 + }
726 +
727 + // Cache the response if caching is enabled.
728 + if ( $cached ) {
729 + $this->analytify_handle_report_cache( $cache_key, $formatted_reports, $name, $cached );
730 + }
731 +
732 + return $formatted_reports;
733 + }
734 +
735 + /**
736 + * Format reports data fetched from Google Analytics Data API.
737 + *
738 + * For references check folder for class definitions: lib\Google\vendor\google\analytics-data\src\V1beta.
739 + *
740 + * @param array $reports The reports data to format.
741 + * @return array
742 + */
743 + public function analytify_format_ga_reports( $reports ) {
744 + $metric_header_data = array();
745 + $dimension_header_data = array();
746 + $aggregations = array();
747 + $rows = array();
748 +
749 + // Get metric headers.
750 + if ( isset( $reports['metricHeaders'] ) ) {
751 + foreach ( $reports['metricHeaders'] as $metric_header ) {
752 + $metric_header_data[] = $metric_header['name'];
753 + }
754 + }
755 +
756 + // Get dimension headers.
757 + if ( isset( $reports['dimensionHeaders'] ) ) {
758 + foreach ( $reports['dimensionHeaders'] as $dimension_header ) {
759 + $dimension_header_data[] = $dimension_header['name'];
760 + }
761 + }
762 +
763 + $headers = array_merge( $metric_header_data, $dimension_header_data );
764 +
765 + // Bind metrics and dimensions to rows.
766 + if ( isset( $reports['rows'] ) ) {
767 + foreach ( $reports['rows'] as $row ) {
768 + $metric_data = array();
769 + $dimension_data = array();
770 +
771 + // Process metric values.
772 + if ( isset( $row['metricValues'] ) ) {
773 + $index_metric = 0;
774 + foreach ( $row['metricValues'] as $value ) {
775 + $metric_data[ $metric_header_data[ $index_metric ] ] = $value['value'];
776 + ++$index_metric;
777 + }
778 + }
779 +
780 + // Process dimension values.
781 + if ( isset( $row['dimensionValues'] ) ) {
782 + $index_dimension = 0;
783 + foreach ( $row['dimensionValues'] as $value ) {
784 + $dimension_data[ $dimension_header_data[ $index_dimension ] ] = $value['value'];
785 + ++$index_dimension;
786 + }
787 + }
788 +
789 + // Combine metric and dimension data.
790 + $rows[] = array_merge( $metric_data, $dimension_data );
791 + }
792 + }
793 +
794 + // Get metric aggregations (totals).
795 + if ( isset( $reports['totals'] ) ) {
796 + foreach ( $reports['totals'] as $total ) {
797 + $index_metric = 0;
798 +
799 + if ( isset( $total['metricValues'] ) ) {
800 + foreach ( $total['metricValues'] as $value ) {
801 + $aggregations[ $metric_header_data[ $index_metric ] ] = $value['value'];
802 + ++$index_metric;
803 + }
804 + }
805 + }
806 + }
807 +
808 + // Format and return the data.
809 + $formatted_data = array(
810 + 'headers' => $headers,
811 + 'rows' => $rows,
812 + 'aggregations' => $aggregations,
813 + );
814 +
815 + return $formatted_data;
816 + }
817 +
818 +
819 + /**
820 + * Query the search console api and return the response.
821 + * Since SC can have two types of domain properties.
822 + * We will first go with the sc-domain prefix with property
823 + * if it fails we will use the second domain type using 'https://'
824 + *
825 + * @param string $transient_name The transient name for caching.
826 + * @param array $dates The date range for the query.
827 + * @param int $limit The limit for the results.
828 + *
829 + * @since 5.0.0
830 + * @version 9.0.0
831 + */
832 + public function get_search_console_stats( $transient_name, $dates = array(), $limit = 10 ) {
833 +
834 + $logger = function_exists( 'analytify_get_logger' ) ? analytify_get_logger() : null;
835 +
836 + if ( class_exists( 'QM' ) ) {
837 + QM::info( 'Analytify: Getting Google Analytics token for Search Console stats.' );
838 + }
839 +
840 + $token = $this->analytify_get_google_token();
841 +
842 + if ( ! is_array( $token ) || ! isset( $token['access_token'] ) ) {
843 + return array( 'error' => array( 'Invalid or missing Google Analytics token.' ) );
844 + }
845 +
846 + $access_token = $token['access_token'];
847 +
848 + $tracking_stream_info = get_option( 'analytify_tracking_property_info' );
849 +
850 + try {
851 + $stream_url = ( isset( $tracking_stream_info['url'] ) && ! empty( $tracking_stream_info['url'] ) ) ? $tracking_stream_info['url'] : null;
852 + } catch ( \Throwable $th ) {
853 + if ( $logger && method_exists( $logger, 'warning' ) ) {
854 + $logger->warning(
855 + 'Error fetching stream URL',
856 + array(
857 + 'source' => 'analytify_fetch_stream_url',
858 + 'message' => $th->getMessage(),
859 + )
860 + );
861 + }
862 +
863 + if ( empty( $stream_url ) ) {
864 + return array(
865 + 'error' => array(
866 + 'status' => 'No Stats Available',
867 + 'message' => __( 'No URL found for the selected stream', 'wp-analytify' ),
868 + ),
869 + );
870 + }
871 + }
872 +
873 + // Validate stream URL.
874 + if ( empty( $stream_url ) ) {
875 + return array(
876 + 'error' => array(
877 + 'status' => 'No Stats Available',
878 + 'message' => __( 'No URL found for the selected stream', 'wp-analytify' ),
879 + ),
880 + );
881 + }
882 +
883 + // Sanitize URL.
884 + $stream_url = trim( $stream_url );
885 + $stream_url = esc_url_raw( $stream_url );
886 +
887 + // Extract domain (handles ports and IPv6).
888 + $domain_stream_url_filtered = preg_replace( '/^(https?:\/\/)?(www\.)?([^\/\s:]+(?::\d+)?|\[[^\]]+\])(\/.*)?$/i', '$3', $stream_url );
889 + $domain_stream_url_filtered = preg_replace( '/:\d+$/', '', $domain_stream_url_filtered ); // Remove port.
890 + $domain_stream_url_filtered = str_replace( array( '[', ']' ), '', $domain_stream_url_filtered ); // Remove IPv6 brackets.
891 +
892 + // Build candidate URLs for Search Console API.
893 + $urls = array(
894 + 'sc-domain:' . $domain_stream_url_filtered,
895 + 'https://' . $domain_stream_url_filtered,
896 + 'https://www.' . $domain_stream_url_filtered,
897 + 'http://' . $domain_stream_url_filtered,
898 + 'http://www.' . $domain_stream_url_filtered,
899 + 'https://' . rtrim( $domain_stream_url_filtered, '/' ) . '/', // URL-prefix format.
900 + );
901 +
902 + // Remove duplicates to avoid redundant API calls.
903 + $urls = array_unique( $urls );
904 +
905 + $base_url = ANALYTIFY_GOOGLE_SEARCH_CONSOLE_API_URL;
906 + $start_date = $dates['start'] ?? 'yesterday';
907 + $end_date = $dates['end'] ?? 'today';
908 +
909 + // Track responses: prefer domains with data, fallback to any accepted domain.
910 + $accepted_domains_with_data = array();
911 + $accepted_domains_no_data = array();
912 +
913 + foreach ( $urls as $url ) {
914 + try {
915 + $query_data = array(
916 + 'startDate' => $start_date,
917 + 'endDate' => $end_date,
918 + 'dimensions' => array( 'query' ),
919 + 'rowLimit' => $limit,
920 + );
921 +
922 + // Make request to Search Console API using WordPress HTTP API.
923 + $http_response = wp_remote_post(
924 + $base_url . rawurlencode( $url ) . '/searchAnalytics/query',
925 + array(
926 + 'headers' => array(
927 + 'Authorization' => 'Bearer ' . $access_token,
928 + 'Content-Type' => 'application/json',
929 + ),
930 + 'body' => wp_json_encode( $query_data ),
931 + 'timeout' => 30,
932 + 'sslverify' => true, // Explicitly ensure SSL verification.
933 + )
934 + );
935 +
936 + if ( is_wp_error( $http_response ) ) {
937 + if ( $logger && method_exists( $logger, 'warning' ) ) {
938 + $logger->warning(
939 + sprintf( 'HTTP request failed for domain "%s": %s', $url, $http_response->get_error_message() ),
940 + array(
941 + 'source' => 'analytify_fetch_search_console_stats',
942 + 'domain' => $url,
943 + )
944 + );
945 + }
946 + continue; // Continue to next URL.
947 + }
948 +
949 + $http_code = wp_remote_retrieve_response_code( $http_response );
950 + $response_body = wp_remote_retrieve_body( $http_response );
951 +
952 + // Log all HTTP responses for debugging, but categorize them.
953 + if ( 200 === $http_code ) {
954 + $decoded = json_decode( $response_body, true );
955 +
956 + // Validate JSON decode result.
957 + if ( json_last_error() !== JSON_ERROR_NONE ) {
958 + if ( $logger && method_exists( $logger, 'error' ) ) {
959 + $logger->error(
960 + sprintf( 'JSON decode failed for domain "%s": %s', $url, json_last_error_msg() ),
961 + array(
962 + 'source' => 'analytify_fetch_search_console_stats',
963 + 'domain' => $url,
964 + )
965 + );
966 + }
967 + continue;
968 + }
969 +
970 + // Ensure decoded result is an array.
971 + if ( ! is_array( $decoded ) ) {
972 + if ( $logger && method_exists( $logger, 'error' ) ) {
973 + $logger->error(
974 + sprintf( 'Unexpected JSON response for domain "%s": not an array', $url ),
975 + array(
976 + 'source' => 'analytify_fetch_search_console_stats',
977 + 'domain' => $url,
978 + )
979 + );
980 + }
981 + continue;
982 + }
983 +
984 + $row_count = count( $decoded['rows'] ?? array() );
985 +
986 + // Log domain check result for debugging.
987 + if ( $logger && method_exists( $logger, 'info' ) ) {
988 + $logger->info(
989 + sprintf( 'Domain "%s" - HTTP %d, %d rows found', $url, $http_code, $row_count ),
990 + array(
991 + 'source' => 'analytify_fetch_search_console_stats',
992 + 'domain' => $url,
993 + 'http_code' => $http_code,
994 + 'row_count' => $row_count,
995 + )
996 + );
997 + }
998 +
999 + // Store this response - prefer domains with data.
1000 + if ( $row_count > 0 ) {
1001 + $accepted_domains_with_data[] = array(
1002 + 'url' => $url,
1003 + 'data' => $decoded,
1004 + );
1005 + if ( $logger && method_exists( $logger, 'info' ) ) {
1006 + $logger->info(
1007 + 'Domain categorized as HAVING DATA',
1008 + array(
1009 + 'source' => 'analytify_fetch_search_console_stats',
1010 + 'domain' => $url,
1011 + 'row_count' => $row_count,
1012 + )
1013 + );
1014 + }
1015 + } else {
1016 + $accepted_domains_no_data[] = array(
1017 + 'url' => $url,
1018 + 'data' => $decoded,
1019 + );
1020 + if ( $logger && method_exists( $logger, 'info' ) ) {
1021 + $logger->info(
1022 + 'Domain categorized as NO DATA',
1023 + array(
1024 + 'source' => 'analytify_fetch_search_console_stats',
1025 + 'domain' => $url,
1026 + 'row_count' => $row_count,
1027 + )
1028 + );
1029 + }
1030 + }
1031 + } elseif ( 200 !== $http_code ) {
1032 + // Log non-200 with code only; do not log response body (may contain sensitive data).
1033 + if ( $logger && method_exists( $logger, 'warning' ) ) {
1034 + $logger->warning(
1035 + sprintf( 'Domain "%s" returned HTTP %d', $url, $http_code ),
1036 + array(
1037 + 'source' => 'analytify_fetch_search_console_stats',
1038 + 'domain' => $url,
1039 + 'http_code' => $http_code,
1040 + )
1041 + );
1042 + }
1043 + }
1044 + } catch ( \Throwable $th ) {
1045 + // Continue to next URL on exception.
1046 + continue;
1047 + }
1048 + }
1049 +
1050 + // Choose the best domain after checking ALL URLs.
1051 + $chosen_domain = null;
1052 +
1053 + // Priority 1: Any domain with actual keyword data (prefer first one found).
1054 + if ( ! empty( $accepted_domains_with_data ) ) {
1055 + $chosen_domain = $accepted_domains_with_data[0]; // Use first domain that has data.
1056 + } // phpcs:ignore Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace
1057 + // Priority 2: If NO domains have data, use first accepted domain (fallback).
1058 + elseif ( ! empty( $accepted_domains_no_data ) ) {
1059 + $chosen_domain = $accepted_domains_no_data[0]; // Use first accepted domain as fallback.
1060 + }
1061 +
1062 + // Return the chosen domain's data.
1063 + if ( $chosen_domain ) {
1064 + return array(
1065 + 'response' => $chosen_domain['data'],
1066 + );
1067 + }
1068 +
1069 + // No domains were accepted at all.
1070 + if ( $logger && method_exists( $logger, 'warning' ) ) {
1071 + $logger->warning(
1072 + 'FINAL FAILURE: No domain accepted',
1073 + array(
1074 + 'source' => 'analytify_fetch_search_console_stats',
1075 + 'site' => $domain_stream_url_filtered,
1076 + )
1077 + );
1078 + }
1079 +
1080 + return array(
1081 + 'error' => array(
1082 + 'status' => "No Stats Available for $domain_stream_url_filtered",
1083 + 'message' => __( 'Analytify gets GA4 keyword stats from Search Console. Make sure the site is verified and you have owner access.', 'wp-analytify' ),
1084 + ),
1085 + );
1086 + }
1087 + } // End of class
1088 +} // End of if class exists