PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / src / abilities / class-modules-abilities.php
jetpack / src / abilities Last commit date
class-modules-abilities.php 1 month ago class-monitor-abilities.php 1 month ago
class-modules-abilities.php
451 lines
1 <?php
2 /**
3 * Jetpack Modules Abilities Registration
4 *
5 * Registers Jetpack module management abilities with the WordPress Abilities API.
6 *
7 * @package automattic/jetpack
8 */
9
10 namespace Automattic\Jetpack\Plugin\Abilities;
11
12 use Automattic\Jetpack\WP_Abilities\Registrar;
13 use Jetpack;
14
15 /**
16 * Registers Jetpack module management abilities with the WordPress Abilities API.
17 *
18 * Exposes a filtered read (`get-modules`) and a declarative state-setter
19 * (`set-module-status`) so AI agents can discover and toggle Jetpack modules
20 * through the standard `wp-abilities/v1` REST surface.
21 */
22 class Modules_Abilities extends Registrar {
23
24 /**
25 * {@inheritDoc}
26 */
27 public static function get_category_slug(): string {
28 return 'jetpack';
29 }
30
31 /**
32 * {@inheritDoc}
33 *
34 * The `jetpack` ability-category is shared across multiple subpackages
35 * (Connection, Plugin, etc.). `wp_register_ability_category()` only honours
36 * the first registration, so the English source string here is kept
37 * byte-identical to the one in
38 * {@see \Automattic\Jetpack\Connection\Abilities\Connection_Abilities::get_category_definition()}
39 * to keep the visible category text consistent regardless of which
40 * registrar runs first.
41 */
42 public static function get_category_definition(): array {
43 return array(
44 // "Jetpack" is a product name and should not be translated.
45 'label' => 'Jetpack',
46 'description' => __( 'Abilities provided by Jetpack.', 'jetpack' ),
47 );
48 }
49
50 /**
51 * {@inheritDoc}
52 */
53 public static function get_abilities(): array {
54 $module_schema = array(
55 'type' => 'object',
56 'properties' => array(
57 'slug' => array( 'type' => 'string' ),
58 'name' => array( 'type' => 'string' ),
59 'description' => array( 'type' => 'string' ),
60 'active' => array( 'type' => 'boolean' ),
61 'sort' => array( 'type' => 'integer' ),
62 'feature' => array(
63 'type' => 'array',
64 'items' => array( 'type' => 'string' ),
65 ),
66 'plan_classes' => array(
67 'type' => 'array',
68 'items' => array( 'type' => 'string' ),
69 ),
70 'requires_connection' => array( 'type' => 'boolean' ),
71 'requires_user_connection' => array( 'type' => 'boolean' ),
72 'auto_activate' => array( 'type' => 'string' ),
73 ),
74 );
75
76 return array(
77 'jetpack/get-modules' => array(
78 'label' => __( 'Get Jetpack modules', 'jetpack' ),
79 'description' => __( 'Return zero or more Jetpack modules as an array. Each element has { slug, name, description, active, sort, feature, plan_classes, requires_connection, requires_user_connection, auto_activate }. Combine slug / active / feature / search filters to narrow the list. When slug is provided and unknown, the result is an empty array (not an error). Use this before calling jetpack/set-module-status to enumerate legal slugs.', 'jetpack' ),
80 'input_schema' => array(
81 'type' => 'object',
82 'default' => array(),
83 'properties' => array(
84 'slug' => array(
85 'type' => 'string',
86 'description' => __( 'Return a single module by slug. Unknown slugs yield an empty array.', 'jetpack' ),
87 'minLength' => 1,
88 ),
89 'active' => array(
90 'type' => 'boolean',
91 'description' => __( 'When set, only return modules whose current active state matches this value.', 'jetpack' ),
92 ),
93 'feature' => array(
94 'type' => 'string',
95 'description' => __( 'Case-insensitive match against a module\'s feature tag (e.g. "Recommended", "Security", "Performance").', 'jetpack' ),
96 'minLength' => 1,
97 ),
98 'search' => array(
99 'type' => 'string',
100 'description' => __( 'Case-insensitive substring match against the module name, slug, and description.', 'jetpack' ),
101 'minLength' => 1,
102 ),
103 ),
104 'additionalProperties' => false,
105 ),
106 'output_schema' => array(
107 'type' => 'array',
108 'items' => $module_schema,
109 ),
110 'execute_callback' => array( __CLASS__, 'get_modules' ),
111 'permission_callback' => array( __CLASS__, 'can_view_modules' ),
112 'meta' => array(
113 'annotations' => array(
114 'readonly' => true,
115 'destructive' => false,
116 'idempotent' => true,
117 ),
118 'show_in_rest' => true,
119 'mcp' => array(
120 'public' => true,
121 'type' => 'tool', // default is already "tool", but can be explicit.
122 ),
123 ),
124 ),
125
126 'jetpack/set-module-status' => array(
127 'label' => __( 'Set Jetpack module status', 'jetpack' ),
128 'description' => __( 'Set a Jetpack module\'s active state. Idempotent — setting a module to its current state returns changed=false. Returns { slug, active, changed }. Call jetpack/get-modules first to enumerate valid slugs. Modules requiring a Jetpack connection or a paid plan may fail with jetpack_modules_activate_failed; the message indicates the next step.', 'jetpack' ),
129 'input_schema' => array(
130 'type' => 'object',
131 'required' => array( 'slug', 'active' ),
132 'properties' => array(
133 'slug' => array(
134 'type' => 'string',
135 'description' => __( 'The Jetpack module slug (e.g. "stats", "sso", "sharedaddy").', 'jetpack' ),
136 'minLength' => 1,
137 ),
138 'active' => array(
139 'type' => 'boolean',
140 'description' => __( 'Desired active state. true activates the module; false deactivates it.', 'jetpack' ),
141 ),
142 ),
143 'additionalProperties' => false,
144 ),
145 'output_schema' => array(
146 'type' => 'object',
147 'properties' => array(
148 'slug' => array( 'type' => 'string' ),
149 'active' => array( 'type' => 'boolean' ),
150 'changed' => array( 'type' => 'boolean' ),
151 ),
152 ),
153 'execute_callback' => array( __CLASS__, 'set_module_status' ),
154 'permission_callback' => array( __CLASS__, 'can_manage_modules' ),
155 'meta' => array(
156 'annotations' => array(
157 'readonly' => false,
158 'destructive' => false,
159 'idempotent' => true,
160 ),
161 'show_in_rest' => true,
162 'mcp' => array(
163 'public' => true,
164 'type' => 'tool', // default is already "tool", but can be explicit.
165 ),
166 ),
167 ),
168 );
169 }
170
171 /**
172 * Permission check: can the current user read module listings?
173 *
174 * Mirrors the capability used by the Jetpack admin page so subscribers and
175 * contributors are denied.
176 */
177 public static function can_view_modules(): bool {
178 return current_user_can( 'jetpack_admin_page' );
179 }
180
181 /**
182 * Permission check: can the current user toggle modules?
183 */
184 public static function can_manage_modules(): bool {
185 return current_user_can( 'jetpack_manage_modules' )
186 && current_user_can( 'jetpack_activate_modules' );
187 }
188
189 /**
190 * Build the high-signal summary shape used by `get-modules`.
191 *
192 * Adapts the raw `Jetpack::get_module()` array down to the fields agents
193 * actually consume — dropping internal bookkeeping like `module_tags`,
194 * changelog URLs, and file paths. Applies the site's current locale to
195 * `name` and `description` so agent-facing output mirrors what a user
196 * would see in the admin UI.
197 *
198 * @param string $slug Module slug.
199 * @param bool $is_active Whether the module is currently active. Passed in to avoid
200 * O(N) calls to the `jetpack_active_modules` filter in a list loop.
201 * @return array|null Compact module entry, or null when the module has no info.
202 */
203 private static function summarize_module( $slug, $is_active ) {
204 $mod = Jetpack::get_module( $slug );
205 if ( ! is_array( $mod ) ) {
206 return null;
207 }
208
209 $i18n = function_exists( 'jetpack_get_module_i18n' ) ? jetpack_get_module_i18n( $slug ) : array();
210 $name = isset( $i18n['name'] ) ? (string) $i18n['name'] : ( isset( $mod['name'] ) ? (string) $mod['name'] : $slug );
211 $desc = isset( $i18n['description'] ) ? (string) $i18n['description'] : ( isset( $mod['description'] ) ? (string) $mod['description'] : '' );
212
213 return array(
214 'slug' => $slug,
215 'name' => $name,
216 'description' => $desc,
217 'active' => $is_active,
218 'sort' => isset( $mod['sort'] ) ? (int) $mod['sort'] : 10,
219 'feature' => isset( $mod['feature'] ) && is_array( $mod['feature'] ) ? array_values( $mod['feature'] ) : array(),
220 'plan_classes' => isset( $mod['plan_classes'] ) && is_array( $mod['plan_classes'] ) ? array_values( $mod['plan_classes'] ) : array(),
221 'requires_connection' => ! empty( $mod['requires_connection'] ),
222 'requires_user_connection' => ! empty( $mod['requires_user_connection'] ),
223 'auto_activate' => isset( $mod['auto_activate'] ) ? (string) $mod['auto_activate'] : 'No',
224 );
225 }
226
227 /**
228 * Consolidated read callback. Returns an array of module summaries.
229 *
230 * When `slug` is provided, returns a 0- or 1-element array; unknown slugs
231 * yield an empty array rather than a `WP_Error` so the shape is uniform.
232 *
233 * @param array|null $input Input matching the ability's input_schema.
234 * @return array
235 */
236 public static function get_modules( $input = null ) {
237 $input = is_array( $input ) ? $input : array();
238
239 // Fetch the active-slug map once per call — `Jetpack::is_module_active()` fires the
240 // user-extensible `jetpack_active_modules` filter on each call, so looking up
241 // active state per-module in a loop amplifies any hook cost by N.
242 $active_map = array_flip( Jetpack::get_active_modules() );
243
244 // Narrow the candidate set up front when `slug` is supplied; remaining
245 // filters (active / feature / search) still apply so combinations like
246 // { slug: 'stats', active: false } correctly return an empty array when
247 // stats is active.
248 if ( isset( $input['slug'] ) && is_string( $input['slug'] ) && '' !== $input['slug'] ) {
249 if ( ! Jetpack::is_module( $input['slug'] ) ) {
250 return array();
251 }
252 $candidate_slugs = array( $input['slug'] );
253 } else {
254 $candidate_slugs = Jetpack::get_available_modules();
255 }
256
257 $active_filter = array_key_exists( 'active', $input ) && is_bool( $input['active'] ) ? $input['active'] : null;
258 $feature_filter = isset( $input['feature'] ) && is_string( $input['feature'] ) && '' !== $input['feature']
259 ? strtolower( $input['feature'] )
260 : null;
261 $search_filter = isset( $input['search'] ) && is_string( $input['search'] ) && '' !== $input['search']
262 ? strtolower( $input['search'] )
263 : null;
264
265 $out = array();
266 foreach ( $candidate_slugs as $slug ) {
267 $summary = self::summarize_module( $slug, isset( $active_map[ $slug ] ) );
268 if ( null === $summary ) {
269 continue;
270 }
271
272 if ( null !== $active_filter && $summary['active'] !== $active_filter ) {
273 continue;
274 }
275
276 if ( null !== $feature_filter ) {
277 $features_lower = array_map( 'strtolower', $summary['feature'] );
278 if ( ! in_array( $feature_filter, $features_lower, true ) ) {
279 continue;
280 }
281 }
282
283 if ( null !== $search_filter ) {
284 $haystack = strtolower( $summary['slug'] . ' ' . $summary['name'] . ' ' . $summary['description'] );
285 if ( false === strpos( $haystack, $search_filter ) ) {
286 continue;
287 }
288 }
289
290 $out[] = $summary;
291 }
292
293 usort(
294 $out,
295 static function ( $a, $b ) {
296 if ( $a['sort'] === $b['sort'] ) {
297 return strcmp( $a['slug'], $b['slug'] );
298 }
299 return $a['sort'] <=> $b['sort'];
300 }
301 );
302
303 return $out;
304 }
305
306 /**
307 * Declarative state-setter callback. Idempotent: returns changed=false
308 * when the desired state already matches current state.
309 *
310 * @param array|null $input Input matching the ability's input_schema.
311 * @return array|\WP_Error
312 */
313 public static function set_module_status( $input = null ) {
314 $input = is_array( $input ) ? $input : array();
315
316 if ( ! isset( $input['slug'] ) || ! is_string( $input['slug'] ) || '' === $input['slug'] ) {
317 return new \WP_Error(
318 'jetpack_modules_missing_slug',
319 __( 'A module slug is required. Call jetpack/get-modules to enumerate valid slugs.', 'jetpack' )
320 );
321 }
322
323 if ( ! array_key_exists( 'active', $input ) ) {
324 return new \WP_Error(
325 'jetpack_modules_missing_active',
326 __( 'A desired active state (boolean) is required.', 'jetpack' )
327 );
328 }
329 if ( ! is_bool( $input['active'] ) ) {
330 return new \WP_Error(
331 'jetpack_modules_invalid_active',
332 __( 'The active parameter must be a boolean. Strings like "true" / "false" are not accepted.', 'jetpack' )
333 );
334 }
335
336 $slug = $input['slug'];
337 $desired = $input['active'];
338
339 if ( ! Jetpack::is_module( $slug ) ) {
340 return new \WP_Error(
341 'jetpack_modules_invalid_slug',
342 __( 'Unknown Jetpack module slug. Call jetpack/get-modules to enumerate valid slugs.', 'jetpack' )
343 );
344 }
345
346 $current = Jetpack::is_module_active( $slug );
347
348 if ( $desired === $current ) {
349 return array(
350 'slug' => $slug,
351 'active' => $current,
352 'changed' => false,
353 );
354 }
355
356 if ( $desired ) {
357 // Preflight: Jetpack::activate_module() ignores its $exit/$redirect flags when a
358 // conflicting standalone plugin (e.g. WordPress.com Stats vs. the stats module) is
359 // active — it calls wp_safe_redirect()+exit() and would terminate this REST request
360 // mid-response. Refuse here with a structured error instead.
361 $conflict = self::find_conflicting_active_plugin( $slug );
362 if ( null !== $conflict ) {
363 return new \WP_Error(
364 'jetpack_modules_conflicting_plugin_active',
365 sprintf(
366 /* translators: %s: name of the conflicting plugin that must be deactivated first. */
367 __( 'Cannot activate the module while a conflicting plugin (%s) is active. Deactivate it on the WordPress Plugins screen, then retry.', 'jetpack' ),
368 $conflict
369 )
370 );
371 }
372
373 // Always pass exit=false, redirect=false so the ability runs headless over REST.
374 $ok = Jetpack::activate_module( $slug, false, false );
375 if ( ! $ok ) {
376 return new \WP_Error(
377 'jetpack_modules_activate_failed',
378 __( 'Unable to activate the module. It may require a Jetpack connection or a higher plan. Inspect requires_connection and plan_classes on the module and retry after those preconditions are met.', 'jetpack' )
379 );
380 }
381 } else {
382 $ok = Jetpack::deactivate_module( $slug );
383 if ( ! $ok ) {
384 return new \WP_Error(
385 'jetpack_modules_deactivate_failed',
386 __( 'Unable to deactivate the module.', 'jetpack' )
387 );
388 }
389 }
390
391 // Re-check state: activate_module() / deactivate_module() can return truthy even when a
392 // pre_update_option_jetpack_active_modules filter blocks the option write, so the response
393 // would otherwise lie about reaching the requested state.
394 $actual = Jetpack::is_module_active( $slug );
395 if ( $desired !== $actual ) {
396 return new \WP_Error(
397 'jetpack_modules_state_mismatch',
398 __( 'The module did not reach the requested state. A filter on jetpack_active_modules may have rejected the change.', 'jetpack' )
399 );
400 }
401
402 return array(
403 'slug' => $slug,
404 'active' => $actual,
405 'changed' => true,
406 );
407 }
408
409 /**
410 * Return the human-readable name of the first standalone plugin currently active that
411 * would force {@see Jetpack::activate_module()} to redirect/exit, or null when none.
412 *
413 * Mirrors the lookup in {@see Jetpack_Client_Server::deactivate_plugin()}: prefer the
414 * known plugin file path, fall back to a name match across active plugins.
415 *
416 * @param string $slug Module slug.
417 * @return string|null
418 */
419 private static function find_conflicting_active_plugin( $slug ) {
420 $jetpack = Jetpack::init();
421 if ( empty( $jetpack->plugins_to_deactivate[ $slug ] ) ) {
422 return null;
423 }
424
425 if ( ! function_exists( 'is_plugin_active' ) ) {
426 require_once ABSPATH . 'wp-admin/includes/plugin.php';
427 }
428
429 $active_plugins = null;
430 foreach ( $jetpack->plugins_to_deactivate[ $slug ] as $candidate ) {
431 list( $plugin_file, $plugin_name ) = $candidate;
432
433 if ( is_plugin_active( $plugin_file ) ) {
434 return $plugin_name;
435 }
436
437 if ( null === $active_plugins ) {
438 $active_plugins = Jetpack::get_active_plugins();
439 }
440 foreach ( $active_plugins as $active ) {
441 $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $active );
442 if ( isset( $data['Name'] ) && $data['Name'] === $plugin_name ) {
443 return $plugin_name;
444 }
445 }
446 }
447
448 return null;
449 }
450 }
451