PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / abilities / zipai / system / theme-delete.php

theme-delete.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at inc/abilities/zipai/system/theme-delete.php

184 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Theme Delete — server-side execution.
4 *
5 * Removes a theme's files in-process via WP core's `delete_theme()` under the
6 * App-Password user's identity. Mirrors `PluginDelete`. The theme MUST NOT be
7 * the active theme (as stylesheet or as the parent template of an active
8 * child) — that is refused here with a clear error before any filesystem work.
9 *
10 * Pre-App-Password versions returned a js_hook envelope and deferred to the
11 * browser's `admin-ajax.php?action=delete-theme`. Server-side execution under
12 * a user-bound credential returns the real success/error in one round-trip.
13 *
14 * @since 0.0.5
15 * @package zip-ai
16 */
17
18 namespace ZipAI\MCP\Classes\Abilities\Zipai\System;
19
20 use ZipAI\MCP\Classes\Abilities\Abstract_Ability;
21 use ZipAI\MCP\Classes\Core\Response;
22 use ZipAI\MCP\Classes\Core\Tool_Types;
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 class ThemeDelete extends Abstract_Ability {
29
30 /**
31 * Flags this ability as destructive (mutates site state).
32 *
33 * @var bool
34 */
35 protected $is_destructive = true;
36
37 /**
38 * Configures the ability's id, label, description and metadata.
39 *
40 * @return void
41 */
42 public function configure() {
43 $this->id = 'zipai/delete-theme';
44 $this->label = 'Delete Theme';
45 $this->description = 'Permanently delete a theme\'s files. The theme must not be active — switch to a different theme first if needed. '
46 . 'Runs in-process via `delete_theme()` under the App-Password user\'s identity. '
47 . 'Requires the `delete_themes` capability. Returns synchronously with the actual result.';
48 $this->capability = 'delete_themes';
49
50 $this->meta = array(
51 'tool_type' => Tool_Types::DELETE,
52 );
53 }
54
55 /**
56 * Returns the tool-type classification for this ability.
57 *
58 * @return string One of the Tool_Types constants.
59 */
60 public function get_tool_type() {
61 return Tool_Types::DELETE;
62 }
63
64 /**
65 * Returns the JSON Schema for this ability's input arguments.
66 *
67 * @return array<string,mixed> JSON Schema describing accepted arguments.
68 */
69 public function get_input_schema() {
70 return array(
71 'type' => 'object',
72 'required' => array( 'slug' ),
73 'additionalProperties' => false,
74 'properties' => array(
75 'slug' => array(
76 'type' => 'string',
77 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9_-]*$',
78 'description' => 'Theme stylesheet slug ("twentytwentyfour"). The folder name under wp-content/themes/.',
79 ),
80 ),
81 );
82 }
83
84 /**
85 * Deletes a non-active theme's files by slug.
86 *
87 * @param array<string,mixed> $args Validated input arguments.
88 * @return array<string,mixed> Standardized success or error response.
89 */
90 public function execute( $args ) {
91 $raw_slug = isset( $args['slug'] ) && is_string( $args['slug'] ) ? $args['slug'] : '';
92 $slug = trim( $raw_slug );
93 if ( '' === $slug || ! preg_match( '/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/', $slug ) ) {
94 return Response::error( 'Invalid theme slug. Use the theme folder name under wp-content/themes/.' );
95 }
96
97 if ( ! wp_is_file_mod_allowed( 'zipai_delete_theme' ) ) {
98 return Response::error( 'Theme deletion is disabled on this site (DISALLOW_FILE_MODS or the file_mod_allowed filter).' );
99 }
100 if ( is_multisite() && ! is_super_admin() ) {
101 return Response::error( 'On multisite, only network super-admins may delete themes.' );
102 }
103
104 if ( ! function_exists( 'delete_theme' ) ) {
105 require_once ABSPATH . 'wp-admin/includes/file.php';
106 require_once ABSPATH . 'wp-admin/includes/theme.php';
107 }
108
109 $theme = wp_get_theme( $slug );
110 if ( ! $theme->exists() ) {
111 return Response::error( sprintf( 'Theme "%s" is not installed.', $slug ) );
112 }
113
114 // Never delete the active theme — neither the active stylesheet nor the
115 // parent template of an active child theme. WP core refuses this too;
116 // stopping it here gives the caller a clear reason instead of a generic
117 // failure.
118 $active = wp_get_theme();
119 if (
120 $theme->get_stylesheet() === $active->get_stylesheet()
121 || $theme->get_stylesheet() === $active->get_template()
122 ) {
123 return Response::error( sprintf( 'Cannot delete the active theme "%s". Switch to another theme first.', $slug ) );
124 }
125
126 if ( ! WP_Filesystem() ) {
127 return Response::error(
128 'Filesystem credentials required for theme deletion. '
129 . 'Configure FS_METHOD or store FTP credentials in wp-config.php.'
130 );
131 }
132
133 $result = delete_theme( $theme->get_stylesheet() );
134
135 if ( is_wp_error( $result ) ) {
136 return Response::error( sprintf( 'Delete failed: %s', $result->get_error_message() ) );
137 }
138 // delete_theme() returns null when filesystem credentials are required
139 // and false on an empty/invalid stylesheet — surface both as failures.
140 if ( null === $result ) {
141 return Response::error( 'Theme deletion requires filesystem credentials that are not available.' );
142 }
143 if ( true !== $result ) {
144 return Response::error( 'Theme deletion did not complete successfully.' );
145 }
146
147 wp_clean_themes_cache();
148
149 // Confirm the files are actually gone rather than trusting the return.
150 if ( wp_get_theme( $slug )->exists() ) {
151 return Response::error( sprintf( 'Theme "%s" still present after deletion.', $slug ) );
152 }
153
154 return array(
155 'success' => true,
156 'message' => sprintf( 'Theme "%s" deleted.', $slug ),
157 'data' => array( 'slug' => $slug ),
158 );
159 }
160
161 /**
162 * Returns the JSON Schema for this ability's response.
163 *
164 * @return array<string,mixed> JSON Schema describing the response shape.
165 */
166 public function get_output_schema() {
167 return array(
168 'type' => 'object',
169 'required' => array( 'success' ),
170 'additionalProperties' => true,
171 'properties' => array(
172 'success' => array( 'type' => 'boolean' ),
173 'message' => array( 'type' => 'string' ),
174 'data' => array(
175 'type' => 'object',
176 'properties' => array(
177 'slug' => array( 'type' => 'string' ),
178 ),
179 ),
180 ),
181 );
182 }
183 }
184