: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// Convert spam errors to form errors if spam entries are not stored.
if ( ! $store_spam_entries && ! empty( $this->spam_errors ) ) {
$this->errors = $this->spam_errors;
* Filter fields after processing.
* @param array $fields Form fields.
* @param array $entry Form submission raw data ($_POST).
* @param array $form_data Form data and settings.
$this->fields = apply_filters( 'wpforms_process_after_filter', $this->fields, $entry, $this->form_data );
// One last error check - don't proceed if there are any errors.
if ( ! empty( $this->errors[ $form_id ] ) ) {
if ( empty( $this->errors[ $form_id ]['header'] ) && empty( $this->errors[ $form_id ]['footer'] ) ) {
$this->errors[ $form_id ]['header'] = esc_html__( 'Form has not been submitted, please see the errors below.', 'wpforms-lite' );
$this->form_data['post_data_raw'] = [
'page_url' => isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '',
// Success - add entry to database.
$this->entry_id = $this->entry_save( $this->fields, $entry, $this->form_data['id'], $this->form_data );
// Add payment to database.
$payment_id = $this->payment_save( $entry );
$this->form_data['entry_meta'] = [
'page_url' => isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '',
'page_title' => isset( $_POST['page_title'] ) ? sanitize_text_field( wp_unslash( $_POST['page_title'] ) ) : '',
'page_id' => isset( $_POST['page_id'] ) ? absint( $_POST['page_id'] ) : '',
'referer' => esc_url_raw( (string) wp_get_referer() ),
$this->save_meta( $this->entry_id, $this->form_data['id'] );
* Runs right after adding entry to the database.
* @since 1.8.2 Added Payment ID param.
* @param array $fields Fields data.
* @param array $entry User submitted data.
* @param array $form_data Form data.
* @param int $entry_id Entry ID.
* @param int $payment_id Payment ID.
do_action( 'wpforms_process_entry_saved', $this->fields, $entry, $this->form_data, $this->entry_id, $payment_id );
// Fire the logic to send notification emails.
$this->entry_email( $this->fields, $entry, $this->form_data, $this->entry_id, 'entry' );
// Pass completed and formatted fields in POST.
$_POST['wpforms']['complete'] = $this->fields;
// Pass entry ID in POST.
$_POST['wpforms']['entry_id'] = $this->entry_id;
// Logs entry depending on log levels set.
if ( wpforms()->is_pro() ) {
$this->entry_id ? "Entry {$this->entry_id}" : 'Entry',
'parent' => $this->entry_id,
'form_id' => $this->form_data['id'],
// Does not proceed if a form is marked as spam.
if ( ! $marked_as_spam ) {
$this->process_complete( $form_id, $this->form_data, $this->fields, $entry, $this->entry_id );
$this->entry_confirmation_redirect( $this->form_data );
* @param int $entry_id Entry ID.
* @param int $form_id Form ID.
protected function save_meta( $entry_id, $form_id ) {
if ( ! wpforms()->is_pro() ) {
$meta_data = $this->form_data['entry_meta'];
$entry_meta = wpforms()->get( 'entry_meta' );
foreach ( $meta_data as $type => $value ) {
'user_id' => get_current_user_id(),
* @param array $entry Form submission raw data ($_POST).
* @param string $message Spam message.
private function log_spam_entry( $entry, $message ) {
'Spam Entry ' . uniqid( '', true ),
'form_id' => $this->form_data['id'],
* Check if the form was submitted too quickly.
private function time_limit_check() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
* Allow bypassing the time limit check.
* @param bool $bypass Whether to bypass the time limit check, default false.
* @param array $form_data Form data.
if ( apply_filters( 'wpforms_process_time_limit_check_bypass', false, $this->form_data ) ) {
$settings = $this->form_data['settings'];
$time_limit = ! empty( $settings['anti_spam']['time_limit'] ) ? $settings['anti_spam']['time_limit'] : [];
$enabled = ! empty( $time_limit['enable'] );
$duration = ! empty( $time_limit['duration'] ) ? absint( $time_limit['duration'] ) : 0;
if ( ! $enabled || $duration <= 0 ) {
// Convert seconds to milliseconds.
//phpcs:disable WordPress.Security.NonceVerification.Missing
$start = ! empty( $_POST['start_timestamp'] ) ? absint( $_POST['start_timestamp'] ) : 0;
$end = ! empty( $_POST['end_timestamp'] ) ? absint( $_POST['end_timestamp'] ) : 0;
//phpcs:enable WordPress.Security.NonceVerification.Missing
// Filter out empty fields.
return ! empty( $field['value'] );
// Skip time limit check if the form was submitted with prefilled values.
if ( $start === 0 && ! empty( $fields ) ) {
// If the form was submitted too quickly, add an error.
if ( ( $end - $start ) < $duration || $start === 0 ) {
$this->errors[ $this->form_data['id'] ]['header'] = esc_html__( 'Please wait a little longer before submitting. We’re running a quick security check.', 'wpforms-lite' );
* @param int $form_id Form ID.
* @param array $form_data Form data and settings.
* @param array $fields Fields data.
* @param array $entry Form submission raw data ($_POST).
* @param int $entry_id Entry ID.
public function process_complete( $form_id, $form_data, $fields, $entry, $entry_id ) {
* Runs right after the form has been successfully submitted.
* @since 1.8.3 Added $entry parameter.
* @param array $fields Fields data.
* @param array $entry Form submission raw data ($_POST).
* @param array $form_data Form data.
* @param int $entry_id Entry ID.
do_action( 'wpforms_process_complete', $fields, $entry, $form_data, $entry_id );
* Runs right after the form has been successfully submitted by form ID.
* @since 1.8.3 Added $entry parameter.
* @param array $fields Fields data.
* @param array $entry Form submission raw data ($_POST).
* @param array $form_data Form data.
* @param int $entry_id Entry ID.
do_action( "wpforms_process_complete_{$form_id}", $fields, $entry, $form_data, $entry_id );
* @param array $entry Form submission raw data ($_POST).
public function process_spam_check( $entry ) {
$this->process_captcha( $entry );
if ( $this->spam_reason ) {
$akismet = wpforms()->get( 'akismet' )->validate( $this->form_data, $entry );
// If Akismet marks the entry as spam, we want to log the entry and fail silently.
$this->spam_errors[ $this->form_data['id'] ]['header'] = $akismet;
// Log the spam entry depending on log levels set.
$this->log_spam_entry( $entry, $akismet );
$this->spam_reason = esc_html__( 'Akismet', 'wpforms-lite' );
* @param array $entry Form submission raw data ($_POST).
protected function is_bypass_spam_check( $entry ) {
* Filter to bypass CAPTCHA check.
* @param bool $bypass_captcha Whether to bypass CAPTCHA check.
* @param array $entry Form submission raw data ($_POST).
* @param array $form_data Form data.
return apply_filters( 'wpforms_process_bypass_captcha', false, $entry, $this->form_data );
* @since 1.8.3 Removed $captcha_settings parameter.
* @param array $entry Form submission raw data ($_POST).
private function process_captcha( $entry ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh,Generic.Metrics.CyclomaticComplexity.MaxExceeded
$captcha_settings = wpforms_get_captcha_settings();
if ( ! $this->allow_process_captcha( $entry, $captcha_settings ) ) {
$provider = $captcha_settings['provider'];
$current_captcha = $this->get_captcha( $provider );
if ( empty( $current_captcha ) ) {
$verify_url_raw = $current_captcha['verify_url_raw'];
$captcha_provider = $current_captcha['provider'];
$post_key = $current_captcha['post_key'];
/* translators: %s - The CAPTCHA provider name. */
$error = wpforms_setting( "{$provider}-fail-msg", sprintf( esc_html__( '%s verification failed, please try again later.', 'wpforms-lite' ), $captcha_provider ) );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
$token = ! empty( $_POST[ $post_key ] ) ? $_POST[ $post_key ] : false;
$is_recaptcha_v3 = $provider === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
if ( $is_recaptcha_v3 ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
$token = ! empty( $_POST['wpforms']['recaptcha'] ) ? $_POST['wpforms']['recaptcha'] : false;
'secret' => $captcha_settings['secret_key'],
'remoteip' => wpforms_get_ip(),
$this->errors[ $this->form_data['id'] ]['recaptcha'] = $error;
* hCaptcha uses user IP to better detect bots and their attacks on a form.
* Majority of our users have GDPR disabled.
* So we remove this data from the request only when it's not needed, depending on wpforms_is_collecting_ip_allowed($this->form_data) check.
if ( ! wpforms_is_collecting_ip_allowed( $this->form_data ) ) {
unset( $verify_query_arg['remoteip'] );
* Change query arguments for remote call to the captcha API.
* @param array $verify_query_arg The query arguments for verify URL.
* @param array $form_data Form data and settings.
$verify_query_arg = apply_filters( 'wpforms_process_captcha_verify_query_arg', $verify_query_arg, $this->form_data );
* Filter the CAPTCHA verify URL.
* @since 1.8.0 Added $form_data argument.
* @param string $verify_url The full CAPTCHA verify URL.
* @param string $verify_url_raw The CAPTCHA verify URL without query.
* @param array $verify_query_arg The query arguments for verify URL.
* @param array $form_data Form data and settings.
$verify_url = apply_filters( 'wpforms_process_captcha_verify_url', $verify_url_raw, $verify_url_raw, $verify_query_arg, $this->form_data );
$response = wp_remote_post( $verify_url, [ 'body' => $verify_query_arg ] );
$response_body = json_decode( wp_remote_retrieve_body( $response ), false );
empty( $response_body->success ) ||
( $is_recaptcha_v3 && $response_body->score <= wpforms_setting( 'recaptcha-v3-threshold', '0.4' ) )
if ( $is_recaptcha_v3 && isset( $response_body->score ) ) {
$error .= ' (' . esc_html( $response_body->score ) . ')';
$this->spam_errors[ $this->form_data['id'] ]['recaptcha'] = $error;
$this->log_spam_entry( $entry, $error );
$this->spam_reason = $captcha_provider;
* Check if CAPTCHA processing is allowed.
* @param array $entry Form entry data.
* @param array $captcha_settings CAPTCHA settings.
private function allow_process_captcha( $entry, $captcha_settings ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
// Skip captcha processing if AMP form.
if ( isset( $_POST['__amp_form_verify'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Skip captcha processing if provider is not set.
if ( empty( $captcha_settings['provider'] ) ) {
$provider = $captcha_settings['provider'];
// Skip captcha processing if provider is set to none.
if ( $provider === 'none' ) {
// Skip captcha processing if site key or secret key is empty.
if ( empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) {
$form_data_settings = isset( $this->form_data['settings'] ) ? $this->form_data['settings'] : [];
$is_recaptcha = isset( $form_data_settings['recaptcha'] ) && (int) $form_data_settings['recaptcha'] === 1;
// Skip captcha processing if reCAPTCHA is disabled for this form.
$recaptcha_type = $captcha_settings['recaptcha_type'];
$is_recaptcha_v3 = $provider === 'recaptcha' && $recaptcha_type === 'v3';
// Skip captcha processing on AMP if not using reCAPTCHA v3. AMP requires Google reCAPTCHA v3.
if ( ! $is_recaptcha_v3 && wpforms_is_amp() ) {
* Get all available CAPTCHA providers.
private function get_captcha_providers() {
* Filter the CAPTCHA providers.
* @param array $providers The CAPTCHA providers.
'wpforms_process_captcha_providers',
'verify_url_raw' => 'https://hcaptcha.com/siteverify',
'provider' => 'hCaptcha',
'post_key' => 'h-captcha-response',
'verify_url_raw' => 'https://www.google.com/recaptcha/api/siteverify',
'provider' => 'Google reCAPTCHA',
'post_key' => 'g-recaptcha-response',
'verify_url_raw' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
'provider' => 'Cloudflare Turnstile',
'post_key' => 'cf-turnstile-response', // The key is specified by the API.
* Get CAPTCHA provider data.
* @param string $provider CAPTCHA provider.