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-deactivate.php

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

177 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Deactivate — server-side execution.
4 *
5 * Sister to PluginActivate. Runs `deactivate_plugins()` in-process
6 * under the App Password user's identity. Idempotent — already-inactive
7 * plugins return success without re-firing deactivation hooks.
8 *
9 * @since 0.0.5
10 * @package zip-ai
11 */
12
13 namespace ZipAI\MCP\Classes\Abilities\Zipai\System;
14
15 use ZipAI\MCP\Classes\Abilities\Abstract_Ability;
16 use ZipAI\MCP\Classes\Abilities\Zipai\System\PluginResolver;
17 use ZipAI\MCP\Classes\Core\Response;
18 use ZipAI\MCP\Classes\Core\Tool_Types;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 class PluginDeactivate extends Abstract_Ability {
25
26 /**
27 * Flags this ability as destructive (mutates site state).
28 *
29 * @var bool
30 */
31 protected $is_destructive = true;
32
33 /**
34 * Configures the ability's id, label, description and metadata.
35 *
36 * @return void
37 */
38 public function configure() {
39 $this->id = 'zipai/deactivate-plugin';
40 $this->label = 'Deactivate Plugin';
41 $this->description = 'Deactivate an active plugin by slug or "folder/file" identifier. '
42 . 'Runs `deactivate_plugins()` in-process under the App-Password user\'s identity. '
43 . 'Requires `activate_plugins` capability.';
44 $this->capability = 'activate_plugins';
45
46 $this->meta = array(
47 'tool_type' => Tool_Types::WRITE,
48 'preflight_resource' => array(
49 'kind' => 'installed_plugin',
50 'arg_field' => 'slug',
51 ),
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::WRITE;
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 'description' => 'Plugin slug ("contact-form-7") or WP REST plugin id "folder/file" (`.php` extension optional, stripped automatically).',
78 ),
79 ),
80 );
81 }
82
83 /**
84 * Deactivates an active plugin by slug or "folder/file" identifier.
85 *
86 * @param array<string,mixed> $args Validated input arguments.
87 * @return array<string,mixed> Standardized success or error response.
88 */
89 public function execute( $args ) {
90 $slug = isset( $args['slug'] ) && is_string( $args['slug'] ) ? sanitize_text_field( $args['slug'] ) : '';
91 if ( '' === $slug ) {
92 return Response::error( 'Plugin slug is required.' );
93 }
94 // Accept real installed-plugin folder names (underscores/dots), not only
95 // wp.org hyphen-slugs — PluginResolver is the safe arbiter. See plugin-activate.php.
96 if ( ! preg_match( '#^[a-z0-9][\w.-]*(?:/[a-z0-9._-]+(?:\.php)?)?$#i', $slug ) ) {
97 return Response::error( 'Invalid plugin identifier.' );
98 }
99
100 require_once ABSPATH . 'wp-admin/includes/plugin.php';
101
102 $plugin_file = PluginResolver::resolve_plugin_file( $slug );
103 if ( null === $plugin_file ) {
104 return Response::error( sprintf( 'Plugin "%s" is not installed.', $slug ) );
105 }
106
107 // Self-preservation: this ability runs INSIDE ZIP AI — deactivating
108 // it here kills the assistant's chat, tools, and this connection
109 // mid-action. Fail closed; wp-admin → Plugins is the supported path.
110 if ( PluginResolver::is_self( $plugin_file ) ) {
111 return Response::error(
112 sprintf( 'Refused: "%s" is the ZIP AI plugin, which runs this AI assistant — deactivating it from here would sever the chat, all tools, and this connection mid-action.', $slug ),
113 'Leave ZIP AI active and continue the remaining work. If the user explicitly wants ZIP AI off, tell them to toggle it in wp-admin → Plugins.'
114 );
115 }
116
117 if ( ! is_plugin_active( $plugin_file ) ) {
118 return array(
119 'success' => true,
120 'message' => sprintf( 'Plugin "%s" is already inactive.', $slug ),
121 'data' => array(
122 'slug' => $slug,
123 'plugin_file' => $plugin_file,
124 'active' => false,
125 ),
126 );
127 }
128
129 // `deactivate_plugins()` returns void. WP core protects critical
130 // plugins via the `pre_deactivate_plugin` action chain — we let
131 // any plugin-side guard surface its own error path via WP_Error
132 // in `pre_update_option_active_plugins` (filtered into our own
133 // Protected_Options_Filter for the essential set).
134 deactivate_plugins( array( $plugin_file ), true );
135
136 // Cast forces a fresh call: analysis can't model that deactivate_plugins() mutated state.
137 if ( is_plugin_active( (string) $plugin_file ) ) {
138 return Response::error( sprintf( 'Plugin "%s" did not deactivate (still active after deactivate_plugins call).', $slug ) );
139 }
140
141 return array(
142 'success' => true,
143 'message' => sprintf( 'Plugin "%s" deactivated.', $slug ),
144 'data' => array(
145 'slug' => $slug,
146 'plugin_file' => $plugin_file,
147 'active' => false,
148 ),
149 );
150 }
151
152 /**
153 * Returns the JSON Schema for this ability's response.
154 *
155 * @return array<string,mixed> JSON Schema describing the response shape.
156 */
157 public function get_output_schema() {
158 return array(
159 'type' => 'object',
160 'required' => array( 'success' ),
161 'additionalProperties' => true,
162 'properties' => array(
163 'success' => array( 'type' => 'boolean' ),
164 'message' => array( 'type' => 'string' ),
165 'data' => array(
166 'type' => 'object',
167 'properties' => array(
168 'slug' => array( 'type' => 'string' ),
169 'plugin_file' => array( 'type' => 'string' ),
170 'active' => array( 'type' => 'boolean' ),
171 ),
172 ),
173 ),
174 );
175 }
176 }
177