--- 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;
}