Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/master-addons/class-master-elementor-addons.php
+++ b/master-addons/class-master-elementor-addons.php
@@ -470,7 +470,6 @@
'Shortcode_Manager' => 'class-shortcode-manager.php',
'Control_Manager' => 'class-control-manager.php',
'Icon_Library_Helper' => 'icon-library-helper.php',
- 'Widget_Builder' => 'widget-builder.php',
'Control_Base' => 'controls/class-control-base.php',
];
if (isset($wb_file_map[$wb_class])) {
@@ -1147,7 +1146,7 @@
MasterAddonsIncAdminTheme_BuilderLoader::get_instance();
MasterAddonsIncAdminPopupBuilderPopup_Builder_Init::get_instance();
- // MasterAddonsIncAdminWidgetBuilderWidget_Builder_Init::get_instance();
+ MasterAddonsIncAdminWidgetBuilderWidget_Builder_Init::get_instance();
REST_API::get_instance();
MasterAddonsIncAdminPage_Importer::get_instance();
--- a/master-addons/inc/admin/config.php
+++ b/master-addons/inc/admin/config.php
@@ -2010,7 +2010,7 @@
'custom-js' => [
'title' => 'Custom JS',
'icon' => 'eicon-code-bold',
- 'class' => 'MasterAddonsModulesUtilitiesCustomJs',
+ 'class' => 'MasterAddonsProModulesUtilitiesCustomJs',
'demo_url' => 'https://master-addons.com/extension/custom-js/',
'docs_url' => 'https://master-addons.com/docs/extensions/custom-js/',
'tuts_url' => 'https://www.youtube.com/watch?v=8G4JLw0s8sI',
@@ -2020,7 +2020,7 @@
"Runs only on the target element",
"No extra plugin or coding setup"
],
- 'is_pro' => false,
+ 'is_pro' => true,
],
'duplicator' => [
'title' => 'Post/Page Duplicator',
--- a/master-addons/inc/admin/popup-builder/class-popup-cpt.php
+++ b/master-addons/inc/admin/popup-builder/class-popup-cpt.php
@@ -447,6 +447,10 @@
public function get_shortcode_ajax() {
check_ajax_referer('jltma_popup_nonce', '_nonce');
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( [ 'message' => __( 'Insufficient permissions', 'master-addons' ) ] );
+ }
+
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$shortcode = '[jltma_popup id="' . $post_id . '"]';
@@ -541,6 +545,17 @@
return $allcaps;
}
+ // Security: only grant the cap if the user already has a base editing
+ // capability through their role. Without this guard, any logged-in user
+ // (incl. subscribers) visiting the Elementor edit URL for a popup would be
+ // granted edit_post for that request — a privilege escalation. With
+ // capability_type='post' on the CPT, authors+ already have edit_posts
+ // natively, so the workaround stays a no-op for them and is fully off for
+ // lower-privileged users.
+ if (empty($user->allcaps['edit_posts']) && empty($user->allcaps['edit_pages'])) {
+ return $allcaps;
+ }
+
// Grant edit capability for this specific post type
if (isset($caps[0]) && in_array($caps[0], ['edit_post', 'edit_posts'])) {
$allcaps[$caps[0]] = true;
--- a/master-addons/inc/admin/popup-builder/class-popup-frontend.php
+++ b/master-addons/inc/admin/popup-builder/class-popup-frontend.php
@@ -504,27 +504,24 @@
if ($expiration > 0) {
$max_age = $expiration - time();
- ?>
- <script>
- const cookieName = '<?php echo esc_js($cookie_name); ?>';
- const value = '<?php echo absint( time() ); ?>';
- const maxAge = <?php echo (int) $max_age; ?>;
- const path = '/';
-
- document.cookie = `${cookieName}=${value}; max-age=${maxAge}; path=${path};`;
- </script>
- <?php
- // setcookie($cookie_name, time(), $expiration, '/');
+ $cookie_js = sprintf(
+ 'document.cookie = "%1$s=%2$d; max-age=%3$d; path=/;";',
+ esc_js($cookie_name),
+ absint(time()),
+ (int) $max_age
+ );
+ $this->add_popup_inline_script($cookie_js);
}
if ($frequency === 'once_session') {
// Session cookie (no max-age/expires) — cleared automatically when the browser closes.
// Avoids PHP sessions, which break full-page caching on the frontend.
- ?>
- <script>
- document.cookie = '<?php echo esc_js($cookie_name); ?>=<?php echo absint(time()); ?>; path=/;';
- </script>
- <?php
+ $session_js = sprintf(
+ 'document.cookie = "%1$s=%2$d; path=/;";',
+ esc_js($cookie_name),
+ absint(time())
+ );
+ $this->add_popup_inline_script($session_js);
}
}
@@ -539,9 +536,24 @@
if ( is_feed() || is_admin() || isset($_COOKIE['ma_popup_visitor']) || isset($_GET['elementor-preview']) || ( defined('ELEMENTOR_VERSION') && ElementorPlugin::$instance->preview->is_preview_mode() ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only existence check, no data processed
return;
}
- echo '<script>if (document.cookie.indexOf("ma_popup_visitor=") === -1) {
- document.cookie = "ma_popup_visitor=1; max-age=31536000; path=/;";
- }</script>';
+ $this->add_popup_inline_script('if (document.cookie.indexOf("ma_popup_visitor=") === -1) { document.cookie = "ma_popup_visitor=1; max-age=31536000; path=/;"; }');
+ }
+
+ /**
+ * Queue a small inline script through the enqueue API (printed in the footer)
+ * instead of echoing a hardcoded <script> tag. Multiple calls accumulate on the
+ * same inline-only handle.
+ *
+ * @param string $js
+ */
+ private function add_popup_inline_script($js) {
+ if (!wp_script_is('jltma-popup-inline', 'registered')) {
+ wp_register_script('jltma-popup-inline', false, array(), JLTMA_VER, true);
+ }
+ if (!wp_script_is('jltma-popup-inline', 'enqueued')) {
+ wp_enqueue_script('jltma-popup-inline');
+ }
+ wp_add_inline_script('jltma-popup-inline', $js);
}
private function get_popup_settings($popup_id) {
--- a/master-addons/inc/admin/settings/settings.php
+++ b/master-addons/inc/admin/settings/settings.php
@@ -114,7 +114,7 @@
add_action('current_screen', [$this, 'jltma_set_page_title']);
add_action('admin_enqueue_scripts', [$this, 'jltma_admin_settings_scripts'], 99);
add_action('admin_body_class', [$this, 'jltma_admin_body_class']);
- add_action('admin_head', [$this, 'jltma_global_admin_css']);
+ add_action('admin_enqueue_scripts', [$this, 'jltma_global_admin_css']);
add_action('wp_ajax_jltma_subscribe', [$this, 'handle_subscribe_ajax']);
}
@@ -151,7 +151,7 @@
*/
public function jltma_global_admin_css()
{
- echo '<style type="text/css">
+ $css = '
/* Hide separator below Master Addons menu */
#adminmenu #toplevel_page_master-addons-settings + .wp-menu-separator {
display: none !important;
@@ -236,10 +236,10 @@
.wrap .jltma-cpt-btn-youtube svg {
fill: #ff0000;
}
- </style>';
+ ';
// JS to reorder submenu items and add separator/pricing classes
- echo '<script type="text/javascript">
+ $js = '
document.addEventListener("DOMContentLoaded", function() {
var menu = document.getElementById("toplevel_page_master-addons-settings");
if (!menu) return;
@@ -286,7 +286,17 @@
}
});
});
- </script>';
+ ';
+
+ // Load the menu styling and ordering through the core enqueue APIs
+ // (inline-only handles) instead of hardcoded <style>/<script> tags.
+ wp_register_style('jltma-admin-menu', false, array(), JLTMA_VER);
+ wp_enqueue_style('jltma-admin-menu');
+ wp_add_inline_style('jltma-admin-menu', $css);
+
+ wp_register_script('jltma-admin-menu', false, array(), JLTMA_VER, true);
+ wp_enqueue_script('jltma-admin-menu');
+ wp_add_inline_script('jltma-admin-menu', $js);
}
/**
@@ -331,9 +341,9 @@
remove_action('admin_notices', 'update_nag', 3);
remove_action('admin_notices', 'maintenance_nag', 10);
- // Hide any remaining notices via CSS
- add_action('admin_head', function () {
- echo '<style type="text/css">
+ // Hide any remaining admin notices on our settings screen. Attached to the
+ // settings stylesheet (enqueued below) instead of a hardcoded <style> tag.
+ $jltma_hide_notices_css = '
.notice, .error, .updated, .update-nag, .admin-notice,
.jltma-plugin-update-notice, .fs-notice, .fs-slug-master-addons,
#wpbody-content > .notice, #wpbody-content > .error, #wpbody-content > .updated,
@@ -345,9 +355,7 @@
}
#wpfooter {
display: none !important;
- }
- </style>';
- });
+ }';
// Dequeue Spectra (UAG) zipwp-images style to prevent CSS conflicts on settings page
add_action('admin_enqueue_scripts', function () {
@@ -369,6 +377,8 @@
wp_enqueue_style('jltma-admin-settings');
wp_enqueue_script('jltma-admin-settings');
+ wp_add_inline_style('jltma-admin-settings', $jltma_hide_notices_css);
+
// White Label logo & Hidden Nav Menus
$white_label_settings = jltma_settings()->get('jltma_white_label_settings');
$white_label_settings = is_array($white_label_settings) ? $white_label_settings : [];
@@ -460,13 +470,9 @@
remove_all_actions('network_admin_notices');
remove_all_actions('user_admin_notices');
- add_action('admin_head', function () {
- echo '<style type="text/css">
- #wpfooter {
- display: none !important;
- }
- </style>';
- });
+ // Hide the footer for the full-screen wizard (attached to the wizard
+ // stylesheet below instead of a hardcoded <style> tag).
+ $jltma_wizard_css = '#wpfooter { display: none !important; }';
// Elementor icons for addon card icons
if (wp_style_is('elementor-icons', 'registered')) {
@@ -478,6 +484,8 @@
wp_enqueue_style('jltma-setup-wizard');
wp_enqueue_script('jltma-setup-wizard');
+ wp_add_inline_style('jltma-setup-wizard', $jltma_wizard_css);
+
// Build recommended plugins data with pre-computed install status.
$recommended_instance = Recommended_Plugins::get_instance();
$raw_plugins = $recommended_instance->plugins_list();
--- a/master-addons/inc/admin/templates/includes/classes/manager.php
+++ b/master-addons/inc/admin/templates/includes/classes/manager.php
@@ -713,7 +713,20 @@
public function get_template_data()
{
- $template = $this->get_template_data_array($_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce available for this AJAX handler
+ // Extract and sanitize only the specific fields we need (never forward the whole superglobal).
+ $source = isset( $_REQUEST['source'] ) ? wp_unslash( $_REQUEST['source'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+ if ( is_array( $source ) ) {
+ $source = array_map( 'sanitize_text_field', $source );
+ } else {
+ $source = sanitize_text_field( $source );
+ }
+ $request_data = array(
+ 'template_id' => isset( $_REQUEST['template_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['template_id'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+ 'tab' => isset( $_REQUEST['tab'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['tab'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+ 'source' => $source,
+ );
+
+ $template = $this->get_template_data_array($request_data);
if (!$template) {
wp_send_json_error();
--- a/master-addons/inc/admin/templates/kits/class-importer.php
+++ b/master-addons/inc/admin/templates/kits/class-importer.php
@@ -94,7 +94,7 @@
*/
public function activate_required_theme()
{
- $nonce = $_POST['nonce'];
+ $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if (!wp_verify_nonce($nonce, 'jltma-template-kits-js') || !current_user_can('manage_options')) {
exit;
@@ -130,9 +130,11 @@
public function sanitize_svg_on_upload($file)
{
if ($file['type'] === 'image/svg+xml') {
- $file_content = file_get_contents($file['tmp_name']);
+ // Direct file ops on the PHP upload temp path, before WordPress moves it.
+ // WP_Filesystem (ftp/ssh methods) cannot reach the local upload temp file.
+ $file_content = file_get_contents($file['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading the PHP upload temp file for sanitization
$sanitized_content = $this->sanitize_svg($file_content);
- file_put_contents($file['tmp_name'], $sanitized_content);
+ file_put_contents($file['tmp_name'], $sanitized_content); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing back the sanitized SVG to the PHP upload temp file before WordPress moves it
}
return $file;
}
@@ -1533,7 +1535,7 @@
$image_data = wp_remote_retrieve_body($response);
$tmp = wp_tempnam();
- file_put_contents($tmp, $image_data);
+ file_put_contents($tmp, $image_data); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing to a wp_tempnam() temp file that media_handle_sideload() then processes
if (!file_exists($tmp) || filesize($tmp) === 0) {
wp_delete_file($tmp);
--- a/master-addons/inc/admin/templates/library/template-library.php
+++ b/master-addons/inc/admin/templates/library/template-library.php
@@ -280,6 +280,25 @@
/**
* Get cached kit categories
*/
+ /**
+ * Write a file through the WP_Filesystem API.
+ *
+ * @param string $file
+ * @param string $contents
+ * @return bool
+ */
+ private function fs_put_contents($file, $contents) {
+ global $wp_filesystem;
+ if (empty($wp_filesystem)) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ WP_Filesystem();
+ }
+ if (empty($wp_filesystem)) {
+ return false;
+ }
+ return $wp_filesystem->put_contents($file, $contents, FS_CHMOD_FILE);
+ }
+
private function get_cached_kit_categories($force_refresh = false) {
$upload_dir = wp_upload_dir();
$categories_file = $upload_dir['basedir'] . '/master_addons/templates_kits/kit-categories.json';
@@ -302,7 +321,7 @@
$cached = $cache_manager->get_cached_kit_categories($force_refresh);
if ($cached !== false) {
if (!file_exists($categories_file) && !empty($cached)) {
- @file_put_contents($categories_file, json_encode($cached));
+ $this->fs_put_contents($categories_file, wp_json_encode($cached));
}
return $cached;
}
@@ -331,7 +350,7 @@
set_transient($transient_key, $fresh_categories, $cache_expiry);
// Save to local file
- @file_put_contents($categories_file, json_encode($fresh_categories));
+ $this->fs_put_contents($categories_file, wp_json_encode($fresh_categories));
return $fresh_categories;
}
@@ -1328,6 +1347,11 @@
return;
}
+ if (!current_user_can('manage_options')) {
+ wp_send_json_error(['message' => 'Insufficient permissions']);
+ return;
+ }
+
$plugins_with_status = array();
if( isset($_POST['required_plugins']) && !empty( $_POST['required_plugins'])){
--- a/master-addons/inc/admin/theme-builder/inc/api/select2-api.php
+++ b/master-addons/inc/admin/theme-builder/inc/api/select2-api.php
@@ -14,10 +14,10 @@
* This is called by WordPress REST API on each request
*/
public function check_for_permission(){
- // Allow any logged-in user who can read posts
- // Using 'read' capability instead of 'edit_posts' to be more permissive
- // Individual methods will perform their own capability checks as needed
- return current_user_can('read');
+ // These endpoints feed editor-only post/page/term pickers, so require
+ // the same capability needed to edit content. The REST API validates the
+ // X-WP-Nonce cookie nonce before this callback runs for logged-in users.
+ return current_user_can('edit_posts');
}
@@ -104,18 +104,10 @@
public function get_singular_list(){
- // Only perform security checks if user is logged in
- if (is_user_logged_in()) {
- // Verify REST API nonce that comes from JavaScript - be more lenient with nonce check
- $nonce = $this->request->get_header('X-WP-Nonce');
- if ($nonce && !wp_verify_nonce($nonce, 'wp_rest')) {
- // If nonce verification fails, don't block the request
- }
-
- // Check if user can read posts
- if (!current_user_can('read')) {
- return new WP_Error( 'rest_forbidden', __( 'Insufficient permissions.', 'master-addons' ), [ 'status' => 403 ] );
- }
+ // Capability is enforced here (and by check_for_permission()); the REST
+ // API has already validated the X-WP-Nonce cookie nonce at this point.
+ if ( ! current_user_can('edit_posts') ) {
+ return new WP_Error( 'rest_forbidden', __( 'Insufficient permissions.', 'master-addons' ), [ 'status' => 403 ] );
}
$query_args = [
--- a/master-addons/inc/admin/theme-builder/inc/cpt-hooks.php
+++ b/master-addons/inc/admin/theme-builder/inc/cpt-hooks.php
@@ -272,21 +272,21 @@
}
} else {
// Fallback to post content
- echo do_shortcode( $template_post->post_content );
+ echo wp_kses_post( do_shortcode( $template_post->post_content ) );
}
-
+
$content = ob_get_clean();
return $content;
-
+
} catch ( Exception $e ) {
ob_end_clean();
// Return post content as fallback if Elementor fails
- return do_shortcode( $template_post->post_content );
+ return wp_kses_post( do_shortcode( $template_post->post_content ) );
}
}
// Fallback: Return post content if Elementor is not available
- return do_shortcode( $template_post->post_content );
+ return wp_kses_post( do_shortcode( $template_post->post_content ) );
}
public static function instance()
--- a/master-addons/inc/admin/theme-builder/inc/theme-builder-assets.php
+++ b/master-addons/inc/admin/theme-builder/inc/theme-builder-assets.php
@@ -12,7 +12,7 @@
public function __construct()
{
- add_action('admin_print_scripts', [$this, 'jltma_admin_js']);
+ add_action('admin_enqueue_scripts', [$this, 'jltma_admin_js']);
// enqueue scripts
add_action('admin_enqueue_scripts', [$this, 'jltma_header_footer_enqueue_scripts']);
@@ -21,9 +21,11 @@
// Declare Variable for Rest API
public function jltma_admin_js()
{
- echo "<script type='text/javascript'>n";
- echo $this->jltma_common_js(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- outputs safe JS built by jltma_common_js()
- echo "n</script>";
+ // Output the REST var through the enqueue API (inline-only handle) instead of a
+ // hardcoded <script> tag.
+ wp_register_script('jltma-theme-builder-vars', false, array(), JLTMA_VER, false);
+ wp_enqueue_script('jltma-theme-builder-vars');
+ wp_add_inline_script('jltma-theme-builder-vars', $this->jltma_common_js());
}
--- a/master-addons/inc/admin/widget-builder/class-control-manager.php
+++ b/master-addons/inc/admin/widget-builder/class-control-manager.php
@@ -130,14 +130,27 @@
private function build_fallback_control($control_key, $field, $type) {
$label = !empty($field['label']) ? esc_js($field['label']) : 'Control';
+ // Elementor control constants are upper-case (e.g. REPEATER, SELECT). Normalise
+ // the type to a safe constant name so the generated PHP is always valid.
+ $type_const = strtoupper(preg_replace('/[^A-Za-z0-9_]/', '', (string) $type));
+ if ('' === $type_const) {
+ $type_const = 'TEXT';
+ }
+
$content = "tt$this->add_control(n";
$content .= "ttt'{$control_key}',n";
$content .= "ttt[n";
$content .= "tttt'label' => esc_html__('{$label}', 'master-addons'),n";
- $content .= "tttt'type' => Controls_Manager::{$type},n";
+ $content .= "tttt'type' => Controls_Manager::{$type_const},n";
+
+ // REPEATER must always carry a 'fields' array, otherwise Elementor's
+ // sanitize_settings() throws a TypeError when iterating null fields.
+ if ('REPEATER' === $type_const) {
+ $content .= "tttt'fields' => array(),n";
+ }
if (isset($field['default'])) {
- $default = is_string($field['default']) ? "'" . esc_js($field['default']) . "'" : $field['default'];
+ $default = $this->export_default_value($field['default']);
$content .= "tttt'default' => {$default},n";
}
@@ -146,4 +159,29 @@
return $content;
}
+
+ /**
+ * Convert a control default value into a valid PHP literal for the generated file.
+ * Arrays (e.g. repeater/group-control defaults) must never be interpolated directly,
+ * which would emit the literal token "Array" and produce a fatal parse error.
+ *
+ * @param mixed $value
+ * @return string
+ */
+ private function export_default_value($value) {
+ if (is_array($value)) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- generating PHP source, not debug output
+ return var_export($value, true);
+ }
+ if (is_bool($value)) {
+ return $value ? 'true' : 'false';
+ }
+ if (is_int($value) || is_float($value)) {
+ return (string) $value;
+ }
+ if (null === $value) {
+ return "''";
+ }
+ return "'" . esc_js((string) $value) . "'";
+ }
}
--- a/master-addons/inc/admin/widget-builder/class-rest-controller.php
+++ b/master-addons/inc/admin/widget-builder/class-rest-controller.php
@@ -627,33 +627,25 @@
$css_code = '';
$js_code = '';
- // Sanitize HTML code - allow all HTML/PHP for widget development
+ // Sanitize HTML code. PHP is NEVER allowed (no arbitrary code execution),
+ // regardless of capability. Inline <script> is stripped — JavaScript belongs
+ // in the JS field, which is enqueued separately. Dynamic values use {{placeholders}}.
if (isset($data['html_code'])) {
- // For admin users with widget building capability, we allow unfiltered HTML
- // This is necessary for Elementor widget development
- if (current_user_can('unfiltered_html')) {
- $html_code = $data['html_code'];
- } else {
- // For other users, use wp_kses_post which allows safe HTML
- $html_code = wp_kses_post($data['html_code']);
- }
+ $html_code = $this->strip_php_tags($data['html_code']);
+ $html_code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $html_code);
+ $html_code = preg_replace('#</?scriptb[^>]*>#i', '', $html_code);
}
- // Sanitize CSS code - allow CSS but strip PHP/JS tags
+ // Sanitize CSS code - strip PHP/script/style tags and dangerous constructs.
if (isset($data['css_code'])) {
- // Remove any potential PHP/script tags from CSS
$css_code = $this->sanitize_css($data['css_code']);
}
- // Sanitize JavaScript code - allow JS but validate syntax
+ // Sanitize JavaScript code. PHP is NEVER allowed; <script> tags are stripped so
+ // the value cannot break out of the generated/enqueued script context.
if (isset($data['js_code'])) {
- // For admin users, allow JavaScript for widget development
- if (current_user_can('unfiltered_html')) {
- $js_code = $data['js_code'];
- } else {
- // For other users, strip tags
- $js_code = wp_strip_all_tags($data['js_code']);
- }
+ $js_code = $this->strip_php_tags($data['js_code']);
+ $js_code = preg_replace('#</?scriptb[^>]*>#i', '', $js_code);
}
// Also save data in unified format for widget generator
@@ -671,6 +663,23 @@
}
/**
+ * Strip every PHP open/close tag (and null bytes) from a string so user input
+ * can never become executable PHP once written into a generated widget file.
+ *
+ * @param string $code
+ * @return string
+ */
+ private function strip_php_tags($code) {
+ if (!is_string($code) || '' === $code) {
+ return '';
+ }
+ $code = str_replace(chr(0), '', $code);
+ $code = preg_replace('/<?php/i', '', $code);
+ $code = str_replace(array('<?=', '<?', '?>'), '', $code);
+ return $code;
+ }
+
+ /**
* Sanitize CSS code
*
* @param string $css
@@ -678,11 +687,19 @@
*/
private function sanitize_css($css) {
// Remove any PHP tags
- $css = preg_replace('/<?php.*??>/s', '', $css);
- $css = preg_replace('/<?.*??>/s', '', $css);
+ $css = $this->strip_php_tags($css);
- // Remove any script tags
+ // Remove any <style>/<script> tags so the value cannot break out of the
+ // generated inline <style> block.
+ $css = preg_replace('#</?styleb[^>]*>#i', '', $css);
$css = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $css);
+ $css = preg_replace('#</?scriptb[^>]*>#i', '', $css);
+
+ // Remove dangerous CSS constructs.
+ $css = preg_replace('/@importb/i', '', $css);
+ $css = preg_replace('/expressions*(/i', '', $css);
+ $css = preg_replace('/(javascript|vbscript)s*:/i', '', $css);
+ $css = preg_replace('/behaviors*:/i', '', $css);
// Remove any JavaScript event handlers
$css = preg_replace('/onw+s*=s*["'].*?["']/i', '', $css);
--- a/master-addons/inc/admin/widget-builder/class-widget-admin.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-admin.php
@@ -795,11 +795,13 @@
}
/**
- * Render widget preview with PHP execution
- * Handles AJAX request to render widget HTML with PHP code executed
+ * Render a static widget preview.
+ * Handles the AJAX request to render widget HTML with mock control values.
+ * No user-supplied PHP or JavaScript is ever executed — placeholders are
+ * replaced with escaped default values and the markup is escaped on output.
*/
public function render_preview() {
- // Capability check: only administrators can execute widget previews (contains eval)
+ // Capability check: only administrators can request a widget preview.
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
return;
@@ -811,13 +813,25 @@
return;
}
- $html_code = isset($_POST['html_code']) ? wp_kses_post( wp_unslash( $_POST['html_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
+ $html_code = isset($_POST['html_code']) ? wp_unslash( $_POST['html_code'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above; PHP/script stripped and output escaped below
$css_code = isset($_POST['css_code']) ? sanitize_textarea_field( wp_unslash( $_POST['css_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
$controls = isset($_POST['controls']) ? json_decode( sanitize_textarea_field( wp_unslash( $_POST['controls'] ) ), true ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
- // Build mock settings array from controls
+ // Preview NEVER executes user code: strip PHP tags and inline <script>.
+ $html_code = str_replace(chr(0), '', $html_code);
+ $html_code = preg_replace('/<?php/i', '', $html_code);
+ $html_code = str_replace(array('<?=', '<?', '?>'), '', $html_code);
+ $html_code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $html_code);
+ $html_code = preg_replace('#</?scriptb[^>]*>#i', '', $html_code);
+
+ // Strip </style> and PHP from preview CSS so it cannot break out of <style>.
+ $css_code = preg_replace('#</?styleb[^>]*>#i', '', $css_code);
+ $css_code = preg_replace('/<?php/i', '', $css_code);
+ $css_code = str_replace(array('<?=', '<?', '?>'), '', $css_code);
+
+ // Build mock settings array from control defaults.
$settings = [];
- if (!empty($controls)) {
+ if (!empty($controls) && is_array($controls)) {
foreach ($controls as $control) {
if (isset($control['name']) && isset($control['default'])) {
$settings[$control['name']] = $control['default'];
@@ -825,86 +839,25 @@
}
}
- // Replace placeholders with PHP variables using regex to handle dot notation
- // Handles: {{field}}, {{field.property}}, {{field.property.subproperty}}
- if (!empty($controls)) {
- foreach ($controls as $control) {
- if (isset($control['name'])) {
- $control_name = $control['name'];
-
- // Regex to match {{control_name}} or {{control_name.property}} or {{control_name.property.subproperty}}
- // Pattern: {{control_name}} followed by optional .property.property...
- $pattern = '/{{' . preg_quote($control_name, '/') . '((?:.[a-zA-Z0-9_]+)*)}}/';
-
- $html_code = preg_replace_callback($pattern, function($matches) use ($control_name) {
- $properties = $matches[1]; // e.g., "" or ".tabs" or ".property.subproperty"
-
- if (empty($properties)) {
- // No properties, just {{control_name}}
- // Return: $settings['control_name']
- return "$settings['" . $control_name . "']";
- } else {
- // Has properties like .tabs or .property.subproperty
- // Convert to: $settings['control_name']['tabs'] or $settings['control_name']['property']['subproperty']
- $props = explode('.', ltrim($properties, '.'));
- $result = "$settings['" . $control_name . "']";
- foreach ($props as $prop) {
- if (!empty($prop)) {
- $result .= "['" . $prop . "']";
- }
- }
- return $result;
- }
- }, $html_code);
+ // Substitute {{field}} / {{field.prop}} placeholders with the mock default
+ // VALUE (escaped). This is a static render — no PHP is evaluated.
+ $html_code = preg_replace_callback('/{{([^}]+)}}/', function($matches) use ($settings) {
+ $path = array_filter(array_map('trim', explode('.', trim($matches[1]))), 'strlen');
+ $value = $settings;
+ foreach ($path as $key) {
+ if (is_array($value) && isset($value[$key])) {
+ $value = $value[$key];
+ } else {
+ return '';
}
}
- }
-
- // Execute PHP code in the HTML
- ob_start();
-
- // Make settings available in the PHP context
- extract($settings, EXTR_SKIP);
-
- // Render the code
- if (strpos($html_code, '<?php') === false && strpos($html_code, '<?=') === false) {
- // No PHP code, just output as is
- echo wp_kses_post( $html_code );
- } else {
- // Has PHP code: write it to a temporary file in the uploads directory
- // and include it. include is used instead of eval(), which is not
- // permitted in WordPress.org hosted plugins.
- $upload_dir = wp_upload_dir();
- $tmp_dir = trailingslashit($upload_dir['basedir']) . 'master-addons/widget-builder/tmp/';
-
- if (!file_exists($tmp_dir)) {
- wp_mkdir_p($tmp_dir);
- file_put_contents($tmp_dir . 'index.php', "<?phpn// Silence is golden.n");
- }
-
- $tmp_file = $tmp_dir . 'preview-' . wp_generate_password(20, false) . '.php';
-
- if (false !== file_put_contents($tmp_file, $html_code)) {
- try {
- include $tmp_file;
- } catch (ParseError $e) {
- echo '<div style="color:red;padding:20px;background:#fff3cd;border:1px solid #ffc107;">';
- echo '<h3>PHP Parse Error in Preview:</h3>';
- echo '<pre>' . esc_html($e->getMessage()) . '</pre>';
- echo '<h4>Line ' . absint( $e->getLine() ) . '</h4>';
- echo '</div>';
- } catch (Exception $e) {
- echo '<div style="color:red;padding:20px;background:#fff3cd;border:1px solid #ffc107;">';
- echo '<h3>PHP Error in Preview:</h3>';
- echo '<pre>' . esc_html($e->getMessage()) . '</pre>';
- echo '</div>';
- } finally {
- wp_delete_file($tmp_file);
- }
+ if (is_array($value)) {
+ $value = isset($value['url']) ? $value['url'] : '';
}
- }
+ return esc_html((string) $value);
+ }, $html_code);
- $rendered_html = ob_get_clean();
+ $rendered_html = wp_kses_post($html_code);
// Build full HTML document with CSS
$output = '<!DOCTYPE html>
--- a/master-addons/inc/admin/widget-builder/class-widget-builder-init.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-builder-init.php
@@ -26,6 +26,7 @@
private function __construct() {
add_action('init', [$this, 'initialize'], 1);
add_action('admin_init', [$this, 'admin_redirects']);
+ add_action('admin_init', [$this, 'maybe_migrate']);
}
public function initialize() {
@@ -146,4 +147,135 @@
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
+
+ /**
+ * One-time migration. Re-sanitizes stored widget code (strips any PHP/script that
+ * older versions may have persisted), purges stale generated files from current and
+ * legacy locations, and regenerates every widget from the now-clean data. Existing
+ * widget posts are never deleted — only their data is scrubbed and files rebuilt.
+ * Runs once per version (gated by an option) inside an authorised admin request.
+ */
+ public function maybe_migrate() {
+ $option_key = 'jltma_widget_builder_migrated';
+ $version = '3.1.1';
+
+ if (get_option($option_key) === $version) {
+ return;
+ }
+
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ // 1) Remove stale generated trees (current + legacy locations).
+ $upload = wp_upload_dir();
+ $base = trailingslashit($upload['basedir']);
+ $this->delete_directory($base . 'master_addons/widgets');
+ $this->delete_directory($base . 'master-addons/widget-builder/generated');
+ $this->delete_directory($base . 'master-addons/widget-builder/tmp');
+
+ // 2) Scrub persisted widget data, then regenerate files from clean data.
+ $widget_ids = get_posts([
+ 'post_type' => 'jltma_widget',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ]);
+
+ foreach ($widget_ids as $widget_id) {
+ $data = get_post_meta($widget_id, '_jltma_widget_data', true);
+ if (is_array($data)) {
+ if (isset($data['html_code'])) {
+ $data['html_code'] = $this->scrub_html($data['html_code']);
+ }
+ if (isset($data['css_code'])) {
+ $data['css_code'] = $this->scrub_css($data['css_code']);
+ }
+ if (isset($data['js_code'])) {
+ $data['js_code'] = $this->scrub_js($data['js_code']);
+ }
+ update_post_meta($widget_id, '_jltma_widget_data', $data);
+ }
+
+ $generator = new Widget_Generator($widget_id);
+ $generator->generate();
+ }
+
+ // 3) Drop the orphaned option left by the old option-based builder.
+ delete_option('jltma_custom_widgets');
+
+ update_option($option_key, $version);
+ }
+
+ /**
+ * Strip PHP tags and inline <script> from widget HTML.
+ *
+ * @param string $code
+ * @return string
+ */
+ private function scrub_html($code) {
+ if (!is_string($code) || '' === $code) {
+ return '';
+ }
+ $code = str_replace(chr(0), '', $code);
+ $code = preg_replace('/<?php/i', '', $code);
+ $code = str_replace(['<?=', '<?', '?>'], '', $code);
+ $code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $code);
+ $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+ return $code;
+ }
+
+ /**
+ * Strip PHP tags and <style>/<script> tags from widget CSS.
+ *
+ * @param string $code
+ * @return string
+ */
+ private function scrub_css($code) {
+ if (!is_string($code) || '' === $code) {
+ return '';
+ }
+ $code = str_replace(chr(0), '', $code);
+ $code = preg_replace('/<?php/i', '', $code);
+ $code = str_replace(['<?=', '<?', '?>'], '', $code);
+ $code = preg_replace('#</?styleb[^>]*>#i', '', $code);
+ $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+ return $code;
+ }
+
+ /**
+ * Strip PHP tags and <script> tags from widget JS.
+ *
+ * @param string $code
+ * @return string
+ */
+ private function scrub_js($code) {
+ if (!is_string($code) || '' === $code) {
+ return '';
+ }
+ $code = str_replace(chr(0), '', $code);
+ $code = preg_replace('/<?php/i', '', $code);
+ $code = str_replace(['<?=', '<?', '?>'], '', $code);
+ $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+ return $code;
+ }
+
+ /**
+ * Recursively delete a directory via WP_Filesystem.
+ *
+ * @param string $dir
+ */
+ private function delete_directory($dir) {
+ if (empty($dir) || !is_dir($dir)) {
+ return;
+ }
+ global $wp_filesystem;
+ if (empty($wp_filesystem)) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ WP_Filesystem();
+ }
+ if (!empty($wp_filesystem)) {
+ $wp_filesystem->delete($dir, true);
+ }
+ }
}
--- a/master-addons/inc/admin/widget-builder/class-widget-generator.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-generator.php
@@ -154,12 +154,43 @@
*/
private function create_directory() {
if (!file_exists($this->widget_dir)) {
- return wp_mkdir_p($this->widget_dir);
+ if (!wp_mkdir_p($this->widget_dir)) {
+ return false;
+ }
}
+
+ // Defense in depth: drop a silence index.php into the base and per-widget
+ // directories so generated files cannot be listed/browsed directly. Generated
+ // PHP is plugin-authored and ABSPATH-guarded, so it is inert over the web.
+ $this->harden_directory($this->upload_dir);
+ $this->harden_directory($this->widget_dir);
+
return true;
}
/**
+ * Write a silence index.php into a generated directory.
+ *
+ * @param string $dir
+ */
+ private function harden_directory($dir) {
+ if (empty($dir)) {
+ return;
+ }
+
+ global $wp_filesystem;
+ if (empty($wp_filesystem)) {
+ require_once(ABSPATH . '/wp-admin/includes/file.php');
+ WP_Filesystem();
+ }
+
+ $index = trailingslashit($dir) . 'index.php';
+ if (!file_exists($index)) {
+ $wp_filesystem->put_contents($index, "<?phpn// Silence is golden.n", FS_CHMOD_FILE);
+ }
+ }
+
+ /**
* Generate PHP widget file
*
* @return bool|WP_Error
@@ -397,6 +428,8 @@
*/
private function build_get_icon() {
$icon = !empty($this->widget_data['icon']) ? $this->widget_data['icon'] : 'eicon-code';
+ // Escape for safe interpolation into a single-quoted PHP string literal.
+ $icon = addslashes(sanitize_text_field($icon));
$content = "tpublic function get_icon() {n";
$content .= "ttreturn '{$icon}';n";
@@ -412,6 +445,8 @@
*/
private function build_get_categories() {
$category = !empty($this->widget_data['category']) ? $this->widget_data['category'] : 'master-addons';
+ // Escape for safe interpolation into a single-quoted PHP string literal.
+ $category = addslashes(sanitize_text_field($category));
$content = "tpublic function get_categories() {n";
$content .= "ttreturn ['{$category}'];n";
@@ -1020,7 +1055,7 @@
// For simple variable references, wrap in echo with appropriate escaping
if (in_array(strtolower($control_type), ['wysiwyg', 'code'])) {
- return '<' . '?php echo ' . $var_ref . '; ?' . '>';
+ return '<' . '?php echo wp_kses_post(' . $var_ref . '); ?' . '>';
} else {
return '<' . '?php echo esc_html(' . $var_ref . '); ?' . '>';
}
@@ -1251,12 +1286,13 @@
return '';
}
- // Ensure the HTML doesn't contain closing PHP tags that would break the context
- // This is a security measure to prevent breaking out of the render method
- // We replace any standalone PHP tag boundaries
- $html = str_replace('?' . '><' . '?php', '<!-- php-boundary -->', $html);
-
- // Ensure the html does not contain {{ }} template string
+ // Security: strip every PHP open/close tag so user-supplied markup can never
+ // execute as PHP once written into the generated widget file. Generated files
+ // contain only plugin-authored PHP; user HTML is treated as inert markup whose
+ // {{placeholders}} are converted to escaped echo statements elsewhere.
+ $html = str_replace(chr(0), '', $html);
+ $html = preg_replace('/<?php/i', '', $html);
+ $html = str_replace(array('<?=', '<?', '?>'), '', $html);
return $html;
}
@@ -1419,9 +1455,9 @@
$css = trim($css);
- // Remove any PHP tags
- $css = preg_replace('/<?php.*??>/s', '', $css);
- $css = preg_replace('/<?.*??>/s', '', $css);
+ // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
+ $css = preg_replace('/<?php/i', '', $css);
+ $css = str_replace(array('<?=', '<?', '?>'), '', $css);
// Remove any HTML script tags
$css = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $css);
@@ -1452,16 +1488,17 @@
$js = trim($js);
- // Remove any PHP tags
- $js = preg_replace('/<?php.*??>/s', '', $js);
- $js = preg_replace('/<?.*??>/s', '', $js);
+ // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
+ $js = preg_replace('/<?php/i', '', $js);
+ $js = str_replace(array('<?=', '<?', '?>'), '', $js);
+
+ // Strip <script> tags so the value cannot break out of the enqueued/inline
+ // <script> context. JS string literals should not contain literal script tags.
+ $js = preg_replace('#</?scriptb[^>]*>#i', '', $js);
// Remove null bytes
$js = str_replace(chr(0), '', $js);
- // Note: We don't strip HTML tags from JS as they might be part of string literals
- // The responsibility is on the admin user to write secure code
-
return $js;
}
--- a/master-addons/inc/admin/widget-builder/widget-builder.php
+++ b/master-addons/inc/admin/widget-builder/widget-builder.php
@@ -1,714 +0,0 @@
-<?php
-
-namespace MasterAddonsIncAdminWidgetBuilder;
-
-if (!defined('ABSPATH')) {
- exit; // Exit if accessed directly.
-}
-
-use MasterAddonsIncClassesExtension_Prototype;
-
-/**
- * Master Addons Widget Builder
- * No-code custom widget creation for Elementor
- */
-
-class Widget_Builder extends Extension_Prototype {
-
- public static $instance = null;
-
- public function __construct() {
- // NOTE: admin_menu is now handled by JLTMA_Widget_Admin class
- // add_action('admin_menu', array($this, 'add_widget_builder_menu'));
-
- // NOTE: admin_enqueue_scripts is now handled by JLTMA_Widget_Admin class
- // add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
-
- add_action('wp_ajax_jltma_save_custom_widget', array($this, 'save_custom_widget'));
- add_action('wp_ajax_jltma_delete_custom_widget', array($this, 'delete_custom_widget'));
- add_action('wp_ajax_jltma_get_custom_widgets', array($this, 'get_custom_widgets'));
- add_action('wp_ajax_jltma_get_widget_fields', array($this, 'get_widget_fields'));
- add_action('elementor/widgets/widgets_registered', array($this, 'register_custom_widgets'));
- add_action('wp_enqueue_scripts', array($this, 'enqueue_widget_styles'));
- }
-
- /**
- * Add Widget Builder submenu under Master Addons
- * NOTE: Menu is now handled by JLTMA_Widget_Admin class
- */
- public function add_widget_builder_menu() {
- // Menu registration moved to JLTMA_Widget_Admin class
- // This method kept for backward compatibility but does nothing
- }
-
- /**
- * Enqueue admin scripts and styles
- */
- public function admin_enqueue_scripts($hook) {
- // Only load on Widget Builder page
- if ($hook !== 'master-addons_page_jltma-widget-builder') {
- return;
- }
-
- // Enqueue Widget Builder React app
- wp_enqueue_script(
- 'jltma-widget-builder-app',
- JLTMA_URL . '/assets/js/admin/widget-builder-app.js',
- array('wp-element', 'wp-i18n'),
- JLTMA_VER,
- true
- );
-
- // Enqueue Widget Builder styles
- wp_enqueue_style(
- 'jltma-widget-builder',
- JLTMA_URL . '/assets/css/admin/widget-builder.css',
- array(),
- JLTMA_VER
- );
-
- // Localize script data
- $localize_data = array(
- 'ajaxurl' => admin_url('admin-ajax.php'),
- 'nonce' => wp_create_nonce('jltma_widget_builder_nonce'),
- 'pluginUrl' => JLTMA_URL,
- 'strings' => array(
- 'widgetBuilder' => __('Widget Builder', 'master-addons'),
- 'createWidget' => __('Create New Widget', 'master-addons'),
- 'editWidget' => __('Edit Widget', 'master-addons'),
- 'deleteWidget' => __('Delete Widget', 'master-addons'),
- 'saveWidget' => __('Save Widget', 'master-addons'),
- 'widgetName' => __('Widget Name', 'master-addons'),
- 'widgetTitle' => __('Widget Title', 'master-addons'),
- 'widgetIcon' => __('Widget Icon', 'master-addons'),
- 'widgetCategory' => __('Widget Category', 'master-addons'),
- 'addField' => __('Add Field', 'master-addons'),
- 'fieldType' => __('Field Type', 'master-addons'),
- 'fieldLabel' => __('Field Label', 'master-addons'),
- 'fieldName' => __('Field Name', 'master-addons'),
- 'preview' => __('Preview', 'master-addons'),
- 'livePreview' => __('Live Preview', 'master-addons'),
- 'widgetCode' => __('Widget Code', 'master-addons'),
- 'exportWidget' => __('Export Widget', 'master-addons'),
- 'importWidget' => __('Import Widget', 'master-addons'),
- 'noCodeBuilder' => __('No-Code Widget Builder', 'master-addons'),
- 'dragDropInterface' => __('Drag & Drop Interface', 'master-addons'),
- 'unlimitedWidgets' => __('Create Unlimited Custom Widgets', 'master-addons'),
- 'searchWidgets' => __('Search widgets...', 'master-addons'),
- 'filterByCategory' => __('Filter by Category', 'master-addons'),
- 'allCategories' => __('All Categories', 'master-addons'),
- 'basic' => __('Basic', 'master-addons'),
- 'advanced' => __('Advanced', 'master-addons'),
- 'ecommerce' => __('eCommerce', 'master-addons'),
- 'forms' => __('Forms', 'master-addons'),
- 'media' => __('Media', 'master-addons'),
- 'fieldTypes' => array(
- 'text' => __('Text Input', 'master-addons'),
- 'textarea' => __('Textarea', 'master-addons'),
- 'wysiwyg' => __('WYSIWYG Editor', 'master-addons'),
- 'select' => __('Select Dropdown', 'master-addons'),
- 'choose' => __('Choose Control', 'master-addons'),
- 'color' => __('Color Picker', 'master-addons'),
- 'media' => __('Media Upload', 'master-addons'),
- 'gallery' => __('Gallery', 'master-addons'),
- 'icon' => __('Icon Picker', 'master-addons'),
- 'url' => __('URL Input', 'master-addons'),
- 'number' => __('Number Input', 'master-addons'),
- 'slider' => __('Range Slider', 'master-addons'),
- 'date_time' => __('Date Time Picker', 'master-addons'),
- 'switcher' => __('Switcher Toggle', 'master-addons'),
- 'border' => __('Border Control', 'master-addons'),
- 'typography' => __('Typography', 'master-addons'),
- 'background' => __('Background', 'master-addons'),
- 'box_shadow' => __('Box Shadow', 'master-addons'),
- 'text_shadow' => __('Text Shadow', 'master-addons'),
- 'repeater' => __('Repeater Fields', 'master-addons')
- )
- ),
- );
-
- wp_localize_script('jltma-widget-builder-app', 'JLTMAWidgetBuilder', $localize_data);
- }
-
- /**
- * Render Widget Builder page
- * NOTE: This is now handled by JLTMA_Widget_Admin class
- * When widget_id is present, it loads React editor
- * When no widget_id, it shows list table (handled by JLTMA_Widget_Admin)
- */
- public function widget_builder_page() {
- // Check if we're editing a specific widget
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query var that selects which widget the admin page renders; not form processing.
- $widget_id = isset($_GET['widget_id']) ? intval($_GET['widget_id']) : 0;
-
- if ($widget_id) {
- // Render the Widget Builder React app editor
- ?>
- <div class="wrap jltma-widget-builder-wrap">
- <div id="jltma-widget-builder-app"></div>
- </div>
- <?php
- } else {
- // List table is handled by JLTMA_Widget_Admin class
- // This ensures backward compatibility
- ?>
- <div class="wrap jltma-widget-builder-wrap">
- <div id="jltma-widget-builder-app"></div>
- </div>
- <?php
- }
- }
-
-
- /**
- * AJAX: Save custom widget
- */
- public function save_custom_widget() {
- check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
- if (!current_user_can('manage_options')) {
- wp_send_json_error(__('Insufficient permissions', 'master-addons'));
- return;
- }
-
- $widget_data = isset($_POST['widget_data']) ? json_decode( wp_unslash( $_POST['widget_data'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded and sanitized via sanitize_widget_data() below
-
- if (empty($widget_data['name']) || empty($widget_data['title'])) {
- wp_send_json_error(__('Widget name and title are required', 'master-addons'));
- return;
- }
-
- // Sanitize widget data
- $widget_data = $this->sanitize_widget_data($widget_data);
-
- // Validate widget name format
- if (!preg_match('/^[a-z0-9_-]+$/', $widget_data['name'])) {
- wp_send_json_error(__('Widget name must contain only lowercase letters, numbers, hyphens and underscores', 'master-addons'));
- return;
- }
-
- // Save to database
- $widgets = get_option('jltma_custom_widgets', array());
- $widget_id = isset($widget_data['id']) ? $widget_data['id'] : uniqid('jltma_widget_');
- $widget_data['id'] = $widget_id;
- $widgets[$widget_id] = $widget_data;
-
- if (update_option('jltma_custom_widgets', $widgets)) {
- // Generate widget class file
- if ($this->generate_widget_class($widget_data)) {
- wp_send_json_success(array(
- 'message' => __('Widget saved successfully', 'master-addons'),
- 'widget_id' => $widget_id
- ));
- } else {
- wp_send_json_error(__('Widget saved but failed to generate class file', 'master-addons'));
- }
- } else {
- wp_send_json_error(__('Failed to save widget', 'master-addons'));
- }
- }
-
- /**
- * AJAX: Delete custom widget
- */
- public function delete_custom_widget() {
- check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
- if (!current_user_can('manage_options')) {
- wp_send_json_error(__('Insufficient permissions', 'master-addons'));
- }
-
- $widget_id = isset( $_POST['widget_id'] ) ? sanitize_text_field( wp_unslash( $_POST['widget_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above via check_ajax_referer
- $widgets = get_option('jltma_custom_widgets', array());
-
- if (isset($widgets[$widget_id])) {
- unset($widgets[$widget_id]);
- update_option('jltma_custom_widgets', $widgets);
-
- // Delete widget class file
- $this->delete_widget_class($widget_id);
-
- wp_send_json_success(__('Widget deleted successfully', 'master-addons'));
- } else {
- wp_send_json_error(__('Widget not found', 'master-addons'));
- }
- }
-
- /**
- * AJAX: Get custom widgets
- */
- public function get_custom_widgets() {
- check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
- $widgets = get_option('jltma_custom_widgets', array());
- wp_send_json_success($widgets);
- }
-
- /**
- * AJAX: Get widget field types
- */
- public function get_widget_fields() {
- check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
- wp_send_json_success($this->get_available_field_types());
- }
-
- /**
- * Register custom widgets with Elementor
- */
- public function register_custom_widgets() {
- if (!class_exists('\Elementor\Plugin')) {
- return;
- }
-
- // Register custom categories first
- $this->register_custom_categories();
-
- $widgets = get_option('jltma_custom_widgets', array());
-
-
- if (empty($widgets) || !is_array($widgets)) {
- return;
- }
-
- foreach ($widgets as $widget_id => $widget_data) {
- if (is_array($widget_data) && !empty($widget_data['name'])) {
- $this->register_single_widget($widget_id, $widget_data);
- }
- }
- }
-
- /**
- * Register custom Elementor categories
- */
- private function register_custom_categories() {
- if (!class_exists('\Elementor\Plugin')) {
- return;
- }
-
- // Get custom categories from options
- $custom_categories = get_option('jltma_custom_widget_categories', array());
-
- if (empty($custom_categories) || !is_array($custom_categories)) {
- return;
- }
-
- $elements_manager = ElementorPlugin::$instance->elements_manager;
-
- // Register each custom category
- foreach ($custom_categories as $slug => $title) {
- // Check if category doesn't already exist
- $existing_categories = $elements_manager->get_categories();
-
- if (!isset($existing_categories[$slug])) {
- $elements_manager->add_category(
- $slug,
- array(
- 'title' => $title,
- 'icon' => 'eicon-posts-ticker',
- )
- );
- }
- }
- }
-
- /**
-