Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.84% covered (danger)
32.84%
89 / 271
52.94% covered (warning)
52.94%
9 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dashboard
33.09% covered (danger)
33.09%
89 / 269
52.94% covered (warning)
52.94%
9 / 17
1760.32
0.00% covered (danger)
0.00%
0 / 1
 load_wp_build
68.18% covered (warning)
68.18%
15 / 22
0.00% covered (danger)
0.00%
0 / 1
7.16
 fix_boot_import_map_ordering
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 init
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 redirect_dashboard_url_cross_variant
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
306
 get_admin_query_page
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 load_admin_scripts
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
30
 add_admin_submenu
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 render_dashboard
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 has_feedback
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 get_classic_forms_state
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 detect_classic_forms
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 mark_classic_form_detected
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_forms_admin_url
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 get_forms_admin_path_wp_build
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 get_forms_admin_suffix_legacy
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 is_jetpack_forms_admin_page
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
7.46
 is_notes_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_admin_url
n/a
0 / 0
n/a
0 / 0
3
1<?php
2/**
3 * Jetpack forms dashboard.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\Dashboard;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Assets;
12use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
13use Automattic\Jetpack\Forms\ContactForm\Contact_Form;
14use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin;
15use Automattic\Jetpack\Tracking;
16use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22/**
23 * Handles the Jetpack Forms dashboard.
24 */
25class Dashboard {
26    /**
27     * Load wp-build generated files if available.
28     * This is for the new DataViews-based responses list.
29     */
30    public static function load_wp_build() {
31        // Always load for the standalone Forms page.
32        $should_load = self::get_admin_query_page() === self::FORMS_WPBUILD_ADMIN_SLUG;
33
34        /**
35         * Filter whether to load the wp-build asset registrations.
36         * Host applications (e.g., CIAB) can return true to opt in.
37         *
38         * @param bool $should_load Whether build.php should be loaded.
39         */
40        $should_load = apply_filters( 'jetpack_forms_load_wp_build', $should_load );
41
42        if ( ! $should_load ) {
43            return;
44        }
45
46        $wp_build_index = dirname( __DIR__, 2 ) . '/build/build.php';
47
48        if ( file_exists( $wp_build_index ) ) {
49            require_once $wp_build_index;
50        }
51
52        // The remaining setup only applies to the standalone Forms page.
53        if ( self::get_admin_query_page() !== self::FORMS_WPBUILD_ADMIN_SLUG ) {
54            return;
55        }
56
57        // When no route path is specified, redirect to the default view
58        // so the client-side router doesn't need a catch-all root route.
59        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
60        if ( ! isset( $_GET['p'] ) ) {
61            $default_tab = Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' )
62                ? 'forms'
63                : 'inbox';
64
65            wp_safe_redirect( self::get_forms_admin_url( $default_tab ) );
66
67            exit;
68        }
69
70        // Register polyfills for WP < 7.0 (must run before enqueue).
71        WP_Build_Polyfills::register(
72            'jetpack-forms',
73            array_merge(
74                WP_Build_Polyfills::SCRIPT_HANDLES,
75                WP_Build_Polyfills::MODULE_IDS
76            )
77        );
78    }
79
80    /**
81     * Fix import map ordering for the wp-build boot script.
82     *
83     * In wp-admin, _wp_footer_scripts (classic scripts) and print_import_map
84     * both hook into admin_print_footer_scripts at priority 10, but
85     * _wp_footer_scripts is registered first. This causes the inline
86     * import("@wordpress/boot") to execute before the import map exists.
87     *
88     * This fix moves the import() call from the classic inline script to a
89     * <script type="module"> printed at priority 20 (after the import map).
90     *
91     * @todo Remove once @wordpress/build ships with the loader.js fix upstream
92     *       (WordPress/gutenberg#76870) and Jetpack updates the dependency.
93     */
94    public static function fix_boot_import_map_ordering() {
95        $handle = self::FORMS_WPBUILD_ADMIN_SLUG . '-prerequisites';
96
97        add_action(
98            'admin_enqueue_scripts',
99            static function () use ( $handle ) {
100                if ( ! Dashboard::is_jetpack_forms_admin_page() ) {
101                    return;
102                }
103
104                $data = wp_scripts()->get_data( $handle, 'after' );
105                if ( empty( $data ) ) {
106                    return;
107                }
108
109                // Find and extract the import("@wordpress/boot") inline script.
110                $boot_script = null;
111                $remaining   = array();
112                foreach ( $data as $line ) {
113                    if ( strpos( $line, '@wordpress/boot' ) !== false ) {
114                        $boot_script = $line;
115                    } else {
116                        $remaining[] = $line;
117                    }
118                }
119
120                if ( $boot_script === null ) {
121                    return;
122                }
123
124                // Remove from the classic script handle.
125                wp_scripts()->add_data( $handle, 'after', $remaining );
126
127                // Re-emit as a module script after the import map.
128                add_action(
129                    'admin_print_footer_scripts',
130                    static function () use ( $boot_script ) {
131                        wp_print_inline_script_tag( $boot_script, array( 'type' => 'module' ) );
132                    },
133                    20
134                );
135            },
136            PHP_INT_MAX
137        );
138    }
139
140    /**
141     * Script handle for the JS file we enqueue in the Feedback admin page.
142     *
143     * @var string
144     */
145    const SCRIPT_HANDLE = 'jp-forms-dashboard';
146
147    const ADMIN_SLUG = 'jetpack-forms-admin';
148
149    /**
150     * Slug for the wp-admin integrated Responses UI (wp-build page).
151     *
152     * Note: This must be a valid submenu slug (sanitize_key compatible), not a full URL.
153     *
154     * @var string
155     */
156    const FORMS_WPBUILD_ADMIN_SLUG = 'jetpack-forms-responses-wp-admin';
157
158    /**
159     * Priority for the dashboard menu.
160     * Needs to be high enough for us to be able to unregister the default edit.php menu item.
161     *
162     * @var int
163     */
164    const MENU_PRIORITY = 999;
165
166    /**
167     * Initialize the dashboard.
168     */
169    public function init() {
170        add_action( 'admin_menu', array( $this, 'add_admin_submenu' ), self::MENU_PRIORITY );
171        add_action( 'admin_menu', array( __CLASS__, 'redirect_dashboard_url_cross_variant' ), 1 );
172
173        /**
174         * Filter to enable or disable the wp-build-based Forms dashboard.
175         *
176         * Enabled by default since Central Forms Management is now available for all sites.
177         * Can be disabled by returning false from this filter.
178         *
179         * @since 7.18.0
180         *
181         * @param bool $enabled Whether the wp-build dashboard is enabled. Default true.
182         */
183        $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
184
185        if ( $is_wp_build_enabled ) {
186            self::load_wp_build();
187            self::fix_boot_import_map_ordering();
188        }
189
190        add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
191
192        // Removed all admin notices on the Jetpack Forms admin page.
193        if ( self::get_admin_query_page() === self::ADMIN_SLUG ) {
194            remove_all_actions( 'admin_notices' );
195        }
196    }
197
198    /**
199     * Redirect dashboard URLs when the wp-build flag has changed since the link was generated.
200     *
201     * Email links may point to the legacy or wp-build dashboard. If the flag has toggled,
202     * the requested page may not exist. This redirects to the correct variant.
203     */
204    public static function redirect_dashboard_url_cross_variant() {
205        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
206        $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
207
208        if ( $page !== self::ADMIN_SLUG && $page !== self::FORMS_WPBUILD_ADMIN_SLUG ) {
209            return;
210        }
211
212        /** This filter is documented in class-dashboard.php::init */
213        $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
214
215        // Legacy URL requested but wp-build is now active â†’ redirect to wp-build.
216        if ( $page === self::ADMIN_SLUG && $is_wp_build_enabled ) {
217            // The hash is never sent to the server. "inbox" used as default tab so we end up specifically in the responses
218            // route, where the client-side router will handle the redirect to the correct status in its beforeLoad hook.
219            $redirect = self::get_forms_admin_url( 'inbox' );
220            wp_safe_redirect( $redirect );
221            exit;
222        }
223
224        // WP-Build URL requested but legacy is now active â†’ redirect to legacy.
225        if ( $page === self::FORMS_WPBUILD_ADMIN_SLUG && ! $is_wp_build_enabled ) {
226            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
227            $p                = isset( $_GET['p'] ) ? rawurldecode( sanitize_text_field( wp_unslash( $_GET['p'] ) ) ) : '';
228            $tab              = 'inbox';
229            $post_id          = null;
230            $has_mark_as_spam = false;
231
232            // Check if mark_as_spam is a separate query parameter (old email format).
233            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
234            if ( isset( $_GET['mark_as_spam'] ) ) {
235                $has_mark_as_spam = true;
236            }
237
238            if ( $p !== '' ) {
239                // Parse path like /responses/inbox?responseIds=["2879"] or /responses/inbox?responseIds=["2879"]&mark_as_spam or /forms.
240                if ( preg_match( '#^/responses/(inbox|spam|trash)(?:\?responseIds=\["(\d+)"\])?(.*)$#', $p, $m ) ) {
241                    $tab     = $m[1];
242                    $post_id = ! empty( $m[2] ) ? absint( $m[2] ) : null;
243
244                    // Check if mark_as_spam parameter is present inside the path.
245                    if ( ! empty( $m[3] ) && strpos( $m[3], 'mark_as_spam' ) !== false ) {
246                        $has_mark_as_spam = true;
247                    }
248                } elseif ( preg_match( '#^/forms#', $p ) ) {
249                    $tab = 'forms';
250                }
251            }
252
253            $redirect = self::get_forms_admin_url( $tab, $post_id );
254
255            // Add mark_as_spam parameter if it was present in the original URL (either format).
256            if ( $has_mark_as_spam ) {
257                $redirect .= '&mark_as_spam';
258            }
259
260            wp_safe_redirect( $redirect );
261            exit;
262        }
263    }
264
265    /**
266     * Get the current query 'page' parameter.
267     *
268     * @return string
269     */
270    private static function get_admin_query_page() {
271        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
272        return isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
273    }
274
275    /**
276     * Load JavaScript for the dashboard.
277     */
278    public function load_admin_scripts() {
279        if ( ! self::is_jetpack_forms_admin_page() ) {
280            return;
281        }
282
283        Assets::register_script(
284            self::SCRIPT_HANDLE,
285            '../../dist/dashboard/jetpack-forms-dashboard.js',
286            __FILE__,
287            array(
288                'in_footer'  => true,
289                'textdomain' => 'jetpack-forms',
290                'enqueue'    => true,
291            )
292        );
293
294        if ( Contact_Form_Plugin::can_use_analytics() ) {
295            Tracking::register_tracks_functions_scripts( true );
296        }
297
298        // Adds Connection package initial state.
299        Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
300
301        // Preload Forms endpoints needed in dashboard context.
302        // Pre-fetch the first inbox page so the UI renders instantly on first load.
303        $preload_params = array(
304            'context'       => 'edit',
305            'fields_format' => 'collection',
306            'order'         => 'desc',
307            'orderby'       => 'date',
308            'page'          => 1,
309            'per_page'      => 20,
310            'status'        => 'draft,publish',
311        );
312        \ksort( $preload_params );
313        $initial_responses_path        = \add_query_arg( $preload_params, '/wp/v2/feedback' );
314        $initial_responses_locale_path = \add_query_arg(
315            \array_merge(
316                $preload_params,
317                array( '_locale' => 'user' )
318            ),
319            '/wp/v2/feedback'
320        );
321        $filters_path                  = '/wp/v2/feedback/filters';
322        $filters_locale_path           = \add_query_arg( array( '_locale' => 'user' ), $filters_path );
323        $preload_paths                 = array(
324            '/wp/v2/types?context=view',
325            '/wp/v2/feedback/config',
326            '/wp/v2/feedback/integrations-metadata',
327            '/wp/v2/feedback/counts',
328            $filters_path,
329            $filters_locale_path,
330            $initial_responses_path,
331            $initial_responses_locale_path,
332        );
333
334        // Only preload the Forms list endpoint when centralized form management is enabled.
335        if ( Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ) ) {
336            $forms_preload_params = array(
337                'context'               => 'edit',
338                'page'                  => 1,
339                'jetpack_forms_context' => 'dashboard',
340                'order'                 => 'desc',
341                'orderby'               => 'modified',
342                'per_page'              => 20,
343                'status'                => 'publish,draft,pending,future,private',
344            );
345            ksort( $forms_preload_params );
346            $preload_paths[] = add_query_arg( $forms_preload_params, '/wp/v2/jetpack-forms' );
347            $preload_paths[] = add_query_arg(
348                array_merge(
349                    $forms_preload_params,
350                    array( '_locale' => 'user' )
351                ),
352                '/wp/v2/jetpack-forms'
353            );
354            $preload_paths[] = '/wp/v2/jetpack-forms/status-counts';
355            $preload_paths[] = add_query_arg( array( '_locale' => 'user' ), '/wp/v2/jetpack-forms/status-counts' );
356        }
357        $preload_data_raw = array_reduce( $preload_paths, 'rest_preload_api_request', array() );
358
359        // Normalize keys to match what apiFetch will request (without domain).
360        $preload_data = array();
361        foreach ( $preload_data_raw as $key => $value ) {
362            $normalized_key                  = preg_replace( '#^https?://[^/]+/wp-json#', '', $key );
363            $preload_data[ $normalized_key ] = $value;
364        }
365
366        wp_add_inline_script(
367            self::SCRIPT_HANDLE,
368            sprintf(
369                'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
370                wp_json_encode( $preload_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
371            ),
372            'before'
373        );
374    }
375
376    /**
377     * Register the dashboard admin submenu Forms under Jetpack menu.
378     */
379    public function add_admin_submenu() {
380
381        /** This filter is documented in class-dashboard.php::init */
382        if ( apply_filters( 'jetpack_forms_alpha', true ) ) {
383
384            // `jetpack_forms_jetpack_forms_responses_wp_admin_render_page` is the callback generated by WP build script.
385            $callback = function_exists( 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page' )
386                ? 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page'
387                : array( $this, 'render_dashboard' );
388
389            Admin_Menu::add_menu(
390                /** "Jetpack Forms" and "Forms" are product names, do not translate. */
391                'Jetpack Forms',
392                'Forms',
393                'edit_pages',
394                self::FORMS_WPBUILD_ADMIN_SLUG,
395                $callback,
396                10
397            );
398
399            return;
400        }
401
402        Admin_Menu::add_menu(
403            /** "Jetpack Forms" and "Forms" are Product names, do not translate. */
404            'Jetpack Forms',
405            'Forms',
406            'edit_pages',
407            self::ADMIN_SLUG,
408            array( $this, 'render_dashboard' ),
409            10
410        );
411    }
412
413    /**
414     * Render the dashboard.
415     */
416    public function render_dashboard() {
417        ?>
418        <div id="jp-forms-dashboard"></div>
419        <?php
420    }
421
422    /**
423     * Returns true if there are any feedback posts on the site.
424     *
425     * @return boolean
426     */
427    public function has_feedback() {
428        $posts = new \WP_Query(
429            array(
430                'post_type'              => 'feedback',
431                'post_status'            => array( 'publish', 'draft', 'spam', 'trash' ),
432                'posts_per_page'         => 1,
433                'fields'                 => 'ids',
434                'no_found_rows'          => true,
435                'update_post_meta_cache' => false,
436                'update_post_term_cache' => false,
437                'suppress_filters'       => true,
438            )
439        );
440        return $posts->have_posts();
441    }
442
443    /**
444     * Option name for storing classic forms state.
445     */
446    const CLASSIC_FORMS_OPTION = 'jetpack_forms_classic_state';
447
448    /**
449     * Classic forms state: site has classic (non-synced) form submissions.
450     */
451    const CLASSIC_FORMS_STATE_CLASSIC = 'classic';
452
453    /**
454     * Classic forms state: no classic form submissions detected.
455     */
456    const CLASSIC_FORMS_STATE_HIDDEN = 'hidden';
457
458    /**
459     * Classic forms state: user dismissed the classic forms notice.
460     */
461    const CLASSIC_FORMS_STATE_DISMISSED = 'dismissed';
462
463    /**
464     * Returns the classic forms state for the current site.
465     *
466     * Returns 'classic' if the site has form submissions (feedback posts) that were not
467     * created by a synced/reusable jetpack_form, 'dismissed' if the user dismissed the
468     * classic forms notice, or 'hidden' otherwise.
469     *
470     * The result is persisted in a WP option so the detection query only runs once per site.
471     * After that, the cached value is returned on every subsequent call. The cache is also
472     * updated eagerly via mark_classic_form_detected() when new classic submissions arrive.
473     *
474     * @since 7.14.0
475     *
476     * @return string 'classic', 'hidden', or 'dismissed'.
477     */
478    public function get_classic_forms_state() {
479        $state = get_option( self::CLASSIC_FORMS_OPTION );
480
481        if ( $state ) {
482            return $state;
483        }
484
485        $state = $this->detect_classic_forms();
486        update_option( self::CLASSIC_FORMS_OPTION, $state, false );
487
488        return $state;
489    }
490
491    /**
492     * Detects whether any feedback posts exist that are not linked to a jetpack_form post,
493     * indicating the site has classic (inline, widget, or template) forms.
494     *
495     * A feedback post is considered "classic" if:
496     * - It has no parent (post_parent = 0), meaning it was created by a form embedded in a
497     *   widget, page template, or other non-post context.
498     * - Its parent exists but is not a jetpack_form post, meaning it was created by a form
499     *   block or shortcode placed directly in a post or page.
500     *
501     * The query uses a LEFT JOIN on the posts table to find feedback posts with no matching
502     * jetpack_form parent. This leverages the primary key index for the join and the
503     * type_status_date index for filtering by post_type, making it efficient even on large
504     * sites. The LIMIT 1 ensures early exit as soon as one classic form is found.
505     *
506     * Note: An alternative approach would be to search post_content for the form block markup
507     * (<!-- wp:jetpack/contact-form) or shortcode ([contact-form]). However, that requires a
508     * full-text scan of the posts table (LIKE '%...%' on a TEXT column) with no usable index,
509     * making it significantly more expensive. The feedback-based approach also better fits the
510     * use case: we only need to surface the "Not seeing all your forms?" prompt when there are
511     * actual submissions that won't appear under any synced form in the dashboard.
512     *
513     * @since 7.14.0
514     *
515     * @return string 'classic' or 'hidden'.
516     */
517    private function detect_classic_forms() {
518        global $wpdb;
519
520        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
521        $result = $wpdb->get_var(
522            $wpdb->prepare(
523                "SELECT 1 FROM {$wpdb->posts} AS f
524                LEFT JOIN {$wpdb->posts} AS p
525                    ON p.ID = f.post_parent AND p.post_type = %s
526                WHERE f.post_type = 'feedback'
527                AND p.ID IS NULL
528                LIMIT 1",
529                Contact_Form::POST_TYPE
530            )
531        );
532
533        return $result ? self::CLASSIC_FORMS_STATE_CLASSIC : self::CLASSIC_FORMS_STATE_HIDDEN;
534    }
535
536    /**
537     * Eagerly marks the site as having classic forms by setting the option to 'classic'.
538     *
539     * Called when a new form submission is saved that does not belong to a synced jetpack_form.
540     * This avoids re-running the detection query â€” once a classic submission is observed, the
541     * state is permanently set without needing to scan the database again.
542     *
543     * If the user has already dismissed the classic forms notice, the state is left as
544     * 'dismissed' so the notice does not reappear.
545     *
546     * @since 7.14.0
547     */
548    public static function mark_classic_form_detected() {
549        $current = get_option( self::CLASSIC_FORMS_OPTION );
550
551        if ( self::CLASSIC_FORMS_STATE_DISMISSED === $current ) {
552            return;
553        }
554
555        update_option( self::CLASSIC_FORMS_OPTION, self::CLASSIC_FORMS_STATE_CLASSIC, false );
556    }
557
558    /**
559     * Returns url of forms admin page.
560     *
561     * @param string|null $tab Tab to open in the forms admin page.
562     * @param int|null    $post_id Post ID of response to open in the forms responses page.
563     *
564     * @return string
565     */
566    public static function get_forms_admin_url( $tab = null, $post_id = null ) {
567        /** This filter is documented in class-dashboard.php::init */
568        $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
569        $url                 = admin_url( 'admin.php' );
570
571        $url .= $is_wp_build_enabled
572            ? '?page=' . self::FORMS_WPBUILD_ADMIN_SLUG
573            : '?page=' . self::ADMIN_SLUG;
574
575        if ( $is_wp_build_enabled ) {
576            $path = self::get_forms_admin_path_wp_build( $tab, $post_id );
577            $url .= '&p=' . rawurlencode( $path );
578        } else {
579            $suffix = self::get_forms_admin_suffix_legacy( $tab, $post_id );
580
581            if ( $suffix !== '' ) {
582                $url .= $suffix;
583            }
584        }
585
586        /**
587         * Filters the Forms admin page URL.
588         *
589         * @module contact-form
590         * @since 7.8.0
591         *
592         * @param string      $url The Forms admin page URL.
593         * @param string|null $tab Tab to open in the forms admin page.
594         * @param int|null $post_id Post ID of response to open in the forms responses page.
595         *
596         * @return string The filtered Forms admin page URL.
597         */
598        return apply_filters( 'jetpack_forms_admin_url', $url, $tab, $post_id );
599    }
600
601    /**
602     * WP-Build path for the forms admin URL.
603     *
604     * @param string|null $tab    Tab to open.
605     * @param int|null    $post_id Post ID of response.
606     * @return string URL path (e.g. '/', '/responses/inbox', '/forms').
607     */
608    private static function get_forms_admin_path_wp_build( $tab, $post_id ) {
609        $post_id      = ! empty( $post_id ) ? absint( $post_id ) : null;
610        $response_ids = ! empty( $post_id ) ? '?responseIds=["' . $post_id . '"]' : '';
611
612        $path_map = array(
613            'inbox'           => '/responses/inbox',
614            'spam'            => '/responses/spam',
615            'trash'           => '/responses/trash',
616            'forms'           => '/forms',
617            'responses/inbox' => '/responses/inbox',
618        );
619
620        if ( $tab !== null && $tab !== '' && isset( $path_map[ $tab ] ) ) {
621            return $path_map[ $tab ] . $response_ids;
622        }
623
624        if ( ! empty( $post_id ) ) {
625            return '/responses/inbox?responseIds=["' . $post_id . '"]';
626        }
627
628        return '/responses/inbox';
629    }
630
631    /**
632     * Legacy (hash-based) URL suffix for the forms admin page.
633     *
634     * @param string|null $tab    Tab to open.
635     * @param int|null    $post_id Post ID of response.
636     * @return string URL suffix (e.g. '#/responses?status=inbox&r=123', or '#/forms').
637     */
638    private static function get_forms_admin_suffix_legacy( $tab, $post_id ) {
639        $post_id    = ! empty( $post_id ) ? absint( $post_id ) : null;
640        $valid_tabs = array( 'spam', 'inbox', 'trash' );
641        $r_param    = ! empty( $post_id ) ? '&r=' . $post_id : '';
642
643        if ( in_array( $tab, $valid_tabs, true ) ) {
644            return '#/responses?status=' . $tab . $r_param;
645        }
646
647        if ( $tab === 'forms' ) {
648            return '#/forms';
649        }
650
651        if ( ! empty( $post_id ) ) {
652            return '#/responses?status=inbox' . $r_param;
653        }
654
655        return '';
656    }
657
658    /**
659     * Returns true if the current screen is the Jetpack Forms admin page.
660     *
661     * @return boolean
662     */
663    public static function is_jetpack_forms_admin_page() {
664        if ( ! function_exists( 'get_current_screen' ) ) {
665            return false;
666        }
667
668        $screen = get_current_screen();
669
670        if ( ! $screen || ! isset( $screen->id ) ) {
671            return false;
672        }
673
674        $forms_admin_screens = array(
675            'jetpack_page_' . self::ADMIN_SLUG,
676            'jetpack_page_' . self::FORMS_WPBUILD_ADMIN_SLUG,
677        );
678
679        return in_array( $screen->id, $forms_admin_screens, true );
680    }
681
682    /**
683     * Returns true if form notes feature is enabled.
684     *
685     * @return boolean
686     */
687    public static function is_notes_enabled() {
688        /**
689        * Enable form notes feature in Jetpack Forms .
690        *
691        * @module contact-form
692        * @since 7.3.0
693        *
694        * @param bool $enabled Should the form notes feature be enabled? Defaults to false.
695        */
696        return apply_filters( 'jetpack_forms_notes_enable', false );
697    }
698
699    /**
700     * Get admin URL for given screen ID.
701     *
702     * @deprecated 7.9.0 Use Dashboard::get_forms_admin_url() instead.
703     *
704     * @param string $screen_id Screen ID.
705     * @return string Admin URL.
706     */
707    public static function get_admin_url( $screen_id ) {
708        _deprecated_function( __METHOD__, 'jetpack-7.9.0', 'Dashboard::get_forms_admin_url' );
709
710        if ( 'edit-jetpack_form' === $screen_id ) {
711            return self::get_forms_admin_url( 'forms' );
712        }
713
714        if ( 'edit-feedback' === $screen_id ) {
715            return self::get_forms_admin_url( 'inbox' );
716        }
717
718        return self::get_forms_admin_url();
719    }
720}