Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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