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

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

179 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Activate — server-side execution.
4 *
5 * Runs `activate_plugin()` in-process under the App Password user's
6 * identity (set by `REST_API::is_basic_authenticated()`). The legacy
7 * js_hook delegation was needed when the service used a Sanctum bearer
8 * not bound to a WP user; App Password auth is the WP-blessed pattern
9 * for third-party services and binds capability checks natively.
10 *
11 * Idempotent — already-active plugins return success without re-firing
12 * activation hooks.
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\Abilities\Zipai\System\PluginResolver;
22 use ZipAI\MCP\Classes\Core\Response;
23 use ZipAI\MCP\Classes\Core\Tool_Types;
24
25 if ( ! defined( 'ABSPATH' ) ) {
26 exit;
27 }
28
29 class PluginActivate extends Abstract_Ability {
30
31 /**
32 * Whether the ability performs a destructive/irreversible action.
33 *
34 * @var bool
35 */
36 protected $is_destructive = true;
37
38 /**
39 * Configure the ability id, label, description, capability, and meta.
40 *
41 * @return void
42 */
43 public function configure() {
44 $this->id = 'zipai/activate-plugin';
45 $this->label = 'Activate Plugin';
46 $this->description = 'Activate an already-installed plugin by slug or "folder/file" identifier. '
47 . 'Runs `activate_plugin()` in-process under the App-Password user\'s identity. '
48 . 'Requires `activate_plugins` capability. '
49 . 'Accepts a bare slug ("contact-form-7") or the WP REST plugin id "folder/file" with or without `.php` — extension is normalised.';
50 $this->capability = 'activate_plugins';
51
52 $this->meta = array(
53 'tool_type' => Tool_Types::WRITE,
54 'preflight_resource' => array(
55 'kind' => 'installed_plugin',
56 'arg_field' => 'slug',
57 ),
58 );
59 }
60
61 /**
62 * Tool-type classification used for permission gating.
63 *
64 * @return string
65 */
66 public function get_tool_type() {
67 return Tool_Types::WRITE;
68 }
69
70 /**
71 * JSON Schema describing the ability's accepted input arguments.
72 *
73 * @return array<string,mixed>
74 */
75 public function get_input_schema() {
76 return array(
77 'type' => 'object',
78 'required' => array( 'slug' ),
79 'additionalProperties' => false,
80 'properties' => array(
81 'slug' => array(
82 'type' => 'string',
83 'description' => 'Plugin slug ("contact-form-7") or WP REST plugin id "folder/file" (`.php` extension optional, stripped automatically).',
84 ),
85 ),
86 );
87 }
88
89 /**
90 * Activate an installed plugin by slug or "folder/file" identifier.
91 *
92 * @param array<string,mixed> $args Validated input arguments (expects `slug`).
93 * @return array<string,mixed> Success payload, or Response::error() on failure.
94 */
95 public function execute( $args ) {
96 $slug = isset( $args['slug'] ) && is_string( $args['slug'] ) ? sanitize_text_field( $args['slug'] ) : '';
97 if ( '' === $slug ) {
98 return Response::error( 'Plugin slug is required.' );
99 }
100 // Folder-segment class accepts real installed-plugin folder names, not only
101 // wp.org hyphen-slugs: premium/renamed plugins use underscores and dots
102 // (e.g. "Ultimate_VC_Addons", "js_composer"). PluginResolver is the real,
103 // safe arbiter (pure get_plugins() key/folder match — the raw slug never
104 // reaches the filesystem), so this is a light shape gate only.
105 if ( ! preg_match( '#^[a-z0-9][\w.-]*(?:/[a-z0-9._-]+(?:\.php)?)?$#i', $slug ) ) {
106 return Response::error( 'Invalid plugin identifier. Use the slug ("contact-form-7") or the WP REST plugin id "folder/file".' );
107 }
108
109 require_once ABSPATH . 'wp-admin/includes/plugin.php';
110
111 $plugin_file = PluginResolver::resolve_plugin_file( $slug );
112 if ( null === $plugin_file ) {
113 return Response::error( sprintf( 'Plugin "%s" is not installed.', $slug ) );
114 }
115
116 if ( is_plugin_active( $plugin_file ) ) {
117 return array(
118 'success' => true,
119 'message' => sprintf( 'Plugin "%s" is already active.', $slug ),
120 'data' => array(
121 'slug' => $slug,
122 'plugin_file' => $plugin_file,
123 'active' => true,
124 ),
125 );
126 }
127
128 $result = activate_plugin( $plugin_file, '', false, true );
129 if ( is_wp_error( $result ) ) {
130 return Response::error( sprintf( 'Activation failed: %s', $result->get_error_message() ) );
131 }
132
133 // Belt-and-braces verify — `activate_plugin()` returns `null` on
134 // success but a fatal activation error can leave the plugin
135 // inactive without surfacing a WP_Error. Re-read the freshly-written
136 // `active_plugins` option directly (single-site activation above) — a
137 // side-effect-aware probe that reflects the post-activation state.
138 $active_plugins = get_option( 'active_plugins', array() );
139 if ( ! is_array( $active_plugins ) || ! in_array( $plugin_file, $active_plugins, true ) ) {
140 return Response::error( sprintf( 'Plugin "%s" did not activate (no error returned, but plugin remains inactive).', $slug ) );
141 }
142
143 return array(
144 'success' => true,
145 'message' => sprintf( 'Plugin "%s" activated.', $slug ),
146 'data' => array(
147 'slug' => $slug,
148 'plugin_file' => $plugin_file,
149 'active' => true,
150 ),
151 );
152 }
153
154 /**
155 * JSON Schema describing the ability's response shape.
156 *
157 * @return array<string,mixed>
158 */
159 public function get_output_schema() {
160 return array(
161 'type' => 'object',
162 'required' => array( 'success' ),
163 'additionalProperties' => true,
164 'properties' => array(
165 'success' => array( 'type' => 'boolean' ),
166 'message' => array( 'type' => 'string' ),
167 'data' => array(
168 'type' => 'object',
169 'properties' => array(
170 'slug' => array( 'type' => 'string' ),
171 'plugin_file' => array( 'type' => 'string' ),
172 'active' => array( 'type' => 'boolean' ),
173 ),
174 ),
175 ),
176 );
177 }
178 }
179