$delay ) { if ( ! WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ) ) { // Leave no partial window behind: a half-applied limit would block the // customer on an attempt that never went through. The failed limit is // cleared too, since set_rate_limit() caches the new expiry even when the // write itself fails. foreach ( array_merge( array( $rate_limit_id ), $applied_rate_limit_ids ) as $applied_rate_limit_id ) { WC_Rate_Limiter::set_rate_limit( $applied_rate_limit_id, -1 ); } return false; } $applied_rate_limit_ids[] = $rate_limit_id; } return true; } /** * Get the rate limits that apply to the current sign-up attempt, keyed by rate limit ID. * * Buckets with a zero delay are left out, so switching one off writes nothing. * * @param string $user_email The e-mail address used to sign up. * @return array */ private static function get_rate_limits( string $user_email ): array { $options = self::get_options(); $rate_limits = array(); if ( ! $options['enabled'] ) { return $rate_limits; } if ( $options['client_delay'] > 0 ) { if ( is_user_logged_in() ) { $rate_limits[ self::RATE_LIMIT_USER_PREFIX . get_current_user_id() ] = $options['client_delay']; } else { $ip_address = self::get_ip_address( $options['proxy_support'] ); if ( '' !== $ip_address ) { $rate_limits[ self::RATE_LIMIT_IP_PREFIX . hash( 'sha256', $ip_address ) ] = $options['client_delay']; } } } $user_email = strtolower( trim( $user_email ) ); if ( '' !== $user_email && $options['email_delay'] > 0 ) { $rate_limits[ self::RATE_LIMIT_EMAIL_PREFIX . hash( 'sha256', $user_email ) ] = $options['email_delay']; } return $rate_limits; } /** * Get the IP address to rate limit a guest request on. * * Only REMOTE_ADDR is trusted by default: forwarded headers are attacker-controlled * unless the store is actually behind a proxy that sets them. With proxy support on, * the address comes from the forwarding headers the rest of WooCommerce trusts. * * @param bool $proxy_support Whether to read the client address from forwarding headers. * @return string The IP address, or an empty string if it could not be resolved. */ private static function get_ip_address( bool $proxy_support ): string { if ( $proxy_support ) { return WC_Geolocation::get_ip_address(); } $ip_address = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- format-validated below via rest_is_ip_address(). return (string) rest_is_ip_address( $ip_address ); } /** * Get the rate limiting options, with the filter applied and every value coerced to its type. * * @return array{enabled: bool, proxy_support: bool, client_delay: int, email_delay: int} */ private static function get_options(): array { $defaults = array( 'enabled' => self::ENABLED, 'proxy_support' => self::PROXY_SUPPORT, 'client_delay' => (int) self::CLIENT_DELAY, 'email_delay' => (int) self::EMAIL_DELAY, ); /** * Filter: woocommerce_customer_stock_notifications_signup_rate_limit_options * * Options for rate limiting stock notification sign-ups. Mirrors the shape of * `woocommerce_store_api_rate_limit_options`. * * - `enabled`: switches the limiter off entirely. Default true. * - `proxy_support`: read the client address from forwarding headers (X-Real-IP, * X-Forwarded-For) rather than REMOTE_ADDR. Enable only when the store is behind a * proxy, load balancer or CDN that sets them, since the headers are otherwise * spoofable. Default false. * - `client_delay`: seconds a client (the logged-in user, or the IP address for * guests) has to wait between two sign-up attempts. 0 switches this limit off. * Default 30. * - `email_delay`: seconds an e-mail address has to wait between two sign-up attempts. * 0 switches this limit off. Default 30. * * @since 11.2.0 * * @param array $options Rate limiting options. */ $options = apply_filters( 'woocommerce_customer_stock_notifications_signup_rate_limit_options', $defaults ); if ( ! is_array( $options ) ) { $options = $defaults; } // A callback can return anything: unknown or non-numeric values fall back to the // default, and a negative delay would clear the limit instead of setting one, so it // is clamped to zero. return array( 'enabled' => self::to_bool( $options['enabled'] ?? $defaults['enabled'], $defaults['enabled'] ), 'proxy_support' => self::to_bool( $options['proxy_support'] ?? $defaults['proxy_support'], $defaults['proxy_support'] ), 'client_delay' => is_numeric( $options['client_delay'] ?? null ) ? max( 0, (int) $options['client_delay'] ) : $defaults['client_delay'], 'email_delay' => is_numeric( $options['email_delay'] ?? null ) ? max( 0, (int) $options['email_delay'] ) : $defaults['email_delay'], ); } /** * Coerce a filtered option to a boolean, accepting the usual 'yes'/'true'/1 spellings. * * @param mixed $value The filtered value. * @param bool $fallback The value to use when the filtered value spells no boolean. */ private static function to_bool( $value, bool $fallback ): bool { if ( is_bool( $value ) ) { return $value; } $filtered = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); return null === $filtered ? $fallback : $filtered; } }