@@ -50,7 +50,26 @@ public function register_routes() {
5050 'permission_callback ' => array ( $ this , 'get_items_permissions_check ' ),
5151 'args ' => $ this ->get_collection_params (),
5252 ),
53- 'schema ' => array ( $ this , 'get_item_schema ' ),
53+ array (
54+ 'methods ' => WP_REST_Server::CREATABLE ,
55+ 'callback ' => array ( $ this , 'create_item ' ),
56+ 'permission_callback ' => array ( $ this , 'create_item_permissions_check ' ),
57+ 'args ' => array (
58+ 'slug ' => array (
59+ 'type ' => 'string ' ,
60+ 'required ' => true ,
61+ 'description ' => __ ( 'WordPress.org theme directory slug. ' ),
62+ 'pattern ' => '[\w\-]+ ' ,
63+ ),
64+ 'status ' => array (
65+ 'description ' => __ ( 'The theme activation status. ' ),
66+ 'type ' => 'string ' ,
67+ 'enum ' => array ( 'inactive ' , 'active ' ),
68+ 'default ' => 'inactive ' ,
69+ ),
70+ ),
71+ ),
72+ 'schema ' => array ( $ this , 'get_public_item_schema ' ),
5473 )
5574 );
5675
@@ -70,6 +89,17 @@ public function register_routes() {
7089 'callback ' => array ( $ this , 'get_item ' ),
7190 'permission_callback ' => array ( $ this , 'get_item_permissions_check ' ),
7291 ),
92+ array (
93+ 'methods ' => WP_REST_Server::EDITABLE ,
94+ 'callback ' => array ( $ this , 'update_item ' ),
95+ 'permission_callback ' => array ( $ this , 'update_item_permissions_check ' ),
96+ 'args ' => $ this ->get_endpoint_args_for_item_schema ( WP_REST_Server::EDITABLE ),
97+ ),
98+ array (
99+ 'methods ' => WP_REST_Server::DELETABLE ,
100+ 'callback ' => array ( $ this , 'delete_item ' ),
101+ 'permission_callback ' => array ( $ this , 'delete_item_permissions_check ' ),
102+ ),
73103 'schema ' => array ( $ this , 'get_public_item_schema ' ),
74104 )
75105 );
@@ -430,6 +460,56 @@ protected function is_same_theme( $theme_a, $theme_b ) {
430460 return $ theme_a ->get_stylesheet () === $ theme_b ->get_stylesheet ();
431461 }
432462
463+ /**
464+ * Checks read permission for a given theme.
465+ *
466+ * @since 6.9.0
467+ *
468+ * @param string $theme_slug Theme slug to check.
469+ * @return true|WP_Error
470+ */
471+ protected function check_read_permission ( $ theme_slug ) {
472+ $ theme = wp_get_theme ( $ theme_slug );
473+ if ( ! $ theme ->exists () ) {
474+ return new WP_Error ( 'rest_theme_not_found ' , __ ( 'Theme not found. ' ), array ( 'status ' => 404 ) );
475+ }
476+ return true ;
477+ }
478+
479+ /**
480+ * Matches request with the theme.
481+ *
482+ * @since 6.9.0
483+ *
484+ * @param WP_REST_Request $request Request data.
485+ * @param WP_Theme $theme Theme object.
486+ * @return bool
487+ */
488+ protected function does_theme_match_request ( $ request , $ theme ) {
489+ $ status = $ request ['status ' ];
490+ $ theme_status = ( $ this ->is_same_theme ( $ theme , wp_get_theme () ) ) ? 'active ' : 'inactive ' ;
491+ if ( ! is_array ( $ status ) || in_array ( $ theme_status , $ status , true ) ) {
492+ return true ;
493+ }
494+ return false ;
495+ }
496+
497+ /**
498+ * Retrieves theme data.
499+ *
500+ * @since 6.9.0
501+ *
502+ * @param string $theme_slug Theme slug to be retrieved.
503+ * @return WP_Theme|WP_Error
504+ */
505+ protected function get_theme_data ( $ theme_slug ) {
506+ $ theme = wp_get_theme ( $ theme_slug );
507+ if ( ! $ theme ->exists () ) {
508+ return new WP_Error ( 'rest_theme_not_found ' , __ ( 'Theme not found. ' ), array ( 'status ' => 404 ) );
509+ }
510+ return $ theme ;
511+ }
512+
433513 /**
434514 * Prepares the theme support value for inclusion in the REST API response.
435515 *
@@ -632,6 +712,7 @@ public function get_item_schema() {
632712 'description ' => __ ( 'A named status for the theme. ' ),
633713 'type ' => 'string ' ,
634714 'enum ' => array ( 'inactive ' , 'active ' ),
715+ 'context ' => array ( 'view ' , 'edit ' ),
635716 ),
636717 'default_template_types ' => array (
637718 'description ' => __ ( 'A list of default template types. ' ),
@@ -750,4 +831,253 @@ public function sanitize_theme_status( $statuses, $request, $parameter ) {
750831
751832 return $ statuses ;
752833 }
834+ /**
835+ * Checks permissions to update a theme (switch themes).
836+ *
837+ * @since 6.9.0
838+ *
839+ * @param WP_REST_Request $request Full data about the request.
840+ * @return true|WP_Error True if the request has access to edit items, WP_Error object otherwise.
841+ */
842+ public function update_item_permissions_check ( $ request ) {
843+ if ( ! current_user_can ( 'switch_themes ' ) ) {
844+ return new WP_Error ( 'rest_forbidden ' , __ ( 'Sorry, you are not allowed to switch themes for this site. ' ), array ( 'status ' => rest_authorization_required_code () ) );
845+ }
846+ return true ;
847+ }
848+
849+ /**
850+ * Update a theme (switch themes).
851+ *
852+ * @since 6.9.0
853+ *
854+ * @param WP_REST_Request $request Full data about the request.
855+ * @return WP_Error|WP_REST_Response
856+ */
857+ public function update_item ( $ request ) {
858+ $ theme_slug = $ request ['theme ' ];
859+
860+ $ theme = $ this ->get_theme_data ( $ theme_slug );
861+ if ( is_wp_error ( $ theme ) ) {
862+ return $ theme ;
863+ }
864+
865+ // Check if we need to switch the theme
866+ if ( isset ( $ request ['status ' ] ) && 'active ' === $ request ['status ' ] ) {
867+ switch_theme ( $ theme_slug );
868+ }
869+
870+ return $ this ->prepare_item_for_response ( $ theme , $ request );
871+ }
872+
873+ /**
874+ * Checks permissions to install a theme.
875+ *
876+ * @since 6.9.0
877+ *
878+ * @param WP_REST_Request $request Full data about the request.
879+ * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
880+ */
881+ public function create_item_permissions_check ( $ request ) {
882+ if ( ! current_user_can ( 'install_themes ' ) ) {
883+ return new WP_Error ( 'rest_forbidden ' , __ ( 'Sorry, you are not allowed to install themes on this site. ' ), array ( 'status ' => rest_authorization_required_code () ) );
884+ }
885+ return true ;
886+ }
887+
888+ /**
889+ * Install a theme.
890+ *
891+ * @since 6.9.0
892+ *
893+ * @param WP_REST_Request $request Full data about the request.
894+ * @return WP_Error|WP_REST_Response
895+ */
896+ public function create_item ( $ request ) {
897+ $ slug = $ request ['slug ' ];
898+ require_once ABSPATH . 'wp-admin/includes/theme-install.php ' ;
899+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php ' ;
900+
901+ // Verify filesystem is accessible first.
902+ $ filesystem_available = $ this ->is_filesystem_available ();
903+ if ( is_wp_error ( $ filesystem_available ) ) {
904+ return $ filesystem_available ;
905+ }
906+
907+ // Get theme information from WordPress.org API.
908+ $ api = themes_api (
909+ 'theme_information ' ,
910+ array (
911+ 'slug ' => $ slug ,
912+ 'fields ' => array ( 'sections ' => false ),
913+ )
914+ );
915+ if ( is_wp_error ( $ api ) ) {
916+ if ( str_contains ( $ api ->get_error_message (), 'Theme not found. ' ) ) {
917+ $ api ->add_data ( array ( 'status ' => 404 ) );
918+ } else {
919+ $ api ->add_data ( array ( 'status ' => 500 ) );
920+ }
921+ return $ api ;
922+ }
923+
924+ $ skin = new WP_Ajax_Upgrader_Skin ();
925+ $ upgrader = new Theme_Upgrader ( $ skin );
926+
927+ $ result = $ upgrader ->install ( $ api ->download_link );
928+ if ( is_wp_error ( $ result ) ) {
929+ $ result ->add_data ( array ( 'status ' => 500 ) );
930+ return $ result ;
931+ }
932+
933+ // Get the installed theme file
934+ $ theme_file = $ upgrader ->theme_info ();
935+ if ( ! $ theme_file ) {
936+ return new WP_Error (
937+ 'unable_to_determine_installed_theme ' ,
938+ __ ( 'Unable to determine what theme was installed. ' ),
939+ array ( 'status ' => 500 )
940+ );
941+ }
942+
943+ $ theme = wp_get_theme ( $ theme_file );
944+
945+ // Check if we need to activate the theme
946+ if ( isset ( $ request ['status ' ] ) && 'active ' === $ request ['status ' ] ) {
947+ switch_theme ( $ theme_file );
948+ }
949+
950+ $ response = $ this ->prepare_item_for_response ( $ theme , $ request );
951+ $ response ->set_status ( 201 );
952+ $ response ->header ( 'Location ' , rest_url ( sprintf ( '%s/%s/%s ' , $ this ->namespace , $ this ->rest_base , $ theme_file ) ) );
953+
954+ return $ response ;
955+ }
956+
957+
958+ /**
959+ * Checks if the filesystem is available to perform theme installations.
960+ *
961+ * @since 6.9.0
962+ *
963+ * @return true|WP_Error True if the filesystem is available, or an error object if not.
964+ */
965+ protected function is_filesystem_available () {
966+ $ filesystem_method = get_filesystem_method ();
967+
968+ if ( 'direct ' === $ filesystem_method ) {
969+ return true ;
970+ }
971+
972+ ob_start ();
973+ $ credentials = request_filesystem_credentials ( site_url () );
974+ ob_end_clean ();
975+
976+ if ( $ credentials ) {
977+ return true ;
978+ }
979+
980+ return new WP_Error ( 'fs_unavailable ' , __ ( 'Could not access filesystem. ' ), array ( 'status ' => 500 ) );
981+ }
982+
983+ /**
984+ * Checks if a given request has access to delete a theme.
985+ *
986+ * @since 6.9.0
987+ *
988+ * @param WP_REST_Request $request Full details about the request.
989+ * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
990+ */
991+ public function delete_item_permissions_check ( $ request ) {
992+ if ( ! current_user_can ( 'delete_themes ' ) ) {
993+ return new WP_Error (
994+ 'rest_cannot_delete_theme ' ,
995+ __ ( 'Sorry, you are not allowed to delete themes for this site. ' ),
996+ array ( 'status ' => rest_authorization_required_code () )
997+ );
998+ }
999+
1000+ $ theme = $ this ->get_theme_data ( $ request ['theme ' ] );
1001+ if ( is_wp_error ( $ theme ) ) {
1002+ return $ theme ;
1003+ }
1004+
1005+ // Cannot delete active theme.
1006+ if ( $ this ->is_same_theme ( $ theme , wp_get_theme () ) ) {
1007+ return new WP_Error (
1008+ 'rest_cannot_delete_active_theme ' ,
1009+ __ ( 'Cannot delete the active theme. ' ),
1010+ array ( 'status ' => 409 )
1011+ );
1012+ }
1013+
1014+ return true ;
1015+ }
1016+
1017+ /**
1018+ * Deletes a theme.
1019+ *
1020+ * @since 6.9.0
1021+ *
1022+ * @param WP_REST_Request $request Full details about the request.
1023+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
1024+ */
1025+ public function delete_item ( $ request ) {
1026+ $ theme = $ this ->get_theme_data ( $ request ['theme ' ] );
1027+ if ( is_wp_error ( $ theme ) ) {
1028+ return $ theme ;
1029+ }
1030+
1031+ // Verify filesystem is accessible first.
1032+ $ filesystem_available = $ this ->is_filesystem_available ();
1033+ if ( is_wp_error ( $ filesystem_available ) ) {
1034+ return $ filesystem_available ;
1035+ }
1036+
1037+ require_once ABSPATH . 'wp-admin/includes/theme.php ' ;
1038+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php ' ;
1039+
1040+ $ previous_theme_data = $ this ->prepare_item_for_response ( $ theme , $ request );
1041+
1042+ $ result = delete_theme ( $ theme ->get_stylesheet () );
1043+ if ( is_wp_error ( $ result ) ) {
1044+ $ result ->add_data ( array ( 'status ' => 500 ) );
1045+ return $ result ;
1046+ }
1047+
1048+ return new WP_REST_Response (
1049+ array (
1050+ 'deleted ' => true ,
1051+ 'previous ' => $ previous_theme_data ->get_data (),
1052+ ),
1053+ 200
1054+ );
1055+ }
1056+
1057+ /**
1058+ * Validates theme param.
1059+ *
1060+ * @since 6.9.0
1061+ *
1062+ * @param string $value Theme parameter value.
1063+ * @return true|WP_Error
1064+ */
1065+ public function validate_theme_param ( $ value ) {
1066+ if ( ! is_string ( $ value ) || empty ( $ value ) ) {
1067+ return new WP_Error ( 'rest_invalid_theme ' , __ ( 'Invalid theme. ' ), array ( 'status ' => 400 ) );
1068+ }
1069+ return true ;
1070+ }
1071+
1072+ /**
1073+ * Sanitizes theme param.
1074+ *
1075+ * @since 6.9.0
1076+ *
1077+ * @param string $value Theme parameter value.
1078+ * @return string
1079+ */
1080+ public function sanitize_theme_param ( $ value ) {
1081+ return urldecode ( $ value );
1082+ }
7531083}
0 commit comments