PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-forms / src / dashboard / class-dashboard.php

class-dashboard.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at jetpack_vendor/automattic/jetpack-forms/src/dashboard/class-dashboard.php

729 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Jetpack forms dashboard.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8 namespace Automattic\Jetpack\Forms\Dashboard;
9
10 use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 use Automattic\Jetpack\Assets;
12 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
13 use Automattic\Jetpack\Forms\ContactForm\Contact_Form;
14 use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin;
15 use Automattic\Jetpack\Tracking;
16 use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit( 0 );
20 }
21
22 /**
23 * Handles the Jetpack Forms dashboard.
24 */
25 class 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 = self::wp_build_index_path();
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 * Overrides the generated entry point path. Test seam, null in production.
82 *
83 * Private with no setter: tests reach it by reflection, so nothing outside the
84 * package can point the dashboard at another file.
85 *
86 * @var string|null
87 */
88 private static $wp_build_index = null;
89
90 /**
91 * Absolute path to the entry point generated by the WP build script.
92 *
93 * @return string
94 */
95 private static function wp_build_index_path() {
96 return self::$wp_build_index ?? dirname( __DIR__, 2 ) . '/build/build.php';
97 }
98
99 /**
100 * Script handle for the JS file we enqueue in the Feedback admin page.
101 *
102 * @var string
103 */
104 const SCRIPT_HANDLE = 'jp-forms-dashboard';
105
106 const ADMIN_SLUG = 'jetpack-forms-admin';
107
108 /**
109 * Slug for the wp-admin integrated Responses UI (wp-build page).
110 *
111 * Note: This must be a valid submenu slug (sanitize_key compatible), not a full URL.
112 *
113 * @var string
114 */
115 const FORMS_WPBUILD_ADMIN_SLUG = 'jetpack-forms-responses-wp-admin';
116
117 /**
118 * Priority for the dashboard menu.
119 * Needs to be high enough for us to be able to unregister the default edit.php menu item.
120 *
121 * @var int
122 */
123 const MENU_PRIORITY = 999;
124
125 /**
126 * Initialize the dashboard.
127 */
128 public function init() {
129 add_action( 'admin_menu', array( $this, 'add_admin_submenu' ), self::MENU_PRIORITY );
130 add_action( 'admin_menu', array( __CLASS__, 'redirect_dashboard_url_cross_variant' ), 1 );
131 add_action( 'admin_notices', array( __CLASS__, 'announce_retired_filter' ) );
132
133 self::load_wp_build();
134
135 add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
136 }
137
138 /**
139 * Tell anyone still filtering `jetpack_forms_alpha` that it no longer does anything.
140 *
141 * The filter gated the wp-build dashboard while it was in development. That dashboard
142 * is now the only one, so a `false` return has nothing left to select and is ignored.
143 *
144 * Announced rather than applied: _deprecated_hook() reports the hook without honoring
145 * it, where apply_filters_deprecated() would return a `false` this code can no longer
146 * act on. Guarded by has_filter() so sites that never used it stay silent.
147 *
148 * Hooked to `admin_notices` rather than called from init(). This class loads at
149 * `after_setup_theme` priority -2, so with WP_DEBUG display on the notice would print
150 * — and send headers — before load_wp_build() and redirect_dashboard_url_cross_variant()
151 * get to redirect, leaving both on "headers already sent" and a blank page.
152 *
153 * That hook also fires only while an admin screen renders, so the notice stays out of
154 * admin-ajax and admin-post responses, which `is_admin()` would have let through. And
155 * it runs late enough that has_filter() sees callbacks registered on `init`, not just
156 * those added at file scope.
157 *
158 * @since 8.1.0
159 */
160 public static function announce_retired_filter() {
161 if ( ! has_filter( 'jetpack_forms_alpha' ) ) {
162 return;
163 }
164
165 // Kept on one line: replace-next-version-tag.sh only recognizes the token in a
166 // single-line deprecation call, and errors the build out otherwise.
167 _deprecated_hook( 'jetpack_forms_alpha', 'jetpack-forms-8.1.0', '', 'The legacy Forms dashboard has been removed, so this filter no longer selects anything.' );
168 }
169
170 /**
171 * Send legacy dashboard URLs to the wp-build dashboard.
172 *
173 * Load-bearing, not a courtesy for stale links: Creative Mail's JITMs and post-install
174 * redirect, and My Jetpack's fallback URL, still emit the legacy slug today. Keep it.
175 */
176 public static function redirect_dashboard_url_cross_variant() {
177 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
178 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
179
180 if ( $page !== self::ADMIN_SLUG ) {
181 return;
182 }
183
184 // The hash is never sent to the server. "inbox" used as default tab so we end up specifically in the responses
185 // route, where the client-side router will handle the redirect to the correct status in its beforeLoad hook.
186 wp_safe_redirect( self::get_forms_admin_url( 'inbox' ) );
187 exit;
188 }
189
190 /**
191 * Get the current query 'page' parameter.
192 *
193 * @return string
194 */
195 private static function get_admin_query_page() {
196 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
197 return isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
198 }
199
200 /**
201 * Load JavaScript for the dashboard.
202 */
203 public function load_admin_scripts() {
204 if ( ! self::is_jetpack_forms_admin_page() ) {
205 return;
206 }
207
208 // The wp-build (script-module) dashboard renders its own UI from build/pages/…,
209 // so the legacy SPA bundle is dead weight there. Only enqueue it on the legacy
210 // dashboard. The shared inline data below (connection initial state + REST
211 // preload) is instead attached to the always-present wp-api-fetch handle so the
212 // wp-build app still receives it.
213 if ( self::is_wp_build_dashboard_page() ) {
214 $inline_handle = 'wp-api-fetch';
215 $preload_position = 'after';
216
217 // The i18n loader is registered on every admin page by jetpack-assets but
218 // only enqueued when depended on; the esbuild bundles don't pull it in.
219 // Enqueue it so the wp-build dashboard's init module can download its JS
220 // translation catalogs.
221 if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
222 wp_enqueue_script( 'wp-jp-i18n-loader' );
223 }
224 } else {
225 $inline_handle = self::SCRIPT_HANDLE;
226 $preload_position = 'before';
227
228 Assets::register_script(
229 self::SCRIPT_HANDLE,
230 '../../dist/dashboard/jetpack-forms-dashboard.js',
231 __FILE__,
232 array(
233 'in_footer' => true,
234 'textdomain' => 'jetpack-forms',
235 'enqueue' => true,
236 )
237 );
238 }
239
240 if ( Contact_Form_Plugin::can_use_analytics() ) {
241 Tracking::register_tracks_functions_scripts( true );
242 }
243
244 // Adds Connection package initial state.
245 Connection_Initial_State::render_script( $inline_handle );
246
247 // Preload Forms endpoints needed in dashboard context.
248 // Pre-fetch the first inbox page so the UI renders instantly on first load.
249 $preload_params = array(
250 'context' => 'edit',
251 'fields_format' => 'collection',
252 'order' => 'desc',
253 'orderby' => 'date',
254 'page' => 1,
255 'per_page' => 20,
256 'status' => 'draft,publish',
257 );
258 \ksort( $preload_params );
259 $initial_responses_path = \add_query_arg( $preload_params, '/wp/v2/feedback' );
260 $initial_responses_locale_path = \add_query_arg(
261 \array_merge(
262 $preload_params,
263 array( '_locale' => 'user' )
264 ),
265 '/wp/v2/feedback'
266 );
267 $filters_path = '/wp/v2/feedback/filters';
268 $filters_locale_path = \add_query_arg( array( '_locale' => 'user' ), $filters_path );
269 $preload_paths = array(
270 '/wp/v2/types?context=view',
271 '/wp/v2/feedback/config',
272 '/wp/v2/feedback/integrations-metadata',
273 '/wp/v2/feedback/counts',
274 $filters_path,
275 $filters_locale_path,
276 $initial_responses_path,
277 $initial_responses_locale_path,
278 );
279
280 // Only preload the Forms list endpoint when centralized form management is enabled.
281 if ( Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ) ) {
282 $forms_preload_params = array(
283 'context' => 'edit',
284 'page' => 1,
285 'jetpack_forms_context' => 'dashboard',
286 'order' => 'desc',
287 'orderby' => 'modified',
288 'per_page' => 20,
289 'status' => 'publish,draft,pending,future,private',
290 );
291 ksort( $forms_preload_params );
292 $preload_paths[] = add_query_arg( $forms_preload_params, '/wp/v2/jetpack-forms' );
293 $preload_paths[] = add_query_arg(
294 array_merge(
295 $forms_preload_params,
296 array( '_locale' => 'user' )
297 ),
298 '/wp/v2/jetpack-forms'
299 );
300 $preload_paths[] = '/wp/v2/jetpack-forms/status-counts';
301 $preload_paths[] = add_query_arg( array( '_locale' => 'user' ), '/wp/v2/jetpack-forms/status-counts' );
302 }
303 $preload_data_raw = array_reduce( $preload_paths, 'rest_preload_api_request', array() );
304
305 // Normalize keys to match what apiFetch will request (without domain).
306 $preload_data = array();
307 foreach ( $preload_data_raw as $key => $value ) {
308 $normalized_key = preg_replace( '#^https?://[^/]+/wp-json#', '', $key );
309 $preload_data[ $normalized_key ] = $value;
310 }
311
312 wp_add_inline_script(
313 $inline_handle,
314 sprintf(
315 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
316 wp_json_encode( $preload_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
317 ),
318 $preload_position
319 );
320 }
321
322 /**
323 * Whether the current request targets the Forms dashboard page.
324 *
325 * @return bool
326 */
327 public static function is_wp_build_dashboard_page() {
328 return self::get_admin_query_page() === self::FORMS_WPBUILD_ADMIN_SLUG;
329 }
330
331 /**
332 * Register the dashboard admin submenu Forms under Jetpack menu.
333 */
334 public function add_admin_submenu() {
335 // Report a missing build here rather than only on the page itself, so a partial
336 // deploy shows up on the first admin request instead of waiting for someone to
337 // open Forms. Keyed on the file and not on the generated callback: load_wp_build()
338 // only requires build.php on the Forms page, so the callback is legitimately
339 // absent on every other admin screen, which runs this method too.
340 if ( ! file_exists( self::wp_build_index_path() ) ) {
341 _doing_it_wrong(
342 __METHOD__,
343 'The Jetpack Forms build output is missing: build/build.php is absent, so the dashboard has nothing to render. The package build did not run for this deploy.',
344 ''
345 );
346 }
347
348 // `jetpack_forms_jetpack_forms_responses_wp_admin_render_page` is the callback generated
349 // by the WP build script, named after the page slug. It only exists once `build/build.php`
350 // is loaded. Without it the page has nothing to render, so show an explanation.
351 $callback = function_exists( 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page' )
352 ? 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page'
353 : array( $this, 'render_wp_build_unavailable' );
354
355 Admin_Menu::add_menu(
356 /** "Jetpack Forms" and "Forms" are product names, do not translate. */
357 'Jetpack Forms',
358 'Forms',
359 'edit_pages',
360 self::FORMS_WPBUILD_ADMIN_SLUG,
361 $callback,
362 null,
363 // The key is not the slug: the page's URL still reads
364 // jetpack-forms-responses-wp-admin, which FORMS-795 tracks separately.
365 array(
366 'product' => 'jetpack-forms',
367 'key' => 'jetpack-forms',
368 )
369 );
370 }
371
372 /**
373 * Render the legacy dashboard mount point.
374 *
375 * Nothing registers this any more — the legacy dashboard was retired and its bundle
376 * is no longer enqueued, so the container it prints stays empty. Kept, and left
377 * printing the same markup, so any caller outside this package behaves as before.
378 *
379 * @deprecated 8.1.0 The legacy dashboard was retired.
380 */
381 public function render_dashboard() {
382 _deprecated_function( __METHOD__, 'jetpack-forms-8.1.0' );
383 ?>
384 <div id="jp-forms-dashboard"></div>
385 <?php
386 }
387
388 /**
389 * Render an error notice when the wp-build dashboard cannot render.
390 *
391 * The wp-build dashboard renders through a callback generated into `build/build.php`.
392 * That file is missing when the package ships without a complete build, and it is
393 * never loaded when a host application filters `jetpack_forms_load_wp_build` to false.
394 * The legacy bundle is no fallback here: load_admin_scripts() skips it on this screen.
395 * So report the problem instead of rendering a blank page.
396 *
397 * @since 7.25.0
398 */
399 public function render_wp_build_unavailable() {
400 ?>
401 <div class="wrap">
402 <?php /* "Jetpack Forms" is a product name, do not translate. */ ?>
403 <h1>Jetpack Forms</h1>
404 <div class="notice notice-error">
405 <p><?php esc_html_e( 'The Forms dashboard is missing the files it needs to load.', 'jetpack-forms' ); ?></p>
406 <p><?php esc_html_e( 'Reinstalling or updating the plugin usually fixes this. If this site is configured not to load the Forms dashboard, contact your site administrator or host.', 'jetpack-forms' ); ?></p>
407 </div>
408 </div>
409 <?php
410 }
411
412 /**
413 * Returns true if there are any feedback posts on the site.
414 *
415 * @return boolean
416 */
417 public function has_feedback() {
418 $posts = new \WP_Query(
419 array(
420 'post_type' => 'feedback',
421 'post_status' => array( 'publish', 'draft', 'spam', 'trash' ),
422 'posts_per_page' => 1,
423 'fields' => 'ids',
424 'no_found_rows' => true,
425 'update_post_meta_cache' => false,
426 'update_post_term_cache' => false,
427 'suppress_filters' => true,
428 )
429 );
430 return $posts->have_posts();
431 }
432
433 /**
434 * Option name for storing classic forms state.
435 */
436 const CLASSIC_FORMS_OPTION = 'jetpack_forms_classic_state';
437
438 /**
439 * Classic forms state: site has classic (non-synced) form submissions.
440 */
441 const CLASSIC_FORMS_STATE_CLASSIC = 'classic';
442
443 /**
444 * Classic forms state: no classic form submissions detected.
445 */
446 const CLASSIC_FORMS_STATE_HIDDEN = 'hidden';
447
448 /**
449 * Classic forms state: user dismissed the classic forms notice.
450 */
451 const CLASSIC_FORMS_STATE_DISMISSED = 'dismissed';
452
453 /**
454 * Returns the classic forms state for the current site.
455 *
456 * Returns 'classic' if the site has form submissions (feedback posts) that were not
457 * created by a synced/reusable jetpack_form, 'dismissed' if the user dismissed the
458 * classic forms notice, or 'hidden' otherwise.
459 *
460 * The result is persisted in a WP option so the detection query only runs once per site.
461 * After that, the cached value is returned on every subsequent call. The cache is also
462 * updated eagerly via mark_classic_form_detected() when new classic submissions arrive.
463 *
464 * @since 7.14.0
465 *
466 * @return string 'classic', 'hidden', or 'dismissed'.
467 */
468 public function get_classic_forms_state() {
469 $state = get_option( self::CLASSIC_FORMS_OPTION );
470
471 if ( $state ) {
472 return $state;
473 }
474
475 $state = $this->detect_classic_forms();
476 update_option( self::CLASSIC_FORMS_OPTION, $state, false );
477
478 return $state;
479 }
480
481 /**
482 * Detects whether any feedback posts exist that are not linked to a jetpack_form post,
483 * indicating the site has classic (inline, widget, or template) forms.
484 *
485 * A feedback post is considered "classic" if:
486 * - It has no parent (post_parent = 0), meaning it was created by a form embedded in a
487 * widget, page template, or other non-post context.
488 * - Its parent exists but is not a jetpack_form post, meaning it was created by a form
489 * block or shortcode placed directly in a post or page.
490 *
491 * The query uses a LEFT JOIN on the posts table to find feedback posts with no matching
492 * jetpack_form parent. This leverages the primary key index for the join and the
493 * type_status_date index for filtering by post_type, making it efficient even on large
494 * sites. The LIMIT 1 ensures early exit as soon as one classic form is found.
495 *
496 * Note: An alternative approach would be to search post_content for the form block markup
497 * (<!-- wp:jetpack/contact-form) or shortcode ([contact-form]). However, that requires a
498 * full-text scan of the posts table (LIKE '%...%' on a TEXT column) with no usable index,
499 * making it significantly more expensive. The feedback-based approach also better fits the
500 * use case: we only need to surface the "Not seeing all your forms?" prompt when there are
501 * actual submissions that won't appear under any synced form in the dashboard.
502 *
503 * @since 7.14.0
504 *
505 * @return string 'classic' or 'hidden'.
506 */
507 private function detect_classic_forms() {
508 global $wpdb;
509
510 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
511 $result = $wpdb->get_var(
512 $wpdb->prepare(
513 "SELECT 1 FROM {$wpdb->posts} AS f
514 LEFT JOIN {$wpdb->posts} AS p
515 ON p.ID = f.post_parent AND p.post_type = %s
516 WHERE f.post_type = 'feedback'
517 AND p.ID IS NULL
518 LIMIT 1",
519 Contact_Form::POST_TYPE
520 )
521 );
522
523 return $result ? self::CLASSIC_FORMS_STATE_CLASSIC : self::CLASSIC_FORMS_STATE_HIDDEN;
524 }
525
526 /**
527 * Eagerly marks the site as having classic forms by setting the option to 'classic'.
528 *
529 * Called when a new form submission is saved that does not belong to a synced jetpack_form.
530 * This avoids re-running the detection query — once a classic submission is observed, the
531 * state is permanently set without needing to scan the database again.
532 *
533 * If the user has already dismissed the classic forms notice, the state is left as
534 * 'dismissed' so the notice does not reappear.
535 *
536 * @since 7.14.0
537 */
538 public static function mark_classic_form_detected() {
539 $current = get_option( self::CLASSIC_FORMS_OPTION );
540
541 if ( self::CLASSIC_FORMS_STATE_DISMISSED === $current ) {
542 return;
543 }
544
545 update_option( self::CLASSIC_FORMS_OPTION, self::CLASSIC_FORMS_STATE_CLASSIC, false );
546 }
547
548 /**
549 * Returns url of forms admin page.
550 *
551 * @param string|null $tab Tab to open in the forms admin page.
552 * @param int|null $post_id Post ID of response to open in the forms responses page.
553 *
554 * @return string
555 */
556 public static function get_forms_admin_url( $tab = null, $post_id = null ) {
557 $url = admin_url( 'admin.php' );
558 $url .= '?page=' . self::FORMS_WPBUILD_ADMIN_SLUG;
559 $url .= '&p=' . rawurlencode( self::get_forms_admin_path_wp_build( $tab, $post_id ) );
560
561 /**
562 * Filters the Forms admin page URL.
563 *
564 * @module contact-form
565 * @since 7.8.0
566 *
567 * @param string $url The Forms admin page URL.
568 * @param string|null $tab Tab to open in the forms admin page.
569 * @param int|null $post_id Post ID of response to open in the forms responses page.
570 *
571 * @return string The filtered Forms admin page URL.
572 */
573 return apply_filters( 'jetpack_forms_admin_url', $url, $tab, $post_id );
574 }
575
576 /**
577 * Returns the URL of the standalone single response page for a given response.
578 *
579 * The standalone page is a wp-build route (`/response/<id>`). The legacy
580 * dashboard has no equivalent, so it falls back to the responses list with the
581 * response selected — as does a missing/empty post ID.
582 *
583 * @since 7.25.0
584 *
585 * @param int|null $post_id Post ID of the response to open.
586 *
587 * @return string
588 */
589 public static function get_single_response_admin_url( $post_id = null ) {
590 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
591
592 // `get_forms_admin_url()` owns the URL scheme for both dashboards. The
593 // 'response' tab resolves to the standalone page on wp-build, and falls
594 // through to the responses list on legacy, which has no such route.
595 return self::get_forms_admin_url( $post_id ? 'response' : 'inbox', $post_id );
596 }
597
598 /**
599 * WP-Build path for the forms admin URL.
600 *
601 * @param string|null $tab Tab to open.
602 * @param int|null $post_id Post ID of response.
603 * @return string URL path (e.g. '/', '/responses/inbox', '/forms').
604 */
605 private static function get_forms_admin_path_wp_build( $tab, $post_id ) {
606 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
607 $response_ids = ! empty( $post_id ) ? '?responseIds=["' . $post_id . '"]' : '';
608
609 // The standalone single response page, which addresses the response by path
610 // rather than selecting it in a list.
611 if ( $tab === 'response' && ! empty( $post_id ) ) {
612 return '/response/' . $post_id;
613 }
614
615 $path_map = array(
616 'inbox' => '/responses/inbox',
617 'spam' => '/responses/spam',
618 'trash' => '/responses/trash',
619 'forms' => '/forms',
620 'responses/inbox' => '/responses/inbox',
621 );
622
623 if ( $tab !== null && $tab !== '' && isset( $path_map[ $tab ] ) ) {
624 return $path_map[ $tab ] . $response_ids;
625 }
626
627 if ( ! empty( $post_id ) ) {
628 return '/responses/inbox?responseIds=["' . $post_id . '"]';
629 }
630
631 return '/responses/inbox';
632 }
633
634 /**
635 * Legacy (hash-based) URL suffix for the forms admin page.
636 *
637 * Unused since the legacy dashboard was retired: get_forms_admin_url() always builds
638 * the wp-build URL now. Private, so nothing outside this class ever called it, which
639 * is why it carries no deprecation notice — there is no audience for one. Goes with
640 * the rest of the legacy tree.
641 *
642 * @param string|null $tab Tab to open.
643 * @param int|null $post_id Post ID of response.
644 * @return string URL suffix (e.g. '#/responses?status=inbox&r=123', or '#/forms').
645 */
646 private static function get_forms_admin_suffix_legacy( $tab, $post_id ) {
647 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
648 $valid_tabs = array( 'spam', 'inbox', 'trash' );
649 $r_param = ! empty( $post_id ) ? '&r=' . $post_id : '';
650
651 if ( in_array( $tab, $valid_tabs, true ) ) {
652 return '#/responses?status=' . $tab . $r_param;
653 }
654
655 if ( $tab === 'forms' ) {
656 return '#/forms';
657 }
658
659 if ( ! empty( $post_id ) ) {
660 return '#/responses?status=inbox' . $r_param;
661 }
662
663 return '';
664 }
665
666 /**
667 * Returns true if the current screen is the Jetpack Forms admin page.
668 *
669 * @return boolean
670 */
671 public static function is_jetpack_forms_admin_page() {
672 if ( ! function_exists( 'get_current_screen' ) ) {
673 return false;
674 }
675
676 $screen = get_current_screen();
677
678 if ( ! $screen || ! isset( $screen->id ) ) {
679 return false;
680 }
681
682 $forms_admin_screens = array(
683 'jetpack_page_' . self::ADMIN_SLUG,
684 'jetpack_page_' . self::FORMS_WPBUILD_ADMIN_SLUG,
685 );
686
687 return in_array( $screen->id, $forms_admin_screens, true );
688 }
689
690 /**
691 * Returns true if form notes feature is enabled.
692 *
693 * @return boolean
694 */
695 public static function is_notes_enabled() {
696 /**
697 * Enable form notes feature in Jetpack Forms .
698 *
699 * @module contact-form
700 * @since 7.3.0
701 *
702 * @param bool $enabled Should the form notes feature be enabled? Defaults to false.
703 */
704 return apply_filters( 'jetpack_forms_notes_enable', false );
705 }
706
707 /**
708 * Get admin URL for given screen ID.
709 *
710 * @deprecated 7.9.0 Use Dashboard::get_forms_admin_url() instead.
711 *
712 * @param string $screen_id Screen ID.
713 * @return string Admin URL.
714 */
715 public static function get_admin_url( $screen_id ) {
716 _deprecated_function( __METHOD__, 'jetpack-7.9.0', 'Dashboard::get_forms_admin_url' );
717
718 if ( 'edit-jetpack_form' === $screen_id ) {
719 return self::get_forms_admin_url( 'forms' );
720 }
721
722 if ( 'edit-feedback' === $screen_id ) {
723 return self::get_forms_admin_url( 'inbox' );
724 }
725
726 return self::get_forms_admin_url();
727 }
728 }
729