back( $redirect, [ 'spam' => __( 'Your session expired, please reload the page and try again.', 'admirly' ) ] ); } $ip = self::clientIp(); if ( ! ( new RateLimiter( new TransientStore(), 3, 60 ) )->allow( $ip ) ) { $this->back( $redirect, [ 'spam' => __( 'Too many submissions. Please try again in a minute.', 'admirly' ) ] ); } $keys = [ 'name', 'role', 'sub', 'rating', 'text', 'consent', 'website', 'started_at', 'lang' ]; /** @param string[] $keys Posted fields to collect; extensions add their own. */ $keys = (array) apply_filters( 'admirly_form_input_keys', $keys ); $input = []; foreach ( $keys as $k ) { // Generic cleanup here; the Validator (and extension filters) apply the field-specific rules next. $input[ $k ] = sanitize_textarea_field( wp_unslash( (string) ( $_POST[ $k ] ?? '' ) ) ); } $result = ( new Validator( Plugin::fields() ) )->validate( $input ); $errors = $result->errors(); if ( isset( $errors['spam'] ) ) { // Bot: silently show "success", store nothing. $this->back( $redirect, [], true ); } $data = $result->data(); /** @param array $errors field => message. @param array $input Cleaned raw input. @param array $data Validated core data. */ $errors = (array) apply_filters( 'admirly_validate', $errors, $input, $data ); if ( $errors !== [] ) { $this->back( $redirect, $errors, false, $input ); } $postId = wp_insert_post( [ 'post_type' => Plugin::CPT, 'post_status' => 'pending', 'post_title' => $data['name'], 'post_content' => $data['text'], ], true ); if ( is_wp_error( $postId ) ) { $this->back( $redirect, [ 'general' => __( 'The review could not be saved, please try again.', 'admirly' ) ], false, $input ); } update_post_meta( $postId, '_admirly_name', $data['name'] ); update_post_meta( $postId, '_admirly_role', $data['role'] ); update_post_meta( $postId, '_admirly_sub', $data['sub'] ); update_post_meta( $postId, '_admirly_rating', $data['rating'] ); update_post_meta( $postId, '_admirly_consent_at', current_time( 'mysql' ) ); update_post_meta( $postId, '_admirly_ip_hash', hash( 'sha256', $ip . wp_salt() ) ); update_post_meta( $postId, '_admirly_source', 'form' ); // The page language comes from the form (admin-post.php has no front-end language context). $lang = \Admirly\Domain\Lang::normalize( $input['lang'] ); update_post_meta( $postId, '_admirly_lang', $lang !== '' ? $lang : Multilingual::current() ); /** @param int $postId New pending review. @param array $data Validated core data. @param array $input Cleaned raw input. */ do_action( 'admirly_review_submitted', (int) $postId, $data, $input ); $this->back( $redirect, [], true ); } /** * Visitor IP for rate limiting. Behind a proxy the `admirly_client_ip` filter can supply the forwarded address. */ public static function clientIp(): string { $ip = sanitize_text_field( wp_unslash( (string) ( $_SERVER['REMOTE_ADDR'] ?? '' ) ) ); $ip = (string) apply_filters( 'admirly_client_ip', $ip ); return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0'; } /** * Stores the result in a transient and redirects back to the form. * @param array $errors @param array $old */ private function back( string $redirect, array $errors, bool $ok = false, array $old = [] ): void { $token = wp_generate_password( 16, false ); unset( $old['website'], $old['consent'] ); set_transient( 'admirly_flash_' . $token, [ 'ok' => $ok, 'errors' => $errors, 'old' => $old, ], 5 * MINUTE_IN_SECONDS ); wp_safe_redirect( add_query_arg( 'admirly', $token, $redirect ) . '#admirly-form' ); exit; } /** State on return to the form: ['ok'=>bool,'errors'=>[],'old'=>[]] or null. @return array|null */ public static function flash(): ?array { $token = sanitize_text_field( wp_unslash( (string) ( $_GET['admirly'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- random one-time token, read-only if ( ! preg_match( '/^[A-Za-z0-9]{16}$/', $token ) ) { return null; } $v = get_transient( 'admirly_flash_' . $token ); if ( ! is_array( $v ) ) { return null; } delete_transient( 'admirly_flash_' . $token ); // one-time: a reload shows a fresh form, the token cannot be replayed return $v; } }