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 / plugin-delete.php

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

198 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Delete — server-side execution.
4 *
5 * Atomic plugin removal — if the plugin is active, this deactivates it
6 * first then deletes the files. Single tool call, single todo. Runs
7 * in-process under the App Password user's identity via WP core's
8 * `delete_plugins()`.
9 *
10 * The response includes `data.deactivated_first` so the LLM can mention
11 * to the user whether a deactivate step was needed.
12 *
13 * @since 0.0.5
14 * @package zip-ai
15 */
16
17 namespace ZipAI\MCP\Classes\Abilities\Zipai\System;
18
19 use ZipAI\MCP\Classes\Abilities\Abstract_Ability;
20 use ZipAI\MCP\Classes\Abilities\Zipai\System\PluginResolver;
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 PluginDelete 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-plugin';
44 $this->label = 'Delete Plugin';
45 $this->description = 'Permanently remove a plugin by slug or "folder/file" identifier. Atomic — deactivates first when active. '
46 . 'Runs in-process under the App-Password user\'s identity. '
47 . 'Requires `delete_plugins` capability. '
48 . 'Returns `data.deactivated_first` so you can mention whether a deactivate step was needed.';
49 $this->capability = 'delete_plugins';
50
51 $this->meta = array(
52 'tool_type' => Tool_Types::DELETE,
53 'preflight_resource' => array(
54 'kind' => 'installed_plugin',
55 'arg_field' => 'slug',
56 ),
57 );
58 }
59
60 /**
61 * Returns the tool-type classification for this ability.
62 *
63 * @return string One of the Tool_Types constants.
64 */
65 public function get_tool_type() {
66 return Tool_Types::DELETE;
67 }
68
69 /**
70 * Returns the JSON Schema for this ability's input arguments.
71 *
72 * @return array<string,mixed> JSON Schema describing accepted arguments.
73 */
74 public function get_input_schema() {
75 return array(
76 'type' => 'object',
77 'required' => array( 'slug' ),
78 'additionalProperties' => false,
79 'properties' => array(
80 'slug' => array(
81 'type' => 'string',
82 'description' => 'Plugin slug ("contact-form-7") or WP REST plugin id "folder/file" (`.php` extension optional, stripped automatically).',
83 ),
84 ),
85 );
86 }
87
88 /**
89 * Deletes a plugin by slug, deactivating it first when active.
90 *
91 * @param array<string,mixed> $args Validated input arguments.
92 * @return array<string,mixed> Standardized success or error response.
93 */
94 public function execute( $args ) {
95 $slug = isset( $args['slug'] ) && is_string( $args['slug'] ) ? sanitize_text_field( $args['slug'] ) : '';
96 if ( '' === $slug ) {
97 return Response::error( 'Plugin slug is required.' );
98 }
99 // Accept real installed-plugin folder names (underscores/dots), not only
100 // wp.org hyphen-slugs — PluginResolver is the safe arbiter. See plugin-activate.php.
101 if ( ! preg_match( '#^[a-z0-9][\w.-]*(?:/[a-z0-9._-]+(?:\.php)?)?$#i', $slug ) ) {
102 return Response::error( 'Invalid plugin identifier.' );
103 }
104
105 if ( ! wp_is_file_mod_allowed( 'zipai_delete_plugin' ) ) {
106 return Response::error( 'Plugin deletion is disabled on this site (DISALLOW_FILE_MODS or the file_mod_allowed filter).' );
107 }
108
109 require_once ABSPATH . 'wp-admin/includes/file.php';
110 require_once ABSPATH . 'wp-admin/includes/plugin.php';
111
112 $plugin_file = PluginResolver::resolve_plugin_file( $slug );
113 if ( null === $plugin_file ) {
114 return Response::error( sprintf( 'Plugin "%s" is not installed.', $slug ) );
115 }
116
117 // Self-preservation: this ability runs INSIDE ZIP AI — deleting it
118 // here removes the assistant itself (chat, tools, this connection)
119 // mid-action. Fail closed; wp-admin → Plugins is the supported path.
120 if ( PluginResolver::is_self( $plugin_file ) ) {
121 return Response::error(
122 sprintf( 'Refused: "%s" is the ZIP AI plugin, which runs this AI assistant — deleting it from here would remove the chat, all tools, and this connection mid-action.', $slug ),
123 'Leave ZIP AI installed and continue the remaining work. If the user explicitly wants ZIP AI removed, tell them to delete it in wp-admin → Plugins.'
124 );
125 }
126
127 // Atomic-from-the-LLM's-POV: deactivate first when active so the
128 // caller doesn't have to chain two tool calls. WP core's
129 // `delete_plugins()` refuses with `could_not_remove_plugin` if
130 // the plugin is still active.
131 $deactivated_first = false;
132 if ( is_plugin_active( $plugin_file ) ) {
133 deactivate_plugins( array( $plugin_file ), true );
134 $deactivated_first = true;
135 }
136
137 // `delete_plugins()` requires `WP_Filesystem` to be available.
138 // On FTP-mode hosts without stored creds it returns `null` — we
139 // surface that as a clear error rather than letting it look
140 // like success.
141 if ( ! WP_Filesystem() ) {
142 return Response::error(
143 'Filesystem credentials required for plugin delete. '
144 . 'Configure FS_METHOD or store FTP credentials in wp-config.php.'
145 );
146 }
147
148 $result = delete_plugins( array( $plugin_file ) );
149 if ( is_wp_error( $result ) ) {
150 return Response::error( sprintf( 'Delete failed: %s', $result->get_error_message() ) );
151 }
152 if ( null === $result ) {
153 return Response::error( 'Filesystem unavailable — plugin not deleted.' );
154 }
155
156 wp_clean_plugins_cache();
157
158 return array(
159 'success' => true,
160 'message' => sprintf(
161 'Plugin "%s" deleted%s.',
162 $slug,
163 $deactivated_first ? ' (deactivated first)' : ''
164 ),
165 'data' => array(
166 'slug' => $slug,
167 'plugin_file' => $plugin_file,
168 'deactivated_first' => $deactivated_first,
169 ),
170 );
171 }
172
173 /**
174 * Returns the JSON Schema for this ability's response.
175 *
176 * @return array<string,mixed> JSON Schema describing the response shape.
177 */
178 public function get_output_schema() {
179 return array(
180 'type' => 'object',
181 'required' => array( 'success' ),
182 'additionalProperties' => true,
183 'properties' => array(
184 'success' => array( 'type' => 'boolean' ),
185 'message' => array( 'type' => 'string' ),
186 'data' => array(
187 'type' => 'object',
188 'properties' => array(
189 'slug' => array( 'type' => 'string' ),
190 'plugin_file' => array( 'type' => 'string' ),
191 'deactivated_first' => array( 'type' => 'boolean' ),
192 ),
193 ),
194 ),
195 );
196 }
197 }
198