get_scan_version(); $notice = get_option( self::NOTICE_OPTION, array() ); if ( get_option( self::SCAN_VERSION_OPTION, '' ) === $version ) { return; } $tracked_ids = array_values( array_unique( array_filter( array_map( 'intval', $notice['tracked_ids'] ?? array() ) ) ) ); if ( array_key_exists( 'tracked_ids', $notice ) && $this->notice_uses_current_scan_schema( $notice ) ) { $forms = empty( $tracked_ids ) ? array() : $this->scan_specific_forms( $tracked_ids ); $this->save_scan( $version, $tracked_ids, $forms ); return; } $forms = $this->scan_affected_forms(); $tracked_ids = array_values( array_unique( array_map( 'intval', wp_list_pluck( $forms, 'id' ) ) ) ); $this->save_scan( $version, $tracked_ids, $forms ); } public function maybe_dismiss_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( empty( $_GET[ self::NOTICE_QUERY_ARG ] ) ) { return; } check_admin_referer( self::NOTICE_QUERY_ARG ); update_user_meta( get_current_user_id(), self::DISMISS_META_KEY, $this->get_dismiss_version() ); wp_safe_redirect( remove_query_arg( array( self::NOTICE_QUERY_ARG, '_wpnonce', ) ) ); exit; } public function render_affected_forms_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $notice = get_option( self::NOTICE_OPTION, array() ); if ( empty( $notice['forms'] ) || empty( $notice['version'] ) ) { return; } if ( $this->get_scan_version() !== $notice['version'] || $this->is_notice_dismissed( $notice ) ) { return; } $forms = array_slice( $notice['forms'], 0, 5 ); $more_forms = count( $notice['forms'] ) - count( $forms ); $dismiss_url = wp_nonce_url( add_query_arg( self::NOTICE_QUERY_ARG, '1' ), self::NOTICE_QUERY_ARG ); ?>

0 ) : ?>

$version, 'notice_token' => wp_generate_uuid4(), 'tracked_ids' => $tracked_ids, 'forms' => $forms, ), false ); } private function scan_affected_forms(): array { $affected_forms = array(); $offset = 0; while ( true ) { $form_ids = get_posts( array( 'post_type' => Post_Type_Module::SLUG, 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ), 'posts_per_page' => self::SCAN_BATCH_SIZE, 'offset' => $offset, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( empty( $form_ids ) ) { break; } foreach ( $form_ids as $form_id ) { $form = $this->get_affected_form( (int) $form_id ); if ( $form ) { $affected_forms[] = $form; } } if ( count( $form_ids ) < self::SCAN_BATCH_SIZE ) { break; } $offset += self::SCAN_BATCH_SIZE; } return $affected_forms; } private function scan_specific_forms( array $form_ids ): array { $affected_forms = array(); foreach ( $form_ids as $form_id ) { $form_id = (int) $form_id; if ( $form_id <= 0 || Post_Type_Module::SLUG !== get_post_type( $form_id ) ) { continue; } $form = $this->get_affected_form( $form_id ); if ( $form ) { $affected_forms[] = $form; } } return $affected_forms; } private function get_affected_form( int $form_id ): array { $settings = jet_form_builder()->post_type->get_gateways( $form_id ); $gateways = $this->get_active_gateways( $settings ); if ( empty( $gateways ) || $this->is_price_protection_enabled( $settings ) ) { return array(); } if ( ! $this->has_explicit_price_protection_setting( $settings ) && $this->has_safe_static_price_source( $form_id, $settings ) && $this->enable_price_protection( $form_id, $settings ) ) { return array(); } return array( 'id' => $form_id, 'title' => get_the_title( $form_id ) ?: sprintf( /* translators: %d: form ID */ __( 'Form #%d', 'jet-form-builder' ), $form_id ), 'edit_link' => get_edit_post_link( $form_id, 'raw' ) ?: admin_url( 'post.php?post=' . absint( $form_id ) . '&action=edit' ), 'gateways' => $gateways, ); } private function has_explicit_price_protection_setting( array $settings ): bool { if ( array_key_exists( 'protect_price_field', $settings ) ) { return true; } foreach ( $settings as $value ) { if ( is_array( $value ) && $this->has_explicit_price_protection_setting( $value ) ) { return true; } } return false; } private function has_safe_static_price_source( int $form_id, array $settings ): bool { $field_name = is_string( $settings['price_field'] ?? null ) ? sanitize_key( $settings['price_field'] ) : ''; if ( ! $field_name ) { return false; } $blocks = Block_Helper::get_blocks_by_post( $form_id, true, true ); $matches = $this->find_price_field_blocks( $field_name, $blocks ); if ( 1 !== count( $matches ) ) { return false; } $block = $matches[0]; $type = Block_Helper::delete_namespace( $block['blockName'] ?? '' ); if ( 'calculated-field' === $type ) { return $this->is_safe_static_calculated_field( $field_name, $block, $blocks ); } if ( 'hidden-field' === $type ) { return $this->is_safe_static_hidden_field( $block ); } if ( in_array( $type, array( 'select-field', 'radio-field', 'checkbox-field' ), true ) ) { return $this->is_safe_static_option_field( $block ); } return false; } private function find_price_field_blocks( string $field_name, array $blocks ): array { $matches = array(); foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); if ( ( $attributes['name'] ?? '' ) === $field_name ) { $matches[] = $block; } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $matches = array_merge( $matches, $this->find_price_field_blocks( $field_name, $block['innerBlocks'] ) ); } } return $matches; } private function is_safe_static_calculated_field( string $field_name, array $block, array $blocks, array $resolving_fields = array() ): bool { $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); $formula = $attributes['calc_formula'] ?? ''; $precision = $attributes['precision'] ?? 2; if ( isset( $resolving_fields[ $field_name ] ) || 'number' !== ( $attributes['value_type'] ?? 'number' ) || ! is_string( $formula ) || '' === trim( $formula ) || strlen( $formula ) > Trusted_Price_Resolver::MAX_FORMULA_LENGTH || substr_count( $formula, '%' ) / 2 > Trusted_Price_Resolver::MAX_MACRO_TOKENS || ! is_numeric( $precision ) || (float) (int) $precision !== (float) $precision || (int) $precision < 0 || (int) $precision > 100 ) { return false; } $formula = html_entity_decode( $formula, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); if ( false !== strpos( $formula, '^' ) || $this->is_field_inside_dynamic_container( $field_name, $blocks ) ) { return false; } $resolving_fields[ $field_name ] = true; return $this->is_safe_calculated_formula( $formula, $blocks, $resolving_fields ); } private function is_safe_calculated_formula( string $formula, array $blocks, array $resolving_fields ): bool { $replacements = array(); $dependencies = array(); $match = preg_match_all( self::CALCULATED_MACRO_PATTERN, $formula, $macros, PREG_SET_ORDER ); if ( false === $match ) { return false; } foreach ( $macros as $macro ) { $token = $macro[1]; if ( false !== strpos( $token, '|' ) ) { return false; } $parts = explode( '::', $token, 2 ); if ( 2 === count( $parts ) ) { if ( 'field' !== strtolower( $parts[0] ) ) { return false; } $dependency = $parts[1]; } else { $dependency = $parts[0]; } if ( ! isset( $dependencies[ $dependency ] ) && ! $this->is_safe_calculated_dependency( $dependency, $blocks, $resolving_fields ) ) { return false; } $dependencies[ $dependency ] = true; $replacements[ $macro[0] ] = '1'; } try { ( new Trusted_Price_Resolver_Expression_Parser( strtr( $formula, $replacements ) ) )->validate(); } catch ( \Throwable $exception ) { return false; } return true; } private function is_safe_calculated_dependency( string $field_name, array $blocks, array $resolving_fields ): bool { $matches = $this->find_price_field_blocks( $field_name, $blocks ); if ( 1 !== count( $matches ) || $this->is_field_inside_dynamic_container( $field_name, $blocks ) ) { return false; } $block = $matches[0]; $type = Block_Helper::delete_namespace( $block['blockName'] ?? '' ); if ( 'calculated-field' === $type ) { return $this->is_safe_static_calculated_field( $field_name, $block, $blocks, $resolving_fields ); } if ( 'hidden-field' === $type ) { return $this->is_safe_static_hidden_field( $block ); } if ( in_array( $type, array( 'select-field', 'radio-field', 'checkbox-field' ), true ) ) { return $this->has_safe_static_option_configuration( $block, false ); } return false; } private function is_field_inside_dynamic_container( string $field_name, array $blocks, bool $inside_dynamic_container = false ): bool { foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $type = Block_Helper::delete_namespace( $block['blockName'] ?? '' ); $inside_current = $inside_dynamic_container || in_array( $type, array( 'repeater-field', 'conditional-block' ), true ); $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); if ( ( $attributes['name'] ?? '' ) === $field_name ) { return $inside_current; } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && $this->is_field_inside_dynamic_container( $field_name, $block['innerBlocks'], $inside_current ) ) { return true; } } return false; } private function is_safe_static_hidden_field( array $block ): bool { $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); $default = $attributes['default'] ?? ''; return false === ( $attributes['render'] ?? true ) && 'manual_input' === ( $attributes['field_value'] ?? '' ) && '' === $default && $this->is_positive_number( $attributes['hidden_value'] ?? null ); } private function is_safe_static_option_field( array $block ): bool { return $this->has_safe_static_option_configuration( $block, true ); } private function has_safe_static_option_configuration( array $block, bool $require_positive_amount ): bool { $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); if ( $this->has_custom_option( $block ) || 'manual_input' !== ( $attributes['field_options_from'] ?? 'manual_input' ) ) { return false; } $options = $attributes['field_options'] ?? array(); if ( empty( $options ) || ! is_array( $options ) ) { return false; } $values = array(); foreach ( $options as $option ) { if ( ! is_array( $option ) || ! isset( $option['value'] ) || ! is_scalar( $option['value'] ) ) { return false; } $value = (string) $option['value']; if ( '' === $value || isset( $values[ $value ] ) ) { return false; } $calculate = $option['calculate'] ?? null; $amount = null !== $calculate && '' !== $calculate ? $calculate : $value; if ( ! $this->is_finite_number( $amount ) || ( $require_positive_amount && (float) $amount <= 0 ) ) { return false; } $values[ $value ] = true; } return true; } private function has_custom_option( array $block ): bool { $type = Block_Helper::delete_namespace( $block['blockName'] ?? '' ); if ( ! in_array( $type, array( 'radio-field', 'checkbox-field' ), true ) ) { return false; } $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); $custom_option = $attributes['custom_option'] ?? false; if ( is_array( $custom_option ) ) { $custom_option = $custom_option['allow'] ?? false; } return filter_var( $custom_option, FILTER_VALIDATE_BOOLEAN ); } private function is_positive_number( $value ): bool { return $this->is_finite_number( $value ) && (float) $value > 0; } private function is_finite_number( $value ): bool { return is_scalar( $value ) && is_numeric( $value ) && is_finite( (float) $value ); } private function enable_price_protection( int $form_id, array $settings ): bool { $settings['protect_price_field'] = true; return false !== update_post_meta( $form_id, Gateways_Meta::META_KEY, wp_json_encode( $settings ) ); } private function get_active_gateways( array $settings ): array { $gateways = array(); if ( 'manual' === ( $settings['mode'] ?? 'single' ) ) { foreach ( $settings as $gateway_id => $gateway_settings ) { if ( ! is_string( $gateway_id ) || ! is_array( $gateway_settings ) || ! filter_var( $gateway_settings['show_on_front'] ?? false, FILTER_VALIDATE_BOOLEAN ) ) { continue; } $gateways[] = sanitize_key( $gateway_id ); } } else { $gateway_id = is_string( $settings['gateway'] ?? null ) ? sanitize_key( $settings['gateway'] ) : ''; if ( $gateway_id && ! in_array( $gateway_id, array( 'none', 'manual' ), true ) ) { $gateways[] = $gateway_id; } } return array_values( array_unique( array_filter( $gateways ) ) ); } private function is_price_protection_enabled( array $settings ): bool { return filter_var( $settings['protect_price_field'] ?? false, FILTER_VALIDATE_BOOLEAN ); } private function get_scan_version(): string { return jet_form_builder()->get_version() . ':' . self::SCAN_SCHEMA_VERSION; } private function notice_uses_current_scan_schema( array $notice ): bool { $version = (string) ( $notice['version'] ?? '' ); $suffix = ':' . self::SCAN_SCHEMA_VERSION; return strlen( $version ) > strlen( $suffix ) && substr( $version, -strlen( $suffix ) ) === $suffix; } private function get_dismiss_version(): string { return 'schema:' . self::SCAN_SCHEMA_VERSION; } private function is_notice_dismissed( array $notice ): bool { $dismissed = (string) get_user_meta( get_current_user_id(), self::DISMISS_META_KEY, true ); if ( '' === $dismissed ) { return false; } if ( $dismissed === $this->get_dismiss_version() || $dismissed === $this->get_notice_dismiss_token( $notice ) ) { return true; } // Older builds stored ":" in user meta. $legacy_suffix = ':' . self::SCAN_SCHEMA_VERSION; return strlen( $dismissed ) > strlen( $legacy_suffix ) && substr( $dismissed, -strlen( $legacy_suffix ) ) === $legacy_suffix; } private function get_notice_dismiss_token( array $notice ): string { return (string) ( $notice['notice_token'] ?? $notice['version'] ?? '' ); } }