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-rest-controller.php

class-rest-controller.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-rest-controller.php

727 lines 25.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Widget Builder REST API Controller
4 *
5 * Provides REST endpoints for:
6 * - GET /assets - Fetches registered WP/Elementor scripts and styles
7 * - POST /widgets/move - Moves widget between categories
8 * - GET/POST/PUT /widgets - CRUD operations for widgets
9 *
10 * @package MasterAddons
11 * @subpackage WidgetBuilder
12 */
13
14 namespace MasterAddons\Inc\Admin\WidgetBuilder;
15
16 use WP_REST_Controller;
17 use WP_REST_Server;
18 use WP_REST_Request;
19 use WP_REST_Response;
20 use WP_Error;
21
22 if (!defined('ABSPATH')) {
23 exit;
24 }
25
26 class REST_Controller extends WP_REST_Controller {
27
28 /**
29 * Namespace
30 */
31 protected $namespace = 'jltma/v1';
32
33 /**
34 * Constructor
35 */
36 public function __construct() {
37 }
38
39 /**
40 * Register routes
41 */
42 public function register_routes() {
43 // GET /assets - Get registered WP/Elementor dependencies
44 register_rest_route($this->namespace, '/assets', [
45 [
46 'methods' => WP_REST_Server::READABLE,
47 'callback' => [$this, 'get_assets'],
48 'permission_callback' => [$this, 'check_permission'],
49 ],
50 ]);
51
52 // POST /widgets/move - Move widget between categories
53 register_rest_route($this->namespace, '/widgets/move', [
54 [
55 'methods' => WP_REST_Server::CREATABLE,
56 'callback' => [$this, 'move_widget'],
57 'permission_callback' => [$this, 'check_permission'],
58 'args' => [
59 'id' => [
60 'required' => true,
61 'type' => 'integer',
62 'sanitize_callback' => 'absint',
63 ],
64 'category' => [
65 'required' => true,
66 'type' => 'string',
67 'sanitize_callback' => 'sanitize_text_field',
68 ],
69 ],
70 ],
71 ]);
72
73 // POST /widgets - Create new widget
74 register_rest_route($this->namespace, '/widgets', [
75 [
76 'methods' => WP_REST_Server::CREATABLE,
77 'callback' => [$this, 'create_widget'],
78 'permission_callback' => [$this, 'check_permission'],
79 ],
80 ]);
81
82 // GET/PUT/DELETE /widgets/{id} - CRUD operations for single widget
83 register_rest_route($this->namespace, '/widgets/(?P<id>\d+)', [
84 [
85 'methods' => WP_REST_Server::READABLE,
86 'callback' => [$this, 'get_widget'],
87 'permission_callback' => [$this, 'check_permission'],
88 ],
89 [
90 'methods' => WP_REST_Server::EDITABLE,
91 'callback' => [$this, 'update_widget'],
92 'permission_callback' => [$this, 'check_permission'],
93 ],
94 [
95 'methods' => WP_REST_Server::DELETABLE,
96 'callback' => [$this, 'delete_widget'],
97 'permission_callback' => [$this, 'check_permission'],
98 ],
99 ]);
100
101 // POST /widgets/{id}/controls - Add control to widget section
102 register_rest_route($this->namespace, '/widgets/(?P<id>\d+)/controls', [
103 [
104 'methods' => WP_REST_Server::CREATABLE,
105 'callback' => [$this, 'add_control'],
106 'permission_callback' => [$this, 'check_permission'],
107 ],
108 ]);
109
110 // DELETE /widgets/{id}/controls/{control_id} - Delete control
111 register_rest_route($this->namespace, '/widgets/(?P<id>\d+)/controls/(?P<control_id>[a-zA-Z0-9_]+)', [
112 [
113 'methods' => WP_REST_Server::DELETABLE,
114 'callback' => [$this, 'delete_control'],
115 'permission_callback' => [$this, 'check_permission'],
116 ],
117 ]);
118
119 // GET /categories - Get Elementor widget categories
120 register_rest_route($this->namespace, '/categories', [
121 [
122 'methods' => WP_REST_Server::READABLE,
123 'callback' => [$this, 'get_categories'],
124 'permission_callback' => [$this, 'check_permission'],
125 ],
126 ]);
127
128 // POST /categories - Register new Elementor category
129 register_rest_route($this->namespace, '/categories', [
130 [
131 'methods' => WP_REST_Server::CREATABLE,
132 'callback' => [$this, 'register_category'],
133 'permission_callback' => [$this, 'check_permission'],
134 'args' => [
135 'name' => [
136 'required' => true,
137 'type' => 'string',
138 'sanitize_callback' => 'sanitize_text_field',
139 ],
140 'slug' => [
141 'required' => false,
142 'type' => 'string',
143 'sanitize_callback' => 'sanitize_title',
144 ],
145 ],
146 ],
147 ]);
148 }
149
150 /**
151 * Permission callback
152 */
153 public function check_permission() {
154 return current_user_can('manage_options');
155 }
156
157 /**
158 * GET /assets
159 * Returns registered WordPress and Elementor scripts/styles
160 */
161 public function get_assets(WP_REST_Request $request) {
162 global $wp_scripts, $wp_styles;
163
164 $response = [
165 'wp_scripts' => [],
166 'wp_styles' => [],
167 'elementor' => defined('ELEMENTOR_VERSION'),
168 'elementor_scripts' => [],
169 'elementor_styles' => [],
170 ];
171
172 // WordPress scripts
173 if (!empty($wp_scripts->registered)) {
174 foreach ($wp_scripts->registered as $handle => $script) {
175 $response['wp_scripts'][] = [
176 'handle' => $handle,
177 'src' => $script->src,
178 'deps' => $script->deps,
179 'version' => $script->ver,
180 ];
181 }
182 }
183
184 // WordPress styles
185 if (!empty($wp_styles->registered)) {
186 foreach ($wp_styles->registered as $handle => $style) {
187 $response['wp_styles'][] = [
188 'handle' => $handle,
189 'src' => $style->src,
190 'deps' => $style->deps,
191 'version' => $style->ver,
192 ];
193 }
194 }
195
196 // Elementor dependencies (if available)
197 if (defined('ELEMENTOR_VERSION')) {
198 $elementor_scripts = apply_filters('jltma_elementor_scripts', [
199 'elementor-frontend',
200 'elementor-waypoints',
201 'swiper',
202 'elementor-dialog',
203 ]);
204
205 foreach ($elementor_scripts as $handle) {
206 if (isset($wp_scripts->registered[$handle])) {
207 $script = $wp_scripts->registered[$handle];
208 $response['elementor_scripts'][] = [
209 'handle' => $handle,
210 'src' => $script->src,
211 'deps' => $script->deps,
212 'version' => $script->ver,
213 ];
214 }
215 }
216 }
217
218 return new WP_REST_Response($response, 200);
219 }
220
221 /**
222 * POST /widgets/move
223 * Move widget to different category
224 */
225 public function move_widget(WP_REST_Request $request) {
226 $widget_id = $request->get_param('id');
227 $category = $request->get_param('category');
228
229 // Verify widget exists
230 $widget = get_post($widget_id);
231 if (!$widget || $widget->post_type !== 'jltma_widget') {
232 return new WP_Error('widget_not_found', 'Widget not found', ['status' => 404]);
233 }
234
235 // Update category meta
236 $updated = update_post_meta($widget_id, '_jltma_widget_category', $category);
237
238 if ($updated !== false) {
239 return new WP_REST_Response([
240 'success' => true,
241 'message' => 'Widget category updated successfully',
242 'data' => [
243 'id' => $widget_id,
244 'category' => $category,
245 ],
246 ], 200);
247 }
248
249 return new WP_Error('update_failed', 'Failed to update widget category', ['status' => 500]);
250 }
251
252 /**
253 * GET /widgets/{id}
254 * Get single widget data
255 */
256 public function get_widget(WP_REST_Request $request) {
257 $widget_id = $request->get_param('id');
258
259 $widget = get_post($widget_id);
260 if (!$widget || $widget->post_type !== 'jltma_widget') {
261 return new WP_Error('widget_not_found', 'Widget not found', ['status' => 404]);
262 }
263
264 // Get the unified widget data that includes HTML/CSS/JS code
265 $unified_data = get_post_meta($widget_id, '_jltma_widget_data', true);
266
267 // Get includes data and ensure proper structure
268 $includes = get_post_meta($widget_id, '_jltma_widget_includes', true);
269 if (empty($includes) || !is_array($includes)) {
270 $includes = array(
271 'css_libraries' => array(),
272 'js_libraries' => array()
273 );
274 }
275
276 // Get dependencies data and ensure proper structure
277 $dependencies = get_post_meta($widget_id, '_jltma_widget_dependencies', true);
278 if (empty($dependencies) || !is_array($dependencies)) {
279 $dependencies = array(
280 'wp' => array(),
281 'elementor' => array()
282 );
283 }
284
285 $widget_data = [
286 'id' => $widget_id,
287 'title' => $widget->post_title,
288 'category' => get_post_meta($widget_id, '_jltma_widget_category', true) ?: 'general',
289 'sections' => get_post_meta($widget_id, '_jltma_widget_sections', true) ?: [],
290 'includes' => $includes,
291 'dependencies' => $dependencies,
292 // Include HTML/CSS/JS code from unified widget data
293 'html_code' => isset($unified_data['html_code']) ? $unified_data['html_code'] : '',
294 'css_code' => isset($unified_data['css_code']) ? $unified_data['css_code'] : '',
295 'js_code' => isset($unified_data['js_code']) ? $unified_data['js_code'] : '',
296 'icon' => isset($unified_data['icon']) ? $unified_data['icon'] : 'eicon-code',
297 ];
298
299 return new WP_REST_Response($widget_data, 200);
300 }
301
302 /**
303 * POST /widgets
304 * Create new widget
305 */
306 public function create_widget(WP_REST_Request $request) {
307 $data = $request->get_json_params();
308
309 $widget_id = wp_insert_post([
310 'post_title' => sanitize_text_field($data['title'] ?? 'New Widget'),
311 'post_type' => 'jltma_widget',
312 'post_status' => 'publish',
313 'post_content' => '',
314 ]);
315
316 if (is_wp_error($widget_id)) {
317 return new WP_Error('create_failed', 'Failed to create widget', ['status' => 500]);
318 }
319
320 // Save meta data
321 $this->save_widget_meta($widget_id, $data);
322
323 // Generate widget files
324 $this->generate_widget_files($widget_id);
325
326 return new WP_REST_Response([
327 'success' => true,
328 'message' => 'Widget created successfully',
329 'data' => [
330 'id' => $widget_id,
331 'title' => get_the_title($widget_id),
332 'edit_url' => admin_url('admin.php?page=jltma-widget-editor&widget_id=' . $widget_id),
333 ],
334 ], 201);
335 }
336
337 /**
338 * PUT /widgets/{id}
339 * Update widget
340 */
341 public function update_widget(WP_REST_Request $request) {
342 $widget_id = $request->get_param('id');
343 $data = $request->get_json_params();
344
345 $widget = get_post($widget_id);
346
347 if (!$widget || $widget->post_type !== 'jltma_widget') {
348 $error_details = [
349 'widget_id' => $widget_id,
350 'widget_exists' => !empty($widget),
351 'widget_post_type' => $widget ? $widget->post_type : null,
352 ];
353
354 return new WP_Error('widget_not_found', 'Widget not found', ['status' => 404, 'details' => $error_details]);
355 }
356
357 // Update post
358 wp_update_post([
359 'ID' => $widget_id,
360 'post_title' => sanitize_text_field($data['title'] ?? $widget->post_title),
361 ]);
362
363 // Save meta data
364 $this->save_widget_meta($widget_id, $data);
365
366 // Generate widget files
367 $this->generate_widget_files($widget_id);
368
369 return new WP_REST_Response([
370 'success' => true,
371 'message' => 'Widget updated successfully',
372 'data' => [
373 'id' => $widget_id,
374 ],
375 ], 200);
376 }
377
378 /**
379 * DELETE /widgets/{id}
380 * Delete widget
381 */
382 public function delete_widget(WP_REST_Request $request) {
383 $widget_id = $request->get_param('id');
384
385 $widget = get_post($widget_id);
386 if (!$widget || $widget->post_type !== 'jltma_widget') {
387 return new WP_Error('widget_not_found', 'Widget not found', ['status' => 404]);
388 }
389
390 $deleted = wp_delete_post($widget_id, true);
391
392 if (!$deleted) {
393 return new WP_Error('delete_failed', 'Failed to delete widget', ['status' => 500]);
394 }
395
396 // Delete generated widget files
397 Widget_Generator::delete_widget_files($widget_id);
398
399 return new WP_REST_Response([
400 'success' => true,
401 'message' => 'Widget deleted successfully',
402 ], 200);
403 }
404
405 /**
406 * POST /widgets/{id}/controls
407 * Add control to widget section
408 */
409 public function add_control(WP_REST_Request $request) {
410 $widget_id = $request->get_param('id');
411 $data = $request->get_json_params();
412
413 $sections = get_post_meta($widget_id, '_jltma_widget_sections', true) ?: [];
414
415 // Find section and add control
416 $section_id = $data['section_id'] ?? null;
417 foreach ($sections as &$section) {
418 if ($section['id'] === $section_id) {
419 $section['controls'][] = $data['control'];
420 break;
421 }
422 }
423
424 update_post_meta($widget_id, '_jltma_widget_sections', $sections);
425
426 return new WP_REST_Response([
427 'success' => true,
428 'message' => 'Control added successfully',
429 ], 200);
430 }
431
432 /**
433 * DELETE /widgets/{id}/controls/{control_id}
434 * Delete control from widget
435 */
436 public function delete_control(WP_REST_Request $request) {
437 $widget_id = $request->get_param('id');
438 $control_id = $request->get_param('control_id');
439
440 $sections = get_post_meta($widget_id, '_jltma_widget_sections', true) ?: [];
441
442 // Find and remove control
443 foreach ($sections as &$section) {
444 $section['controls'] = array_filter($section['controls'], function($control) use ($control_id) {
445 return $control['id'] !== $control_id;
446 });
447 $section['controls'] = array_values($section['controls']); // Re-index
448 }
449
450 update_post_meta($widget_id, '_jltma_widget_sections', $sections);
451
452 return new WP_REST_Response([
453 'success' => true,
454 'message' => 'Control deleted successfully',
455 ], 200);
456 }
457
458 /**
459 * GET /categories
460 * Get all registered Elementor widget categories
461 */
462 public function get_categories(WP_REST_Request $request) {
463 $categories = [];
464
465 // Check if Elementor is active
466 if (did_action('elementor/loaded')) {
467 $elements_manager = \Elementor\Plugin::$instance->elements_manager;
468 $elementor_categories = $elements_manager->get_categories();
469
470 foreach ($elementor_categories as $slug => $category_data) {
471 $categories[] = [
472 'slug' => $slug,
473 'title' => isset($category_data['title']) ? $category_data['title'] : $slug,
474 'icon' => isset($category_data['icon']) ? $category_data['icon'] : '',
475 ];
476 }
477 }
478
479 // Add custom categories from options
480 $custom_categories = get_option('jltma_custom_widget_categories', []);
481 if (!empty($custom_categories) && is_array($custom_categories)) {
482 foreach ($custom_categories as $slug => $title) {
483 // Check if not already in list
484 $exists = false;
485 foreach ($categories as $cat) {
486 if ($cat['slug'] === $slug) {
487 $exists = true;
488 break;
489 }
490 }
491 if (!$exists) {
492 $categories[] = [
493 'slug' => $slug,
494 'title' => $title,
495 'icon' => '',
496 ];
497 }
498 }
499 }
500
501 return new WP_REST_Response($categories, 200);
502 }
503
504 /**
505 * POST /categories
506 * Register a new Elementor widget category
507 */
508 public function register_category(WP_REST_Request $request) {
509 $name = $request->get_param('name');
510 $slug = $request->get_param('slug');
511
512 // Generate slug from name if not provided
513 if (empty($slug)) {
514 $slug = sanitize_title($name);
515 }
516
517 // Store in custom categories option
518 $custom_categories = get_option('jltma_custom_widget_categories', []);
519 $custom_categories[$slug] = $name;
520 update_option('jltma_custom_widget_categories', $custom_categories);
521
522 // Register with Elementor if active
523 if (did_action('elementor/loaded')) {
524 $elements_manager = \Elementor\Plugin::$instance->elements_manager;
525 $elements_manager->add_category(
526 $slug,
527 [
528 'title' => $name,
529 'icon' => 'eicon-posts-ticker',
530 ]
531 );
532 }
533
534 return new WP_REST_Response([
535 'success' => true,
536 'message' => 'Category registered successfully',
537 'data' => [
538 'slug' => $slug,
539 'title' => $name,
540 ],
541 ], 201);
542 }
543
544 /**
545 * Helper: Save widget meta data
546 */
547 private function save_widget_meta($widget_id, $data) {
548
549
550 if (isset($data['category'])) {
551 update_post_meta($widget_id, '_jltma_widget_category', sanitize_text_field($data['category']));
552 }
553
554 if (isset($data['sections'])) {
555 // Debug log sections and controls data in cleaner format
556
557 // Group sections by tab
558 $widget_details = [
559 'widget_settings' => [
560 'title' => get_the_title($widget_id),
561 'icon' => isset($data['icon']) ? $data['icon'] : 'eicon-code',
562 'category' => isset($data['category']) ? $data['category'] : 'general',
563 ],
564 'content_tab' => [],
565 'style_tab' => [],
566 'advanced_tab' => []
567 ];
568
569 foreach ($data['sections'] as $section) {
570 $tab = isset($section['tab']) ? $section['tab'] : 'content';
571 $section_data = [
572 'section_id' => isset($section['id']) ? $section['id'] : '',
573 'section_label' => isset($section['label']) ? $section['label'] : '',
574 'controls' => []
575 ];
576
577 if (isset($section['controls']) && is_array($section['controls'])) {
578 foreach ($section['controls'] as $control) {
579 // Only include non-empty values
580 $clean_control = [];
581 foreach ($control as $key => $value) {
582 // Skip empty values and default values
583 if ($value !== '' && $value !== null && $value !== [] &&
584 !($key === 'show_label' && $value == 1) &&
585 !($key === 'label_on' && $value === 'Yes') &&
586 !($key === 'label_off' && $value === 'No') &&
587 !($key === 'return_value' && $value === 'yes') &&
588 !($key === 'separator' && $value === 'default') &&
589 !($key === 'language' && $value === 'html') &&
590 !($key === 'minute_increment' && $value == 1) &&
591 !($key === 'skin' && $value === 'inline') &&
592 !($key === 'prevent_empty' && $value == 1) &&
593 !($key === 'media_types' && is_array($value) && count($value) === 1 && $value[0] === 'image') &&
594 !($key === 'allowed_dimensions' && is_array($value) && count($value) === 4)
595 ) {
596 $clean_control[$key] = $value;
597 }
598 }
599 $section_data['controls'][] = $clean_control;
600 }
601 }
602
603 // Add to appropriate tab
604 $tab_key = $tab . '_tab';
605 $widget_details[$tab_key][] = $section_data;
606 }
607
608 update_post_meta($widget_id, '_jltma_widget_sections', $data['sections']);
609 }
610
611 if (isset($data['includes'])) {
612 update_post_meta($widget_id, '_jltma_widget_includes', $data['includes']);
613 }
614
615 if (isset($data['dependencies'])) {
616 update_post_meta($widget_id, '_jltma_widget_dependencies', $data['dependencies']);
617 }
618
619 // Generate widget name from title
620 $widget_name = get_post_meta($widget_id, '_jltma_widget_name', true);
621 if (empty($widget_name) && isset($data['title'])) {
622 update_post_meta($widget_id, '_jltma_widget_name', sanitize_title($data['title']));
623 }
624
625 // Sanitize code data before saving
626 $html_code = '';
627 $css_code = '';
628 $js_code = '';
629
630 // Sanitize HTML code. PHP is NEVER allowed (no arbitrary code execution),
631 // regardless of capability. Inline <script> is stripped — JavaScript belongs
632 // in the JS field, which is enqueued separately. Dynamic values use {{placeholders}}.
633 if (isset($data['html_code'])) {
634 $html_code = $this->strip_php_tags($data['html_code']);
635 $html_code = preg_replace('#<script\b[^>]*>.*?</script>#is', '', $html_code);
636 $html_code = preg_replace('#</?script\b[^>]*>#i', '', $html_code);
637 }
638
639 // Sanitize CSS code - strip PHP/script/style tags and dangerous constructs.
640 if (isset($data['css_code'])) {
641 $css_code = $this->sanitize_css($data['css_code']);
642 }
643
644 // Sanitize JavaScript code. PHP is NEVER allowed; <script> tags are stripped so
645 // the value cannot break out of the generated/enqueued script context.
646 if (isset($data['js_code'])) {
647 $js_code = $this->strip_php_tags($data['js_code']);
648 $js_code = preg_replace('#</?script\b[^>]*>#i', '', $js_code);
649 }
650
651 // Capability gate: only users who can post unfiltered HTML may store raw
652 // JavaScript or unrestricted HTML. Everyone else gets a strict allowlist
653 // and no raw JS. On multisite, unfiltered_html is granted to super admins
654 // only, so site admins there cannot persist raw code.
655 if (!current_user_can('unfiltered_html')) {
656 $html_code = wp_kses_post($html_code);
657 $js_code = '';
658 }
659
660 // Also save data in unified format for widget generator
661 $widget_data = [
662 'title' => get_the_title($widget_id),
663 'icon' => isset($data['icon']) ? sanitize_text_field($data['icon']) : 'eicon-code',
664 'category' => isset($data['category']) ? sanitize_text_field($data['category']) : 'master-addons',
665 'sections' => isset($data['sections']) ? $data['sections'] : [],
666 'html_code' => $html_code,
667 'css_code' => $css_code,
668 'js_code' => $js_code
669 ];
670
671 update_post_meta($widget_id, '_jltma_widget_data', $widget_data);
672 }
673
674 /**
675 * Strip every PHP open/close tag (and null bytes) from a string so user input
676 * can never become executable PHP once written into a generated widget file.
677 *
678 * @param string $code
679 * @return string
680 */
681 private function strip_php_tags($code) {
682 if (!is_string($code) || '' === $code) {
683 return '';
684 }
685 $code = str_replace(chr(0), '', $code);
686 $code = preg_replace('/<\?php/i', '', $code);
687 $code = str_replace(array('<?=', '<?', '?>'), '', $code);
688 return $code;
689 }
690
691 /**
692 * Sanitize CSS code
693 *
694 * @param string $css
695 * @return string
696 */
697 private function sanitize_css($css) {
698 // Remove any PHP tags
699 $css = $this->strip_php_tags($css);
700
701 // Remove any <style>/<script> tags so the value cannot break out of the
702 // generated inline <style> block.
703 $css = preg_replace('#</?style\b[^>]*>#i', '', $css);
704 $css = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $css);
705 $css = preg_replace('#</?script\b[^>]*>#i', '', $css);
706
707 // Remove dangerous CSS constructs.
708 $css = preg_replace('/@import\b/i', '', $css);
709 $css = preg_replace('/expression\s*\(/i', '', $css);
710 $css = preg_replace('/(javascript|vbscript)\s*:/i', '', $css);
711 $css = preg_replace('/behavior\s*:/i', '', $css);
712
713 // Remove any JavaScript event handlers
714 $css = preg_replace('/on\w+\s*=\s*["\'].*?["\']/i', '', $css);
715
716 return $css;
717 }
718
719 /**
720 * Generate widget files
721 */
722 private function generate_widget_files($widget_id) {
723 $generator = new Widget_Generator($widget_id);
724 $result = $generator->generate();
725 }
726 }
727