Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.26% covered (warning)
84.26%
926 / 1099
27.27% covered (danger)
27.27%
9 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Contact_Form_Endpoint
84.32% covered (warning)
84.32%
925 / 1097
27.27% covered (danger)
27.27%
9 / 33
279.09
0.00% covered (danger)
0.00%
0 / 1
 get_supported_integrations
84.29% covered (warning)
84.29%
59 / 70
0.00% covered (danger)
0.00%
0 / 1
2.02
 register_routes
99.53% covered (success)
99.53%
211 / 212
0.00% covered (danger)
0.00%
0 / 1
1
 get_source_array
6.90% covered (danger)
6.90%
2 / 29
0.00% covered (danger)
0.00%
0 / 1
16.91
 get_filters
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
1.01
 get_status_counts
78.26% covered (warning)
78.26%
36 / 46
0.00% covered (danger)
0.00%
0 / 1
11.03
 get_item_schema
100.00% covered (success)
100.00%
263 / 263
100.00% covered (success)
100.00%
1 / 1
3
 update_item
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
7.03
 resend_email
85.19% covered (warning)
85.19%
23 / 27
0.00% covered (danger)
0.00%
0 / 1
9.26
 prepare_item_for_response
98.21% covered (success)
98.21%
55 / 56
0.00% covered (danger)
0.00%
0 / 1
25
 get_items
42.86% covered (danger)
42.86%
6 / 14
0.00% covered (danger)
0.00%
0 / 1
6.99
 modify_query_for_invalid_ids
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 prepare_items_query
29.17% covered (danger)
29.17%
7 / 24
0.00% covered (danger)
0.00%
0 / 1
30.75
 join_source_meta
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 filter_by_source_id
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 get_collection_params
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
1 / 1
1
 bulk_actions
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
6.40
 delete_posts_by_status
63.33% covered (warning)
63.33%
19 / 30
0.00% covered (danger)
0.00%
0 / 1
12.99
 bulk_action_mark_as_spam
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 bulk_action_mark_as_not_spam
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 get_items_permissions_check
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
 delete_items_permissions_check
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
 get_item_permissions_check
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
5.67
 get_integration_metadata_fields
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 get_integration
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 get_single_integration_status
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 get_all_integrations_status
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 get_integrations_metadata
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_service_status
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 get_plugin_status
52.00% covered (warning)
52.00%
26 / 50
0.00% covered (danger)
0.00%
0 / 1
64.24
 disable_integration
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 update_read_status
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
4.14
 dismiss_classic_forms_notice
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_forms_config
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Contact_Form_Endpoint class.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\ContactForm;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\External_Connections;
12use Automattic\Jetpack\Forms\Dashboard\Dashboard as Forms_Dashboard;
13use Automattic\Jetpack\Forms\Jetpack_Forms;
14use Automattic\Jetpack\Forms\Service\Google_Drive;
15use Automattic\Jetpack\Forms\Service\MailPoet_Integration;
16use Automattic\Jetpack\Redirect;
17use Automattic\Jetpack\Status;
18use Automattic\Jetpack\Status\Host;
19use WP_Error;
20use WP_Query;
21use WP_REST_Request;
22use WP_REST_Response;
23
24if ( ! defined( 'ABSPATH' ) ) {
25    exit( 0 );
26}
27
28/**
29 * Class Contact_Form_Endpoint
30 * Used as 'rest_controller_class' parameter when 'feedback' post type is
31 * registered in \Automattic\Jetpack\Forms\ContactForm\Contact_Form.
32 */
33class Contact_Form_Endpoint extends \WP_REST_Posts_Controller {
34
35    /**
36     * Temporary storage for the source filter ID used in query modifications.
37     *
38     * @var int|null
39     */
40    private $temp_source_filter_id;
41
42    /**
43     * Cached SQL fragments for source filtering, to avoid recomputing per filter hook.
44     *
45     * @var array{join: string, where: string}|null
46     */
47    private $temp_source_filter_sql;
48
49    /**
50     * Get filtered list of supported integrations
51     *
52     * @return array Filtered list of supported integrations
53     */
54    private function get_supported_integrations() {
55        $supported_integrations = array(
56            'akismet'      => array(
57                'type'                    => 'plugin',
58                'file'                    => 'akismet/akismet.php',
59                'settings_url'            => 'admin.php?page=akismet-key-config',
60                'marketing_redirect_slug' => 'org-spam',
61                'title'                   => __( 'Akismet Spam Protection', 'jetpack-forms' ),
62                'subtitle'                => __( 'Akismet filters out form spam with 99% accuracy', 'jetpack-forms' ),
63                'active_tooltip'          => __( 'This form is protected with Akismet spam protection.', 'jetpack-forms' ),
64                // Overriding this may automatically enable/disable the integration when editing a form.
65                'enabled_by_default'      => false,
66                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/akismet.svg',
67            ),
68            'zero-bs-crm'  => array(
69                'type'                    => 'plugin',
70                'file'                    => 'zero-bs-crm/ZeroBSCRM.php',
71                'settings_url'            => 'admin.php?page=zerobscrm-plugin-settings',
72                'marketing_redirect_slug' => 'org-crm',
73                'title'                   => __( 'Jetpack CRM', 'jetpack-forms' ),
74                'subtitle'                => __( 'Store contact form submissions in your CRM', 'jetpack-forms' ),
75                'active_tooltip'          => __( 'Jetpack CRM is connected for this form.', 'jetpack-forms' ),
76                // Overriding this may automatically enable/disable the integration when editing a form.
77                'enabled_by_default'      => false,
78                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/zero-bs-crm.svg',
79            ),
80            'salesforce'   => array(
81                'type'                    => 'service',
82                'file'                    => null,
83                'settings_url'            => null,
84                'marketing_redirect_slug' => null,
85                'title'                   => __( 'Salesforce', 'jetpack-forms' ),
86                'subtitle'                => __( 'Send form contacts to Salesforce', 'jetpack-forms' ),
87                'active_tooltip'          => __( 'Salesforce is connected for this form.', 'jetpack-forms' ),
88                // Overriding this may automatically enable/disable the integration when editing a form.
89                'enabled_by_default'      => false,
90                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/salesforce.svg',
91            ),
92            'google-drive' => array(
93                'type'                    => 'service',
94                'file'                    => null,
95                'settings_url'            => null,
96                'marketing_redirect_slug' => null,
97                'title'                   => __( 'Google Sheets', 'jetpack-forms' ),
98                'subtitle'                => __( 'Export form responses to Google Sheets.', 'jetpack-forms' ),
99                'active_tooltip'          => __( 'Google Sheets is connected for this form.', 'jetpack-forms' ),
100                // Overriding this may automatically enable/disable the integration when editing a form.
101                'enabled_by_default'      => false,
102                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/google-drive.svg',
103            ),
104            'mailpoet'     => array(
105                'type'                    => 'plugin',
106                'file'                    => 'mailpoet/mailpoet.php',
107                'settings_url'            => 'admin.php?page=mailpoet-homepage',
108                'marketing_redirect_slug' => 'org-mailpoet',
109                'title'                   => __( 'MailPoet email marketing', 'jetpack-forms' ),
110                'subtitle'                => __( 'Send newsletters and marketing emails directly from your site.', 'jetpack-forms' ),
111                'active_tooltip'          => __( 'MailPoet is connected for this form.', 'jetpack-forms' ),
112                // Overriding this may automatically enable/disable the integration when editing a form.
113                'enabled_by_default'      => false,
114                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/mailpoet.svg',
115            ),
116        );
117
118        // Conditionally add Hostinger Reach integration behind feature flag.
119        if ( Jetpack_Forms::is_hostinger_reach_enabled() ) {
120            $supported_integrations['hostinger-reach'] = array(
121                'type'                    => 'plugin',
122                'file'                    => 'hostinger-reach/hostinger-reach.php',
123                'settings_url'            => 'admin.php?page=hostinger-reach#/home',
124                'marketing_redirect_slug' => 'hostinger-reach',
125                'title'                   => __( 'Hostinger Reach', 'jetpack-forms' ),
126                'subtitle'                => __( 'Send newsletters and marketing emails via Hostinger Reach.', 'jetpack-forms' ),
127                'active_tooltip'          => __( 'Hostinger Reach is connected for this form.', 'jetpack-forms' ),
128                // Overriding this may automatically enable/disable the integration when editing a form.
129                'enabled_by_default'      => false,
130                'icon_url'                => trailingslashit( Jetpack_Forms::assets_url() ) . 'images/integrations/hostinger-reach.svg',
131            );
132        }
133
134        /**
135         * Filters the list of supported integrations available in Jetpack Forms.
136         *
137         * Use this filter to add, modify, or remove integrations. Removing an
138         * integration here will prevent it from being returned by the REST
139         * integrations endpoints and from being displayed in the UI.
140         *
141         * @since 6.4.0
142         *
143         * @param array $integrations Associative array of integration configurations keyed by slug.
144         *                            Each configuration supports the following keys:
145         *                            - type (string)                  : 'plugin' or 'service'.
146         *                            - file (string|null)             : Plugin file path for plugins; null for services.
147         *                            - settings_url (string|null)     : Relative admin URL for settings, or null.
148         *                            - marketing_redirect_slug (string|null) : Redirect slug for marketing links, or null.
149         *                            - title (string)                 : Default UI title for the integration.
150         *                            - subtitle (string)              : Default UI subtitle/description for the integration.
151         *                            - active_tooltip (string)        : Tooltip copy for when the integration is active/connected.
152         *                            - enabled_by_default (bool)      : Whether the integration is enabled by default on new forms.
153         *                            - icon_url (string|null)         : Absolute URL to an icon to display in the UI.
154         */
155        return apply_filters( 'jetpack_forms_supported_integrations', $supported_integrations );
156    }
157
158    /**
159     * Registers the REST routes.
160     *
161     * @access public
162     */
163    public function register_routes() {
164        parent::register_routes();
165        register_rest_route(
166            $this->namespace,
167            $this->rest_base . '/filters',
168            array(
169                'methods'             => \WP_REST_Server::READABLE,
170                'callback'            => array( $this, 'get_filters' ),
171                'permission_callback' => array( $this, 'get_items_permissions_check' ),
172            )
173        );
174
175        // Register integrations routes
176        register_rest_route(
177            $this->namespace,
178            $this->rest_base . '/integrations',
179            array(
180                'methods'             => \WP_REST_Server::READABLE,
181                'callback'            => array( $this, 'get_all_integrations_status' ),
182                'permission_callback' => array( $this, 'get_items_permissions_check' ),
183                'args'                => array(
184                    'version' => array(
185                        'type'              => 'integer',
186                        'default'           => 1,
187                        'sanitize_callback' => 'absint',
188                        'validate_callback' => function ( $param ) {
189                            $version = absint( $param );
190                            return in_array( $version, array( 1, 2 ), true );
191                        },
192                    ),
193                ),
194            )
195        );
196
197        register_rest_route(
198            $this->namespace,
199            $this->rest_base . '/integrations-metadata',
200            array(
201                'methods'             => \WP_REST_Server::READABLE,
202                'callback'            => array( $this, 'get_integrations_metadata' ),
203                'permission_callback' => array( $this, 'get_items_permissions_check' ),
204            )
205        );
206
207        register_rest_route(
208            $this->namespace,
209            $this->rest_base . '/integrations/(?P<slug>[\w-]+)',
210            array(
211                'methods'             => \WP_REST_Server::READABLE,
212                'callback'            => array( $this, 'get_single_integration_status' ),
213                'permission_callback' => array( $this, 'get_items_permissions_check' ),
214                'args'                => array(
215                    'slug' => array(
216                        'type'              => 'string',
217                        'required'          => true,
218                        'sanitize_callback' => 'sanitize_text_field',
219                        'validate_callback' => function ( $param ) {
220                            return isset( $this->get_supported_integrations()[ $param ] );
221                        },
222                    ),
223                ),
224            )
225        );
226
227        register_rest_route(
228            $this->namespace,
229            $this->rest_base . '/integrations/(?P<slug>[\w-]+)',
230            array(
231                'methods'             => \WP_REST_Server::DELETABLE,
232                'callback'            => array( $this, 'disable_integration' ),
233                'permission_callback' => array( $this, 'get_items_permissions_check' ),
234                'args'                => array(
235                    'slug' => array(
236                        'type'              => 'string',
237                        'required'          => true,
238                        'sanitize_callback' => 'sanitize_text_field',
239                        'validate_callback' => function ( $param ) {
240                            return isset( $this->get_supported_integrations()[ $param ] );
241                        },
242                    ),
243                ),
244            )
245        );
246
247        register_rest_route(
248            $this->namespace,
249            $this->rest_base . '/bulk_actions',
250            array(
251                'methods'             => \WP_REST_Server::CREATABLE,
252                'callback'            => array( $this, 'bulk_actions' ),
253                'permission_callback' => array( $this, 'get_items_permissions_check' ),
254                'args'                => array(
255                    'action'   => array(
256                        'type'     => 'string',
257                        'enum'     => array(
258                            'mark_as_spam',
259                            'mark_as_not_spam',
260                        ),
261                        'required' => true,
262                    ),
263                    'post_ids' => array(
264                        'type'     => 'array',
265                        'items'    => array( 'type' => 'integer' ),
266                        'required' => true,
267                    ),
268                ),
269            )
270        );
271
272        register_rest_route(
273            $this->namespace,
274            $this->rest_base . '/trash',
275            array(
276                'methods'             => \WP_REST_Server::DELETABLE,
277                'callback'            => array( $this, 'delete_posts_by_status' ),
278                'permission_callback' => array( $this, 'delete_items_permissions_check' ),
279                'args'                => array(
280                    'status' => array(
281                        'type'     => 'string',
282                        'enum'     => array( 'trash', 'spam' ),
283                        'required' => false,
284                        'default'  => 'trash',
285                    ),
286                ),
287            )
288        );
289
290        // Forms config endpoint.
291        register_rest_route(
292            $this->namespace,
293            $this->rest_base . '/config',
294            array(
295                'methods'             => \WP_REST_Server::READABLE,
296                'permission_callback' => array( $this, 'get_items_permissions_check' ),
297                'callback'            => array( $this, 'get_forms_config' ),
298            )
299        );
300
301        // Mark feedback as read/unread endpoint.
302        register_rest_route(
303            $this->namespace,
304            $this->rest_base . '/(?P<id>\d+)/read',
305            array(
306                'methods'             => \WP_REST_Server::CREATABLE,
307                'callback'            => array( $this, 'update_read_status' ),
308                'permission_callback' => array( $this, 'update_item_permissions_check' ),
309                'args'                => array(
310                    'id'        => array(
311                        'type'              => 'integer',
312                        'required'          => true,
313                        'sanitize_callback' => 'absint',
314                    ),
315                    'is_unread' => array(
316                        'type'              => 'boolean',
317                        'required'          => true,
318                        'sanitize_callback' => 'rest_sanitize_boolean',
319                    ),
320                ),
321            )
322        );
323
324        // Dismiss the classic forms notice.
325        register_rest_route(
326            $this->namespace,
327            $this->rest_base . '/dismiss-classic-forms-notice',
328            array(
329                'methods'             => \WP_REST_Server::CREATABLE,
330                'callback'            => array( $this, 'dismiss_classic_forms_notice' ),
331                'permission_callback' => array( $this, 'get_items_permissions_check' ),
332            )
333        );
334
335        // Get optimized status counts.
336        register_rest_route(
337            $this->namespace,
338            $this->rest_base . '/counts',
339            array(
340                'methods'             => \WP_REST_Server::READABLE,
341                'permission_callback' => array( $this, 'get_items_permissions_check' ),
342                'callback'            => array( $this, 'get_status_counts' ),
343                'args'                => array(
344                    'search'    => array(
345                        'description'       => 'Limit results to those matching a string.',
346                        'type'              => 'string',
347                        'sanitize_callback' => 'sanitize_text_field',
348                        'validate_callback' => 'rest_validate_request_arg',
349                    ),
350                    'parent'    => array(
351                        'description'       => 'Limit results to those of a specific parent ID.',
352                        'type'              => 'integer',
353                        'sanitize_callback' => 'absint',
354                        'validate_callback' => 'rest_validate_request_arg',
355                    ),
356                    'before'    => array(
357                        'description'       => 'Limit results to feedback published before a given ISO8601 compliant date.',
358                        'type'              => 'string',
359                        'format'            => 'date-time',
360                        'sanitize_callback' => 'sanitize_text_field',
361                        'validate_callback' => 'rest_validate_request_arg',
362                    ),
363                    'after'     => array(
364                        'description'       => 'Limit results to feedback published after a given ISO8601 compliant date.',
365                        'type'              => 'string',
366                        'format'            => 'date-time',
367                        'sanitize_callback' => 'sanitize_text_field',
368                        'validate_callback' => 'rest_validate_request_arg',
369                    ),
370                    'is_unread' => array(
371                        'description'       => 'Limit results to read or unread feedback items.',
372                        'type'              => 'boolean',
373                        'sanitize_callback' => 'rest_sanitize_boolean',
374                        'validate_callback' => 'rest_validate_request_arg',
375                    ),
376                    'is_test'   => array(
377                        'description'       => 'Limit results to test responses or exclude them.',
378                        'type'              => 'boolean',
379                        'sanitize_callback' => 'rest_sanitize_boolean',
380                        'validate_callback' => 'rest_validate_request_arg',
381                    ),
382                    'source'    => array(
383                        'description'       => 'Limit results to feedback submitted from a specific source post ID.',
384                        'type'              => 'integer',
385                        'sanitize_callback' => 'absint',
386                        'validate_callback' => 'rest_validate_request_arg',
387                    ),
388                ),
389            )
390        );
391    }
392    /**
393     * Get source array from post IDs
394     *
395     * @param array $post_ids Array of post IDs.
396     *
397     * @return array Array of sources.
398     */
399    private static function get_source_array( $post_ids ) {
400        if ( empty( $post_ids ) ) {
401            return array();
402        }
403
404        $source_query = new WP_Query(
405            array(
406                'post__in'       => $post_ids,
407                'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private', 'inherit', 'trash' ),
408                'post_type'      => 'any',
409                'orderby'        => 'post_title',
410                'order'          => 'ASC',
411                'posts_per_page' => count( $post_ids ), // Retrieve all in the post_ids array but no more than that.
412            )
413        );
414
415        return array_map(
416            static function ( $post ) {
417                $permalink = get_permalink( $post->ID );
418                if ( $permalink === false ) {
419                    $permalink = '';
420                }
421                $status = get_post_status( $post );
422                $title  = get_the_title( $post->ID );
423                if ( 'trash' === $status ) {
424                    $title = sprintf( /* translators: %s: post title */ __( '(trashed) %s', 'jetpack-forms' ), $title );
425                }
426                return array(
427                    'id'    => $post->ID,
428                    'title' => $title,
429                    'url'   => $permalink,
430                );
431            },
432            $source_query->posts
433        );
434    }
435
436    /**
437     * Retrieves all distinct sources (posts) and all the distinct available dates that
438     * any feedback was received, in order to be used as filters in the list.
439     *
440     * @return WP_REST_Response Response object on success.
441     */
442    public function get_filters() {
443        global $wpdb;
444        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
445        $months = $wpdb->get_results(
446            "SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
447            FROM $wpdb->posts
448            WHERE post_type = 'feedback'
449            ORDER BY post_date DESC"
450        );
451
452        $source_ids = Feedback::get_all_source_post_ids();
453        // phpcs:enable
454
455        return rest_ensure_response(
456            array(
457                'date'   => array_map(
458                    static function ( $row ) {
459                        return array(
460                            'month' => (int) $row->month,
461                            'year'  => (int) $row->year,
462                        );
463                    },
464                    $months
465                ),
466                'source' => self::get_source_array( $source_ids ),
467            )
468        );
469    }
470
471    /**
472     * Retrieves status counts for inbox, spam, and trash.
473     *
474     * @param WP_REST_Request $request Full data about the request.
475     * @return WP_REST_Response Response object on success.
476     */
477    public function get_status_counts( $request ) {
478        global $wpdb;
479
480        $search    = $request->get_param( 'search' );
481        $parent    = $request->get_param( 'parent' );
482        $source    = $request->get_param( 'source' );
483        $before    = $request->get_param( 'before' );
484        $after     = $request->get_param( 'after' );
485        $is_unread = $request->get_param( 'is_unread' );
486        $is_test   = $request->get_param( 'is_test' );
487
488        $join_clause      = '';
489        $where_conditions = array( $wpdb->prepare( "{$wpdb->posts}.post_type = %s", 'feedback' ) );
490
491        if ( ! empty( $search ) ) {
492            $search_like        = '%' . $wpdb->esc_like( $search ) . '%';
493            $where_conditions[] = $wpdb->prepare( "({$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_content LIKE %s)", $search_like, $search_like );
494        }
495
496        if ( ! empty( $parent ) ) {
497            $where_conditions[] = $wpdb->prepare( "{$wpdb->posts}.post_parent = %d", $parent );
498        }
499
500        if ( ! empty( $source ) ) {
501            $source_sql         = Feedback::get_source_filter_sql( absint( $source ) );
502            $join_clause       .= $source_sql['join'];
503            $where_conditions[] = $source_sql['where'];
504        }
505
506        if ( ! empty( $before ) ) {
507            $where_conditions[] = $wpdb->prepare( "{$wpdb->posts}.post_date <= %s", $before );
508        }
509
510        if ( ! empty( $after ) ) {
511            $where_conditions[] = $wpdb->prepare( "{$wpdb->posts}.post_date >= %s", $after );
512        }
513
514        if ( null !== $is_unread ) {
515            $comment_status     = $is_unread ? Feedback::STATUS_UNREAD : Feedback::STATUS_READ;
516            $where_conditions[] = $wpdb->prepare( "{$wpdb->posts}.comment_status = %s", $comment_status );
517        }
518
519        if ( null !== $is_test ) {
520            $is_test_meta_key   = esc_sql( Feedback::IS_TEST_META_KEY );
521            $join_clause       .= " LEFT JOIN {$wpdb->postmeta} AS is_test_meta ON ({$wpdb->posts}.ID = is_test_meta.post_id AND is_test_meta.meta_key = '{$is_test_meta_key}')";
522            $where_conditions[] = $is_test ? "is_test_meta.meta_value = '1'" : 'is_test_meta.meta_id IS NULL';
523        }
524
525        $where_clause = implode( ' AND ', $where_conditions );
526
527        // Execute single query with CASE statements for all status counts.
528        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
529        $counts = $wpdb->get_row(
530            "SELECT
531            SUM(CASE WHEN {$wpdb->posts}.post_status IN ('publish', 'draft') THEN 1 ELSE 0 END) as inbox,
532            SUM(CASE WHEN {$wpdb->posts}.post_status = 'spam' THEN 1 ELSE 0 END) as spam,
533            SUM(CASE WHEN {$wpdb->posts}.post_status = 'trash' THEN 1 ELSE 0 END) as trash
534            FROM {$wpdb->posts}
535            {$join_clause}
536            WHERE {$where_clause}",
537            ARRAY_A
538        );
539        // phpcs:enable
540
541        $result = array(
542            'inbox' => (int) ( $counts['inbox'] ?? 0 ),
543            'spam'  => (int) ( $counts['spam'] ?? 0 ),
544            'trash' => (int) ( $counts['trash'] ?? 0 ),
545        );
546
547        return rest_ensure_response( $result );
548    }
549
550    /**
551     * Adds the additional fields to the item's schema.
552     *
553     * @return array Item schema as an array.
554     */
555    public function get_item_schema() {
556        $schema = parent::get_item_schema();
557
558        // Remove fields that are not relevant to feedback.
559        foreach ( array( 'link', 'password', 'template', 'title', 'content', 'excerpt' ) as $key ) {
560            if ( isset( $schema['properties'][ $key ] ) ) {
561                unset( $schema['properties'][ $key ] );
562            }
563        }
564
565        $schema['properties']['parent'] = array(
566            'description' => __( 'The ID for the parent of the post. This refers to the post/page where the feedback was created.', 'jetpack-forms' ),
567            'type'        => 'integer',
568            'context'     => array( 'view', 'edit', 'embed' ),
569            'readonly'    => true,
570        );
571
572        $schema['properties']['uid'] = array(
573            'description' => __( 'Unique identifier for the form response.', 'jetpack-forms' ),
574            'type'        => 'string',
575            'context'     => array( 'view', 'edit', 'embed' ),
576            'arg_options' => array(
577                'sanitize_callback' => 'sanitize_text_field',
578            ),
579            'readonly'    => true,
580        );
581
582        $schema['properties']['author_name'] = array(
583            'description' => __( 'The name of the person who submitted the form.', 'jetpack-forms' ),
584            'type'        => 'string',
585            'context'     => array( 'view', 'edit', 'embed' ),
586            'arg_options' => array(
587                'sanitize_callback' => 'sanitize_text_field',
588            ),
589            'readonly'    => true,
590        );
591
592        $schema['properties']['author_display_name'] = array(
593            'description' => __( 'The display name of the person who submitted the form. Either the name or the email if the name is not set.', 'jetpack-forms' ),
594            'type'        => 'string',
595            'context'     => array( 'view', 'edit', 'embed' ),
596            'arg_options' => array(
597                'sanitize_callback' => 'sanitize_text_field',
598            ),
599            'readonly'    => true,
600        );
601
602        $schema['properties']['author_email'] = array(
603            'description' => __( 'The email address of the person who submitted the form.', 'jetpack-forms' ),
604            'type'        => 'string',
605            'context'     => array( 'view', 'edit', 'embed' ),
606            'arg_options' => array(
607                'sanitize_callback' => 'sanitize_text_field',
608            ),
609            'readonly'    => true,
610        );
611
612        $schema['properties']['author_url'] = array(
613            'description' => __( 'The website URL of the person who submitted the form.', 'jetpack-forms' ),
614            'type'        => 'string',
615            'context'     => array( 'view', 'edit', 'embed' ),
616            'arg_options' => array(
617                'sanitize_callback' => 'sanitize_text_field',
618            ),
619            'readonly'    => true,
620        );
621
622        $schema['properties']['author_avatar'] = array(
623            'description' => __( 'The URL of the avatar image for the person who submitted the form.', 'jetpack-forms' ),
624            'type'        => 'string',
625            'context'     => array( 'view', 'edit', 'embed' ),
626            'arg_options' => array(
627                'sanitize_callback' => 'sanitize_text_field',
628            ),
629            'readonly'    => true,
630        );
631
632        $schema['properties']['email_marketing_consent'] = array(
633            'description' => __( 'Whether the person consented to email marketing when submitting the form.', 'jetpack-forms' ),
634            'type'        => 'string',
635            'context'     => array( 'view', 'edit', 'embed' ),
636            'arg_options' => array(
637                'sanitize_callback' => 'sanitize_text_field',
638            ),
639            'readonly'    => true,
640        );
641
642        $schema['properties']['ip'] = array(
643            'description' => __( 'The IP address from which the form was submitted.', 'jetpack-forms' ),
644            'type'        => 'string',
645            'context'     => array( 'view', 'edit', 'embed' ),
646            'arg_options' => array(
647                'sanitize_callback' => 'sanitize_text_field',
648            ),
649            'readonly'    => true,
650        );
651
652        $schema['properties']['country_code'] = array(
653            'description' => __( 'The country code derived from the IP address.', 'jetpack-forms' ),
654            'type'        => 'string',
655            'context'     => array( 'view', 'edit', 'embed' ),
656            'arg_options' => array(
657                'sanitize_callback' => 'sanitize_text_field',
658            ),
659            'readonly'    => true,
660        );
661
662        $schema['properties']['browser'] = array(
663            'description' => __( 'The browser and platform used to submit the form.', 'jetpack-forms' ),
664            'type'        => 'string',
665            'context'     => array( 'view', 'edit', 'embed' ),
666            'arg_options' => array(
667                'sanitize_callback' => 'sanitize_text_field',
668            ),
669            'readonly'    => true,
670        );
671
672        $schema['properties']['logged_in_user'] = array(
673            'description' => __( 'The logged-in user who submitted the form, if any.', 'jetpack-forms' ),
674            'type'        => array( 'object', 'null' ),
675            'context'     => array( 'view', 'edit', 'embed' ),
676            'properties'  => array(
677                'display_name' => array(
678                    'type'        => 'string',
679                    'description' => __( 'The display name of the logged-in user.', 'jetpack-forms' ),
680                    'arg_options' => array(
681                        'sanitize_callback' => 'sanitize_text_field',
682                    ),
683                ),
684                'username'     => array(
685                    'type'        => 'string',
686                    'description' => __( 'The username of the logged-in user.', 'jetpack-forms' ),
687                    'arg_options' => array(
688                        'sanitize_callback' => 'sanitize_text_field',
689                    ),
690                ),
691                'id'           => array(
692                    'type'        => 'integer',
693                    'description' => __( 'The ID of the logged-in user.', 'jetpack-forms' ),
694                    'arg_options' => array(
695                        'sanitize_callback' => 'absint',
696                    ),
697                ),
698            ),
699            'readonly'    => true,
700        );
701
702        $schema['properties']['entry_title'] = array(
703            'description' => __( 'The title of the page or post where the form was submitted.', 'jetpack-forms' ),
704            'type'        => 'string',
705            'context'     => array( 'view', 'edit', 'embed' ),
706            'arg_options' => array(
707                'sanitize_callback' => 'sanitize_text_field',
708            ),
709            'readonly'    => true,
710        );
711
712        $schema['properties']['entry_permalink'] = array(
713            'description' => __( 'The URL of the page or post where the form was submitted.', 'jetpack-forms' ),
714            'type'        => 'string',
715            'context'     => array( 'view', 'edit', 'embed' ),
716            'arg_options' => array(
717                'sanitize_callback' => 'sanitize_text_field',
718            ),
719            'readonly'    => true,
720        );
721
722        $schema['properties']['edit_form_url'] = array(
723            'description' => __( 'The URL to edit the form.', 'jetpack-forms' ),
724            'type'        => 'string',
725            'context'     => array( 'view', 'edit', 'embed' ),
726            'arg_options' => array(
727                'sanitize_callback' => 'sanitize_text_field',
728            ),
729            'readonly'    => true,
730        );
731
732        $schema['properties']['subject'] = array(
733            'description' => __( 'The subject line of the form submission.', 'jetpack-forms' ),
734            'type'        => 'string',
735            'context'     => array( 'view', 'edit', 'embed' ),
736            'arg_options' => array(
737                'sanitize_callback' => 'sanitize_text_field',
738            ),
739            'readonly'    => true,
740        );
741
742        $schema['properties']['fields'] = array(
743            'description' => __( 'The custom form fields and their submitted values.', 'jetpack-forms' ),
744            'type'        => 'object',
745            'context'     => array( 'view', 'edit', 'embed' ),
746            'arg_options' => array(
747                'sanitize_callback' => 'sanitize_text_field',
748            ),
749            'properties'  => array(
750                'files' => array(
751                    'type'       => 'object',
752                    'properties' => array(
753                        'field_id' => array(
754                            'type'        => 'string',
755                            'arg_options' => array(
756                                'sanitize_callback' => 'sanitize_text_field',
757                            ),
758                        ),
759                        'files'    => array(
760                            'type'  => 'array',
761                            'items' => array(
762                                'type'       => 'object',
763                                'properties' => array(
764                                    'file_id'        => array(
765                                        'type'        => 'integer',
766                                        'arg_options' => array(
767                                            'sanitize_callback' => 'sanitize_text_field',
768                                        ),
769                                    ),
770                                    'name'           => array(
771                                        'type'        => 'string',
772                                        'arg_options' => array(
773                                            'sanitize_callback' => 'sanitize_text_field',
774                                        ),
775                                    ),
776                                    'size'           => array(
777                                        'type'        => 'string',
778                                        'arg_options' => array(
779                                            'sanitize_callback' => 'sanitize_text_field',
780                                        ),
781                                    ),
782                                    'url'            => array(
783                                        'type'        => 'string',
784                                        'arg_options' => array(
785                                            'sanitize_callback' => 'esc_url_raw',
786                                        ),
787                                    ),
788                                    'is_previewable' => array(
789                                        'type'        => 'boolean',
790                                        'arg_options' => array(
791                                            'sanitize_callback' => 'rest_sanitize_boolean',
792                                        ),
793                                    ),
794                                ),
795                            ),
796                        ),
797                    ),
798                ),
799            ),
800            'readonly'    => true,
801        );
802
803        $schema['properties']['has_file'] = array(
804            'description' => __( 'Does the form response contain a file.', 'jetpack-forms' ),
805            'type'        => 'boolean',
806            'context'     => array( 'view', 'edit', 'embed' ),
807            'arg_options' => array(
808                'sanitize_callback' => 'booleanval',
809            ),
810            'readonly'    => true,
811        );
812
813        $schema['properties']['is_unread'] = array(
814            'description' => __( 'Whether the form response is unread.', 'jetpack-forms' ),
815            'type'        => 'boolean',
816            'context'     => array( 'view', 'edit', 'embed' ),
817            'arg_options' => array(
818                'sanitize_callback' => 'rest_sanitize_boolean',
819            ),
820            'readonly'    => true,
821        );
822
823        $schema['properties']['is_test'] = array(
824            'description' => __( 'Whether the form response was submitted from a form preview (test response).', 'jetpack-forms' ),
825            'type'        => 'boolean',
826            'context'     => array( 'view', 'edit', 'embed' ),
827            'arg_options' => array(
828                'sanitize_callback' => 'rest_sanitize_boolean',
829            ),
830            'readonly'    => true,
831        );
832
833        $schema['properties']['preview_url'] = array(
834            'description' => __( 'URL to the form preview that produced this response, when the response is a test submission.', 'jetpack-forms' ),
835            'type'        => array( 'string', 'null' ),
836            'context'     => array( 'view', 'edit', 'embed' ),
837            'arg_options' => array(
838                'sanitize_callback' => 'esc_url_raw',
839            ),
840            'readonly'    => true,
841        );
842
843        $this->schema = $schema;
844
845        return $this->add_additional_fields_schema( $this->schema );
846    }
847
848    /**
849     * Updates the item.
850     * Overrides the parent method to resend the email when the item is updated from spam to publish.
851     *
852     * @param WP_REST_Request $request Request object.
853     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
854     */
855    public function update_item( $request ) {
856        $valid_check = parent::get_post( $request['id'] );
857        if ( is_wp_error( $valid_check ) ) {
858            return $valid_check;
859        }
860
861        $post_id         = $request['id'];
862        $previous_status = get_post_status( $post_id );
863        $updated_item    = parent::update_item( $request );
864
865        if ( ! is_wp_error( $updated_item ) && ! empty( $updated_item->data && ! empty( $updated_item->data['status'] ) ) ) {
866            if ( $previous_status === 'spam' && $updated_item->data['status'] === 'publish' ) {
867                // updated item is going from spam to inbox
868                $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
869                /** This action is documented in \Automattic\Jetpack\Forms\ContactForm\Admin */
870                do_action( 'contact_form_akismet', 'ham', $akismet_values );
871                $this->resend_email( $post_id );
872            }
873        }
874        return $updated_item;
875    }
876
877    /**
878     * Resends the email for a given post ID.
879     *
880     * @param int $post_id The ID of the post to resend the email for.
881     */
882    public function resend_email( $post_id ) {
883        $comment_author_email = false;
884        $reply_to_addr        = false;
885        $message              = '';
886        $to                   = false;
887        $headers              = false;
888        $blog_url             = wp_parse_url( site_url() );
889
890        // resend the original email
891        $email = get_post_meta( $post_id, '_feedback_email', true );
892
893        $response = Feedback::get( $post_id );
894        if ( ! $response ) {
895            return;
896        }
897
898        if ( ! empty( $response->get_author_email() ) ) {
899            $comment_author_email = $response->get_author_email();
900        }
901
902        if ( isset( $email['to'] ) ) {
903            $to = $email['to'];
904        }
905
906        if ( isset( $email['message'] ) ) {
907            $message = $email['message'];
908        }
909
910        if ( isset( $email['headers'] ) ) {
911            $headers = $email['headers'];
912        } else {
913            $headers = 'From: "' . $response->get_author() . '" <wordpress@' . $blog_url['host'] . ">\r\n";
914
915            if ( ! empty( $comment_author_email ) ) {
916                $reply_to_addr = $comment_author_email;
917            } elseif ( is_array( $to ) ) {
918                $reply_to_addr = $to[0];
919            }
920
921            if ( $reply_to_addr ) {
922                $headers .= 'Reply-To: "' . $response->get_author() . '" <' . $reply_to_addr . ">\r\n";
923            }
924
925            $headers .= 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
926        }
927        Contact_Form::wp_mail( $to, $response->get_subject(), $message, $headers );
928    }
929
930    /**
931     * Prepares the item for the REST response.
932     *
933     * @param object          $item    WP Cron event.
934     * @param WP_REST_Request $request Request object.
935     * @return WP_REST_Response Response object on success.
936     */
937    public function prepare_item_for_response( $item, $request ) {
938        $response = parent::prepare_item_for_response( $item, $request );
939        $data     = $response->get_data();
940        $fields   = $this->get_fields_for_response( $request );
941
942        $feedback_response = Feedback::get( $item->ID );
943        if ( ! $feedback_response ) {
944            return rest_ensure_response( $data );
945        }
946
947        // Lazily backfill source meta for old feedback that doesn't have it yet.
948        Feedback::maybe_backfill_source_meta( $item->ID, $feedback_response );
949
950        $data['date'] = get_the_date( 'c', $data['id'] );
951        if ( rest_is_field_included( 'uid', $fields ) ) {
952            $data['uid'] = $feedback_response->get_feedback_id();
953        }
954
955        if ( rest_is_field_included( 'author_name', $fields ) ) {
956            $data['author_name'] = $feedback_response->get_author_name();
957        }
958
959        if ( rest_is_field_included( 'author_display_name', $fields ) ) {
960            $data['author_display_name'] = $feedback_response->get_author();
961        }
962
963        if ( rest_is_field_included( 'author_email', $fields ) ) {
964            $data['author_email'] = $feedback_response->get_author_email();
965        }
966
967        if ( rest_is_field_included( 'author_url', $fields ) ) {
968            $data['author_url'] = $feedback_response->get_author_url();
969        }
970
971        if ( rest_is_field_included( 'author_avatar', $fields ) ) {
972            $data['author_avatar'] = $feedback_response->get_author_avatar();
973        }
974
975        if ( rest_is_field_included( 'email_marketing_consent', $fields ) ) {
976            $data['email_marketing_consent'] = $feedback_response->has_consent() ? '1' : '';
977        }
978
979        if ( rest_is_field_included( 'ip', $fields ) ) {
980            $data['ip'] = $feedback_response->get_ip_address();
981        }
982
983        if ( rest_is_field_included( 'country_code', $fields ) ) {
984            $data['country_code'] = $feedback_response->get_country_code();
985        }
986
987        if ( rest_is_field_included( 'browser', $fields ) ) {
988            $data['browser'] = $feedback_response->get_browser();
989        }
990
991        if ( rest_is_field_included( 'logged_in_user', $fields ) ) {
992            $data['logged_in_user'] = $feedback_response->get_logged_in_user();
993        }
994
995        if ( rest_is_field_included( 'entry_title', $fields ) ) {
996            $data['entry_title'] = $feedback_response->get_entry_title();
997        }
998
999        if ( rest_is_field_included( 'entry_permalink', $fields ) ) {
1000            $data['entry_permalink'] = $feedback_response->get_entry_permalink();
1001        }
1002
1003        if ( rest_is_field_included( 'edit_form_url', $fields ) ) {
1004            $data['edit_form_url'] = $feedback_response->get_edit_form_url();
1005        }
1006
1007        if ( rest_is_field_included( 'subject', $fields ) ) {
1008            $data['subject'] = $feedback_response->get_subject();
1009        }
1010
1011        if ( rest_is_field_included( 'fields', $fields ) ) {
1012            $fields_format  = $request->get_param( 'fields_format' ) ?? 'label-value';
1013            $data['fields'] = $feedback_response->get_compiled_fields( 'api', $fields_format );
1014        }
1015
1016        if ( rest_is_field_included( 'has_file', $fields ) ) {
1017            $data['has_file'] = $feedback_response->has_file();
1018        }
1019
1020        if ( rest_is_field_included( 'is_unread', $fields ) ) {
1021            $data['is_unread'] = $feedback_response->is_unread();
1022        }
1023
1024        if ( rest_is_field_included( 'is_test', $fields ) ) {
1025            $data['is_test'] = $feedback_response->is_test();
1026        }
1027
1028        if ( rest_is_field_included( 'preview_url', $fields ) ) {
1029            $preview_url = null;
1030            if ( $feedback_response->is_test() ) {
1031                $form_id = $feedback_response->get_form_id();
1032                if ( $form_id ) {
1033                    $preview_url = Form_Preview::generate_preview_url( (int) $form_id );
1034                }
1035            }
1036            $data['preview_url'] = $preview_url;
1037        }
1038
1039        $response->set_data( $data );
1040
1041        return rest_ensure_response( $response );
1042    }
1043
1044    /**
1045     * Retrieves a collection of feedback items.
1046     * Overrides parent to support invalid_ids with OR logic.
1047     *
1048     * @param WP_REST_Request $request Full details about the request.
1049     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
1050     */
1051    public function get_items( $request ) {
1052        $invalid_ids = $request->get_param( 'invalid_ids' );
1053
1054        // If we have invalid_ids, we need to modify the query with a WHERE clause
1055        if ( ! empty( $invalid_ids ) ) {
1056            add_filter( 'posts_where', array( $this, 'modify_query_for_invalid_ids' ), 10, 2 );
1057            // Store invalid_ids temporarily so the filter can access them
1058            $this->temp_invalid_ids = $invalid_ids;
1059        }
1060
1061        $response = parent::get_items( $request );
1062
1063        // Clean up
1064        if ( ! empty( $invalid_ids ) ) {
1065            remove_filter( 'posts_where', array( $this, 'modify_query_for_invalid_ids' ), 10 );
1066            unset( $this->temp_invalid_ids );
1067        }
1068        if ( ! empty( $this->temp_source_filter_id ) ) {
1069            remove_filter( 'posts_join', array( $this, 'join_source_meta' ), 10 );
1070            remove_filter( 'posts_where', array( $this, 'filter_by_source_id' ), 10 );
1071            $this->temp_source_filter_id  = null;
1072            $this->temp_source_filter_sql = null;
1073        }
1074
1075        return $response;
1076    }
1077
1078    /**
1079     * Modify the WHERE clause to include invalid_ids with OR logic.
1080     *
1081     * @param string   $where The WHERE clause.
1082     * @param WP_Query $query The WP_Query instance.
1083     * @return string Modified WHERE clause.
1084     */
1085    public function modify_query_for_invalid_ids( $where, $query ) {
1086        global $wpdb;
1087
1088        // Only modify our feedback queries
1089        if ( ! isset( $this->temp_invalid_ids ) || empty( $this->temp_invalid_ids ) ) {
1090            return $where;
1091        }
1092
1093        // Only modify if this is a feedback query
1094        $post_type = $query->get( 'post_type' );
1095        if ( $post_type !== 'feedback' ) {
1096            return $where;
1097        }
1098
1099        $invalid_ids_sql = implode( ',', array_map( 'absint', $this->temp_invalid_ids ) );
1100
1101        // Add OR condition for invalid_ids at the end of the WHERE clause
1102        // Keep the AND at the beginning since WordPress WHERE clauses start with "AND"
1103        $where .= " OR {$wpdb->posts}.ID IN ({$invalid_ids_sql})";
1104
1105        return $where;
1106    }
1107
1108    /**
1109     * Filters the query arguments for the feedback collection.
1110     *
1111     * @param array           $args    Key value array of query var to query value.
1112     * @param WP_REST_Request $request The request used.
1113     * @return array Modified query arguments.
1114     */
1115    protected function prepare_items_query( $args = array(), $request = null ) {
1116        $args = parent::prepare_items_query( $args, $request );
1117
1118        if ( isset( $request['is_unread'] ) ) {
1119            $args['comment_status'] = $request['is_unread'] ? Feedback::STATUS_UNREAD : Feedback::STATUS_READ;
1120        }
1121
1122        // Filter by source post ID using meta (with fallback to post_parent for old data).
1123        $source = $request->get_param( 'source' );
1124        if ( ! empty( $source ) ) {
1125            $this->temp_source_filter_id  = absint( $source );
1126            $this->temp_source_filter_sql = Feedback::get_source_filter_sql( $this->temp_source_filter_id );
1127            add_filter( 'posts_join', array( $this, 'join_source_meta' ), 10, 2 );
1128            add_filter( 'posts_where', array( $this, 'filter_by_source_id' ), 10, 2 );
1129        }
1130
1131        // Filter by test/non-test responses via the _feedback_is_test meta.
1132        $is_test = $request->get_param( 'is_test' );
1133        if ( null !== $is_test ) {
1134            $meta_query = isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : array();
1135            if ( $is_test ) {
1136                $meta_query[] = array(
1137                    'key'     => Feedback::IS_TEST_META_KEY,
1138                    'value'   => '1',
1139                    'compare' => '=',
1140                );
1141            } else {
1142                $meta_query[] = array(
1143                    'key'     => Feedback::IS_TEST_META_KEY,
1144                    'compare' => 'NOT EXISTS',
1145                );
1146            }
1147            $args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1148        }
1149
1150        return $args;
1151    }
1152
1153    /**
1154     * Joins the postmeta table for source filtering.
1155     *
1156     * @param string   $join  The JOIN clause.
1157     * @param WP_Query $query The WP_Query instance.
1158     * @return string Modified JOIN clause.
1159     */
1160    public function join_source_meta( $join, $query ) {
1161        if ( empty( $this->temp_source_filter_sql ) || Feedback::POST_TYPE !== $query->get( 'post_type' ) ) {
1162            return $join;
1163        }
1164        return $join . $this->temp_source_filter_sql['join'];
1165    }
1166
1167    /**
1168     * Filters feedback by source post ID, using meta with fallback to post_parent for old data.
1169     *
1170     * @param string   $where The WHERE clause.
1171     * @param WP_Query $query The WP_Query instance.
1172     * @return string Modified WHERE clause.
1173     */
1174    public function filter_by_source_id( $where, $query ) {
1175        if ( empty( $this->temp_source_filter_sql ) || Feedback::POST_TYPE !== $query->get( 'post_type' ) ) {
1176            return $where;
1177        }
1178        return $where . ' AND ' . $this->temp_source_filter_sql['where'];
1179    }
1180
1181    /**
1182     * Retrieves the query params for the feedback collection.
1183     *
1184     * @return array Collection parameters.
1185     */
1186    public function get_collection_params() {
1187        $query_params = parent::get_collection_params();
1188
1189        // Add parent related query parameters since the `feedback` post type is not hierarchical, but
1190        // it uses the `parent` field to store the ID of the post/page where the feedback was created.
1191        $query_params['parent']         = array(
1192            'description' => __( 'Limit result set to items with particular parent IDs.', 'jetpack-forms' ),
1193            'type'        => 'array',
1194            'items'       => array(
1195                'type' => 'integer',
1196            ),
1197            'default'     => array(),
1198        );
1199        $query_params['parent_exclude'] = array(
1200            'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'jetpack-forms' ),
1201            'type'        => 'array',
1202            'items'       => array(
1203                'type' => 'integer',
1204            ),
1205            'default'     => array(),
1206        );
1207        $query_params['source']         = array(
1208            'description'       => __( 'Limit result set to feedback submitted from a particular source post ID.', 'jetpack-forms' ),
1209            'type'              => 'integer',
1210            'sanitize_callback' => 'absint',
1211            'validate_callback' => 'rest_validate_request_arg',
1212        );
1213        $query_params['is_unread']      = array(
1214            'description'       => __( 'Limit result set to read or unread feedback items.', 'jetpack-forms' ),
1215            'type'              => 'boolean',
1216            'sanitize_callback' => 'rest_sanitize_boolean',
1217            'validate_callback' => 'rest_validate_request_arg',
1218        );
1219        $query_params['is_test']        = array(
1220            'description'       => __( 'Limit result set to test responses (from form preview) or exclude them.', 'jetpack-forms' ),
1221            'type'              => 'boolean',
1222            'sanitize_callback' => 'rest_sanitize_boolean',
1223            'validate_callback' => 'rest_validate_request_arg',
1224        );
1225        $query_params['invalid_ids']    = array(
1226            'description'       => __( 'List of item IDs to include in results regardless of filters.', 'jetpack-forms' ),
1227            'type'              => 'array',
1228            'items'             => array(
1229                'type' => 'integer',
1230            ),
1231            'default'           => array(),
1232            'sanitize_callback' => function ( $param ) {
1233                return array_map( 'absint', (array) $param );
1234            },
1235            'validate_callback' => 'rest_validate_request_arg',
1236        );
1237        $query_params['fields_format']  = array(
1238            'description'       => __( 'Format for the fields data in the response.', 'jetpack-forms' ),
1239            'type'              => 'string',
1240            'enum'              => array( 'label-value', 'collection' ),
1241            'default'           => 'label-value',
1242            'sanitize_callback' => 'sanitize_text_field',
1243            'validate_callback' => 'rest_validate_request_arg',
1244        );
1245        return $query_params;
1246    }
1247
1248    /**
1249     * Handles bulk actions for Jetpack Forms responses.
1250     *
1251     * @param WP_REST_Request $request The request sent to the WP REST API.
1252     *
1253     * @return WP_REST_Response A response object..
1254     */
1255    public function bulk_actions( $request ) {
1256        $action   = $request->get_param( 'action' );
1257        $post_ids = $request->get_param( 'post_ids' );
1258
1259        if ( $action && ! is_array( $post_ids ) ) {
1260            return new WP_REST_Response( array( 'error' => __( 'Bad request', 'jetpack-forms' ) ), 400 );
1261        }
1262
1263        switch ( $action ) {
1264            case 'mark_as_spam':
1265                return $this->bulk_action_mark_as_spam( $post_ids );
1266
1267            case 'mark_as_not_spam':
1268                return $this->bulk_action_mark_as_not_spam( $post_ids );
1269
1270            default:
1271                return new WP_REST_Response( array( 'error' => __( 'Bad request', 'jetpack-forms' ) ), 400 );
1272        }
1273    }
1274
1275    /**
1276     * Handles emptying Jetpack Forms responses based on status.
1277     *
1278     * By default, it empties the trash, meaning it will delete all feedbacks in the trash (status = trash).
1279     * Passing a status will delete all feedbacks in the status.
1280     * The operation is non reversible and thus restricted to statuses spam and trash,
1281     * enforced by endpoint query args enum[spam, trash] but also double checked by the endpoint.
1282     *
1283     * @param WP_REST_Request $request The request sent to the WP REST API.
1284     *
1285     * @return WP_REST_Response A response object..
1286     */
1287    public function delete_posts_by_status( $request ) {
1288        $from_status = $request->get_param( 'status' );
1289
1290        if ( ! in_array( $from_status, array( 'spam', 'trash' ), true ) ) {
1291            return new WP_REST_Response( array( 'error' => __( 'Bad request', 'jetpack-forms' ) ), 400 );
1292        }
1293
1294        $status        = $from_status ?? 'trash';
1295        $batch_size    = 1000; // Process in batches to avoid memory issues
1296        $total_deleted = 0;
1297        $has_more      = true;
1298
1299        while ( $has_more ) {
1300            $query_args = array(
1301                'post_type'      => 'feedback',
1302                'post_status'    => $status,
1303                'posts_per_page' => $batch_size, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
1304                'fields'         => 'ids', // Only get IDs to reduce memory usage
1305            );
1306
1307            $query    = new \WP_Query( $query_args );
1308            $post_ids = $query->get_posts();
1309
1310            if ( empty( $post_ids ) ) {
1311                $has_more = false;
1312
1313                break;
1314            }
1315
1316            foreach ( $post_ids as $post_id ) {
1317                $feedback_deleted = wp_delete_post( $post_id, true );
1318
1319                if ( ! $feedback_deleted ) {
1320                    if ( $status === 'trash' ) {
1321                        return new WP_REST_Response( array( 'error' => __( 'Failed to empty trash.', 'jetpack-forms' ) ), 400 );
1322                    }
1323
1324                    if ( $status === 'spam' ) {
1325                        return new WP_REST_Response( array( 'error' => __( 'Failed to empty spam.', 'jetpack-forms' ) ), 400 );
1326                    }
1327                }
1328
1329                ++$total_deleted;
1330            }
1331
1332            if ( count( $post_ids ) < $batch_size ) {
1333                $has_more = false;
1334            }
1335        }
1336
1337        return new WP_REST_Response( array( 'deleted' => $total_deleted ), 200 );
1338    }
1339
1340    /**
1341     * Performs the Akismet action to mark all feedback posts matching the given IDs as spam.
1342     *
1343     * @param  array $post_ids Array of post IDs.
1344     * @return WP_REST_Response
1345     */
1346    private function bulk_action_mark_as_spam( $post_ids ) {
1347        foreach ( $post_ids as $post_id ) {
1348            /** This action is documented in \Automattic\Jetpack\Forms\ContactForm\Admin */
1349            do_action(
1350                'contact_form_akismet',
1351                'spam',
1352                get_post_meta( $post_id, '_feedback_akismet_values', true )
1353            );
1354        }
1355        return new WP_REST_Response( array(), 200 );
1356    }
1357
1358    /**
1359     * Performs the Akismet action to mark all feedback posts matching the given IDs as not spam.
1360     *
1361     * @param  array $post_ids Array of post IDs.
1362     * @return WP_REST_Response
1363     */
1364    private function bulk_action_mark_as_not_spam( $post_ids ) {
1365        foreach ( $post_ids as $post_id ) {
1366            /** This action is documented in \Automattic\Jetpack\Forms\ContactForm\Admin */
1367            do_action(
1368                'contact_form_akismet',
1369                'ham',
1370                get_post_meta( $post_id, '_feedback_akismet_values', true )
1371            );
1372        }
1373        return new WP_REST_Response( array(), 200 );
1374    }
1375
1376    /**
1377     * Check whether a given request has proper authorization to view and edit feedback items.
1378     *
1379     * @param  WP_REST_Request $request Full details about the request.
1380     * @return WP_Error|boolean
1381     */
1382    public function get_items_permissions_check( $request ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1383        if ( is_super_admin() ) {
1384            return true;
1385        }
1386
1387        if ( ! current_user_can( 'edit_pages' ) ) {
1388            return false;
1389        }
1390
1391        if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1392            return new WP_Error(
1393                'rest_cannot_view',
1394                esc_html__( 'Sorry, you cannot view this resource.', 'jetpack-forms' ),
1395                array( 'status' => 401 )
1396            );
1397        }
1398
1399        return true;
1400    }
1401
1402    /**
1403     * Check whether a given request has proper authorization to delete feedback items.
1404     *
1405     * @param  WP_REST_Request $request Full details about the request.
1406     * @return WP_Error|boolean
1407     */
1408    public function delete_items_permissions_check( $request ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1409        if ( is_super_admin() ) {
1410            return true;
1411        }
1412
1413        if ( ! current_user_can( 'delete_posts' ) ) {
1414            return false;
1415        }
1416
1417        if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1418            return new WP_Error(
1419                'rest_user_cannot_delete_post',
1420                esc_html__( 'Sorry, you cannot delete this resource.', 'jetpack-forms' ),
1421                array( 'status' => 401 )
1422            );
1423        }
1424
1425        return true;
1426    }
1427
1428    /**
1429     * Check whether a given request has proper authorization to view feedback item.
1430     *
1431     * @param  WP_REST_Request $request Full details about the request.
1432     * @return WP_Error|boolean
1433     */
1434    public function get_item_permissions_check( $request ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1435        if ( ! current_user_can( 'edit_pages' ) ) {
1436            return false;
1437        }
1438        if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1439            return new WP_Error(
1440                'rest_cannot_view',
1441                esc_html__( 'Sorry, you cannot view this resource.', 'jetpack-forms' ),
1442                array( 'status' => 401 )
1443            );
1444        }
1445
1446        return true;
1447    }
1448
1449    /**
1450     * Get static metadata for an integration (without status checks).
1451     *
1452     * @param string $slug Integration slug.
1453     * @param array  $config Integration configuration.
1454     * @return array Integration metadata.
1455     */
1456    private function get_integration_metadata_fields( $slug, $config ) {
1457        $type                    = $config['type'] ?? null;
1458        $marketing_redirect_slug = $config['marketing_redirect_slug'] ?? null;
1459        $icon_url                = $config['icon_url'] ?? null;
1460
1461        return array(
1462            'id'               => $slug,
1463            'slug'             => $slug,
1464            'type'             => $type,
1465            'title'            => isset( $config['title'] ) ? sanitize_text_field( $config['title'] ) : '',
1466            'subtitle'         => isset( $config['subtitle'] ) ? sanitize_text_field( $config['subtitle'] ) : '',
1467            'marketingUrl'     => $marketing_redirect_slug ? Redirect::get_url( $marketing_redirect_slug ) : null,
1468            'enabledByDefault' => isset( $config['enabled_by_default'] ) ? (bool) $config['enabled_by_default'] : false,
1469            'iconUrl'          => $icon_url ? esc_url_raw( $icon_url ) : null,
1470            'activeTooltip'    => isset( $config['active_tooltip'] ) ? sanitize_text_field( $config['active_tooltip'] ) : '',
1471        );
1472    }
1473
1474    /**
1475     * Core logic for a single integration
1476     *
1477     * @param string $slug Integration slug.
1478     * @return array Integration status data.
1479     */
1480    private function get_integration( $slug ) {
1481        $config = $this->get_supported_integrations()[ $slug ];
1482        $type   = $config['type'] ?? null;
1483
1484        // Start with metadata fields
1485        $base = $this->get_integration_metadata_fields( $slug, $config );
1486
1487        // Add status fields that require checks
1488        $base['pluginFile']      = ( $type === 'plugin' && ! empty( $config['file'] ) ) ? str_replace( '.php', '', $config['file'] ) : null;
1489        $base['isInstalled']     = false;
1490        $base['isActive']        = false;
1491        $base['needsConnection'] = ( $type === 'service' );
1492        $base['isConnected']     = false;
1493        $base['version']         = null;
1494        $base['settingsUrl']     = null;
1495        $base['details']         = array();
1496
1497        // Override base shape based on integration type.
1498        $status = $type === 'plugin'
1499            ? $this->get_plugin_status( $slug, $base )
1500            : $this->get_service_status( $slug, $base );
1501
1502        return $status;
1503    }
1504
1505    /**
1506     * REST callback for /integrations/{slug}
1507     *
1508     * @param WP_REST_Request $request Request object.
1509     * @return WP_REST_Response|WP_Error Response object or error.
1510     */
1511    public function get_single_integration_status( $request ) {
1512        $slug         = $request->get_param( 'slug' );
1513        $integrations = $this->get_supported_integrations();
1514        if ( ! isset( $integrations[ $slug ] ) ) {
1515            return new \WP_Error( 'rest_integration_not_found', __( 'Integration not found.', 'jetpack-forms' ), array( 'status' => 404 ) );
1516        }
1517        return rest_ensure_response( $this->get_integration( $slug ) );
1518    }
1519
1520    /**
1521     * REST callback for /integrations
1522     *
1523     * @param WP_REST_Request $request Request object.
1524     * @return WP_REST_Response Response object.
1525     */
1526    public function get_all_integrations_status( $request ) {
1527        $version      = absint( $request->get_param( 'version' ) );
1528        $integrations = array();
1529
1530        foreach ( $this->get_supported_integrations() as $slug => $config ) {
1531            $status = $this->get_integration( $slug );
1532            if ( 1 === $version ) {
1533                $integrations[ $slug ] = $status;
1534            } else {
1535                $integrations[] = $status;
1536            }
1537        }
1538
1539        return rest_ensure_response( $integrations );
1540    }
1541
1542    /**
1543     * REST callback for /integrations-metadata
1544     *
1545     * Returns only static metadata (name, description, type, etc.) without making
1546     * expensive calls to check connection status or plugin installation status.
1547     * This endpoint is designed to be fast and suitable for preloading.
1548     *
1549     * Uses the same field generation logic as get_integration() to ensure consistency.
1550     *
1551     * @param WP_REST_Request $request Request object.
1552     * @return WP_REST_Response Response object.
1553     */
1554    public function get_integrations_metadata( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1555        $integrations = array();
1556
1557        foreach ( $this->get_supported_integrations() as $slug => $config ) {
1558            $integrations[] = $this->get_integration_metadata_fields( $slug, $config );
1559        }
1560
1561        return rest_ensure_response( $integrations );
1562    }
1563
1564    /**
1565     * Get status for internal/service integrations.
1566     *
1567     * @param string $slug   Service slug.
1568     * @param array  $status Base status shape to mutate and return.
1569     * @return array Service status data.
1570     */
1571    private function get_service_status( $slug, array $status ) {
1572        $config = $this->get_supported_integrations()[ $slug ];
1573
1574        // If a settings redirect slug/url is configured, convert to full URL
1575        if ( ! empty( $config['settings_url'] ) ) {
1576            $status['settingsUrl'] = esc_url( Redirect::get_url( $config['settings_url'] ) );
1577        }
1578
1579        // Override base shape for specific services.
1580        switch ( $slug ) {
1581            case 'google-drive':
1582                $user_id               = get_current_user_id();
1583                $jetpack_connected     = ( new Host() )->is_wpcom_simple() || ( new Connection_Manager( 'jetpack-forms' ) )->is_user_connected( $user_id );
1584                $status['isConnected'] = $jetpack_connected && Google_Drive::has_valid_connection();
1585                $status['settingsUrl'] = External_Connections::get_connect_url( $slug );
1586                break;
1587            case 'salesforce':
1588                // No overrides needed for now; keep defaults.
1589                break;
1590            // Add other service cases as needed.
1591        }
1592
1593        return $status;
1594    }
1595
1596    /**
1597     * Get plugin status.
1598     *
1599     * @param string $plugin_slug Plugin slug.
1600     * @param array  $status      Base status shape to mutate and return.
1601     * @return array Plugin status data.
1602     */
1603    private function get_plugin_status( $plugin_slug, array $status ) {
1604        if ( ! function_exists( 'get_plugins' ) ) {
1605            require_once ABSPATH . 'wp-admin/includes/plugin.php';
1606        }
1607
1608        $integrations  = $this->get_supported_integrations();
1609        $plugin_config = $integrations[ $plugin_slug ];
1610
1611        $installed_plugins = get_plugins();
1612        $is_installed      = isset( $installed_plugins[ $plugin_config['file'] ] );
1613        $is_active         = is_plugin_active( $plugin_config['file'] );
1614
1615        // Override base shape for all plugins.
1616        $status['pluginFile']  = str_replace( '.php', '', $plugin_config['file'] );
1617        $status['isInstalled'] = $is_installed;
1618        $status['isActive']    = $is_active;
1619        $status['version']     = $is_installed ? $installed_plugins[ $plugin_config['file'] ]['Version'] : null;
1620        $status['settingsUrl'] = ( $is_active && ! empty( $plugin_config['settings_url'] ) )
1621            ? admin_url( $plugin_config['settings_url'] )
1622            : null;
1623
1624        // Override base shape for specific plugins.
1625        switch ( $plugin_slug ) {
1626            case 'akismet':
1627                $status['isConnected']                       = class_exists( 'Jetpack' ) && \Jetpack::is_akismet_active();
1628                $status['details']['formSubmissionsSpamUrl'] = Forms_Dashboard::get_forms_admin_url( 'spam' );
1629                $status['needsConnection']                   = true;
1630                break;
1631            case 'zero-bs-crm':
1632                if ( $is_active ) {
1633                    $has_extension     = function_exists( 'zeroBSCRM_isExtensionInstalled' ) && zeroBSCRM_isExtensionInstalled( 'jetpackforms' ); // @phan-suppress-current-line PhanUndeclaredFunction -- We're checking the function exists first
1634                    $status['details'] = array(
1635                        'hasExtension'         => $has_extension,
1636                        'canActivateExtension' => current_user_can( 'manage_options' ),
1637                    );
1638                }
1639                break;
1640            case 'mailpoet':
1641                $status['needsConnection'] = true;
1642                // Determine if MailPoet setup is complete using the public API.
1643                if ( class_exists( \MailPoet\API\API::class ) ) { // @phan-suppress-current-line PhanUndeclaredClassReference
1644                    $mailpoet_api = \MailPoet\API\API::MP( 'v1' ); // @phan-suppress-current-line PhanUndeclaredClassMethod
1645                    if ( $mailpoet_api && method_exists( $mailpoet_api, 'isSetupComplete' ) ) {
1646                        $status['isConnected'] = (bool) $mailpoet_api->isSetupComplete();
1647                    }
1648                }
1649                // Add MailPoet lists to details
1650                $status['details']['lists'] = MailPoet_Integration::get_all_lists();
1651                break;
1652            case 'hostinger-reach':
1653                // Hostinger Reach is a plugin that requires additional setup/connection.
1654                $status['needsConnection'] = true;
1655                $status['isConnected']     = false;
1656                // Determine if Hostinger Reach is connected using its public handler.
1657                if ( $is_active
1658                    // @phan-suppress-next-line PhanUndeclaredClassReference
1659                    && class_exists( \Hostinger\Reach\Api\Handlers\ReachApiHandler::class )
1660                    // @phan-suppress-next-line PhanUndeclaredClassReference
1661                    && class_exists( \Hostinger\Reach\Functions::class )
1662                    // @phan-suppress-next-line PhanUndeclaredClassReference
1663                    && class_exists( \Hostinger\Reach\Api\ApiKeyManager::class )
1664                ) {
1665                    $reach_handler = new \Hostinger\Reach\Api\Handlers\ReachApiHandler( // @phan-suppress-current-line PhanUndeclaredClassMethod
1666                        new \Hostinger\Reach\Functions(), // @phan-suppress-current-line PhanUndeclaredClassMethod
1667                        new \Hostinger\Reach\Api\ApiKeyManager() // @phan-suppress-current-line PhanUndeclaredClassMethod
1668                    );
1669                    if ( method_exists( $reach_handler, 'is_connected' ) ) {
1670                        // @phan-suppress-next-line PhanUndeclaredClassMethod
1671                        $status['isConnected'] = (bool) $reach_handler->is_connected();
1672                    }
1673                }
1674                break;
1675        }
1676
1677        return $status;
1678    }
1679
1680    /**
1681     * REST callback for DELETE /integrations/{slug}
1682     *
1683     * @param WP_REST_Request $request Request object.
1684     * @return WP_REST_Response|WP_Error Response object or error.
1685     */
1686    public function disable_integration( $request ) {
1687        $slug         = $request->get_param( 'slug' );
1688        $integrations = $this->get_supported_integrations();
1689        if ( ! isset( $integrations[ $slug ] ) ) {
1690            return new \WP_Error( 'rest_integration_not_found', __( 'Integration not found.', 'jetpack-forms' ), array( 'status' => 404 ) );
1691        }
1692        if ( $slug !== 'google-drive' ) {
1693            return new \WP_Error( 'rest_integration_invalid', __( 'This integration cannot be disabled.', 'jetpack-forms' ), array( 'status' => 404 ) );
1694        }
1695        $is_deleted = External_Connections::delete_connection( $slug );
1696        return rest_ensure_response( array( 'deleted' => $is_deleted ) );
1697    }
1698
1699    /**
1700     * Updates the read/unread status of a feedback item.
1701     *
1702     * @param WP_REST_Request $request Request object.
1703     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
1704     */
1705    public function update_read_status( $request ) {
1706        $post_id   = $request->get_param( 'id' );
1707        $is_unread = $request->get_param( 'is_unread' );
1708
1709        $feedback_response = Feedback::get( $post_id );
1710        if ( ! $feedback_response ) {
1711            return new WP_Error(
1712                'rest_post_invalid_id',
1713                __( 'Invalid feedback ID.', 'jetpack-forms' ),
1714                array( 'status' => 404 )
1715            );
1716        }
1717
1718        $success = $is_unread ? $feedback_response->mark_as_unread() : $feedback_response->mark_as_read();
1719
1720        Contact_Form_Plugin::recalculate_unread_count();
1721        if ( ! $success ) {
1722            return new WP_Error(
1723                'rest_cannot_update',
1724                __( 'Failed to update feedback read status.', 'jetpack-forms' ),
1725                array( 'status' => 500 )
1726            );
1727        }
1728
1729        return rest_ensure_response(
1730            array(
1731                'id'        => $post_id,
1732                'is_unread' => $feedback_response->is_unread(),
1733                'count'     => Contact_Form_Plugin::get_unread_count(),
1734            )
1735        );
1736    }
1737
1738    /**
1739     * Dismiss the classic forms notice by updating the option to 'dismissed'.
1740     *
1741     * @return WP_REST_Response
1742     */
1743    public function dismiss_classic_forms_notice() {
1744        update_option( Forms_Dashboard::CLASSIC_FORMS_OPTION, Forms_Dashboard::CLASSIC_FORMS_STATE_DISMISSED, false );
1745
1746        return rest_ensure_response( array( 'success' => true ) );
1747    }
1748
1749    /**
1750     * Return consolidated Forms config payload.
1751     *
1752     * @param WP_REST_Request $request Request.
1753     * @return WP_REST_Response
1754     */
1755    public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1756        $config = array(
1757            // Feature flags.
1758            'isCentralFormManagementEnabled' => Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ),
1759            // From jpFormsBlocks in class-contact-form-block.php.
1760            'formsResponsesUrl'              => Forms_Dashboard::get_forms_admin_url(),
1761            'isMailPoetEnabled'              => Jetpack_Forms::is_mailpoet_enabled(),
1762            'isHostingerReachEnabled'        => Jetpack_Forms::is_hostinger_reach_enabled(),
1763            // From config in class-dashboard.php.
1764            'blogId'                         => get_current_blog_id(),
1765            'gdriveConnectSupportURL'        => esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ),
1766            'pluginAssetsURL'                => Jetpack_Forms::assets_url(),
1767            'fileIconsUrl'                   => Jetpack_Forms::plugin_url() . 'contact-form/images/file-icons/',
1768            'siteURL'                        => ( new Status() )->get_site_suffix(),
1769            'hasFeedback'                    => ( new Forms_Dashboard() )->has_feedback(),
1770            'hasClassicForms'                => ( new Forms_Dashboard() )->get_classic_forms_state() === Forms_Dashboard::CLASSIC_FORMS_STATE_CLASSIC,
1771            'isNotesEnabled'                 => Forms_Dashboard::is_notes_enabled(),
1772            'isIntegrationsEnabled'          => Jetpack_Forms::is_integrations_enabled(),
1773            'isWebhooksEnabled'              => Jetpack_Forms::is_webhooks_enabled(),
1774            'showDashboardIntegrations'      => Jetpack_Forms::show_dashboard_integrations(),
1775            'showBlockIntegrations'          => Jetpack_Forms::show_block_integrations(),
1776            'showIntegrationIcons'           => Jetpack_Forms::show_integration_icons(),
1777            'dashboardURL'                   => Forms_Dashboard::get_forms_admin_url(),
1778            // New data.
1779            'canInstallPlugins'              => current_user_can( 'install_plugins' ),
1780            'canActivatePlugins'             => current_user_can( 'activate_plugins' ),
1781            'exportNonce'                    => wp_create_nonce( 'feedback_export' ),
1782            'newFormNonce'                   => wp_create_nonce( 'create_new_form' ),
1783            'emptyTrashDays'                 => defined( 'EMPTY_TRASH_DAYS' ) ? EMPTY_TRASH_DAYS : 0,
1784            // Admin URLs for external admin contexts.
1785            'adminUrl'                       => admin_url(),
1786            'ajaxUrl'                        => admin_url( 'admin-ajax.php' ),
1787        );
1788
1789        return rest_ensure_response( $config );
1790    }
1791}