prefix . 'aistma_traffic_info'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table_name} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, viewed_at DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_post_id (post_id), KEY idx_viewed_at (viewed_at) ) {$charset_collate};"; dbDelta( $sql ); // Ensure we don't keep any previously created personal data columns. self::migrate_table_to_minimal_schema(); } /** * Log a view for a specific post. */ public static function log_post_view( int $post_id ): void { if ( $post_id <= 0 || ! is_single() ) { return; } global $wpdb; $table = $wpdb->prefix . 'aistma_traffic_info'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Required for analytics logging to custom table $wpdb->insert( $table, array( 'post_id' => $post_id, 'viewed_at' => current_time( 'mysql', true ), // GMT time ), array( '%d', '%s' ) ); } /** * Hook helper to log on template_redirect before headers are sent. */ public static function maybe_log_current_view(): void { if ( ! is_single() ) { return; } $post_id = get_queried_object_id(); // Restrict to standard posts to avoid attachments or custom types if undesired. if ( get_post_type( $post_id ) !== 'post' ) { return; } self::log_post_view( (int) $post_id ); } /** * Migrate existing traffic table to remove personal data columns if present. */ private static function migrate_table_to_minimal_schema(): void { global $wpdb; $table = $wpdb->prefix . 'aistma_traffic_info'; // Helper to drop a column if it exists. $columns_to_drop = array( 'ip_address', 'cookie_id', 'is_returning', 'user_agent' ); foreach ( $columns_to_drop as $col ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema migration check, temporary operation $exists = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s', $table, $col ) ); if ( (int) $exists > 0 ) { // Sanitize identifiers for DDL query - table name is controlled, column names are from predefined array $safe_table = esc_sql( $table ); $safe_col = esc_sql( $col ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "ALTER TABLE `{$safe_table}` DROP COLUMN `{$safe_col}`" ); } } } }