Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.39% covered (warning)
88.39%
198 / 224
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Forms_Abilities
88.39% covered (warning)
88.39%
198 / 224
45.45% covered (danger)
45.45%
5 / 11
29.23
0.00% covered (danger)
0.00%
0 / 1
 init
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 register_category
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 register_abilities
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 register_get_responses_ability
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
1 / 1
1
 register_update_response_ability
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
1
 register_get_status_counts_ability
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
1
 can_edit_pages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_params_from_args
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_form_responses
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 update_form_response
9.52% covered (danger)
9.52%
2 / 21
0.00% covered (danger)
0.00%
0 / 1
32.66
 get_status_counts
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
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
11namespace Automattic\Jetpack\Forms\Abilities;
12
13use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Endpoint;
14
15/**
16 * Class Forms_Abilities
17 *
18 * Registers Jetpack Forms abilities with the WordPress Abilities API.
19 * Provides abilities for managing form responses and status counts.
20 */
21class Forms_Abilities {
22
23    /**
24     * The category slug for forms abilities.
25     *
26     * @var string
27     */
28    const CATEGORY_SLUG = 'jetpack-forms';
29
30    /**
31     * Initialize the abilities registration.
32     *
33     * @return void
34     */
35    public static function init() {
36        // Register category
37        if ( did_action( 'wp_abilities_api_categories_init' ) ) {
38            self::register_category();
39        } else {
40            add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'register_category' ) );
41        }
42
43        // Register abilities
44        if ( did_action( 'wp_abilities_api_init' ) ) {
45            self::register_abilities();
46        } else {
47            add_action( 'wp_abilities_api_init', array( __CLASS__, 'register_abilities' ) );
48        }
49    }
50
51    /**
52     * Register the Jetpack Forms ability category.
53     *
54     * @return void
55     */
56    public static function register_category() {
57        if ( ! function_exists( 'wp_register_ability_category' ) ) {
58            return;
59        }
60
61        wp_register_ability_category(
62            self::CATEGORY_SLUG,
63            array(
64                // "Jetpack Forms" is a product name and should not be translated.
65                'label'       => 'Jetpack Forms',
66                'description' => __( 'Abilities for managing Jetpack Forms responses.', 'jetpack-forms' ),
67            )
68        );
69    }
70
71    /**
72     * Register all Jetpack Forms abilities.
73     *
74     * @return void
75     */
76    public static function register_abilities() {
77        if ( ! function_exists( 'wp_register_ability' ) ) {
78            return;
79        }
80
81        self::register_get_responses_ability();
82        self::register_update_response_ability();
83        self::register_get_status_counts_ability();
84    }
85
86    /**
87     * Register ability to get form responses.
88     *
89     * @return void
90     */
91    private static function register_get_responses_ability() {
92        wp_register_ability(
93            'jetpack-forms/get-responses',
94            array(
95                'label'               => __( 'Get form responses', 'jetpack-forms' ),
96                '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' ),
97                'category'            => self::CATEGORY_SLUG,
98                'input_schema'        => array(
99                    'type'                 => 'object',
100                    'default'              => array(),
101                    'properties'           => array(
102                        'ids'       => array(
103                            'type'        => 'array',
104                            'description' => __( 'Fetch specific responses by their IDs.', 'jetpack-forms' ),
105                            'items'       => array( 'type' => 'integer' ),
106                        ),
107                        'page'      => array(
108                            'type'        => 'integer',
109                            'description' => __( 'Page number for paginated results.', 'jetpack-forms' ),
110                            'default'     => 1,
111                        ),
112                        'per_page'  => array(
113                            'type'        => 'integer',
114                            'description' => __( 'Number of responses to return per page (max 100).', 'jetpack-forms' ),
115                            'default'     => 10,
116                        ),
117                        'parent'    => array(
118                            'type'        => 'array',
119                            'description' => __( 'Filter by the page or post ID where the form is embedded.', 'jetpack-forms' ),
120                            'items'       => array( 'type' => 'integer' ),
121                        ),
122                        'status'    => array(
123                            'type'        => 'string',
124                            'description' => __( 'Filter by response status.', 'jetpack-forms' ),
125                            'enum'        => array( 'publish', 'draft', 'spam', 'trash' ),
126                        ),
127                        'is_unread' => array(
128                            'type'        => 'boolean',
129                            'description' => __( 'Set true for unread only, false for read only.', 'jetpack-forms' ),
130                        ),
131                        'search'    => array(
132                            'type'        => 'string',
133                            'description' => __( 'Search within response content and sender info.', 'jetpack-forms' ),
134                        ),
135                        'before'    => array(
136                            'type'        => 'string',
137                            'description' => __( 'Only responses before this date (ISO8601 format).', 'jetpack-forms' ),
138                            'format'      => 'date-time',
139                        ),
140                        'after'     => array(
141                            'type'        => 'string',
142                            'description' => __( 'Only responses after this date (ISO8601 format).', 'jetpack-forms' ),
143                            'format'      => 'date-time',
144                        ),
145                    ),
146                    'additionalProperties' => false,
147                ),
148                'execute_callback'    => array( __CLASS__, 'get_form_responses' ),
149                'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
150                'meta'                => array(
151                    'annotations'  => array(
152                        'readonly'    => true,
153                        'destructive' => false,
154                        'idempotent'  => true,
155                    ),
156                    'show_in_rest' => true,
157                ),
158            )
159        );
160    }
161
162    /**
163     * Register ability to update a form response.
164     *
165     * @return void
166     */
167    private static function register_update_response_ability() {
168        wp_register_ability(
169            'jetpack-forms/update-response',
170            array(
171                'label'               => __( 'Update form response', 'jetpack-forms' ),
172                'description'         => __( 'Modify a form response. Use to mark as spam, move to trash, restore from trash, or toggle read/unread state.', 'jetpack-forms' ),
173                'category'            => self::CATEGORY_SLUG,
174                'input_schema'        => array(
175                    'type'                 => 'object',
176                    'required'             => array( 'id' ),
177                    'properties'           => array(
178                        'id'        => array(
179                            'type'        => 'integer',
180                            'description' => __( 'The response ID to update.', 'jetpack-forms' ),
181                        ),
182                        'status'    => array(
183                            'type'        => 'string',
184                            'description' => __( 'New status: "publish" (restore), "spam" (mark spam), "trash" (soft delete).', 'jetpack-forms' ),
185                            'enum'        => array( 'publish', 'draft', 'spam', 'trash' ),
186                        ),
187                        'is_unread' => array(
188                            'type'        => 'boolean',
189                            'description' => __( 'Set false to mark as read, true to mark as unread.', 'jetpack-forms' ),
190                        ),
191                    ),
192                    'additionalProperties' => false,
193                ),
194                'execute_callback'    => array( __CLASS__, 'update_form_response' ),
195                'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
196                'meta'                => array(
197                    'annotations'  => array(
198                        'readonly'    => false,
199                        'destructive' => false,
200                        'idempotent'  => true,
201                    ),
202                    'show_in_rest' => true,
203                ),
204            )
205        );
206    }
207
208    /**
209     * Register ability to get status counts.
210     *
211     * @return void
212     */
213    private static function register_get_status_counts_ability() {
214        wp_register_ability(
215            'jetpack-forms/get-status-counts',
216            array(
217                'label'               => __( 'Get response status counts', 'jetpack-forms' ),
218                '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' ),
219                'category'            => self::CATEGORY_SLUG,
220                'input_schema'        => array(
221                    'type'                 => 'object',
222                    'default'              => array(),
223                    'properties'           => array(
224                        'search'    => array(
225                            'type'        => 'string',
226                            'description' => __( 'Only count responses matching this search term.', 'jetpack-forms' ),
227                        ),
228                        'parent'    => array(
229                            'type'        => 'integer',
230                            'description' => __( 'Only count responses from a specific page or post.', 'jetpack-forms' ),
231                        ),
232                        'before'    => array(
233                            'type'        => 'string',
234                            'description' => __( 'Only count responses before this date (ISO8601 format).', 'jetpack-forms' ),
235                            'format'      => 'date-time',
236                        ),
237                        'after'     => array(
238                            'type'        => 'string',
239                            'description' => __( 'Only count responses after this date (ISO8601 format).', 'jetpack-forms' ),
240                            'format'      => 'date-time',
241                        ),
242                        'is_unread' => array(
243                            'type'        => 'boolean',
244                            'description' => __( 'Set true to count only unread, false for only read.', 'jetpack-forms' ),
245                        ),
246                    ),
247                    'additionalProperties' => false,
248                ),
249                'execute_callback'    => array( __CLASS__, 'get_status_counts' ),
250                'permission_callback' => array( __CLASS__, 'can_edit_pages' ),
251                'meta'                => array(
252                    'annotations'  => array(
253                        'readonly'    => true,
254                        'destructive' => false,
255                        'idempotent'  => true,
256                    ),
257                    'show_in_rest' => true,
258                ),
259            )
260        );
261    }
262
263    /**
264     * Check if user can edit pages.
265     *
266     * @return bool
267     */
268    public static function can_edit_pages() {
269        return current_user_can( 'edit_pages' );
270    }
271
272    /**
273     * Helper to set multiple parameters on a request from args array.
274     *
275     * @param \WP_REST_Request $request The request object.
276     * @param array            $args    The arguments array.
277     * @param array            $keys    The keys to copy from args to request.
278     * @return void
279     */
280    private static function set_params_from_args( $request, $args, $keys ) {
281        foreach ( $keys as $key ) {
282            if ( isset( $args[ $key ] ) ) {
283                $request->set_param( $key, $args[ $key ] );
284            }
285        }
286    }
287
288    /**
289     * Get form responses callback.
290     *
291     * @param array $args Arguments from the ability input.
292     * @return array|\WP_Error Returns array of responses or WP_Error on failure.
293     */
294    public static function get_form_responses( $args = array() ) {
295        $args     = is_array( $args ) ? $args : array();
296        $endpoint = new Contact_Form_Endpoint( 'feedback' );
297        $request  = new \WP_REST_Request( 'GET', '/wp/v2/feedback' );
298
299        self::set_params_from_args(
300            $request,
301            $args,
302            array( 'page', 'per_page', 'parent', 'status', 'is_unread', 'search', 'before', 'after' )
303        );
304
305        // Filter by specific IDs if provided
306        if ( isset( $args['ids'] ) && is_array( $args['ids'] ) ) {
307            $request->set_param( 'include', $args['ids'] );
308        }
309
310        $response = $endpoint->get_items( $request );
311        if ( is_wp_error( $response ) ) {
312            return $response;
313        }
314
315        return $response->get_data();
316    }
317
318    /**
319     * Update form response callback.
320     *
321     * @param array $args Arguments from the ability input.
322     * @return array|\WP_Error Returns updated response data or WP_Error on failure.
323     */
324    public static function update_form_response( $args ) {
325        if ( ! isset( $args['id'] ) ) {
326            return new \WP_Error( 'missing_id', __( 'Response ID is required.', 'jetpack-forms' ) );
327        }
328
329        $endpoint = new Contact_Form_Endpoint( 'feedback' );
330        $result   = array();
331
332        // Update status if provided
333        if ( isset( $args['status'] ) ) {
334            $request = new \WP_REST_Request( 'POST', '/wp/v2/feedback/' . $args['id'] );
335            $request->set_url_params( array( 'id' => $args['id'] ) );
336            $request->set_body_params( array( 'status' => $args['status'] ) );
337
338            $response = $endpoint->update_item( $request );
339            if ( is_wp_error( $response ) ) {
340                return $response;
341            }
342            $result = $response->get_data();
343        }
344
345        // Update read status if provided
346        if ( isset( $args['is_unread'] ) ) {
347            $request = new \WP_REST_Request( 'POST', '/wp/v2/feedback/' . $args['id'] . '/read' );
348            $request->set_url_params( array( 'id' => $args['id'] ) );
349            $request->set_body_params( array( 'is_unread' => $args['is_unread'] ) );
350
351            $response = $endpoint->update_read_status( $request );
352            if ( is_wp_error( $response ) ) {
353                return $response;
354            }
355            $result = array_merge( $result, $response->get_data() );
356        }
357
358        return $result;
359    }
360
361    /**
362     * Get status counts callback.
363     *
364     * @param array $args Arguments from the ability input.
365     * @return array|\WP_Error Returns status counts or WP_Error on failure.
366     */
367    public static function get_status_counts( $args = array() ) {
368        $args     = is_array( $args ) ? $args : array();
369        $endpoint = new Contact_Form_Endpoint( 'feedback' );
370        $request  = new \WP_REST_Request( 'GET', '/wp/v2/feedback/counts' );
371
372        self::set_params_from_args(
373            $request,
374            $args,
375            array( 'search', 'parent', 'before', 'after', 'is_unread' )
376        );
377
378        $response = $endpoint->get_status_counts( $request );
379        if ( $response instanceof \WP_Error ) {
380            return $response;
381        }
382
383        return (array) $response->get_data();
384    }
385}