prefix . 'wp_multi_user_analytics'); /** * Erstellt die Datenbanktabelle für Benutzer-Analytics. */ function wp_multi_create_analytics_table() { global $wpdb; $table_name = WP_MULTI_ANALYTICS_TABLE; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, action varchar(255) NOT NULL, post_id bigint(20) DEFAULT NULL, timestamp datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_timestamp (timestamp), INDEX idx_action (action) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } register_activation_hook(WP_MULTI_FILE, 'wp_multi_create_analytics_table'); /** * Verfolgt Benutzerinteraktionen (Kommentare und Beitragsaufrufe). * * @param int $user_id Benutzer-ID. * @param string $action Aktion (z. B. 'view', 'comment'). * @param int $post_id Beitrag-ID (optional). * @return bool Erfolg der Operation. */ function wp_multi_track_user_activity($user_id, $action, $post_id = null) { global $wpdb; $table_name = WP_MULTI_ANALYTICS_TABLE; $user_id = absint($user_id); $action = sanitize_text_field($action); $post_id = $post_id ? absint($post_id) : null; if ($action === 'view' && is_single()) { $post_id = get_the_ID(); } // user_id 0 ist gewollt: Die Seite bietet keinen Login, Aufrufe sind anonym. if (!$action) { return false; } return $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'action' => $action, 'post_id' => $post_id, ), array('%d', '%s', '%d') ); } /** * Verfolgt Kommentar-Aktivitäten. * * @param int $comment_id Kommentar-ID. */ function wp_multi_comment_activity($comment_id) { $comment = get_comment($comment_id); $user_id = absint($comment->user_id); if ($user_id) { wp_multi_track_user_activity($user_id, 'comment', $comment->comment_post_ID); } } add_action('comment_post', 'wp_multi_comment_activity'); /** * Erkennt Bots und Crawler anhand des User-Agents. * * @return bool */ function wp_multi_is_bot_request() { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT']))) : ''; // Ohne User-Agent ist es so gut wie nie ein echter Besucher if ($user_agent === '') { return true; } $signatures = apply_filters('wp_multi_bot_signatures', [ 'bot', 'crawl', 'spider', 'slurp', 'archiver', 'preview', 'monitor', 'validator', 'facebookexternalhit', 'headless', 'phantomjs', 'python', 'curl', 'wget', 'go-http', 'java/', 'okhttp', 'axios', 'scrapy', 'lighthouse', 'pingdom', 'uptime', ]); foreach ($signatures as $signature) { if (strpos($user_agent, $signature) !== false) { return true; } } return false; } /** * Prüft, ob dieser Besucher den Beitrag gerade erst aufgerufen hat. * * Ohne Login gibt es keine Benutzer-ID, deshalb dient ein kurzlebiger Hash aus * IP und User-Agent als Erkennungsmerkmal. Gespeichert wird nur der Hash, nie * die IP selbst – ein Reload zählt damit nicht mehrfach. * * @param int $post_id Beitrag-ID. * @return bool True, wenn der Aufruf bereits gezählt wurde. */ function wp_multi_view_already_counted($post_id) { $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $key = 'wp_multi_view_' . md5($post_id . '|' . $ip . '|' . $user_agent . '|' . wp_salt()); if (get_transient($key)) { return true; } // Sperrfrist je Besucher und Beitrag $ttl = (int) apply_filters('wp_multi_view_throttle', 30 * MINUTE_IN_SECONDS); set_transient($key, 1, $ttl); return false; } /** * Verfolgt Beitragsaufrufe – auch von nicht eingeloggten Besuchern. */ function wp_multi_post_view_activity() { if (is_admin() || !is_single() || is_feed()) { return; } // Eigene Aufrufe der Redaktion verfälschen die Zahlen if (is_user_logged_in() && current_user_can('edit_posts') && !apply_filters('wp_multi_track_editor_views', false)) { return; } if (wp_multi_is_bot_request()) { return; } $post_id = get_the_ID(); if (!$post_id || wp_multi_view_already_counted($post_id)) { return; } // Ohne Login ist die Benutzer-ID 0 – der Aufruf zählt trotzdem wp_multi_track_user_activity(get_current_user_id(), 'view', $post_id); } add_action('wp_head', 'wp_multi_post_view_activity'); /** * Baut die WHERE-Bedingung für den gewählten Zeitraum. * * @return array [WHERE-Fragment, Parameter] */ function wp_multi_analytics_date_where($start_datetime = null, $end_datetime = null) { if (!$start_datetime) { $start_datetime = gmdate('Y-m-d H:i:s', strtotime('-7 days')); } $where = 'WHERE timestamp >= %s'; $params = [$start_datetime]; if ($end_datetime) { $where .= ' AND timestamp <= %s'; $params[] = $end_datetime; } return [$where, $params]; } /** * Ruft die Tagessummen je Aktion ab – Grundlage für das Diagramm. * * Aggregiert in der Datenbank statt in PHP: Seit auch anonyme Aufrufe gezählt * werden, kämen hier sonst zehntausende Einzelzeilen an. * * @return array Zeilen mit date, action und count. */ function wp_multi_fetch_analytics_totals($start_datetime = null, $end_datetime = null) { global $wpdb; list($where, $params) = wp_multi_analytics_date_where($start_datetime, $end_datetime); $sql = "SELECT DATE(timestamp) AS date, action, COUNT(*) AS count FROM " . WP_MULTI_ANALYTICS_TABLE . " $where GROUP BY date, action ORDER BY date ASC"; return $wpdb->get_results($wpdb->prepare($sql, $params)); } /** * Ruft die jüngsten Einzelereignisse ab – Grundlage für die Detailtabelle. * * @return array Rohe Analytics-Daten (begrenzt). */ function wp_multi_fetch_raw_analytics($start_datetime = null, $end_datetime = null) { global $wpdb; list($where, $params) = wp_multi_analytics_date_where($start_datetime, $end_datetime); // Ohne Limit würde die Detailtabelle bei anonymem Tracking die Seite sprengen $params[] = (int) apply_filters('wp_multi_analytics_detail_limit', 200); $sql = "SELECT DATE(timestamp) AS date, action, post_id, 1 AS count, user_id, timestamp FROM " . WP_MULTI_ANALYTICS_TABLE . " $where ORDER BY timestamp DESC LIMIT %d"; return $wpdb->get_results($wpdb->prepare($sql, $params)); } /** * Verarbeitet rohe Analytics-Daten für Diagramm und Tabelle. * * @param array $results Rohe Analytics-Daten. * @return array Verarbeitete Daten. */ function wp_multi_process_analytics_data($results, $recent = null) { $dates = []; $comment_counts = []; $view_counts = []; foreach ($results as $result) { $date = $result->date; if (!in_array($date, $dates)) { $dates[] = $date; } if ($result->action === 'comment') { $comment_counts[$date] = ($comment_counts[$date] ?? 0) + $result->count; } elseif ($result->action === 'view') { $view_counts[$date] = ($view_counts[$date] ?? 0) + $result->count; } } $all_dates = []; $datasets = ['comments' => [], 'views' => []]; for ($i = 6; $i >= 0; $i--) { $date = date('Y-m-d', strtotime("-$i day")); $all_dates[] = $date; $datasets['comments'][] = $comment_counts[$date] ?? 0; $datasets['views'][] = $view_counts[$date] ?? 0; } return [ 'dates' => array_reverse($all_dates), 'datasets' => [ [ 'label' => __('Kommentare', 'wp-multi'), 'data' => array_reverse($datasets['comments']), 'borderColor' => 'rgba(75, 192, 192, 1)', 'borderWidth' => 1, 'fill' => false, ], [ 'label' => __('Beitragsaufrufe', 'wp-multi'), 'data' => array_reverse($datasets['views']), 'borderColor' => 'rgba(153, 102, 255, 1)', 'borderWidth' => 1, 'fill' => false, ] ], 'data' => ($recent === null) ? $results : $recent ]; } /** * Ruft Analytics-Daten mit Caching ab. * * @param string $date_query Datum für die Abfrage. * @return array Analytics-Daten. */ function wp_multi_get_analytics_data($start_datetime = null, $end_datetime = null) { $cache_key = 'wp_multi_analytics_data_' . md5($start_datetime . '|' . $end_datetime); $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } $totals = wp_multi_fetch_analytics_totals($start_datetime, $end_datetime); $recent = wp_multi_fetch_raw_analytics($start_datetime, $end_datetime); $data = wp_multi_process_analytics_data($totals, $recent); set_transient($cache_key, $data, HOUR_IN_SECONDS); return $data; } /** * Zeigt die Benutzer-Analytics-Seite im Admin-Bereich an. */ function wp_multi_display_user_analytics() { global $wpdb; if (!$wpdb->get_var("SHOW TABLES LIKE '" . WP_MULTI_ANALYTICS_TABLE . "'")) { echo '

' . esc_html__('Die Analytics-Tabelle existiert nicht. Bitte aktiviere das Plugin erneut.', 'wp-multi') . '

'; return; } $time_range = isset($_GET['time_range']) ? sanitize_text_field($_GET['time_range']) : '7days'; $start_datetime = gmdate('Y-m-d H:i:s', strtotime('-7 days')); $end_datetime = null; if ($time_range === '30days') { $start_datetime = gmdate('Y-m-d H:i:s', strtotime('-30 days')); } elseif ($time_range === 'custom' && isset($_GET['start_date'], $_GET['end_date'])) { $custom_start = sanitize_text_field(wp_unslash($_GET['start_date'])); $custom_end = sanitize_text_field(wp_unslash($_GET['end_date'])); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $custom_start) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $custom_end)) { $start_datetime = $custom_start . ' 00:00:00'; $end_datetime = $custom_end . ' 23:59:59'; } } $results = wp_multi_get_analytics_data($start_datetime, $end_datetime); wpmt_admin_page_open( __('Benutzer Analytics', 'wp-multi'), __('Beitragsaufrufe und Kommentar-Aktivität deiner Besucher', 'wp-multi') ); wpmt_admin_card_open(__('Zeitraum', 'wp-multi')); ?>
user_id ? esc_html($row->user_id) : esc_html__('Gast', 'wp-multi'); ?> action); ?> post_id) { $post_title = get_the_title($row->post_id); echo esc_html($post_title ?: __('Kein Titel verfügbar', 'wp-multi')); } else { echo esc_html__('Kein Beitrag', 'wp-multi'); } ?> post_id ?: '-'); ?> timestamp); ?>