PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.0.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.0.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 / rest-api.php

rest-api.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.0.9, at inc/admin/rest-api.php

1,156 lines 28.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API endpoints for admin settings.
4 *
5 * @package MasterAddons
6 */
7
8 namespace MasterAddons\Inc\Admin;
9
10 use MasterAddons\Inc\Admin\Settings\Settings;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class REST_API
19 *
20 * Registers REST API endpoints for settings management.
21 */
22 class REST_API {
23
24 /**
25 * Instance of this class.
26 *
27 * @var REST_API
28 */
29 private static $instance = null;
30
31 /**
32 * REST namespace.
33 *
34 * @var string
35 */
36 private $namespace = 'master-addons/v1';
37
38 /**
39 * Option key for setup wizard completion status.
40 *
41 * @var string
42 */
43 const SETUP_COMPLETE_OPTION = 'jltma_setup_wizard_complete';
44
45 /**
46 * Option key for setup wizard current step.
47 *
48 * @var string
49 */
50 const SETUP_STEP_OPTION = 'jltma_setup_wizard_step';
51
52 /**
53 * Option key for setup wizard site type selection.
54 *
55 * @var string
56 */
57 const SETUP_SITE_TYPE_OPTION = 'jltma_setup_wizard_site_type';
58
59 /**
60 * Get instance of this class.
61 *
62 * @return REST_API
63 */
64 public static function get_instance() {
65 if ( null === self::$instance ) {
66 self::$instance = new self();
67 }
68 return self::$instance;
69 }
70
71 /**
72 * Constructor.
73 */
74 private function __construct() {
75 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
76 }
77
78 /**
79 * Register REST routes.
80 */
81 public function register_routes() {
82 // Get/save admin settings.
83 register_rest_route(
84 $this->namespace,
85 '/jltma-options',
86 array(
87 array(
88 'methods' => 'GET',
89 'callback' => array( $this, 'get_settings_options' ),
90 'permission_callback' => array( $this, 'check_permission' ),
91 ),
92 array(
93 'methods' => 'POST',
94 'callback' => array( $this, 'save_settings_options' ),
95 'permission_callback' => array( $this, 'check_permission' ),
96 ),
97 )
98 );
99
100 // Reset settings.
101 register_rest_route(
102 $this->namespace,
103 '/reset',
104 array(
105 'methods' => 'POST',
106 'callback' => array( $this, 'reset_settings' ),
107 'permission_callback' => array( $this, 'check_permission' ),
108 )
109 );
110
111 // Export settings.
112 register_rest_route(
113 $this->namespace,
114 '/export',
115 array(
116 'methods' => 'GET',
117 'callback' => array( $this, 'export_settings' ),
118 'permission_callback' => array( $this, 'check_permission' ),
119 )
120 );
121
122 // Import settings.
123 register_rest_route(
124 $this->namespace,
125 '/import',
126 array(
127 'methods' => 'POST',
128 'callback' => array( $this, 'import_settings' ),
129 'permission_callback' => array( $this, 'check_permission' ),
130 )
131 );
132
133 // Get system info.
134 register_rest_route(
135 $this->namespace,
136 '/system-info',
137 array(
138 'methods' => 'GET',
139 'callback' => array( $this, 'get_system_info' ),
140 'permission_callback' => array( $this, 'check_permission' ),
141 )
142 );
143
144 // Get spotlight items.
145 register_rest_route(
146 $this->namespace,
147 '/spotlight',
148 array(
149 'methods' => 'GET',
150 'callback' => array( $this, 'get_spotlight' ),
151 'permission_callback' => array( $this, 'check_permission' ),
152 )
153 );
154
155 // Save dark mode preference.
156 register_rest_route(
157 $this->namespace,
158 '/user/dark-mode',
159 array(
160 'methods' => 'POST',
161 'callback' => array( $this, 'save_dark_mode' ),
162 'permission_callback' => array( $this, 'check_basic_permission' ),
163 )
164 );
165
166 // Setup Wizard Routes.
167 register_rest_route(
168 $this->namespace,
169 '/setup-complete',
170 array(
171 'methods' => 'POST',
172 'callback' => array( $this, 'handle_setup_complete' ),
173 'permission_callback' => array( $this, 'check_permission' ),
174 )
175 );
176
177 register_rest_route(
178 $this->namespace,
179 '/setup-status',
180 array(
181 'methods' => 'GET',
182 'callback' => array( $this, 'get_setup_status' ),
183 'permission_callback' => array( $this, 'check_permission' ),
184 )
185 );
186
187 // Theme management routes for setup wizard.
188 register_rest_route(
189 $this->namespace,
190 '/theme-status',
191 array(
192 'methods' => 'GET',
193 'callback' => array( $this, 'get_theme_status' ),
194 'permission_callback' => array( $this, 'check_permission' ),
195 )
196 );
197
198 register_rest_route(
199 $this->namespace,
200 '/theme-install',
201 array(
202 'methods' => 'POST',
203 'callback' => array( $this, 'install_theme' ),
204 'permission_callback' => function () {
205 return current_user_can( 'install_themes' );
206 },
207 )
208 );
209
210 register_rest_route(
211 $this->namespace,
212 '/theme-activate',
213 array(
214 'methods' => 'POST',
215 'callback' => array( $this, 'activate_theme' ),
216 'permission_callback' => function () {
217 return current_user_can( 'switch_themes' );
218 },
219 )
220 );
221
222
223 }
224
225 /**
226 * Check permission for REST requests.
227 *
228 * @param \WP_REST_Request $request Request object.
229 * @return bool
230 */
231 public function check_permission( $request ) {
232 return current_user_can( 'manage_options' );
233 }
234
235 /**
236 * Check basic permission (logged in).
237 *
238 * @return bool
239 */
240 public function check_basic_permission() {
241 return is_user_logged_in();
242 }
243
244 /**
245 * Get all settings options.
246 *
247 * @param \WP_REST_Request $request Request object.
248 * @return \WP_REST_Response
249 */
250 public function get_settings_options( $request ) {
251 // Get option key from query param.
252 $option_key = $request->get_param( 'key' );
253 $allowed_keys = $this->get_allowed_option_keys();
254
255 // If no key provided, return all allowed options.
256 if ( empty( $option_key ) ) {
257 $getters = $this->get_option_getters();
258 $all_options = array();
259 foreach ( $getters as $key => $getter ) {
260 $all_options[ $key ] = $getter();
261 }
262
263 return new \WP_REST_Response(
264 array(
265 'success' => true,
266 'data' => $all_options,
267 )
268 );
269 }
270
271 // Validate option key against whitelist.
272 if ( ! in_array( $option_key, array_keys($allowed_keys), true ) ) {
273 return new \WP_REST_Response(
274 array(
275 'success' => false,
276 'error' => array(
277 'code' => 'invalid_option_key',
278 'message' => __( 'Invalid option key.', 'master-addons' ),
279 ),
280 ),
281 400
282 );
283 }
284
285 // Get option value via lazy dispatch.
286 $getters = $this->get_option_getters();
287 $option_value = isset( $getters[ $option_key ] ) ? $getters[ $option_key ]() : [];
288
289 return new \WP_REST_Response(
290 array(
291 'success' => true,
292 'data' => $option_value,
293 )
294 );
295 }
296
297 /**
298 * Get allowed option keys whitelist.
299 *
300 * @return array
301 */
302 private function get_allowed_option_keys() {
303 return Settings::get_option_keys();
304 }
305
306 /**
307 * Get option getter callbacks keyed by short name.
308 *
309 * On first install (option doesn't exist in DB), seeds defaults from Config
310 * so the admin page always shows correct initial state — even if Elementor
311 * is not active and the activated_*() hooks never fired.
312 *
313 * Shared between GET and POST endpoints to avoid duplication.
314 *
315 * @return array<string, callable>
316 */
317 private function get_option_getters() {
318 return [
319 'addons' => function () {
320 // get_addons() checks legacy keys and migrates automatically.
321 // Only seed defaults on a genuine first install (no new key AND no legacy key).
322 $data = Settings::get_addons();
323 if ( empty( $data ) ) {
324 $data = Settings::get_default_addon_settings();
325 Settings::save_addons( $data );
326 }
327 return $data;
328 },
329 'extensions' => function () {
330 $data = Settings::get_extensions();
331 if ( empty( $data ) ) {
332 $data = Settings::get_default_extension_settings();
333 Settings::save_extensions( $data );
334 }
335 return $data;
336 },
337 'plugins' => function () {
338 $data = Settings::get_plugins();
339 if ( empty( $data ) ) {
340 $data = Settings::get_default_plugin_settings();
341 Settings::save_plugins( $data );
342 }
343 return $data;
344 },
345 'icons' => function () {
346 $data = Settings::get_icons();
347 if ( empty( $data ) ) {
348 $data = Settings::get_default_icon_settings();
349 Settings::save_icons( $data );
350 }
351 return $data;
352 },
353 'api' => fn() => Settings::get_api(),
354 'white_label' => fn() => get_option( Settings::WHITE_LABEL, [] ),
355 ];
356 }
357
358 /**
359 * Save all settings options.
360 *
361 * @param \WP_REST_Request $request Request object.
362 * @return \WP_REST_Response
363 */
364 public function save_settings_options( $request ) {
365 // Get and validate request body.
366 $body = $request->get_json_params();
367
368 if ( empty( $body ) || ! isset( $body['key'] ) || ! isset( $body['data'] ) ) {
369 return new \WP_REST_Response(
370 array(
371 'success' => false,
372 'error' => array(
373 'code' => 'invalid_request',
374 'message' => __( 'Missing required fields: key and data.', 'master-addons' ),
375 ),
376 ),
377 400
378 );
379 }
380
381 $option_key = $body['key'];
382 $option_data = $body['data'];
383
384 // Validate option key against whitelist.
385 $allowed_keys = $this->get_allowed_option_keys();
386 if ( ! in_array( $option_key, array_keys($allowed_keys), true ) ) {
387 return new \WP_REST_Response(
388 array(
389 'success' => false,
390 'error' => array(
391 'code' => 'invalid_option_key',
392 'message' => __( 'Invalid option key.', 'master-addons' ),
393 ),
394 ),
395 400
396 );
397 }
398
399 // Sanitize the option key.
400 $option_key = sanitize_key( $option_key );
401
402 // Recursively sanitize data.
403 $sanitized_data = $this->sanitize_option_data( $option_data );
404
405 // Lazy dispatch — only execute the saver for the requested key.
406 $savers = [
407 'addons' => fn( $d ) => Settings::save_addons( $d ),
408 'extensions' => fn( $d ) => Settings::save_extensions( $d ),
409 'plugins' => fn( $d ) => Settings::save_plugins( $d ),
410 'icons' => fn( $d ) => Settings::save_icons( $d ),
411 'api' => fn( $d ) => Settings::save_api( $d ),
412 'white_label' => fn( $d ) => update_option( Settings::WHITE_LABEL, $d ),
413 ];
414
415 if ( ! isset( $savers[ $option_key ] ) ) {
416 return new \WP_REST_Response(
417 array(
418 'success' => false,
419 'error' => array(
420 'code' => 'unsupported_option_key',
421 'message' => __( 'This option key cannot be saved.', 'master-addons' ),
422 ),
423 ),
424 400
425 );
426 }
427
428 $savers[ $option_key ]( $sanitized_data );
429
430 // Return the freshly saved data using the matching getter.
431 $getters = $this->get_option_getters();
432
433 return new \WP_REST_Response(
434 array(
435 'success' => true,
436 'message' => __( 'Settings saved successfully.', 'master-addons' ),
437 'data' => $getters[ $option_key ](),
438 )
439 );
440 }
441
442 /**
443 * Recursively sanitize option data.
444 *
445 * @param mixed $data Data to sanitize.
446 * @return mixed Sanitized data.
447 */
448 private function sanitize_option_data( $data ) {
449 if ( is_array( $data ) ) {
450 $sanitized = array();
451 foreach ( $data as $key => $value ) {
452 $sanitized_key = sanitize_key( $key );
453 $sanitized[ $sanitized_key ] = $this->sanitize_option_data( $value );
454 }
455 return $sanitized;
456 }
457
458 if ( is_string( $data ) ) {
459 // Allow safe HTML for WYSIWYG fields.
460 return wp_kses_post( $data );
461 }
462
463 if ( is_numeric( $data ) || is_bool( $data ) || is_null( $data ) ) {
464 return $data;
465 }
466
467 return '';
468 }
469
470 /**
471 * Reset all settings to defaults.
472 *
473 * @param \WP_REST_Request $request Request object.
474 * @return \WP_REST_Response
475 */
476 public function reset_settings( $request ) {
477 // Get and validate request body.
478 $body = $request->get_json_params();
479
480 $allowed_keys = $this->get_allowed_option_keys();
481 $options_key = isset( $body['key'] ) ? $body['key'] : '';
482
483 if ( ! empty( $options_key ) && isset( $allowed_keys[ $options_key ] ) ) {
484 // Remove one section by short name mapped to DB key.
485 $db_key = $allowed_keys[ $options_key ];
486 delete_option( $db_key );
487
488 return new \WP_REST_Response(
489 array(
490 'success' => true,
491 'reset_key' => $options_key,
492 'message' => __( 'Reset settings successfully!', 'master-addons' ),
493 )
494 );
495 }
496
497 // Remove all settings options.
498 if ( empty( $options_key ) ) {
499 foreach ( $allowed_keys as $short_name => $db_key ) {
500 delete_option( $db_key );
501 }
502
503 return new \WP_REST_Response(
504 array(
505 'success' => true,
506 'reset_key' => 'all',
507 'message' => __( 'Reset all settings successfully!', 'master-addons' ),
508 )
509 );
510 }
511
512 // Handle error messages.
513 return new \WP_REST_Response(
514 array(
515 'success' => false,
516 'message' => __( 'Something went wrong', 'master-addons' ),
517 )
518 );
519 }
520
521 /**
522 * Export settings as JSON.
523 *
524 * @param \WP_REST_Request $request Request object.
525 * @return \WP_REST_Response
526 */
527 public function export_settings( $request ) {
528 $plugin_slug = 'master-addons';
529 $getters = $this->get_option_getters();
530
531 // Build settings data keyed by short name (addons, extensions, etc.)
532 // to match the frontend OPTIONS_KEYS mapping.
533 $settings = array();
534 foreach ( $getters as $short_name => $getter ) {
535 $value = $getter();
536 if ( ! empty( $value ) ) {
537 $settings[ $short_name ] = $value;
538 }
539 }
540
541 // Build export data with metadata.
542 $export_data = array(
543 'plugin' => $plugin_slug,
544 'version' => defined( 'JLTMA_VER' ) ? JLTMA_VER : '1.0.0',
545 'timestamp' => current_time( 'c' ),
546 'settings' => $settings,
547 );
548
549 return new \WP_REST_Response(
550 array(
551 'success' => true,
552 'data' => $export_data,
553 'filename' => $plugin_slug . '-settings-' . gmdate( 'Y-m-d' ) . '.json',
554 )
555 );
556 }
557
558 /**
559 * Import settings from JSON.
560 *
561 * @param \WP_REST_Request $request Request object.
562 * @return \WP_REST_Response
563 */
564 public function import_settings( $request ) {
565 $import_data = $request->get_json_params();
566 $allowed_keys = $this->get_allowed_option_keys();
567
568 // Validate import data structure.
569 if ( empty( $import_data ) ) {
570 return new \WP_REST_Response(
571 array(
572 'success' => false,
573 'error' => array(
574 'code' => 'empty_data',
575 'message' => __( 'No import data provided.', 'master-addons' ),
576 ),
577 ),
578 400
579 );
580 }
581
582 // Check for settings key in import data.
583 if ( ! isset( $import_data['settings'] ) || ! is_array( $import_data['settings'] ) ) {
584 return new \WP_REST_Response(
585 array(
586 'success' => false,
587 'error' => array(
588 'code' => 'invalid_format',
589 'message' => __( 'Invalid import file format. Missing settings data.', 'master-addons' ),
590 ),
591 ),
592 400
593 );
594 }
595
596 $settings = $import_data['settings'];
597 $imported_count = 0;
598 $skipped_keys = array();
599
600 // Import each setting: key is a short name (addons, extensions, etc.)
601 // mapped to the full DB option key via $allowed_keys.
602 foreach ( $settings as $key => $value ) {
603 if ( isset( $allowed_keys[ $key ] ) ) {
604 $db_key = $allowed_keys[ $key ];
605 $sanitized_value = $this->sanitize_option_data( $value );
606 update_option( $db_key, $sanitized_value );
607 $imported_count++;
608 } else {
609 $skipped_keys[] = $key;
610 }
611 }
612
613 // Return response.
614 if ( $imported_count > 0 ) {
615 return new \WP_REST_Response(
616 array(
617 'success' => true,
618 'message' => sprintf(
619 /* translators: %d: number of imported settings */
620 __( 'Successfully imported %d setting(s).', 'master-addons' ),
621 $imported_count
622 ),
623 'imported_count' => $imported_count,
624 'skipped_keys' => $skipped_keys,
625 )
626 );
627 }
628
629 return new \WP_REST_Response(
630 array(
631 'success' => false,
632 'error' => array(
633 'code' => 'no_valid_settings',
634 'message' => __( 'No valid settings found in import file.', 'master-addons' ),
635 ),
636 ),
637 400
638 );
639 }
640
641 /**
642 * Get spotlight items.
643 *
644 * @param \WP_REST_Request $request Request object.
645 * @return \WP_REST_Response
646 */
647 public function get_spotlight( $request ) {
648 // Return empty spotlight items - can be extended later.
649 return new \WP_REST_Response(
650 array(
651 'success' => true,
652 'data' => array(),
653 )
654 );
655 }
656
657 /**
658 * Get system info data for the System Info page.
659 *
660 * Uses a transient cache (5 min) to avoid repeated expensive operations
661 * like reading plugin file headers on every request.
662 *
663 * @param \WP_REST_Request $request Request object.
664 * @return \WP_REST_Response
665 */
666 public function get_system_info( $request ) {
667 $refresh = $request->get_param( 'refresh' );
668 $cache_key = 'jltma_system_info';
669 $cache_time = 5 * MINUTE_IN_SECONDS;
670
671 // Return cached data unless refresh is requested.
672 if ( ! $refresh ) {
673 $cached = get_transient( $cache_key );
674 if ( false !== $cached ) {
675 return new \WP_REST_Response(
676 array(
677 'success' => true,
678 'data' => $cached,
679 )
680 );
681 }
682 }
683
684 global $wp_version, $wpdb;
685
686 // WordPress Environment
687 $memory_limit = ini_get( 'memory_limit' ) ?: 'N/A';
688 $uploads = wp_upload_dir( null, false ); // false = skip creating dirs.
689 $upload_writable = is_writable( $uploads['basedir'] );
690
691 $wordpress = array(
692 array(
693 'label' => 'Home URL',
694 'value' => home_url(),
695 'status' => 'none',
696 ),
697 array(
698 'label' => 'Site URL',
699 'value' => site_url(),
700 'status' => 'none',
701 ),
702 array(
703 'label' => 'WP Version',
704 'value' => $wp_version,
705 'status' => version_compare( $wp_version, '4.0', '>=' ) ? 'ok' : 'error',
706 ),
707 array(
708 'label' => 'WP Multisite',
709 'value' => is_multisite() ? 'Enabled' : 'Disabled',
710 'status' => 'none',
711 ),
712 array(
713 'label' => 'WP Memory Limit',
714 'value' => $memory_limit,
715 'status' => (int) $memory_limit >= 256 ? 'ok' : 'error',
716 ),
717 array(
718 'label' => 'WP Path',
719 'value' => ABSPATH,
720 'status' => 'none',
721 ),
722 array(
723 'label' => 'Writable Uploads Folder',
724 'value' => $upload_writable ? 'Writable' : 'Not Writable',
725 'status' => $upload_writable ? 'ok' : 'error',
726 ),
727 array(
728 'label' => 'WP Debug Mode',
729 'value' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Enabled' : 'Disabled',
730 'status' => 'none',
731 ),
732 array(
733 'label' => 'Language',
734 'value' => get_locale(),
735 'status' => 'none',
736 ),
737 );
738
739 // Server Requirements
740 $php_version = function_exists( 'phpversion' ) ? phpversion() : 'N/A';
741 $php_memory_limit = ini_get( 'memory_limit' ) ?: 'N/A';
742 $post_max_size = ini_get( 'post_max_size' ) ?: 'N/A';
743 $time_limit = ini_get( 'max_execution_time' ) ?: 'N/A';
744 $max_input_vars = ini_get( 'max_input_vars' ) ?: 'N/A';
745 $mysql_version = $wpdb->db_version();
746 $max_upload_size = size_format( wp_max_upload_size() );
747
748 $server = array(
749 array(
750 'label' => 'Server Info',
751 'value' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'N/A',
752 'status' => 'none',
753 ),
754 array(
755 'label' => 'PHP Version',
756 'value' => $php_version,
757 'status' => version_compare( $php_version, '5.6', '>=' ) ? 'ok' : 'error',
758 ),
759 array(
760 'label' => 'PHP Memory Limit',
761 'value' => $php_memory_limit,
762 'status' => (int) $php_memory_limit >= 256 ? 'ok' : 'error',
763 ),
764 array(
765 'label' => 'PHP Post Max Size',
766 'value' => $post_max_size,
767 'status' => (int) $post_max_size >= 32 ? 'ok' : 'error',
768 ),
769 array(
770 'label' => 'PHP Time Limit',
771 'value' => $time_limit,
772 'status' => ( (int) $time_limit >= 120 || (int) $time_limit === 0 ) ? 'ok' : 'error',
773 ),
774 array(
775 'label' => 'PHP Max Input Vars',
776 'value' => $max_input_vars,
777 'status' => (int) $max_input_vars >= 1000 ? 'ok' : 'error',
778 ),
779 array(
780 'label' => 'MySQL Version',
781 'value' => $mysql_version,
782 'status' => version_compare( $mysql_version, '5.3', '>=' ) ? 'ok' : 'error',
783 ),
784 array(
785 'label' => 'Max Upload Size',
786 'value' => $max_upload_size,
787 'status' => (int) $max_upload_size >= 20 ? 'ok' : 'error',
788 ),
789 );
790
791 // PHP Extensions
792 $curl_ok = function_exists( 'curl_init' );
793 $fsock_ok = function_exists( 'fsockopen' );
794 $soap_ok = class_exists( 'SoapClient' );
795 $suhosin_ok = extension_loaded( 'suhosin' );
796
797 $php_extensions = array(
798 array(
799 'label' => 'cURL',
800 'value' => $curl_ok ? 'Supported' : 'Not Installed',
801 'status' => $curl_ok ? 'ok' : 'error',
802 ),
803 array(
804 'label' => 'fsockopen',
805 'value' => $fsock_ok ? 'Supported' : 'Not Installed',
806 'status' => $fsock_ok ? 'ok' : 'error',
807 ),
808 array(
809 'label' => 'SOAP Client',
810 'value' => $soap_ok ? 'Supported' : 'Not Installed',
811 'status' => $soap_ok ? 'ok' : 'error',
812 ),
813 array(
814 'label' => 'Suhosin',
815 'value' => $suhosin_ok ? 'Supported' : 'Not Installed',
816 'status' => $suhosin_ok ? 'ok' : 'error',
817 ),
818 );
819
820 // Active Plugins
821 // Use get_plugins() + active list instead of get_plugin_data() per file.
822 // get_plugins() is internally cached by WP and reads all headers in one pass.
823 if ( ! function_exists( 'get_plugins' ) ) {
824 require_once ABSPATH . 'wp-admin/includes/plugin.php';
825 }
826
827 $all_plugins = get_plugins();
828 $active_plugins_list = (array) get_option( 'active_plugins', array() );
829
830 if ( is_multisite() ) {
831 $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
832 $active_plugins_list = array_merge( $active_plugins_list, $network_plugins );
833 }
834
835 $active_plugins = array();
836 foreach ( $active_plugins_list as $plugin_file ) {
837 if ( ! isset( $all_plugins[ $plugin_file ] ) ) {
838 continue;
839 }
840
841 $p = $all_plugins[ $plugin_file ];
842
843 $active_plugins[] = array(
844 'name' => $p['Name'],
845 'author' => wp_strip_all_tags( $p['Author'] ),
846 'version' => $p['Version'],
847 'plugin_url' => ! empty( $p['PluginURI'] ) ? $p['PluginURI'] : '',
848 'author_url' => ! empty( $p['AuthorURI'] ) ? $p['AuthorURI'] : '',
849 );
850 }
851
852 $data = array(
853 'wordpress' => $wordpress,
854 'server' => $server,
855 'php_extensions' => $php_extensions,
856 'active_plugins' => $active_plugins,
857 );
858
859 // Cache the result for 5 minutes.
860 set_transient( $cache_key, $data, $cache_time );
861
862 return new \WP_REST_Response(
863 array(
864 'success' => true,
865 'data' => $data,
866 )
867 );
868 }
869
870 /**
871 * Save dark mode preference.
872 *
873 * @param \WP_REST_Request $request Request object.
874 * @return \WP_REST_Response
875 */
876 public function save_dark_mode( $request ) {
877 $mode = $request->get_param( 'mode' );
878 $user_id = get_current_user_id();
879
880 if ( ! in_array( $mode, array( 'dark', 'light', 'auto' ), true ) ) {
881 $mode = 'auto';
882 }
883
884 update_user_meta( $user_id, 'jltma_dark_mode', $mode );
885
886 return new \WP_REST_Response(
887 array(
888 'success' => true,
889 'data' => array( 'mode' => $mode ),
890 )
891 );
892 }
893
894 /**
895 * Handle setup complete REST API request.
896 *
897 * Saves the current wizard step progress and optional site type.
898 * When step is "done", marks the wizard as fully complete.
899 *
900 * @param \WP_REST_Request $request Request object.
901 * @return \WP_REST_Response
902 */
903 public function handle_setup_complete( $request ) {
904 $step = sanitize_text_field( $request->get_param( 'step' ) );
905 $site_type = sanitize_text_field( $request->get_param( 'siteType' ) );
906
907 // Save current step progress.
908 if ( ! empty( $step ) ) {
909 update_option( self::SETUP_STEP_OPTION, $step );
910 }
911
912 // Save site type if provided.
913 if ( ! empty( $site_type ) ) {
914 update_option( self::SETUP_SITE_TYPE_OPTION, $site_type );
915 }
916
917 // Mark setup as complete when step is "done".
918 if ( 'done' === $step ) {
919 update_option( self::SETUP_COMPLETE_OPTION, true );
920 }
921
922 return rest_ensure_response(
923 array(
924 'success' => true,
925 'message' => __( 'Setup progress saved.', 'master-addons' ),
926 )
927 );
928 }
929
930 /**
931 * Get setup status REST API handler.
932 *
933 * Returns the current step, site type, and completion status.
934 *
935 * @return \WP_REST_Response
936 */
937 public function get_setup_status() {
938 return rest_ensure_response(
939 array(
940 'step' => get_option( self::SETUP_STEP_OPTION, null ),
941 'siteType' => get_option( self::SETUP_SITE_TYPE_OPTION, null ),
942 'isComplete' => self::is_setup_complete(),
943 )
944 );
945 }
946
947 /**
948 * Check if setup wizard is complete.
949 *
950 * Returns true if the saved step is "done" or the legacy complete flag is set.
951 *
952 * @return bool
953 */
954 public static function is_setup_complete() {
955 $step = get_option( self::SETUP_STEP_OPTION, null );
956 if ( 'done' === $step ) {
957 return true;
958 }
959 return (bool) get_option( self::SETUP_COMPLETE_OPTION, false );
960 }
961
962 /**
963 * Get theme status (not_installed, installed, or active).
964 *
965 * @param \WP_REST_Request $request Request object.
966 * @return \WP_REST_Response
967 */
968 public function get_theme_status( $request ) {
969 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
970
971 if ( empty( $slug ) ) {
972 return new \WP_REST_Response(
973 array(
974 'success' => false,
975 'error' => array(
976 'code' => 'missing_slug',
977 'message' => __( 'Theme slug is required.', 'master-addons' ),
978 ),
979 ),
980 400
981 );
982 }
983
984 $theme = wp_get_theme( $slug );
985 $active_theme = wp_get_theme();
986
987 if ( $active_theme->get_stylesheet() === $slug ) {
988 $status = 'active';
989 } elseif ( $theme->exists() ) {
990 $status = 'installed';
991 } else {
992 $status = 'not_installed';
993 }
994
995 return rest_ensure_response(
996 array(
997 'success' => true,
998 'data' => array( 'status' => $status ),
999 )
1000 );
1001 }
1002
1003 /**
1004 * Install a theme from wordpress.org by slug.
1005 *
1006 * @param \WP_REST_Request $request Request object.
1007 * @return \WP_REST_Response
1008 */
1009 public function install_theme( $request ) {
1010 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
1011
1012 if ( empty( $slug ) ) {
1013 return new \WP_REST_Response(
1014 array(
1015 'success' => false,
1016 'error' => array(
1017 'code' => 'missing_slug',
1018 'message' => __( 'Theme slug is required.', 'master-addons' ),
1019 ),
1020 ),
1021 400
1022 );
1023 }
1024
1025 // Check if already installed.
1026 $theme = wp_get_theme( $slug );
1027 if ( $theme->exists() ) {
1028 return rest_ensure_response(
1029 array(
1030 'success' => true,
1031 'message' => __( 'Theme is already installed.', 'master-addons' ),
1032 'data' => array( 'status' => 'installed' ),
1033 )
1034 );
1035 }
1036
1037 require_once ABSPATH . 'wp-admin/includes/file.php';
1038 require_once ABSPATH . 'wp-admin/includes/misc.php';
1039 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1040 require_once ABSPATH . 'wp-admin/includes/theme.php';
1041
1042 // Get theme info from wordpress.org API.
1043 $api = themes_api(
1044 'theme_information',
1045 array(
1046 'slug' => $slug,
1047 'fields' => array( 'sections' => false ),
1048 )
1049 );
1050
1051 if ( is_wp_error( $api ) ) {
1052 return new \WP_REST_Response(
1053 array(
1054 'success' => false,
1055 'error' => array(
1056 'code' => 'api_error',
1057 'message' => $api->get_error_message(),
1058 ),
1059 ),
1060 500
1061 );
1062 }
1063
1064 $skin = new \WP_Ajax_Upgrader_Skin();
1065 $upgrader = new \Theme_Upgrader( $skin );
1066 $result = $upgrader->install( $api->download_link );
1067
1068 if ( is_wp_error( $result ) ) {
1069 return new \WP_REST_Response(
1070 array(
1071 'success' => false,
1072 'error' => array(
1073 'code' => 'install_error',
1074 'message' => $result->get_error_message(),
1075 ),
1076 ),
1077 500
1078 );
1079 }
1080
1081 if ( ! $result ) {
1082 // Check skin for errors.
1083 $errors = $skin->get_errors();
1084 $msg = is_wp_error( $errors ) ? $errors->get_error_message() : __( 'Theme installation failed.', 'master-addons' );
1085
1086 return new \WP_REST_Response(
1087 array(
1088 'success' => false,
1089 'error' => array(
1090 'code' => 'install_failed',
1091 'message' => $msg,
1092 ),
1093 ),
1094 500
1095 );
1096 }
1097
1098 return rest_ensure_response(
1099 array(
1100 'success' => true,
1101 'message' => __( 'Theme installed successfully.', 'master-addons' ),
1102 'data' => array( 'status' => 'installed' ),
1103 )
1104 );
1105 }
1106
1107 /**
1108 * Activate an installed theme by slug.
1109 *
1110 * @param \WP_REST_Request $request Request object.
1111 * @return \WP_REST_Response
1112 */
1113 public function activate_theme( $request ) {
1114 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
1115
1116 if ( empty( $slug ) ) {
1117 return new \WP_REST_Response(
1118 array(
1119 'success' => false,
1120 'error' => array(
1121 'code' => 'missing_slug',
1122 'message' => __( 'Theme slug is required.', 'master-addons' ),
1123 ),
1124 ),
1125 400
1126 );
1127 }
1128
1129 $theme = wp_get_theme( $slug );
1130
1131 if ( ! $theme->exists() ) {
1132 return new \WP_REST_Response(
1133 array(
1134 'success' => false,
1135 'error' => array(
1136 'code' => 'not_installed',
1137 'message' => __( 'Theme is not installed.', 'master-addons' ),
1138 ),
1139 ),
1140 404
1141 );
1142 }
1143
1144 switch_theme( $slug );
1145
1146 return rest_ensure_response(
1147 array(
1148 'success' => true,
1149 'message' => __( 'Theme activated successfully.', 'master-addons' ),
1150 'data' => array( 'status' => 'active' ),
1151 )
1152 );
1153 }
1154
1155 }
1156