64 chars, * the CHAR(64) column width). * @param int $limit Max events inside the window (<= 0 = unlimited). * @param int $window_seconds Window length in seconds. * @return bool True when the attempt is allowed. */ public function allow( string $bucket_key, int $limit, int $window_seconds ): bool { if ( $limit <= 0 ) { return true; } global $wpdb; $bucket = strlen( $bucket_key ) <= 64 ? $bucket_key : hash( 'sha256', $bucket_key ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->insert( $wpdb->prefix . 'agyl_rate_events', array( 'bucket' => $bucket, 'event_time' => gmdate( 'Y-m-d H:i:s' ), ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM ' . $wpdb->prefix . 'agyl_rate_events WHERE bucket = %s AND event_time > DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d SECOND)', $bucket, max( 1, $window_seconds ) ) ); return $count <= $limit; } /** * Bucket id for per-session message throttling. * * @param int $session_id Session row id. */ public static function bucket_session_msg( int $session_id ): string { return 'session:' . $session_id . ':msg'; } /** * Bucket id for per-IP throttling. The sha256 hash is truncated to 40 * chars so every ip bucket fits the CHAR(64) column with prefix and * suffix intact (collisions are negligible at 160 bits). * * @param string $ip_hash Sha256 IP hash (SessionManager::hash_ip()). * @param string $suffix One of 'msg_h', 'msg_d', 'session'. */ public static function bucket_ip( string $ip_hash, string $suffix ): string { return 'ip:' . substr( $ip_hash, 0, 40 ) . ':' . $suffix; } }