__( 'Spam-Schutz: Anfrage abgelehnt.', 'adventura-interactive-advent-calendar' ) ) ); } // Time-trap: submitting faster than humanly possible. $min = (int) $opts['spam_min_seconds']; if ( $min > 0 ) { $elapsed = isset( $_POST['elapsed'] ) ? (int) $_POST['elapsed'] : 0; if ( $elapsed < $min ) { wp_send_json_error( array( 'message' => __( 'Bitte nimm dir kurz Zeit zum Ausfüllen und versuche es erneut.', 'adventura-interactive-advent-calendar' ) ) ); } } } // IP rate-limit (independent of the honeypot toggle). $rate = (int) $opts['spam_rate_limit']; if ( $rate > 0 && ! $this->within_rate_limit( $rate ) ) { wp_send_json_error( array( 'message' => __( 'Zu viele Teilnahmen in kurzer Zeit. Bitte versuche es später erneut.', 'adventura-interactive-advent-calendar' ) ) ); } $day = isset( $_POST['day'] ) ? (int) $_POST['day'] : 0; if ( $day < 1 || $day > ADVK_Data::door_count() ) { wp_send_json_error( array( 'message' => __( 'Ungültiges Türchen.', 'adventura-interactive-advent-calendar' ) ) ); } // Participation is only allowed on the door's own day. Enforced here on // the server so it cannot be bypassed by a crafted request; the frontend // mirrors this by hiding the entry form on other days. Logged-in admins // bypass this so they can preview/test every door. if ( ! current_user_can( 'manage_options' ) && $day !== ADVK_Data::today_day() ) { wp_send_json_error( array( 'message' => __( 'Die Teilnahme ist nur am jeweiligen Tag des Türchens möglich.', 'adventura-interactive-advent-calendar' ) ) ); } $first = sanitize_text_field( wp_unslash( $_POST['first'] ?? '' ) ); $last = sanitize_text_field( wp_unslash( $_POST['last'] ?? '' ) ); $email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); $consent = isset( $_POST['consent'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['consent'] ) ); if ( '' === $first || '' === $last ) { wp_send_json_error( array( 'message' => __( 'Bitte Vor- und Nachnamen angeben.', 'adventura-interactive-advent-calendar' ) ) ); } if ( ! is_email( $email ) ) { wp_send_json_error( array( 'message' => __( 'Bitte eine gültige E-Mail-Adresse angeben.', 'adventura-interactive-advent-calendar' ) ) ); } if ( ! $consent ) { wp_send_json_error( array( 'message' => __( 'Bitte bestätige den Datenschutz.', 'adventura-interactive-advent-calendar' ) ) ); } // Cloudflare Turnstile check (skipped for logged-in admins so they can test). if ( ! empty( $opts['turnstile_enabled'] ) && '' !== trim( (string) $opts['turnstile_secret_key'] ) && ! current_user_can( 'manage_options' ) ) { $token = isset( $_POST['turnstile_token'] ) ? sanitize_text_field( wp_unslash( $_POST['turnstile_token'] ) ) : ''; if ( ! $this->verify_turnstile( $token, (string) $opts['turnstile_secret_key'] ) ) { wp_send_json_error( array( 'message' => __( 'Bot-Prüfung fehlgeschlagen. Bitte lade die Seite neu und versuche es erneut.', 'adventura-interactive-advent-calendar' ) ) ); } } // Optionally block a second participation per e-mail and day. if ( ! empty( $opts['prevent_duplicates'] ) && ADVK_Entries::email_exists_for_day( $email, $day ) ) { wp_send_json_error( array( 'message' => __( 'Mit dieser E-Mail hast du an diesem Türchen bereits teilgenommen.', 'adventura-interactive-advent-calendar' ) ) ); } // Decode answers { qIndex: selectedIndex } and followed social channels. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON string; decoded below and every value re-cast to int / sanitize_key(). $answers_raw = isset( $_POST['answers'] ) ? json_decode( wp_unslash( $_POST['answers'] ), true ) : array(); $answers_raw = is_array( $answers_raw ) ? $answers_raw : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON string; decoded below and every value re-cast via sanitize_key(). $social_raw = isset( $_POST['social'] ) ? json_decode( wp_unslash( $_POST['social'] ), true ) : array(); $social_raw = is_array( $social_raw ) ? $social_raw : array(); // Authoritative door data (with correct answers) lives server-side. $door = $this->get_door( $day ); if ( ! $door ) { wp_send_json_error( array( 'message' => __( 'Türchen nicht gefunden.', 'adventura-interactive-advent-calendar' ) ) ); } $quiz = isset( $door['quiz'] ) ? (array) $door['quiz'] : array(); $total_questions = count( $quiz ); $correct_count = 0; $answers_clean = array(); foreach ( $quiz as $qi => $q ) { $sel = isset( $answers_raw[ $qi ] ) ? (int) $answers_raw[ $qi ] : null; $answers_clean[ $qi ] = $sel; if ( null !== $sel && isset( $q['correct'] ) && (int) $q['correct'] === $sel ) { $correct_count++; } } // Sanitize social against allowed keys + this door's offered channels. $offered = isset( $door['social'] ) ? (array) $door['social'] : array(); $social_clean = array_values( array_intersect( array_map( 'sanitize_key', $social_raw ), $offered ) ); $losnr = 'ADV-' . str_pad( (string) $day, 2, '0', STR_PAD_LEFT ) . '-' . wp_rand( 1000, 9999 ); // Opt-ins (newsletter / reminder) and referral handling. $newsletter_optin = isset( $_POST['newsletter'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['newsletter'] ) ); $reminder_optin = isset( $_POST['reminder'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['reminder'] ) ); $referral_enabled = ! empty( $opts['referral_enabled'] ); $ref_code = ''; $referred_by = ''; if ( $referral_enabled ) { $ref_code = ADVK_Entries::generate_ref_code(); $inbound = isset( $_POST['ref'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_POST['ref'] ) ) ) : ''; $inbound = preg_replace( '/[^A-Z0-9]/', '', $inbound ); if ( '' !== $inbound && ADVK_Entries::ref_code_exists( $inbound ) ) { $referred_by = $inbound; } } // Consent proof: record which terms/privacy targets were shown at the // moment of consent (they may change later), alongside the timestamp // (created_at) and IP already stored on the entry. $consent_ref_parts = array(); if ( ! empty( $opts['terms_url'] ) ) { $consent_ref_parts[] = 'AGB: ' . $opts['terms_url']; } if ( ! empty( $opts['privacy_url'] ) ) { $consent_ref_parts[] = 'Datenschutz: ' . $opts['privacy_url']; } $consent_ref = implode( ' | ', $consent_ref_parts ); $id = ADVK_Entries::insert( array( 'day' => $day, 'first_name' => $first, 'last_name' => $last, 'email' => $email, 'consent' => $consent, 'losnr' => $losnr, 'answers' => $answers_clean, 'social' => $social_clean, 'correct_count' => $correct_count, 'total_questions' => $total_questions, 'ref_code' => $ref_code, 'referred_by' => $referred_by, 'ip' => $this->client_ip(), 'consent_ref' => $consent_ref, ) ); if ( ! $id ) { wp_send_json_error( array( 'message' => __( 'Speichern fehlgeschlagen. Bitte versuche es erneut.', 'adventura-interactive-advent-calendar' ) ) ); } /** * Fires after a participation entry is stored. Lets other code send * a confirmation email, push to a CRM/newsletter, store reminders, etc. */ $extra = array( 'first' => $first, 'last' => $last, 'newsletter' => $newsletter_optin, 'reminder' => $reminder_optin, 'ref' => $ref_code, ); do_action( 'advk_entry_created', $id, $day, $email, $losnr, $extra ); $response = array( 'losnr' => $losnr ); if ( $referral_enabled && '' !== $ref_code ) { $response['refUrl'] = esc_url_raw( add_query_arg( 'ref', $ref_code, home_url( '/' ) ) ); $response['refCode'] = $ref_code; } wp_send_json_success( $response ); } /** * Find the authoritative door (including correct quiz indices) by day. * * @param int $day Day. * @return array|null */ private function get_door( $day ) { foreach ( ADVK_Data::get_calendar() as $d ) { if ( (int) $d['day'] === (int) $day ) { return $d; } } return null; } private function client_ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; return substr( $ip, 0, 100 ); } /** * Verify a Cloudflare Turnstile token against the siteverify endpoint. * * @param string $token Token from the widget. * @param string $secret Turnstile secret key. * @return bool True when the token is valid. */ private function verify_turnstile( $token, $secret ) { if ( '' === $token ) { return false; } $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', array( 'timeout' => 10, 'body' => array( 'secret' => $secret, 'response' => $token, 'remoteip' => $this->client_ip(), ), ) ); if ( is_wp_error( $resp ) ) { return false; } $data = json_decode( wp_remote_retrieve_body( $resp ), true ); return is_array( $data ) && ! empty( $data['success'] ); } /** * Sliding hourly rate limit per IP using a transient counter. * * @param int $max Max submissions per IP per hour. * @return bool True if the request is still within the limit. */ private function within_rate_limit( $max ) { $ip = $this->client_ip(); if ( '' === $ip ) { return true; } $key = 'advk_rl_' . md5( $ip ); $count = (int) get_transient( $key ); if ( $count >= $max ) { return false; } set_transient( $key, $count + 1, HOUR_IN_SECONDS ); return true; } }