namespace WPForms\Integrations\Stripe;
* Stripe error rate limiting.
* Allowed number of attempts.
private $allowed_attempts;
* Rate Limit block expiration time.
* Perform certain things on class init.
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* This filter allow to modify Stripe rate limit attempts count.
* @param int $count Attempts count.
$this->allowed_attempts = (int) apply_filters( 'wpforms_stripe_rate_limit_allowed_attempts', 3 );
* This filter allow to modify Stripe rate limit expiration time.
* @param int $expires_in Expiration time.
$this->expires_in = (int) apply_filters( 'wpforms_stripe_rate_limit_expires_in', HOUR_IN_SECONDS * 6 );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
* Check if rate limit is under threshold and passes.
public function is_ok() {
$entry = $this->get_entry();
if ( empty( $entry['attempts'] ) ) {
if ( $entry['attempts'] < $this->allowed_attempts ) {
$this->increment_attempts( $entry );
* Increment the number of attempts for a specific IP address.
* @param array $entry Rate limit entry data.
public function increment_attempts( $entry = [] ) {
$entry = $this->get_entry();
$entry['attempts'] = (int) $entry['attempts'] + 1;
return $this->update_entry( $entry['attempts'] );
* Get rate limit entry id based on IP address.
private function get_entry_id() {
return 'wpforms_stripe_attempt_' . wp_hash( wpforms_get_ip() );
* Get rate limit entry attempts and expiration data.
private function get_entry() {
$storage = $this->get_storage_type();
$entry_id = $this->get_entry_id();
if ( $storage === 'file' ) {
return $this->get_file_entry( $entry_id );
if ( $storage === 'transient' ) {
return $this->get_transient_entry( $entry_id );
* Update rate limit entry attempts and expiration data.
* @param int $attempts Number of attempts to set.
private function update_entry( $attempts ) {
$storage = $this->get_storage_type();
$entry_id = $this->get_entry_id();
if ( $storage === 'file' ) {
return $this->update_file_entry( $entry_id, $attempts );
if ( $storage === 'transient' ) {
return $this->update_transient_entry( $entry_id, $attempts );
* Get a storage type where rate limit entries are saved.
private function get_storage_type() {
$file = $this->get_file_path();
if ( ! file_exists( $file ) ) {
$this->create_file( $file );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
if ( ! is_writable( $file ) ) {
* Get file path to store the rate limit entries in.
private function get_file_path() {
if ( function_exists( 'wpforms_upload_dir' ) ) {
$upload_dir = wpforms_upload_dir();
if ( isset( $upload_dir['path'] ) ) {
$upload_dir['path'] = trailingslashit( $upload_dir['path'] ) . 'stripe';
$file_name = wp_hash( site_url() ) . '-rate-limiting.log';
return isset( $upload_dir['path'] ) ? wp_normalize_path( trailingslashit( $upload_dir['path'] ) . $file_name ) : '';
* Create index.html file in the specified directory if it doesn't exist.
* @return bool True if file exists or was successfully created, false on failure.
private function create_index_html_file() {
$file = $this->get_file_path();
$index_file = wp_normalize_path( trailingslashit( dirname( $file ) ) . 'index.html' );
// Do nothing if index.html exists in the directory.
if ( file_exists( $index_file ) ) {
// Create empty index.html.
// phpcs:ignore WordPress.WP.AlternativeFunctions
return file_put_contents( $index_file, '' ) !== false;
* Create a file path to store the rate limit entries in.
* @param string $file File path.
private function create_file( $file ) {
if ( ! wp_mkdir_p( dirname( $file ) ) ) {
if ( ! $this->create_index_html_file() ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
if ( file_put_contents( $file, '' ) === false ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
* Read full contents of a rate limit entries file.
private function read_whole_file() {
$file = $this->get_file_path();
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$contents = file_get_contents( $file );
$contents = json_decode( $contents, true );
return is_array( $contents ) ? $contents : [];
* Write full contents to a rate limit entries file.
* @param array $contents Array of all rate limit entries.
private function write_whole_file( $contents ) {
if ( ! is_array( $contents ) ) {
$file = $this->get_file_path();
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
if ( ! is_writable( $file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions
return (bool) file_put_contents( $file, wp_json_encode( $contents ) );
* Filter out the expired rate limit entries from a file.
* @param array $contents Array of all rate limit entries.
* @param string $entry_id Rate limit entry id.
private function filter_expired_file_entry( $contents, $entry_id ) {
$expiration = isset( $contents[ $entry_id ]['expiration'] ) ? (int) $contents[ $entry_id ]['expiration'] : false;
if ( empty( $expiration ) ) {
if ( $expiration >= time() ) {
unset( $contents[ $entry_id ] );
$this->write_whole_file( $contents );
* Get rate limit entry attempts and expiration data from a file.
* @param string $entry_id Rate limit entry id.
private function get_file_entry( $entry_id ) {
$contents = $this->read_whole_file();
$contents = $this->filter_expired_file_entry( $contents, $entry_id );
'attempts' => isset( $contents[ $entry_id ]['attempts'] ) ? $contents[ $entry_id ]['attempts'] : false,
'expiration' => isset( $contents[ $entry_id ]['expiration'] ) ? $contents[ $entry_id ]['expiration'] : false,
* Update rate limit entry attempts and expiration data in a file.
* @param string $entry_id Rate limit entry id.
* @param int $attempts Number of attempts to set.
private function update_file_entry( $entry_id, $attempts ) {
if ( ! $this->create_index_html_file() ) {
$contents = $this->read_whole_file();
$contents[ $entry_id ] = [
'expiration' => time() + $this->expires_in,
return $this->write_whole_file( $contents );
* Get rate limit entry attempts and expiration data from a transient.
* @param string $entry_id Rate limit entry id.
private function get_transient_entry( $entry_id ) {
'attempts' => get_transient( $entry_id ),
'expiration' => get_option( '_transient_timeout_' . $entry_id ),
* Update rate limit entry attempts and expiration data in a transient.
* @param string $entry_id Rate limit entry id.
* @param int $attempts Number of attempts to set.
private function update_transient_entry( $entry_id, $attempts ) {
return set_transient( $entry_id, $attempts, $this->expires_in );