response = $response; parent::__construct( 'Stacklar REST response' ); } public function get_response() { return $this->response; } } } /* * Each action's handle() checks its nonce with verify_nonce_with() before it * calls the helpers below, which read the submitted form from $_POST. */ // phpcs:disable WordPress.Security.NonceVerification.Missing abstract class Stacklar_Action_Base { protected $stacklar_field_label_map_cache = null; protected $stacklar_label_mode_cache = null; protected $is_rest_request = false; abstract public function get_action(); abstract public function handle(); public function init() { // Kept for handlers that call parent::init(); REST dispatch owns action execution. } public function set_rest_request( $is_rest = true ) { $this->is_rest_request = (bool) $is_rest; } /** * Forget values cached from the previous dispatch's $_POST. Instances are * shared for the whole request (see stacklar_action_handlers()). */ public function reset_request_state() { $this->stacklar_field_label_map_cache = null; $this->stacklar_label_mode_cache = null; } protected function json_success( $data = array(), $status_code = 200 ) { if ( $this->is_rest_request ) { return new WP_REST_Response( array( 'success' => true, 'data' => $data, ), $status_code ); } if ( $status_code !== 200 ) { status_header( $status_code ); } wp_send_json_success( $data ); } protected function json_error( $data = array(), $status_code = 400 ) { if ( $this->is_rest_request ) { $response = new WP_REST_Response( array( 'success' => false, 'data' => $data, ), $status_code ); // The exception carries a WP_REST_Response for the REST controller; its message is fixed. throw new Stacklar_Rest_Response_Exception( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } status_header( $status_code ); wp_send_json_error( $data ); } // Collect POST data while removing reserved fields protected function collect_post_data( $reserved = array() ) { $reserved = array_merge( (array) $reserved, array( 'stacklar_action_settings', 'stacklar_actions', 'stacklar_entry_id', 'stacklar_entry_token', 'stacklar_locale', 'stacklar_submission_token', '__stacklar_tel_fields', '__stacklar_tel_messages', 'stacklar_autosave_token', '_stacklar_autosave_nonce', '_stacklar_config_nonce', '_stacklar_recaptcha_nonce', '__stacklar_hp', '__stacklar_protection_issued_at', '__stacklar_protection_sig', 'g-recaptcha-response', 'cf-turnstile-response', ) ); $out = array(); foreach ( $_POST as $key => $val ) { if ( is_string( $key ) ) { if ( strpos( $key, '__stacklar_' ) === 0 || strpos( $key, '_stacklar_' ) === 0 ) { continue; } } if ( in_array( $key, $reserved, true ) ) { continue; } if ( is_string( $key ) && substr( $key, -13 ) === '_intl_display' ) { continue; } $sanitized_key = is_string( $key ) ? sanitize_key( $key ) : $key; if ( '' === $sanitized_key && ! is_int( $key ) ) { continue; } $out[ $sanitized_key ] = $this->sanitize_post_value( wp_unslash( $val ) ); } if ( ! empty( $_POST['__stacklar_tel_fields'] ) ) { $out = $this->validate_tel_fields( $out ); } if ( isset( $out['__stacklar_field_labels'] ) ) { unset( $out['__stacklar_field_labels'] ); } if ( isset( $out['__stacklar_key_mode'] ) ) { unset( $out['__stacklar_key_mode'] ); } $form_id = isset( $_POST['stacklar_id'] ) ? absint( $_POST['stacklar_id'] ) : 0; if ( $form_id ) { $out = $this->apply_server_calculations( $form_id, $out ); } return $out; } /** * Remove hidden field values from a payload, based on: * 1) Hidden blocks and blocks with attribute hidden=true in the form content * 2) Password fields and their confirmation inputs, which must never be * stored in entries or forwarded to email, webhooks or integrations * 3) Without a stored form only, the hidden-field meta input * (__stacklar_hidden_meta) sent by the hidden block * * The request's hidden meta is ignored whenever the form is known: a * visitor could otherwise declare any priced field "hidden" and have it * removed before the payment amount is calculated. * * @param array $payload * @param WP_Post|mixed $form_post * @return array */ protected function strip_hidden_field_values( array $payload, $form_post = null ) { $hidden_names = array(); $has_form = $form_post && isset( $form_post->post_content ) && function_exists( 'parse_blocks' ); if ( ! $has_form && isset( $_POST['__stacklar_hidden_meta'] ) && is_array( $_POST['__stacklar_hidden_meta'] ) ) { // Only the field names (keys) are used, each through sanitize_key(). foreach ( array_map( 'sanitize_key', array_keys( (array) wp_unslash( $_POST['__stacklar_hidden_meta'] ) ) ) as $field_key ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( '' !== $field_key ) { $hidden_names[ $field_key ] = true; } } } if ( $has_form ) { $blocks = parse_blocks( (string) $form_post->post_content ); if ( is_array( $blocks ) ) { $this->collect_hidden_field_names_from_blocks( $blocks, $hidden_names ); $this->collect_password_field_names_from_blocks( $blocks, $hidden_names ); } } if ( empty( $hidden_names ) ) { if ( $form_post && isset( $form_post->ID ) ) { return $this->apply_server_calculations( $form_post->ID, $payload ); } return $payload; } $payload = $this->strip_payload_keys_recursive( $payload, $hidden_names ); if ( $form_post && isset( $form_post->ID ) ) { $payload = $this->apply_server_calculations( $form_post->ID, $payload ); } return $payload; } protected function apply_server_calculations( $form_id, array $payload ) { if ( function_exists( 'stacklar_calculations_service' ) ) { return stacklar_calculations_service()->apply_to_payload( $form_id, $payload ); } return $payload; } /** * Recursively collect hidden field names from parsed blocks. * * @param array $blocks * @param array $hidden_names * @return void */ protected function collect_hidden_field_names_from_blocks( array $blocks, array &$hidden_names ) { foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : ''; $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); if ( strpos( $block_name, 'stacklar/' ) === 0 ) { $is_hidden_block = ( 'stacklar/hidden' === $block_name ); $is_attr_hidden = ! empty( $attrs['hidden'] ); if ( $is_hidden_block || $is_attr_hidden ) { // An unrenamed field keeps its block.json default name. $raw_name = array_key_exists( 'name', $attrs ) ? $attrs['name'] : ( function_exists( 'stacklar_block_attribute_default' ) ? stacklar_block_attribute_default( $block_name, 'name' ) : '' ); $name = sanitize_key( (string) $raw_name ); if ( '' !== $name ) { $hidden_names[ $name ] = true; } } } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $this->collect_hidden_field_names_from_blocks( $block['innerBlocks'], $hidden_names ); } if ( ! empty( $block['inner_blocks'] ) && is_array( $block['inner_blocks'] ) ) { $this->collect_hidden_field_names_from_blocks( $block['inner_blocks'], $hidden_names ); } } } /** * Recursively collect password and password-confirmation field names. * * @param array $blocks * @param array $names * @return void */ protected function collect_password_field_names_from_blocks( array $blocks, array &$names ) { foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : ''; $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); // The text block renders a password input when its type says so. if ( 'stacklar/text' === $block_name && isset( $attrs['type'] ) && 'password' === $attrs['type'] ) { $raw_name = array_key_exists( 'name', $attrs ) ? $attrs['name'] : ( function_exists( 'stacklar_block_attribute_default' ) ? stacklar_block_attribute_default( $block_name, 'name' ) : '' ); $name = sanitize_key( (string) $raw_name ); if ( '' !== $name ) { $names[ $name ] = true; } } if ( 'stacklar/password' === $block_name ) { // Match the defaults the password block renders when the // attribute was never changed in the editor. $name = sanitize_key( isset( $attrs['name'] ) ? (string) $attrs['name'] : 'password' ); if ( '' !== $name ) { $names[ $name ] = true; } if ( ! empty( $attrs['confirmEnabled'] ) ) { $confirm = sanitize_key( isset( $attrs['confirmName'] ) ? (string) $attrs['confirmName'] : 'password_confirm' ); if ( '' !== $confirm ) { $names[ $confirm ] = true; } } } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $this->collect_password_field_names_from_blocks( $block['innerBlocks'], $names ); } } } /** * Recursively strip keys from a payload. * * @param mixed $payload * @param array $keys_to_strip * @return mixed */ protected function strip_payload_keys_recursive( $payload, array $keys_to_strip ) { if ( ! is_array( $payload ) ) { return $payload; } $clean = array(); foreach ( $payload as $key => $value ) { $key_str = is_string( $key ) ? sanitize_key( $key ) : $key; if ( is_string( $key_str ) && '' === $key_str ) { continue; } if ( is_string( $key_str ) && isset( $keys_to_strip[ $key_str ] ) ) { continue; } if ( is_array( $value ) ) { $clean[ $key_str ] = $this->strip_payload_keys_recursive( $value, $keys_to_strip ); } else { $clean[ $key_str ] = $value; } } return $clean; } protected function sanitize_post_value( $value ) { if ( is_array( $value ) ) { $clean = array(); foreach ( $value as $k => $v ) { $clean_key = is_string( $k ) ? sanitize_key( $k ) : $k; if ( '' === $clean_key && ! is_int( $k ) ) { continue; } $clean[ $clean_key ] = $this->sanitize_post_value( $v ); } return $clean; } if ( is_object( $value ) ) { return $this->sanitize_post_value( (array) $value ); } $value = $this->sanitize_scalar_value( $value ); if ( is_array( $_POST ) && ! empty( $_POST['__stacklar_tel_fields'] ) ) { // processed later in validate_tel_fields } return $value; } protected function sanitize_scalar_value( $value ) { if ( is_bool( $value ) ) { return $value ? '1' : '0'; } if ( null === $value ) { return ''; } if ( ! is_scalar( $value ) ) { return ''; } $raw = (string) $value; $sanitized = sanitize_text_field( $raw ); if ( strpos( $raw, '+' ) === 0 && strpos( $sanitized, '+' ) !== 0 ) { $sanitized = '+' . ltrim( $sanitized, '+' ); } return $sanitized; } protected function validate_tel_fields( array $data ) { $fields_raw = isset( $_POST['__stacklar_tel_fields'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['__stacklar_tel_fields'] ) ) : array(); if ( empty( $fields_raw ) || ! class_exists( '\libphonenumber\\PhoneNumberUtil' ) ) { return $data; } $messages = array(); if ( isset( $_POST['__stacklar_tel_messages'] ) && is_array( $_POST['__stacklar_tel_messages'] ) ) { foreach ( array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['__stacklar_tel_messages'] ) ) as $raw_name => $message ) { $messages[ sanitize_key( $raw_name ) ] = $message; } } $util = \libphonenumber\PhoneNumberUtil::getInstance(); foreach ( $fields_raw as $raw_field ) { $field_key = sanitize_key( $raw_field ); if ( '' === $field_key ) { continue; } if ( ! isset( $data[ $field_key ] ) ) { continue; } $country_key = $field_key . '_country'; $national_key = $field_key . '_national'; $country = ''; if ( isset( $_POST[ $country_key ] ) ) { $country = sanitize_key( wp_unslash( $_POST[ $country_key ] ) ); } $raw_number = $data[ $field_key ]; $fallback_national = isset( $data[ $national_key ] ) ? $data[ $national_key ] : ''; $fallback_country = $country; try { $number = $util->parse( $raw_number, $country ? strtoupper( $country ) : null ); if ( ! $util->isValidNumber( $number ) ) { throw new \RuntimeException(); } $data[ $field_key ] = $util->format( $number, \libphonenumber\PhoneNumberFormat::E164 ); $formattedNational = $util->format( $number, \libphonenumber\PhoneNumberFormat::NATIONAL ); $region = $util->getRegionCodeForNumber( $number ); $data[ $national_key ] = $formattedNational; $data[ $country_key ] = strtolower( $region ?: $country ); } catch ( \Throwable $th ) { if ( $fallback_country && strpos( $raw_number, '+' ) === 0 ) { $data[ $field_key ] = $raw_number; $data[ $country_key ] = $fallback_country; $data[ $national_key ] = $fallback_national; } else { $message = isset( $messages[ $field_key ] ) && $messages[ $field_key ] !== '' ? $messages[ $field_key ] : __( 'Phone number is not valid for the selected country.', 'stacklar-forms' ); $this->json_error( array( 'code' => 'invalid_phone', 'message' => $message, 'field' => $field_key, ), 422 ); } } } return $data; } /** * Resolve action settings from the saved form configuration only. * This prevents request payload tampering from changing action behavior. * * @param int $form_id Form post ID. * @param string $action_slug Action slug to resolve. * @return array */ protected function get_server_action_settings_or_fail( $form_id, $action_slug ) { $form_id = intval( $form_id ); $action_slug = sanitize_key( $action_slug ); if ( ! $form_id || '' === $action_slug ) { $this->json_error( array( 'code' => 'action_not_configured', 'message' => __( 'This action is not configured for the form.', 'stacklar-forms' ), ), 403 ); } $this->assert_form_runtime_access( $form_id ); $config = function_exists( 'stacklar_build_form_actions_config' ) ? stacklar_build_form_actions_config( $form_id ) : array(); $actions = isset( $config['actions'] ) && is_array( $config['actions'] ) ? array_map( 'sanitize_key', $config['actions'] ) : array(); if ( ! in_array( $action_slug, $actions, true ) ) { $this->json_error( array( 'code' => 'action_not_configured', 'message' => __( 'This action is not configured for the form.', 'stacklar-forms' ), ), 403 ); } if ( ! function_exists( 'stacklar_get_action_settings_for_action' ) ) { return array(); } $settings = stacklar_get_action_settings_for_action( $form_id, $action_slug ); $settings = is_array( $settings ) ? $settings : array(); $this->maybe_skip_action_for_conditions( $form_id, $action_slug, $settings ); return $settings; } protected function maybe_skip_action_for_conditions( $form_id, $action_slug, array $settings ) { if ( ! function_exists( 'stacklar_action_conditions_enabled' ) || ! function_exists( 'stacklar_action_conditions_should_run' ) ) { return; } if ( ! stacklar_action_conditions_enabled( $settings ) ) { return; } $payload = function_exists( 'stacklar_action_condition_payload_from_request' ) ? stacklar_action_condition_payload_from_request() : array(); $form = get_post( $form_id ); if ( $form && 'stacklar' === $form->post_type ) { $payload = $this->strip_hidden_field_values( $payload, $form ); } if ( stacklar_action_conditions_should_run( $settings, $payload ) ) { return; } $data = array( 'code' => 'action_skipped', 'skipped' => true, 'action' => sanitize_key( $action_slug ), 'message' => '', ); if ( $this->is_rest_request ) { $response = new WP_REST_Response( array( 'success' => true, 'data' => $data, ), 200 ); // The exception carries a WP_REST_Response for the REST controller; its message is fixed. throw new Stacklar_Rest_Response_Exception( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } wp_send_json_success( $data ); } // Verify a shared nonce, allowing dynamic keys protected function verify_nonce_with( $key ) { $nonce = isset( $_POST['_stacklar_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_stacklar_nonce'] ) ) : ''; if ( ! $nonce || ! wp_verify_nonce( $nonce, $key ) ) { $this->json_error( array( 'code' => 'invalid_nonce', 'message' => __( 'Invalid request.', 'stacklar-forms' ) ), 400 ); } $form_id = isset( $_POST['stacklar_id'] ) ? absint( $_POST['stacklar_id'] ) : 0; if ( $form_id ) { $this->assert_form_runtime_access( $form_id ); } } /** * Block non-public forms unless the current user can preview that exact * form. Callers should verify their nonce first to avoid a post-existence * oracle. * * @param int $form_id Form post ID. * @return void */ protected function assert_form_runtime_access( $form_id ) { $allowed = function_exists( 'stacklar_can_execute_form' ) ? stacklar_can_execute_form( $form_id ) : false; if ( ! $allowed ) { $this->json_error( array( 'code' => 'form_not_available', 'message' => __( 'Form not found.', 'stacklar-forms' ), ), 404 ); } } protected function is_form_test_mode( $form_id ) { return function_exists( 'stacklar_is_form_test_mode' ) && stacklar_is_form_test_mode( $form_id ); } protected function test_mode_success_response( $action_slug, $message = '', array $extra = array() ) { $action_slug = sanitize_key( (string) $action_slug ); $message = is_string( $message ) ? trim( $message ) : ''; if ( '' === $message ) { $message = __( 'Test mode dry-run completed.', 'stacklar-forms' ); } return $this->json_success( array_merge( array( 'code' => 'ok', 'message' => $message, 'action' => $action_slug, 'test_mode' => true, 'dry_run' => true, ), $extra ) ); } protected function enforce_form_protection( $form_id, array $args = array() ) { $mode = isset( $args['mode'] ) ? sanitize_key( (string) $args['mode'] ) : 'enforce'; $results = array(); if ( function_exists( 'stacklar_submission_protection_service' ) ) { $service = stacklar_submission_protection_service(); if ( 'assert_passed' === $mode ) { $result = $service->assert_passed( $form_id, isset( $args['submission_token'] ) ? $args['submission_token'] : '' ); } else { $result = $service->enforce( $form_id, $args ); } if ( is_wp_error( $result ) ) { $this->send_guard_error( $result ); } $results['protection'] = is_array( $result ) ? $result : array(); } if ( function_exists( 'stacklar_restrictions_service' ) ) { $service = stacklar_restrictions_service(); if ( 'assert_passed' === $mode ) { $result = $service->assert_passed( $form_id, isset( $args['submission_token'] ) ? $args['submission_token'] : '' ); } else { $result = $service->enforce( $form_id, $args ); } if ( is_wp_error( $result ) ) { $this->send_guard_error( $result ); } $results['restrictions'] = is_array( $result ) ? $result : array(); } if ( function_exists( 'stacklar_survey_results_service' ) ) { $service = stacklar_survey_results_service(); if ( 'assert_passed' === $mode ) { $result = $service->assert_passed( $form_id, isset( $args['submission_token'] ) ? $args['submission_token'] : '' ); } else { $result = $service->enforce( $form_id, $args ); } if ( is_wp_error( $result ) ) { $this->send_guard_error( $result ); } $results['surveyResults'] = is_array( $result ) ? $result : array(); } return $results; } protected function send_guard_error( WP_Error $result ) { if ( is_wp_error( $result ) ) { $code = $result->get_error_code(); $message = $result->get_error_message(); $status = 422; if ( in_array( $code, array( 'spam_rate_limited', 'restriction_total_limit_reached', 'restriction_ip_limit_reached', 'restriction_user_limit_reached', 'restriction_email_limit_reached' ), true ) ) { $status = 429; } elseif ( in_array( $code, array( 'restriction_license_required', 'restriction_login_required', 'restriction_ip_blocked', 'restriction_country_blocked', 'restriction_email_domain_blocked', 'survey_results_license_required' ), true ) ) { $status = 403; } elseif ( in_array( $code, array( 'recaptcha_not_configured', 'turnstile_not_configured', 'spam_submission_token_missing', 'spam_attempt_missing' ), true ) ) { $status = 400; } elseif ( in_array( $code, array( 'restriction_submission_token_missing', 'restriction_attempt_missing', 'restriction_attempt_mismatch', 'restriction_email_field_missing', 'survey_results_submission_token_missing', 'survey_results_attempt_missing' ), true ) ) { $status = 400; } $this->json_error( array( 'code' => $code, 'message' => $message, ), $status ); } } /** * Collect contextual request data (IP, user agent, referer, submitted timestamp). * * @return array */ protected function get_request_context( $form_id = 0 ) { $form_id = $form_id ? absint( $form_id ) : ( isset( $_POST['stacklar_id'] ) ? absint( $_POST['stacklar_id'] ) : 0 ); $attribution_settings = function_exists( 'stacklar_get_form_attribution_settings' ) ? stacklar_get_form_attribution_settings( $form_id ) : array(); $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; if ( ! empty( $attribution_settings['anonymizeIp'] ) && function_exists( 'stacklar_anonymize_ip_address' ) ) { $ip = stacklar_anonymize_ip_address( $ip ); } $referer = wp_get_referer(); $referer = is_string( $referer ) ? esc_url_raw( $referer ) : ''; $context = array( 'ip' => $ip, 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 255 ) : '', 'referer' => $referer, 'submitted' => current_time( 'mysql', true ), ); if ( function_exists( 'stacklar_is_form_test_mode' ) && stacklar_is_form_test_mode( $form_id ) ) { $context['test_mode'] = '1'; } $attribution = $this->get_attribution_context( $form_id, $attribution_settings ); if ( ! empty( $attribution ) ) { $context['attribution'] = $attribution; if ( ! empty( $attribution_settings['retentionDays'] ) ) { $context['attribution_retention_expires'] = gmdate( 'Y-m-d H:i:s', time() + ( absint( $attribution_settings['retentionDays'] ) * DAY_IN_SECONDS ) ); } } return array_filter( $context, static function ( $value ) { return $value !== null && $value !== ''; } ); } protected function get_attribution_context( $form_id, array $settings ) { $settings = function_exists( 'stacklar_sanitize_attribution_settings' ) ? stacklar_sanitize_attribution_settings( $settings ) : $settings; if ( empty( $settings['enabled'] ) || empty( $_POST['__stacklar_attribution'] ) ) { return array(); } // JSON; decoded and sanitized value by value below. $raw = wp_unslash( $_POST['__stacklar_attribution'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( is_array( $raw ) ) { $decoded = $raw; } else { $decoded = json_decode( (string) $raw, true ); } if ( ! is_array( $decoded ) ) { return array(); } $consent_granted = $this->attribution_truthy( isset( $decoded['consent_granted'] ) ? $decoded['consent_granted'] : false ); if ( ! empty( $settings['consentRequired'] ) && ! $consent_granted ) { return array( 'consent_granted' => '0' ); } $out = array( 'consent_granted' => $consent_granted ? '1' : '0' ); if ( ! empty( $decoded['captured_at'] ) ) { $out['captured_at'] = $this->sanitize_attribution_text( $decoded['captured_at'], 40 ); } if ( ! empty( $settings['captureUtm'] ) ) { $utm = $this->sanitize_attribution_utm( $decoded ); if ( ! empty( $utm ) ) { $out = array_merge( $out, $utm ); } } if ( ! empty( $settings['captureLandingPage'] ) && ! empty( $decoded['landing_page'] ) ) { $landing_page = $this->sanitize_attribution_url( $decoded['landing_page'] ); if ( '' !== $landing_page ) { $out['landing_page'] = $landing_page; } } if ( ! empty( $settings['captureCurrentPage'] ) && ! empty( $decoded['current_page'] ) ) { $current_page = $this->sanitize_attribution_url( $decoded['current_page'] ); if ( '' !== $current_page ) { $out['current_page'] = $current_page; } } if ( ! empty( $settings['captureReferrer'] ) && ! empty( $decoded['referrer'] ) ) { $referrer = $this->sanitize_attribution_url( $decoded['referrer'] ); if ( '' !== $referrer ) { $out['referrer'] = $referrer; } } if ( ! empty( $settings['captureBrowser'] ) && ! empty( $decoded['browser'] ) && is_array( $decoded['browser'] ) ) { $browser = $this->sanitize_attribution_browser( $decoded['browser'] ); if ( ! empty( $browser ) ) { $out['browser'] = $browser; } } if ( ! empty( $settings['captureJourney'] ) && ! empty( $decoded['journey'] ) && is_array( $decoded['journey'] ) ) { $journey = $this->sanitize_attribution_journey( $decoded['journey'], isset( $settings['journeyLimit'] ) ? absint( $settings['journeyLimit'] ) : 10 ); if ( ! empty( $journey ) ) { $out['journey'] = $journey; } } return $out; } protected function sanitize_attribution_utm( array $payload ) { $out = array(); $utm = isset( $payload['utm'] ) && is_array( $payload['utm'] ) ? $payload['utm'] : array(); foreach ( array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ) as $key ) { $value = null; if ( isset( $payload[ $key ] ) ) { $value = $payload[ $key ]; } elseif ( isset( $utm[ $key ] ) ) { $value = $utm[ $key ]; } elseif ( isset( $utm[ str_replace( 'utm_', '', $key ) ] ) ) { $value = $utm[ str_replace( 'utm_', '', $key ) ]; } if ( null === $value ) { continue; } $value = $this->sanitize_attribution_text( $value, 200 ); if ( '' !== $value ) { $out[ $key ] = $value; } } return $out; } protected function sanitize_attribution_browser( array $browser ) { $out = array(); $limits = array( 'name' => 80, 'version' => 40, 'os' => 120, 'language' => 40, 'platform' => 120, ); foreach ( $limits as $key => $limit ) { if ( ! isset( $browser[ $key ] ) ) { continue; } $value = $this->sanitize_attribution_text( $browser[ $key ], $limit ); if ( '' !== $value ) { $out[ $key ] = $value; } } return $out; } protected function sanitize_attribution_journey( array $journey, $limit = 10 ) { $out = array(); $limit = max( 1, min( 50, absint( $limit ) ) ); foreach ( $journey as $item ) { if ( ! is_array( $item ) ) { continue; } $url = isset( $item['url'] ) ? $this->sanitize_attribution_url( $item['url'] ) : ''; if ( '' === $url || ! $this->is_same_site_attribution_url( $url ) ) { continue; } $entry = array( 'url' => $url ); if ( ! empty( $item['title'] ) ) { $entry['title'] = $this->sanitize_attribution_text( $item['title'], 200 ); } if ( ! empty( $item['visited_at'] ) ) { $entry['visited_at'] = $this->sanitize_attribution_text( $item['visited_at'], 40 ); } $out[] = array_filter( $entry, static function ( $value ) { return $value !== null && $value !== ''; } ); if ( count( $out ) >= $limit ) { break; } } return $out; } protected function sanitize_attribution_text( $value, $limit = 200 ) { $value = sanitize_text_field( is_scalar( $value ) ? (string) $value : '' ); if ( '' === $value ) { return ''; } return substr( $value, 0, max( 1, absint( $limit ) ) ); } protected function sanitize_attribution_url( $value ) { $value = trim( wp_strip_all_tags( is_scalar( $value ) ? (string) $value : '' ) ); if ( '' === $value ) { return ''; } $value = substr( $value, 0, 2048 ); return esc_url_raw( $value ); } protected function is_same_site_attribution_url( $url ) { $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { return true; } $home = wp_parse_url( home_url() ); $host = isset( $home['host'] ) ? strtolower( (string) $home['host'] ) : ''; $url_host = strtolower( (string) $parts['host'] ); if ( '' === $host || '' === $url_host ) { return false; } return preg_replace( '/^www\./', '', $host ) === preg_replace( '/^www\./', '', $url_host ); } protected function attribution_truthy( $value ) { return function_exists( 'stacklar_attribution_is_truthy' ) ? stacklar_attribution_is_truthy( $value ) : in_array( strtolower( trim( (string) $value ) ), array( '1', 'true', 'yes', 'on' ), true ); } protected function is_label_key_mode() { if ( null !== $this->stacklar_label_mode_cache ) { return $this->stacklar_label_mode_cache; } $mode = ''; if ( isset( $_POST['__stacklar_key_mode'] ) ) { $mode = sanitize_key( wp_unslash( (string) $_POST['__stacklar_key_mode'] ) ); } $this->stacklar_label_mode_cache = ( 'label' === $mode ); return $this->stacklar_label_mode_cache; } protected function get_field_label_map() { if ( null !== $this->stacklar_field_label_map_cache ) { return $this->stacklar_field_label_map_cache; } $this->stacklar_field_label_map_cache = array(); if ( ! $this->is_label_key_mode() ) { return $this->stacklar_field_label_map_cache; } if ( empty( $_POST['__stacklar_field_labels'] ) ) { return $this->stacklar_field_label_map_cache; } // JSON; decoded and sanitized label by label below. $raw = wp_unslash( (string) $_POST['__stacklar_field_labels'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $decoded = json_decode( $raw, true ); if ( ! is_array( $decoded ) ) { return $this->stacklar_field_label_map_cache; } foreach ( $decoded as $path => $label ) { if ( ! is_string( $path ) ) { continue; } $trimmed_path = trim( $path ); if ( '' === $trimmed_path ) { continue; } $path_key = preg_replace( '/[^A-Za-z0-9_\\|\\-]+/', '', $trimmed_path ); if ( '' === $path_key ) { continue; } $label_text = is_string( $label ) ? trim( wp_strip_all_tags( $label ) ) : ''; if ( '' === $label_text ) { continue; } $this->stacklar_field_label_map_cache[ $path_key ] = $label_text; } return $this->stacklar_field_label_map_cache; } protected function lookup_label_for_path( $path, $fallback = '' ) { $map = $this->get_field_label_map(); if ( ! is_string( $path ) ) { return $fallback; } $primary = preg_replace( '/[^A-Za-z0-9_\\|\\-]+/', '', $path ); if ( '' !== $primary && ! empty( $map ) && isset( $map[ $primary ] ) ) { return $map[ $primary ]; } if ( false !== strpos( $primary, '|' ) ) { $reduced = preg_replace( '/\\|[0-9]+\\b/', '', $primary ); $reduced = preg_replace( '/\\|{2,}/', '|', $reduced ); $reduced = trim( $reduced, '|' ); if ( '' !== $reduced && ! empty( $map ) && isset( $map[ $reduced ] ) ) { return $map[ $reduced ]; } } $form_id = isset( $_POST['stacklar_id'] ) ? absint( $_POST['stacklar_id'] ) : 0; if ( $form_id && function_exists( 'stacklar_translate_string' ) ) { $field_key = sanitize_key( str_replace( '|', '_', $primary ) ); if ( '' !== $field_key ) { return stacklar_translate_string( $form_id, 'field.' . $field_key . '.label', $fallback, '', array( 'type' => 'field_label', 'field' => $field_key, ) ); } } return $fallback; } }