post_id = absint($post_id);
$this->widget_slug = $this->name_prefix . $this->post_id;
$this->widget_class = $this->class_prefix . $this->post_id;
$this->init_directories();
$this->load_widget_data();
// Initialize control manager
require_once __DIR__ . '/class-control-manager.php';
$this->control_manager = Control_Manager::get_instance();
}
/**
* Initialize upload directories
*/
private function init_directories() {
$upload = wp_upload_dir();
$this->upload_dir = $upload['basedir'] . '/master_addons/widgets';
$this->upload_url = $upload['baseurl'] . '/master_addons/widgets';
$this->widget_dir = $this->upload_dir . '/' . $this->post_id;
$this->widget_url = $this->upload_url . '/' . $this->post_id;
}
/**
* Load widget data from post meta
*/
private function load_widget_data() {
$this->widget_data = get_post_meta($this->post_id, '_jltma_widget_data', true);
if (empty($this->widget_data)) {
$this->widget_data = $this->get_default_data();
}
// Load sections data separately
$sections = get_post_meta($this->post_id, '_jltma_widget_sections', true);
if (!empty($sections) && is_array($sections)) {
$this->widget_data['sections'] = $sections;
} else {
$this->widget_data['sections'] = [];
}
// Load includes data separately (CSS/JS libraries)
$includes = get_post_meta($this->post_id, '_jltma_widget_includes', true);
if (!empty($includes) && is_array($includes)) {
$this->widget_data['includes'] = $includes;
} else {
$this->widget_data['includes'] = [
'css_libraries' => [],
'js_libraries' => []
];
}
}
/**
* Get default widget data structure
*
* @return array
*/
private function get_default_data() {
return [
'title' => get_the_title($this->post_id),
'icon' => 'eicon-code',
'category' => get_post_meta($this->post_id, '_jltma_widget_category', true) ?: 'master-addons',
'sections' => [],
'html_code' => '',
'css_code' => '',
'js_code' => ''
];
}
/**
* Generate all widget files
*
* @return bool|WP_Error
*/
public function generate() {
// No-op. Widgets are now rendered at runtime by Dynamic_Widget directly
// from post meta — no PHP/CSS/JS files are written under uploads and no
// user-derived PHP is ever executed. Retained as a no-op so existing call
// sites (REST save, admin save, migration) stay valid. The build_* methods
// below remain for reference/tests only.
return true;
}
/**
* Create widget directory
*
* @return bool
*/
private function create_directory() {
if (!file_exists($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, "build_php_content();
$file_path = $this->widget_dir . '/widget.php';
// Use WP_Filesystem
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once(ABSPATH . '/wp-admin/includes/file.php');
WP_Filesystem();
}
$result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
if (!$result) {
return new \WP_Error('file_write_failed', 'Failed to write widget.php file');
}
return true;
}
/**
* Build PHP widget file content
*
* @return string
*/
private function build_php_content() {
$content = "widget_data['title']) . "\n";
$content .= " * Generated by Master Addons Widget Builder\n";
$content .= " * Widget ID: " . $this->post_id . "\n";
$content .= " */\n";
$content .= "class " . $this->widget_class . " extends Master_Widget {\n\n";
// Constructor if CSS/JS files exist
if (!empty($this->widget_data['css_code']) || !empty($this->widget_data['js_code'])) {
$content .= $this->build_constructor();
}
// Widget name
$content .= $this->build_get_name();
// Widget title
$content .= $this->build_get_title();
// Widget icon
$content .= $this->build_get_icon();
// Widget categories
$content .= $this->build_get_categories();
// Register controls
$content .= $this->build_register_controls();
// Add tabs data helper method if there are tab controls
$tabs_helper = $this->build_get_tabs_data_helper();
if (!empty($tabs_helper)) {
$content .= $tabs_helper;
}
// Render method
$content .= $this->build_render();
$content .= "}\n";
return $content;
}
/**
* Build constructor method
*
* @return string
*/
private function build_constructor() {
$handle = 'jltma-wb-' . $this->post_id;
$content = "\tpublic function __construct(\$data = [], \$args = null) {\n";
$content .= "\t\tparent::__construct(\$data, \$args);\n\n";
// Register external CSS libraries
if (!empty($this->widget_data['includes']['css_libraries'])) {
foreach ($this->widget_data['includes']['css_libraries'] as $css_lib) {
if (!empty($css_lib['handle']) && !empty($css_lib['src'])) {
$deps = !empty($css_lib['dependencies']) && is_array($css_lib['dependencies'])
? $css_lib['dependencies']
: [];
$deps_string = $this->build_deps_array($deps);
// Only register if src is a URL (external library)
if (filter_var($css_lib['src'], FILTER_VALIDATE_URL)) {
$content .= "\t\twp_register_style('{$css_lib['handle']}', '{$css_lib['src']}', {$deps_string}, '1.0.0');\n";
}
}
}
}
// Register external JS libraries
if (!empty($this->widget_data['includes']['js_libraries'])) {
foreach ($this->widget_data['includes']['js_libraries'] as $js_lib) {
if (!empty($js_lib['handle']) && !empty($js_lib['src'])) {
$deps = !empty($js_lib['dependencies']) && is_array($js_lib['dependencies'])
? $js_lib['dependencies']
: [];
$deps_string = $this->build_deps_array($deps);
// Only register if src is a URL (external library)
if (filter_var($js_lib['src'], FILTER_VALIDATE_URL)) {
$content .= "\t\twp_register_script('{$js_lib['handle']}', '{$js_lib['src']}', {$deps_string}, '1.0.0', true);\n";
}
}
}
}
// Register widget's own CSS
if (!empty($this->widget_data['css_code'])) {
$content .= "\t\twp_register_style('{$handle}-style', '{$this->widget_url}/style.css', [], '1.0.0');\n";
}
// Register widget's own JS
if (!empty($this->widget_data['js_code'])) {
$content .= "\t\twp_register_script('{$handle}-script', '{$this->widget_url}/script.js', ['elementor-frontend'], '1.0.0', true);\n";
}
$content .= "\t}\n\n";
// Build get_style_depends method
$style_deps = [];
// Add external CSS library handles
if (!empty($this->widget_data['includes']['css_libraries'])) {
foreach ($this->widget_data['includes']['css_libraries'] as $css_lib) {
if (!empty($css_lib['handle'])) {
$style_deps[] = $css_lib['handle'];
}
}
}
// Add widget's own CSS
if (!empty($this->widget_data['css_code'])) {
$style_deps[] = "{$handle}-style";
}
if (!empty($style_deps)) {
$content .= "\tpublic function get_style_depends() {\n";
$content .= "\t\treturn " . $this->build_deps_array($style_deps) . ";\n";
$content .= "\t}\n\n";
}
// Build get_script_depends method
$script_deps = [];
// Add external JS library handles
if (!empty($this->widget_data['includes']['js_libraries'])) {
foreach ($this->widget_data['includes']['js_libraries'] as $js_lib) {
if (!empty($js_lib['handle'])) {
$script_deps[] = $js_lib['handle'];
}
}
}
// Add widget's own JS
if (!empty($this->widget_data['js_code'])) {
$script_deps[] = "{$handle}-script";
}
if (!empty($script_deps)) {
$content .= "\tpublic function get_script_depends() {\n";
$content .= "\t\treturn " . $this->build_deps_array($script_deps) . ";\n";
$content .= "\t}\n\n";
}
return $content;
}
/**
* Build dependencies array string for PHP code
*
* @param array $deps
* @return string
*/
private function build_deps_array($deps) {
if (empty($deps)) {
return '[]';
}
$quoted_deps = array_map(function($dep) {
return "'" . esc_attr($dep) . "'";
}, $deps);
return '[' . implode(', ', $quoted_deps) . ']';
}
/**
* Build get_name method
*
* @return string
*/
private function build_get_name() {
$content = "\tpublic function get_name() {\n";
$content .= "\t\treturn '{$this->widget_slug}';\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Build get_title method
*
* @return string
*/
private function build_get_title() {
$title = !empty($this->widget_data['title']) ? esc_html($this->widget_data['title']) : 'Custom Widget';
$content = "\tpublic function get_title() {\n";
$content .= "\t\treturn esc_html__('{$title}', 'master-addons');\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Build get_icon method
*
* @return string
*/
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 .= "\t\treturn '{$icon}';\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Build get_categories method
*
* @return string
*/
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 .= "\t\treturn ['{$category}'];\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Build register_controls method
*
* @return string
*/
private function build_register_controls() {
$content = "\tprotected function register_controls() {\n";
if (!empty($this->widget_data['sections']) && is_array($this->widget_data['sections'])) {
// Sort sections by tab order: content, style, advanced
$sorted_sections = $this->sort_sections_by_tab($this->widget_data['sections']);
foreach ($sorted_sections as $section_id => $section) {
// Ensure section has proper structure
if (is_array($section)) {
$content .= $this->build_section($section_id, $section);
}
}
}
$content .= "\t}\n\n";
return $content;
}
/**
* Sort sections by tab order: content, style, advanced
*
* @param array $sections
* @return array
*/
private function sort_sections_by_tab($sections) {
$content_sections = [];
$style_sections = [];
$advanced_sections = [];
foreach ($sections as $section_id => $section) {
if (!is_array($section)) {
continue;
}
$tab = !empty($section['tab']) ? $section['tab'] : 'content';
if ($tab === 'style') {
$style_sections[$section_id] = $section;
} elseif ($tab === 'advanced') {
$advanced_sections[$section_id] = $section;
} else {
$content_sections[$section_id] = $section;
}
}
// Merge in correct order: content, style, advanced
return array_merge($content_sections, $style_sections, $advanced_sections);
}
/**
* Build a control section
*
* @param string $section_id
* @param array $section
* @return string
*/
private function build_section($section_id, $section) {
// Try 'title' first (used by Widget Builder), fall back to 'label', then default to 'Section'
$label = !empty($section['title']) ? esc_html($section['title']) : (!empty($section['label']) ? esc_html($section['label']) : 'Section');
$tab = !empty($section['tab']) ? $section['tab'] : 'content';
// Generate section key with proper prefix based on tab
$tab_prefix = '';
$tab_const = 'Controls_Manager::TAB_CONTENT';
if ($tab === 'style') {
$tab_prefix = 'jltma_style_';
$tab_const = 'Controls_Manager::TAB_STYLE';
} elseif ($tab === 'advanced') {
$tab_prefix = 'jltma_advanced_';
$tab_const = 'Controls_Manager::TAB_ADVANCED';
} else {
$tab_prefix = 'jltma_content_';
}
// Create sanitized section slug from label and add post ID
$section_slug = $this->sanitize_key($label);
$section_key = $tab_prefix . $section_slug . '_' . $section_id. '_' . $this->post_id;
$content = "\n\t\t\$this->start_controls_section(\n";
$content .= "\t\t\t'{$section_key}',\n";
$content .= "\t\t\t[\n";
$content .= "\t\t\t\t'label' => esc_html__('{$label}', 'master-addons'),\n";
$content .= "\t\t\t\t'tab' => {$tab_const},\n";
$content .= "\t\t\t]\n";
$content .= "\t\t);\n\n";
// Add controls (check both 'fields' and 'controls' for backwards compatibility)
$controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
if (!empty($controls) && is_array($controls)) {
foreach ($controls as $field_id => $field) {
$content .= $this->build_control($field_id, $field, $tab);
}
}
$content .= "\t\t\$this->end_controls_section();\n";
return $content;
}
/**
* Build a control
*
* @param string $field_id
* @param array $field
* @param string $tab Current tab (content/style/advanced)
* @return string
*/
private function build_control($field_id, $field, $tab = 'content') {
$label = !empty($field['label']) ? esc_html($field['label']) : 'Control';
$type = !empty($field['type']) ? $field['type'] : 'TEXT';
// Special handling for TABS control - it's a structural element, not a regular control
// TABS control requires the full field data with 'tabs' array and should use field['name'] as key
if (strtoupper($type) === 'TABS') {
// For TABS, use the control name directly as the key (not label-based)
$control_name = !empty($field['name']) ? $field['name'] : 'tabs_' . $field_id;
// Pass tab and widget_id context
$field['_tab'] = $tab;
$field['_widget_id'] = $this->post_id;
// Pass used control keys reference for global uniqueness tracking
$field['_used_control_keys'] = &$this->used_control_keys;
// Call TABS control builder directly with the name as key
return $this->control_manager->build_control($control_name, $field, $type);
}
// Generate control key with proper prefix based on tab
$tab_prefix = '';
if ($tab === 'style') {
$tab_prefix = 'jltma_style_';
} elseif ($tab === 'advanced') {
$tab_prefix = 'jltma_advanced_';
} else {
$tab_prefix = 'jltma_content_';
}
// Create sanitized control slug from label and add post ID
$control_slug = $this->sanitize_key($label);
$base_key = $tab_prefix . $control_slug . '_' . $this->post_id;
// Ensure unique control key - append counter if duplicate
$control_key = $base_key;
$counter = 1;
while (in_array($control_key, $this->used_control_keys)) {
$control_key = $tab_prefix . $control_slug . '_' . $counter . '_' . $this->post_id;
$counter++;
}
$this->used_control_keys[] = $control_key;
// Pass tab and widget_id context to control manager for condition key conversion
$field['_tab'] = $tab;
$field['_widget_id'] = $this->post_id;
$field['_tab_prefix'] = $tab_prefix;
$field['_sections_data'] = $this->widget_data['sections'] ?? [];
// Preprocess date_time controls - convert UI settings to picker_options
if ($type === 'date_time') {
$picker_options = [];
// Convert enable_time to picker_options
$enable_time = isset($field['enable_time']) ? (bool) $field['enable_time'] : false;
if (isset($field['enable_time'])) {
$picker_options['enableTime'] = $enable_time;
}
// Set dateFormat based on enableTime
$picker_options['dateFormat'] = $enable_time ? 'Y-m-d H:i' : 'Y-m-d';
// Always use 24-hour format
$picker_options['time_24hr'] = true;
// Convert minute_increment to picker_options
if (isset($field['minute_increment']) && !empty($field['minute_increment'])) {
$picker_options['minuteIncrement'] = intval($field['minute_increment']);
}
// Merge with existing picker_options if any
if (!empty($field['picker_options']) && is_array($field['picker_options'])) {
$picker_options = array_merge($field['picker_options'], $picker_options);
}
// Set picker_options
if (!empty($picker_options)) {
$field['picker_options'] = $picker_options;
}
}
// Use Control Manager to build control
return $this->control_manager->build_control($control_key, $field, $type);
}
/**
* Get tab data structures from widget sections
* Returns array of tab control names with their field mappings
*
* @return array ['tab_control_name' => ['name' => 'tab_name', 'tabs' => [...]]]
*/
private function get_tab_structures() {
$tab_structures = [];
if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
return $tab_structures;
}
foreach ($this->widget_data['sections'] as $section) {
if (!is_array($section) || empty($section['controls'])) {
continue;
}
foreach ($section['controls'] as $control) {
if (empty($control['type']) || strtoupper($control['type']) !== 'TABS') {
continue;
}
if (empty($control['name'])) {
continue;
}
// Get tab info - check for both 'tabs' array (processed) and 'fields'+'tab_fields' (raw from UI)
$tabs = [];
if (!empty($control['tabs']) && is_array($control['tabs'])) {
// Already processed tabs structure
$tabs = $control['tabs'];
} elseif (!empty($control['fields']) && is_array($control['fields'])) {
// Raw structure from UI - build tabs array
$tab_fields = !empty($control['tab_fields']) && is_array($control['tab_fields']) ? $control['tab_fields'] : [];
foreach ($control['fields'] as $tab_definition) {
if (empty($tab_definition['name'])) {
continue;
}
$tab_name = $tab_definition['name'];
$tab = [
'name' => $tab_name,
'label' => !empty($tab_definition['label']) ? $tab_definition['label'] : ucfirst($tab_name),
'controls' => []
];
// Add controls for this tab if they exist
if (!empty($tab_fields[$tab_name]) && is_array($tab_fields[$tab_name])) {
$tab['controls'] = $tab_fields[$tab_name];
}
$tabs[] = $tab;
}
}
if (!empty($tabs)) {
$tab_control_name = $control['name'];
$tab_structures[$tab_control_name] = [
'name' => $tab_control_name,
'tabs' => $tabs
];
}
}
}
return $tab_structures;
}
/**
* Build get_tabs_data helper method
* Creates a helper method that organizes tab control data into accessible array structure
*
* @return string
*/
private function build_get_tabs_data_helper() {
$tab_structures = $this->get_tab_structures();
if (empty($tab_structures)) {
return '';
}
$content = "\t/**\n";
$content .= "\t * Get tabs data organized by tab control\n";
$content .= "\t * Helper method to access tab data as arrays\n";
$content .= "\t *\n";
$content .= "\t * @param array \$settings Widget settings\n";
$content .= "\t * @return array Organized tab data\n";
$content .= "\t */\n";
$content .= "\tprivate function get_tabs_data(\$settings) {\n";
$content .= "\t\t\$tabs_data = [];\n\n";
// Build data structure for each tab control
foreach ($tab_structures as $tab_control_name => $tab_info) {
$content .= "\t\t// Tab control: {$tab_control_name}\n";
$content .= "\t\t\$tabs_data['{$tab_control_name}'] = [\n";
$content .= "\t\t\t'tabs' => [],\n";
$content .= "\t\t];\n\n";
foreach ($tab_info['tabs'] as $tab_index => $tab) {
$tab_name = $tab['name'];
$tab_label = $tab['label'] ?? ucfirst($tab_name);
$content .= "\t\t// Tab: {$tab_label}\n";
$content .= "\t\t\$tabs_data['{$tab_control_name}']['tabs']['{$tab_name}'] = [\n";
$content .= "\t\t\t'name' => '{$tab_name}',\n";
$content .= "\t\t\t'label' => '{$tab_label}',\n";
$content .= "\t\t\t'content' => [],\n";
$content .= "\t\t];\n\n";
// Map controls from this tab
if (!empty($tab['controls']) && is_array($tab['controls'])) {
foreach ($tab['controls'] as $control) {
if (empty($control['name']) || empty($control['label'])) {
continue;
}
// Get the actual control key in settings
$control_label = $control['label'];
$control_slug = $this->sanitize_key($control_label);
// Get tab context from widget data
$tab_context = $this->get_tab_context_for_control($control['name']);
$tab_prefix = $this->get_tab_prefix($tab_context);
$control_key = $tab_prefix . $control_slug . '_' . $this->post_id;
$control_name = $control['name'];
$content .= "\t\t\$tabs_data['{$tab_control_name}']['tabs']['{$tab_name}']['content']['{$control_name}'] = \$settings['{$control_key}'] ?? '';\n";
}
}
$content .= "\n";
}
}
$content .= "\t\treturn \$tabs_data;\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Get tab context (content/style/advanced) for a control
*
* @param string $control_name Control name to search for
* @return string Tab context (content/style/advanced)
*/
private function get_tab_context_for_control($control_name) {
if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
return 'content';
}
foreach ($this->widget_data['sections'] as $section) {
if (!is_array($section) || empty($section['controls'])) {
continue;
}
foreach ($section['controls'] as $control) {
if (empty($control['type']) || strtoupper($control['type']) !== 'TABS') {
continue;
}
if (!empty($control['tabs']) && is_array($control['tabs'])) {
foreach ($control['tabs'] as $tab) {
if (!empty($tab['controls']) && is_array($tab['controls'])) {
foreach ($tab['controls'] as $tab_control) {
if (isset($tab_control['name']) && $tab_control['name'] === $control_name) {
// Found the control, return the section's tab context
return $section['tab'] ?? 'content';
}
}
}
}
}
}
}
return 'content';
}
/**
* Get tab prefix based on tab context
*
* @param string $tab Tab context (content/style/advanced)
* @return string Prefix for control keys
*/
private function get_tab_prefix($tab) {
if ($tab === 'style') {
return 'jltma_style_';
} elseif ($tab === 'advanced') {
return 'jltma_advanced_';
} else {
return 'jltma_content_';
}
}
/**
* Sanitize label to create control/section key
* Converts spaces to underscores instead of hyphens
*
* @param string $label Label to sanitize
* @return string Sanitized key with underscores
*/
private function sanitize_key($label) {
// Convert to lowercase
$key = strtolower($label);
// Replace spaces with underscores
$key = str_replace(' ', '_', $key);
// Remove special characters, keeping only alphanumeric and underscores
$key = preg_replace('/[^a-z0-9_]/', '', $key);
// Remove multiple consecutive underscores
$key = preg_replace('/_+/', '_', $key);
// Trim underscores from beginning and end
$key = trim($key, '_');
return $key;
}
/**
* Build render method
*
* @return string
*/
private function build_render() {
$html = !empty($this->widget_data['html_code']) ? $this->widget_data['html_code'] : '';
$html = $this->prepare_html_for_render($html);
$content = "\tprotected function render() {\n";
$content .= "\t\t\$settings = \$this->get_settings_for_display();\n";
$content .= "\t\t\$this->render_widget_content(\$settings);\n";
$content .= "\t}\n\n";
// Add render_shortcode method for shortcode support
$content .= "\t/**\n";
$content .= "\t * Render widget as shortcode\n";
$content .= "\t * \n";
$content .= "\t * @param array \$settings Settings array from shortcode attributes\n";
$content .= "\t */\n";
$content .= "\tpublic function render_shortcode(\$settings = []) {\n";
$content .= "\t\t// Merge with defaults\n";
$content .= "\t\tif (empty(\$settings)) {\n";
$content .= "\t\t\t\$settings = \$this->get_settings_for_display();\n";
$content .= "\t\t}\n";
$content .= "\t\t\$this->render_widget_content(\$settings);\n";
$content .= "\t}\n\n";
// Add render_widget_content method - shared between Elementor and shortcode
$content .= "\t/**\n";
$content .= "\t * Render widget HTML content\n";
$content .= "\t * Shared method for both Elementor widget and shortcode output\n";
$content .= "\t * \n";
$content .= "\t * @param array \$settings Widget settings\n";
$content .= "\t */\n";
$content .= "\tprotected function render_widget_content(\$settings) {\n";
// Check if we have tab controls and add tabs data
$tab_structures = $this->get_tab_structures();
if (!empty($tab_structures)) {
$content .= "\t\t// Get organized tab data\n";
$content .= "\t\t\$tabs_data = \$this->get_tabs_data(\$settings);\n\n";
}
// Build control mapping for placeholder replacement
$control_mapping = $this->build_control_mapping();
// Get tab structures for tab data access
$tab_structures = $this->get_tab_structures();
// Replace placeholders in HTML with PHP code
// Two-pass approach:
// 1. First pass: Replace placeholders INSIDE PHP tags with just variable references
// 2. Second pass: Replace placeholders OUTSIDE PHP tags with full $param){
$control_info = $this->get_control_info($control);
$control_type = strtolower($control_info['type'] ?? 'text');
$fallback = in_array($control_type, $array_types) ? '[]' : "''";
$content .= "\t \$$control = !empty(\$settings['$param']) ? \$settings['$param'] : $fallback;\n";
}
// Pass 1: Handle placeholders inside PHP tags
$html = $this->replace_placeholders_in_php_context($html, $control_mapping, $tab_structures);
// Pass 2: Handle placeholders outside PHP tags
$html = $this->replace_placeholders_outside_php_context($html, $control_mapping, $tab_structures);
}
// Strip any leftover {{...}} placeholders that don't map to a value control
// (e.g. HEADING/DIVIDER controls store no value) so they never render
// literally on the frontend.
$html = preg_replace('/\{\{[^}]+\}\}/', '', $html);
// Process CSS code if it contains template strings
$css_code = !empty($this->widget_data['css_code']) ? $this->widget_data['css_code'] : '';
$has_css_templates = !empty($css_code) && preg_match('/\{\{[^}]+\}\}/', $css_code);
// Process JS code if it contains template strings
$js_code = !empty($this->widget_data['js_code']) ? $this->widget_data['js_code'] : '';
$has_js_templates = !empty($js_code) && preg_match('/\{\{[^}]+\}\}/', $js_code);
// Output the processed HTML
$content .= "\t\t?" . ">\n";
// Output dynamic CSS if it contains template strings
if ($has_css_templates && !empty($control_mapping)) {
$content .= "\t\t\n";
}
if (!empty($html)) {
$content .= $html . "\n";
}
// Output dynamic JS if it contains template strings
if ($has_js_templates && !empty($control_mapping)) {
$content .= "\t\t\n";
}
$content .= "\t\t<" . "?php\n";
$content .= "\t}\n\n";
return $content;
}
/**
* Replace placeholders inside PHP context
* Replaces {{placeholder}} with just variable references, no PHP tags
*
* @param string $html HTML code with placeholders
* @param array $control_mapping Mapping of control names to keys
* @param array $tab_structures Tab control structures
* @return string HTML with placeholders inside PHP replaced
*/
private function replace_placeholders_in_php_context($html, $control_mapping, $tab_structures) {
// Match PHP blocks and replace placeholders within them
$php_open = '<' . '?php';
$php_close = '?' . '>';
$pattern = '/' . preg_quote($php_open, '/') . '(.*?)' . preg_quote($php_close, '/') . '/s';
return preg_replace_callback(
$pattern,
function($matches) use ($control_mapping, $tab_structures, $php_open, $php_close) {
$php_code = $matches[1];
// Replace placeholders within this PHP block
$php_code = preg_replace_callback(
'/\{\{([^}]+)\}\}/',
function($inner_matches) use ($control_mapping, $tab_structures) {
return $this->get_variable_reference($inner_matches[1], $control_mapping, $tab_structures);
},
$php_code
);
return $php_open . $php_code . $php_close;
},
$html
);
}
/**
* Replace placeholders outside PHP context
* Replaces {{placeholder}} with full PHP echo statements
*
* @param string $html HTML code with placeholders
* @param array $control_mapping Mapping of control names to keys
* @param array $tab_structures Tab control structures
* @return string HTML with remaining placeholders replaced
*/
private function replace_placeholders_outside_php_context($html, $control_mapping, $tab_structures) {
return preg_replace_callback(
'/\{\{([^}]+)\}\}/',
function($matches) use ($control_mapping, $tab_structures) {
$placeholder = trim($matches[1]);
// Get the variable reference
$var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
// If it returned just a variable (not a full echo statement), wrap it in echo
if ($var_ref !== $matches[0] && strpos($var_ref, '<' . '?php') === false) {
// Determine appropriate escaping based on control type
$parts = explode('.', $placeholder);
$field_name = $parts[0];
$control_info = $this->get_control_info($field_name);
$control_type = $control_info['type'] ?? 'text';
// For simple variable references, wrap in echo with appropriate escaping
if (in_array(strtolower($control_type), ['wysiwyg', 'code'])) {
return '<' . '?php echo wp_kses_post(' . $var_ref . '); ?' . '>';
} else {
return '<' . '?php echo esc_html(' . $var_ref . '); ?' . '>';
}
}
return $var_ref;
},
$html
);
}
/**
* Get variable reference for a placeholder
* Returns just the PHP variable access code without PHP tags or echo
*
* @param string $placeholder Placeholder string (without {{ }})
* @param array $control_mapping Mapping of control names to keys
* @param array $tab_structures Tab control structures
* @return string Variable reference or original placeholder if not found
*/
private function get_variable_reference($placeholder, $control_mapping, $tab_structures) {
$placeholder = trim($placeholder);
// Check if this is a tab data access pattern (e.g., abc_tabs.tabs)
if (!empty($tab_structures)) {
foreach ($tab_structures as $tab_control_name => $tab_info) {
// Check for exact match: tab_name.tabs
if ($placeholder === $tab_control_name . '.tabs') {
return "\$tabs_data['{$tab_control_name}']['tabs']";
}
// Also support just the tab control name to get entire tab data
if ($placeholder === $tab_control_name) {
return "\$tabs_data['{$tab_control_name}']";
}
}
}
// Parse placeholder for nested properties (e.g., url.url, url.target, icons.value)
$parts = explode('.', $placeholder);
$field_name = $parts[0];
$property = isset($parts[1]) ? $parts[1] : null;
if (isset($control_mapping[$field_name])) {
$control_key = $control_mapping[$field_name];
$control_info = $this->get_control_info($field_name);
$control_type = $control_info['type'] ?? 'text';
// Return just the variable reference for use in PHP context
// For simple controls, return the settings value
// For complex controls with properties, return the nested array access
if ($property) {
// Handle nested properties
switch (strtolower($control_type)) {
case 'url':
case 'media':
case 'image':
case 'icons':
case 'icon':
case 'slider':
case 'dimensions':
return "\$settings['{$control_key}']['{$property}']";
default:
return "\$settings['{$control_key}']";
}
} else {
// No property, just return the setting value
return "\$settings['{$control_key}']";
}
}
// If placeholder not found in mapping, return as-is
return '{{' . $placeholder . '}}';
}
/**
* Build control mapping for placeholder replacement
* Maps control names (placeholders) to their full control keys
*
* @return array Associative array: placeholder => control_key
*/
private function build_control_mapping() {
$mapping = [];
// Check if sections exist
if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
return $mapping;
}
// Map tab names to prefixes
$tab_prefix_map = [
'content' => 'jltma_content_',
'style' => 'jltma_style_',
'advanced' => 'jltma_advanced_',
];
// Iterate through all sections
foreach ($this->widget_data['sections'] as $section_id => $section) {
if (!is_array($section)) {
continue;
}
// Get tab name (default to content)
$tab = !empty($section['tab']) ? $section['tab'] : 'content';
$tab_prefix = $tab_prefix_map[$tab] ?? 'jltma_content_';
// Check both 'controls' and 'fields' keys for backwards compatibility
$controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
// Iterate through controls
foreach ($controls as $control) {
if (empty($control['name'])) {
continue;
}
// Control name as used in HTML (placeholder)
$control_name = $control['name'];
// Full control key as used in Elementor
$control_slug = $this->sanitize_key($control['label'] ?? $control_name);
$control_key = $tab_prefix . $control_slug . '_' . $this->post_id;
// Map placeholder to control key
$mapping[$control_name] = $control_key;
// Handle popover_toggle child fields
if (!empty($control['type']) && strtoupper($control['type']) === 'POPOVER_TOGGLE') {
if (!empty($control['popover_fields']) && is_array($control['popover_fields'])) {
foreach ($control['popover_fields'] as $popover_field) {
if (empty($popover_field['name'])) {
continue;
}
// Child field name
$child_field_name = $popover_field['name'];
// Placeholder pattern: parent_name_child_name (e.g., popover_toggle_color)
$placeholder = $control_name . '_' . $child_field_name;
// Actual control key pattern: parent_control_key_child_name
// (e.g., jltma_content_popover_toggle_381_color)
$child_field_slug = $this->sanitize_key($child_field_name);
$child_control_key = $control_key . '_' . $child_field_slug;
// Map placeholder to control key
$mapping[$placeholder] = $child_control_key;
}
}
}
}
}
return $mapping;
}
/**
* Get control info by control name
* Returns control type and other metadata
*
* @param string $control_name
* @return array Control info with 'type' and other properties
*/
private function get_control_info($control_name) {
// Check if sections exist
if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
return ['type' => 'text'];
}
// Search through all sections
foreach ($this->widget_data['sections'] as $section_id => $section) {
if (!is_array($section)) {
continue;
}
// Check both 'controls' and 'fields' keys for backwards compatibility
$controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
// Search for the control by name
foreach ($controls as $control) {
if (!empty($control['name']) && $control['name'] === $control_name) {
return [
'type' => $control['type'] ?? 'text',
'label' => $control['label'] ?? '',
'default' => $control['default'] ?? '',
'responsive' => $control['responsive'] ?? false,
];
}
// Check if this is a popover_toggle child field pattern (parent_name_child_name)
if (!empty($control['type']) && strtoupper($control['type']) === 'POPOVER_TOGGLE') {
if (!empty($control['popover_fields']) && is_array($control['popover_fields'])) {
$parent_name = $control['name'];
foreach ($control['popover_fields'] as $popover_field) {
if (empty($popover_field['name'])) {
continue;
}
// Check if control_name matches pattern: parent_name_child_name
$expected_pattern = $parent_name . '_' . $popover_field['name'];
if ($control_name === $expected_pattern) {
return [
'type' => $popover_field['type'] ?? 'text',
'label' => $popover_field['label'] ?? '',
'default' => $popover_field['default'] ?? '',
'responsive' => $popover_field['responsive'] ?? false,
'parent' => $parent_name,
];
}
}
}
}
}
}
// Default if not found
return ['type' => 'text'];
}
/**
* Prepare HTML code for render method
* Ensures the HTML doesn't break PHP context
*
* @param string $html
* @return string
*/
private function prepare_html_for_render($html) {
if (empty($html)) {
return '';
}
// 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;
}
/**
* Build dynamic CSS output with template replacement
* Replaces {{placeholder}} with PHP echo statements for CSS values
*
* @param string $css_code CSS code with placeholders
* @param array $control_mapping Mapping of control names to keys
* @param array $tab_structures Tab control structures
* @return string PHP code that echoes CSS with replaced placeholders
*/
private function build_dynamic_css_output($css_code, $control_mapping, $tab_structures) {
// Split CSS by template string patterns to build echo statements
$pattern = '/\{\{([^}]+)\}\}/';
$parts = preg_split($pattern, $css_code, -1, PREG_SPLIT_DELIM_CAPTURE);
$output = "echo \"";
for ($i = 0; $i < count($parts); $i++) {
if ($i % 2 === 0) {
// This is regular CSS content (not a placeholder)
// Escape for PHP string
$escaped = str_replace('"', '\\"', $parts[$i]);
$escaped = str_replace("\n", "\\n", $escaped);
$output .= $escaped;
} else {
// This is a placeholder - close the string and add PHP code
$placeholder = trim($parts[$i]);
$var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
// Check if we got a valid replacement
if ($var_ref !== '{{' . $placeholder . '}}') {
$output .= "\" . esc_attr(" . $var_ref . ") . \"";
} else {
// Placeholder not found, keep as-is
$output .= "{{" . $placeholder . "}}";
}
}
}
$output .= "\";\n";
return $output;
}
/**
* Build dynamic JS output with template replacement
* Replaces {{placeholder}} with PHP echo statements for JS values
*
* @param string $js_code JavaScript code with placeholders
* @param array $control_mapping Mapping of control names to keys
* @param array $tab_structures Tab control structures
* @return string PHP code that echoes JS with replaced placeholders
*/
private function build_dynamic_js_output($js_code, $control_mapping, $tab_structures) {
// Split JS by template string patterns to build echo statements
$pattern = '/\{\{([^}]+)\}\}/';
$parts = preg_split($pattern, $js_code, -1, PREG_SPLIT_DELIM_CAPTURE);
$output = "echo \"";
for ($i = 0; $i < count($parts); $i++) {
if ($i % 2 === 0) {
// This is regular JS content (not a placeholder)
// Escape for PHP string
$escaped = str_replace('"', '\\"', $parts[$i]);
$escaped = str_replace("\n", "\\n", $escaped);
$output .= $escaped;
} else {
// This is a placeholder - close the string and add PHP code
$placeholder = trim($parts[$i]);
$var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
// Check if we got a valid replacement
if ($var_ref !== '{{' . $placeholder . '}}') {
$output .= "\" . esc_js(" . $var_ref . ") . \"";
} else {
// Placeholder not found, keep as-is
$output .= "{{" . $placeholder . "}}";
}
}
}
$output .= "\";\n";
return $output;
}
/**
* Generate CSS file
*
* @return bool|WP_Error
*/
private function generate_css_file() {
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once(ABSPATH . '/wp-admin/includes/file.php');
WP_Filesystem();
}
$file_path = $this->widget_dir . '/style.css';
// Sanitize and validate CSS code
$content = $this->sanitize_css_code($this->widget_data['css_code']);
// Add file header comment
$header = "/**\n * Widget Styles\n * Generated by Master Addons Widget Builder\n * Widget ID: {$this->post_id}\n */\n\n";
$content = $header . $content;
$result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
if (!$result) {
return new \WP_Error('css_write_failed', 'Failed to write style.css file');
}
return true;
}
/**
* Generate JS file
*
* @return bool|WP_Error
*/
private function generate_js_file() {
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once(ABSPATH . '/wp-admin/includes/file.php');
WP_Filesystem();
}
$file_path = $this->widget_dir . '/script.js';
// Sanitize and validate JavaScript code
$content = $this->sanitize_js_code($this->widget_data['js_code']);
// Add file header comment
$header = "/**\n * Widget Scripts\n * Generated by Master Addons Widget Builder\n * Widget ID: {$this->post_id}\n */\n\n";
$content = $header . $content;
$result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
if (!$result) {
return new \WP_Error('js_write_failed', 'Failed to write script.js file');
}
return true;
}
/**
* Sanitize CSS code
* Removes potentially dangerous code while preserving valid CSS
*
* @param string $css
* @return string
*/
private function sanitize_css_code($css) {
if (empty($css)) {
return '';
}
$css = trim($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('/