Skip to content

Commit dd451c9

Browse files
committed
REST API: Add install, activate, and delete endpoints to Themes controller
1 parent 063a74f commit dd451c9

File tree

1 file changed

+331
-1
lines changed

1 file changed

+331
-1
lines changed

src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php

Lines changed: 331 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)