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

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

1,157 lines 28.5 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 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only writability probe of the uploads directory for a diagnostics report.
690 $upload_writable = is_writable( $uploads['basedir'] );
691
692 $wordpress = array(
693 array(
694 'label' => 'Home URL',
695 'value' => home_url(),
696 'status' => 'none',
697 ),
698 array(
699 'label' => 'Site URL',
700 'value' => site_url(),
701 'status' => 'none',
702 ),
703 array(
704 'label' => 'WP Version',
705 'value' => $wp_version,
706 'status' => version_compare( $wp_version, '4.0', '>=' ) ? 'ok' : 'error',
707 ),
708 array(
709 'label' => 'WP Multisite',
710 'value' => is_multisite() ? 'Enabled' : 'Disabled',
711 'status' => 'none',
712 ),
713 array(
714 'label' => 'WP Memory Limit',
715 'value' => $memory_limit,
716 'status' => (int) $memory_limit >= 256 ? 'ok' : 'error',
717 ),
718 array(
719 'label' => 'WP Path',
720 'value' => ABSPATH,
721 'status' => 'none',
722 ),
723 array(
724 'label' => 'Writable Uploads Folder',
725 'value' => $upload_writable ? 'Writable' : 'Not Writable',
726 'status' => $upload_writable ? 'ok' : 'error',
727 ),
728 array(
729 'label' => 'WP Debug Mode',
730 'value' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Enabled' : 'Disabled',
731 'status' => 'none',
732 ),
733 array(
734 'label' => 'Language',
735 'value' => get_locale(),
736 'status' => 'none',
737 ),
738 );
739
740 // Server Requirements
741 $php_version = function_exists( 'phpversion' ) ? phpversion() : 'N/A';
742 $php_memory_limit = ini_get( 'memory_limit' ) ?: 'N/A';
743 $post_max_size = ini_get( 'post_max_size' ) ?: 'N/A';
744 $time_limit = ini_get( 'max_execution_time' ) ?: 'N/A';
745 $max_input_vars = ini_get( 'max_input_vars' ) ?: 'N/A';
746 $mysql_version = $wpdb->db_version();
747 $max_upload_size = size_format( wp_max_upload_size() );
748
749 $server = array(
750 array(
751 'label' => 'Server Info',
752 'value' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'N/A',
753 'status' => 'none',
754 ),
755 array(
756 'label' => 'PHP Version',
757 'value' => $php_version,
758 'status' => version_compare( $php_version, '5.6', '>=' ) ? 'ok' : 'error',
759 ),
760 array(
761 'label' => 'PHP Memory Limit',
762 'value' => $php_memory_limit,
763 'status' => (int) $php_memory_limit >= 256 ? 'ok' : 'error',
764 ),
765 array(
766 'label' => 'PHP Post Max Size',
767 'value' => $post_max_size,
768 'status' => (int) $post_max_size >= 32 ? 'ok' : 'error',
769 ),
770 array(
771 'label' => 'PHP Time Limit',
772 'value' => $time_limit,
773 'status' => ( (int) $time_limit >= 120 || (int) $time_limit === 0 ) ? 'ok' : 'error',
774 ),
775 array(
776 'label' => 'PHP Max Input Vars',
777 'value' => $max_input_vars,
778 'status' => (int) $max_input_vars >= 1000 ? 'ok' : 'error',
779 ),
780 array(
781 'label' => 'MySQL Version',
782 'value' => $mysql_version,
783 'status' => version_compare( $mysql_version, '5.3', '>=' ) ? 'ok' : 'error',
784 ),
785 array(
786 'label' => 'Max Upload Size',
787 'value' => $max_upload_size,
788 'status' => (int) $max_upload_size >= 20 ? 'ok' : 'error',
789 ),
790 );
791
792 // PHP Extensions
793 $curl_ok = function_exists( 'curl_init' );
794 $fsock_ok = function_exists( 'fsockopen' );
795 $soap_ok = class_exists( 'SoapClient' );
796 $suhosin_ok = extension_loaded( 'suhosin' );
797
798 $php_extensions = array(
799 array(
800 'label' => 'cURL',
801 'value' => $curl_ok ? 'Supported' : 'Not Installed',
802 'status' => $curl_ok ? 'ok' : 'error',
803 ),
804 array(
805 'label' => 'fsockopen',
806 'value' => $fsock_ok ? 'Supported' : 'Not Installed',
807 'status' => $fsock_ok ? 'ok' : 'error',
808 ),
809 array(
810 'label' => 'SOAP Client',
811 'value' => $soap_ok ? 'Supported' : 'Not Installed',
812 'status' => $soap_ok ? 'ok' : 'error',
813 ),
814 array(
815 'label' => 'Suhosin',
816 'value' => $suhosin_ok ? 'Supported' : 'Not Installed',
817 'status' => $suhosin_ok ? 'ok' : 'error',
818 ),
819 );
820
821 // Active Plugins
822 // Use get_plugins() + active list instead of get_plugin_data() per file.
823 // get_plugins() is internally cached by WP and reads all headers in one pass.
824 if ( ! function_exists( 'get_plugins' ) ) {
825 require_once ABSPATH . 'wp-admin/includes/plugin.php';
826 }
827
828 $all_plugins = get_plugins();
829 $active_plugins_list = (array) get_option( 'active_plugins', array() );
830
831 if ( is_multisite() ) {
832 $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
833 $active_plugins_list = array_merge( $active_plugins_list, $network_plugins );
834 }
835
836 $active_plugins = array();
837 foreach ( $active_plugins_list as $plugin_file ) {
838 if ( ! isset( $all_plugins[ $plugin_file ] ) ) {
839 continue;
840 }
841
842 $p = $all_plugins[ $plugin_file ];
843
844 $active_plugins[] = array(
845 'name' => $p['Name'],
846 'author' => wp_strip_all_tags( $p['Author'] ),
847 'version' => $p['Version'],
848 'plugin_url' => ! empty( $p['PluginURI'] ) ? $p['PluginURI'] : '',
849 'author_url' => ! empty( $p['AuthorURI'] ) ? $p['AuthorURI'] : '',
850 );
851 }
852
853 $data = array(
854 'wordpress' => $wordpress,
855 'server' => $server,
856 'php_extensions' => $php_extensions,
857 'active_plugins' => $active_plugins,
858 );
859
860 // Cache the result for 5 minutes.
861 set_transient( $cache_key, $data, $cache_time );
862
863 return new \WP_REST_Response(
864 array(
865 'success' => true,
866 'data' => $data,
867 )
868 );
869 }
870
871 /**
872 * Save dark mode preference.
873 *
874 * @param \WP_REST_Request $request Request object.
875 * @return \WP_REST_Response
876 */
877 public function save_dark_mode( $request ) {
878 $mode = $request->get_param( 'mode' );
879 $user_id = get_current_user_id();
880
881 if ( ! in_array( $mode, array( 'dark', 'light', 'auto' ), true ) ) {
882 $mode = 'auto';
883 }
884
885 update_user_meta( $user_id, 'jltma_dark_mode', $mode );
886
887 return new \WP_REST_Response(
888 array(
889 'success' => true,
890 'data' => array( 'mode' => $mode ),
891 )
892 );
893 }
894
895 /**
896 * Handle setup complete REST API request.
897 *
898 * Saves the current wizard step progress and optional site type.
899 * When step is "done", marks the wizard as fully complete.
900 *
901 * @param \WP_REST_Request $request Request object.
902 * @return \WP_REST_Response
903 */
904 public function handle_setup_complete( $request ) {
905 $step = sanitize_text_field( $request->get_param( 'step' ) );
906 $site_type = sanitize_text_field( $request->get_param( 'siteType' ) );
907
908 // Save current step progress.
909 if ( ! empty( $step ) ) {
910 update_option( self::SETUP_STEP_OPTION, $step );
911 }
912
913 // Save site type if provided.
914 if ( ! empty( $site_type ) ) {
915 update_option( self::SETUP_SITE_TYPE_OPTION, $site_type );
916 }
917
918 // Mark setup as complete when step is "done".
919 if ( 'done' === $step ) {
920 update_option( self::SETUP_COMPLETE_OPTION, true );
921 }
922
923 return rest_ensure_response(
924 array(
925 'success' => true,
926 'message' => __( 'Setup progress saved.', 'master-addons' ),
927 )
928 );
929 }
930
931 /**
932 * Get setup status REST API handler.
933 *
934 * Returns the current step, site type, and completion status.
935 *
936 * @return \WP_REST_Response
937 */
938 public function get_setup_status() {
939 return rest_ensure_response(
940 array(
941 'step' => get_option( self::SETUP_STEP_OPTION, null ),
942 'siteType' => get_option( self::SETUP_SITE_TYPE_OPTION, null ),
943 'isComplete' => self::is_setup_complete(),
944 )
945 );
946 }
947
948 /**
949 * Check if setup wizard is complete.
950 *
951 * Returns true if the saved step is "done" or the legacy complete flag is set.
952 *
953 * @return bool
954 */
955 public static function is_setup_complete() {
956 $step = get_option( self::SETUP_STEP_OPTION, null );
957 if ( 'done' === $step ) {
958 return true;
959 }
960 return (bool) get_option( self::SETUP_COMPLETE_OPTION, false );
961 }
962
963 /**
964 * Get theme status (not_installed, installed, or active).
965 *
966 * @param \WP_REST_Request $request Request object.
967 * @return \WP_REST_Response
968 */
969 public function get_theme_status( $request ) {
970 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
971
972 if ( empty( $slug ) ) {
973 return new \WP_REST_Response(
974 array(
975 'success' => false,
976 'error' => array(
977 'code' => 'missing_slug',
978 'message' => __( 'Theme slug is required.', 'master-addons' ),
979 ),
980 ),
981 400
982 );
983 }
984
985 $theme = wp_get_theme( $slug );
986 $active_theme = wp_get_theme();
987
988 if ( $active_theme->get_stylesheet() === $slug ) {
989 $status = 'active';
990 } elseif ( $theme->exists() ) {
991 $status = 'installed';
992 } else {
993 $status = 'not_installed';
994 }
995
996 return rest_ensure_response(
997 array(
998 'success' => true,
999 'data' => array( 'status' => $status ),
1000 )
1001 );
1002 }
1003
1004 /**
1005 * Install a theme from wordpress.org by slug.
1006 *
1007 * @param \WP_REST_Request $request Request object.
1008 * @return \WP_REST_Response
1009 */
1010 public function install_theme( $request ) {
1011 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
1012
1013 if ( empty( $slug ) ) {
1014 return new \WP_REST_Response(
1015 array(
1016 'success' => false,
1017 'error' => array(
1018 'code' => 'missing_slug',
1019 'message' => __( 'Theme slug is required.', 'master-addons' ),
1020 ),
1021 ),
1022 400
1023 );
1024 }
1025
1026 // Check if already installed.
1027 $theme = wp_get_theme( $slug );
1028 if ( $theme->exists() ) {
1029 return rest_ensure_response(
1030 array(
1031 'success' => true,
1032 'message' => __( 'Theme is already installed.', 'master-addons' ),
1033 'data' => array( 'status' => 'installed' ),
1034 )
1035 );
1036 }
1037
1038 require_once ABSPATH . 'wp-admin/includes/file.php';
1039 require_once ABSPATH . 'wp-admin/includes/misc.php';
1040 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1041 require_once ABSPATH . 'wp-admin/includes/theme.php';
1042
1043 // Get theme info from wordpress.org API.
1044 $api = themes_api(
1045 'theme_information',
1046 array(
1047 'slug' => $slug,
1048 'fields' => array( 'sections' => false ),
1049 )
1050 );
1051
1052 if ( is_wp_error( $api ) ) {
1053 return new \WP_REST_Response(
1054 array(
1055 'success' => false,
1056 'error' => array(
1057 'code' => 'api_error',
1058 'message' => $api->get_error_message(),
1059 ),
1060 ),
1061 500
1062 );
1063 }
1064
1065 $skin = new \WP_Ajax_Upgrader_Skin();
1066 $upgrader = new \Theme_Upgrader( $skin );
1067 $result = $upgrader->install( $api->download_link );
1068
1069 if ( is_wp_error( $result ) ) {
1070 return new \WP_REST_Response(
1071 array(
1072 'success' => false,
1073 'error' => array(
1074 'code' => 'install_error',
1075 'message' => $result->get_error_message(),
1076 ),
1077 ),
1078 500
1079 );
1080 }
1081
1082 if ( ! $result ) {
1083 // Check skin for errors.
1084 $errors = $skin->get_errors();
1085 $msg = is_wp_error( $errors ) ? $errors->get_error_message() : __( 'Theme installation failed.', 'master-addons' );
1086
1087 return new \WP_REST_Response(
1088 array(
1089 'success' => false,
1090 'error' => array(
1091 'code' => 'install_failed',
1092 'message' => $msg,
1093 ),
1094 ),
1095 500
1096 );
1097 }
1098
1099 return rest_ensure_response(
1100 array(
1101 'success' => true,
1102 'message' => __( 'Theme installed successfully.', 'master-addons' ),
1103 'data' => array( 'status' => 'installed' ),
1104 )
1105 );
1106 }
1107
1108 /**
1109 * Activate an installed theme by slug.
1110 *
1111 * @param \WP_REST_Request $request Request object.
1112 * @return \WP_REST_Response
1113 */
1114 public function activate_theme( $request ) {
1115 $slug = sanitize_text_field( $request->get_param( 'slug' ) );
1116
1117 if ( empty( $slug ) ) {
1118 return new \WP_REST_Response(
1119 array(
1120 'success' => false,
1121 'error' => array(
1122 'code' => 'missing_slug',
1123 'message' => __( 'Theme slug is required.', 'master-addons' ),
1124 ),
1125 ),
1126 400
1127 );
1128 }
1129
1130 $theme = wp_get_theme( $slug );
1131
1132 if ( ! $theme->exists() ) {
1133 return new \WP_REST_Response(
1134 array(
1135 'success' => false,
1136 'error' => array(
1137 'code' => 'not_installed',
1138 'message' => __( 'Theme is not installed.', 'master-addons' ),
1139 ),
1140 ),
1141 404
1142 );
1143 }
1144
1145 switch_theme( $slug );
1146
1147 return rest_ensure_response(
1148 array(
1149 'success' => true,
1150 'message' => __( 'Theme activated successfully.', 'master-addons' ),
1151 'data' => array( 'status' => 'active' ),
1152 )
1153 );
1154 }
1155
1156 }
1157