Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.36% covered (danger)
36.36%
28 / 77
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Form_Endpoint
36.36% covered (danger)
36.36%
28 / 77
42.86% covered (danger)
42.86%
3 / 7
159.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_collection_params
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 get_items
22.73% covered (danger)
22.73%
5 / 22
0.00% covered (danger)
0.00%
0 / 1
56.14
 get_entries_count_by_form_id
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 get_items_permissions_check
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 create_item_permissions_check
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
2.98
 check_read_permission
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2/**
3 * Jetpack_Form_Endpoint class.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\ContactForm;
9
10use WP_REST_Request;
11
12/**
13 * REST endpoint for the jetpack_form custom post type.
14 */
15class Jetpack_Form_Endpoint extends \WP_REST_Posts_Controller {
16    /**
17     * Cached map of form_id => entries count for the current request.
18     *
19     * @var array<int,int>|null
20     */
21    private $entries_count_by_form_id = null;
22
23    /**
24     * Constructor.
25     */
26    public function __construct() {
27        parent::__construct( Contact_Form::POST_TYPE );
28    }
29
30    /**
31     * Add opt-in dashboard fields.
32     *
33     * @return array
34     */
35    public function get_collection_params() {
36        $params = parent::get_collection_params();
37
38        // Note: We do not use the built-in WP REST "context" param for this, because it's validated
39        // against core values (view/embed/edit). This param is for Jetpack Forms dashboard usage only.
40        $params['jetpack_forms_context'] = array(
41            'description'       => __( 'Request context for Jetpack Forms. Use "dashboard" to include dashboard-only fields.', 'jetpack-forms' ),
42            'type'              => 'string',
43            'default'           => '',
44            'enum'              => array( '', 'dashboard' ),
45            'sanitize_callback' => 'sanitize_key',
46        );
47
48        return $params;
49    }
50
51    /**
52     * Return a collection of forms.
53     *
54     * We override this to compute dashboard aggregate fields in a single pass.
55     *
56     * @param WP_REST_Request $request Full details about the request.
57     * @return \WP_REST_Response|\WP_Error
58     */
59    public function get_items( $request ) {
60        $response = parent::get_items( $request );
61
62        if ( is_wp_error( $response ) ) {
63            return $response;
64        }
65
66        $forms_context = (string) $request->get_param( 'jetpack_forms_context' );
67        if ( 'dashboard' !== $forms_context ) {
68            return $response;
69        }
70
71        $forms = $response->get_data();
72        if ( ! is_array( $forms ) || empty( $forms ) ) {
73            return $response;
74        }
75
76        $form_ids = array();
77        foreach ( $forms as $form ) {
78            if ( isset( $form['id'] ) ) {
79                $form_ids[] = (int) $form['id'];
80            }
81        }
82        $form_ids = array_values( array_unique( array_filter( $form_ids ) ) );
83
84        $this->entries_count_by_form_id = $this->get_entries_count_by_form_id( $form_ids );
85
86        foreach ( $forms as &$form ) {
87            $form_id               = isset( $form['id'] ) ? (int) $form['id'] : 0;
88            $form['entries_count'] = (int) ( $this->entries_count_by_form_id[ $form_id ] ?? 0 );
89            if ( $form_id ) {
90                $form['edit_url'] = get_edit_post_link( $form_id, 'raw' );
91            }
92        }
93
94        $response->set_data( $forms );
95        return $response;
96    }
97
98    /**
99     * Batch compute feedback counts for a list of form IDs.
100     *
101     * @param int[] $form_ids Form IDs to count entries for.
102     * @return array<int,int> Map of form_id => count
103     */
104    private function get_entries_count_by_form_id( array $form_ids ): array {
105        global $wpdb;
106
107        $form_ids = array_values( array_unique( array_map( 'absint', $form_ids ) ) );
108        if ( empty( $form_ids ) ) {
109            return array();
110        }
111
112        // Count only "inbox-visible" feedback statuses.
113        // Note: This is about feedback (response) statuses, not form post statuses (publish/draft/pending/future/private).
114        $statuses = array( 'publish', 'draft' );
115
116        // Cache the grouped counts briefly to avoid repeated DB hits (e.g. on reload / concurrent requests).
117        sort( $form_ids );
118        $cache_key   = 'feedback_counts_' . md5( implode( ',', $form_ids ) . '|' . implode( ',', $statuses ) );
119        $cache_group = 'jetpack_forms';
120        $cached      = wp_cache_get( $cache_key, $cache_group );
121        if ( false !== $cached && is_array( $cached ) ) {
122            return $cached;
123        }
124
125        $args = array_merge( array( Feedback::POST_TYPE ), $form_ids, $statuses );
126
127        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
128        $rows              = $wpdb->get_results(
129            $wpdb->prepare(
130                "SELECT post_parent, COUNT(1) AS entry_count
131                FROM {$wpdb->posts}
132                WHERE post_type = %s
133                  AND post_parent IN (" . implode( ',', array_fill( 0, count( $form_ids ), '%d' ) ) . ')
134                  AND post_status IN (' . implode( ',', array_fill( 0, count( $statuses ), '%s' ) ) . ')
135                GROUP BY post_parent',
136                $args
137            )
138        );
139        $counts_by_form_id = array();
140        foreach ( (array) $rows as $row ) {
141            $counts_by_form_id[ (int) $row->post_parent ] = (int) $row->entry_count;
142        }
143
144        wp_cache_set( $cache_key, $counts_by_form_id, $cache_group, 15 ); // 15 seconds.
145        return $counts_by_form_id;
146    }
147
148    /**
149     * Checks if a given request has access to get items.
150     *
151     * @param \WP_REST_Request $request Full details about the request.
152     * @return true|\WP_Error True if the request has read access, WP_Error object otherwise.
153     */
154    public function get_items_permissions_check( $request ) {
155        $post_type = get_post_type_object( $this->post_type );
156
157        if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
158            return new \WP_Error(
159                'rest_cannot_read',
160                __( 'Sorry, you are not allowed to view forms.', 'jetpack-forms' ),
161                array( 'status' => rest_authorization_required_code() )
162            );
163        }
164
165        return parent::get_items_permissions_check( $request );
166    }
167
168    /**
169     * Checks if a given request has access to create items.
170     *
171     * @param \WP_REST_Request $request Full details about the request.
172     * @return true|\WP_Error True if the request has access to create items, WP_Error object otherwise.
173     */
174    public function create_item_permissions_check( $request ) {
175        $post_type = get_post_type_object( $this->post_type );
176
177        if ( ! current_user_can( $post_type->cap->create_posts ) ) {
178            return new \WP_Error(
179                'rest_cannot_create',
180                __( 'Sorry, you are not allowed to create forms.', 'jetpack-forms' ),
181                array( 'status' => rest_authorization_required_code() )
182            );
183        }
184
185        return parent::create_item_permissions_check( $request );
186    }
187
188    /**
189     * Checks if a jetpack-form can be read.
190     *
191     * @param \WP_Post $post Post object that backs the block.
192     * @return bool Whether the pattern can be read.
193     */
194    public function check_read_permission( $post ) {
195        // By default the read_post capability is mapped to edit_posts.
196        if ( ! current_user_can( 'read_post', $post->ID ) ) {
197            return false;
198        }
199
200        return parent::check_read_permission( $post );
201    }
202}