0 ) { self::fail( array(), __( 'The uploaded files are too large in total. Please send smaller files.', 'alpeg-forms' ), null, array() ); } $post = wp_unslash( $_POST ); $config = Token::decode( isset( $post['_af_token'] ) ? $post['_af_token'] : '' ); // On multisite all sites share the salts: a token is only valid on the site that issued it. // Tokens from before 0.3.1 carry no site ID and stay valid (cached pages keep working). if ( $config && isset( $config['blog'] ) && (int) $config['blog'] !== get_current_blog_id() ) { $config = null; } if ( ! $config || empty( $config['fields'] ) ) { self::fail( array(), __( 'The form has expired or is invalid. Please reload the page and try again.', 'alpeg-forms' ), null, array() ); } // Honeypot: bots fill in the hidden field. We pretend everything is fine. if ( ! empty( $post['af_website'] ) ) { self::succeed( $config ); } // Timing check: humans need a few seconds, bots do not. $elapsed = '1' === ( isset( $post['_af_js'] ) ? $post['_af_js'] : '' ) ? (int) ( isset( $post['_af_t'] ) ? $post['_af_t'] : 0 ) / 1000 : time() - (int) $config['ts']; if ( $elapsed < self::MIN_SECONDS ) { self::fail( array(), __( 'That was quick! Please wait a moment and then submit the form again.', 'alpeg-forms' ), $config, array() ); } if ( self::rate_limited() ) { self::fail( array(), __( 'Too many requests were sent just now. Please try again in a few minutes.', 'alpeg-forms' ), $config, array() ); } $fields = Fields::normalize( $config['fields'] ); $input = isset( $post['af'] ) && is_array( $post['af'] ) ? $post['af'] : array(); $visible = Fields::visibility( $fields, $input ); $errors = array(); $values = array(); $data = array(); $uploads = array(); $email = ''; foreach ( $fields as $field ) { if ( ! Fields::is_input( $field ) || empty( $visible[ $field['id'] ] ) ) { continue; // Layout element or hidden by a condition. } if ( 'file' === $field['type'] ) { $files = Files::from_request( $field['id'] ); $error = Files::validate( $field, $files ); if ( $error ) { $errors[ $field['id'] ] = $error; } else { $uploads[ $field['id'] ] = $files; } continue; } $raw = isset( $input[ $field['id'] ] ) ? $input[ $field['id'] ] : ''; list( $value, $error ) = Fields::validate( $field, $raw ); $values[ $field['id'] ] = is_array( $raw ) ? array_map( 'sanitize_text_field', $raw ) : sanitize_textarea_field( (string) $raw ); if ( $error ) { $errors[ $field['id'] ] = $error; continue; } if ( '' === $email && 'email' === $field['type'] && '' !== $value ) { $email = $value; } $data[ $field['id'] ] = array( 'id' => $field['id'], 'label' => 'consent' === $field['type'] ? __( 'Consent', 'alpeg-forms' ) : $field['label'], 'type' => $field['type'], 'value' => $value, ); } // phpcs:enable /** * Add custom checks (e.g. from a Pro module). * * @param array $errors Field ID => message. * @param array $data Cleaned values. * @param array $config Form configuration. */ $errors = (array) apply_filters( 'alpeg_forms_validate', $errors, array_values( $data ), $config ); if ( $errors ) { self::fail( $errors, Fields::messages()['summary'], $config, $values ); } // Only now, after everything is valid, move the files into storage. foreach ( $uploads as $field_id => $files ) { $field = wp_list_filter( $fields, array( 'id' => $field_id ) ); $field = reset( $field ); $stored = Files::store( $files ); $data[ $field_id ] = array( 'id' => $field_id, 'label' => $field['label'], 'type' => 'file', 'value' => implode( ', ', wp_list_pluck( $stored, 'name' ) ), 'files' => $stored, ); if ( count( $stored ) !== count( $files ) ) { Files::delete_for_entry( (object) array( 'data' => array_values( $data ) ) ); self::fail( array(), __( 'An uploaded file could not be saved. Please select your files again and retry.', 'alpeg-forms' ), $config, $values ); } } // Keep the order of the form. $ordered = array(); foreach ( $fields as $field ) { if ( isset( $data[ $field['id'] ] ) ) { $ordered[] = $data[ $field['id'] ]; } } $data = $ordered; $entry = array( 'form_id' => $config['fid'], 'form_title' => $config['title'], 'post_id' => (int) $config['pid'], 'page_url' => self::page_url( $config ), 'data' => $data, 'email' => $email, 'ip' => self::ip(), 'user_agent' => substr( sanitize_text_field( isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : '' ), 0, 255 ), 'user_id' => get_current_user_id(), 'consent' => wp_list_filter( $data, array( 'type' => 'consent' ) ) ? 1 : 0, ); /** * Adjust an entry before saving. Returning false prevents saving. * * @param array|false $entry Record. * @param array $config Form configuration. */ $entry = apply_filters( 'alpeg_forms_entry', $entry, $config ); $entry_id = 0; if ( is_array( $entry ) ) { $entry_id = (int) Entries::insert( $entry ); if ( ! $entry_id ) { foreach ( $data as $row ) { if ( ! empty( $row['files'] ) ) { Files::delete_for_entry( (object) array( 'data' => array( $row ) ) ); } } self::fail( array(), __( 'Unfortunately your message could not be saved. Please try again later.', 'alpeg-forms' ), $config, $values ); } } $result = Mailer::notify( $config, $data, $entry_id, is_array( $entry ) ? $entry['page_url'] : '' ); if ( $entry_id ) { Entries::set_mail_status( $entry_id, $result['status'], $result['error'] ); } if ( ! empty( $config['confirm']['on'] ) && $email ) { Mailer::confirm( $config, $data, $email ); } /** * After a successful submission (hook for webhooks, CRM etc.). * * @param int $entry_id ID (0 if not saved). * @param array $data Values. * @param array $config Form configuration. */ do_action( 'alpeg_forms_submitted', $entry_id, $data, $config ); self::succeed( $config ); } /** * Report success and exit. * * @param array $config Configuration. */ private static function succeed( array $config ) { $message = ! empty( $config['success'] ) ? $config['success'] : Renderer::defaults()['successMessage']; if ( self::is_ajax() ) { wp_send_json( array( 'success' => true, 'message' => $message, ) ); } $url = add_query_arg( array( 'alpeg_form' => $config['fid'], 'alpeg_form_status' => 'ok', ), self::back_url() ); wp_safe_redirect( $url . '#' . rawurlencode( $config['fid'] ) ); exit; } /** * Report failure and exit. * * @param array $field_errors Field ID => message. * @param string $message General message. * @param array|null $config Configuration (null for an invalid token). * @param array $values Input to restore (non-JS only). */ private static function fail( array $field_errors, $message, $config, array $values ) { if ( self::is_ajax() ) { wp_send_json( array( 'success' => false, 'message' => $message, 'errors' => (object) $field_errors, ), $config ? 422 : 400 ); } if ( ! $config ) { wp_die( esc_html( $message ), esc_html__( 'Form', 'alpeg-forms' ), array( 'back_link' => true, 'response' => 400 ) ); } $key = strtolower( wp_generate_password( 20, false ) ); set_transient( 'alpeg_forms_state_' . $key, array( 'errors' => array_merge( array( $message ), array_values( $field_errors ) ), 'field_errors' => $field_errors, 'values' => $values, ), 10 * MINUTE_IN_SECONDS ); $url = add_query_arg( array( 'alpeg_form' => $config['fid'], 'alpeg_form_key' => $key, ), self::back_url() ); wp_safe_redirect( $url . '#' . rawurlencode( $config['fid'] ) ); exit; } /** * Back to the page with the form. * * @return string */ private static function back_url() { $ref = wp_get_referer(); $ref = $ref ? remove_query_arg( array( 'alpeg_form', 'alpeg_form_status', 'alpeg_form_key' ), $ref ) : ''; return wp_validate_redirect( $ref, home_url( '/' ) ); } /** * Page the form was sent from. * * @param array $config Configuration. * @return string */ private static function page_url( array $config ) { $ref = wp_get_referer(); if ( $ref && wp_validate_redirect( $ref, '' ) ) { return esc_url_raw( remove_query_arg( array( 'alpeg_form', 'alpeg_form_status', 'alpeg_form_key' ), $ref ) ); } return ! empty( $config['pid'] ) ? (string) get_permalink( (int) $config['pid'] ) : ''; } /** * IP address according to the setting (none, anonymized or full). * * @return string */ private static function ip() { $mode = alpeg_forms_setting( 'ip_mode', 'anonymized' ); if ( 'none' === $mode ) { return ''; } $ip = self::raw_ip(); return 'full' === $mode ? $ip : (string) wp_privacy_anonymize_ip( $ip ); } /** * IP from REMOTE_ADDR (proxy headers can be forged and are ignored on purpose). * * @return string */ private static function raw_ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; /** Adjustable for sites behind their own reverse proxy. */ $ip = (string) apply_filters( 'alpeg_forms_client_ip', $ip ); return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : ''; } /** * Simple rate limit per IP (hashed, the IP itself is not stored). * * @return bool true if the limit is exceeded. */ private static function rate_limited() { $ip = self::raw_ip(); if ( '' === $ip ) { return false; } $key = 'alpeg_forms_rl_' . substr( hash_hmac( 'sha256', $ip, wp_salt( 'nonce' ) ), 0, 32 ); $count = (int) get_transient( $key ); if ( $count >= (int) apply_filters( 'alpeg_forms_rate_limit', self::RATE_LIMIT ) ) { return true; } set_transient( $key, $count + 1, self::RATE_WINDOW ); return false; } }