Published : August 5, 2026

CVE-2025-15028: FormGent – Next-Gen AI Form Builder for WordPress with Multi-Step, Quizzes, Payments & More <= 1.9.2 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Plugin formgent
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.9.2
Patched Version 1.10.0
Disclosed August 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-15028:
This vulnerability is an unauthenticated stored cross-site scripting (XSS) flaw in the FormGent – Next-Gen AI Form Builder for WordPress plugin, affecting all versions up to and including 1.9.2. The flaw resides in the form submission handling logic, specifically within the dropdown field type’s validation and sanitization routines. It allows unauthenticated attackers to inject arbitrary web scripts into form submissions, which execute whenever an administrator or other user views the submission data. The vulnerability has a CVSS score of 7.2 and is classified under CWE-79.

Root Cause:
The defect originates in the `formgent/app/Fields/Dropdown/MethodResolver.php` file. Prior to the patch, the `get_field_dto` method lacked proper sanitization of the submitted value, directly passing the raw `$value` from the request into the `AnswerDTO`. The newly added `normalize_multi_select_values` and `get_normalized_option_values` methods show the intent to sanitize with `sanitize_text_field`, but the vulnerable version does not apply this sanitization. For single-select dropdowns, the value is taken directly from `$wp_rest_request->get_param( $field[‘name’] )` without any escaping or sanitization. When this value is later rendered in the admin dashboard or submission views, it can contain malicious HTML or JavaScript. The patch introduces validation rules and sanitization but the key fix is the addition of `sanitize_text_field` within `normalize_multi_select_values` and `get_normalized_option_values`. However, Atomic Edge analysis finds that the primary fix lies in ensuring that the stored value is sanitized before being saved, which the patch implements for both single and multi-select paths.

Exploitation:
An unauthenticated attacker can craft a POST request to the plugin’s form submission endpoint, typically an AJAX action or REST API route, with a malicious payload in a dropdown field. The attack does not require authentication because the plugin processes front-end form submissions without verifying user privileges. The attacker sets the dropdown field’s `name` parameter to a payload like `alert(document.cookie)` or an event handler such as ``. Since the vulnerable code does not sanitize this input, the malicious script is stored in the database as part of the form entry. When an administrator views the submissions in the wp-admin dashboard, the script executes in the administrator’s browser session, allowing the attacker to steal admin session cookies, modify site content, or perform administrative actions.

Patch Analysis:
The patch modifies several files, but the core security fix is in the dropdown field’s `MethodResolver.php`. The patch adds a `validate` method that enforces input validation rules. For single-select fields, it applies `string|max:255` validation, and for multi-select fields, it applies `array` validation. It then uses `normalize_multi_select_values` to sanitize each value with `sanitize_text_field` before storing. Additionally, the patch adds a `get_normalized_option_values` method that sanitizes the allowed option values, ensuring that comparisons are done against sanitized data. For multi-select fields, it also checks that submitted values are one-level arrays, contains no duplicates, and only contain allowed options. This prevents the injection of arbitrary HTML by stripping tags and special characters. The patch also introduces a new `Hidden` field type with its own validation rules, and updates form helpers to sanitize merge-tag data. However, Atomic Edge analysis notes that the patch does not include output escaping on the stored values when rendered elsewhere in the plugin, leaving a potential stored XSS vector if the sanitization is incomplete or bypassable.

Impact:
Successful exploitation allows an unauthenticated attacker to inject arbitrary JavaScript into the plugin’s submission data. When an administrator views the form submissions in the WordPress admin dashboard, the malicious script executes with the administrator’s privileges. This can lead to complete site compromise, including theft of administrative session cookies, creation of new administrator accounts, modification of site content, and installation of malicious plugins. The attacker could also use the compromised admin session to execute arbitrary PHP code via the theme or plugin editors, resulting in remote code execution. The impact is severe because it requires no prior authentication and the payload is stored persistently, affecting anyone who views the injected submission data.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/formgent/app/Fields/Dropdown/MethodResolver.php
+++ b/formgent/app/Fields/Dropdown/MethodResolver.php
@@ -4,7 +4,13 @@

 defined( 'ABSPATH' ) || exit;

+use FormGentAppDTOAnswerDTO;
 use FormGentAppFieldsSingleChoiceMethodResolver as SingleChoiceMethodResolver;
+use FormGentWpMVCExceptionsException;
+use FormGentWpMVCHelpersHelpers;
+use FormGentWpMVCRequestValidatorValidator;
+use stdClass;
+use WP_REST_Request;

 trait MethodResolver {

@@ -13,4 +19,172 @@
     public static function get_key(): string {
         return 'dropdown';
     }
+
+    protected function get_validation_rules( array $field, bool $force_multi_select = false ): array {
+        return ( $force_multi_select || $this->is_multi_select_enabled( $field ) )
+            ? [ 'array' ]
+            : [ 'string|max:255' ];
+    }
+
+    public function get_field_dto( array $field, WP_REST_Request $wp_rest_request, stdClass $form ): AnswerDTO {
+        $value = $wp_rest_request->get_param( $field['name'] );
+
+        if ( $this->should_treat_as_multi_select( $field, $wp_rest_request ) ) {
+            $value = $this->normalize_multi_select_values( $value );
+        }
+
+        return ( new AnswerDTO() )
+            ->set_form_id( $form->ID )
+            ->set_field_type( $field['field_type'] )
+            ->set_field_name( $field['name'] )
+            ->set_value( $value );
+    }
+
+    public function validate( array $field, WP_REST_Request $wp_rest_request, Validator $validator, stdClass $form ) {
+        if ( ! $this->should_treat_as_multi_select( $field, $wp_rest_request ) ) {
+            $rules = $this->get_validation_rules( $field );
+
+            if ( isset( $field['required'] ) && $field['required'] ) {
+                $rules[] = 'required';
+            }
+
+            $validator->validate(
+                [
+                    $field['name'] => implode( '|', $rules ),
+                ]
+            );
+
+            return;
+        }
+
+        $validator->validate(
+            [
+                $field['name'] => implode( '|', $this->get_validation_rules( $field, true ) ),
+            ]
+        );
+
+        $values      = $this->normalize_multi_select_values( $wp_rest_request->get_param( $field['name'] ) );
+        $is_required = ! empty( $field['required'] );
+
+        if ( $is_required && empty( $values ) ) {
+            throw ( new Exception() )->set_messages(
+                [
+                    $field['name'] => [
+                        sprintf( 'The %s field is required.', $field['name'] ),
+                    ],
+                ]
+            );
+        }
+
+        if ( empty( $values ) ) {
+            return;
+        }
+
+        if ( ! Helpers::is_one_level_array( $values ) ) {
+            throw ( new Exception() )->set_messages(
+                [
+                    $field['name'] => [
+                        'Something was wrong',
+                    ],
+                ]
+            );
+        }
+
+        if ( array_unique( $values ) !== $values ) {
+            throw ( new Exception() )->set_messages(
+                [
+                    $field['name'] => [
+                        sprintf( 'The %s field does not allow the same value multiple times', $field['name'] ),
+                    ],
+                ]
+            );
+        }
+
+        $options = $this->get_normalized_option_values( $field );
+
+        if ( ! empty( array_diff( $values, $options ) ) ) {
+            throw ( new Exception() )->set_messages(
+                [
+                    $field['name'] => [
+                        sprintf( 'The value of %s must be between %s', $field['name'], implode( ',', $options ) ),
+                    ],
+                ]
+            );
+        }
+    }
+
+    private function is_multi_select_enabled( array $field ): bool {
+        return ! empty( $field['allow_multi_select'] );
+    }
+
+    private function should_treat_as_multi_select( array $field, WP_REST_Request $wp_rest_request ): bool {
+        if ( $this->is_multi_select_enabled( $field ) ) {
+            return true;
+        }
+
+        return ! empty( $wp_rest_request->get_param( '_formgent_admin_edit' ) )
+            && is_array( $wp_rest_request->get_param( $field['name'] ) );
+    }
+
+    private function normalize_multi_select_values( $value ): array {
+        if ( is_string( $value ) ) {
+            $decoded = json_decode( $value, true );
+
+            if ( is_array( $decoded ) ) {
+                $value = $decoded;
+            } elseif ( false !== strpos( $value, ',' ) ) {
+                $value = array_map( 'trim', explode( ',', $value ) );
+            }
+        }
+
+        if ( ! is_array( $value ) ) {
+            $value = empty( $value ) ? [] : [ $value ];
+        }
+
+        $value = array_map(
+            static function( $item ) {
+                return is_scalar( $item ) ? sanitize_text_field( (string) $item ) : '';
+            },
+            $value
+        );
+
+        return array_values(
+            array_filter(
+                $value,
+                static function( $item ) {
+                    return '' !== $item;
+                }
+            )
+        );
+    }
+
+    private function get_normalized_option_values( array $field ): array {
+        $normalized = [];
+
+        foreach ( $field['options'] ?? [] as $index => $option ) {
+            $label = '';
+            $value = '';
+
+            if ( is_array( $option ) ) {
+                $label = isset( $option['label'] ) ? (string) $option['label'] : '';
+                $value = isset( $option['value'] ) ? (string) $option['value'] : '';
+            } elseif ( is_scalar( $option ) ) {
+                $label = (string) $option;
+            }
+
+            $value = sanitize_text_field( $value );
+
+            if ( '' === $value ) {
+                $value = sanitize_title( wp_strip_all_tags( $label ) );
+            }
+
+            if ( '' === $value ) {
+                $value = 'option_' . $index;
+            }
+
+            $normalized[] = $value;
+        }
+
+        return array_values( array_unique( $normalized ) );
+    }
 }
--- a/formgent/app/Fields/FileUpload/MethodResolver.php
+++ b/formgent/app/Fields/FileUpload/MethodResolver.php
@@ -6,6 +6,8 @@

 use FormGentAppDTOAnswerDTO;
 use FormGentAppSummaryPagination;
+use FormGentAppUtilsUploadFileToken;
+use FormGentWpMVCExceptionsException;
 use FormGentWpMVCRequestValidatorValidator;
 use stdClass;
 use WP_REST_Request;
@@ -28,10 +30,16 @@

         $values = array_map(
             function ( $value ) {
-                return base64_decode( $value );
+                $relative_path = UploadFileToken::path_from_token( (string) $value );
+
+                if ( null === $relative_path ) {
+                    throw new Exception( esc_html__( 'Invalid file token', 'formgent' ), 400 );
+                }
+
+                return $relative_path;
             }, $values
         );

         return ( new AnswerDTO() )->set_form_id( $form->ID )->set_field_type( $field['field_type'] )->set_field_name( $field['name'] )->set_value( $values );
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Fields/Hidden/Hidden.php
+++ b/formgent/app/Fields/Hidden/Hidden.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace FormGentAppFieldsHidden;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppFieldsField;
+
+class Hidden extends Field {
+    use MethodResolver;
+}
--- a/formgent/app/Fields/Hidden/MethodResolver.php
+++ b/formgent/app/Fields/Hidden/MethodResolver.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace FormGentAppFieldsHidden;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppSummaryPagination;
+
+trait MethodResolver {
+    use Pagination;
+
+    public static function get_key(): string {
+        return 'hidden';
+    }
+
+    protected function get_validation_rules( array $field ): array {
+        return ['string'];
+    }
+}
--- a/formgent/app/Helpers/Form.php
+++ b/formgent/app/Helpers/Form.php
@@ -63,7 +63,7 @@
      */
     protected function remove_labels( array &$attributes ): void {
         // Keep a minimal representation of choice options for frontend runtime features
-        // (e.g. calculations using numeric_value). Labels are stripped to keep payload small.
+        // (e.g. calculations and conditional logic using selected option values).
         if ( isset( $attributes['options'] ) && is_array( $attributes['options'] ) ) {
             $attributes['options'] = array_map(
                 static function( $opt ) {
@@ -72,7 +72,7 @@
                     }

                     $keep = [];
-                    foreach ( [ 'value', 'numeric_value', 'price', 'is_default', 'is_other' ] as $k ) {
+                    foreach ( [ 'label', 'value', 'numeric_value', 'price', 'is_default', 'is_other' ] as $k ) {
                         if ( array_key_exists( $k, $opt ) ) {
                             $keep[ $k ] = $opt[ $k ];
                         }
--- a/formgent/app/Helpers/form-helpers.php
+++ b/formgent/app/Helpers/form-helpers.php
@@ -74,16 +74,69 @@
     return 'conversational' === $form->form_type;
 }

-function formgent_form_default_values_functions() {
+function formgent_form_default_values_functions( int $form_id = 0 ) {
     return apply_filters(
         'formgent_default_values_functions',
         [
-            'ip'         => 'formgent_get_user_ip_address',
-            'site_url'   => 'site_url',
-            'site_title' => function () {
+            'ip'                  => 'formgent_get_user_ip_address',
+            'site_url'            => 'site_url',
+            'site_title'          => function () {
                 return get_bloginfo( 'name' );
             },
-            'user'       => function ( $property ) {
+            'form_title'          => function () use ( $form_id ) {
+                $form = $form_id ? get_post( $form_id ) : null;
+                return $form instanceof WP_Post ? $form->post_title : '';
+            },
+            'embedded_post_id'    => function () {
+                $post = get_post();
+                return $post instanceof WP_Post ? (string) $post->ID : '';
+            },
+            'embedded_post_title' => function () {
+                $post = get_post();
+                return $post instanceof WP_Post ? $post->post_title : '';
+            },
+            'current_date'        => function () {
+                return current_datetime()->format( 'm/d/Y' );
+            },
+            'login_url'           => function () {
+                return esc_url_raw( wp_login_url() );
+            },
+            'registration_url'    => function () {
+                return esc_url_raw( wp_registration_url() );
+            },
+            'lost_password_url'   => function () {
+                return esc_url_raw( wp_lostpassword_url() );
+            },
+            'forgot_password_url' => function () {
+                return esc_url_raw( wp_lostpassword_url() );
+            },
+            'logout_url'          => function () {
+                return esc_url_raw( wp_logout_url() );
+            },
+            'browser_name'        => 'formgent_get_browser_name',
+            'browser_platform'    => 'formgent_get_browser_platform',
+            'current_page_url'    => 'formgent_get_current_page_url',
+            'referrer_url'        => 'formgent_get_referrer_url',
+            'cookie_value'        => 'formgent_get_cookie_value',
+            'cookie_values'       => 'formgent_get_cookie_value',
+            'query'               => function ( $property ) {
+                $property        = sanitize_key( (string) $property );
+                $has_query_param = isset( $_GET[$property] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only URL merge tag, sanitized before use.
+
+                if ( '' === $property || ! $has_query_param ) {
+                    return '';
+                }
+
+                $value = wp_unslash( $_GET[$property] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only URL merge tag, sanitized before return.
+
+                if ( is_array( $value ) ) {
+                    $value = wp_json_encode( map_deep( $value, 'sanitize_text_field' ) );
+                    return is_string( $value ) ? $value : '';
+                }
+
+                return sanitize_text_field( (string) $value );
+            },
+            'user'                => function ( $property ) {
                 // For non-logged-in users, keep user-based defaults blank.
                 if ( ! is_user_logged_in() ) {
                     return '';
@@ -91,19 +144,20 @@

                 $user = wp_get_current_user();
                 return isset( $user->$property ) ? $user->$property : '';
-            }
-        ]
+            },
+        ],
+        $form_id
     );
 }

-function formgent_form_default_values( array $data ) {
-    $values_functions = formgent_form_default_values_functions();
+function formgent_form_default_values( array $data, int $form_id = 0 ) {
+    $values_functions = formgent_form_default_values_functions( $form_id );
     $dynamic_values   = [];
     $values           = [];

     foreach ( $data as $key => $item ) {
         if ( ! empty( $item['children'] ) ) {
-            $children_values = formgent_form_default_values( $item['children'] );
+            $children_values = formgent_form_default_values( $item['children'], $form_id );

             if ( 'step' === $item['field_type'] ) {
                 $values = array_merge( $values, $children_values );
@@ -161,8 +215,8 @@
                 $dynamic_value = '';

                 if ( isset( $values_functions[$base] ) && is_callable( $values_functions[$base] ) ) {
-                    // Resolve the user property if applicable.
-                    if ( 'user' === $base && ! empty( $parts ) ) {
+                    // Resolve token properties if applicable.
+                    if ( in_array( $base, ['user', 'query'], true ) && ! empty( $parts ) ) {
                         $property      = implode( '.', $parts );
                         $dynamic_value = $values_functions[$base]( $property );
                     } else {
@@ -318,6 +372,135 @@
 }

 /**
+ * Get the current visitor browser name from the user agent.
+ *
+ * @return string
+ */
+function formgent_get_browser_name(): string {
+    $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
+        ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
+        : '';
+
+    if ( '' === $user_agent ) {
+        return '';
+    }
+
+    if ( stripos( $user_agent, 'edg' ) !== false || stripos( $user_agent, 'edge' ) !== false ) {
+        return 'Edge';
+    }
+
+    if ( stripos( $user_agent, 'opr/' ) !== false || stripos( $user_agent, 'opera' ) !== false ) {
+        return 'Opera';
+    }
+
+    if ( stripos( $user_agent, 'chrome' ) !== false ) {
+        return 'Chrome';
+    }
+
+    if ( stripos( $user_agent, 'firefox' ) !== false ) {
+        return 'Firefox';
+    }
+
+    if ( stripos( $user_agent, 'safari' ) !== false ) {
+        return 'Safari';
+    }
+
+    if ( stripos( $user_agent, 'msie' ) !== false || stripos( $user_agent, 'trident/' ) !== false ) {
+        return 'Internet Explorer';
+    }
+
+    return '';
+}
+
+/**
+ * Get the current visitor browser platform from the user agent.
+ *
+ * @return string
+ */
+function formgent_get_browser_platform(): string {
+    $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
+        ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
+        : '';
+
+    if ( '' === $user_agent ) {
+        return '';
+    }
+
+    if ( stripos( $user_agent, 'iphone' ) !== false || stripos( $user_agent, 'ipad' ) !== false ) {
+        return 'iOS';
+    }
+
+    if ( stripos( $user_agent, 'android' ) !== false ) {
+        return 'Android';
+    }
+
+    if ( stripos( $user_agent, 'windows' ) !== false ) {
+        return 'Windows';
+    }
+
+    if ( stripos( $user_agent, 'mac' ) !== false ) {
+        return 'macOS';
+    }
+
+    if ( stripos( $user_agent, 'linux' ) !== false ) {
+        return 'Linux';
+    }
+
+    return '';
+}
+
+/**
+ * Get the current page URL for the active request.
+ *
+ * @return string
+ */
+function formgent_get_current_page_url(): string {
+    if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
+        $scheme      = is_ssl() ? 'https://' : 'http://';
+        $host        = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
+        $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
+
+        return esc_url_raw( $scheme . $host . $request_uri );
+    }
+
+    return esc_url_raw( home_url( '/' ) );
+}
+
+/**
+ * Get the referrer URL for the active request.
+ *
+ * @return string
+ */
+function formgent_get_referrer_url(): string {
+    if ( empty( $_SERVER['HTTP_REFERER'] ) ) {
+        return '';
+    }
+
+    return esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) );
+}
+
+/**
+ * Get all cookies as a JSON string for string-based default-value fields.
+ *
+ * @return string
+ */
+function formgent_get_cookie_value(): string {
+    if ( empty( $_COOKIE ) || ! is_array( $_COOKIE ) ) {
+        return '';
+    }
+
+    $cookies = [];
+
+    foreach ( $_COOKIE as $key => $value ) {
+        $cookies[ sanitize_key( (string) $key ) ] = is_string( $value )
+            ? sanitize_text_field( wp_unslash( $value ) )
+            : $value;
+    }
+
+    return wp_json_encode( $cookies );
+}
+
+/**
  * Get preset values for HTML block dynamic tags.
  *
  * @param int $form_id Form post ID.
@@ -329,40 +512,6 @@
     $current_user = is_user_logged_in() ? wp_get_current_user() : null;
     $embed_post   = get_post();

-    $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
-        ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
-        : '';
-    $browser    = '';
-    $platform   = '';
-
-    if ( $user_agent ) {
-        if ( stripos( $user_agent, 'chrome' ) !== false ) {
-            $browser = 'Chrome';
-        } elseif ( stripos( $user_agent, 'safari' ) !== false ) {
-            $browser = 'Safari';
-        } elseif ( stripos( $user_agent, 'firefox' ) !== false ) {
-            $browser = 'Firefox';
-        } elseif ( stripos( $user_agent, 'edge' ) !== false ) {
-            $browser = 'Edge';
-        } elseif ( stripos( $user_agent, 'opera' ) !== false || stripos( $user_agent, 'opr/' ) !== false ) {
-            $browser = 'Opera';
-        } elseif ( stripos( $user_agent, 'msie' ) !== false || stripos( $user_agent, 'trident/' ) !== false ) {
-            $browser = 'Internet Explorer';
-        }
-
-        if ( stripos( $user_agent, 'windows' ) !== false ) {
-            $platform = 'Windows';
-        } elseif ( stripos( $user_agent, 'mac' ) !== false ) {
-            $platform = 'macOS';
-        } elseif ( stripos( $user_agent, 'linux' ) !== false ) {
-            $platform = 'Linux';
-        } elseif ( stripos( $user_agent, 'iphone' ) !== false || stripos( $user_agent, 'ipad' ) !== false ) {
-            $platform = 'iOS';
-        } elseif ( stripos( $user_agent, 'android' ) !== false ) {
-            $platform = 'Android';
-        }
-    }
-
     $now        = current_datetime();
     $admin_user = get_user_by( 'email', get_option( 'admin_email', '' ) );

@@ -371,16 +520,21 @@
         'site_name'           => get_option( 'blogname', '' ),
         'site_url'            => esc_url_raw( site_url() ),
         'form_title'          => $form_post ? $form_post->post_title : '',
-        'browser_name'        => $browser,
-        'browser_platform'    => $platform,
+        'browser_name'        => formgent_get_browser_name(),
+        'browser_platform'    => formgent_get_browser_platform(),
         'embedded_post_id'    => $embed_post instanceof WP_Post ? (string) $embed_post->ID : '',
         'embedded_post_title' => $embed_post instanceof WP_Post ? $embed_post->post_title : '',
         'current_date'        => $now->format( 'm/d/Y' ),
+        'current_page_url'    => formgent_get_current_page_url(),
+        'referrer_url'        => formgent_get_referrer_url(),
         'admin_name'          => $admin_user ? sanitize_text_field( $admin_user->display_name ) : '',
         'login_url'           => esc_url_raw( wp_login_url() ),
-        'register_url'        => esc_url_raw( wp_registration_url() ),
+        'registration_url'    => esc_url_raw( wp_registration_url() ),
+        'lost_password_url'   => esc_url_raw( wp_lostpassword_url() ),
         'forgot_password_url' => esc_url_raw( wp_lostpassword_url() ),
         'logout_url'          => esc_url_raw( wp_logout_url() ),
+        'cookie_value'        => formgent_get_cookie_value(),
+        'cookie_values'       => formgent_get_cookie_value(),
     ];

     if ( $current_user ) {
@@ -398,20 +552,10 @@

     if ( current_user_can( 'manage_options' ) ) {
         $get_params = filter_input_array( INPUT_GET, FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?: [];
-        $cookies    = [];
-
-        foreach ( $_COOKIE as $key => $value ) {
-            $cookies[$key] = is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : $value;
-        }

         $preset += [
-            'admin_email'       => get_option( 'admin_email', '' ),
-            'login_url'         => esc_url_raw( wp_login_url() ),
-            'registration_url'  => esc_url_raw( wp_registration_url() ),
-            'lost_password_url' => esc_url_raw( wp_lostpassword_url() ),
-            'logout_url'        => esc_url_raw( wp_logout_url() ),
-            'get_params'        => $get_params,
-            'cookie_values'     => $cookies,
+            'admin_email' => get_option( 'admin_email', '' ),
+            'get_params'  => $get_params,
         ];
     }

@@ -1291,4 +1435,4 @@
     }

     return str_replace( $search, $replace, $content );
-}
 No newline at end of file
+}
--- a/formgent/app/Helpers/helper.php
+++ b/formgent/app/Helpers/helper.php
@@ -71,6 +71,284 @@
     echo file_get_contents( $svg );
 }

+/**
+ * Allowed SVG tags and attributes for sanitizing inline icons.
+ *
+ * @return array<string, array<string, bool>>
+ */
+function formgent_allowed_svg_tags(): array {
+    return [
+        'svg'      => [
+            'xmlns'       => true,
+            'viewbox'     => true,
+            'width'       => true,
+            'height'      => true,
+            'fill'        => true,
+            'class'       => true,
+            'aria-hidden' => true,
+            'focusable'   => true,
+            'role'        => true,
+        ],
+        'path'     => [
+            'd'            => true,
+            'fill'         => true,
+            'fill-rule'    => true,
+            'clip-rule'    => true,
+            'stroke'       => true,
+            'stroke-width' => true,
+        ],
+        'circle'   => [
+            'cx'     => true,
+            'cy'     => true,
+            'r'      => true,
+            'fill'   => true,
+            'stroke' => true,
+        ],
+        'rect'     => [
+            'x'      => true,
+            'y'      => true,
+            'width'  => true,
+            'height' => true,
+            'rx'     => true,
+            'ry'     => true,
+            'fill'   => true,
+        ],
+        'g'        => [
+            'fill'      => true,
+            'transform' => true,
+        ],
+        'polyline' => [
+            'points' => true,
+            'fill'   => true,
+            'stroke' => true,
+        ],
+        'polygon'  => [
+            'points' => true,
+            'fill'   => true,
+        ],
+        'line'     => [
+            'x1'     => true,
+            'y1'     => true,
+            'x2'     => true,
+            'y2'     => true,
+            'stroke' => true,
+        ],
+        'defs'     => [],
+        'clippath' => [
+            'id' => true,
+        ],
+        'span'     => [
+            'class' => true,
+        ],
+    ];
+}
+
+function formgent_choice_option_image_size(): string {
+    return (string) apply_filters( 'formgent_choice_option_image_size', 'formgent-option-thumb' );
+}
+
+/**
+ * Image size used when rendering tiny choice/dropdown option thumbnails.
+ *
+ * @return array{width:int,height:int,crop:bool}
+ */
+function formgent_choice_option_image_size_args(): array {
+    $args = apply_filters(
+        'formgent_choice_option_image_size_args',
+        [
+            'width'  => 64,
+            'height' => 64,
+            'crop'   => true,
+        ]
+    );
+
+    return [
+        'width'  => max( 1, absint( $args['width'] ?? 64 ) ),
+        'height' => max( 1, absint( $args['height'] ?? 64 ) ),
+        'crop'   => (bool) ( $args['crop'] ?? true ),
+    ];
+}
+
+function formgent_get_choice_option_image_id( array $image ): int {
+    $image_id = absint( $image['id'] ?? 0 );
+
+    if ( $image_id ) {
+        return $image_id;
+    }
+
+    foreach ( [ $image['thumbnail'] ?? '', $image['url'] ?? '' ] as $image_url ) {
+        if ( empty( $image_url ) ) {
+            continue;
+        }
+
+        $image_id = attachment_url_to_postid( $image_url );
+
+        if ( $image_id ) {
+            return absint( $image_id );
+        }
+    }
+
+    return 0;
+}
+
+function formgent_get_attachment_intermediate_image_url( int $image_id, string $size ): string {
+    if ( ! $image_id || 'full' === $size || ! image_get_intermediate_size( $image_id, $size ) ) {
+        return '';
+    }
+
+    $image_url = wp_get_attachment_image_url( $image_id, $size );
+    return $image_url ? esc_url( $image_url ) : '';
+}
+
+function formgent_generate_choice_option_image_size( int $image_id, string $size ): string {
+    if ( ! $image_id || formgent_choice_option_image_size() !== $size ) {
+        return '';
+    }
+
+    $metadata = wp_get_attachment_metadata( $image_id );
+
+    if ( ! is_array( $metadata ) ) {
+        return '';
+    }
+
+    if ( ! empty( $metadata['sizes'][ $size ]['file'] ) ) {
+        return formgent_get_attachment_intermediate_image_url( $image_id, $size );
+    }
+
+    $file = get_attached_file( $image_id );
+
+    if ( ! $file || ! is_file( $file ) ) {
+        return '';
+    }
+
+    if ( ! function_exists( 'image_make_intermediate_size' ) ) {
+        require_once ABSPATH . 'wp-admin/includes/image.php';
+    }
+
+    $size_args = formgent_choice_option_image_size_args();
+    $resized   = image_make_intermediate_size( $file, $size_args['width'], $size_args['height'], $size_args['crop'] );
+
+    if ( ! is_array( $resized ) || empty( $resized['file'] ) ) {
+        return '';
+    }
+
+    $metadata['sizes'][ $size ] = $resized;
+    wp_update_attachment_metadata( $image_id, $metadata );
+
+    return formgent_get_attachment_intermediate_image_url( $image_id, $size );
+}
+
+function formgent_get_choice_option_image_url( array $image, string $size = '' ): string {
+    $image_size = $size ?: formgent_choice_option_image_size();
+    $image_id   = formgent_get_choice_option_image_id( $image );
+
+    if ( $image_id ) {
+        $image_url = formgent_get_attachment_intermediate_image_url( $image_id, $image_size );
+
+        if ( ! $image_url ) {
+            $image_url = formgent_generate_choice_option_image_size( $image_id, $image_size );
+        }
+
+        if ( ! $image_url && 'thumbnail' !== $image_size ) {
+            $image_url = formgent_get_attachment_intermediate_image_url( $image_id, 'thumbnail' );
+        }
+
+        if ( $image_url ) {
+            return esc_url( $image_url );
+        }
+    }
+
+    return esc_url( $image['thumbnail'] ?? $image['url'] ?? '' );
+}
+
+function formgent_get_choice_option_media( array $option ): array {
+    $allowed_svg_tags = formgent_allowed_svg_tags();
+    $media            = [];
+
+    if ( ! empty( $option['icon'] ) && is_array( $option['icon'] ) && ! empty( $option['icon']['svg'] ) ) {
+        $media['icon'] = [
+            'name' => esc_attr( $option['icon']['name'] ?? '' ),
+            'svg'  => wp_kses( $option['icon']['svg'], $allowed_svg_tags ),
+        ];
+    }
+
+    if ( ! empty( $option['image'] ) && is_array( $option['image'] ) ) {
+        $image_url = formgent_get_choice_option_image_url( $option['image'] );
+
+        if ( $image_url ) {
+            $media['image'] = [
+                'id'        => absint( $option['image']['id'] ?? 0 ),
+                'url'       => esc_url( $option['image']['url'] ?? '' ),
+                'alt'       => esc_attr( $option['image']['alt'] ?? '' ),
+                'thumbnail' => $image_url,
+            ];
+        }
+    }
+
+    return $media;
+}
+
+function formgent_render_choice_option_media( array $option ): void {
+    $media = formgent_get_choice_option_media( $option );
+
+    if ( ! empty( $media['icon']['svg'] ) ) {
+        echo '<span class="formgent-option-icon">' . wp_kses( $media['icon']['svg'], formgent_allowed_svg_tags() ) . '</span>';
+        return;
+    }
+
+    if ( ! empty( $media['image']['thumbnail'] ) ) {
+        printf(
+            '<img class="formgent-option-image" src="%s" alt="%s" width="24" height="24" loading="lazy" decoding="async" />',
+            esc_url( $media['image']['thumbnail'] ),
+            esc_attr( $media['image']['alt'] ?? '' )
+        );
+    }
+}
+
+/**
+ * Sanitize choice options including icon SVG and image data.
+ *
+ * @param mixed $options
+ * @return array<int, array<string, mixed>>
+ */
+function formgent_sanitize_choice_options( $options ): array {
+    if ( empty( $options ) || ! is_array( $options ) ) {
+        return [];
+    }
+
+    return array_map(
+        static function( $option ) {
+            if ( ! is_array( $option ) ) {
+                return [];
+            }
+
+            $sanitized = map_deep(
+                array_diff_key( $option, array_flip( [ 'icon', 'image' ] ) ),
+                'esc_attr'
+            );
+
+            if ( ! empty( $option['icon'] ) && is_array( $option['icon'] ) ) {
+                $media = formgent_get_choice_option_media( $option );
+
+                if ( ! empty( $media['icon'] ) ) {
+                    $sanitized['icon'] = $media['icon'];
+                }
+            }
+
+            if ( ! empty( $option['image'] ) && is_array( $option['image'] ) ) {
+                $media = $media ?? formgent_get_choice_option_media( $option );
+
+                if ( ! empty( $media['image'] ) ) {
+                    $sanitized['image'] = $media['image'];
+                }
+            }
+
+            return $sanitized;
+        },
+        $options
+    );
+}
+
 function formgent_date_time_format() {
     return apply_filters( 'formgent_date_time_format', 'Y-m-d H:i:s' );
 }
--- a/formgent/app/Http/Controllers/Admin/ResponseController.php
+++ b/formgent/app/Http/Controllers/Admin/ResponseController.php
@@ -782,6 +782,10 @@
                 }
             }

+            if ( 'dropdown' === $field['field_type'] && isset( $field['allow_multi_select'] ) ) {
+                $field_data['allow_multi_select'] = (bool) $field['allow_multi_select'];
+            }
+
             // Include "other option" config for choice fields so the edit UI
             // can show/hide the "Other" input without heuristics.
             if ( in_array( $field['field_type'], ['single-choice', 'multiple-choice'], true ) ) {
@@ -871,6 +875,11 @@
                 $sanitized_option['value'] = sanitize_text_field( $option['value'] );
             }

+            $sanitized_option = array_merge(
+                $sanitized_option,
+                formgent_get_choice_option_media( $option )
+            );
+
             if ( ! empty( $sanitized_option ) ) {
                 $sanitized_options[] = $sanitized_option;
             }
@@ -1118,4 +1127,4 @@
         $count = $this->repository->get_total_unread_count();
         return Response::send( [ 'total_unread' => $count ] );
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Http/Controllers/AttachmentController.php
+++ b/formgent/app/Http/Controllers/AttachmentController.php
@@ -4,6 +4,7 @@

 defined( "ABSPATH" ) || exit;

+use FormGentAppUtilsUploadFileToken;
 use FormGentWpMVCHelpersHelpers;
 use FormGentAppHttpControllersController;
 use FormGentWpMVCExceptionsException;
@@ -40,12 +41,19 @@
         // Remove the filter after upload
         remove_filter( 'upload_dir', [ $this, 'custom_upload_dir' ] );

-        $trimmed_path = preg_replace( '/^.*uploads//', '', $attachment['file'] );
+        $upload_dir    = wp_upload_dir( null, false );
+        $uploads_base  = trailingslashit( wp_normalize_path( $upload_dir['basedir'] ) );
+        $uploaded_file = wp_normalize_path( $attachment['file'] );
+        $relative_path = UploadFileToken::normalize_relative_path( str_replace( $uploads_base, '', $uploaded_file ) );
+
+        if ( null === $relative_path ) {
+            throw new Exception( esc_html__( 'Invalid file path', 'formgent' ), 400 );
+        }

         return Response::send(
             [
                 "data" => [
-                    "file_token" => base64_encode( $trimmed_path )
+                    "file_token" => UploadFileToken::create( $relative_path )
                 ]
             ], 201
         );
@@ -63,17 +71,16 @@
             ]
         );

-        $file_path = ABSPATH . 'wp-content/uploads/' . base64_decode( $request->get_param( "file_token" ) );
+        $relative_path = UploadFileToken::path_from_token( (string) $request->get_param( "file_token" ) );

-        $read_file_path = realpath( $file_path );
-        $real_base_path = realpath( ABSPATH . 'wp-content/uploads/formgent' ) . DIRECTORY_SEPARATOR;
-
-        if ( $read_file_path === false || strpos( $read_file_path, $real_base_path ) !== 0 ) {
-            throw new Exception( "Invalid file path", 400 );
+        if ( null === $relative_path ) {
+            throw new Exception( esc_html__( 'Invalid file token', 'formgent' ), 400 );
         }

-        if ( ! file_exists( $file_path ) ) {
-            throw new Exception( "File not found", 404 );
+        $file_path = UploadFileToken::resolve_existing_upload_path( $relative_path );
+
+        if ( null === $file_path ) {
+            throw new Exception( esc_html__( 'Invalid file path', 'formgent' ), 400 );
         }

         //delete the file
@@ -102,4 +109,4 @@

         return $upload_dir;
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Http/Middleware/EnsureCanAccessFormGent.php
+++ b/formgent/app/Http/Middleware/EnsureCanAccessFormGent.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanAccessFormGent implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_access();
+    }
+}
--- a/formgent/app/Http/Middleware/EnsureCanCreateForms.php
+++ b/formgent/app/Http/Middleware/EnsureCanCreateForms.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanCreateForms implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_create_forms();
+    }
+}
--- a/formgent/app/Http/Middleware/EnsureCanDeleteForms.php
+++ b/formgent/app/Http/Middleware/EnsureCanDeleteForms.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanDeleteForms implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_delete_forms();
+    }
+}
--- a/formgent/app/Http/Middleware/EnsureCanEditForms.php
+++ b/formgent/app/Http/Middleware/EnsureCanEditForms.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanEditForms implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_edit_forms();
+    }
+}
--- a/formgent/app/Http/Middleware/EnsureCanPublishForms.php
+++ b/formgent/app/Http/Middleware/EnsureCanPublishForms.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanPublishForms implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_publish_forms();
+    }
+}
--- a/formgent/app/Http/Middleware/EnsureCanReadForms.php
+++ b/formgent/app/Http/Middleware/EnsureCanReadForms.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppHttpMiddleware;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCRoutingContractsMiddleware;
+use WP_REST_Request;
+
+class EnsureCanReadForms implements Middleware {
+    public function handle( WP_REST_Request $wp_rest_request ): bool {
+        return Capabilities::can_read_forms();
+    }
+}
--- a/formgent/app/Providers/Admin/MenuServiceProvider.php
+++ b/formgent/app/Providers/Admin/MenuServiceProvider.php
@@ -5,6 +5,7 @@
 defined( 'ABSPATH' ) || exit;

 use FormGentWpMVCContractsProvider;
+use FormGentAppUtilsCapabilities;

 class MenuServiceProvider implements Provider
 {
@@ -48,8 +49,8 @@
         $icon = file_get_contents( $icon_dir );
         $icon = 'data:image/svg+xml;base64,' . base64_encode( $icon );

-        add_menu_page( esc_html__( 'FormGent', 'formgent' ), esc_html__( 'FormGent', 'formgent' ), 'manage_options', 'formgent-menu', function () { }, $icon, 5 );
-        add_submenu_page( 'formgent-menu', esc_html__( 'All Forms', 'formgent' ), esc_html__( 'All Forms', 'formgent' ), 'manage_options', 'formgent', [$this, 'content'] );
+        add_menu_page( esc_html__( 'FormGent', 'formgent' ), esc_html__( 'FormGent', 'formgent' ), Capabilities::ACCESS, 'formgent-menu', function () { }, $icon, 5 );
+        add_submenu_page( 'formgent-menu', esc_html__( 'All Forms', 'formgent' ), esc_html__( 'All Forms', 'formgent' ), Capabilities::ACCESS, 'formgent', [$this, 'content'] );

         $entries_title = esc_html__( 'Entries', 'formgent' );
         $unread_count  = formgent_response_repository()->get_total_unread_count();
@@ -73,4 +74,4 @@
     public function content() {
         echo '<div class="formgent-root"></div>';
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Providers/BlockServiceProvider.php
+++ b/formgent/app/Providers/BlockServiceProvider.php
@@ -9,10 +9,22 @@

 class BlockServiceProvider implements Provider {
     public function boot() {
+        add_action( 'after_setup_theme', [ $this, 'register_image_sizes' ] );
         add_action( 'init', [ $this, 'action_init' ] );
         add_filter( 'formgent_pagination_summery', [$this, 'get_summary'], 10, 2 );
     }

+    public function register_image_sizes(): void {
+        $size_args = formgent_choice_option_image_size_args();
+
+        add_image_size(
+            formgent_choice_option_image_size(),
+            $size_args['width'],
+            $size_args['height'],
+            $size_args['crop']
+        );
+    }
+
     public function get_summary( $answers, $field ) {
         if ( FileUpload::get_key() === $field['field_type'] ) {
             $answers = array_map(
@@ -86,4 +98,4 @@
         $tag_name = $first_element->nodeName;
         return $tag_name;
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Providers/CapabilitiesServiceProvider.php
+++ b/formgent/app/Providers/CapabilitiesServiceProvider.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FormGentAppProviders;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppUtilsCapabilities;
+use FormGentWpMVCContractsProvider;
+
+class CapabilitiesServiceProvider implements Provider {
+    public function boot() {
+        Capabilities::maybe_install();
+        add_filter( 'user_has_cap', [Capabilities::class, 'filter_user_has_cap'] );
+    }
+}
--- a/formgent/app/Providers/MailchimpProvider.php
+++ b/formgent/app/Providers/MailchimpProvider.php
@@ -185,8 +185,38 @@

     protected function get_field_value( AnswerFieldDTO $answer, string $option_value_type = 'label' ) {
         if ( 'label' === $option_value_type && ( 'single-choice' === $answer->get_field_type() || 'dropdown' === $answer->get_field_type() ) ) {
+            $value = $answer->get_value();
+
+            if ( is_string( $value ) ) {
+                $decoded = json_decode( $value, true );
+
+                if ( is_array( $decoded ) ) {
+                    $value = $decoded;
+                }
+            }
+
+            if ( is_array( $value ) ) {
+                return implode(
+                    ', ',
+                    array_filter(
+                        array_map(
+                            function( $item ) use ( $answer ) {
+                                foreach ( $answer->get_options() as $option ) {
+                                    if ( $option['value'] === $item ) {
+                                        return $option['label'];
+                                    }
+                                }
+
+                                return $item;
+                            },
+                            $value
+                        )
+                    )
+                );
+            }
+
             foreach ( $answer->get_options() as $option ) {
-                if ( $option['value'] === $answer->get_value() ) {
+                if ( $option['value'] === $value ) {
                     return $option['label'];
                 }
             }
--- a/formgent/app/Providers/PaymentServiceProvider.php
+++ b/formgent/app/Providers/PaymentServiceProvider.php
@@ -156,6 +156,71 @@
         return $map;
     }

+    private function normalize_selected_values( $value ): array {
+        if ( is_string( $value ) ) {
+            $decoded = json_decode( $value, true );
+
+            if ( is_array( $decoded ) ) {
+                $value = $decoded;
+            } elseif ( false !== strpos( $value, ',' ) ) {
+                $value = array_map( 'trim', explode( ',', $value ) );
+            }
+        }
+
+        if ( ! is_array( $value ) ) {
+            return '' === $value || null === $value ? [] : [ strval( $value ) ];
+        }
+
+        $selected = [];
+
+        foreach ( $value as $key => $item ) {
+            if ( false === $item || null === $item || '' === $item ) {
+                continue;
+            }
+
+            if ( is_int( $key ) ) {
+                if ( is_scalar( $item ) ) {
+                    $selected[] = strval( $item );
+                }
+                continue;
+            }
+
+            $selected[] = strval( $key );
+        }
+
+        return array_values( array_unique( $selected ) );
+    }
+
+    private function add_choice_order_items( array &$items, float &$total, array $field, array $selected_values, int $quantity ): void {
+        if ( empty( $field['options'] ) || empty( $selected_values ) ) {
+            return;
+        }
+
+        $selected_values = array_map( 'strval', $selected_values );
+
+        foreach ( $field['options'] as $opt ) {
+            if ( ! in_array( strval( $opt['value'] ?? '' ), $selected_values, true ) ) {
+                continue;
+            }
+
+            $raw_unit = $opt['price'] ?? ( $opt['numeric_value'] ?? ( $opt['numericValue'] ?? null ) );
+
+            if ( null === $raw_unit || '' === $raw_unit ) {
+                continue;
+            }
+
+            $unit    = abs( (float) $raw_unit );
+            $amount  = $unit * $quantity;
+            $items[] = [
+                'title'        => $opt['label'] ?? ( $field['label'] ?? ( $opt['value'] ?? '' ) ),
+                'unit_amount'  => $unit,
+                'quantity'     => $quantity,
+                'total_amount' => $amount,
+            ];
+            $total  += $amount;
+        }
+    }
+
     private function build_order_items( array $fields, array $form_data ): array {
         $items = [];
         $total = 0;
@@ -361,60 +426,23 @@

                     case 'single-choice':
                     case 'dropdown':
-                        // Single selection - match option price
-                        if ( ! empty( $ref_field['options'] ) ) {
-                            $selected_value = $form_data[$field_name];
-                            // Could be an object like { value: true } or just a string
-                            if ( is_array( $selected_value ) ) {
-                                // Extract the actual selected value from the object
-                                foreach ( $selected_value as $val => $checked ) {
-                                    if ( $checked ) {
-                                        $selected_value = $val;
-                                        break;
-                                    }
-                                }
-                            }
-                            foreach ( $ref_field['options'] as $opt ) {
-                                $raw_unit = $opt['price'] ?? ( $opt['numeric_value'] ?? ( $opt['numericValue'] ?? null ) );
-                                if ( $opt['value'] === $selected_value && $raw_unit !== null && $raw_unit !== '' ) {
-                                    $unit    = abs( (float) $raw_unit );
-                                    $amount  = $unit * $field_qty;
-                                    $items[] = [
-                                        'title'        => $opt['label'] ?? $field_name,
-                                        'unit_amount'  => $unit,
-                                        'quantity'     => $field_qty,
-                                        'total_amount' => $amount,
-                                    ];
-                                    $total  += $amount;
-                                    break;
-                                }
-                            }
-                        }
+                        $this->add_choice_order_items(
+                            $items,
+                            $total,
+                            $ref_field,
+                            $this->normalize_selected_values( $form_data[$field_name] ),
+                            $field_qty
+                        );
                         break;

                     case 'multiple-choice':
-                        // Multi selection - sum selected options' prices
-                        if ( ! empty( $ref_field['options'] ) && is_array( $form_data[$field_name] ) ) {
-                            foreach ( $form_data[$field_name] as $val => $checked ) {
-                                if ( ! $checked )
-                                    continue;
-                                foreach ( $ref_field['options'] as $opt ) {
-                                    $raw_unit = $opt['price'] ?? ( $opt['numeric_value'] ?? ( $opt['numericValue'] ?? null ) );
-                                    if ( $opt['value'] === $val && $raw_unit !== null && $raw_unit !== '' ) {
-                                        $unit    = abs( (float) $raw_unit );
-                                        $amount  = $unit * $field_qty;
-                                        $items[] = [
-                                            'title'        => $opt['label'] ?? $val,
-                                            'unit_amount'  => $unit,
-                                            'quantity'     => $field_qty,
-                                            'total_amount' => $amount,
-                                        ];
-                                        $total  += $amount;
-                                        break;
-                                    }
-                                }
-                            }
-                        }
+                        $this->add_choice_order_items(
+                            $items,
+                            $total,
+                            $ref_field,
+                            $this->normalize_selected_values( $form_data[$field_name] ),
+                            $field_qty
+                        );
                         break;
                 }
             }
--- a/formgent/app/Providers/PdfCleanupServiceProvider.php
+++ b/formgent/app/Providers/PdfCleanupServiceProvider.php
@@ -15,7 +15,10 @@
         // Clean up generated PDF files when responses are deleted.
         add_action( 'formgent_before_delete_responses', [ $this, 'on_responses_deleted' ], 10, 1 );
         add_action( 'formgent_before_delete_all_responses', [ $this, 'on_responses_deleted' ], 10, 1 );
+        add_action( 'init', [ $this, 'schedule_cleanup' ] );
+    }

+    public function schedule_cleanup(): void {
         // Schedule cron only on admin requests to avoid per-request DB queries on frontend.
         if ( is_admin() && ! wp_next_scheduled( self::CRON_HOOK ) ) {
             wp_schedule_event( time(), 'daily', self::CRON_HOOK );
--- a/formgent/app/Providers/PostTypeServiceProvider.php
+++ b/formgent/app/Providers/PostTypeServiceProvider.php
@@ -6,6 +6,7 @@

 use FormGentWpMVCViewView;
 use FormGentWpMVCContractsProvider;
+use FormGentAppUtilsCapabilities;
 use WP_Post;

 class PostTypeServiceProvider implements Provider {
@@ -133,7 +134,7 @@
     public function filter_the_content( string $content ) : string {
         global $post;

-        if ( $post->post_type !==  formgent_post_type() ) {
+        if ( ! $post instanceof WP_Post || $post->post_type !== formgent_post_type() ) {
             return $content;
         }

@@ -181,7 +182,24 @@
             'show_in_menu'       => false,
             'query_var'          => true,
             'rewrite'            => [ 'slug' => 'form' ],
-            'capability_type'    => 'post',
+            'capability_type'    => [ 'formgent_form', 'formgent_forms' ],
+            'capabilities'       => [
+                'edit_post'              => 'formgent_edit_form',
+                'read_post'              => 'formgent_read_form',
+                'delete_post'            => 'formgent_delete_form',
+                'edit_posts'             => Capabilities::EDIT_FORMS,
+                'edit_others_posts'      => Capabilities::EDIT_FORMS,
+                'publish_posts'          => Capabilities::PUBLISH_FORMS,
+                'read_private_posts'     => Capabilities::READ_FORMS,
+                'delete_posts'           => Capabilities::DELETE_FORMS,
+                'delete_private_posts'   => Capabilities::DELETE_FORMS,
+                'delete_published_posts' => Capabilities::DELETE_FORMS,
+                'delete_others_posts'    => Capabilities::DELETE_FORMS,
+                'edit_private_posts'     => Capabilities::EDIT_FORMS,
+                'edit_published_posts'   => Capabilities::EDIT_FORMS,
+                'create_posts'           => Capabilities::CREATE_FORMS,
+            ],
+            'map_meta_cap'       => true,
             'has_archive'        => true,
             'hierarchical'       => false,
             'menu_position'      => null,
@@ -226,7 +244,7 @@
                 'single'        => true, // One value per post
                 'default'       => [], // Default is an empty array
                 'auth_callback' => function () {
-                    return current_user_can( 'edit_posts' );
+                    return Capabilities::can_edit_forms();
                 },
             ]
         );
@@ -287,7 +305,7 @@
                     "show_labels"              => true
                 ],
                 'auth_callback' => function () {
-                    return current_user_can( 'edit_posts' );
+                    return Capabilities::can_edit_forms();
                 },
             ]
         );
@@ -302,7 +320,7 @@
                     ],
                 ],
                 'auth_callback' => function() {
-                    return current_user_can( 'edit_posts' );
+                    return Capabilities::can_edit_forms();
                 },
             ]
         );
@@ -321,7 +339,7 @@
                     ],
                 ],
                 'auth_callback' => function() {
-                    return current_user_can( 'edit_posts' );
+                    return Capabilities::can_edit_forms();
                 },
             ]
         );
@@ -341,7 +359,7 @@
                     ],
                 ],
                 'auth_callback' => function() {
-                    return current_user_can( 'edit_posts' );
+                    return Capabilities::can_edit_forms();
                 },
             ]
         );
--- a/formgent/app/Providers/ResponseLogServiceProvider.php
+++ b/formgent/app/Providers/ResponseLogServiceProvider.php
@@ -130,6 +130,17 @@

         switch ( $field_type ) {
             case Dropdown::get_key():
+                $decoded = json_decode( $raw_value, true );
+                if ( is_array( $decoded ) ) {
+                    $labels = array_map(
+                        function ( $val ) use ( $field_data ) {
+                            $label = $this->get_option_label( $field_data, $val );
+                            return '' !== $label ? $label : $val;
+                        },
+                        $decoded
+                    );
+                    return implode( ', ', array_filter( $labels ) );
+                }
             case SingleChoice::get_key():
                 $label = $this->get_option_label( $field_data, $raw_value );
                 if ( '' !== $label ) {
--- a/formgent/app/Providers/SpreadsheetServiceProvider.php
+++ b/formgent/app/Providers/SpreadsheetServiceProvider.php
@@ -167,6 +167,28 @@
      * Get option label by value from field options
      */
     private function get_option_label_by_value( AnswerFieldDTO $field, $value ) {
+        if ( is_array( $value ) ) {
+            return implode(
+                PHP_EOL,
+                array_filter(
+                    array_map(
+                        function( $item ) use ( $field ) {
+                            return $this->get_option_label_by_value( $field, $item );
+                        },
+                        $value
+                    )
+                )
+            );
+        }
+
+        if ( is_string( $value ) ) {
+            $decoded = json_decode( $value, true );
+
+            if ( is_array( $decoded ) ) {
+                return $this->get_option_label_by_value( $field, $decoded );
+            }
+        }
+
         $option_key = array_search( $value, array_column( $field->get_options(), 'value' ) );
         return is_int( $option_key ) ? $field->get_options()[$option_key]['label'] : '';
     }
@@ -260,4 +282,4 @@

         $this->spreadsheet_header_job->dispatch_spreadsheet( $spreadsheet, $post_id );
     }
-}
 No newline at end of file
+}
--- a/formgent/app/Repositories/AnswerRepository.php
+++ b/formgent/app/Repositories/AnswerRepository.php
@@ -6,6 +6,7 @@

 use FormGentAppDTOAnswerDTO;
 use FormGentAppModelsAnswer;
+use FormGentAppUtilsAnswerValueSanitizer;
 use FormGentWpMVCDatabaseQueryBuilder;
 use FormGentWpMVCRepositoriesRepository;
 use FormGentWpMVCDTODTO;
@@ -26,7 +27,7 @@
         return Answer::query()->insert(
             array_map(
                 function( AnswerDTO $field ) use( $response_id ) {
-                    return $this->process_values( $field->set_response_id( $response_id )->to_array() );
+                    return $this->prepare_values( $field->set_response_id( $response_id )->to_array() );
                 }, $items
             )
         );
@@ -39,7 +40,7 @@
         return Answer::query()->insert(
             array_map(
                 function( $item ) {
-                    return $this->process_values( $item );
+                    return $this->prepare_values( $item );
                 }, $array
             )
         );
@@ -69,7 +70,7 @@
             return false;
         }

-        return Answer::query()->insert_get_id( $this->process_values( $dto->to_array() ) );
+        return Answer::query()->insert_get_id( $this->prepare_values( $dto->to_array() ) );
     }

     public function update( DTO $dto ) {
@@ -78,7 +79,7 @@
             return false;
         }

-        $data = $this->process_values( $dto->to_array( true ) );
+        $data = $this->prepare_values( $dto->to_array( true ) );

         // Remove 'id' from update data as it's used in WHERE clause
         unset( $data['id'] );
@@ -86,7 +87,7 @@
         // Ensure 'value' field is always included in update (even if null)
         // This is critical for updates to work correctly
         if ( ! isset( $data['value'] ) ) {
-            $data['value'] = $dto->get_value();
+            $data['value'] = AnswerValueSanitizer::sanitize( $dto->get_value() );
             // Process the value if it's an array or object
             if ( is_array( $data['value'] ) || ( is_object( $data['value'] ) && get_class( $data['value'] ) === 'stdClass' ) ) {
                 $data['value'] = wp_json_encode( $data['value'] );
@@ -107,4 +108,12 @@
             ->where( 'id', $dto->get_id() )
             ->update( $data );
     }
-}
 No newline at end of file
+
+    private function prepare_values( array $values ): array {
+        if ( array_key_exists( 'value', $values ) ) {
+            $values['value'] = AnswerValueSanitizer::sanitize( $values['value'] );
+        }
+
+        return $this->process_values( $values );
+    }
+}
--- a/formgent/app/Repositories/QuizRepository.php
+++ b/formgent/app/Repositories/QuizRepository.php
@@ -73,11 +73,12 @@
             // If the field has choices
             if ( in_array( $field['field_type'], $choice_fields ) ) {
                 $field_options      = [];
-                $is_multiple_choice = 'multiple-choice' === $field['field_type'];
+                $is_multiple_choice = 'multiple-choice' === $field['field_type']
+                    || ( 'dropdown' === $field['field_type'] && ! empty( $field['allow_multi_select'] ) );

                 if ( $is_multiple_choice ) {
-                    // Multiple-choice: new quiz config uses options[].is_default for correct answers
-                    // and sums their numeric_value for total points.
+                    // Multiple-choice and multi-select dropdown use options[].is_default for correct answers.
+                    // The total points are the sum of those correct options' numeric_value values.
                     // Back-compat: fall back to legacy correct_answer/points when is_default/numeric_value are not present.
                     $derived_correct = [];
                     $derived_points  = null;
@@ -108,11 +109,23 @@
                         }
                     }

-                    $correct_answer   = ! empty( $derived_correct )
-                        ? $derived_correct
-                        : ( ( isset( $field['correct_answer'] ) && is_array( $field['correct_answer'] ) ) ? $field['correct_answer'] : [] );
-                    $submitted_answer = isset( $field['value'] ) ? json_decode( $field

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.