Enwicklung 'Visit Duration Teil 2'

Entwicklungsteile 'Visit Duration Teil 2'

Bots werden heutzutage immer besser darin, menschliches Verhalten zu simulieren, selbst einfache Schutzmaßnahmen wie Scrollinteraktionen oder Captchas (z. B. "Ich bin kein Bot"-Abfragen) zu umgehen. Moderne Bots können sogar komplexe Interaktionen nachahmen, einschließlich Cookie-Akzeptanz, Scrollbewegungen oder das Lösen von Captchas durch maschinelles Lernen.

Visit Duration – Einfach. Praktisch. Kompatibel.
Aus dem Herzen von WP Wegerl. Nur hier zum Download.

/wp-content/plugins/visit-duration/  
├── visit-duration.php  
└── bot-helper/  
    └── bot-functions.php

Hier ist der derselbe Code wie des Plug-ins, für des Moments Verbesserungen sind immer in Aussicht!

Erstens 'visit-duration.php'

  1. visit-duration.php
<?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.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, AJAX-Skript laden und Besuche beenden
    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);  // Generiert eine eindeutige ID für den Besuch
    var isUserInteracted = false;

    // Funktion zum Starten des Besuchs, falls der Nutzer mit der Seite interagiert
    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);  // Verzögerung von 3 Sekunden für den Besuchsstart
        }
    }
    
    // Verwaltet die einzigartige ID, die entweder von der aktuellen Seite oder von der Sitzung beibehalten wird
    (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);  // Generiert eine zufällige ID
        }

        // console.log('Aktuelle uniqueId:', uniqueId);
    })();

    let debounceTimer = null; // Debounce-Timer für das Scroll-Event
    let scrollTimeout;
    // Scroll-Tracking: Startet das Besuchs-Tracking mit einer Verzögerung (Debounce), um wiederholte Anfragen zu vermeiden
    document.addEventListener('scroll', function () {
        if (scrollTimeout) clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(startVisitTracking, 200);  // Verzögerung von 200ms zwischen den Scrollereignissen
    });

    // Besuch beenden, wenn die Seite verlassen wird (vor dem Schließen oder Neuladen)
    window.addEventListener('beforeunload', function(event) {
        sendEndVisit(); // Versucht sofort, die Sitzung zu beenden
    });

    // Funktion zum Senden des Besuchs-Ende-Updates an den Server
    function sendEndVisit() {
        if (window.uniqueId && window.ajaxUrl) {
            const payload = JSON.stringify({
                unique_id: window.uniqueId,
                end_time: Date.now()  // Sendet die Endzeit des Besuchs
            });

            // Primäre Methode: sendBeacon (optimal für das Senden von Daten beim Verlassen der Seite)
            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                // Fallback: XMLHttpRequest, falls sendBeacon nicht verfügbar oder gescheitert
                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 Aktualisieren des Besuchszustands, z. B. bei Pausieren oder Fortsetzen des Besuchs
    async function updateVisitState(state) {
        if (window.uniqueId && window.ajaxUrl) {
            await fetch(window.ajaxUrl + '?action=update_visit_state', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ unique_id: window.uniqueId, state: state, referrer: document.referrer, current_url: window.location.href })
            }).catch(err => console.error('Fehler beim Aktualisieren des Besuchszustands:', err));
        }
    }

    // Kombinierter Event-Listener für die Sichtbarkeit der Seite
    document.addEventListener('visibilitychange', function () {
        if (document.visibilityState === 'hidden') {
            updateVisitState('paused');  // Seite in den Hintergrund, Besuch pausiert
            sendEndVisit();  // Besuch beenden
        } else if (document.visibilityState === 'visible') {
            updateVisitState('resumed');  // Seite wieder sichtbar, Besuch fortgesetzt
            startVisitTracking();  // Besuch neu starten
        }
    });
    
    // 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, um den Besuch aktiv zu halten
    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, wenn sendBeacon nicht verfügbar ist
    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);
            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');

// Kontext: Funktion zum Aktualisieren des Besuchszustands, z. B. bei Pausieren oder Fortsetzen des Besuchs
add_action('wp_ajax_update_visit_state', function () {
    $unique_id = sanitize_text_field($_POST['unique_id']);
    $referrer = sanitize_text_field($_POST['referrer']);
    $current_url = sanitize_text_field($_POST['current_url']);

    // Überprüfen, ob die Referrer- und URL-Informationen vorhanden sind
    if ($referrer && $current_url) {
        // Hier kannst du deine Logik für Seitenwechsel implementieren
        // Z. B. prüfen, ob der Referrer und die URL der aktuellen Seite zur gleichen Domain gehören
        $parsed_referrer = parse_url($referrer);
        $parsed_url = parse_url($current_url);

        // Beispiel: Wenn der Referrer und die aktuelle URL auf derselben Domain liegen
        if ($parsed_referrer['host'] === $parsed_url['host']) {
            // Hier kannst du dann den Besuch in der Datenbank aktualisieren, z.B. Zeit oder Status
            // z.B. Besuch fortsetzen oder pausieren, basierend auf dem Zustand 'paused' oder 'resumed'
        }
    }
    
    wp_send_json_success('Visit state updated.');
});

// 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');
}

Zweitens 'bot-functions.php'

  1. bot-functions.php
<?php
/**
 * Erkennung von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
 *
 * Diese Funktionen überprüfen, ob der aktuelle Benutzeragent (User-Agent) mit einem bekannten Bot- oder 
 * Testtool-Identifikator übereinstimmt. Zusätzlich wird auch der Referrer und andere verdächtige Header überprüft, 
 * um Bots oder Testtools genauer zu erkennen. 
 * 
 * Alle Ergebnisse werden für 12 Stunden im Cache gespeichert, um wiederholte Anfragen effizient zu vermeiden 
 * und die Performance zu verbessern. Bei jeder Anfrage wird überprüft, ob es sich um einen Bot, ein Testtool 
 * oder eine verdächtige Anfrage handelt.
 *
 * Funktionen:
 * - `is_bot_or_spider()`: Prüft, ob der User-Agent einem bekannten Bot entspricht und verwendet Caching.
 * - `is_suspicious_bot()`: Prüft verdächtige Header (wie `X-Forwarded-For`) und ob der Referrer ungültig ist.
 * - `is_test_tool()`: Prüft, ob der User-Agent oder zusätzliche Header auf Testtools hinweisen.
 * - `has_valid_referer()`: Überprüft, ob der Referrer ein gültiger interner oder externer Ursprung ist.
 * 
 * @since 1.0.0
 * @return bool True, wenn es sich um einen Bot handelt, sonst False.
 */
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        // Cache-Schlüssel auf Basis der IP und des User-Agents erstellen
        $cache_key = 'is_bot_' . md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
        $cached_result = get_transient($cache_key);
        
        if ($cached_result !== false) {
            return $cached_result;
        }

        // User-Agent in Kleinbuchstaben umwandeln
        $user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);

        // Liste der bekannten Bots
        $bots = [
            'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
            'sogou', 'exabot', 'linkedin', 'pinterest', 'msnbot', 'crawl', 'crawler',
            'spider', 'ia_archiver', 'curl', 'fetch', 'python', 'wget', 'monitor',
            'botcheck', 'webcrawler', 'mj12bot', '360spider', 'addthis', 'adsbot',
            'adscanner', 'ahrefsbot', 'fast-webcrawler', 'scooter', 'amazonaws.com',
            'aspiegelbot', 'axios', 'bingpreview', 'blexbot', 'bloglines', 'bubing',
            'bytespider', 'ccbot', 'chatgpt', 'cliqzbot', 'crawl', 'cyotek', 'daum',
            'dispatch', 'domaincrawler', 'dotbot', 'everyonesocialbot', 'facebookexternalhit',
            'facebot', 'feedfetcher', 'femtosearchbot', 'findexa', 'flipboardproxy',
            'gaisbot', 'gigabot', 'go-http-client', 'gptbot', 'qwantbot', 'gridbot', 'heritrix',
            'ips-agent', 'james bot', 'komodiabot', 'linkdexbot', 'lycos', 'mauibot',
            'mediatoolkitbot', 'megaindex', 'metauri', 'mojeekbot', 'moreover', 'nbot',
            'node-fetch', 'obot', 'omgili', 'panscient.com', 'paperlibot', 'petalbot',
            'phantomjs', 'picsearch', 'proximic', 'pubsub', 'radian6', 'rogerbot',
            'rytebot', 'screaming frog seo spider', 'seokicks-robot', 'semrushbot', 'seznambot',
            'serendeputybot', 'sirdata', 'siteexplorer', 'sixtrix', 'smmtbot',
            'spbot', 'surveybot', 'technorati', 'telegrambot', 'thither', 'trendsmap',
            'turnitinbot', 'tweetmeme', 'twingly', 'twitterbot', 'voilabot', 'whatsapp',
            'zyborg', 'wotbox', 'xenu', 'xoviBot', 'disqus', 'yisouspider'
        ];

        // RegExp zum Prüfen auf Bot-User-Agent
        $pattern = '/(' . implode('|', array_map('preg_quote', $bots)) . ')/i';

        if (preg_match($pattern, $user_agent)) {
            // Caching das Ergebnis, dass es sich um einen Bot handelt
            set_transient($cache_key, true, 12 * HOUR_IN_SECONDS);
            return true;
        }

        // Wenn kein Bot gefunden wurde
        set_transient($cache_key, false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

function is_suspicious_bot() {
    $headers_to_check = ['HTTP_X_FORWARDED_FOR', 'HTTP_VIA', 'HTTP_PROXY_CONNECTION'];
    foreach ($headers_to_check as $header) {
        if (isset($_SERVER[$header])) {
            return true; // Verdächtige Header entdeckt, könnte ein Bot sein
        }
    }

    // Optional: Referrer-Überprüfung direkt hier
    return !has_valid_referer(); // Rückgabe von false, wenn der Referrer ungültig ist
}


function is_test_tool() {
    // User-Agent prüfen
    $user_agent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
    $test_tools = ['lighthouse', 'pagespeed', 'gtmetrix', 'webpagetest', 'chrome-lighthouse', 'pingdom', 'uptimerobot', 'newrelic', 'siteimprove'];
    
    // Weitere Header analysieren
    $is_test_tool = false;
    
    // 1. Prüfen, ob der User-Agent eines bekannten Tools entspricht
    foreach ($test_tools as $tool) {
        if (strpos($user_agent, $tool) !== false) {
            $is_test_tool = true;
            break;
        }
    }

    // 2. Zusätzliche Header prüfen
    $special_headers = ['x-lighthouse-request', 'chrome-proxy'];
    foreach ($special_headers as $header) {
        if (isset($_SERVER[$header])) {
            $is_test_tool = true;
            break;
        }
    }

    // Ergebnis zurückgeben
    return $is_test_tool;
}

/**
 * Überprüft, ob der Referrer-Header valide ist.
 * Ein gültiger Referrer könnte ein interner Link oder ein zugehöriger Ursprung sein.
 *
 * @return bool True, wenn der Referrer gültig ist, sonst False.
 */
 function has_valid_referer() {
    if (!isset($_SERVER['HTTP_REFERER'])) {
        return true; // Keine Referer-Überprüfung, wenn kein Referer vorhanden ist
    }  
    // Prüfe, ob ein Referer vorhanden ist
    if (isset($_SERVER['HTTP_REFERER'])) {
        $referer = strtolower($_SERVER['HTTP_REFERER']);
        $referer_host = parse_url($referer, PHP_URL_HOST);

        // Liste erlaubter externer Domains mit regulären Ausdrücken
        $allowed_external_referers = [
            '/\.google\./i',         // Alle Google-Domains (z. B. google.com, google.de)
            '/\.bing\./i',           // Alle Bing-Domains
            '/\.yahoo\./i',          // Alle Yahoo-Domains
            '/duckduckgo\.com/i',    // DuckDuckGo
            '/\.baidu\./i',          // Alle Baidu-Domains
            '/\.yandex\./i',         // Alle Yandex-Domains
            '/\.facebook\./i',       // Facebook
            '/\.twitter\./i',        // Twitter
            '/\.linkedin\./i',       // LinkedIn
            '/\.instagram\./i',      // Instagram
            '/\.pinterest\./i',      // Pinterest
            // Weitere Domains oder Netzwerke bei Bedarf hinzufügen
        ];

        // Überprüfen, ob der Referer von einer erlaubten externen Domain stammt
        foreach ($allowed_external_referers as $pattern) {
            if (preg_match($pattern, $referer_host)) {
                return true; // Externer Referer von einer Suchmaschine
            }
        }

        // Liste von gültigen internen Domains/Seiten
        $valid_internal_referers = [
            parse_url(home_url(), PHP_URL_HOST), // Hauptseite (Startseite)
            parse_url(site_url(), PHP_URL_HOST), // Basis-URL der Site
            'example.com', // Weitere interne Domains (bei Bedarf ersetzen)
        ];

        // Prüfen, ob der Referer eine gültige interne Quelle ist
        if (in_array($referer_host, $valid_internal_referers, true)) {
            return true; // Gültiger interner Referer
        }

        // Prüfen, ob der Referer zu einer spezifischen internen Seite führt
if (strpos($referer, site_url() . '/') !== false || 
    strpos($referer, site_url() . '/category/') !== false || 
    strpos($referer, site_url() . '/tag/') !== false ||
    strpos($referer, site_url() . '/post/') !== false ||
    strpos($referer, site_url() . '/page/') !== false) {
    return true; // Gültiger interner Referer (Home-Seite, Kategorie, Tag, Beitrag oder Seite)
}
		
        // Wenn keine Bedingungen erfüllt sind, ist der Referer ungültig
        return false;
    }

    // Kein Referer im Header, als ungültig betrachten
    return false;
}

Bots werden heutzutage immer besser darin, menschliches Verhalten zu simulieren, selbst einfache Schutzmaßnahmen wie Scrollinteraktionen oder Captchas (z. B. "Ich bin kein Bot"-Abfragen) zu umgehen. Moderne Bots können sogar komplexe Interaktionen nachahmen, einschließlich Cookie-Akzeptanz, Scrollbewegungen oder das Lösen von Captchas durch maschinelles Lernen.

Beipiel von Bot-Tracking

Beipiel von Bot-Tracking trots bot-functions.php.

Zum Beispiel traten folgende Probleme auf: Die Klicks wurden trotz des Trackings, das ab einer Benutzerinteraktion mit dem Scrollen ausgelöst wurde, erfasst. Das unerwünschte Crawling konnte jedoch keinen einzigen Aufruf abschließen, was bereits ein erstes Indiz für Bot-Aktivitäten darstellt.

  • Der separate Klickzähler (Click & Bounce Counter) erfasste 32 Klicks und 16 Absprünge innerhalb von nur 15 Sekunden.
  • Statify, (mit Zeitverzögerung + Scrolltiefe) das das Tracking erst nach 30 Sekunden und einer Scrolltiefe  von 1/3 aktiviert, zeichnete 37 Seitenaufrufe auf – dies könnte die Gesamtzahl des Bot-Besuchs gewesen sein.
  • Ergebnis in Visit Duration waren alle auf 'Timeout', folgend ausschnittsweise der Start des Trackings:
05:10:5805:11:0105:11:0505:11:3205:11:3405:11:3605:11:4305:12:02

Das ist ein Problem, das wir mit zusätzlichen Logiken im zukünftigen Ausschlussverfahren durch die bot-functions.php behandeln werden. Der Test läuft derzeit.

Verhaltenserkennung: Ergänze Scroll-Interaktionen durch komplexere Analysen, wie zum Beispiel:

  • Mausbewegungen: Bots bewegen den Cursor oft gleichmäßig oder springen direkt zu bestimmten Positionen. Menschliches Verhalten hingegen ist ungleichmäßiger.

Auf diese Weise sollte es künftig möglich sein, eine Unterscheidung zu treffen, wenn in Visit Duration kein Tracking angezeigt wird, im Gegensatz zu Statify, das dies nicht berücksichtigt.


Vorhergehende erste Bot-Datei:

<?php
/**
 * Erkennung von Bots, Spidern und Testtools basierend auf dem User-Agent.
 *
 * Diese Funktion überprüft, ob der aktuelle Benutzeragent (User-Agent) mit einem bekannten Bot- oder Spider-Identifikator übereinstimmt.
 * Die Funktion nutzt Caching, um wiederholte Anfragen innerhalb eines bestimmten Zeitrahmens zu vermeiden.
 * Die Ergebnisse werden für 12 Stunden gespeichert, um unnötige Bot-Überprüfungen zu minimieren.
 *
 * Wenn der Benutzeragent mit einem bekannten Bot übereinstimmt, wird `true` zurückgegeben,
 * andernfalls `false`. Das Ergebnis wird auch in einem transienten Cache für 12 Stunden gespeichert,
 * um die Performance zu verbessern.
 *
 * @since 1.0.0
 * @return bool True, wenn es sich um einen Bot handelt, sonst False.
 */
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        // Cache-Schlüssel auf Basis der IP und des User-Agents erstellen
        $cache_key = 'is_bot_' . md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
        $cached_result = get_transient($cache_key);
        
        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', 'botcheck',
            'crawlerbot', 'webcrawler', 'robozilla', 'mj12bot', 
        ];

        $pattern = '/(' . implode('|', $bots) . ')/i';

        if (preg_match($pattern, $user_agent)) {
            set_transient($cache_key, true, 12 * HOUR_IN_SECONDS);
            return true;
        }

        set_transient($cache_key, false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

function is_test_tool() {
    // User-Agent prüfen
    $user_agent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
    $test_tools = ['lighthouse', 'pagespeed', 'gtmetrix', 'webpagetest', 'chrome-lighthouse', 'pingdom', 'uptimerobot', 'newrelic', 'siteimprove'];
    
    // Weitere Header analysieren
    $is_test_tool = false;
    
    // 1. Prüfen, ob der User-Agent eines bekannten Tools entspricht
    foreach ($test_tools as $tool) {
        if (strpos($user_agent, $tool) !== false) {
            $is_test_tool = true;
            break;
        }
    }

    // 2. Zusätzliche Header prüfen
    $special_headers = ['x-lighthouse-request', 'chrome-proxy'];
    foreach ($special_headers as $header) {
        if (isset($_SERVER[$header])) {
            $is_test_tool = true;
            break;
        }
    }

    // Ergebnis zurückgeben
    return $is_test_tool;
}

Funktionen des Plug-ins

Schaffe mit WordPress und Advanced Editor schöne Websites. Hier ist für dich, euch eine leicht lesbare und freundliche Anleitung.

Katze
Die Website verwendet funktionelle Cookies. Sie verwendet keine Cookies von Drittanbietern.

Aber hallo! – zur Begrüßung eine Rundfrage?

🧡 … das so zum Zeit entschleunigen.

Die Erstellung von Website-Inhalten erfordert oft kreative Ideen. Es wäre interessant zu erfahren, wo Ideen für die Gestaltung der Website-Inhalte gefunden werden und wie diese gestaltet werden. Bitte mitteilen, wie typischerweise Inspiration für die Website-Inhalte gefunden wird.

Sie können die Tastaturnavigation nutzen: Tab zum Fokussieren, Leertaste zum Auswählen und Esc zum Schließen der Rundfrage.

Was sind die besten Quellen zur
Erstellung von Website-Inhalten?
Bis zu drei Antworten sind möglich!



Start der Umfrage im September 2024

ERGEBNISSE

Wird geladen ... Wird geladen ...

Falls alle Optionen ausgewählt werden sollten, sollte man im Ausschussverfahren herausfinden, welches die Hauptsächlichen sind.


Im Inhalt die Tastaturnavigation:

Sobald Sie die Website scrollen, können Sie die Enter-Taste drücken, um den Inhalt direkt zu fokussieren, ohne den Tabindex zu durchlaufen. Das zweite Enter aktiviert das Element.

  • Tabulator (Tab): Navigiert durch interaktive Elemente.
  • Shift + Tab: Gehe zurück zum vorherigen Element.
  • Enter: Erste Betätigung fokussiert, zweite aktiviert.
  • Pfeiltasten: Scrollen in Texten oder Menüs.
  • Leertaste: Aktiviert Header-Buttons und scrollt im Content.
  • Shift + Leertaste: Scrollt nach oben.
  • 7: Aktiviert die Suche; Esc zum Schließen.
  • Esc: Bricht Fokussierung und Dialoge ab.

Echte Besucher statt nur Klicks, siehe:
effektives Tracking mit Statify! (neuer Tab)

So in 8-Tage mag sein 80 Besucher

Zur Optimierung unserer Website nutzen wir Tools wie 'Statify' sowie 'Visit Duration', das anonymisierte Daten zur Verweildauer, erfasst.

Danke sehr!

 🎶  Während des Besuchs kann es neben informativen Ergebnissen auch zu Klangeffekten kommen, um bestimmte Elemente hervorzuheben. – Viel Spaß beim Erkunden!


Erfolgreichen Besuch
wünscht Ihnen! – WP Wegerl.at

Pop-up  

Das zur Umfrage ist von WP-Polls und
das Pop-up ist von Boxzilla.

WP Wegerl.at
Leistungsmetriken im Blick
× -
Index