0 && !$review_locked)) { return false; } $now = time(); $window_started = (int) (floor($now / $window_seconds) * $window_seconds); $subject_hash = hash('sha256', $subject); $expires_at = gmdate('Y-m-d H:i:s', $window_started + $window_seconds + 60); $updated_at = gmdate('Y-m-d H:i:s', $now); $table = Database::table('commentmate_rate_limits'); $query = $wpdb->prepare('INSERT INTO %i (review_id, bucket, subject_hash, window_started, request_count, expires_at, updated_at) VALUES (%d, %s, %s, %d, 1, %s, %s) ON DUPLICATE KEY UPDATE request_count = request_count + 1, updated_at = VALUES(updated_at)', $table, $review_id, $bucket, $subject_hash, $window_started, $expires_at, $updated_at); if (false === $wpdb->query($query)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The INSERT directly above is prepared with identifier and value placeholders; atomic increment cannot use cached results. Atomic plugin-table counters require fresh reads; cached counters would weaken request limits. // Fail closed for guest mutations if the rate-limit store is unavailable. return false; } $count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic plugin-table counters require fresh reads; cached counters would weaken request limits. $wpdb->prepare('SELECT request_count FROM %i WHERE review_id = %d AND bucket = %s AND subject_hash = %s AND window_started = %d LIMIT 1', $table, $review_id, $bucket, $subject_hash, $window_started) ); // Opportunistic, bounded cleanup avoids a scheduled task solely for limits. $wpdb->query($wpdb->prepare('DELETE FROM %i WHERE expires_at < UTC_TIMESTAMP() LIMIT 100', $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic plugin-table counters require fresh reads; cached counters would weaken request limits. return $count > 0 && $count <= $limit; } }