453 lines
16 KiB
PHP
453 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* WP Multi – Modul: Benutzer-Analytics
|
||
*
|
||
* Automatisch aus wp-multi.php ausgelagert. Wird über den Loader in
|
||
* wp-multi.php eingebunden (nur wenn das WP Multi Toolkit aktiv ist).
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) { exit; }
|
||
|
||
/*
|
||
* Benutzer-Analytics mit verbesserten Sicherheits-, Performance- und Benutzerfreundlichkeits-Features
|
||
*/
|
||
|
||
|
||
// global ist nötig, weil die Plugin-Datei bei der Aktivierung innerhalb einer
|
||
// Funktion eingebunden wird und $wpdb dort sonst nicht verfügbar ist.
|
||
global $wpdb;
|
||
define('WP_MULTI_ANALYTICS_TABLE', $wpdb->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 '<div class="error"><p>' . esc_html__('Die Analytics-Tabelle existiert nicht. Bitte aktiviere das Plugin erneut.', 'wp-multi') . '</p></div>';
|
||
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'));
|
||
?>
|
||
<form method="get">
|
||
<input type="hidden" name="page" value="wp_multi_analytics">
|
||
<select name="time_range" onchange="this.form.submit()" style="max-width:240px;">
|
||
<option value="7days" <?php selected($time_range, '7days'); ?>><?php esc_html_e('Letzte 7 Tage', 'wp-multi'); ?></option>
|
||
<option value="30days" <?php selected($time_range, '30days'); ?>><?php esc_html_e('Letzte 30 Tage', 'wp-multi'); ?></option>
|
||
<option value="custom" <?php selected($time_range, 'custom'); ?>><?php esc_html_e('Benutzerdefiniert', 'wp-multi'); ?></option>
|
||
</select>
|
||
<?php if ($time_range === 'custom') : ?>
|
||
<input type="date" name="start_date" value="<?php echo esc_attr($_GET['start_date'] ?? ''); ?>" style="max-width:180px;">
|
||
<input type="date" name="end_date" value="<?php echo esc_attr($_GET['end_date'] ?? ''); ?>" style="max-width:180px;">
|
||
<?php endif; ?>
|
||
<button type="submit" class="wpm-btn wpm-btn--primary"><?php esc_html_e('Filtern', 'wp-multi'); ?></button>
|
||
</form>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
|
||
if (empty($results['data'])) {
|
||
wpmt_admin_notice('info', __('Keine Daten für den gewählten Zeitraum verfügbar.', 'wp-multi'));
|
||
} else {
|
||
wpmt_admin_card_open(__('Aktivitätsverlauf', 'wp-multi'));
|
||
?>
|
||
<canvas id="userActivityChart" style="height: 300px; width: 100%;"></canvas>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<script>
|
||
if (typeof Chart !== 'undefined') {
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var ctx = document.getElementById('userActivityChart').getContext('2d');
|
||
var chart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: <?php echo wp_json_encode($results['dates']); ?>,
|
||
datasets: <?php echo wp_json_encode($results['datasets']); ?>,
|
||
},
|
||
options: {
|
||
scales: {
|
||
x: { title: { display: true, text: '<?php echo esc_js(__('Datum', 'wp-multi')); ?>' } },
|
||
y: { title: { display: true, text: '<?php echo esc_js(__('Anzahl', 'wp-multi')); ?>' }, beginAtZero: true }
|
||
}
|
||
}
|
||
});
|
||
});
|
||
} else {
|
||
console.error('Chart.js konnte nicht geladen werden.');
|
||
}
|
||
</script>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
|
||
wpmt_admin_card_open(__('Einzelne Aktivitäten', 'wp-multi'));
|
||
?>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Benutzer ID', 'wp-multi'); ?></th>
|
||
<th><?php esc_html_e('Aktion', 'wp-multi'); ?></th>
|
||
<th><?php esc_html_e('Beitrag Titel', 'wp-multi'); ?></th>
|
||
<th><?php esc_html_e('Beitrag ID', 'wp-multi'); ?></th>
|
||
<th><?php esc_html_e('Zeitstempel', 'wp-multi'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($results['data'] as $row) : ?>
|
||
<tr>
|
||
<td><?php echo $row->user_id ? esc_html($row->user_id) : esc_html__('Gast', 'wp-multi'); ?></td>
|
||
<td><span class="wpm-badge <?php echo $row->action === 'comment' ? 'wpm-badge--success' : ''; ?>"><?php echo esc_html($row->action); ?></span></td>
|
||
<td>
|
||
<?php
|
||
if ($row->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');
|
||
}
|
||
?>
|
||
</td>
|
||
<td><?php echo esc_html($row->post_id ?: '-'); ?></td>
|
||
<td><?php echo esc_html($row->timestamp); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
}
|
||
|
||
wpmt_admin_page_close();
|
||
}
|
||
|
||
// Der Analytics-Menüpunkt wird zentral in wp_multi_register_admin_menus() registriert.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|