Entwicklung 'Visit Duration Teil 1'
- Visit Duration
- Entwicklung 'Visit Duration Teil 1'
- Entwicklung 'Visit Duration Teil 2'
- Entwicklung 'Visit Duration: Scenarien'.
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 12.12.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Die vollständige URL mit Fragment holen
$full_url = isset($_POST['full_url']) ? sanitize_text_field($_POST['full_url']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
// Besuchsdaten holen oder initialisieren
$visits = get_option('current_visits', []);
// Besuchsdaten für diesen Besuch
$visit_data = [
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'full_url' => $full_url, // Speichern der vollständigen URL mit Fragment
'visit_time' => current_time('mysql')
];
// Besuchsdaten in die Option einfügen oder aktualisieren
$visits[$hashed_unique_id] = $visit_data;
update_option('current_visits', $visits);
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
(function () {
const currentUrl = window.location.href;
const previousUrl = sessionStorage.getItem('lastVisitedUrl');
let uniqueId = sessionStorage.getItem('uniqueId');
if (previousUrl === currentUrl) {
// Gleiche Seite: Behalte die aktuelle uniqueId oder generiere eine neue, falls keine existiert
uniqueId = uniqueId || generateUniqueId();
} else {
// Neue Seite oder erster Besuch: Erstelle neue uniqueId
uniqueId = generateUniqueId();
sessionStorage.setItem('lastVisitedUrl', currentUrl);
}
sessionStorage.setItem('uniqueId', uniqueId);
window.uniqueId = uniqueId;
function generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 16);
}
// console.log('Aktuelle uniqueId:', uniqueId);
})();
let debounceTimer = null; // Debounce-Timer
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', function(event) {
sendEndVisit(); // Versucht sofort, die Sitzung zu beenden
});
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// console.log('Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
// Speichere die Daten im localStorage für spätere Verarbeitung
localStorage.setItem('visitData', payload);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
// Speichere die Daten im localStorage für spätere Verarbeitung
localStorage.setItem('visitData', payload);
};
xhr.send(payload);
}
}
}
// Prüfen, ob noch zu sendende Daten im localStorage sind, und diese bei Gelegenheit senden
window.addEventListener('load', function() {
const storedData = localStorage.getItem('visitData');
if (storedData) {
const payload = JSON.parse(storedData);
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Daten nachträglich erfolgreich gesendet');
localStorage.removeItem('visitData'); // Entferne die zwischengespeicherten Daten
} else {
console.error('Fehler beim nachträglichen Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim nachträglichen Senden');
};
xhr.send(payload);
}
});
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
// Abschlusssignal an den Server senden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', function () {
navigator.sendBeacon(window.ajaxUrl + '?action=end_visit', JSON.stringify({
unique_id: window.uniqueId,
page_title: document.title
}));
});
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
// Fallback für den Fall, dass sendBeacon nicht verfügbar ist
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true); // Asynchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Fallback-Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
})();
</script>";
}
// End Visit Handler: Zusätzliche Logik zur Aktualisierung des Besuchsstatus und der Dauer
function end_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// unique_id aus der Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
$hashed_unique_id = hash('sha256', $unique_id);
// Besuchsdaten holen und aktualisieren
$visits = get_option('current_visits', []);
$current_time = time();
if (isset($visits[$hashed_unique_id])) {
$visit_data = &$visits[$hashed_unique_id];
// Sitzung beenden
if (!isset($visit_data['end_time'])) {
$visit_data['end_time'] = $current_time;
$visit_data['duration'] = $current_time - $visit_data['start_time'];
$visit_data['status'] = 'Beendet';
}
}
// Daten speichern
update_option('current_visits', $visits);
wp_send_json_success('Besuch beendet');
}
add_action('wp_ajax_end_visit', 'end_visit_handler');
add_action('wp_ajax_nopriv_end_visit', 'end_visit_handler');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf der Verweildauer
if (isset($visit_data['duration'])) {
// Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
$status = 'Beendet';
} else {
// Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
$current_time = time();
$timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten
// Wenn die Sitzung das Timeout überschritten hat
if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
$status = 'Offen'; // Timeout erreicht, als "Offen" markieren
} else {
$status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
}
}
if (isset($visit_data['start_time'])) {
// Zeit eine Stunde vorverlegen (3600 Sekunden)
$start_time = date('H:i:s', $visit_data['start_time'] + 3600);
} else {
$start_time = 'Unbekannt';
}
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
if ($status === 'Aktiv') {
$row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
} elseif ($status === 'Offen') {
$row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
} else {
$row_color = '#ECF3F9'; // Hellblau für beendet
}
// Zeile in der Tabelle ausgeben
echo "<tr style='background-color: $row_color;'>
<td>{$visit_data['page_title']}</td>
<td>{$start_time}</td>
<td style='text-align: center;'>{$formatted_duration}</td>
<td>{$status}</td>
</tr>";
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
let rowColor;
// Farben basierend auf dem Status setzen
if (visit.status === "Aktiv") {
rowColor = "#ffeb3b"; // Gelb für aktiv
} else if (visit.status === "Timeout") {
rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
} else {
rowColor = "#ECF3F9"; // Hellblau für beendet
}
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
// Falls visit.start_time ein Unix-Timestamp ist, umwandeln
var formattedStartTime = visit.start_time
? formatTime(visit.start_time)
: 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
'<td style="text-align: center;">' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
var hours = date.getHours().toString().padStart(2, '0');
var minutes = date.getMinutes().toString().padStart(2, '0');
var seconds = date.getSeconds().toString().padStart(2, '0');
return hours + ':' + minutes + ':' + seconds;
}
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$timeout_limit = 30 * 60; // 30 Minuten in Sekunden
$current_time = time();
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
$duration = $current_time - $visit_data['start_time'];
// Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
if ($duration > $timeout_limit) {
// Beende die Sitzung und setze die Verweildauer auf 30 Minuten
$visit_data['duration'] = $timeout_limit;
$visit_data['status'] = 'Timeout';
} else {
// Sitzung ist noch aktiv, aktualisiere die Dauer
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv';
}
} else {
// Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
$visit_data['status'] = 'Beendet';
}
// Formatiere die Dauer für die Anzeige
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
});
// Speichere die aktualisierten Besuchsdaten in der Option
update_option('current_visits', $visits);
// Sende die aktualisierten Daten zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
/* Cronjob? – ist nicht relevant
// Cron-Intervall hinzufügen
add_filter('cron_schedules', function ($schedules) {
$schedules['every_five_minutes'] = [
'interval' => 5 * 60, // Alle 5 Minuten
'display' => __('Every 5 Minutes'),
];
return $schedules;
});
// Cronjob registrieren
add_action('wp', function () {
if (!wp_next_scheduled('check_incomplete_sessions_event')) {
wp_schedule_event(time(), 'every_five_minutes', 'check_incomplete_sessions_event');
}
});
// Funktion für den Cronjob
add_action('check_incomplete_sessions_event', function () {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
$incomplete_sessions = $wpdb->get_results("SELECT * FROM $table_name WHERE visit_time IS NULL");
foreach ($incomplete_sessions as $session) {
$wpdb->update(
$table_name,
['visit_time' => current_time('mysql')],
['id' => $session->id]
);
}
});
// Cronjob bei Deaktivierung entfernen
register_deactivation_hook(__FILE__, function () {
$timestamp = wp_next_scheduled('check_incomplete_sessions_event');
if ($timestamp) {
wp_unschedule_event($timestamp, 'check_incomplete_sessions_event');
}
});
*/
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 11.12.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Die vollständige URL mit Fragment holen
$full_url = isset($_POST['full_url']) ? sanitize_text_field($_POST['full_url']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
// Besuchsdaten holen oder initialisieren
$visits = get_option('current_visits', []);
// Besuchsdaten für diesen Besuch
$visit_data = [
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'full_url' => $full_url, // Speichern der vollständigen URL mit Fragment
'visit_time' => current_time('mysql')
];
// Besuchsdaten in die Option einfügen oder aktualisieren
$visits[$hashed_unique_id] = $visit_data;
update_option('current_visits', $visits);
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// console.log('Fallback-Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
let debounceTimer = null;
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// console.log('Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
// Abschlusssignal an den Server senden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', function () {
navigator.sendBeacon(window.ajaxUrl + '?action=end_visit', JSON.stringify({
unique_id: window.uniqueId,
page_title: document.title
}));
});
})();
</script>";
}
// End Visit Handler: Zusätzliche Logik zur Aktualisierung des Besuchsstatus und der Dauer
function end_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// unique_id aus der Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
$hashed_unique_id = hash('sha256', $unique_id);
// Besuchsdaten holen und aktualisieren
$visits = get_option('current_visits', []);
$current_time = time();
if (isset($visits[$hashed_unique_id])) {
$visit_data = &$visits[$hashed_unique_id];
// Sitzung beenden
if (!isset($visit_data['end_time'])) {
$visit_data['end_time'] = $current_time;
$visit_data['duration'] = $current_time - $visit_data['start_time'];
$visit_data['status'] = 'Beendet';
}
}
// Daten speichern
update_option('current_visits', $visits);
wp_send_json_success('Besuch beendet');
}
add_action('wp_ajax_end_visit', 'end_visit_handler');
add_action('wp_ajax_nopriv_end_visit', 'end_visit_handler');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf der Verweildauer
if (isset($visit_data['duration'])) {
// Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
$status = 'Beendet';
} else {
// Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
$current_time = time();
$timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten
// Wenn die Sitzung das Timeout überschritten hat
if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
$status = 'Offen'; // Timeout erreicht, als "Offen" markieren
} else {
$status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
}
}
if (isset($visit_data['start_time'])) {
// Zeit eine Stunde vorverlegen (3600 Sekunden)
$start_time = date('H:i:s', $visit_data['start_time'] + 3600);
} else {
$start_time = 'Unbekannt';
}
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
if ($status === 'Aktiv') {
$row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
} elseif ($status === 'Offen') {
$row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
} else {
$row_color = '#ECF3F9'; // Hellblau für beendet
}
// Zeile in der Tabelle ausgeben
echo "<tr style='background-color: $row_color;'>
<td>{$visit_data['page_title']}</td>
<td>{$start_time}</td>
<td style='text-align: center;'>{$formatted_duration}</td>
<td>{$status}</td>
</tr>";
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
let rowColor;
// Farben basierend auf dem Status setzen
if (visit.status === "Aktiv") {
rowColor = "#ffeb3b"; // Gelb für aktiv
} else if (visit.status === "Timeout") {
rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
} else {
rowColor = "#ECF3F9"; // Hellblau für beendet
}
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
// Falls visit.start_time ein Unix-Timestamp ist, umwandeln
var formattedStartTime = visit.start_time
? formatTime(visit.start_time)
: 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
'<td style="text-align: center;">' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
var hours = date.getHours().toString().padStart(2, '0');
var minutes = date.getMinutes().toString().padStart(2, '0');
var seconds = date.getSeconds().toString().padStart(2, '0');
return hours + ':' + minutes + ':' + seconds;
}
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$timeout_limit = 30 * 60; // 30 Minuten in Sekunden
$current_time = time();
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
$duration = $current_time - $visit_data['start_time'];
// Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
if ($duration > $timeout_limit) {
// Beende die Sitzung und setze die Verweildauer auf 30 Minuten
$visit_data['duration'] = $timeout_limit;
$visit_data['status'] = 'Timeout';
} else {
// Sitzung ist noch aktiv, aktualisiere die Dauer
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv';
}
} else {
// Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
$visit_data['status'] = 'Beendet';
}
// Formatiere die Dauer für die Anzeige
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
});
// Speichere die aktualisierten Besuchsdaten in der Option
update_option('current_visits', $visits);
// Sende die aktualisierten Daten zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Anpassungen obigen Codes: URL auch mit Fragment wie '#visit' (bspw. https://wegerl.at/visit-duration/#visit) tracken und diverse Verbesserungen durch end_visit_handler()
. – Vorhergehende Datei:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 4.12.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
// Fallback für den Fall, dass sendBeacon nicht verfügbar ist
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true); // Asynchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Fallback-Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
let debounceTimer = null; // Debounce-Timer
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true); // Asynchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
// Optional: Hier könnte eine Retry-Logik hinzugefügt werden
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
</script>";
}
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf der Verweildauer
if (isset($visit_data['duration'])) {
// Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
$status = 'Beendet';
} else {
// Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
$current_time = time();
$timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten
// Wenn die Sitzung das Timeout überschritten hat
if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
$status = 'Offen'; // Timeout erreicht, als "Offen" markieren
} else {
$status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
}
}
if (isset($visit_data['start_time'])) {
// Zeit eine Stunde vorverlegen (3600 Sekunden)
$start_time = date('H:i:s', $visit_data['start_time'] + 3600);
} else {
$start_time = 'Unbekannt';
}
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
if ($status === 'Aktiv') {
$row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
} elseif ($status === 'Offen') {
$row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
} else {
$row_color = '#ECF3F9'; // Hellblau für beendet
}
// Zeile in der Tabelle ausgeben
echo "<tr style='background-color: $row_color;'>
<td>{$visit_data['page_title']}</td>
<td>{$start_time}</td>
<td style='text-align: center;'>{$formatted_duration}</td>
<td>{$status}</td>
</tr>";
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
let rowColor;
// Farben basierend auf dem Status setzen
if (visit.status === "Aktiv") {
rowColor = "#ffeb3b"; // Gelb für aktiv
} else if (visit.status === "Timeout") {
rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
} else {
rowColor = "#ECF3F9"; // Hellblau für beendet
}
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
// Falls visit.start_time ein Unix-Timestamp ist, umwandeln
var formattedStartTime = visit.start_time
? formatTime(visit.start_time)
: 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
'<td style="text-align: center;">' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
var hours = date.getHours().toString().padStart(2, '0');
var minutes = date.getMinutes().toString().padStart(2, '0');
var seconds = date.getSeconds().toString().padStart(2, '0');
return hours + ':' + minutes + ':' + seconds;
}
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$timeout_limit = 30 * 60; // 30 Minuten in Sekunden
$current_time = time();
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
$duration = $current_time - $visit_data['start_time'];
// Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
if ($duration > $timeout_limit) {
// Beende die Sitzung und setze die Verweildauer auf 30 Minuten
$visit_data['duration'] = $timeout_limit;
$visit_data['status'] = 'Timeout';
} else {
// Sitzung ist noch aktiv, aktualisiere die Dauer
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv';
}
} else {
// Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
$visit_data['status'] = 'Beendet';
}
// Formatiere die Dauer für die Anzeige
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
});
// Speichere die aktualisierten Besuchsdaten in der Option
update_option('current_visits', $visits);
// Sende die aktualisierten Daten zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
Verbesserung von Beenden des Trackings und Dashboards, Neuladung bei Sitzungen mit Timeout Status "Offen" statt "Aktiv".
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 3.12.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
// Fallback für den Fall, dass sendBeacon nicht verfügbar ist
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true); // Asynchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Fallback-Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
let debounceTimer = null; // Debounce-Timer
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true); // Asynchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Daten erfolgreich gesendet');
} else {
console.error('Fehler beim Fallback-Senden:', xhr.status);
}
};
xhr.onerror = function() {
console.error('Netzwerkfehler beim Fallback');
};
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
// Optional: Hier könnte eine Retry-Logik hinzugefügt werden
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
</script>";
}
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf der Verweildauer
if (isset($visit_data['duration'])) {
// Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
$status = 'Beendet';
} else {
// Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
$current_time = time();
$timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten
// Wenn die Sitzung das Timeout überschritten hat
if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
$status = 'Offen'; // Timeout erreicht, als "Offen" markieren
} else {
$status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
}
}
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
if ($status === 'Aktiv') {
$row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb opac für aktiv
} elseif ($status === 'Offen') {
$row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für aktiv
} else {
$row_color = '#ECF3F9'; // Hellblau für beendet
}
// Zeile in der Tabelle ausgeben
echo "<tr style='background-color: $row_color;'>
<td>{$visit_data['page_title']}</td>
<td>{$formatted_duration}</td>
<td>{$status}</td>
</tr>";
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
let rowColor;
// Farben basierend auf dem Status setzen
if (visit.status === "Aktiv") {
rowColor = "#ffeb3b"; // Gelb für aktiv
} else if (visit.status === "Timeout") {
rowColor = "#fff"; // Weiß für Timeout
} else {
rowColor = "#ECF3F9"; // Hellblau für beendet
}
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$timeout_limit = 30 * 60; // 30 Minuten in Sekunden
$current_time = time();
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
$duration = $current_time - $visit_data['start_time'];
// Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
if ($duration > $timeout_limit) {
// Beende die Sitzung und setze die Verweildauer auf 30 Minuten
$visit_data['duration'] = $timeout_limit;
$visit_data['status'] = 'Timeout';
} else {
// Sitzung ist noch aktiv, aktualisiere die Dauer
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv';
}
} else {
// Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
$visit_data['status'] = 'Beendet';
}
// Formatiere die Dauer für die Anzeige
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
});
// Speichere die aktualisierten Besuchsdaten in der Option
update_option('current_visits', $visits);
// Sende die aktualisierten Daten zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
Verbesserung von Beenden des Trackings und Dashboards Neuladung bei Sitzungen mit Timeout Status "Offen" statt "Aktiv".
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 29.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
// Fallback für den Fall, dass sendBeacon nicht verfügbar ist
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
// Optional: Hier könnte eine Retry-Logik hinzugefügt werden
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
</script>";
}
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$timeout_limit = 30 * 60; // 30 Minuten in Sekunden
$current_time = time();
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
$duration = $current_time - $visit_data['start_time'];
// Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
if ($duration > $timeout_limit) {
// Beende die Sitzung und setze die Verweildauer auf 30 Minuten
$visit_data['duration'] = $timeout_limit;
$visit_data['status'] = 'Timeout';
} else {
// Sitzung ist noch aktiv, aktualisiere die Dauer
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv';
}
} else {
// Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
$visit_data['status'] = 'Beendet';
}
// Formatiere die Dauer für die Anzeige
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
});
// Speichere die aktualisierten Besuchsdaten in der Option
update_option('current_visits', $visits);
// Sende die aktualisierten Daten zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 29.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents:
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
add_action('wp_footer', 'start_visit_tracking');
} else {
error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function () {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Scroll-Ereignis mit Debouncing
let scrollTimeout;
document.addEventListener('scroll', function () {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
if (!navigator.sendBeacon) {
// Fallback für den Fall, dass sendBeacon nicht verfügbar ist
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
// Primäre Methode: sendBeacon
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
// Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
async function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
try {
await fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
});
} catch (err) {
console.error('Fehler beim Heartbeat, versuche erneut:', err);
// Optional: Hier könnte eine Retry-Logik hinzugefügt werden
}
}
}
// Heartbeat alle 10 Sekunden senden
setInterval(sendHeartbeat, 10000);
console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
</script>";
}
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
$unique_id = sanitize_text_field($data['unique_id']);
$timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden
// Hole die aktuellen Besuche
$visits = get_option('current_visits', []);
// Wenn der Besuch existiert, aktualisiere den Heartbeat
if (isset($visits[$unique_id])) {
$visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
}
// Fehlerprotokollierung für Debugging
error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 27.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// 2) Sehr vereinfachtes Scenario: Tracking startet sofort nach Seitenaufruf (ohne 5 Sekunden-Verzögerung und ohne Scroll-Aktivität)
/*(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
// Funktion zum Starten des Besuchs
function startVisitTracking() {
console.log('Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
unique_id: window.uniqueId,
page_title: document.title
})
})
.then(response => {
if (!response.ok) {
throw new Error('Server-Antwort nicht erfolgreich.');
}
return response.json();
})
.then(data => {
console.log('Tracking erfolgreich:', data);
})
.catch(err => console.error('Fehler beim Start des Besuchs:', err));
}
// Start des Besuchs sofort nach Seitenaufruf
startVisitTracking();
*/
// 1) Einfaches Scenario: Tracking mit 5 Sekunden Verzögerung nach Scroll-Aktivität
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 5 Sekunden...');
setTimeout(function() {
console.log('5 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 5000);
}
}
// Tracking bei Scroll-Aktivität starten
document.addEventListener('scroll', function() {
startVisitTracking();
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
})();
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
// Verarbeite die Daten, z. B. speichere den letzten Heartbeat-Zeitstempel
// Integriere die Logik in dein bestehendes Tracking
error_log("Heartbeat erhalten: " . print_r($data, true));
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 26.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
// Prüfen, ob die Sitzung bereits gestartet wurde
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
if (isset($_SESSION)) {
// Besuchszeit setzen, falls noch nicht vorhanden
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
} else {
// Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
}
/*function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}*/
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Vereinfachtes Scenario: Kein 5-Minuten-Inaktivitätstimer und Tracking nur bei Scroll-Aktivität
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isUserInteracted = false;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 10 Sekunden...');
setTimeout(function() {
console.log('10 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 10000);
}
}
// Tracking bei Scroll-Aktivität starten
document.addEventListener('scroll', function() {
startVisitTracking();
});
/* Tracking starten, sobald das DOM geladen ist
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM vollständig geladen. Tracking wird in Kürze gestartet.');
startVisitTracking();
});*/
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Funktion zum Senden des Heartbeats
function sendHeartbeat() {
if (window.uniqueId && window.ajaxUrl) {
fetch(window.ajaxUrl + '?action=heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
}).catch(err => console.error('Fehler beim Heartbeat:', err));
}
}
// Heartbeat alle 5 Sekunden senden
setInterval(sendHeartbeat, 5000);
console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');
function handle_heartbeat() {
// Stelle sicher, dass die Daten korrekt sind
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['unique_id']) && isset($data['timestamp'])) {
// Verarbeite die Daten, z. B. speichere den letzten Heartbeat-Zeitstempel
// Integriere die Logik in dein bestehendes Tracking
error_log("Heartbeat erhalten: " . print_r($data, true));
}
wp_die(); // Beendet die AJAX-Anfrage
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: 1.0.0
* Entwicklung: 23.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
// Scenario 2) Tracking-Start bei Scroll-Aktivität und 5-Minuten-Inaktivitätstimer sofort starten
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isTabActive = true;
var isUserInteracted = false;
let inactivityTimeout;
const inactivityDelay = 5 * 60 * 1000; // 5 Minuten Inaktivität
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function() {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
}
}
// Funktion zum Zurücksetzen des Inaktivitätstimers
function resetInactivityTimer() {
clearTimeout(inactivityTimeout);
inactivityTimeout = setTimeout(endSessionDueToInactivity, inactivityDelay);
console.log('Inaktivitätstimer zurückgesetzt.');
}
// Funktion zum Beenden des Besuchs wegen Inaktivität
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Externe Links abfangen
document.addEventListener('click', function(event) {
const target = event.target.closest('a');
if (target && target.target === '_blank') {
console.log('Externer Link in neuem Tab geöffnet. Sitzung bleibt aktiv.');
isTabActive = true;
}
});
// Inaktivitätstimer direkt nach Seitenaufruf starten
resetInactivityTimer();
// Auf Scroll-Aktivität setzen, um Tracking zu starten
document.addEventListener('scroll', function() {
startVisitTracking();
});
// Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
})();
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');
// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
delete_option('current_visits');
}
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: 1.0.0
* Entwicklung: 21.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isTabActive = true;
var isUserInteracted = false;
let inactivityTimeout;
let visibilityTimeout;
// Funktion zum Starten des Besuchs
function startVisitTracking() {
if (!isUserInteracted) {
isUserInteracted = true;
console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
setTimeout(function() {
console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
if (isTabActive) {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
}).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
}
}, 30000);
resetInactivityTimer();
}, 3000);
}
}
function resetInactivityTimer() {
clearTimeout(inactivityTimeout);
inactivityTimeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000);
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen 5 Minuten Inaktivität.');
}
}
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Sichtbarkeitsprüfung
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
console.log('Tab unsichtbar, aber Sitzung bleibt aktiv.');
visibilityTimeout = setTimeout(sendEndVisit, 15000); // 15 Sekunden
} else {
console.log('Tab sichtbar.');
clearTimeout(visibilityTimeout);
}
});
// Externe Links abfangen
document.addEventListener('click', function(event) {
const target = event.target.closest('a');
if (target && target.target === '_blank') {
console.log('Externer Link in neuem Tab geöffnet. Sitzung bleibt aktiv.');
isTabActive = true;
}
});
// Benutzerinteraktionen
document.addEventListener('mousemove', function() {
startVisitTracking();
resetInactivityTimer();
}, { once: true });
document.addEventListener('scroll', function() {
startVisitTracking();
resetInactivityTimer();
}, { once: true });
document.addEventListener('keydown', function() {
startVisitTracking();
resetInactivityTimer();
}, { once: true });
window.addEventListener('beforeunload', sendEndVisit);
})();
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: 1.0.0
* Entwicklung: 20.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
// Die unique_id aus der POST-Anfrage holen
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
// SHA-256 Hash der unique_id erstellen
$hashed_unique_id = hash('sha256', $unique_id);
// Den Seitentitel aus der POST-Anfrage holen
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
// Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
// Gehashten Wert in die Datenbank einfügen
$wpdb->insert($table_name, array(
'unique_id' => $hashed_unique_id, // Gehashte unique_id speichern
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool()) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist, sich um einen Bot oder Testtool handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isTabActive = true;
// Besuch nur nach 3 Sekunden tracken
setTimeout(function() {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
if (isTabActive) {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
}).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
}
}, 30000);
// Besuch beenden: Sichtbarkeit oder Schließen
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Sichtbarkeitswechsel überwachen
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
isTabActive = false;
sendEndVisit();
} else {
isTabActive = true;
}
});
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', sendEndVisit);
})();
// Safari spezifisch
document.addEventListener('DOMContentLoaded', function() {
// Safari-Erkennung
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
var uniqueId = window.uniqueId || 'default_user';
var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';
localStorage.setItem('visit_start_time', visitStartTime);
if (isSafari) {
console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');
function sendDataForSafari(startTime, endTime) {
var data = {
unique_id: uniqueId,
start_time: startTime,
end_time: endTime,
};
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Safari-Spezifisch:', xhr.responseText);
} else {
console.error('Safari Fehler:', xhr.statusText);
}
};
}
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
sendDataForSafari(visitStartTime, Date.now());
}
});
window.addEventListener('beforeunload', function() {
sendDataForSafari(visitStartTime, Date.now());
});
setInterval(function() {
sendDataForSafari(visitStartTime, Date.now());
}, 30000);
}
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen.
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten.
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
let timeout;
let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
sessionEnded = true;
console.log('Besuch beendet wegen Inaktivität.');
sendSessionUpdate();
}
}
function sendSessionUpdate() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
}
function handlePageNavigation() {
// Sitzung vor dem Verlassen der Seite sauber beenden
if (!sessionEnded) {
console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
sendSessionUpdate();
sessionEnded = true; // Markiere Sitzung als beendet
}
}
// Benutzeraktivität überwachen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
// Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
window.addEventListener('beforeunload', handlePageNavigation);
window.addEventListener('unload', handlePageNavigation);
// Timer starten, sobald die Seite geladen wird
resetTimer();
</script>";
}
});*/
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: (2/19.11.24)
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
$wpdb->insert($table_name, array(
'unique_id' => $unique_id,
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool()) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist, sich um einen Bot oder Testtool handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isTabActive = true;
// Besuch nur nach 3 Sekunden tracken
setTimeout(function() {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
if (isTabActive) {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
}).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
}
}, 30000);
// Besuch beenden: Sichtbarkeit oder Schließen
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Sichtbarkeitswechsel überwachen
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
isTabActive = false;
sendEndVisit();
} else {
isTabActive = true;
}
});
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', sendEndVisit);
})();
// Safari spezifisch
document.addEventListener('DOMContentLoaded', function() {
// Safari-Erkennung
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
var uniqueId = window.uniqueId || 'default_user';
var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';
localStorage.setItem('visit_start_time', visitStartTime);
if (isSafari) {
console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');
function sendDataForSafari(startTime, endTime) {
var data = {
unique_id: uniqueId,
start_time: startTime,
end_time: endTime,
};
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Safari-Spezifisch:', xhr.responseText);
} else {
console.error('Safari Fehler:', xhr.statusText);
}
};
}
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
sendDataForSafari(visitStartTime, Date.now());
}
});
window.addEventListener('beforeunload', function() {
sendDataForSafari(visitStartTime, Date.now());
});
setInterval(function() {
sendDataForSafari(visitStartTime, Date.now());
}, 30000);
}
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen.
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten.
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
let timeout;
let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
sessionEnded = true;
console.log('Besuch beendet wegen Inaktivität.');
sendSessionUpdate();
}
}
function sendSessionUpdate() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
}
function handlePageNavigation() {
// Sitzung vor dem Verlassen der Seite sauber beenden
if (!sessionEnded) {
console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
sendSessionUpdate();
sessionEnded = true; // Markiere Sitzung als beendet
}
}
// Benutzeraktivität überwachen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
// Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
window.addEventListener('beforeunload', handlePageNavigation);
window.addEventListener('unload', handlePageNavigation);
// Timer starten, sobald die Seite geladen wird
resetTimer();
</script>";
}
});*/
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: (1/19.11.24)
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
$wpdb->insert($table_name, array(
'unique_id' => $unique_id,
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
(function() {
var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
if (!ajaxUrl) {
console.error('AJAX-URL konnte nicht geladen werden.');
return;
}
window.ajaxUrl = ajaxUrl;
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
var isTabActive = true;
// Besuch nur nach 3 Sekunden tracken
setTimeout(function() {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
}).catch(err => console.error('Fehler beim Start des Besuchs:', err));
}, 3000);
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
if (isTabActive) {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
}).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
}
}, 30000);
// Besuch beenden: Sichtbarkeit oder Schließen
function sendEndVisit() {
if (window.uniqueId && window.ajaxUrl) {
const payload = JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
});
const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);
if (!beaconSuccess) {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
}
}
// Sichtbarkeitswechsel überwachen
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
isTabActive = false;
sendEndVisit();
} else {
isTabActive = true;
}
});
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', sendEndVisit);
})();
// Safari spezifisch
document.addEventListener('DOMContentLoaded', function() {
// Safari-Erkennung
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
var uniqueId = window.uniqueId || 'default_user';
var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';
localStorage.setItem('visit_start_time', visitStartTime);
if (isSafari) {
console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');
function sendDataForSafari(startTime, endTime) {
var data = {
unique_id: uniqueId,
start_time: startTime,
end_time: endTime,
};
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Safari-Spezifisch:', xhr.responseText);
} else {
console.error('Safari Fehler:', xhr.statusText);
}
};
}
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
sendDataForSafari(visitStartTime, Date.now());
}
});
window.addEventListener('beforeunload', function() {
sendDataForSafari(visitStartTime, Date.now());
});
setInterval(function() {
sendDataForSafari(visitStartTime, Date.now());
}, 30000);
}
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sek:Min)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen.
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten.
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
let timeout;
let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
sessionEnded = true;
console.log('Besuch beendet wegen Inaktivität.');
sendSessionUpdate();
}
}
function sendSessionUpdate() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
}
function handlePageNavigation() {
// Sitzung vor dem Verlassen der Seite sauber beenden
if (!sessionEnded) {
console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
sendSessionUpdate();
sessionEnded = true; // Markiere Sitzung als beendet
}
}
// Benutzeraktivität überwachen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
// Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
window.addEventListener('beforeunload', handlePageNavigation);
window.addEventListener('unload', handlePageNavigation);
// Timer starten, sobald die Seite geladen wird
resetTimer();
</script>";
}
});*/
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: (18.11.24)
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();
function start_visit_handler() {
check_ajax_referer('your_nonce', 'security');
$unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
$page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
if ($unique_id && $page_title) {
global $wpdb;
$table_name = $wpdb->prefix . 'visit_tracking';
$wpdb->insert($table_name, array(
'unique_id' => $unique_id,
'page_title' => $page_title,
'visit_time' => current_time('mysql')
));
wp_send_json_success('Besuch protokolliert');
} else {
wp_send_json_error('Ungültige Daten');
}
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
// Besuch nur nach 3 Sekunden tracken
setTimeout(function() {
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
}, 3000);
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
});
}, 30000);
// Besuch beenden beim Schließen des Tabs oder Browsers
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
});
// Safari spezifisch
document.addEventListener('DOMContentLoaded', function() {
// Safari-Erkennung
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
var uniqueId = window.uniqueId || 'default_user';
var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';
localStorage.setItem('visit_start_time', visitStartTime);
if (isSafari) {
console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');
function sendDataForSafari(startTime, endTime) {
var data = {
unique_id: uniqueId,
start_time: startTime,
end_time: endTime,
};
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Safari-Spezifisch:', xhr.responseText);
} else {
console.error('Safari Fehler:', xhr.statusText);
}
};
}
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
sendDataForSafari(visitStartTime, Date.now());
}
});
window.addEventListener('beforeunload', function() {
sendDataForSafari(visitStartTime, Date.now());
});
setInterval(function() {
sendDataForSafari(visitStartTime, Date.now());
}, 30000);
}
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sek:Min)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen.
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten.
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
let timeout;
let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
sessionEnded = true;
console.log('Besuch beendet wegen Inaktivität.');
sendSessionUpdate();
}
}
function sendSessionUpdate() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
}
function handlePageNavigation() {
// Sitzung vor dem Verlassen der Seite sauber beenden
if (!sessionEnded) {
console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
sendSessionUpdate();
sessionEnded = true; // Markiere Sitzung als beendet
}
}
// Benutzeraktivität überwachen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
// Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
window.addEventListener('beforeunload', handlePageNavigation);
window.addEventListener('unload', handlePageNavigation);
// Timer starten, sobald die Seite geladen wird
resetTimer();
</script>";
}
});*/
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: 1.0.0 (Entwicklung vom 15.11.24)
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
// Bot erkannt, keine Zählung vornehmen
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
});
}, 30000);
// Besuch beenden beim Schließen des Tabs oder Browsers
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
// Bot erkannt
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sek:Min)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 10 * 60 * 1000); // 10 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
Entwicklung.
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Entwicklungs-Datum: 15.11.24 / 1
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
// Bot erkannt, keine Zählung vornehmen
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
});
}, 30000);
// Besuch beenden beim Schließen des Tabs oder Browsers
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
if (!isset($_SESSION['visit_start_time'])) {
wp_send_json(['timeout_reached' => false]);
return;
}
$max_duration = 90 * 60; // 90 Minuten
$current_duration = time() - $_SESSION['visit_start_time'];
// Fehlerprotokollierung hinzufügen
error_log("Current visit duration: " . $current_duration . " seconds.");
if ($current_duration > $max_duration) {
end_visit_tracking(); // Besuch beenden bei Timeout
wp_send_json(['timeout_reached' => true]);
} else {
wp_send_json(['timeout_reached' => false]);
}
}
add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
// Bot erkannt
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sek:Min)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Funktionsblock zur Überwachung der Inaktivität
let inactivityTime = function() {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(endSessionDueToInactivity, 10 * 60 * 1000); // 5 Minuten Inaktivität
}
function endSessionDueToInactivity() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Besuch beendet wegen Inaktivität.');
}
}
// Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
document.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
document.onscroll = resetTimer;
};
inactivityTime();
// Intervallüberprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
// alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Intervall von 30 Sekunden
// Besuch beim Schließen des Tabs oder Browsers beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
}
});
</script>";
}
});
Entwicklung;
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Entwicklungs-Datum: 13.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
// Bot erkannt, keine Zählung vornehmen
}
// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
}
add_action('init', 'start_session_if_needed');
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
});
}, 30000);
// Besuch beenden beim Schließen des Tabs oder Browsers
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
if (!isset($_SESSION['visit_start_time'])) {
wp_send_json(['timeout_reached' => false]);
return;
}
$max_duration = 90 * 60; // 90 Minuten
$current_duration = time() - $_SESSION['visit_start_time'];
// Fehlerprotokollierung hinzufügen
error_log("Current visit duration: " . $current_duration . " seconds.");
if ($current_duration > $max_duration) {
end_visit_tracking(); // Besuch beenden bei Timeout
wp_send_json(['timeout_reached' => true]);
} else {
wp_send_json(['timeout_reached' => false]);
}
}
add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
// Bot erkannt
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen, nur für nicht-Admins
add_action('wp_footer', function() {
if ( !current_user_can('administrator') ) { // Überprüft, ob der aktuelle Benutzer kein Administrator ist
echo "<script>
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
// alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Intervall von 30 Sekunden
// Besuch beim Schließen des Tabs beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}), function(response) {
console.log('Daten erfolgreich gesendet:', response);
});
}
});
</script>";
}
});
Entwicklung:
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* Version: Entwicklung 7
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration
Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt.
Diese Funktion nutzt Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if ( file_exists( plugin_dir_path( __FILE__ ) . 'bot-functions.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
// Fehlerbehandlung, falls die Datei nicht gefunden wurde
error_log('bot-functions.php wurde nicht gefunden.');
}
// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
// Bot erkannt, keine Zählung vornehmen
}
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
// Den angemeldeten Admin und Bots ausschließen
if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
}
});
}, 30000);
// Besuch beenden beim Schließen des Tabs oder Browsers
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
});
</script>";
}
// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
} else {
error_log('bot-functions.php wurde nicht gefunden.');
}
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
if (!isset($_SESSION['visit_start_time'])) {
wp_send_json(['timeout_reached' => false]);
return;
}
$max_duration = 90 * 60; // 90 Minuten
$current_duration = time() - $_SESSION['visit_start_time'];
if ($current_duration > $max_duration) {
end_visit_tracking(); // Besuch beenden bei Timeout
wp_send_json(['timeout_reached' => true]);
} else {
wp_send_json(['timeout_reached' => false]);
}
}
add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
// Bot erkannt
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Visit Duration: Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
// alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Intervall von 30 Sekunden
// Besuch beim Schließen des Tabs beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Entwicklung:
<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 6
Author: Team WP Wegerl.at
Author URI: https://wegerl.at
Text Domain: visit-duration-tracker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
$user_ip = $_SERVER['REMOTE_ADDR'];
// Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Wenn keine Startzeit gesetzt ist, wird sie auf den aktuellen Zeitpunkt gesetzt
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
// Hier kannst du zusätzliche Aktionen durchführen, z.B. Seite neu laden oder den Status im Frontend ändern.
// alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Alle 30 Sekunden prüfen
// Besuch beim Schließen des Tabs oder Browsers beenden mit sendBeacon
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
// sendBeacon anstelle von fetch verwenden, um die Anfrage zuverlässig abzuschicken
navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now() // Besuch beenden
}));
}
});
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
if (isset($_SESSION['visit_start_time'])) {
$max_duration = 90 * 60; // 90 Minuten in Sekunden
$current_duration = time() - $_SESSION['visit_start_time'];
if ($current_duration > $max_duration) {
end_visit_tracking(); // Besuch beenden bei Timeout
// Besuchsdaten im Backend aktualisieren
$visits = get_option('current_visits', []);
foreach ($visits as $unique_id => $visit_data) {
if (!isset($visit_data['duration'])) {
// Besuch als beendet markieren
$visits[$unique_id]['status'] = 'Beendet';
$visits[$unique_id]['duration'] = $current_duration;
$visits[$unique_id]['formatted_duration'] = sprintf('%02d:%02d', floor($current_duration / 60), $current_duration % 60);
}
}
update_option('current_visits', $visits); // Besuche aktualisieren
wp_send_json(['timeout_reached' => true]);
} else {
wp_send_json(['timeout_reached' => false]);
}
} else {
wp_send_json(['timeout_reached' => false]);
}
}
add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
function is_bot_or_spider() {
$cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
if ($cached_result !== false) {
return $cached_result;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
$bots = [
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
'sogou', 'exabot', 'facebook', 'twitter', 'linkedin', 'pinterest',
'msnbot', 'bot', 'crawl', 'crawler', 'spider', 'ia_archiver',
'curl', 'fetch', 'python', 'wget', 'monitor'
];
// Erstellen des Regex-Musters basierend auf der Liste der Bot-Namen
$pattern = '/(' . implode('|', $bots) . ')/i';
// Prüfen, ob der User-Agent mit dem Muster übereinstimmt
if (preg_match($pattern, $user_agent)) {
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
return true;
}
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
return false;
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen sehr funktionell
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Intervall von 30 Sekunden
// Besuch beim Schließen des Tabs beenden
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Entwicklung :
<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 5
Author: Team WP Wegerl.at
Author URI: https://wegerl.at
Text Domain: visit-duration-tracker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
$user_ip = $_SERVER['REMOTE_ADDR'];
// Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Wenn keine Startzeit gesetzt ist, wird sie auf den aktuellen Zeitpunkt gesetzt
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// Besuchsdaten abrufen und AJAX-Skript laden
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
// Timeout-Überprüfung alle 30 Sekunden
setInterval(function() {
fetch(window.ajaxUrl + '?action=check_visit_timeout', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
if (data.timeout_reached) {
console.log('Besuch beendet wegen Timeout.');
// Hier kannst du zusätzliche Aktionen durchführen, z.B. Seite neu laden oder den Status im Frontend ändern.
// alert('Der Besuch wurde aufgrund des Timeouts beendet.');
}
});
}, 30000); // Alle 30 Sekunden prüfen
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time'];
update_option('last_visitor_duration', $duration);
unset($_SESSION['visit_start_time']);
error_log("Visit ended. Duration: " . $duration . " seconds.");
}
}
// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
if (isset($_SESSION['visit_start_time'])) {
$max_duration = 90 * 60; // 90 Minuten in Sekunden
$current_duration = time() - $_SESSION['visit_start_time'];
if ($current_duration > $max_duration) {
end_visit_tracking(); // Besuch beenden bei Timeout
// Besuchsdaten im Backend aktualisieren
$visits = get_option('current_visits', []);
foreach ($visits as $unique_id => $visit_data) {
if (!isset($visit_data['duration'])) {
// Besuch als beendet markieren
$visits[$unique_id]['status'] = 'Beendet';
$visits[$unique_id]['duration'] = $current_duration;
$visits[$unique_id]['formatted_duration'] = sprintf('%02d:%02d', floor($current_duration / 60), $current_duration % 60);
}
}
update_option('current_visits', $visits); // Besuche aktualisieren
wp_send_json(['timeout_reached' => true]);
} else {
wp_send_json(['timeout_reached' => false]);
}
} else {
wp_send_json(['timeout_reached' => false]);
}
}
add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
function is_bot_or_spider() {
$cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
if ($cached_result !== false) {
return $cached_result;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
$bots = [
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
'sogou', 'exabot', 'facebook', 'twitter', 'linkedin', 'pinterest',
'msnbot', 'bot', 'crawl', 'spider', 'ia_archiver'
];
foreach ($bots as $bot) {
if (strpos($user_agent, $bot) !== false) {
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
return true;
}
}
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
return false;
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Besuche nach Startzeit sortieren (neueste oben)
usort($visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
// Besucher nach Startzeit absteigend sortieren
response.data.updated_visits.sort(function(a, b) {
return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
});
// Besucher in die Tabelle einfügen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
// Besuchsdaten nach Startzeit absteigend sortieren
usort($updated_visits, function($a, $b) {
return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
});
// Speichern der neuen Verweildauern
update_option('current_visits', $visits);
// Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
wp_send_json_success(['updated_visits' => $updated_visits]);
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Entwicklung :
<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 4
Author: Team WP Wegerl.at
Author URI: https://wegerl.at
Text Domain: visit-duration-tracker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
$user_ip = $_SERVER['REMOTE_ADDR'];
// Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Wenn keine Startzeit gesetzt, dann setzen wir sie auf den aktuellen Zeitpunkt
if (!isset($_SESSION['visit_start_time'])) {
$_SESSION['visit_start_time'] = time();
}
// 90 Minuten Timeout: Überprüfen, ob die Zeit überschritten wurde
$max_duration = 90 * 60; // 90 Minuten in Sekunden
if (isset($_SESSION['visit_start_time']) && (time() - $_SESSION['visit_start_time']) > $max_duration) {
// Timeout erreicht, Verweildauer als beendet setzen
end_visit_tracking(); // Diese Funktion behandelt das Beenden der Verweildauer
}
// Besuchsdaten abrufen und AJAX-Skript laden
$visits = get_option('current_visits', []);
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
function end_visit_tracking() {
// Berechnung der Verweildauer
if (isset($_SESSION['visit_start_time'])) {
$duration = time() - $_SESSION['visit_start_time']; // Verweildauer in Sekunden
// Verweildauer speichern, z.B. in einer Option oder einer Sitzung
// Zum Beispiel: update_option('visit_duration_' . session_id(), $duration);
// Oder hier eine einfache Option, um die Verweildauer zu speichern
update_option('last_visitor_duration', $duration); // Option für den letzten Besuch
// Verweildauer zurücksetzen
unset($_SESSION['visit_start_time']);
}
// Optional: Sende eine Nachricht oder führe eine Aktion nach dem Timeout aus
// Zum Beispiel: Benachrichtigung in der Admin-Oberfläche
// wp_mail('admin@deinedomain.com', 'Besucher hat die Seite 90 Minuten lang offen gelassen', 'Die Verweildauer wurde überschritten.');
}
// Optionale Funktion zum Abfragen der letzten Verweildauer
function get_last_visitor_duration() {
return get_option('last_visitor_duration', 0); // Standardwert ist 0, falls keine Verweildauer gesetzt
}
add_action('wp_footer', 'end_visit_tracking');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
function is_bot_or_spider() {
$cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
if ($cached_result !== false) {
return $cached_result;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
$bots = [
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
'sogou', 'exabot', 'facebook', 'twitter', 'linkedin', 'pinterest',
'msnbot', 'bot', 'crawl', 'spider', 'ia_archiver'
];
foreach ($bots as $bot) {
if (strpos($user_agent, $bot) !== false) {
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
return true;
}
}
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
return false;
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 25 Einträge begrenzen
$max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
// Überprüfen, ob die erforderlichen Daten vorhanden sind
if (!isset($data['unique_id']) || !isset($data['end_time'])) {
wp_send_json_error(['message' => 'Fehlende Parameter']);
return;
}
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
// Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
echo '<style>
/* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
#visit-duration-container {
height: 360px;
overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
border: 1px solid #ddd;
}
/* Schmaler Scrollbalken für Webkit-basierte Browser */
#visit-duration-container::-webkit-scrollbar {
width: 4px; /* Schmaler Scrollbalken */
}
#visit-duration-container::-webkit-scrollbar-thumb {
background-color: darkgray;
border-radius: 10px;
}
#visit-duration-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
</style>';
// Scrollbarer Container für die Tabelle
echo '<div id="visit-duration-container">';
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
// <thead> mit Sticky-Header-Styling
echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
echo '</thead>';
echo '<tbody>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
// Setze die Hintergrundfarbe abhängig vom Status
$row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';
echo '<tr style="background-color: ' . $row_color . ';">';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</div>'; // Ende des scrollbaren Containers
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
var tableBody = jQuery('#visit-duration-table').find('tbody');
tableBody.empty(); // Bestehende Zeilen löschen
response.data.updated_visits.forEach(function(visit) {
var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
var formattedDuration = visit.formatted_duration || 'Noch aktiv';
tableBody.append(
'<tr style="background-color: ' + rowColor + '">' +
'<td>' + visit.page_title + '</td>' +
'<td>' + formattedDuration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr></thead><tbody></tbody>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
update_option('current_visits', $visits); // Speichern der neuen Verweildauern
wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Entwicklung:
<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 3
Author: Team WP Wegerl.at
*/
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
$user_ip = $_SERVER['REMOTE_ADDR'];
// Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
return; // Frühzeitig abbrechen
}
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Besuchsdaten abrufen und AJAX-Skript laden
$visits = get_option('current_visits', []);
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
function is_ip_excluded($user_ip) {
$excluded_ips = get_option('excluded_ips', array());
return in_array($user_ip, $excluded_ips);
}
}
// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
function is_bot_or_spider() {
$cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
if ($cached_result !== false) {
return $cached_result;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
$bots = [
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
'sogou', 'exabot', 'facebook', 'twitter', 'linkedin', 'pinterest',
'msnbot', 'bot', 'crawl', 'spider', 'ia_archiver'
];
foreach ($bots as $bot) {
if (strpos($user_agent, $bot) !== false) {
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
return true;
}
}
set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
return false;
}
}
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 12 Einträge begrenzen
if (count($visits) > 12) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000);
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
echo '<tr>';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>'; // Formatiert als "Min:Sek"
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</table>';
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Erstelle die neue Tabelle ohne Unique ID
jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>');
response.data.updated_visits.forEach(function(visit) {
jQuery('#visit-duration-table').append(
'<tr>' +
'<td>' + visit.page_title + '</td>' +
'<td>' + visit.formatted_duration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
// Funktion zur Aktualisierung der Verweildauer aller Besuche
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
update_option('current_visits', $visits); // Speichern der neuen Verweildauern
wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Entwicklung:
<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 2
Author: Team WP Wegerl.at
Author URI: https://wegerl.at
Text Domain: visit-duration-tracker
*/
// Funktion zur Initialisierung und Startzeit eines Besuchs
function start_visit_tracking() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Besuchsdaten aus der Optionstabelle abrufen
$visits = get_option('current_visits', []);
// Übergabe von AJAX-URL und Anweisungen an den Browser
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
// Bei jedem Neuladen der Seite wird eine neue ID generiert
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16); // Zufällige ID erstellen
// Senden der Startzeit und ID an den Server
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 12 Einträge begrenzen
if (count($visits) > 12) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000);
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
// Berechne Minuten und Sekunden für die Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
} else {
$formatted_duration = 'Noch aktiv';
}
echo '<tr>';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($formatted_duration) . '</td>'; // Formatiert als "Min:Sek"
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</table>';
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Erstelle die neue Tabelle ohne Unique ID
jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>');
response.data.updated_visits.forEach(function(visit) {
jQuery('#visit-duration-table').append(
'<tr>' +
'<td>' + visit.page_title + '</td>' +
'<td>' + visit.formatted_duration + '</td>' +
'<td>' + visit.status + '</td>' +
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button mit Doppel-Klick-Mechanismus
document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
event.preventDefault();
if (this.dataset.clickedOnce === "true") {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
} else {
this.dataset.clickedOnce = "true";
this.innerText = "Zum Bestätigen erneut klicken";
setTimeout(() => {
this.dataset.clickedOnce = "false";
this.innerText = "Tabelle zurücksetzen";
}, 1500);
}
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
update_option('current_visits', $visits); // Speichern der neuen Verweildauern
wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});
Verweildauer in WordPress tracken: Eine einfache Lösung mit "Visit Duration Tracker"
Entwicklung:
<?php
/*
Plugin Name: Visit Duration Tracker
Description:
Version: Entwicklung 1
Author: Team WP Wegerl.at
*/
// Funktion zur Initialisierung und Startzeit eines Besuchs
function start_visit_tracking() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Besuchsdaten aus der Optionstabelle abrufen
$visits = get_option('current_visits', []);
// Übergabe von AJAX-URL und Anweisungen an den Browser
echo "<script>
window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
// Bei jedem Neuladen der Seite wird eine neue ID generiert
window.addEventListener('load', function() {
window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16); // Zufällige ID erstellen
// Senden der Startzeit und ID an den Server
fetch(window.ajaxUrl + '?action=start_visit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
});
});
</script>";
}
add_action('wp_footer', 'start_visit_tracking');
// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$page_title = strip_tags($data['page_title']);
$start_time = time();
$visits = get_option('current_visits', []);
// Neuer Eintrag für jeden Seitenaufruf speichern
$visits[$unique_id] = [
'unique_id' => $unique_id,
'page_title' => $page_title,
'start_time' => $start_time
];
// Besuche auf 12 Einträge begrenzen
if (count($visits) > 12) {
array_shift($visits);
}
update_option('current_visits', $visits);
wp_send_json_success();
}
add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');
// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
$data = json_decode(file_get_contents('php://input'), true);
$unique_id = sanitize_text_field($data['unique_id']);
$end_time = intval($data['end_time'] / 1000);
$visits = get_option('current_visits', []);
if (isset($visits[$unique_id])) {
$duration = $end_time - $visits[$unique_id]['start_time'];
$visits[$unique_id]['duration'] = $duration;
update_option('current_visits', $visits);
}
wp_send_json_success();
}
add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');
// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
wp_add_dashboard_widget(
'visit_duration_widget',
'Aktuelle Verweildauer der Besucher',
'display_visit_duration_widget'
);
}
add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');
// Widget für das Dashboard mit aktualisierten Besuchsdaten
function display_visit_duration_widget() {
$visits = get_option('current_visits', []);
if (empty($visits)) {
echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
return;
}
echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
echo '<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>';
foreach ($visits as $visit_data) {
// Setze den Status basierend auf dem Vorhandensein der Verweildauer
$status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';
$duration = isset($visit_data['duration']) ? $visit_data['duration'] : 'Noch aktiv';
echo '<tr>';
echo '<td>' . esc_html($visit_data['unique_id']) . '</td>';
echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
echo '<td>' . esc_html($duration) . '</td>';
echo '<td>' . esc_html($status) . '</td>';
echo '</tr>';
}
echo '</table>';
echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
?>
<script type="text/javascript">
// "Update"-Button
document.getElementById('update-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'update_all_visit_durations',
},
success: function(response) {
if (response.success) {
// Erstelle die neue Tabelle mit formatierten Verweildauern und Status
jQuery('#visit-duration-table').html('<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Minuten:Sekunden)</th><th>Status</th></tr>');
response.data.updated_visits.forEach(function(visit) {
// Zeige die formatierten Zeiten (Minuten:Sekunden) und den Status an
jQuery('#visit-duration-table').append(
'<tr>' +
'<td>' + visit.unique_id + '</td>' +
'<td>' + visit.page_title + '</td>' +
'<td>' + visit.formatted_duration + '</td>' +
'<td>' + visit.status + '</td>' + // Status hinzugefügt
'</tr>'
);
});
} else {
alert('Fehler bei der Aktualisierung der Verweildauer.');
}
},
error: function() {
alert('Fehler beim Aktualisieren der Verweildauer.');
}
});
});
// "Reset"-Button
document.getElementById('reset-duration-btn').addEventListener('click', function() {
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'reset_visit_duration',
},
success: function(response) {
if (response.success) {
// Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
jQuery('#visit-duration-table').html('<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
} else {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
},
error: function() {
alert('Fehler beim Zurücksetzen der Tabelle.');
}
});
});
</script>
<?php
}
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
// Funktion zur Aktualisierung der Verweildauer aller Besuche
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
$visits = get_option('current_visits', []);
$updated_visits = [];
foreach ($visits as $visit_id => $visit_data) {
// Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
// Berechne die Verweildauer für die aktiven Besuche
$duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
$visit_data['duration'] = $duration;
$visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
} else {
// Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
$visit_data['status'] = 'Beendet';
}
// Berechne Minuten und Sekunden für jede Verweildauer
if (isset($visit_data['duration'])) {
$minutes = floor($visit_data['duration'] / 60);
$seconds = $visit_data['duration'] % 60;
$formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
$visit_data['formatted_duration'] = $formatted_duration;
} else {
$visit_data['formatted_duration'] = 'Noch aktiv';
}
$updated_visits[] = $visit_data; // Füge die (aktualisierte) Besuchsdaten hinzu
}
update_option('current_visits', $visits); // Speichern der neuen Verweildauern
wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}
add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');
// Besuchsdaten zurücksetzen
function reset_visit_duration() {
delete_option('current_visits');
update_option('current_visits', []);
wp_cache_flush();
wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');
// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
echo "<script>
window.addEventListener('beforeunload', function() {
if (window.uniqueId && window.ajaxUrl) {
navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
unique_id: window.uniqueId,
end_time: Date.now()
}));
}
});
</script>";
});