' . esc_html(sprintf(__('Total tracked views in the last 30 days: %s', 'ansico-stat-plugin'), number_format_i18n($monthly_total))) . '
';
- echo $this->render_chart_markup($chart_data, 'ansico-stat-widget-chart', ['is_widget' => true]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo $this->render_multi_line_chart_markup($chart_data, 'ansico-stat-widget-chart', [
+ 'aria_label' => __('Daily views and unique visitors (last 30 days)', 'ansico-stat-plugin'),
+ 'height' => 250,
+ 'width' => 920,
+ 'chart_top' => 12,
+ 'chart_bottom' => 198,
+ 'chart_left' => 44,
+ 'chart_right' => 888,
+ 'x_label_y' => 226,
+ 'max_x_labels' => 8,
+ ]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '
';
@@ -1206,11 +1818,211 @@ if (!class_exists('Ansico_Stat_Plugin')) {
return ob_get_clean();
}
- protected function render_bar_chart_markup(array $chart_data, string $chart_id, string $aria_label = '') {
+
+ protected function render_multi_line_chart_markup(array $chart_data, string $chart_id, array $args = []) {
+ $args = wp_parse_args($args, [
+ 'label_key' => 'label',
+ 'x_fallback_key' => 'date',
+ 'views_label' => __('Views', 'ansico-stat-plugin'),
+ 'unique_label' => __('Unique visitors', 'ansico-stat-plugin'),
+ 'aria_label' => __('Views and unique visitors chart', 'ansico-stat-plugin'),
+ 'height' => 240,
+ 'width' => 860,
+ 'chart_top' => 18,
+ 'chart_bottom' => 190,
+ 'chart_left' => 34,
+ 'chart_right' => 830,
+ 'x_label_y' => 216,
+ 'tick_count' => 5,
+ 'max_x_labels' => 8,
+ ]);
+
+ if (empty($chart_data)) {
+ return '
' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '
';
+ }
+
+ $labels = [];
+ $full_labels = [];
+ $views = [];
+ $unique = [];
+ foreach ($chart_data as $row) {
+ $label = '';
+ if (!empty($row[$args['label_key']])) {
+ $label = (string) $row[$args['label_key']];
+ } elseif (!empty($row[$args['x_fallback_key']])) {
+ $label = (string) $row[$args['x_fallback_key']];
+ }
+ $full_label = '';
+ if (!empty($row['tooltip_label'])) {
+ $full_label = (string) $row['tooltip_label'];
+ } elseif (!empty($row[$args['x_fallback_key']])) {
+ $full_label = (string) $row[$args['x_fallback_key']];
+ } else {
+ $full_label = $label;
+ }
+ $labels[] = $label;
+ $full_labels[] = $full_label;
+ $views[] = (int) ($row['views'] ?? $row['total_views'] ?? 0);
+ $unique[] = (int) ($row['unique_visitors'] ?? $row['total_unique'] ?? 0);
+ }
+
+ $max = max(array_merge($views, $unique, [0]));
+ if ($max <= 0) {
+ return '
' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '
';
+ }
+
+ $height = (int) $args['height'];
+ $width = (int) $args['width'];
+ $chart_top = (float) $args['chart_top'];
+ $chart_bottom = (float) $args['chart_bottom'];
+ $chart_left = (float) $args['chart_left'];
+ $chart_right = (float) $args['chart_right'];
+ $count = count($labels);
+ $step_x = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : 0;
+
+ $build_series = function(array $values, string $series_key, string $value_label) use ($count, $labels, $full_labels, $step_x, $chart_left, $chart_right, $chart_bottom, $chart_top, $max) {
+ $poly_points = [];
+ $circles = [];
+ foreach ($values as $index => $value) {
+ $x = $count > 1 ? $chart_left + ($step_x * $index) : (($chart_left + $chart_right) / 2);
+ $y = $chart_bottom;
+ if ($max > 0) {
+ $y = $chart_bottom - (($value / $max) * ($chart_bottom - $chart_top));
+ }
+ $x = round($x, 2);
+ $y = round($y, 2);
+ $poly_points[] = $x . ',' . $y;
+ $circles[] = [
+ 'x' => $x,
+ 'y' => $y,
+ 'label' => (string) ($labels[$index] ?? ''),
+ 'tooltip_label' => (string) ($full_labels[$index] ?? ($labels[$index] ?? '')),
+ 'value' => (int) $value,
+ 'series_key' => $series_key,
+ 'series_label' => $value_label,
+ ];
+ }
+ return [
+ 'points' => implode(' ', $poly_points),
+ 'circles' => $circles,
+ ];
+ };
+
+ $view_series = $build_series($views, 'views', (string) $args['views_label']);
+ $unique_series = $build_series($unique, 'unique', (string) $args['unique_label']);
+
+ $y_ticks = [];
+ $tick_count = max(2, (int) $args['tick_count']);
+ for ($i = 0; $i < $tick_count; $i++) {
+ $ratio = $tick_count > 1 ? $i / ($tick_count - 1) : 0;
+ $value = (int) round($max * (1 - $ratio));
+ $y = $chart_top + (($chart_bottom - $chart_top) * $ratio);
+ $y_ticks[] = [
+ 'value' => $value,
+ 'y' => round($y, 2),
+ ];
+ }
+
+ ob_start();
+ ?>
+
+
+ 'vertical',
+ ]);
+
+ $orientation = $args['orientation'] === 'horizontal' ? 'horizontal' : 'vertical';
+
$values = [];
foreach ($chart_data as $row) {
$values[] = (int) ($row['views'] ?? $row['total_views'] ?? 0);
@@ -1218,26 +2030,62 @@ if (!class_exists('Ansico_Stat_Plugin')) {
$max = max($values ?: [0]);
$sum = array_sum($values);
+ $item_count = count($chart_data);
+ $slot_width = 52;
+ $bar_width = 28;
+ $row_height = 26;
+
+ if ($item_count >= 10) {
+ $slot_width = 40;
+ $bar_width = 18;
+ $row_height = 22;
+ } elseif ($item_count >= 8) {
+ $slot_width = 44;
+ $bar_width = 20;
+ $row_height = 24;
+ } elseif ($item_count >= 6) {
+ $slot_width = 48;
+ $bar_width = 24;
+ $row_height = 25;
+ }
ob_start();
?>
-
+
-
- 0 && $value > 0) ? max(6, (int) round(($value / $max) * 140)) : 0;
- ?>
-
-
-
+
+
+ 0 && $value > 0) ? max(2, (int) round(($value / $max) * 100)) : 0;
+ ?>
+
+
+
+
+
+ 0 && $value > 0) ? max(6, (int) round(($value / $max) * 140)) : 0;
+ ?>
+
+
+
+
+
+
+ ' . esc_html__('No lifetime data has been tracked yet.', 'ansico-stat-plugin') . '';
+ return;
+ }
+ ?>
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+ get_results(
+ "SELECT stat_date, SUM(views) AS total_views
+ FROM {$daily_table}
+ GROUP BY stat_date
+ ORDER BY stat_date ASC",
+ ARRAY_A
+ );
+
+ $unique_rows = $wpdb->get_results($wpdb->prepare(
+ "SELECT period_key, COUNT(*) AS total_unique
+ FROM {$unique_table}
+ WHERE period_type = %s
+ GROUP BY period_key
+ ORDER BY period_key ASC",
+ 'day'
+ ), ARRAY_A);
+
+ $labels = [
+ 1 => __('Monday', 'ansico-stat-plugin'),
+ 2 => __('Tuesday', 'ansico-stat-plugin'),
+ 3 => __('Wednesday', 'ansico-stat-plugin'),
+ 4 => __('Thursday', 'ansico-stat-plugin'),
+ 5 => __('Friday', 'ansico-stat-plugin'),
+ 6 => __('Saturday', 'ansico-stat-plugin'),
+ 7 => __('Sunday', 'ansico-stat-plugin'),
+ ];
+
+ $map = [];
+ foreach ($labels as $day_number => $label) {
+ $map[$day_number] = [
+ 'label' => $label,
+ 'views' => 0,
+ 'unique_visitors' => 0,
+ ];
+ }
+
+ if (is_array($view_rows)) {
+ foreach ($view_rows as $row) {
+ $date = (string) ($row['stat_date'] ?? '');
+ if ($date === '') {
+ continue;
+ }
+ $weekday = (int) wp_date('N', strtotime($date));
+ if (!isset($map[$weekday])) {
+ continue;
+ }
+ $map[$weekday]['views'] += (int) ($row['total_views'] ?? 0);
+ }
+ }
+
+ if (is_array($unique_rows)) {
+ foreach ($unique_rows as $row) {
+ $date = (string) ($row['period_key'] ?? '');
+ if ($date === '') {
+ continue;
+ }
+ $weekday = (int) wp_date('N', strtotime($date));
+ if (!isset($map[$weekday])) {
+ continue;
+ }
+ $map[$weekday]['unique_visitors'] += (int) ($row['total_unique'] ?? 0);
+ }
+ }
+
+ $total_views = 0;
+ $total_unique = 0;
+ foreach ($map as $row) {
+ $total_views += (int) $row['views'];
+ $total_unique += (int) $row['unique_visitors'];
+ }
+
+ $rows = [];
+ foreach ($map as $day_number => $row) {
+ $views = (int) $row['views'];
+ $unique = (int) $row['unique_visitors'];
+ $rows[] = [
+ 'day_number' => $day_number,
+ 'label' => (string) $row['label'],
+ 'views' => $views,
+ 'unique_visitors' => $unique,
+ 'views_percent' => $total_views > 0 ? number_format_i18n(($views / $total_views) * 100, 1) . '%' : '0%',
+ 'unique_percent' => $total_unique > 0 ? number_format_i18n(($unique / $total_unique) * 100, 1) . '%' : '0%',
+ ];
+ }
+
+ return $rows;
+ }
+
+ protected function render_lifetime_weekday_distribution_table(array $rows) {
+ if (empty($rows)) {
+ echo '
' . esc_html__('No weekday distribution data has been tracked yet.', 'ansico-stat-plugin') . '
';
+ return;
+ }
+ ?>
+
+
+
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ (int) ($summary['direct'] ?? 0), 'unique_visitors' => 0];
+ $search = is_array($summary['search'] ?? null) ? $summary['search'] : ['visits' => (int) ($summary['search'] ?? 0), 'unique_visitors' => 0];
+ $social = is_array($summary['social'] ?? null) ? $summary['social'] : ['visits' => (int) ($summary['social'] ?? 0), 'unique_visitors' => 0];
+ $website = is_array($summary['website'] ?? null) ? $summary['website'] : ['visits' => (int) ($summary['website'] ?? 0), 'unique_visitors' => 0];
+
+ $total = (int) ($direct['visits'] ?? 0) + (int) ($search['visits'] ?? 0) + (int) ($social['visits'] ?? 0) + (int) ($website['visits'] ?? 0);
+ $unique_total = (int) ($direct['unique_visitors'] ?? 0) + (int) ($search['unique_visitors'] ?? 0) + (int) ($social['unique_visitors'] ?? 0) + (int) ($website['unique_visitors'] ?? 0);
?>
|
|
- |
+ |
+ |
+ |
- | | format_percent((int) ($summary['direct'] ?? 0), $total)); ?> |
- | | format_percent((int) ($summary['search'] ?? 0), $total)); ?> |
- | | format_percent((int) ($summary['social'] ?? 0), $total)); ?> |
- | | format_percent((int) ($summary['website'] ?? 0), $total)); ?> |
+ | | | format_percent((int) ($direct['visits'] ?? 0), $total)); ?> | format_percent((int) ($direct['unique_visitors'] ?? 0), $unique_total)); ?> |
+ | | | format_percent((int) ($search['visits'] ?? 0), $total)); ?> | format_percent((int) ($search['unique_visitors'] ?? 0), $unique_total)); ?> |
+ | | | format_percent((int) ($social['visits'] ?? 0), $total)); ?> | format_percent((int) ($social['unique_visitors'] ?? 0), $unique_total)); ?> |
+ | | | format_percent((int) ($website['visits'] ?? 0), $total)); ?> | format_percent((int) ($website['unique_visitors'] ?? 0), $unique_total)); ?> |
$threshold) {
+ $classes = trim('ansico-table-scroll-wrap ' . $extra_class);
+ $style = 'max-height:470px; overflow-y:auto; overflow-x:auto; display:block; width:100%;';
+ echo '
';
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function maybe_close_scrollable_table_wrapper(bool $opened) {
+ if ($opened) {
+ echo '
';
+ }
+ }
+
protected function render_dimension_table(array $rows, string $value_label, string $views_label = '') {
if ($views_label === '') {
$views_label = __('Views', 'ansico-stat-plugin');
@@ -1857,10 +3347,9 @@ if (!class_exists('Ansico_Stat_Plugin')) {
return;
}
- $total = 0;
- foreach ($rows as $row) {
- $total += (int) ($row['total_views'] ?? 0);
- }
+ $total = $this->get_rows_total($rows, 'total_views');
+ $unique_total = $this->get_rows_total($rows, 'total_unique');
+ $opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
@@ -1868,12 +3357,14 @@ if (!class_exists('Ansico_Stat_Plugin')) {
|
|
|
- |
+ |
+ |
+ |
$row) : ?>
-
+
|
@@ -1887,11 +3378,52 @@ if (!class_exists('Ansico_Stat_Plugin')) {
?>
|
|
+ |
format_percent($count, $total)); ?> |
+ format_percent($unique, $unique_total)); ?> |
+ maybe_close_scrollable_table_wrapper($opened_scroll); ?>
+ ' . esc_html($empty_message) . '';
+ return;
+ }
+
+ $unique_total = $this->get_rows_total($rows, 'total_unique');
+ $opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
+ ?>
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ $row) : ?>
+
+
+ |
+ format_landing_page_link((string) ($row['page_url'] ?? ''), (string) ($row['page_label'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
+ |
+ format_percent($unique, $unique_total)); ?> |
+
+
+
+
+ maybe_close_scrollable_table_wrapper($opened_scroll); ?>
' . esc_html__('No tracked 404 views for this month.', 'ansico-stat-plugin') . '';
return;
}
+ $opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
@@ -1907,6 +3440,7 @@ if (!class_exists('Ansico_Stat_Plugin')) {
|
|
|
+ |
@@ -1915,10 +3449,12 @@ if (!class_exists('Ansico_Stat_Plugin')) {
|
format_external_link((string) ($row['page_url'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
|
+ |
+ maybe_close_scrollable_table_wrapper($opened_scroll); ?>
' . esc_html__('No views tracked for this period.', 'ansico-stat-plugin') . '';
return;
}
+ $opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
@@ -1935,6 +3472,7 @@ if (!class_exists('Ansico_Stat_Plugin')) {
|
|
|
+ |
|
@@ -1951,14 +3489,29 @@ if (!class_exists('Ansico_Stat_Plugin')) {
format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
|
|
+ |
|
+ maybe_close_scrollable_table_wrapper($opened_scroll); ?>
get_truncated_url_text($label, 100));
+ }
+
+ return $this->format_url_display_link($url, $label);
+ }
+
protected function render_referral_sources_table(array $rows, string $label) {
echo '
' . esc_html($label) . '
';
if (empty($rows)) {
@@ -1966,1177 +3519,779 @@ if (!class_exists('Ansico_Stat_Plugin')) {
return;
}
?>
-
+ get_rows_total($rows, 'total_visits'); $unique_total = $this->get_rows_total($rows, 'total_unique'); ?>
+
get_settings();
+ return !empty($settings['google_access_token']) || !empty($settings['google_refresh_token']);
}
- protected function get_single_stats_date_range() {
- $default_end = current_time('Y-m-d');
- $default_start = $this->date_days_ago(29);
-
- $start = isset($_GET['start_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['start_date'])) : '';
- $end = isset($_GET['end_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['end_date'])) : '';
-
- if ($start === '') {
- $start = $default_start;
+ protected function get_google_connection_status_label() {
+ $settings = $this->get_settings();
+ if (!$this->is_google_connected()) {
+ return __('Not connected', 'ansico-stat-plugin');
}
- if ($end === '') {
- $end = $default_end;
+ if (!empty($settings['google_token_expires_at']) && (int) $settings['google_token_expires_at'] <= time()) {
+ return __('Connected, token refresh needed', 'ansico-stat-plugin');
}
- if (strtotime($start) > strtotime($end)) {
- [$start, $end] = [$end, $start];
+ return __('Connected', 'ansico-stat-plugin');
+ }
+
+ protected function get_google_site_url_prefix() {
+ return trailingslashit(home_url('/'));
+ }
+
+ protected function get_google_url_prefix_property() {
+ $site_url = $this->get_google_site_url_prefix();
+ if (!preg_match('#^https?://#i', $site_url)) {
+ $site_url = 'https://' . ltrim($site_url, '/');
+ }
+ return trailingslashit($site_url);
+ }
+
+ protected function get_google_auto_verification_property() {
+ $selected_property = $this->get_google_selected_property();
+ if ($selected_property !== '' && !$this->is_google_domain_property($selected_property)) {
+ return $selected_property;
+ }
+ return $this->get_google_url_prefix_property();
+ }
+
+ protected function get_google_selected_property() {
+ $settings = $this->get_settings();
+ $property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
+ return $property !== '' ? $property : $this->get_google_site_url_prefix();
+ }
+
+ protected function is_google_domain_property(string $property = '') {
+ if ($property === '') {
+ $property = $this->get_google_selected_property();
+ }
+ return strpos($property, 'sc-domain:') === 0;
+ }
+
+ protected function get_google_verification_request_details(string $property = '') {
+ $property = $property !== '' ? $property : $this->get_google_selected_property();
+ if ($this->is_google_domain_property($property)) {
+ return [
+ 'identifier' => substr($property, strlen('sc-domain:')),
+ 'site_type' => 'INET_DOMAIN',
+ 'method' => 'DNS_TXT',
+ ];
}
return [
- 'start' => $start,
- 'end' => $end,
- 'days' => max(1, (int) floor((strtotime($end) - strtotime($start)) / DAY_IN_SECONDS) + 1),
- 'is_custom' => isset($_GET['start_date']) || isset($_GET['end_date']),
+ 'identifier' => $property,
+ 'site_type' => 'SITE',
+ 'method' => 'META',
];
}
- protected function build_single_stats_admin_url(string $target_url, string $start_date = '', string $end_date = '') {
+ protected function build_google_oauth_authorization_url() {
+ $settings = $this->get_settings();
+ if (empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
+ return '';
+ }
+
+ $state = wp_generate_password(20, false, false);
+ set_transient('ansico_stat_google_oauth_state_' . get_current_user_id(), $state, 15 * MINUTE_IN_SECONDS);
+
$args = [
- 'page' => self::MENU_SLUG_SINGLE,
- 'target_url' => $target_url !== '' ? $target_url : home_url('/'),
+ 'client_id' => $settings['google_client_id'],
+ 'redirect_uri' => $this->get_google_redirect_uri(),
+ 'response_type' => 'code',
+ 'scope' => implode(' ', $this->get_google_auth_scopes()),
+ 'access_type' => 'offline',
+ 'prompt' => 'consent',
+ 'include_granted_scopes' => 'true',
+ 'state' => $state,
];
- if ($start_date !== '') {
- $args['start_date'] = $start_date;
- }
- if ($end_date !== '') {
- $args['end_date'] = $end_date;
- }
- return add_query_arg($args, admin_url('admin.php'));
+
+ return add_query_arg($args, 'https://accounts.google.com/o/oauth2/v2/auth');
}
- public function register_frontend_admin_bar_link($wp_admin_bar) {
- if (is_admin() || !is_admin_bar_showing() || !current_user_can('manage_options') || !$this->is_trackable_request()) {
- return;
- }
+ protected function update_google_settings(array $changes) {
+ $settings = $this->get_settings();
+ $settings = array_merge($settings, $changes);
+ update_option(self::OPTION_KEY, $settings, false);
+ return $settings;
+ }
- $page_context = $this->get_current_page_context();
- if (empty($page_context) || empty($page_context['url'])) {
- return;
- }
-
- $target_url = $this->normalize_target_url((string) $page_context['url']);
- if ($target_url === '') {
- return;
- }
-
- $wp_admin_bar->add_node([
- 'id' => 'ansico-stat-single-page',
- 'parent' => 'top-secondary',
- 'title' => '
' . esc_html__('Page stats', 'ansico-stat-plugin') . '',
- 'href' => esc_url($this->build_single_stats_admin_url($target_url)),
- 'meta' => [
- 'class' => 'ansico-stat-admin-bar-link',
- 'title' => esc_attr__('Open statistics for this page', 'ansico-stat-plugin'),
- ],
+ protected function clear_google_connection_data() {
+ $this->update_google_settings([
+ 'google_access_token' => '',
+ 'google_refresh_token' => '',
+ 'google_token_expires_at' => 0,
+ 'google_connected_email' => '',
+ 'google_property' => '',
+ 'google_site_verification_token' => '',
+ 'google_last_sync_month' => '',
]);
}
- protected function resolve_single_stats_target(string $url) {
- global $wpdb;
-
- $normalized_url = $this->normalize_target_url($url);
- if ($normalized_url === '') {
- return [
- 'success' => false,
- 'message' => __('Please enter a valid URL.', 'ansico-stat-plugin'),
- 'requested_url' => $url,
- 'normalized_url' => '',
- ];
+ protected function get_google_access_token() {
+ $settings = $this->get_settings();
+ if (!empty($settings['google_access_token']) && (!empty($settings['google_token_expires_at']) && (int) $settings['google_token_expires_at'] > time() + 60)) {
+ return $settings['google_access_token'];
}
- $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
- $target_host = strtolower((string) wp_parse_url($normalized_url, PHP_URL_HOST));
- if ($site_host === '' || $target_host === '' || preg_replace('/^www\./', '', $site_host) !== preg_replace('/^www\./', '', $target_host)) {
- return [
- 'success' => false,
- 'message' => __('The URL must belong to this WordPress site.', 'ansico-stat-plugin'),
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- ];
+ if (empty($settings['google_refresh_token']) || empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
+ return '';
}
- $post_id = url_to_postid($normalized_url);
- if ($post_id > 0) {
- $post = get_post($post_id);
- if ($this->is_trackable_post($post)) {
- return [
- 'success' => true,
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- 'target' => [
- 'kind' => 'singular',
- 'post_id' => (int) $post_id,
- 'post_type' => sanitize_key((string) $post->post_type),
- 'page_key' => 'post:' . (int) $post_id,
- 'page_type' => sanitize_key((string) $post->post_type),
- 'object_id' => (int) $post_id,
- 'label' => get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin'),
- 'url' => get_permalink($post_id) ?: $normalized_url,
- 'resolved_via' => 'post',
- ],
- ];
- }
+ $response = wp_remote_post('https://oauth2.googleapis.com/token', [
+ 'timeout' => 20,
+ 'body' => [
+ 'client_id' => $settings['google_client_id'],
+ 'client_secret' => $settings['google_client_secret'],
+ 'refresh_token' => $settings['google_refresh_token'],
+ 'grant_type' => 'refresh_token',
+ ],
+ ]);
+
+ if (is_wp_error($response)) {
+ return '';
}
- $front_url = $this->normalize_target_url(home_url('/'));
- if ($normalized_url === $front_url) {
- return [
- 'success' => true,
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- 'target' => [
- 'kind' => 'archive',
- 'page_key' => 'front_page',
- 'page_type' => 'front_page',
- 'object_id' => 0,
- 'label' => __('Front page', 'ansico-stat-plugin'),
- 'url' => home_url('/'),
- 'resolved_via' => 'front_page',
- ],
- ];
+ $code = (int) wp_remote_retrieve_response_code($response);
+ $body = json_decode((string) wp_remote_retrieve_body($response), true);
+ if ($code < 200 || $code >= 300 || !is_array($body) || empty($body['access_token'])) {
+ return '';
}
- $posts_page_id = (int) get_option('page_for_posts');
- if ($posts_page_id > 0) {
- $posts_page_url = $this->normalize_target_url((string) get_permalink($posts_page_id));
- if ($posts_page_url !== '' && $normalized_url === $posts_page_url) {
- return [
- 'success' => true,
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- 'target' => [
- 'kind' => 'archive',
- 'page_key' => 'home',
- 'page_type' => 'home',
- 'object_id' => $posts_page_id,
- 'label' => __('Posts page', 'ansico-stat-plugin'),
- 'url' => get_permalink($posts_page_id) ?: $normalized_url,
- 'resolved_via' => 'posts_page',
- ],
- ];
- }
- }
+ $this->update_google_settings([
+ 'google_access_token' => sanitize_text_field((string) $body['access_token']),
+ 'google_token_expires_at' => time() + max(60, (int) ($body['expires_in'] ?? 3600)),
+ ]);
- $page_daily_table = self::page_daily_table_name();
- $url_variants = array_values(array_unique(array_filter([
- $normalized_url,
- untrailingslashit($normalized_url),
- trailingslashit($normalized_url),
- ])));
-
- if (!empty($url_variants)) {
- $placeholders = implode(',', array_fill(0, count($url_variants), '%s'));
- $sql = "SELECT page_key, page_type, object_id, page_label, page_url\n FROM {$page_daily_table}\n WHERE page_url IN ({$placeholders})\n ORDER BY stat_date DESC\n LIMIT 1";
- $row = $wpdb->get_row($wpdb->prepare($sql, $url_variants), ARRAY_A);
- if (is_array($row) && !empty($row['page_key'])) {
- return [
- 'success' => true,
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- 'target' => [
- 'kind' => 'archive',
- 'page_key' => (string) $row['page_key'],
- 'page_type' => sanitize_key((string) ($row['page_type'] ?? 'archive')),
- 'object_id' => !empty($row['object_id']) ? (int) $row['object_id'] : 0,
- 'label' => sanitize_text_field((string) ($row['page_label'] ?? $normalized_url)),
- 'url' => !empty($row['page_url']) ? esc_url_raw((string) $row['page_url']) : $normalized_url,
- 'resolved_via' => 'page_daily',
- ],
- ];
- }
- }
-
- return [
- 'success' => false,
- 'message' => __('No tracked page or post was found for that URL yet. Try opening the page on the site first so the plugin can register it, and then update this report again.', 'ansico-stat-plugin'),
- 'requested_url' => $url,
- 'normalized_url' => $normalized_url,
- ];
+ return sanitize_text_field((string) $body['access_token']);
}
- protected function get_target_lifetime_views(array $target) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- return (int) get_post_meta((int) $target['post_id'], self::TOTAL_META_KEY, true);
+ protected function google_api_request($method, $url, $body = null, $access_token = '') {
+ if ($access_token === '') {
+ $access_token = $this->get_google_access_token();
+ }
+ if ($access_token === '') {
+ return new WP_Error('ansico_google_missing_token', __('Google access token is missing.', 'ansico-stat-plugin'));
}
- $table = self::page_daily_table_name();
- $views = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(views) FROM {$table} WHERE page_key = %s",
- (string) $target['page_key']
- ));
-
- return (int) $views;
- }
-
-
- protected function get_target_lifetime_unique_visitors(array $target) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $uniques = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d",
- (int) $target['post_id']
- ));
- return (int) $uniques;
- }
-
- $table = self::page_daily_table_name();
- $uniques = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s",
- (string) $target['page_key']
- ));
- return (int) $uniques;
- }
-
- protected function get_target_period_unique_visitors(array $target, string $start, string $end) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $uniques = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s",
- (int) $target['post_id'],
- $start,
- $end
- ));
- return (int) $uniques;
- }
-
- $table = self::page_daily_table_name();
- $uniques = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s",
- (string) $target['page_key'],
- $start,
- $end
- ));
- return (int) $uniques;
- }
-
- protected function get_target_daily_chart_data_between(array $target, string $start, string $end, string $metric = 'views') {
- global $wpdb;
-
- $metric = $metric === 'unique_visitors' ? 'unique_visitors' : 'views';
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (int) $target['post_id'],
- $start,
- $end
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (string) $target['page_key'],
- $start,
- $end
- ), ARRAY_A);
- }
-
- $indexed = [];
- foreach ((array) $rows as $row) {
- $indexed[(string) $row['stat_date']] = (int) $row['metric_value'];
- }
-
- $data = [];
- $cursor = strtotime($start);
- $end_ts = strtotime($end);
- while ($cursor <= $end_ts) {
- $date = gmdate('Y-m-d', $cursor);
- $data[] = [
- 'date' => $date,
- 'views' => $indexed[$date] ?? 0,
- ];
- $cursor = strtotime('+1 day', $cursor);
- }
-
- return $data;
- }
-
- protected function get_target_monthly_chart_data_between(array $target, string $start, string $end) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC",
- (int) $target['post_id'],
- $start,
- $end
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC",
- (string) $target['page_key'],
- $start,
- $end
- ), ARRAY_A);
- }
-
- $indexed = [];
- foreach ((array) $rows as $row) {
- $indexed[(string) $row['month_key']] = (int) $row['total_views'];
- }
-
- $data = [];
- $cursor = strtotime(gmdate('Y-m-01', strtotime($start)));
- $end_month = strtotime(gmdate('Y-m-01', strtotime($end)));
- while ($cursor <= $end_month) {
- $month_key = gmdate('Y-m', $cursor);
- $data[] = [
- 'label' => wp_date('M Y', $cursor),
- 'views' => $indexed[$month_key] ?? 0,
- ];
- $cursor = strtotime('+1 month', $cursor);
- }
-
- return $data;
- }
-
- protected function get_target_weekday_breakdown_between(array $target, string $start, string $end) {
- $daily_data = $this->get_target_daily_chart_data_between($target, $start, $end, 'views');
- $weekday_rows = [
- __('Mon', 'ansico-stat-plugin') => 0,
- __('Tue', 'ansico-stat-plugin') => 0,
- __('Wed', 'ansico-stat-plugin') => 0,
- __('Thu', 'ansico-stat-plugin') => 0,
- __('Fri', 'ansico-stat-plugin') => 0,
- __('Sat', 'ansico-stat-plugin') => 0,
- __('Sun', 'ansico-stat-plugin') => 0,
- ];
- $weekday_keys = array_keys($weekday_rows);
-
- foreach ($daily_data as $row) {
- $timestamp = strtotime((string) ($row['date'] ?? ''));
- if (!$timestamp) {
- continue;
- }
- $weekday_index = (int) gmdate('N', $timestamp) - 1;
- if (isset($weekday_keys[$weekday_index])) {
- $weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0);
- }
- }
-
- $data = [];
- foreach ($weekday_rows as $label => $views) {
- $data[] = [
- 'label' => $label,
- 'views' => (int) $views,
- ];
- }
-
- return $data;
- }
-
- protected function render_dual_chart_markup(array $views_data, array $unique_data, string $chart_id, string $aria_label = '') {
- if ($aria_label === '') {
- $aria_label = __('Views and unique visitors chart', 'ansico-stat-plugin');
- }
-
- $count = max(count($views_data), count($unique_data));
- if ($count < 1) {
- return '
' . esc_html__('No chart data available.', 'ansico-stat-plugin') . '
';
- }
-
- $values = [];
- foreach ($views_data as $row) {
- $values[] = (int) ($row['views'] ?? 0);
- }
- foreach ($unique_data as $row) {
- $values[] = (int) ($row['views'] ?? 0);
- }
- $max = max(1, !empty($values) ? max($values) : 1);
- $width = 920;
- $height = 240;
- $chart_top = 20;
- $chart_bottom = 185;
- $chart_left = 26;
- $chart_right = 830;
- $stepX = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : ($chart_right - $chart_left);
-
- $build_points = static function(array $series, string $series_label) use ($chart_top, $chart_bottom, $chart_left, $stepX, $max) {
- $points = [];
- foreach (array_values($series) as $index => $row) {
- $value = (int) ($row['views'] ?? 0);
- $x = $chart_left + ($stepX * $index);
- $y = $value > 0 ? $chart_top + (($chart_bottom - $chart_top) * (1 - ($value / $max))) : $chart_bottom;
- $label = (string) ($row['label'] ?? $row['date'] ?? '');
- $points[] = [
- 'x' => $x,
- 'y' => $y,
- 'label' => $label,
- 'value' => $value,
- 'series_label' => $series_label,
- ];
- }
- return $points;
- };
-
- $view_points = $build_points($views_data, __('Views', 'ansico-stat-plugin'));
- $unique_points = $build_points($unique_data, __('Unique visitors', 'ansico-stat-plugin'));
- $view_path = implode(' ', array_map(static function($point) {
- return $point['x'] . ',' . $point['y'];
- }, $view_points));
- $unique_path = implode(' ', array_map(static function($point) {
- return $point['x'] . ',' . $point['y'];
- }, $unique_points));
-
- ob_start();
- ?>
-
-
-
-
-
-
-
-
- date_days_ago($days - 1);
- $end = current_time('Y-m-d');
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, unique_visitors FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (int) $target['post_id'],
- $start,
- $end
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, unique_visitors FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (string) $target['page_key'],
- $start,
- $end
- ), ARRAY_A);
- }
-
- $indexed = [];
- foreach ((array) $rows as $row) {
- $indexed[(string) $row['stat_date']] = (int) $row['unique_visitors'];
- }
-
- $data = [];
- $cursor = strtotime($start);
- $end_ts = strtotime($end);
- while ($cursor <= $end_ts) {
- $date = gmdate('Y-m-d', $cursor);
- $data[] = [
- 'date' => $date,
- 'views' => $indexed[$date] ?? 0,
- ];
- $cursor = strtotime('+1 day', $cursor);
- }
-
- return $data;
- }
-
- protected function get_target_period_views(array $target, string $start, string $end) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $views = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(views) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s",
- (int) $target['post_id'],
- $start,
- $end
- ));
- return (int) $views;
- }
-
- $table = self::page_daily_table_name();
- $views = $wpdb->get_var($wpdb->prepare(
- "SELECT SUM(views) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s",
- (string) $target['page_key'],
- $start,
- $end
- ));
- return (int) $views;
- }
-
- protected function get_target_daily_chart_data(array $target, int $days = 60) {
- global $wpdb;
-
- $days = max(1, $days);
- $start = $this->date_days_ago($days - 1);
- $end = current_time('Y-m-d');
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (int) $target['post_id'],
- $start,
- $end
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT stat_date, views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
- (string) $target['page_key'],
- $start,
- $end
- ), ARRAY_A);
- }
-
- $indexed = [];
- foreach ((array) $rows as $row) {
- $indexed[(string) $row['stat_date']] = (int) $row['views'];
- }
-
- $data = [];
- $cursor = strtotime($start);
- $end_ts = strtotime($end);
- while ($cursor <= $end_ts) {
- $date = gmdate('Y-m-d', $cursor);
- $data[] = [
- 'date' => $date,
- 'views' => $indexed[$date] ?? 0,
- ];
- $cursor = strtotime('+1 day', $cursor);
- }
-
- return $data;
- }
-
- protected function get_target_monthly_chart_data(array $target, int $months = 12) {
- global $wpdb;
-
- $months = max(1, $months);
- $current_month = strtotime(wp_date('Y-m-01', current_time('timestamp')));
- $start_month = strtotime('-' . ($months - 1) . ' months', $current_month);
- $start = wp_date('Y-m-01', $start_month);
- $end = wp_date('Y-m-t', current_time('timestamp'));
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE post_id = %d AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC",
- (int) $target['post_id'],
- $start,
- $end
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $rows = $wpdb->get_results($wpdb->prepare(
- "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE page_key = %s AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC",
- (string) $target['page_key'],
- $start,
- $end
- ), ARRAY_A);
- }
-
- $indexed = [];
- foreach ((array) $rows as $row) {
- $indexed[(string) $row['month_key']] = (int) $row['total_views'];
- }
-
- $data = [];
- $cursor = $start_month;
- while ($cursor <= $current_month) {
- $month_key = wp_date('Y-m', $cursor);
- $data[] = [
- 'label' => wp_date('M Y', $cursor),
- 'views' => $indexed[$month_key] ?? 0,
- ];
- $cursor = strtotime('+1 month', $cursor);
- }
-
- return $data;
- }
-
- protected function get_target_best_day(array $target) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $row = $wpdb->get_row($wpdb->prepare(
- "SELECT stat_date, views FROM {$table} WHERE post_id = %d ORDER BY views DESC, stat_date ASC LIMIT 1",
- (int) $target['post_id']
- ), ARRAY_A);
- } else {
- $table = self::page_daily_table_name();
- $row = $wpdb->get_row($wpdb->prepare(
- "SELECT stat_date, views FROM {$table} WHERE page_key = %s ORDER BY views DESC, stat_date ASC LIMIT 1",
- (string) $target['page_key']
- ), ARRAY_A);
- }
-
- return is_array($row) ? $row : [];
- }
-
- protected function get_target_first_tracked_date(array $target) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $date = $wpdb->get_var($wpdb->prepare(
- "SELECT MIN(stat_date) FROM {$table} WHERE post_id = %d",
- (int) $target['post_id']
- ));
- } else {
- $table = self::page_daily_table_name();
- $date = $wpdb->get_var($wpdb->prepare(
- "SELECT MIN(stat_date) FROM {$table} WHERE page_key = %s",
- (string) $target['page_key']
- ));
- }
-
- return is_string($date) ? $date : '';
- }
-
-
- protected function get_target_total_tracked_days(array $target) {
- global $wpdb;
-
- if (($target['kind'] ?? '') === 'singular') {
- $table = self::post_daily_table_name();
- $count = $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$table} WHERE post_id = %d",
- (int) $target['post_id']
- ));
- } else {
- $table = self::page_daily_table_name();
- $count = $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$table} WHERE page_key = %s",
- (string) $target['page_key']
- ));
- }
-
- return (int) $count;
- }
-
-
- protected function get_target_average_unique_visitors_per_day(array $target, int $days) {
- $days = max(1, $days);
- $uniques = $this->get_target_period_unique_visitors($target, $this->date_days_ago($days - 1), current_time('Y-m-d'));
- return $uniques / $days;
- }
-
- protected function get_target_unique_period_comparison(array $target, int $days) {
- $days = max(1, $days);
- $current_end = current_time('Y-m-d');
- $current_start = $this->date_days_ago($days - 1);
- $previous_end = $this->date_days_ago($days);
- $previous_start = $this->date_days_ago(($days * 2) - 1);
-
- $current_uniques = $this->get_target_period_unique_visitors($target, $current_start, $current_end);
- $previous_uniques = $this->get_target_period_unique_visitors($target, $previous_start, $previous_end);
- $change = $current_uniques - $previous_uniques;
- $percent_change = null;
- if ($previous_uniques > 0) {
- $percent_change = (($current_uniques - $previous_uniques) / $previous_uniques) * 100;
- } elseif ($current_uniques > 0) {
- $percent_change = 100.0;
- }
-
- return [
- 'days' => $days,
- 'current_views' => (int) $current_uniques,
- 'previous_views' => (int) $previous_uniques,
- 'change' => (int) $change,
- 'percent_change' => $percent_change,
- 'current_start' => $current_start,
- 'current_end' => $current_end,
- 'previous_start' => $previous_start,
- 'previous_end' => $previous_end,
- ];
- }
-
- protected function get_target_average_views_per_day(array $target, int $days) {
- $days = max(1, $days);
- $views = $this->get_target_period_views($target, $this->date_days_ago($days - 1), current_time('Y-m-d'));
- return $views / $days;
- }
-
- protected function get_target_period_comparison(array $target, int $days) {
- $days = max(1, $days);
- $current_end = current_time('Y-m-d');
- $current_start = $this->date_days_ago($days - 1);
- $previous_end = $this->date_days_ago($days);
- $previous_start = $this->date_days_ago(($days * 2) - 1);
-
- $current_views = $this->get_target_period_views($target, $current_start, $current_end);
- $previous_views = $this->get_target_period_views($target, $previous_start, $previous_end);
- $change = $current_views - $previous_views;
- $percent_change = null;
- if ($previous_views > 0) {
- $percent_change = (($current_views - $previous_views) / $previous_views) * 100;
- } elseif ($current_views > 0) {
- $percent_change = 100.0;
- }
-
- return [
- 'days' => $days,
- 'current_views' => (int) $current_views,
- 'previous_views' => (int) $previous_views,
- 'change' => (int) $change,
- 'percent_change' => $percent_change,
- 'current_start' => $current_start,
- 'current_end' => $current_end,
- 'previous_start' => $previous_start,
- 'previous_end' => $previous_end,
- ];
- }
-
- protected function format_change_percent($percent_change) {
- if ($percent_change === null) {
- return __('New data', 'ansico-stat-plugin');
- }
-
- $prefix = $percent_change > 0 ? '+' : '';
- return $prefix . number_format_i18n((float) $percent_change, 1) . '%';
- }
-
- protected function get_target_weekday_breakdown(array $target, int $days = 90) {
- $days = max(1, $days);
- $daily_data = $this->get_target_daily_chart_data($target, $days);
- $weekday_rows = [
- __('Mon', 'ansico-stat-plugin') => 0,
- __('Tue', 'ansico-stat-plugin') => 0,
- __('Wed', 'ansico-stat-plugin') => 0,
- __('Thu', 'ansico-stat-plugin') => 0,
- __('Fri', 'ansico-stat-plugin') => 0,
- __('Sat', 'ansico-stat-plugin') => 0,
- __('Sun', 'ansico-stat-plugin') => 0,
- ];
- $weekday_keys = array_keys($weekday_rows);
-
- foreach ($daily_data as $row) {
- $timestamp = strtotime((string) ($row['date'] ?? ''));
- if (!$timestamp) {
- continue;
- }
- $weekday_index = (int) gmdate('N', $timestamp) - 1;
- if (!isset($weekday_keys[$weekday_index])) {
- continue;
- }
- $weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0);
- }
-
- $data = [];
- foreach ($weekday_rows as $label => $views) {
- $data[] = [
- 'label' => $label,
- 'views' => (int) $views,
- ];
- }
-
- return $data;
- }
-
- protected function build_single_export_url(string $target_url, string $start_date = '', string $end_date = '') {
$args = [
- 'action' => 'ansico_stat_export_single_csv',
- 'target_url' => $target_url,
+ 'method' => strtoupper($method),
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $access_token,
+ 'Accept' => 'application/json',
+ ],
];
- if ($start_date !== '') {
- $args['start_date'] = $start_date;
- }
- if ($end_date !== '') {
- $args['end_date'] = $end_date;
+
+ if ($body !== null) {
+ $args['headers']['Content-Type'] = 'application/json';
+ $args['body'] = wp_json_encode($body);
}
- return wp_nonce_url(
- add_query_arg($args, admin_url('admin-post.php')),
- 'ansico_stat_export_single_csv'
- );
- }
+ $response = wp_remote_request($url, $args);
+ if (is_wp_error($response)) {
+ return $response;
+ }
- protected function render_single_stats_summary_cards(array $items) {
- echo '
';
- foreach ($items as $item) {
- echo '
';
- echo '
' . esc_html((string) ($item['label'] ?? '')) . '
';
- echo '
' . esc_html((string) ($item['value'] ?? '')) . '
';
- if (!empty($item['description'])) {
- echo '
' . esc_html((string) $item['description']) . '
';
+ $code = (int) wp_remote_retrieve_response_code($response);
+ $raw_body = (string) wp_remote_retrieve_body($response);
+ $decoded = $raw_body !== '' ? json_decode($raw_body, true) : [];
+ if ($code < 200 || $code >= 300) {
+ $message = __('Google API request failed.', 'ansico-stat-plugin');
+ if (is_array($decoded) && !empty($decoded['error']['message'])) {
+ $message = sanitize_text_field((string) $decoded['error']['message']);
}
- echo '
';
+ return new WP_Error('ansico_google_api_error', $message, ['status' => $code, 'body' => $decoded]);
}
- echo '
';
+
+ return is_array($decoded) ? $decoded : [];
}
- public function render_single_page_stats_page() {
+ protected function get_google_sites() {
+ $result = $this->google_api_request('GET', 'https://www.googleapis.com/webmasters/v3/sites');
+ if (is_wp_error($result)) {
+ return $result;
+ }
+ return is_array($result['siteEntry'] ?? null) ? $result['siteEntry'] : [];
+ }
+
+ protected function get_google_selected_site_permission() {
+ $site_url = $this->get_google_selected_property();
+ $sites = $this->get_google_sites();
+ if (is_wp_error($sites)) {
+ return '';
+ }
+ foreach ($sites as $site) {
+ if (($site['siteUrl'] ?? '') === $site_url) {
+ return sanitize_text_field((string) ($site['permissionLevel'] ?? ''));
+ }
+ }
+ return '';
+ }
+
+
+ protected function get_google_api_diagnostic_message($error, string $api_label) {
+ if (!is_wp_error($error)) {
+ return '';
+ }
+
+ $message = (string) $error->get_error_message();
+ $lower = strtolower($message);
+ if (strpos($lower, 'access not configured') !== false || strpos($lower, 'has not been used in project') !== false || strpos($lower, 'api has not been used') !== false) {
+ return sprintf(__('The %s is probably not enabled in Google Cloud yet.', 'ansico-stat-plugin'), $api_label);
+ }
+ if (strpos($lower, 'insufficient authentication scopes') !== false || strpos($lower, 'request had insufficient authentication scopes') !== false) {
+ return __('Reconnect with Google so the plugin can request the required permissions again.', 'ansico-stat-plugin');
+ }
+ if (strpos($lower, 'forbidden') !== false || strpos($lower, 'permission') !== false) {
+ return __('Google accepted the login, but the connected account does not currently have permission for this action.', 'ansico-stat-plugin');
+ }
+ if (strpos($lower, 'invalid client') !== false || strpos($lower, 'deleted_client') !== false) {
+ return __('The OAuth client ID or secret appears invalid. Recheck the credentials in Google Cloud.', 'ansico-stat-plugin');
+ }
+ if (strpos($lower, 'redirect_uri_mismatch') !== false) {
+ return __('The redirect URI in Google Cloud does not match the redirect URI shown in this plugin.', 'ansico-stat-plugin');
+ }
+ return $message !== '' ? $message : __('The Google API returned an unknown error.', 'ansico-stat-plugin');
+ }
+
+ protected function get_google_setup_diagnostics() {
+ $settings = $this->get_settings();
+ $diagnostics = [];
+
+ $diagnostics[] = [
+ 'label' => __('OAuth client credentials', 'ansico-stat-plugin'),
+ 'status' => (!empty($settings['google_client_id']) && !empty($settings['google_client_secret'])) ? 'success' : 'error',
+ 'message' => (!empty($settings['google_client_id']) && !empty($settings['google_client_secret']))
+ ? __('Client ID and client secret are saved.', 'ansico-stat-plugin')
+ : __('Add and save both the Google OAuth client ID and client secret first.', 'ansico-stat-plugin'),
+ ];
+
+ if (!$this->is_google_connected()) {
+ $diagnostics[] = [
+ 'label' => __('OAuth connection', 'ansico-stat-plugin'),
+ 'status' => 'warning',
+ 'message' => __('Not connected yet. If the Connect button fails before returning here, check the OAuth consent screen and authorized redirect URI in Google Cloud.', 'ansico-stat-plugin'),
+ ];
+ $diagnostics[] = [
+ 'label' => __('Search Console API', 'ansico-stat-plugin'),
+ 'status' => 'warning',
+ 'message' => __('This can be tested after you connect Google.', 'ansico-stat-plugin'),
+ ];
+ $diagnostics[] = [
+ 'label' => __('Site Verification API', 'ansico-stat-plugin'),
+ 'status' => 'warning',
+ 'message' => __('This can be tested after you connect Google.', 'ansico-stat-plugin'),
+ ];
+ return $diagnostics;
+ }
+
+ $access_token = $this->get_google_access_token();
+ if ($access_token === '') {
+ $diagnostics[] = [
+ 'label' => __('OAuth connection', 'ansico-stat-plugin'),
+ 'status' => 'error',
+ 'message' => __('The plugin is marked as connected, but it could not refresh the Google access token. Try reconnecting with Google.', 'ansico-stat-plugin'),
+ ];
+ return $diagnostics;
+ }
+
+ $diagnostics[] = [
+ 'label' => __('OAuth connection', 'ansico-stat-plugin'),
+ 'status' => 'success',
+ 'message' => !empty($settings['google_connected_email'])
+ ? sprintf(__('Connected as %s.', 'ansico-stat-plugin'), $settings['google_connected_email'])
+ : __('Connected and the access token is working.', 'ansico-stat-plugin'),
+ ];
+
+ $sites_result = $this->google_api_request('GET', 'https://www.googleapis.com/webmasters/v3/sites', null, $access_token);
+ $diagnostics[] = [
+ 'label' => __('Search Console API', 'ansico-stat-plugin'),
+ 'status' => is_wp_error($sites_result) ? 'error' : 'success',
+ 'message' => is_wp_error($sites_result)
+ ? $this->get_google_api_diagnostic_message($sites_result, __('Search Console API', 'ansico-stat-plugin'))
+ : __('The plugin can list Search Console properties successfully.', 'ansico-stat-plugin'),
+ ];
+
+ $verification_result = $this->google_api_request('GET', 'https://www.googleapis.com/siteVerification/v1/webResource', null, $access_token);
+ $diagnostics[] = [
+ 'label' => __('Site Verification API', 'ansico-stat-plugin'),
+ 'status' => is_wp_error($verification_result) ? 'error' : 'success',
+ 'message' => is_wp_error($verification_result)
+ ? $this->get_google_api_diagnostic_message($verification_result, __('Site Verification API', 'ansico-stat-plugin'))
+ : __('The plugin can reach the Site Verification API successfully.', 'ansico-stat-plugin'),
+ ];
+
+ $property = $this->get_google_selected_property();
+ $diagnostics[] = [
+ 'label' => __('Selected property', 'ansico-stat-plugin'),
+ 'status' => $property !== '' ? 'success' : 'warning',
+ 'message' => $property !== ''
+ ? sprintf(__('Current property: %s', 'ansico-stat-plugin'), $property)
+ : __('Choose a Search Console property and save the settings.', 'ansico-stat-plugin'),
+ ];
+
+ return $diagnostics;
+ }
+
+ protected function render_google_setup_diagnostics() {
+ $diagnostics = $this->get_google_setup_diagnostics();
+ $status_labels = [
+ 'success' => __('OK', 'ansico-stat-plugin'),
+ 'warning' => __('Check', 'ansico-stat-plugin'),
+ 'error' => __('Problem', 'ansico-stat-plugin'),
+ ];
+
+ echo '
';
+ echo '| ' . esc_html__('Check', 'ansico-stat-plugin') . ' | ' . esc_html__('Status', 'ansico-stat-plugin') . ' | ' . esc_html__('Details', 'ansico-stat-plugin') . ' |
';
+ foreach ($diagnostics as $row) {
+ $status = (string) ($row['status'] ?? 'warning');
+ $label = (string) ($status_labels[$status] ?? $status_labels['warning']);
+ $color = $status === 'success' ? '#2271b1' : ($status === 'error' ? '#b32d2e' : '#996800');
+ echo '';
+ echo '| ' . esc_html((string) ($row['label'] ?? '')) . ' | ';
+ echo '' . esc_html($label) . ' | ';
+ echo '' . esc_html((string) ($row['message'] ?? '')) . ' | ';
+ echo '
';
+ }
+ echo '
';
+ }
+
+ protected function get_google_site_verification_status() {
+ $settings = $this->get_settings();
+ if (!$this->is_google_connected()) {
+ return __('Not connected', 'ansico-stat-plugin');
+ }
+ $permission = $this->get_google_selected_site_permission();
+ if ($permission !== '') {
+ return sprintf(__('Verified in Search Console (%s)', 'ansico-stat-plugin'), $permission);
+ }
+ if (!empty($settings['google_site_verification_token'])) {
+ if ($this->is_google_domain_property()) {
+ return __('DNS verification token generated, waiting for DNS verification', 'ansico-stat-plugin');
+ }
+ return __('Verification token generated, waiting for verification', 'ansico-stat-plugin');
+ }
+ return __('Not verified in Search Console yet', 'ansico-stat-plugin');
+ }
+
+ protected function get_google_search_console_help_text() {
+ return __('Use the Connect with Google button below for a Site Kit-style OAuth flow. First save your Google client ID and client secret, then connect, choose a property, verify ownership, and sync the selected month of search queries.', 'ansico-stat-plugin');
+ }
+
+ protected function get_google_connection_help_text() {
+ if ($this->is_google_connected()) {
+ return __('Google is already connected. You can reconnect to refresh permissions or disconnect and connect a different Google account.', 'ansico-stat-plugin');
+ }
+
+ return __('After you save the client ID and client secret, click Connect with Google. WordPress will send you to Google, and the access and refresh tokens will be stored automatically when you return.', 'ansico-stat-plugin');
+ }
+
+ public function render_google_site_verification_meta_tag() {
+ $settings = $this->get_settings();
+ if (empty($settings['google_site_verification_token']) || is_admin()) {
+ return;
+ }
+ echo "
+" . '
' . "
+";
+ }
+
+ public function handle_google_connect() {
if (!current_user_can('manage_options')) {
- wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin'));
+ wp_die(esc_html__('You do not have permission to connect Google Search Console.', 'ansico-stat-plugin'));
+ }
+ check_admin_referer('ansico_stat_google_connect');
+ $url = $this->build_google_oauth_authorization_url();
+ if ($url === '') {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=missing-credentials'));
+ exit;
+ }
+ wp_safe_redirect($url);
+ exit;
+ }
+
+ public function handle_google_disconnect() {
+ if (!current_user_can('manage_options')) {
+ wp_die(esc_html__('You do not have permission to disconnect Google Search Console.', 'ansico-stat-plugin'));
+ }
+ check_admin_referer('ansico_stat_google_disconnect');
+ $this->clear_google_connection_data();
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=disconnected'));
+ exit;
+ }
+
+ public function handle_google_oauth_callback() {
+ if (!current_user_can('manage_options')) {
+ wp_die(esc_html__('You do not have permission to complete Google authentication.', 'ansico-stat-plugin'));
}
- $requested_url = $this->get_requested_single_stats_url();
- $date_range = $this->get_single_stats_date_range();
- $range_start = (string) $date_range['start'];
- $range_end = (string) $date_range['end'];
- $range_days = (int) $date_range['days'];
- $result = $this->resolve_single_stats_target($requested_url);
- $target = !empty($result['success']) ? (array) $result['target'] : [];
- $daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'views') : [];
- $unique_daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'unique_visitors') : [];
- $monthly_chart = !empty($target) ? $this->get_target_monthly_chart_data_between($target, $range_start, $range_end) : [];
- $weekday_chart = !empty($target) ? $this->get_target_weekday_breakdown_between($target, $range_start, $range_end) : [];
- $lifetime_views = !empty($target) ? $this->get_target_lifetime_views($target) : 0;
- $lifetime_unique_visitors = !empty($target) ? $this->get_target_lifetime_unique_visitors($target) : 0;
- $views_today = !empty($target) ? $this->get_target_period_views($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0;
- $unique_today = !empty($target) ? $this->get_target_period_unique_visitors($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0;
- $range_views = !empty($target) ? $this->get_target_period_views($target, $range_start, $range_end) : 0;
- $range_unique = !empty($target) ? $this->get_target_period_unique_visitors($target, $range_start, $range_end) : 0;
- $best_day = !empty($target) ? $this->get_target_best_day($target) : [];
- $first_tracked = !empty($target) ? $this->get_target_first_tracked_date($target) : '';
- $tracked_days = !empty($target) ? $this->get_target_total_tracked_days($target) : 0;
- $avg_range = $range_days > 0 ? ($range_views / $range_days) : 0;
- $avg_unique_range = $range_days > 0 ? ($range_unique / $range_days) : 0;
- $compare_range = !empty($target) ? $this->get_target_period_comparison($target, $range_days) : [];
- $unique_compare_range = !empty($target) ? $this->get_target_unique_period_comparison($target, $range_days) : [];
- $views_7 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0;
- $views_30 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0;
- $unique_7 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0;
- $unique_30 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0;
- $export_url = !empty($target) ? $this->build_single_export_url($requested_url, $range_start, $range_end) : '';
- $reset_range_url = $this->build_single_stats_admin_url($requested_url);
- $post_type_label = '';
- if (!empty($target) && ($target['kind'] ?? '') === 'singular') {
- $post_type_obj = get_post_type_object((string) ($target['post_type'] ?? ''));
- $post_type_label = $post_type_obj && !empty($post_type_obj->labels->singular_name) ? (string) $post_type_obj->labels->singular_name : (string) ($target['post_type'] ?? '');
+ $state = isset($_GET['state']) ? sanitize_text_field(wp_unslash($_GET['state'])) : '';
+ $saved_state = get_transient('ansico_stat_google_oauth_state_' . get_current_user_id());
+ delete_transient('ansico_stat_google_oauth_state_' . get_current_user_id());
+ if ($state === '' || !$saved_state || !hash_equals((string) $saved_state, $state)) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=invalid-state'));
+ exit;
}
+
+ if (!empty($_GET['error'])) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-denied'));
+ exit;
+ }
+
+ $code = isset($_GET['code']) ? sanitize_text_field(wp_unslash($_GET['code'])) : '';
+ $settings = $this->get_settings();
+ if ($code === '' || empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
+ exit;
+ }
+
+ $response = wp_remote_post('https://oauth2.googleapis.com/token', [
+ 'timeout' => 20,
+ 'body' => [
+ 'code' => $code,
+ 'client_id' => $settings['google_client_id'],
+ 'client_secret' => $settings['google_client_secret'],
+ 'redirect_uri' => $this->get_google_redirect_uri(),
+ 'grant_type' => 'authorization_code',
+ ],
+ ]);
+ if (is_wp_error($response)) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
+ exit;
+ }
+ $body = json_decode((string) wp_remote_retrieve_body($response), true);
+ if ((int) wp_remote_retrieve_response_code($response) < 200 || (int) wp_remote_retrieve_response_code($response) >= 300 || !is_array($body) || empty($body['access_token'])) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
+ exit;
+ }
+
+ $email = '';
+ if (!empty($body['id_token'])) {
+ $parts = explode('.', (string) $body['id_token']);
+ if (count($parts) === 3) {
+ $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
+ if (is_array($payload) && !empty($payload['email'])) {
+ $email = sanitize_email((string) $payload['email']);
+ }
+ }
+ }
+
+ $this->update_google_settings([
+ 'google_access_token' => sanitize_text_field((string) $body['access_token']),
+ 'google_refresh_token' => !empty($body['refresh_token']) ? sanitize_text_field((string) $body['refresh_token']) : $settings['google_refresh_token'],
+ 'google_token_expires_at' => time() + max(60, (int) ($body['expires_in'] ?? 3600)),
+ 'google_connected_email' => $email,
+ ]);
+
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=connected'));
+ exit;
+ }
+
+ public function handle_google_generate_verification_token() {
+ if (!current_user_can('manage_options')) {
+ wp_die(esc_html__('You do not have permission to generate a Google verification token.', 'ansico-stat-plugin'));
+ }
+ check_admin_referer('ansico_stat_google_generate_verification_token');
+
+ $verification_property = $this->get_google_auto_verification_property();
+ $details = $this->get_google_verification_request_details($verification_property);
+ $result = $this->google_api_request('POST', 'https://www.googleapis.com/siteVerification/v1/token', [
+ 'site' => [
+ 'type' => $details['site_type'],
+ 'identifier' => $details['identifier'],
+ ],
+ 'verificationMethod' => $details['method'],
+ ]);
+ if (is_wp_error($result) || empty($result['token'])) {
+ $message = is_wp_error($result) ? $result->get_error_message() : __('Could not generate a Google site verification token.', 'ansico-stat-plugin');
+ set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
+ 'type' => 'error',
+ 'message' => $message,
+ ], MINUTE_IN_SECONDS * 5);
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=token-failed'));
+ exit;
+ }
+
+ $this->update_google_settings([
+ 'google_site_verification_token' => sanitize_text_field((string) $result['token']),
+ 'google_site_verification_method' => sanitize_key((string) $details['method']),
+ ]);
+ set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
+ 'type' => 'success',
+ 'message' => __('Google site verification token generated for the URL-prefix property.', 'ansico-stat-plugin'),
+ ], MINUTE_IN_SECONDS * 5);
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=token-generated'));
+ exit;
+ }
+
+ public function handle_google_verify_site() {
+ if (!current_user_can('manage_options')) {
+ wp_die(esc_html__('You do not have permission to verify the site in Google Search Console.', 'ansico-stat-plugin'));
+ }
+ check_admin_referer('ansico_stat_google_verify_site');
+
+ $property = $this->get_google_auto_verification_property();
+ $details = $this->get_google_verification_request_details($property);
+ $result = $this->google_api_request('PUT', 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property), null);
+ if (is_wp_error($result) && !in_array($result->get_error_code(), ['http_409', 'http_201'], true)) {
+ wp_safe_redirect(add_query_arg([
+ 'page' => 'ansico-stat-plugin',
+ 'tab' => 'settings',
+ 'google_status' => 'verify-failed',
+ 'google_message' => rawurlencode($result->get_error_message()),
+ ], admin_url('admin.php')));
+ exit;
+ }
+ $result = $this->google_api_request('POST', 'https://www.googleapis.com/siteVerification/v1/webResource?verificationMethod=' . rawurlencode($details['method']), [
+ 'site' => [
+ 'type' => $details['site_type'],
+ 'identifier' => $details['identifier'],
+ ],
+ ]);
+
+ if (is_wp_error($result)) {
+ set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
+ 'type' => 'error',
+ 'message' => $result->get_error_message(),
+ ], MINUTE_IN_SECONDS * 5);
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=verify-failed'));
+ exit;
+ }
+
+ // Try to add the verified property to Search Console.
+ $this->google_api_request('PUT', 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property), null);
+ $this->update_google_settings([
+ 'google_property' => $property,
+ ]);
+
+ set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
+ 'type' => 'success',
+ 'message' => __('Site verification completed.', 'ansico-stat-plugin'),
+ ], MINUTE_IN_SECONDS * 5);
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=verified'));
+ exit;
+ }
+
+
+
+ protected function get_google_available_properties() {
+ $sites = $this->get_google_sites();
+ if (is_wp_error($sites)) {
+ return [];
+ }
+
+ $properties = [];
+ foreach ($sites as $site) {
+ $site_url = sanitize_text_field((string) ($site['siteUrl'] ?? ''));
+ if ($site_url === '') {
+ continue;
+ }
+ $properties[] = [
+ 'siteUrl' => $site_url,
+ 'permissionLevel' => sanitize_text_field((string) ($site['permissionLevel'] ?? '')),
+ ];
+ }
+
+ usort($properties, function ($a, $b) {
+ return strcmp($a['siteUrl'], $b['siteUrl']);
+ });
+
+ return $properties;
+ }
+
+ protected function get_month_date_range(string $month_key) {
+ $month_key = preg_match('/^\d{4}-\d{2}$/', $month_key) ? $month_key : wp_date('Y-m', current_time('timestamp'));
+ $start = $month_key . '-01';
+ $end = wp_date('Y-m-t', strtotime($start));
+ return [$start, $end];
+ }
+
+ protected function sync_google_search_queries_for_month(string $month_key, int $row_limit = 50) {
+ global $wpdb;
+
+ $settings = $this->get_settings();
+ $property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
+ if ($property === '') {
+ return new WP_Error('ansico_google_missing_property', __('No Google Search Console property has been selected yet.', 'ansico-stat-plugin'));
+ }
+
+ [$start_date, $end_date] = $this->get_month_date_range($month_key);
+ $endpoint = 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property) . '/searchAnalytics/query';
+ $result = $this->google_api_request('POST', $endpoint, [
+ 'startDate' => $start_date,
+ 'endDate' => $end_date,
+ 'dimensions' => ['query'],
+ 'rowLimit' => max(1, min(250, $row_limit)),
+ 'dataState' => 'final',
+ ]);
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ $table = self::search_query_table_name();
+ $wpdb->delete($table, [
+ 'month_key' => $month_key,
+ 'property_url' => $property,
+ ], ['%s', '%s']);
+
+ $rows = is_array($result['rows'] ?? null) ? $result['rows'] : [];
+ $timestamp = current_time('mysql');
+ foreach ($rows as $row) {
+ $query = sanitize_text_field((string) (($row['keys'][0] ?? '')));
+ if ($query === '') {
+ continue;
+ }
+ $wpdb->replace($table, [
+ 'month_key' => $month_key,
+ 'property_url' => $property,
+ 'query_text' => mb_substr($query, 0, 255),
+ 'clicks' => max(0, (int) round((float) ($row['clicks'] ?? 0))),
+ 'impressions' => max(0, (int) round((float) ($row['impressions'] ?? 0))),
+ 'ctr' => (float) ($row['ctr'] ?? 0),
+ 'position' => (float) ($row['position'] ?? 0),
+ 'updated_at' => $timestamp,
+ ], ['%s', '%s', '%s', '%d', '%d', '%f', '%f', '%s']);
+ }
+
+ $this->update_google_settings([
+ 'google_last_sync_month' => $month_key,
+ ]);
+
+ return count($rows);
+ }
+
+ protected function get_google_search_queries_for_month(string $month_key, int $limit = 50) {
+ global $wpdb;
+ $settings = $this->get_settings();
+ $property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
+ if ($property === '') {
+ return [];
+ }
+
+ $table = self::search_query_table_name();
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT query_text, clicks, impressions, ctr, position, updated_at
+ FROM {$table}
+ WHERE month_key = %s AND property_url = %s
+ ORDER BY clicks DESC, impressions DESC, query_text ASC
+ LIMIT %d",
+ $month_key,
+ $property,
+ max(1, min(250, $limit))
+ ), ARRAY_A) ?: [];
+ }
+
+ protected function render_google_search_queries_table(array $rows, string $empty_message = '') {
+ if ($empty_message === '') {
+ $empty_message = __('No Google Search Console query data has been imported for this month yet.', 'ansico-stat-plugin');
+ }
+
+ if (empty($rows)) {
+ echo '
' . esc_html($empty_message) . '
';
+ return;
+ }
+ $opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
-
-
-
-
-
-
-
-
-
-
-
- render_single_stats_summary_cards([
- [
- 'label' => __('Views in selected period', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($range_views),
- 'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_range, 1)),
- ],
- [
- 'label' => __('Unique visitors in selected period', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($range_unique),
- 'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_unique_range, 1)),
- ],
- [
- 'label' => __('Views today', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($views_today),
- 'description' => __('Today only', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Unique visitors today', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($unique_today),
- 'description' => __('Today only', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Views last 7 days', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($views_7),
- 'description' => __('Rolling 7-day total', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Unique visitors last 7 days', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($unique_7),
- 'description' => __('Rolling 7-day total', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Views last 30 days', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($views_30),
- 'description' => __('Rolling 30-day total', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Unique visitors last 30 days', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($unique_30),
- 'description' => __('Rolling 30-day total', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Lifetime views', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($lifetime_views),
- 'description' => $first_tracked !== '' ? sprintf(__('Tracked since %s', 'ansico-stat-plugin'), wp_date(get_option('date_format'), strtotime($first_tracked))) : '',
- ],
- [
- 'label' => __('Lifetime unique visitors', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($lifetime_unique_visitors),
- 'description' => __('Stored from version 1.0.0.6 and forward', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Best day', 'ansico-stat-plugin'),
- 'value' => !empty($best_day['views']) ? number_format_i18n((int) $best_day['views']) : '0',
- 'description' => !empty($best_day['stat_date']) ? wp_date(get_option('date_format'), strtotime((string) $best_day['stat_date'])) : __('No tracked data yet', 'ansico-stat-plugin'),
- ],
- [
- 'label' => __('Tracked days with views', 'ansico-stat-plugin'),
- 'value' => number_format_i18n($tracked_days),
- 'description' => __('Days stored in the daily statistics table', 'ansico-stat-plugin'),
- ],
- ]); ?>
-
-
-
-
-
- render_dual_chart_markup($daily_chart, $unique_daily_chart, 'ansico-single-target-dual-chart', __('Views and unique visitors for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-
-
-
-
- render_chart_markup($daily_chart, 'ansico-single-target-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-
-
-
-
- render_chart_markup($unique_daily_chart, 'ansico-single-target-unique-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-
-
-
-
- render_bar_chart_markup($monthly_chart, 'ansico-single-target-monthly-chart', __('Monthly totals for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-
-
-
-
- render_bar_chart_markup($weekday_chart, 'ansico-single-target-weekday-chart', __('Views by weekday for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-
-
-
-
-
-
-
- |
- |
- |
- |
- |
-
-
-
-
- |
- |
- |
- |
- format_change_percent($compare_range['percent_change'] ?? null)); ?> |
-
-
- |
- |
- |
- |
- format_change_percent($unique_compare_range['percent_change'] ?? null)); ?> |
-
-
-
-
-
-
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+ $row) : ?>
+
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ maybe_close_scrollable_table_wrapper($opened_scroll); ?>
sync_google_search_queries_for_month($month);
+ if (is_wp_error($result)) {
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_STATS . '&month=' . rawurlencode($month) . '&google-sync=failed'));
+ exit;
+ }
+
+ wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_STATS . '&month=' . rawurlencode($month) . '&google-sync=success'));
+ exit;
+ }
+
public function render_settings_page() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin'));
}
$settings = $this->get_settings();
+ $google_properties = $this->is_google_connected() ? $this->get_google_available_properties() : [];
+ $google_auth_url = $this->build_google_oauth_authorization_url();
?>
@@ -3150,12 +4305,24 @@ if (!class_exists('Ansico_Stat_Plugin')) {
+
+
+
+
+
+
+
+
+
+
+