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