comment_author_IP : ( $comment['comment_author_IP'] ?? '' ); if ( '' === $ip ) { return; } $anonymized = self::anonymize_ip( $ip ); if ( $anonymized === $ip ) { return; } global $wpdb; // wp_update_comment() wuerde edit_comment/transition-Hooks erneut ausloesen; hier soll nur das IP-Feld still ueberschrieben werden, Cache wird direkt danach geleert. $wpdb->update( $wpdb->comments, array( 'comment_author_IP' => $anonymized ), array( 'comment_ID' => (int) $comment_id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- gezieltes Update nur des IP-Felds, clean_comment_cache() folgt. clean_comment_cache( (int) $comment_id ); } /** * IPv4: letztes Oktett auf 0 (Standard vieler DSGVO-Leitfaeden, * z. B. Googles eigene "IP-Anonymisierung"). IPv6: letzte 80 Bit auf 0 * (nur die ersten 48 Bit, der vom Provider zugewiesene Praefix, bleiben * fuer grobe Geo-/Missbrauchs-Auswertung erhalten; Subnetz-ID und * Geraeteteil, die den einzelnen Anschluss identifizieren, entfallen -- * wie bei wp_privacy_anonymize_ip() in WordPress selbst). */ public static function anonymize_ip( $ip ) { $ip = trim( (string) $ip ); if ( '' === $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { $parts = explode( '.', $ip ); if ( 4 !== count( $parts ) ) { return $ip; } $parts[3] = '0'; return implode( '.', $parts ); } $packed = @inet_pton( $ip ); // phpcs:ignore WordPress.PHP.NoSilencedErrors if ( false === $packed || 16 !== strlen( $packed ) ) { return $ip; } // Nur die ersten 6 Bytes (48 Bit Provider-Praefix) behalten, Rest nullen. $anonymized = substr( $packed, 0, 6 ) . str_repeat( "\0", 10 ); return inet_ntop( $anonymized ); } public static function is_anonymized( $ip ) { return self::anonymize_ip( $ip ) === $ip; } /** * Anzahl Kommentare mit noch nicht anonymisierter IP -- fuer Status-Tab und Gesundheitscheck. * Bereits anonymisierte IPs (IPv4 auf ".0", IPv6 auf "::") filtert die Abfrage vorab aus, * damit die Stichprobe (LIMIT) nur echte Kandidaten enthaelt und "keine" nicht vorgetaeuscht * wird, solange aeltere Kommentare noch Roh-IPs tragen. */ public static function pending_count() { global $wpdb; $ips = $wpdb->get_col( "SELECT comment_author_IP FROM {$wpdb->comments} WHERE comment_author_IP != '' AND comment_author_IP NOT LIKE '%.0' AND comment_author_IP NOT LIKE '%::' ORDER BY comment_ID DESC LIMIT 5000" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Status-Zaehlung ohne Benutzerwerte, Ergebnis darf nicht gecacht werden. $count = 0; foreach ( $ips as $ip ) { if ( ! self::is_anonymized( $ip ) ) { ++$count; } } return $count; } const CURSOR_OPTION = 'wp360_pt_cleanup_cursor'; /** * Ein Batch der Alt-Daten-Migration: verarbeitet bis zu BATCH_SIZE * Kommentare mit hoeherer ID als beim letzten Durchlauf. Ein * gespeicherter Cursor (statt einfach "die ersten N mit IP") ist noetig, * sonst wuerden wiederholte Aufrufe (taeglicher Cron, mehrfaches Klicken * von "Jetzt ausfuehren") auf einer Tabelle mit mehr als BATCH_SIZE * Kommentaren immer nur dieselben ersten Zeilen erneut betrachten und nie * bei den spaeteren ankommen. Erreicht der Cursor das Ende der Tabelle, * beginnt er wieder bei 0 -- ein voller Durchlauf ist danach guenstig * (nur SELECT, kein UPDATE), faengt aber zuverlaessig alles ab, was ueber * einen Weg ausserhalb von preprocess_comment/wp_insert_comment * eingefuegt wurde. * * @return int Anzahl tatsaechlich geaenderter Kommentare in diesem Batch. */ public static function run_cleanup() { if ( ! Wp360_Privacy_Toolkit_Settings::get()['anonymize_comment_ips'] ) { return 0; } global $wpdb; $cursor = (int) get_option( self::CURSOR_OPTION, 0 ); // get_comments() liefert keinen ID-Cursor ("comment_ID > X") und laedt volle Kommentar-Objekte samt Meta-Cache -- fuer den Batch-Durchlauf ueber grosse Tabellen zu teuer. $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Batch-Wartungsabfrage mit ID-Cursor, Ergebnis darf nicht gecacht werden. $wpdb->prepare( "SELECT comment_ID, comment_author_IP FROM {$wpdb->comments} WHERE comment_ID > %d AND comment_author_IP != '' ORDER BY comment_ID ASC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nur der Tabellenname ist eingesetzt, Werte laufen ueber prepare(). $cursor, self::BATCH_SIZE ) ); $changed = 0; $last_id = $cursor; foreach ( $rows as $row ) { $last_id = (int) $row->comment_ID; $anonymized = self::anonymize_ip( $row->comment_author_IP ); if ( $anonymized === $row->comment_author_IP ) { continue; } $wpdb->update( $wpdb->comments, array( 'comment_author_IP' => $anonymized ), array( 'comment_ID' => $last_id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Batch-Update nur des IP-Felds ohne edit_comment-Hooks, clean_comment_cache() folgt. clean_comment_cache( $last_id ); ++$changed; } // Weniger als ein voller Batch zurueckbekommen -- Ende der Tabelle // erreicht, Cursor fuer den naechsten vollen Durchlauf zuruecksetzen. update_option( self::CURSOR_OPTION, count( $rows ) < self::BATCH_SIZE ? 0 : $last_id, false ); return $changed; } }