PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.2
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.2, at inc/admin/widget-builder/class-widget-generator.php

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