PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.9
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / admin / widget-builder / class-widget-generator.php

class-widget-generator.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.9, at inc/admin/widget-builder/class-widget-generator.php

1,519 lines 55.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace MasterAddons\Inc\Admin\WidgetBuilder;
3
4 defined('ABSPATH') || exit;
5
6 /**
7 * Widget File Generator
8 * Generates widget PHP files from widget builder data
9 *
10 * @package MasterAddons
11 * @subpackage WidgetBuilder
12 */
13 if (!class_exists('MasterAddons\Inc\Admin\WidgetBuilder\Widget_Generator')) {
14 class Widget_Generator {
15
16 private $post_id;
17 private $widget_data;
18 private $widget_slug;
19 private $widget_class;
20 private $upload_dir;
21 private $upload_url;
22 private $widget_dir;
23 private $widget_url;
24 private $control_manager;
25
26 // Track used control keys to ensure uniqueness
27 private $used_control_keys = [];
28
29 // Prefixes
30 private $name_prefix = 'jltma_wb_';
31 private $class_prefix = 'JLTMA_WB_';
32
33 /**
34 * Constructor
35 *
36 * @param int $post_id Widget post ID
37 */
38 public function __construct($post_id) {
39 $this->post_id = absint($post_id);
40 $this->widget_slug = $this->name_prefix . $this->post_id;
41 $this->widget_class = $this->class_prefix . $this->post_id;
42
43 $this->init_directories();
44 $this->load_widget_data();
45
46 // Initialize control manager
47 require_once __DIR__ . '/class-control-manager.php';
48 $this->control_manager = Control_Manager::get_instance();
49 }
50
51 /**
52 * Initialize upload directories
53 */
54 private function init_directories() {
55 $upload = wp_upload_dir();
56
57 $this->upload_dir = $upload['basedir'] . '/master_addons/widgets';
58 $this->upload_url = $upload['baseurl'] . '/master_addons/widgets';
59
60 $this->widget_dir = $this->upload_dir . '/' . $this->post_id;
61 $this->widget_url = $this->upload_url . '/' . $this->post_id;
62 }
63
64 /**
65 * Load widget data from post meta
66 */
67 private function load_widget_data() {
68 $this->widget_data = get_post_meta($this->post_id, '_jltma_widget_data', true);
69
70 if (empty($this->widget_data)) {
71 $this->widget_data = $this->get_default_data();
72 }
73
74 // Load sections data separately
75 $sections = get_post_meta($this->post_id, '_jltma_widget_sections', true);
76 if (!empty($sections) && is_array($sections)) {
77 $this->widget_data['sections'] = $sections;
78 } else {
79 $this->widget_data['sections'] = [];
80 }
81
82 // Load includes data separately (CSS/JS libraries). Premium-gated, so
83 // free builds get an empty set.
84 $this->widget_data['includes'] = Widget_Builder_Init::get_widget_includes($this->post_id);
85 }
86
87 /**
88 * Get default widget data structure
89 *
90 * @return array
91 */
92 private function get_default_data() {
93 return [
94 'title' => get_the_title($this->post_id),
95 'icon' => 'eicon-code',
96 'category' => get_post_meta($this->post_id, '_jltma_widget_category', true) ?: 'master-addons',
97 'sections' => [],
98 'html_code' => '',
99 'css_code' => '',
100 'js_code' => ''
101 ];
102 }
103
104 /**
105 * Generate all widget files
106 *
107 * @return bool|WP_Error
108 */
109 public function generate() {
110 // No-op. Widgets are now rendered at runtime by Dynamic_Widget directly
111 // from post meta — no PHP/CSS/JS files are written under uploads and no
112 // user-derived PHP is ever executed. Retained as a no-op so existing call
113 // sites (REST save, admin save, migration) stay valid. The build_* methods
114 // below remain for reference/tests only.
115 return true;
116 }
117
118 /**
119 * Create widget directory
120 *
121 * @return bool
122 */
123 private function create_directory() {
124 if (!file_exists($this->widget_dir)) {
125 if (!wp_mkdir_p($this->widget_dir)) {
126 return false;
127 }
128 }
129
130 // Defense in depth: drop a silence index.php into the base and per-widget
131 // directories so generated files cannot be listed/browsed directly. Generated
132 // PHP is plugin-authored and ABSPATH-guarded, so it is inert over the web.
133 $this->harden_directory($this->upload_dir);
134 $this->harden_directory($this->widget_dir);
135
136 return true;
137 }
138
139 /**
140 * Write a silence index.php into a generated directory.
141 *
142 * @param string $dir
143 */
144 private function harden_directory($dir) {
145 if (empty($dir)) {
146 return;
147 }
148
149 global $wp_filesystem;
150 if (empty($wp_filesystem)) {
151 require_once(ABSPATH . '/wp-admin/includes/file.php');
152 WP_Filesystem();
153 }
154
155 $index = trailingslashit($dir) . 'index.php';
156 if (!file_exists($index)) {
157 $wp_filesystem->put_contents($index, "<?php\n// Silence is golden.\n", FS_CHMOD_FILE);
158 }
159 }
160
161 /**
162 * Generate PHP widget file
163 *
164 * @return bool|WP_Error
165 */
166 private function generate_php_file() {
167 $content = $this->build_php_content();
168
169 $file_path = $this->widget_dir . '/widget.php';
170
171 // Use WP_Filesystem
172 global $wp_filesystem;
173 if (empty($wp_filesystem)) {
174 require_once(ABSPATH . '/wp-admin/includes/file.php');
175 WP_Filesystem();
176 }
177
178 $result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
179
180 if (!$result) {
181 return new \WP_Error('file_write_failed', 'Failed to write widget.php file');
182 }
183
184 return true;
185 }
186
187 /**
188 * Build PHP widget file content
189 *
190 * @return string
191 */
192 private function build_php_content() {
193 $content = "<?php\n";
194 $content .= "namespace MasterAddons\\Addons;\n\n";
195 $content .= "use MasterAddons\\Inc\\Classes\\Base\\Master_Widget;\n";
196 $content .= "use \\Elementor\\Controls_Manager;\n";
197 $content .= "use \\Elementor\\Core\\Kits\\Documents\\Tabs\\Global_Typography;\n\n";
198 $content .= "if (!defined('ABSPATH')) exit;\n\n";
199 $content .= "/**\n";
200 $content .= " * " . esc_html($this->widget_data['title']) . "\n";
201 $content .= " * Generated by Master Addons Widget Builder\n";
202 $content .= " * Widget ID: " . $this->post_id . "\n";
203 $content .= " */\n";
204 $content .= "class " . $this->widget_class . " extends Master_Widget {\n\n";
205
206 // Constructor if CSS/JS files exist
207 if (!empty($this->widget_data['css_code']) || !empty($this->widget_data['js_code'])) {
208 $content .= $this->build_constructor();
209 }
210
211 // Widget name
212 $content .= $this->build_get_name();
213
214 // Widget title
215 $content .= $this->build_get_title();
216
217 // Widget icon
218 $content .= $this->build_get_icon();
219
220 // Widget categories
221 $content .= $this->build_get_categories();
222
223 // Register controls
224 $content .= $this->build_register_controls();
225
226 // Add tabs data helper method if there are tab controls
227 $tabs_helper = $this->build_get_tabs_data_helper();
228 if (!empty($tabs_helper)) {
229 $content .= $tabs_helper;
230 }
231
232 // Render method
233 $content .= $this->build_render();
234
235 $content .= "}\n";
236
237 return $content;
238 }
239
240 /**
241 * Build constructor method
242 *
243 * @return string
244 */
245 private function build_constructor() {
246 $handle = 'jltma-wb-' . $this->post_id;
247
248 $content = "\tpublic function __construct(\$data = [], \$args = null) {\n";
249 $content .= "\t\tparent::__construct(\$data, \$args);\n\n";
250
251 // Register external CSS libraries
252 if (!empty($this->widget_data['includes']['css_libraries'])) {
253 foreach ($this->widget_data['includes']['css_libraries'] as $css_lib) {
254 if (!empty($css_lib['handle']) && !empty($css_lib['src'])) {
255 $deps = !empty($css_lib['dependencies']) && is_array($css_lib['dependencies'])
256 ? $css_lib['dependencies']
257 : [];
258 $deps_string = $this->build_deps_array($deps);
259
260 // Only register if src is a URL (external library)
261 if (filter_var($css_lib['src'], FILTER_VALIDATE_URL)) {
262 $content .= "\t\twp_register_style('{$css_lib['handle']}', '{$css_lib['src']}', {$deps_string}, '1.0.0');\n";
263 }
264 }
265 }
266 }
267
268 // Register external JS libraries
269 if (!empty($this->widget_data['includes']['js_libraries'])) {
270 foreach ($this->widget_data['includes']['js_libraries'] as $js_lib) {
271 if (!empty($js_lib['handle']) && !empty($js_lib['src'])) {
272 $deps = !empty($js_lib['dependencies']) && is_array($js_lib['dependencies'])
273 ? $js_lib['dependencies']
274 : [];
275 $deps_string = $this->build_deps_array($deps);
276
277 // Only register if src is a URL (external library)
278 if (filter_var($js_lib['src'], FILTER_VALIDATE_URL)) {
279 $content .= "\t\twp_register_script('{$js_lib['handle']}', '{$js_lib['src']}', {$deps_string}, '1.0.0', true);\n";
280 }
281 }
282 }
283 }
284
285 // Register widget's own CSS
286 if (!empty($this->widget_data['css_code'])) {
287 $content .= "\t\twp_register_style('{$handle}-style', '{$this->widget_url}/style.css', [], '1.0.0');\n";
288 }
289
290 // Register widget's own JS
291 if (!empty($this->widget_data['js_code'])) {
292 $content .= "\t\twp_register_script('{$handle}-script', '{$this->widget_url}/script.js', ['elementor-frontend'], '1.0.0', true);\n";
293 }
294
295 $content .= "\t}\n\n";
296
297 // Build get_style_depends method
298 $style_deps = [];
299
300 // Add external CSS library handles
301 if (!empty($this->widget_data['includes']['css_libraries'])) {
302 foreach ($this->widget_data['includes']['css_libraries'] as $css_lib) {
303 if (!empty($css_lib['handle'])) {
304 $style_deps[] = $css_lib['handle'];
305 }
306 }
307 }
308
309 // Add widget's own CSS
310 if (!empty($this->widget_data['css_code'])) {
311 $style_deps[] = "{$handle}-style";
312 }
313
314 if (!empty($style_deps)) {
315 $content .= "\tpublic function get_style_depends() {\n";
316 $content .= "\t\treturn " . $this->build_deps_array($style_deps) . ";\n";
317 $content .= "\t}\n\n";
318 }
319
320 // Build get_script_depends method
321 $script_deps = [];
322
323 // Add external JS library handles
324 if (!empty($this->widget_data['includes']['js_libraries'])) {
325 foreach ($this->widget_data['includes']['js_libraries'] as $js_lib) {
326 if (!empty($js_lib['handle'])) {
327 $script_deps[] = $js_lib['handle'];
328 }
329 }
330 }
331
332 // Add widget's own JS
333 if (!empty($this->widget_data['js_code'])) {
334 $script_deps[] = "{$handle}-script";
335 }
336
337 if (!empty($script_deps)) {
338 $content .= "\tpublic function get_script_depends() {\n";
339 $content .= "\t\treturn " . $this->build_deps_array($script_deps) . ";\n";
340 $content .= "\t}\n\n";
341 }
342
343 return $content;
344 }
345
346 /**
347 * Build dependencies array string for PHP code
348 *
349 * @param array $deps
350 * @return string
351 */
352 private function build_deps_array($deps) {
353 if (empty($deps)) {
354 return '[]';
355 }
356
357 $quoted_deps = array_map(function($dep) {
358 return "'" . esc_attr($dep) . "'";
359 }, $deps);
360
361 return '[' . implode(', ', $quoted_deps) . ']';
362 }
363
364 /**
365 * Build get_name method
366 *
367 * @return string
368 */
369 private function build_get_name() {
370 $content = "\tpublic function get_name() {\n";
371 $content .= "\t\treturn '{$this->widget_slug}';\n";
372 $content .= "\t}\n\n";
373
374 return $content;
375 }
376
377 /**
378 * Build get_title method
379 *
380 * @return string
381 */
382 private function build_get_title() {
383 $title = !empty($this->widget_data['title']) ? esc_html($this->widget_data['title']) : 'Custom Widget';
384
385 $content = "\tpublic function get_title() {\n";
386 $content .= "\t\treturn esc_html__('{$title}', 'master-addons');\n";
387 $content .= "\t}\n\n";
388
389 return $content;
390 }
391
392 /**
393 * Build get_icon method
394 *
395 * @return string
396 */
397 private function build_get_icon() {
398 $icon = !empty($this->widget_data['icon']) ? $this->widget_data['icon'] : 'eicon-code';
399 // Escape for safe interpolation into a single-quoted PHP string literal.
400 $icon = addslashes(sanitize_text_field($icon));
401
402 $content = "\tpublic function get_icon() {\n";
403 $content .= "\t\treturn '{$icon}';\n";
404 $content .= "\t}\n\n";
405
406 return $content;
407 }
408
409 /**
410 * Build get_categories method
411 *
412 * @return string
413 */
414 private function build_get_categories() {
415 $category = !empty($this->widget_data['category']) ? $this->widget_data['category'] : 'master-addons';
416 // Escape for safe interpolation into a single-quoted PHP string literal.
417 $category = addslashes(sanitize_text_field($category));
418
419 $content = "\tpublic function get_categories() {\n";
420 $content .= "\t\treturn ['{$category}'];\n";
421 $content .= "\t}\n\n";
422
423 return $content;
424 }
425
426 /**
427 * Build register_controls method
428 *
429 * @return string
430 */
431 private function build_register_controls() {
432 $content = "\tprotected function register_controls() {\n";
433
434 if (!empty($this->widget_data['sections']) && is_array($this->widget_data['sections'])) {
435 // Sort sections by tab order: content, style, advanced
436 $sorted_sections = $this->sort_sections_by_tab($this->widget_data['sections']);
437
438 foreach ($sorted_sections as $section_id => $section) {
439 // Ensure section has proper structure
440 if (is_array($section)) {
441 $content .= $this->build_section($section_id, $section);
442 }
443 }
444 }
445
446 $content .= "\t}\n\n";
447
448 return $content;
449 }
450
451 /**
452 * Sort sections by tab order: content, style, advanced
453 *
454 * @param array $sections
455 * @return array
456 */
457 private function sort_sections_by_tab($sections) {
458 $content_sections = [];
459 $style_sections = [];
460 $advanced_sections = [];
461
462 foreach ($sections as $section_id => $section) {
463 if (!is_array($section)) {
464 continue;
465 }
466
467 $tab = !empty($section['tab']) ? $section['tab'] : 'content';
468
469 if ($tab === 'style') {
470 $style_sections[$section_id] = $section;
471 } elseif ($tab === 'advanced') {
472 $advanced_sections[$section_id] = $section;
473 } else {
474 $content_sections[$section_id] = $section;
475 }
476 }
477
478 // Merge in correct order: content, style, advanced
479 return array_merge($content_sections, $style_sections, $advanced_sections);
480 }
481
482 /**
483 * Build a control section
484 *
485 * @param string $section_id
486 * @param array $section
487 * @return string
488 */
489 private function build_section($section_id, $section) {
490 // Try 'title' first (used by Widget Builder), fall back to 'label', then default to 'Section'
491 $label = !empty($section['title']) ? esc_html($section['title']) : (!empty($section['label']) ? esc_html($section['label']) : 'Section');
492 $tab = !empty($section['tab']) ? $section['tab'] : 'content';
493
494 // Generate section key with proper prefix based on tab
495 $tab_prefix = '';
496 $tab_const = 'Controls_Manager::TAB_CONTENT';
497
498 if ($tab === 'style') {
499 $tab_prefix = 'jltma_style_';
500 $tab_const = 'Controls_Manager::TAB_STYLE';
501 } elseif ($tab === 'advanced') {
502 $tab_prefix = 'jltma_advanced_';
503 $tab_const = 'Controls_Manager::TAB_ADVANCED';
504 } else {
505 $tab_prefix = 'jltma_content_';
506 }
507
508 // Create sanitized section slug from label and add post ID
509 $section_slug = $this->sanitize_key($label);
510 $section_key = $tab_prefix . $section_slug . '_' . $section_id. '_' . $this->post_id;
511
512 $content = "\n\t\t\$this->start_controls_section(\n";
513 $content .= "\t\t\t'{$section_key}',\n";
514 $content .= "\t\t\t[\n";
515 $content .= "\t\t\t\t'label' => esc_html__('{$label}', 'master-addons'),\n";
516 $content .= "\t\t\t\t'tab' => {$tab_const},\n";
517 $content .= "\t\t\t]\n";
518 $content .= "\t\t);\n\n";
519
520 // Add controls (check both 'fields' and 'controls' for backwards compatibility)
521 $controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
522
523
524 if (!empty($controls) && is_array($controls)) {
525 foreach ($controls as $field_id => $field) {
526 $content .= $this->build_control($field_id, $field, $tab);
527 }
528 }
529
530 $content .= "\t\t\$this->end_controls_section();\n";
531 return $content;
532 }
533
534 /**
535 * Build a control
536 *
537 * @param string $field_id
538 * @param array $field
539 * @param string $tab Current tab (content/style/advanced)
540 * @return string
541 */
542 private function build_control($field_id, $field, $tab = 'content') {
543 $label = !empty($field['label']) ? esc_html($field['label']) : 'Control';
544 $type = !empty($field['type']) ? $field['type'] : 'TEXT';
545
546 // Special handling for TABS control - it's a structural element, not a regular control
547 // TABS control requires the full field data with 'tabs' array and should use field['name'] as key
548 if (strtoupper($type) === 'TABS') {
549 // For TABS, use the control name directly as the key (not label-based)
550 $control_name = !empty($field['name']) ? $field['name'] : 'tabs_' . $field_id;
551
552 // Pass tab and widget_id context
553 $field['_tab'] = $tab;
554 $field['_widget_id'] = $this->post_id;
555 // Pass used control keys reference for global uniqueness tracking
556 $field['_used_control_keys'] = &$this->used_control_keys;
557
558 // Call TABS control builder directly with the name as key
559 return $this->control_manager->build_control($control_name, $field, $type);
560 }
561
562 // Generate control key with proper prefix based on tab
563 $tab_prefix = '';
564 if ($tab === 'style') {
565 $tab_prefix = 'jltma_style_';
566 } elseif ($tab === 'advanced') {
567 $tab_prefix = 'jltma_advanced_';
568 } else {
569 $tab_prefix = 'jltma_content_';
570 }
571
572 // Create sanitized control slug from label and add post ID
573 $control_slug = $this->sanitize_key($label);
574 $base_key = $tab_prefix . $control_slug . '_' . $this->post_id;
575
576 // Ensure unique control key - append counter if duplicate
577 $control_key = $base_key;
578 $counter = 1;
579 while (in_array($control_key, $this->used_control_keys)) {
580 $control_key = $tab_prefix . $control_slug . '_' . $counter . '_' . $this->post_id;
581 $counter++;
582 }
583 $this->used_control_keys[] = $control_key;
584
585 // Pass tab and widget_id context to control manager for condition key conversion
586 $field['_tab'] = $tab;
587 $field['_widget_id'] = $this->post_id;
588 $field['_tab_prefix'] = $tab_prefix;
589 $field['_sections_data'] = $this->widget_data['sections'] ?? [];
590
591 // Preprocess date_time controls - convert UI settings to picker_options
592 if ($type === 'date_time') {
593 $picker_options = [];
594
595 // Convert enable_time to picker_options
596 $enable_time = isset($field['enable_time']) ? (bool) $field['enable_time'] : false;
597 if (isset($field['enable_time'])) {
598 $picker_options['enableTime'] = $enable_time;
599 }
600
601 // Set dateFormat based on enableTime
602 $picker_options['dateFormat'] = $enable_time ? 'Y-m-d H:i' : 'Y-m-d';
603
604 // Always use 24-hour format
605 $picker_options['time_24hr'] = true;
606
607 // Convert minute_increment to picker_options
608 if (isset($field['minute_increment']) && !empty($field['minute_increment'])) {
609 $picker_options['minuteIncrement'] = intval($field['minute_increment']);
610 }
611
612 // Merge with existing picker_options if any
613 if (!empty($field['picker_options']) && is_array($field['picker_options'])) {
614 $picker_options = array_merge($field['picker_options'], $picker_options);
615 }
616
617 // Set picker_options
618 if (!empty($picker_options)) {
619 $field['picker_options'] = $picker_options;
620 }
621 }
622
623 // Use Control Manager to build control
624 return $this->control_manager->build_control($control_key, $field, $type);
625 }
626
627 /**
628 * Get tab data structures from widget sections
629 * Returns array of tab control names with their field mappings
630 *
631 * @return array ['tab_control_name' => ['name' => 'tab_name', 'tabs' => [...]]]
632 */
633 private function get_tab_structures() {
634 $tab_structures = [];
635
636 if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
637 return $tab_structures;
638 }
639
640 foreach ($this->widget_data['sections'] as $section) {
641 if (!is_array($section) || empty($section['controls'])) {
642 continue;
643 }
644
645 foreach ($section['controls'] as $control) {
646 if (empty($control['type']) || strtoupper($control['type']) !== 'TABS') {
647 continue;
648 }
649
650 if (empty($control['name'])) {
651 continue;
652 }
653
654 // Get tab info - check for both 'tabs' array (processed) and 'fields'+'tab_fields' (raw from UI)
655 $tabs = [];
656
657 if (!empty($control['tabs']) && is_array($control['tabs'])) {
658 // Already processed tabs structure
659 $tabs = $control['tabs'];
660 } elseif (!empty($control['fields']) && is_array($control['fields'])) {
661 // Raw structure from UI - build tabs array
662 $tab_fields = !empty($control['tab_fields']) && is_array($control['tab_fields']) ? $control['tab_fields'] : [];
663
664 foreach ($control['fields'] as $tab_definition) {
665 if (empty($tab_definition['name'])) {
666 continue;
667 }
668
669 $tab_name = $tab_definition['name'];
670 $tab = [
671 'name' => $tab_name,
672 'label' => !empty($tab_definition['label']) ? $tab_definition['label'] : ucfirst($tab_name),
673 'controls' => []
674 ];
675
676 // Add controls for this tab if they exist
677 if (!empty($tab_fields[$tab_name]) && is_array($tab_fields[$tab_name])) {
678 $tab['controls'] = $tab_fields[$tab_name];
679 }
680
681 $tabs[] = $tab;
682 }
683 }
684
685 if (!empty($tabs)) {
686 $tab_control_name = $control['name'];
687 $tab_structures[$tab_control_name] = [
688 'name' => $tab_control_name,
689 'tabs' => $tabs
690 ];
691 }
692 }
693 }
694
695 return $tab_structures;
696 }
697
698 /**
699 * Build get_tabs_data helper method
700 * Creates a helper method that organizes tab control data into accessible array structure
701 *
702 * @return string
703 */
704 private function build_get_tabs_data_helper() {
705 $tab_structures = $this->get_tab_structures();
706
707 if (empty($tab_structures)) {
708 return '';
709 }
710
711 $content = "\t/**\n";
712 $content .= "\t * Get tabs data organized by tab control\n";
713 $content .= "\t * Helper method to access tab data as arrays\n";
714 $content .= "\t *\n";
715 $content .= "\t * @param array \$settings Widget settings\n";
716 $content .= "\t * @return array Organized tab data\n";
717 $content .= "\t */\n";
718 $content .= "\tprivate function get_tabs_data(\$settings) {\n";
719 $content .= "\t\t\$tabs_data = [];\n\n";
720
721 // Build data structure for each tab control
722 foreach ($tab_structures as $tab_control_name => $tab_info) {
723 $content .= "\t\t// Tab control: {$tab_control_name}\n";
724 $content .= "\t\t\$tabs_data['{$tab_control_name}'] = [\n";
725 $content .= "\t\t\t'tabs' => [],\n";
726 $content .= "\t\t];\n\n";
727
728 foreach ($tab_info['tabs'] as $tab_index => $tab) {
729 $tab_name = $tab['name'];
730 $tab_label = $tab['label'] ?? ucfirst($tab_name);
731
732 $content .= "\t\t// Tab: {$tab_label}\n";
733 $content .= "\t\t\$tabs_data['{$tab_control_name}']['tabs']['{$tab_name}'] = [\n";
734 $content .= "\t\t\t'name' => '{$tab_name}',\n";
735 $content .= "\t\t\t'label' => '{$tab_label}',\n";
736 $content .= "\t\t\t'content' => [],\n";
737 $content .= "\t\t];\n\n";
738
739 // Map controls from this tab
740 if (!empty($tab['controls']) && is_array($tab['controls'])) {
741 foreach ($tab['controls'] as $control) {
742 if (empty($control['name']) || empty($control['label'])) {
743 continue;
744 }
745
746 // Get the actual control key in settings
747 $control_label = $control['label'];
748 $control_slug = $this->sanitize_key($control_label);
749
750 // Get tab context from widget data
751 $tab_context = $this->get_tab_context_for_control($control['name']);
752 $tab_prefix = $this->get_tab_prefix($tab_context);
753 $control_key = $tab_prefix . $control_slug . '_' . $this->post_id;
754
755 $control_name = $control['name'];
756
757 $content .= "\t\t\$tabs_data['{$tab_control_name}']['tabs']['{$tab_name}']['content']['{$control_name}'] = \$settings['{$control_key}'] ?? '';\n";
758 }
759 }
760
761 $content .= "\n";
762 }
763 }
764
765 $content .= "\t\treturn \$tabs_data;\n";
766 $content .= "\t}\n\n";
767
768 return $content;
769 }
770
771 /**
772 * Get tab context (content/style/advanced) for a control
773 *
774 * @param string $control_name Control name to search for
775 * @return string Tab context (content/style/advanced)
776 */
777 private function get_tab_context_for_control($control_name) {
778 if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
779 return 'content';
780 }
781
782 foreach ($this->widget_data['sections'] as $section) {
783 if (!is_array($section) || empty($section['controls'])) {
784 continue;
785 }
786
787 foreach ($section['controls'] as $control) {
788 if (empty($control['type']) || strtoupper($control['type']) !== 'TABS') {
789 continue;
790 }
791
792 if (!empty($control['tabs']) && is_array($control['tabs'])) {
793 foreach ($control['tabs'] as $tab) {
794 if (!empty($tab['controls']) && is_array($tab['controls'])) {
795 foreach ($tab['controls'] as $tab_control) {
796 if (isset($tab_control['name']) && $tab_control['name'] === $control_name) {
797 // Found the control, return the section's tab context
798 return $section['tab'] ?? 'content';
799 }
800 }
801 }
802 }
803 }
804 }
805 }
806
807 return 'content';
808 }
809
810 /**
811 * Get tab prefix based on tab context
812 *
813 * @param string $tab Tab context (content/style/advanced)
814 * @return string Prefix for control keys
815 */
816 private function get_tab_prefix($tab) {
817 if ($tab === 'style') {
818 return 'jltma_style_';
819 } elseif ($tab === 'advanced') {
820 return 'jltma_advanced_';
821 } else {
822 return 'jltma_content_';
823 }
824 }
825
826 /**
827 * Sanitize label to create control/section key
828 * Converts spaces to underscores instead of hyphens
829 *
830 * @param string $label Label to sanitize
831 * @return string Sanitized key with underscores
832 */
833 private function sanitize_key($label) {
834 // Convert to lowercase
835 $key = strtolower($label);
836
837 // Replace spaces with underscores
838 $key = str_replace(' ', '_', $key);
839
840 // Remove special characters, keeping only alphanumeric and underscores
841 $key = preg_replace('/[^a-z0-9_]/', '', $key);
842
843 // Remove multiple consecutive underscores
844 $key = preg_replace('/_+/', '_', $key);
845
846 // Trim underscores from beginning and end
847 $key = trim($key, '_');
848
849 return $key;
850 }
851
852 /**
853 * Build render method
854 *
855 * @return string
856 */
857 private function build_render() {
858 $html = !empty($this->widget_data['html_code']) ? $this->widget_data['html_code'] : '';
859
860 $html = $this->prepare_html_for_render($html);
861
862 $content = "\tprotected function render() {\n";
863 $content .= "\t\t\$settings = \$this->get_settings_for_display();\n";
864 $content .= "\t\t\$this->render_widget_content(\$settings);\n";
865 $content .= "\t}\n\n";
866
867 // Add render_shortcode method for shortcode support
868 $content .= "\t/**\n";
869 $content .= "\t * Render widget as shortcode\n";
870 $content .= "\t * \n";
871 $content .= "\t * @param array \$settings Settings array from shortcode attributes\n";
872 $content .= "\t */\n";
873 $content .= "\tpublic function render_shortcode(\$settings = []) {\n";
874 $content .= "\t\t// Merge with defaults\n";
875 $content .= "\t\tif (empty(\$settings)) {\n";
876 $content .= "\t\t\t\$settings = \$this->get_settings_for_display();\n";
877 $content .= "\t\t}\n";
878 $content .= "\t\t\$this->render_widget_content(\$settings);\n";
879 $content .= "\t}\n\n";
880
881 // Add render_widget_content method - shared between Elementor and shortcode
882 $content .= "\t/**\n";
883 $content .= "\t * Render widget HTML content\n";
884 $content .= "\t * Shared method for both Elementor widget and shortcode output\n";
885 $content .= "\t * \n";
886 $content .= "\t * @param array \$settings Widget settings\n";
887 $content .= "\t */\n";
888 $content .= "\tprotected function render_widget_content(\$settings) {\n";
889
890 // Check if we have tab controls and add tabs data
891 $tab_structures = $this->get_tab_structures();
892 if (!empty($tab_structures)) {
893 $content .= "\t\t// Get organized tab data\n";
894 $content .= "\t\t\$tabs_data = \$this->get_tabs_data(\$settings);\n\n";
895 }
896
897 // Build control mapping for placeholder replacement
898 $control_mapping = $this->build_control_mapping();
899
900 // Get tab structures for tab data access
901 $tab_structures = $this->get_tab_structures();
902
903 // Replace placeholders in HTML with PHP code
904 // Two-pass approach:
905 // 1. First pass: Replace placeholders INSIDE PHP tags with just variable references
906 // 2. Second pass: Replace placeholders OUTSIDE PHP tags with full <?php echo ... tags
907 // Control types that return array values in Elementor
908 $array_types = ['select2', 'media', 'gallery', 'typography', 'dimensions', 'box_shadow', 'background', 'border', 'text_shadow', 'divider', 'repeater', 'tabs'];
909
910 if (!empty($control_mapping)) {
911 foreach($control_mapping as $control => $param){
912 $control_info = $this->get_control_info($control);
913 $control_type = strtolower($control_info['type'] ?? 'text');
914 $fallback = in_array($control_type, $array_types) ? '[]' : "''";
915 $content .= "\t \$$control = !empty(\$settings['$param']) ? \$settings['$param'] : $fallback;\n";
916 }
917 // Pass 1: Handle placeholders inside PHP tags
918 $html = $this->replace_placeholders_in_php_context($html, $control_mapping, $tab_structures);
919
920 // Pass 2: Handle placeholders outside PHP tags
921 $html = $this->replace_placeholders_outside_php_context($html, $control_mapping, $tab_structures);
922 }
923
924 // Strip any leftover {{...}} placeholders that don't map to a value control
925 // (e.g. HEADING/DIVIDER controls store no value) so they never render
926 // literally on the frontend.
927 $html = preg_replace('/\{\{[^}]+\}\}/', '', $html);
928
929 // Process CSS code if it contains template strings
930 $css_code = !empty($this->widget_data['css_code']) ? $this->widget_data['css_code'] : '';
931 $has_css_templates = !empty($css_code) && preg_match('/\{\{[^}]+\}\}/', $css_code);
932
933 // Process JS code if it contains template strings
934 $js_code = !empty($this->widget_data['js_code']) ? $this->widget_data['js_code'] : '';
935 $has_js_templates = !empty($js_code) && preg_match('/\{\{[^}]+\}\}/', $js_code);
936
937 // Output the processed HTML
938 $content .= "\t\t?" . ">\n";
939
940 // Output dynamic CSS if it contains template strings
941 if ($has_css_templates && !empty($control_mapping)) {
942 $content .= "\t\t<style>\n";
943 $content .= "\t\t\t<" . "?php\n";
944 $content .= "\t\t\t" . $this->build_dynamic_css_output($css_code, $control_mapping, $tab_structures);
945 $content .= "\t\t\t?" . ">\n";
946 $content .= "\t\t</style>\n";
947 }
948
949 if (!empty($html)) {
950 $content .= $html . "\n";
951 }
952
953 // Output dynamic JS if it contains template strings
954 if ($has_js_templates && !empty($control_mapping)) {
955 $content .= "\t\t<script>\n";
956 $content .= "\t\t\t<" . "?php\n";
957 $content .= "\t\t\t" . $this->build_dynamic_js_output($js_code, $control_mapping, $tab_structures);
958 $content .= "\t\t\t?" . ">\n";
959 $content .= "\t\t</script>\n";
960 }
961
962 $content .= "\t\t<" . "?php\n";
963 $content .= "\t}\n\n";
964
965 return $content;
966 }
967
968 /**
969 * Replace placeholders inside PHP context
970 * Replaces {{placeholder}} with just variable references, no PHP tags
971 *
972 * @param string $html HTML code with placeholders
973 * @param array $control_mapping Mapping of control names to keys
974 * @param array $tab_structures Tab control structures
975 * @return string HTML with placeholders inside PHP replaced
976 */
977 private function replace_placeholders_in_php_context($html, $control_mapping, $tab_structures) {
978 // Match PHP blocks and replace placeholders within them
979 $php_open = '<' . '?php';
980 $php_close = '?' . '>';
981 $pattern = '/' . preg_quote($php_open, '/') . '(.*?)' . preg_quote($php_close, '/') . '/s';
982
983 return preg_replace_callback(
984 $pattern,
985 function($matches) use ($control_mapping, $tab_structures, $php_open, $php_close) {
986 $php_code = $matches[1];
987
988 // Replace placeholders within this PHP block
989 $php_code = preg_replace_callback(
990 '/\{\{([^}]+)\}\}/',
991 function($inner_matches) use ($control_mapping, $tab_structures) {
992 return $this->get_variable_reference($inner_matches[1], $control_mapping, $tab_structures);
993 },
994 $php_code
995 );
996
997 return $php_open . $php_code . $php_close;
998 },
999 $html
1000 );
1001 }
1002
1003 /**
1004 * Replace placeholders outside PHP context
1005 * Replaces {{placeholder}} with full PHP echo statements
1006 *
1007 * @param string $html HTML code with placeholders
1008 * @param array $control_mapping Mapping of control names to keys
1009 * @param array $tab_structures Tab control structures
1010 * @return string HTML with remaining placeholders replaced
1011 */
1012 private function replace_placeholders_outside_php_context($html, $control_mapping, $tab_structures) {
1013 return preg_replace_callback(
1014 '/\{\{([^}]+)\}\}/',
1015 function($matches) use ($control_mapping, $tab_structures) {
1016 $placeholder = trim($matches[1]);
1017
1018 // Get the variable reference
1019 $var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
1020
1021 // If it returned just a variable (not a full echo statement), wrap it in echo
1022 if ($var_ref !== $matches[0] && strpos($var_ref, '<' . '?php') === false) {
1023 // Determine appropriate escaping based on control type
1024 $parts = explode('.', $placeholder);
1025 $field_name = $parts[0];
1026 $control_info = $this->get_control_info($field_name);
1027 $control_type = $control_info['type'] ?? 'text';
1028
1029 // For simple variable references, wrap in echo with appropriate escaping
1030 if (in_array(strtolower($control_type), ['wysiwyg', 'code'])) {
1031 return '<' . '?php echo wp_kses_post(' . $var_ref . '); ?' . '>';
1032 } else {
1033 return '<' . '?php echo esc_html(' . $var_ref . '); ?' . '>';
1034 }
1035 }
1036
1037 return $var_ref;
1038 },
1039 $html
1040 );
1041 }
1042
1043 /**
1044 * Get variable reference for a placeholder
1045 * Returns just the PHP variable access code without PHP tags or echo
1046 *
1047 * @param string $placeholder Placeholder string (without {{ }})
1048 * @param array $control_mapping Mapping of control names to keys
1049 * @param array $tab_structures Tab control structures
1050 * @return string Variable reference or original placeholder if not found
1051 */
1052 private function get_variable_reference($placeholder, $control_mapping, $tab_structures) {
1053 $placeholder = trim($placeholder);
1054
1055 // Check if this is a tab data access pattern (e.g., abc_tabs.tabs)
1056 if (!empty($tab_structures)) {
1057 foreach ($tab_structures as $tab_control_name => $tab_info) {
1058 // Check for exact match: tab_name.tabs
1059 if ($placeholder === $tab_control_name . '.tabs') {
1060 return "\$tabs_data['{$tab_control_name}']['tabs']";
1061 }
1062 // Also support just the tab control name to get entire tab data
1063 if ($placeholder === $tab_control_name) {
1064 return "\$tabs_data['{$tab_control_name}']";
1065 }
1066 }
1067 }
1068
1069 // Parse placeholder for nested properties (e.g., url.url, url.target, icons.value)
1070 $parts = explode('.', $placeholder);
1071 $field_name = $parts[0];
1072 $property = isset($parts[1]) ? $parts[1] : null;
1073
1074 if (isset($control_mapping[$field_name])) {
1075 $control_key = $control_mapping[$field_name];
1076 $control_info = $this->get_control_info($field_name);
1077 $control_type = $control_info['type'] ?? 'text';
1078
1079 // Return just the variable reference for use in PHP context
1080 // For simple controls, return the settings value
1081 // For complex controls with properties, return the nested array access
1082 if ($property) {
1083 // Handle nested properties
1084 switch (strtolower($control_type)) {
1085 case 'url':
1086 case 'media':
1087 case 'image':
1088 case 'icons':
1089 case 'icon':
1090 case 'slider':
1091 case 'dimensions':
1092 return "\$settings['{$control_key}']['{$property}']";
1093 default:
1094 return "\$settings['{$control_key}']";
1095 }
1096 } else {
1097 // No property, just return the setting value
1098 return "\$settings['{$control_key}']";
1099 }
1100 }
1101
1102 // If placeholder not found in mapping, return as-is
1103 return '{{' . $placeholder . '}}';
1104 }
1105
1106 /**
1107 * Build control mapping for placeholder replacement
1108 * Maps control names (placeholders) to their full control keys
1109 *
1110 * @return array Associative array: placeholder => control_key
1111 */
1112 private function build_control_mapping() {
1113 $mapping = [];
1114
1115 // Check if sections exist
1116 if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
1117 return $mapping;
1118 }
1119
1120 // Map tab names to prefixes
1121 $tab_prefix_map = [
1122 'content' => 'jltma_content_',
1123 'style' => 'jltma_style_',
1124 'advanced' => 'jltma_advanced_',
1125 ];
1126
1127 // Iterate through all sections
1128 foreach ($this->widget_data['sections'] as $section_id => $section) {
1129 if (!is_array($section)) {
1130 continue;
1131 }
1132
1133 // Get tab name (default to content)
1134 $tab = !empty($section['tab']) ? $section['tab'] : 'content';
1135 $tab_prefix = $tab_prefix_map[$tab] ?? 'jltma_content_';
1136
1137 // Check both 'controls' and 'fields' keys for backwards compatibility
1138 $controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
1139
1140 // Iterate through controls
1141 foreach ($controls as $control) {
1142 if (empty($control['name'])) {
1143 continue;
1144 }
1145
1146 // Control name as used in HTML (placeholder)
1147 $control_name = $control['name'];
1148
1149 // Full control key as used in Elementor
1150 $control_slug = $this->sanitize_key($control['label'] ?? $control_name);
1151 $control_key = $tab_prefix . $control_slug . '_' . $this->post_id;
1152
1153 // Map placeholder to control key
1154 $mapping[$control_name] = $control_key;
1155
1156 // Handle popover_toggle child fields
1157 if (!empty($control['type']) && strtoupper($control['type']) === 'POPOVER_TOGGLE') {
1158 if (!empty($control['popover_fields']) && is_array($control['popover_fields'])) {
1159 foreach ($control['popover_fields'] as $popover_field) {
1160 if (empty($popover_field['name'])) {
1161 continue;
1162 }
1163
1164 // Child field name
1165 $child_field_name = $popover_field['name'];
1166
1167 // Placeholder pattern: parent_name_child_name (e.g., popover_toggle_color)
1168 $placeholder = $control_name . '_' . $child_field_name;
1169
1170 // Actual control key pattern: parent_control_key_child_name
1171 // (e.g., jltma_content_popover_toggle_381_color)
1172 $child_field_slug = $this->sanitize_key($child_field_name);
1173 $child_control_key = $control_key . '_' . $child_field_slug;
1174
1175 // Map placeholder to control key
1176 $mapping[$placeholder] = $child_control_key;
1177 }
1178 }
1179 }
1180 }
1181 }
1182
1183
1184 return $mapping;
1185 }
1186
1187 /**
1188 * Get control info by control name
1189 * Returns control type and other metadata
1190 *
1191 * @param string $control_name
1192 * @return array Control info with 'type' and other properties
1193 */
1194 private function get_control_info($control_name) {
1195 // Check if sections exist
1196 if (empty($this->widget_data['sections']) || !is_array($this->widget_data['sections'])) {
1197 return ['type' => 'text'];
1198 }
1199
1200 // Search through all sections
1201 foreach ($this->widget_data['sections'] as $section_id => $section) {
1202 if (!is_array($section)) {
1203 continue;
1204 }
1205
1206 // Check both 'controls' and 'fields' keys for backwards compatibility
1207 $controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
1208
1209 // Search for the control by name
1210 foreach ($controls as $control) {
1211 if (!empty($control['name']) && $control['name'] === $control_name) {
1212 return [
1213 'type' => $control['type'] ?? 'text',
1214 'label' => $control['label'] ?? '',
1215 'default' => $control['default'] ?? '',
1216 'responsive' => $control['responsive'] ?? false,
1217 ];
1218 }
1219
1220 // Check if this is a popover_toggle child field pattern (parent_name_child_name)
1221 if (!empty($control['type']) && strtoupper($control['type']) === 'POPOVER_TOGGLE') {
1222 if (!empty($control['popover_fields']) && is_array($control['popover_fields'])) {
1223 $parent_name = $control['name'];
1224 foreach ($control['popover_fields'] as $popover_field) {
1225 if (empty($popover_field['name'])) {
1226 continue;
1227 }
1228
1229 // Check if control_name matches pattern: parent_name_child_name
1230 $expected_pattern = $parent_name . '_' . $popover_field['name'];
1231 if ($control_name === $expected_pattern) {
1232 return [
1233 'type' => $popover_field['type'] ?? 'text',
1234 'label' => $popover_field['label'] ?? '',
1235 'default' => $popover_field['default'] ?? '',
1236 'responsive' => $popover_field['responsive'] ?? false,
1237 'parent' => $parent_name,
1238 ];
1239 }
1240 }
1241 }
1242 }
1243 }
1244 }
1245
1246 // Default if not found
1247 return ['type' => 'text'];
1248 }
1249
1250 /**
1251 * Prepare HTML code for render method
1252 * Ensures the HTML doesn't break PHP context
1253 *
1254 * @param string $html
1255 * @return string
1256 */
1257 private function prepare_html_for_render($html) {
1258 if (empty($html)) {
1259 return '';
1260 }
1261
1262 // Security: strip every PHP open/close tag so user-supplied markup can never
1263 // execute as PHP once written into the generated widget file. Generated files
1264 // contain only plugin-authored PHP; user HTML is treated as inert markup whose
1265 // {{placeholders}} are converted to escaped echo statements elsewhere.
1266 $html = str_replace(chr(0), '', $html);
1267 $html = preg_replace('/<\?php/i', '', $html);
1268 $html = str_replace(array('<?=', '<?', '?>'), '', $html);
1269
1270 return $html;
1271 }
1272
1273 /**
1274 * Build dynamic CSS output with template replacement
1275 * Replaces {{placeholder}} with PHP echo statements for CSS values
1276 *
1277 * @param string $css_code CSS code with placeholders
1278 * @param array $control_mapping Mapping of control names to keys
1279 * @param array $tab_structures Tab control structures
1280 * @return string PHP code that echoes CSS with replaced placeholders
1281 */
1282 private function build_dynamic_css_output($css_code, $control_mapping, $tab_structures) {
1283 // Split CSS by template string patterns to build echo statements
1284 $pattern = '/\{\{([^}]+)\}\}/';
1285 $parts = preg_split($pattern, $css_code, -1, PREG_SPLIT_DELIM_CAPTURE);
1286
1287 $output = "echo \"";
1288
1289 for ($i = 0; $i < count($parts); $i++) {
1290 if ($i % 2 === 0) {
1291 // This is regular CSS content (not a placeholder)
1292 // Escape for PHP string
1293 $escaped = str_replace('"', '\\"', $parts[$i]);
1294 $escaped = str_replace("\n", "\\n", $escaped);
1295 $output .= $escaped;
1296 } else {
1297 // This is a placeholder - close the string and add PHP code
1298 $placeholder = trim($parts[$i]);
1299 $var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
1300
1301 // Check if we got a valid replacement
1302 if ($var_ref !== '{{' . $placeholder . '}}') {
1303 $output .= "\" . esc_attr(" . $var_ref . ") . \"";
1304 } else {
1305 // Placeholder not found, keep as-is
1306 $output .= "{{" . $placeholder . "}}";
1307 }
1308 }
1309 }
1310
1311 $output .= "\";\n";
1312 return $output;
1313 }
1314
1315 /**
1316 * Build dynamic JS output with template replacement
1317 * Replaces {{placeholder}} with PHP echo statements for JS values
1318 *
1319 * @param string $js_code JavaScript code with placeholders
1320 * @param array $control_mapping Mapping of control names to keys
1321 * @param array $tab_structures Tab control structures
1322 * @return string PHP code that echoes JS with replaced placeholders
1323 */
1324 private function build_dynamic_js_output($js_code, $control_mapping, $tab_structures) {
1325 // Split JS by template string patterns to build echo statements
1326 $pattern = '/\{\{([^}]+)\}\}/';
1327 $parts = preg_split($pattern, $js_code, -1, PREG_SPLIT_DELIM_CAPTURE);
1328
1329 $output = "echo \"";
1330
1331 for ($i = 0; $i < count($parts); $i++) {
1332 if ($i % 2 === 0) {
1333 // This is regular JS content (not a placeholder)
1334 // Escape for PHP string
1335 $escaped = str_replace('"', '\\"', $parts[$i]);
1336 $escaped = str_replace("\n", "\\n", $escaped);
1337 $output .= $escaped;
1338 } else {
1339 // This is a placeholder - close the string and add PHP code
1340 $placeholder = trim($parts[$i]);
1341 $var_ref = $this->get_variable_reference($placeholder, $control_mapping, $tab_structures);
1342
1343 // Check if we got a valid replacement
1344 if ($var_ref !== '{{' . $placeholder . '}}') {
1345 $output .= "\" . esc_js(" . $var_ref . ") . \"";
1346 } else {
1347 // Placeholder not found, keep as-is
1348 $output .= "{{" . $placeholder . "}}";
1349 }
1350 }
1351 }
1352
1353 $output .= "\";\n";
1354 return $output;
1355 }
1356
1357 /**
1358 * Generate CSS file
1359 *
1360 * @return bool|WP_Error
1361 */
1362 private function generate_css_file() {
1363 global $wp_filesystem;
1364 if (empty($wp_filesystem)) {
1365 require_once(ABSPATH . '/wp-admin/includes/file.php');
1366 WP_Filesystem();
1367 }
1368
1369 $file_path = $this->widget_dir . '/style.css';
1370
1371 // Sanitize and validate CSS code
1372 $content = $this->sanitize_css_code($this->widget_data['css_code']);
1373
1374 // Add file header comment
1375 $header = "/**\n * Widget Styles\n * Generated by Master Addons Widget Builder\n * Widget ID: {$this->post_id}\n */\n\n";
1376 $content = $header . $content;
1377
1378 $result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
1379
1380 if (!$result) {
1381 return new \WP_Error('css_write_failed', 'Failed to write style.css file');
1382 }
1383
1384 return true;
1385 }
1386
1387 /**
1388 * Generate JS file
1389 *
1390 * @return bool|WP_Error
1391 */
1392 private function generate_js_file() {
1393 global $wp_filesystem;
1394 if (empty($wp_filesystem)) {
1395 require_once(ABSPATH . '/wp-admin/includes/file.php');
1396 WP_Filesystem();
1397 }
1398
1399 $file_path = $this->widget_dir . '/script.js';
1400
1401 // Sanitize and validate JavaScript code
1402 $content = $this->sanitize_js_code($this->widget_data['js_code']);
1403
1404 // Add file header comment
1405 $header = "/**\n * Widget Scripts\n * Generated by Master Addons Widget Builder\n * Widget ID: {$this->post_id}\n */\n\n";
1406 $content = $header . $content;
1407
1408 $result = $wp_filesystem->put_contents($file_path, $content, FS_CHMOD_FILE);
1409
1410 if (!$result) {
1411 return new \WP_Error('js_write_failed', 'Failed to write script.js file');
1412 }
1413
1414 return true;
1415 }
1416
1417 /**
1418 * Sanitize CSS code
1419 * Removes potentially dangerous code while preserving valid CSS
1420 *
1421 * @param string $css
1422 * @return string
1423 */
1424 private function sanitize_css_code($css) {
1425 if (empty($css)) {
1426 return '';
1427 }
1428
1429 $css = trim($css);
1430
1431 // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
1432 $css = preg_replace('/<\?php/i', '', $css);
1433 $css = str_replace(array('<?=', '<?', '?>'), '', $css);
1434
1435 // Remove any HTML script tags
1436 $css = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $css);
1437
1438 // Remove any HTML tags
1439 $css = preg_replace('/<[^>]*>/', '', $css);
1440
1441 // Remove any JavaScript event handlers
1442 $css = preg_replace('/on\w+\s*=\s*["\'].*?["\']/i', '', $css);
1443
1444 // Remove null bytes
1445 $css = str_replace(chr(0), '', $css);
1446
1447 return $css;
1448 }
1449
1450 /**
1451 * Sanitize JavaScript code
1452 * Basic validation to prevent obvious security issues
1453 *
1454 * @param string $js
1455 * @return string
1456 */
1457 private function sanitize_js_code($js) {
1458 if (empty($js)) {
1459 return '';
1460 }
1461
1462 $js = trim($js);
1463
1464 // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
1465 $js = preg_replace('/<\?php/i', '', $js);
1466 $js = str_replace(array('<?=', '<?', '?>'), '', $js);
1467
1468 // Strip <script> tags so the value cannot break out of the enqueued/inline
1469 // <script> context. JS string literals should not contain literal script tags.
1470 $js = preg_replace('#</?script\b[^>]*>#i', '', $js);
1471
1472 // Remove null bytes
1473 $js = str_replace(chr(0), '', $js);
1474
1475 return $js;
1476 }
1477
1478 /**
1479 * Delete widget files
1480 *
1481 * @param int $post_id
1482 * @return bool
1483 */
1484 public static function delete_widget_files($post_id) {
1485 global $wp_filesystem;
1486 if (empty($wp_filesystem)) {
1487 require_once(ABSPATH . '/wp-admin/includes/file.php');
1488 WP_Filesystem();
1489 }
1490
1491 $upload = wp_upload_dir();
1492 $widget_dir = $upload['basedir'] . '/master_addons/widgets/' . $post_id;
1493
1494 if (file_exists($widget_dir)) {
1495 return $wp_filesystem->delete($widget_dir, true);
1496 }
1497
1498 return true;
1499 }
1500
1501 /**
1502 * Get widget file path
1503 *
1504 * @return string
1505 */
1506 public function get_widget_file_path() {
1507 return $this->widget_dir . '/widget.php';
1508 }
1509
1510 /**
1511 * Get widget class name
1512 *
1513 * @return string
1514 */
1515 public function get_widget_class_name() {
1516 return $this->widget_class;
1517 }
1518 }
1519 }