PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 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 / jetpack_vendor / automattic / jetpack-forms / src / abilities / class-forms-abilities.php
jetpack / jetpack_vendor / automattic / jetpack-forms / src / abilities Last commit date
class-forms-abilities.php 2 months ago
class-forms-abilities.php
939 lines
1 <?php
2 /**
3 * Jetpack Forms Abilities Registration
4 *
5 * Registers Jetpack Forms abilities with the WordPress Abilities API.
6 *
7 * @package automattic/jetpack-forms
8 * @since 1.0.0
9 */
10
11 namespace Automattic\Jetpack\Forms\Abilities;
12
13 use Automattic\Jetpack\WP_Abilities\Registrar;
14
15 /**
16 * Class Forms_Abilities
17 *
18 * Registers Jetpack Forms abilities with the WordPress Abilities API.
19 * Ability callbacks delegate to REST endpoints via rest_do_request()
20 * so they inherit endpoint validation, sanitization, and hooks.
21 */
22 class Forms_Abilities extends Registrar {
23
24 const CATEGORY_SLUG = 'jetpack-forms';
25
26 /**
27 * Form post statuses accepted by list-forms.
28 */
29 const FORM_STATUSES = array( 'publish', 'draft', 'trash' );
30
31 /**
32 * Form post statuses accepted by create-form.
33 */
34 const CREATE_FORM_STATUSES = array( 'publish', 'draft' );
35
36 /**
37 * Response post statuses accepted by get-responses / update-response.
38 */
39 const RESPONSE_STATUSES = array( 'publish', 'draft', 'spam', 'trash' );
40
41 /**
42 * Bulk actions accepted by bulk-update-responses.
43 */
44 const BULK_ACTIONS = array( 'mark_as_spam', 'mark_as_not_spam' );
45
46 /**
47 * Default block content used when create-form is called without `content`.
48 */
49 const DEFAULT_FORM_CONTENT = '<!-- wp:jetpack/contact-form --><!-- wp:jetpack/button {"element":"button","text":"Submit","lock":{"remove":true}} /--><!-- /wp:jetpack/contact-form -->';
50
51 /**
52 * Register the category and abilities.
53 *
54 * Forms abilities shipped (see #45998) before the
55 * `jetpack_wp_abilities_enabled` rollout filter existed, so this
56 * deliberately bypasses that gate — backing the `get-responses`,
57 * `update-response`, and `get-status-counts` abilities out behind a
58 * default-off filter would silently break consumers that already
59 * dispatch them. The newer admin abilities ride along for parity:
60 * Forms abilities are uniformly available wherever the package is
61 * loaded.
62 *
63 * @return void
64 */
65 public static function init() {
66 if ( did_action( self::CATEGORIES_INIT_ACTION ) ) {
67 static::register_category();
68 } else {
69 add_action( self::CATEGORIES_INIT_ACTION, array( static::class, 'register_category' ) );
70 }
71
72 if ( did_action( self::ABILITIES_INIT_ACTION ) ) {
73 static::register_abilities();
74 } else {
75 add_action( self::ABILITIES_INIT_ACTION, array( static::class, 'register_abilities' ) );
76 }
77 }
78
79 /**
80 * {@inheritDoc}
81 */
82 public static function get_category_slug(): string {
83 return self::CATEGORY_SLUG;
84 }
85
86 /**
87 * {@inheritDoc}
88 */
89 public static function get_category_definition(): array {
90 return array(
91 // "Jetpack Forms" is a product name and should not be translated.
92 'label' => 'Jetpack Forms',
93 'description' => __( 'Abilities for managing Jetpack Forms and their responses.', 'jetpack-forms' ),
94 );
95 }
96
97 /**
98 * {@inheritDoc}
99 */
100 public static function get_abilities(): array {
101 return array(
102 'jetpack-forms/list-forms' => self::spec_list_forms(),
103 'jetpack-forms/get-form' => self::spec_get_form(),
104 'jetpack-forms/create-form' => self::spec_create_form(),
105 'jetpack-forms/delete-form' => self::spec_delete_form(),
106 'jetpack-forms/get-responses' => self::spec_get_responses(),
107 'jetpack-forms/update-response' => self::spec_update_response(),
108 'jetpack-forms/bulk-update-responses' => self::spec_bulk_update_responses(),
109 'jetpack-forms/get-status-counts' => self::spec_get_status_counts(),
110 );
111 }
112
113 /*
114 ---------------------------------------------------------------------
115 * Ability specs
116 * ---------------------------------------------------------------------
117 */
118
119 /**
120 * Spec: jetpack-forms/list-forms.
121 */
122 private static function spec_list_forms(): array {
123 return array(
124 'label' => __( 'List forms (admin)', 'jetpack-forms' ),
125 'description' => __( 'List all forms with admin detail including response counts, status, and edit URLs. Supports pagination, search, and status filtering.', 'jetpack-forms' ),
126 'input_schema' => array(
127 'type' => 'object',
128 'default' => array(),
129 'properties' => array(
130 'page' => array(
131 'type' => 'integer',
132 'description' => __( 'Page number for paginated results.', 'jetpack-forms' ),
133 'default' => 1,
134 'minimum' => 1,
135 ),
136 'per_page' => array(
137 'type' => 'integer',
138 'description' => __( 'Number of forms per page.', 'jetpack-forms' ),
139 'default' => 10,
140 'minimum' => 1,
141 'maximum' => 100,
142 ),
143 'search' => array(
144 'type' => 'string',
145 'description' => __( 'Search forms by title.', 'jetpack-forms' ),
146 ),
147 'status' => array(
148 'type' => 'string',
149 'description' => __( 'Filter by form status.', 'jetpack-forms' ),
150 'enum' => self::FORM_STATUSES,
151 ),
152 ),
153 'additionalProperties' => false,
154 ),
155 'execute_callback' => array( __CLASS__, 'list_forms' ),
156 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
157 'meta' => array(
158 'annotations' => array(
159 'readonly' => true,
160 'destructive' => false,
161 'idempotent' => true,
162 ),
163 'show_in_rest' => true,
164 'mcp' => array(
165 'public' => true,
166 'type' => 'tool', // default is already "tool", but can be explicit.
167 ),
168 ),
169 );
170 }
171
172 /**
173 * Spec: jetpack-forms/get-form.
174 */
175 private static function spec_get_form(): array {
176 return array(
177 'label' => __( 'Get form details', 'jetpack-forms' ),
178 'description' => __( 'Get a single form with its full structure including field definitions, status, and edit URL.', 'jetpack-forms' ),
179 'input_schema' => array(
180 'type' => 'object',
181 'required' => array( 'id' ),
182 'properties' => array(
183 'id' => array(
184 'type' => 'integer',
185 'description' => __( 'The form ID.', 'jetpack-forms' ),
186 ),
187 ),
188 'additionalProperties' => false,
189 ),
190 'execute_callback' => array( __CLASS__, 'get_form' ),
191 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
192 'meta' => array(
193 'annotations' => array(
194 'readonly' => true,
195 'destructive' => false,
196 'idempotent' => true,
197 ),
198 'show_in_rest' => true,
199 'mcp' => array(
200 'public' => true,
201 'type' => 'tool', // default is already "tool", but can be explicit.
202 ),
203 ),
204 );
205 }
206
207 /**
208 * Spec: jetpack-forms/create-form.
209 */
210 private static function spec_create_form(): array {
211 return array(
212 'label' => __( 'Create a form', 'jetpack-forms' ),
213 'description' => __( 'Create a new form with a title. Optionally provide block content for the form structure. Returns the new form ID and edit URL.', 'jetpack-forms' ),
214 'input_schema' => array(
215 'type' => 'object',
216 'required' => array( 'title' ),
217 'properties' => array(
218 'title' => array(
219 'type' => 'string',
220 'description' => __( 'The form title/name.', 'jetpack-forms' ),
221 ),
222 'content' => array(
223 'type' => 'string',
224 'description' => __( 'Block content for the form structure. If omitted, creates an empty form with a submit button.', 'jetpack-forms' ),
225 ),
226 'status' => array(
227 'type' => 'string',
228 'description' => __( 'Initial form status.', 'jetpack-forms' ),
229 'enum' => self::CREATE_FORM_STATUSES,
230 'default' => 'publish',
231 ),
232 ),
233 'additionalProperties' => false,
234 ),
235 'execute_callback' => array( __CLASS__, 'create_form' ),
236 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
237 'meta' => array(
238 'annotations' => array(
239 'readonly' => false,
240 'destructive' => false,
241 'idempotent' => false,
242 ),
243 'show_in_rest' => true,
244 'mcp' => array(
245 'public' => true,
246 'type' => 'tool', // default is already "tool", but can be explicit.
247 ),
248 ),
249 );
250 }
251
252 /**
253 * Spec: jetpack-forms/delete-form.
254 */
255 private static function spec_delete_form(): array {
256 return array(
257 'label' => __( 'Delete a form', 'jetpack-forms' ),
258 'description' => __( 'Move a form to the trash. Does not permanently delete. Trashed forms can be restored.', 'jetpack-forms' ),
259 'input_schema' => array(
260 'type' => 'object',
261 'required' => array( 'id' ),
262 'properties' => array(
263 'id' => array(
264 'type' => 'integer',
265 'description' => __( 'The form ID to delete.', 'jetpack-forms' ),
266 ),
267 ),
268 'additionalProperties' => false,
269 ),
270 'execute_callback' => array( __CLASS__, 'delete_form' ),
271 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
272 'meta' => array(
273 'annotations' => array(
274 'readonly' => false,
275 'destructive' => true,
276 'idempotent' => true,
277 ),
278 'show_in_rest' => true,
279 'mcp' => array(
280 'public' => true,
281 'type' => 'tool', // default is already "tool", but can be explicit.
282 ),
283 ),
284 );
285 }
286
287 /**
288 * Spec: jetpack-forms/get-responses.
289 */
290 private static function spec_get_responses(): array {
291 return array(
292 'label' => __( 'Get form responses', 'jetpack-forms' ),
293 'description' => __( 'List or search form responses. Returns response data including sender info, form fields, and metadata. Supports filtering by status, date range, read state, and search terms.', 'jetpack-forms' ),
294 'input_schema' => array(
295 'type' => 'object',
296 'default' => array(),
297 'properties' => array(
298 'ids' => array(
299 'type' => 'array',
300 'description' => __( 'Fetch specific responses by their IDs.', 'jetpack-forms' ),
301 'items' => array( 'type' => 'integer' ),
302 ),
303 'page' => array(
304 'type' => 'integer',
305 'description' => __( 'Page number for paginated results.', 'jetpack-forms' ),
306 'default' => 1,
307 'minimum' => 1,
308 ),
309 'per_page' => array(
310 'type' => 'integer',
311 'description' => __( 'Number of responses to return per page.', 'jetpack-forms' ),
312 'default' => 10,
313 'minimum' => 1,
314 'maximum' => 100,
315 ),
316 'parent' => array(
317 'type' => 'array',
318 'description' => __( 'Filter by the page or post ID where the form is embedded.', 'jetpack-forms' ),
319 'items' => array( 'type' => 'integer' ),
320 ),
321 'status' => array(
322 'type' => 'string',
323 'description' => __( 'Filter by response status.', 'jetpack-forms' ),
324 'enum' => self::RESPONSE_STATUSES,
325 ),
326 'is_unread' => array(
327 'type' => 'boolean',
328 'description' => __( 'Set true for unread only, false for read only.', 'jetpack-forms' ),
329 ),
330 'search' => array(
331 'type' => 'string',
332 'description' => __( 'Search within response content and sender info.', 'jetpack-forms' ),
333 ),
334 'before' => array(
335 'type' => 'string',
336 'description' => __( 'Only responses before this date (ISO8601 format).', 'jetpack-forms' ),
337 'format' => 'date-time',
338 ),
339 'after' => array(
340 'type' => 'string',
341 'description' => __( 'Only responses after this date (ISO8601 format).', 'jetpack-forms' ),
342 'format' => 'date-time',
343 ),
344 ),
345 'additionalProperties' => false,
346 ),
347 'execute_callback' => array( __CLASS__, 'get_form_responses' ),
348 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
349 'meta' => array(
350 'annotations' => array(
351 'readonly' => true,
352 'destructive' => false,
353 'idempotent' => true,
354 ),
355 'show_in_rest' => true,
356 'mcp' => array(
357 'public' => true,
358 'type' => 'tool', // default is already "tool", but can be explicit.
359 ),
360 ),
361 );
362 }
363
364 /**
365 * Spec: jetpack-forms/update-response.
366 */
367 private static function spec_update_response(): array {
368 return array(
369 'label' => __( 'Update form response', 'jetpack-forms' ),
370 'description' => __( 'Modify a form response. Use to mark as spam, move to trash, restore from trash, or toggle read/unread state.', 'jetpack-forms' ),
371 'input_schema' => array(
372 'type' => 'object',
373 'required' => array( 'id' ),
374 'properties' => array(
375 'id' => array(
376 'type' => 'integer',
377 'description' => __( 'The response ID to update.', 'jetpack-forms' ),
378 ),
379 'status' => array(
380 'type' => 'string',
381 'description' => __( 'New status: "publish" (restore), "spam" (mark spam), "trash" (soft delete).', 'jetpack-forms' ),
382 'enum' => self::RESPONSE_STATUSES,
383 ),
384 'is_unread' => array(
385 'type' => 'boolean',
386 'description' => __( 'Set false to mark as read, true to mark as unread.', 'jetpack-forms' ),
387 ),
388 ),
389 'additionalProperties' => false,
390 ),
391 'execute_callback' => array( __CLASS__, 'update_form_response' ),
392 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
393 'meta' => array(
394 'annotations' => array(
395 'readonly' => false,
396 'destructive' => false,
397 'idempotent' => true,
398 ),
399 'show_in_rest' => true,
400 'mcp' => array(
401 'public' => true,
402 'type' => 'tool', // default is already "tool", but can be explicit.
403 ),
404 ),
405 );
406 }
407
408 /**
409 * Spec: jetpack-forms/bulk-update-responses.
410 */
411 private static function spec_bulk_update_responses(): array {
412 return array(
413 'label' => __( 'Bulk update form responses', 'jetpack-forms' ),
414 'description' => __( 'Mark multiple responses as spam (or restore from spam) in a single call. Each response is processed individually; the result reports per-id success and any per-id failures so callers can see exactly which responses were updated. Also teaches Akismet from the successful updates.', 'jetpack-forms' ),
415 'input_schema' => array(
416 'type' => 'object',
417 'required' => array( 'action', 'ids' ),
418 'properties' => array(
419 'action' => array(
420 'type' => 'string',
421 'description' => __( 'The bulk action to perform.', 'jetpack-forms' ),
422 'enum' => self::BULK_ACTIONS,
423 ),
424 'ids' => array(
425 'type' => 'array',
426 'description' => __( 'Response IDs to update.', 'jetpack-forms' ),
427 'items' => array( 'type' => 'integer' ),
428 'minItems' => 1,
429 ),
430 ),
431 'additionalProperties' => false,
432 ),
433 'execute_callback' => array( __CLASS__, 'bulk_update_responses' ),
434 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
435 'meta' => array(
436 'annotations' => array(
437 'readonly' => false,
438 'destructive' => false,
439 'idempotent' => true,
440 ),
441 'show_in_rest' => true,
442 'mcp' => array(
443 'public' => true,
444 'type' => 'tool', // default is already "tool", but can be explicit.
445 ),
446 ),
447 );
448 }
449
450 /**
451 * Spec: jetpack-forms/get-status-counts.
452 */
453 private static function spec_get_status_counts(): array {
454 return array(
455 'label' => __( 'Get response status counts', 'jetpack-forms' ),
456 'description' => __( 'Get a summary of form responses grouped by status. Returns counts for inbox (active), spam, and trash. Useful for dashboard stats or checking if there are new responses.', 'jetpack-forms' ),
457 'input_schema' => array(
458 'type' => 'object',
459 'default' => array(),
460 'properties' => array(
461 'search' => array(
462 'type' => 'string',
463 'description' => __( 'Only count responses matching this search term.', 'jetpack-forms' ),
464 ),
465 'parent' => array(
466 'type' => 'integer',
467 'description' => __( 'Only count responses from a specific page or post.', 'jetpack-forms' ),
468 ),
469 'before' => array(
470 'type' => 'string',
471 'description' => __( 'Only count responses before this date (ISO8601 format).', 'jetpack-forms' ),
472 'format' => 'date-time',
473 ),
474 'after' => array(
475 'type' => 'string',
476 'description' => __( 'Only count responses after this date (ISO8601 format).', 'jetpack-forms' ),
477 'format' => 'date-time',
478 ),
479 'is_unread' => array(
480 'type' => 'boolean',
481 'description' => __( 'Set true to count only unread, false for only read.', 'jetpack-forms' ),
482 ),
483 ),
484 'additionalProperties' => false,
485 ),
486 'execute_callback' => array( __CLASS__, 'get_status_counts' ),
487 'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
488 'meta' => array(
489 'annotations' => array(
490 'readonly' => true,
491 'destructive' => false,
492 'idempotent' => true,
493 ),
494 'show_in_rest' => true,
495 'mcp' => array(
496 'public' => true,
497 'type' => 'tool', // default is already "tool", but can be explicit.
498 ),
499 ),
500 );
501 }
502
503 /*
504 ---------------------------------------------------------------------
505 * Permission callbacks
506 * ---------------------------------------------------------------------
507 */
508
509 /**
510 * Permission callback shared by every Forms ability. The delegated REST
511 * controller re-checks the user's edit permission per-route, so this is
512 * a coarse early-rejection gate, not the authoritative authorization.
513 *
514 * @return bool
515 */
516 public static function can_edit_pages() {
517 return current_user_can( 'edit_pages' );
518 }
519
520 /*
521 ---------------------------------------------------------------------
522 * Execute callbacks
523 * ---------------------------------------------------------------------
524 */
525
526 /**
527 * Execute: list-forms.
528 *
529 * Delegates to GET /wp/v2/jetpack-forms with dashboard context, then
530 * reshapes to a compact format for AI consumption.
531 *
532 * @param array $args Arguments from the ability input.
533 * @return array|\WP_Error
534 */
535 public static function list_forms( $args = array() ) {
536 $args = is_array( $args ) ? $args : array();
537 $request = new \WP_REST_Request( 'GET', '/wp/v2/jetpack-forms' );
538 $request->set_param( 'jetpack_forms_context', 'dashboard' );
539 self::set_params_from_args( $request, $args, array( 'page', 'per_page', 'search', 'status' ) );
540
541 $data = self::dispatch( $request );
542 if ( is_wp_error( $data ) ) {
543 return $data;
544 }
545
546 if ( ! is_array( $data ) ) {
547 return array();
548 }
549
550 $result = array();
551 foreach ( $data as $form ) {
552 $result[] = array(
553 'id' => $form['id'],
554 'title' => $form['title']['rendered'] ?? '',
555 'status' => $form['status'],
556 'entries_count' => $form['entries_count'] ?? 0,
557 'edit_url' => $form['edit_url'] ?? '',
558 'date' => $form['date'],
559 'modified' => $form['modified'],
560 );
561 }
562
563 return $result;
564 }
565
566 /**
567 * Execute: get-form.
568 *
569 * Delegates to GET /wp/v2/jetpack-forms/{id} with `context=edit` so the
570 * raw block content comes back in the response — no second `get_post()`
571 * fetch needed.
572 *
573 * @param array $args Arguments from the ability input.
574 * @return array|\WP_Error
575 */
576 public static function get_form( $args ) {
577 if ( ! isset( $args['id'] ) ) {
578 return new \WP_Error( 'missing_id', __( 'Form ID is required.', 'jetpack-forms' ) );
579 }
580
581 $request = new \WP_REST_Request( 'GET', '/wp/v2/jetpack-forms/' . absint( $args['id'] ) );
582 $request->set_param( 'context', 'edit' );
583
584 $data = self::dispatch( $request );
585 if ( is_wp_error( $data ) ) {
586 return $data;
587 }
588
589 $raw_content = $data['content']['raw'] ?? '';
590
591 return array(
592 'id' => $data['id'],
593 'title' => $data['title']['raw'] ?? $data['title']['rendered'] ?? '',
594 'status' => $data['status'],
595 'fields' => self::extract_fields_from_content( $raw_content ),
596 'date' => $data['date'],
597 'modified' => $data['modified'],
598 'edit_url' => $data['link'] ?? get_edit_post_link( $data['id'], 'raw' ),
599 );
600 }
601
602 /**
603 * Execute: create-form.
604 *
605 * @param array $args Arguments from the ability input.
606 * @return array|\WP_Error
607 */
608 public static function create_form( $args ) {
609 if ( empty( $args['title'] ) ) {
610 return new \WP_Error( 'missing_title', __( 'Form title is required.', 'jetpack-forms' ) );
611 }
612
613 $content = $args['content'] ?? '';
614 if ( '' === $content ) {
615 $content = self::DEFAULT_FORM_CONTENT;
616 }
617
618 $request = new \WP_REST_Request( 'POST', '/wp/v2/jetpack-forms' );
619 $request->set_body_params(
620 array(
621 'title' => $args['title'],
622 'content' => $content,
623 'status' => $args['status'] ?? 'publish',
624 )
625 );
626
627 $data = self::dispatch( $request );
628 if ( is_wp_error( $data ) ) {
629 return $data;
630 }
631
632 return array(
633 'id' => $data['id'],
634 'title' => $data['title']['raw'] ?? $data['title']['rendered'] ?? '',
635 'status' => $data['status'],
636 'edit_url' => get_edit_post_link( $data['id'], 'raw' ),
637 );
638 }
639
640 /**
641 * Execute: delete-form.
642 *
643 * @param array $args Arguments from the ability input.
644 * @return array|\WP_Error
645 */
646 public static function delete_form( $args ) {
647 if ( ! isset( $args['id'] ) ) {
648 return new \WP_Error( 'missing_id', __( 'Form ID is required.', 'jetpack-forms' ) );
649 }
650
651 $request = new \WP_REST_Request( 'DELETE', '/wp/v2/jetpack-forms/' . absint( $args['id'] ) );
652
653 $data = self::dispatch( $request );
654 if ( is_wp_error( $data ) ) {
655 return $data;
656 }
657
658 return array(
659 'id' => $data['id'] ?? absint( $args['id'] ),
660 'deleted' => true,
661 'status' => $data['status'] ?? 'trash',
662 );
663 }
664
665 /**
666 * Execute: get-responses.
667 *
668 * @param array $args Arguments from the ability input.
669 * @return array|\WP_Error
670 */
671 public static function get_form_responses( $args = array() ) {
672 $args = is_array( $args ) ? $args : array();
673 $request = new \WP_REST_Request( 'GET', '/wp/v2/feedback' );
674 self::set_params_from_args( $request, $args, array( 'page', 'per_page', 'parent', 'status', 'is_unread', 'search', 'before', 'after' ) );
675
676 if ( isset( $args['ids'] ) && is_array( $args['ids'] ) ) {
677 $request->set_param( 'include', $args['ids'] );
678 }
679
680 return self::dispatch( $request );
681 }
682
683 /**
684 * Execute: update-response.
685 *
686 * @param array $args Arguments from the ability input.
687 * @return array|\WP_Error
688 */
689 public static function update_form_response( $args ) {
690 if ( ! isset( $args['id'] ) ) {
691 return new \WP_Error( 'missing_id', __( 'Response ID is required.', 'jetpack-forms' ) );
692 }
693
694 $id = absint( $args['id'] );
695 $result = array();
696
697 if ( isset( $args['status'] ) ) {
698 $request = new \WP_REST_Request( 'POST', '/wp/v2/feedback/' . $id );
699 $request->set_body_params( array( 'status' => $args['status'] ) );
700 $data = self::dispatch( $request );
701 if ( is_wp_error( $data ) ) {
702 return $data;
703 }
704 $result = $data;
705 }
706
707 if ( isset( $args['is_unread'] ) ) {
708 $request = new \WP_REST_Request( 'POST', '/wp/v2/feedback/' . $id . '/read' );
709 $request->set_body_params( array( 'is_unread' => $args['is_unread'] ) );
710 $data = self::dispatch( $request );
711 if ( is_wp_error( $data ) ) {
712 return $data;
713 }
714 $result = array_merge( $result, $data );
715 }
716
717 return $result;
718 }
719
720 /**
721 * Execute: bulk-update-responses.
722 *
723 * The `/wp/v2/feedback/bulk_actions` REST endpoint only teaches Akismet —
724 * it does not change post status. The dashboard handles bulk spam/not-spam
725 * by issuing per-id status updates first, then calling bulk_actions to
726 * teach Akismet from the successful flips. We mirror that here so callers
727 * get a faithful per-id confirmation: each id either lands in `succeeded`
728 * or in `failed` with the underlying error code/message.
729 *
730 * @param array $args Arguments from the ability input.
731 * @return array|\WP_Error
732 */
733 public static function bulk_update_responses( $args ) {
734 if ( empty( $args['action'] ) || empty( $args['ids'] ) || ! is_array( $args['ids'] ) ) {
735 return new \WP_Error( 'missing_params', __( 'Action and IDs are required.', 'jetpack-forms' ) );
736 }
737
738 $action = $args['action'];
739 $target_status = 'mark_as_spam' === $action ? 'spam' : 'publish';
740 $ids = array_values( array_unique( array_map( 'absint', $args['ids'] ) ) );
741
742 $succeeded = array();
743 $failed = array();
744 foreach ( $ids as $id ) {
745 if ( $id <= 0 ) {
746 $failed[] = array(
747 'id' => $id,
748 'code' => 'invalid_id',
749 'message' => __( 'Response IDs must be positive integers.', 'jetpack-forms' ),
750 );
751 continue;
752 }
753 $request = new \WP_REST_Request( 'POST', '/wp/v2/feedback/' . $id );
754 $request->set_body_params( array( 'status' => $target_status ) );
755 $data = self::dispatch( $request );
756 if ( is_wp_error( $data ) ) {
757 $failed[] = array(
758 'id' => $id,
759 'code' => $data->get_error_code(),
760 'message' => $data->get_error_message(),
761 );
762 continue;
763 }
764 $succeeded[] = $id;
765 }
766
767 // Teach Akismet from the responses whose status actually flipped.
768 // Akismet learning is best-effort and independent of the status update,
769 // so a failure here doesn't roll back the per-id changes above.
770 if ( ! empty( $succeeded ) ) {
771 $teach = new \WP_REST_Request( 'POST', '/wp/v2/feedback/bulk_actions' );
772 $teach->set_body_params(
773 array(
774 'action' => $action,
775 'post_ids' => $succeeded,
776 )
777 );
778 self::dispatch( $teach );
779 }
780
781 return array(
782 'action' => $action,
783 'succeeded' => $succeeded,
784 'failed' => $failed,
785 );
786 }
787
788 /**
789 * Execute: get-status-counts.
790 *
791 * @param array $args Arguments from the ability input.
792 * @return array|\WP_Error
793 */
794 public static function get_status_counts( $args = array() ) {
795 $args = is_array( $args ) ? $args : array();
796 $request = new \WP_REST_Request( 'GET', '/wp/v2/feedback/counts' );
797 self::set_params_from_args( $request, $args, array( 'search', 'parent', 'before', 'after', 'is_unread' ) );
798
799 return self::dispatch( $request );
800 }
801
802 /*
803 ---------------------------------------------------------------------
804 * Helpers
805 * ---------------------------------------------------------------------
806 */
807
808 /**
809 * Dispatch an internal REST request and unwrap the response.
810 *
811 * @param \WP_REST_Request $request The REST request to dispatch.
812 * @return array|\WP_Error Response data array, or WP_Error on failure.
813 */
814 private static function dispatch( $request ) {
815 $response = rest_do_request( $request );
816 if ( $response->is_error() ) {
817 return $response->as_error();
818 }
819 return $response->get_data();
820 }
821
822 /**
823 * Copy a whitelisted subset of `$args` onto a REST request as parameters.
824 *
825 * @param \WP_REST_Request $request Target request.
826 * @param array $args Caller-supplied arguments.
827 * @param array $keys Allowed keys to forward.
828 */
829 private static function set_params_from_args( \WP_REST_Request $request, array $args, array $keys ): void {
830 foreach ( $keys as $key ) {
831 if ( isset( $args[ $key ] ) ) {
832 $request->set_param( $key, $args[ $key ] );
833 }
834 }
835 }
836
837 /**
838 * Extract field definitions from raw block content.
839 *
840 * Walks `jetpack/field-*` blocks and projects each into a compact
841 * `{ label, type, required, options?, placeholder? }` shape. Modern
842 * field blocks store the label/placeholder in `jetpack/label` and
843 * `jetpack/input` sub-blocks; legacy fixtures keep them inline as
844 * top-level attrs. Both layouts are supported.
845 *
846 * @param string $raw_content Raw block content (typically `content.raw` from REST).
847 * @return array
848 */
849 private static function extract_fields_from_content( string $raw_content ): array {
850 if ( '' === $raw_content ) {
851 return array();
852 }
853 $fields = array();
854 self::collect_field_blocks( parse_blocks( $raw_content ), $fields );
855 return $fields;
856 }
857
858 /**
859 * Recursively walk parsed blocks and append field definitions to `$fields`.
860 *
861 * Field blocks are not recursed into — their inner blocks are layout
862 * sub-blocks (`jetpack/label`, `jetpack/input`), not nested fields.
863 * Non-field containers (columns, groups, the contact-form block itself)
864 * are recursed so fields nested inside them still get picked up.
865 *
866 * @param array $blocks Parsed blocks.
867 * @param array $fields Reference to the fields array being built.
868 */
869 private static function collect_field_blocks( array $blocks, array &$fields ): void {
870 foreach ( $blocks as $block ) {
871 $name = $block['blockName'] ?? '';
872
873 if ( strpos( $name, 'jetpack/field-' ) === 0 ) {
874 $summary = self::summarize_field_block( $block );
875 if ( null !== $summary ) {
876 $fields[] = $summary;
877 }
878 continue;
879 }
880
881 if ( ! empty( $block['innerBlocks'] ) ) {
882 self::collect_field_blocks( $block['innerBlocks'], $fields );
883 }
884 }
885 }
886
887 /**
888 * Project a single `jetpack/field-*` block into the compact field shape.
889 *
890 * @param array $block Parsed block array.
891 * @return array|null Field summary, or null if the block has no usable label.
892 */
893 private static function summarize_field_block( array $block ): ?array {
894 $attrs = $block['attrs'] ?? array();
895 $inner_attrs = self::collect_inner_attrs( $block['innerBlocks'] ?? array() );
896 $label_attrs = $inner_attrs['jetpack/label'] ?? array();
897 $input_attrs = $inner_attrs['jetpack/input'] ?? array();
898
899 $label = (string) ( $label_attrs['label'] ?? $attrs['label'] ?? '' );
900 if ( '' === $label ) {
901 return null;
902 }
903
904 $field = array(
905 'label' => $label,
906 'type' => str_replace( 'jetpack/field-', '', (string) ( $block['blockName'] ?? '' ) ),
907 'required' => ! empty( $attrs['required'] ),
908 );
909
910 if ( ! empty( $attrs['options'] ) ) {
911 $field['options'] = $attrs['options'];
912 }
913
914 $placeholder = $input_attrs['placeholder'] ?? $attrs['placeholder'] ?? '';
915 if ( '' !== $placeholder ) {
916 $field['placeholder'] = $placeholder;
917 }
918
919 return $field;
920 }
921
922 /**
923 * Build a `block_name => attrs` lookup from the field's direct children.
924 *
925 * @param array $inner_blocks Direct children of a field block.
926 * @return array
927 */
928 private static function collect_inner_attrs( array $inner_blocks ): array {
929 $out = array();
930 foreach ( $inner_blocks as $child ) {
931 $name = $child['blockName'] ?? '';
932 if ( '' !== $name && ! isset( $out[ $name ] ) ) {
933 $out[ $name ] = $child['attrs'] ?? array();
934 }
935 }
936 return $out;
937 }
938 }
939