PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-forms / src / dashboard / class-dashboard.php

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

791 lines 25.7 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
132 /**
133 * Filter to enable or disable the wp-build-based Forms dashboard.
134 *
135 * Enabled by default since Central Forms Management is now available for all sites.
136 * Can be disabled by returning false from this filter.
137 *
138 * @since 7.18.0
139 *
140 * @param bool $enabled Whether the wp-build dashboard is enabled. Default true.
141 */
142 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
143
144 if ( $is_wp_build_enabled ) {
145 self::load_wp_build();
146 }
147
148 add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
149
150 // Removed all admin notices on the Jetpack Forms admin page.
151 if ( self::get_admin_query_page() === self::ADMIN_SLUG ) {
152 remove_all_actions( 'admin_notices' );
153 }
154 }
155
156 /**
157 * Redirect dashboard URLs when the wp-build flag has changed since the link was generated.
158 *
159 * Email links may point to the legacy or wp-build dashboard. If the flag has toggled,
160 * the requested page may not exist. This redirects to the correct variant.
161 */
162 public static function redirect_dashboard_url_cross_variant() {
163 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
164 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
165
166 if ( $page !== self::ADMIN_SLUG && $page !== self::FORMS_WPBUILD_ADMIN_SLUG ) {
167 return;
168 }
169
170 /** This filter is documented in class-dashboard.php::init */
171 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
172
173 // Legacy URL requested but wp-build is now active → redirect to wp-build.
174 if ( $page === self::ADMIN_SLUG && $is_wp_build_enabled ) {
175 // The hash is never sent to the server. "inbox" used as default tab so we end up specifically in the responses
176 // route, where the client-side router will handle the redirect to the correct status in its beforeLoad hook.
177 $redirect = self::get_forms_admin_url( 'inbox' );
178 wp_safe_redirect( $redirect );
179 exit;
180 }
181
182 // WP-Build URL requested but legacy is now active → redirect to legacy.
183 if ( $page === self::FORMS_WPBUILD_ADMIN_SLUG && ! $is_wp_build_enabled ) {
184 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
185 $p = isset( $_GET['p'] ) ? rawurldecode( sanitize_text_field( wp_unslash( $_GET['p'] ) ) ) : '';
186 $tab = 'inbox';
187 $post_id = null;
188 $has_mark_as_spam = false;
189
190 // Check if mark_as_spam is a separate query parameter (old email format).
191 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
192 if ( isset( $_GET['mark_as_spam'] ) ) {
193 $has_mark_as_spam = true;
194 }
195
196 if ( $p !== '' ) {
197 // Parse path like /responses/inbox?responseIds=["2879"] or /responses/inbox?responseIds=["2879"]&mark_as_spam or /forms.
198 if ( preg_match( '#^/responses/(inbox|spam|trash)(?:\?responseIds=\["(\d+)"\])?(.*)$#', $p, $m ) ) {
199 $tab = $m[1];
200 $post_id = ! empty( $m[2] ) ? absint( $m[2] ) : null;
201
202 // Check if mark_as_spam parameter is present inside the path.
203 if ( ! empty( $m[3] ) && strpos( $m[3], 'mark_as_spam' ) !== false ) {
204 $has_mark_as_spam = true;
205 }
206 } elseif ( preg_match( '#^/response/(\d+)(?:\?(.*))?$#', $p, $m ) ) {
207 // Standalone single response page (wp-build only) — the legacy
208 // dashboard shows the response in the inbox list instead. The path
209 // is matched whole so trailing junk isn't read as a response ID,
210 // but it may legitimately carry the email's mark_as_spam trigger.
211 $post_id = absint( $m[1] );
212
213 if ( ! empty( $m[2] ) && strpos( $m[2], 'mark_as_spam' ) !== false ) {
214 $has_mark_as_spam = true;
215 }
216 } elseif ( preg_match( '#^/forms#', $p ) ) {
217 $tab = 'forms';
218 }
219 }
220
221 $redirect = self::get_forms_admin_url( $tab, $post_id );
222
223 // Add mark_as_spam parameter if it was present in the original URL (either format).
224 if ( $has_mark_as_spam ) {
225 $redirect .= '&mark_as_spam';
226 }
227
228 wp_safe_redirect( $redirect );
229 exit;
230 }
231 }
232
233 /**
234 * Get the current query 'page' parameter.
235 *
236 * @return string
237 */
238 private static function get_admin_query_page() {
239 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
240 return isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
241 }
242
243 /**
244 * Load JavaScript for the dashboard.
245 */
246 public function load_admin_scripts() {
247 if ( ! self::is_jetpack_forms_admin_page() ) {
248 return;
249 }
250
251 // The wp-build (script-module) dashboard renders its own UI from build/pages/…,
252 // so the legacy SPA bundle is dead weight there. Only enqueue it on the legacy
253 // dashboard. The shared inline data below (connection initial state + REST
254 // preload) is instead attached to the always-present wp-api-fetch handle so the
255 // wp-build app still receives it.
256 if ( self::is_wp_build_dashboard_page() ) {
257 $inline_handle = 'wp-api-fetch';
258 $preload_position = 'after';
259
260 // The i18n loader is registered on every admin page by jetpack-assets but
261 // only enqueued when depended on; the esbuild bundles don't pull it in.
262 // Enqueue it so the wp-build dashboard's init module can download its JS
263 // translation catalogs.
264 if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
265 wp_enqueue_script( 'wp-jp-i18n-loader' );
266 }
267 } else {
268 $inline_handle = self::SCRIPT_HANDLE;
269 $preload_position = 'before';
270
271 Assets::register_script(
272 self::SCRIPT_HANDLE,
273 '../../dist/dashboard/jetpack-forms-dashboard.js',
274 __FILE__,
275 array(
276 'in_footer' => true,
277 'textdomain' => 'jetpack-forms',
278 'enqueue' => true,
279 )
280 );
281 }
282
283 if ( Contact_Form_Plugin::can_use_analytics() ) {
284 Tracking::register_tracks_functions_scripts( true );
285 }
286
287 // Adds Connection package initial state.
288 Connection_Initial_State::render_script( $inline_handle );
289
290 // Preload Forms endpoints needed in dashboard context.
291 // Pre-fetch the first inbox page so the UI renders instantly on first load.
292 $preload_params = array(
293 'context' => 'edit',
294 'fields_format' => 'collection',
295 'order' => 'desc',
296 'orderby' => 'date',
297 'page' => 1,
298 'per_page' => 20,
299 'status' => 'draft,publish',
300 );
301 \ksort( $preload_params );
302 $initial_responses_path = \add_query_arg( $preload_params, '/wp/v2/feedback' );
303 $initial_responses_locale_path = \add_query_arg(
304 \array_merge(
305 $preload_params,
306 array( '_locale' => 'user' )
307 ),
308 '/wp/v2/feedback'
309 );
310 $filters_path = '/wp/v2/feedback/filters';
311 $filters_locale_path = \add_query_arg( array( '_locale' => 'user' ), $filters_path );
312 $preload_paths = array(
313 '/wp/v2/types?context=view',
314 '/wp/v2/feedback/config',
315 '/wp/v2/feedback/integrations-metadata',
316 '/wp/v2/feedback/counts',
317 $filters_path,
318 $filters_locale_path,
319 $initial_responses_path,
320 $initial_responses_locale_path,
321 );
322
323 // Only preload the Forms list endpoint when centralized form management is enabled.
324 if ( Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ) ) {
325 $forms_preload_params = array(
326 'context' => 'edit',
327 'page' => 1,
328 'jetpack_forms_context' => 'dashboard',
329 'order' => 'desc',
330 'orderby' => 'modified',
331 'per_page' => 20,
332 'status' => 'publish,draft,pending,future,private',
333 );
334 ksort( $forms_preload_params );
335 $preload_paths[] = add_query_arg( $forms_preload_params, '/wp/v2/jetpack-forms' );
336 $preload_paths[] = add_query_arg(
337 array_merge(
338 $forms_preload_params,
339 array( '_locale' => 'user' )
340 ),
341 '/wp/v2/jetpack-forms'
342 );
343 $preload_paths[] = '/wp/v2/jetpack-forms/status-counts';
344 $preload_paths[] = add_query_arg( array( '_locale' => 'user' ), '/wp/v2/jetpack-forms/status-counts' );
345 }
346 $preload_data_raw = array_reduce( $preload_paths, 'rest_preload_api_request', array() );
347
348 // Normalize keys to match what apiFetch will request (without domain).
349 $preload_data = array();
350 foreach ( $preload_data_raw as $key => $value ) {
351 $normalized_key = preg_replace( '#^https?://[^/]+/wp-json#', '', $key );
352 $preload_data[ $normalized_key ] = $value;
353 }
354
355 wp_add_inline_script(
356 $inline_handle,
357 sprintf(
358 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
359 wp_json_encode( $preload_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
360 ),
361 $preload_position
362 );
363 }
364
365 /**
366 * Whether the current request targets the wp-build (script-module) Forms dashboard,
367 * as opposed to the legacy SPA dashboard.
368 *
369 * When true, the legacy dashboard bundle should not be enqueued: the wp-build page
370 * (build/pages/jetpack-forms-responses/…) provides its own UI and asset loading.
371 *
372 * @return bool
373 */
374 public static function is_wp_build_dashboard_page() {
375 /** This filter is documented in class-dashboard.php::init */
376 return apply_filters( 'jetpack_forms_alpha', true )
377 && self::get_admin_query_page() === self::FORMS_WPBUILD_ADMIN_SLUG;
378 }
379
380 /**
381 * Register the dashboard admin submenu Forms under Jetpack menu.
382 */
383 public function add_admin_submenu() {
384
385 /** This filter is documented in class-dashboard.php::init */
386 if ( apply_filters( 'jetpack_forms_alpha', true ) ) {
387
388 // Report a missing build here rather than only on the page itself, so a partial
389 // deploy shows up on the first admin request instead of waiting for someone to
390 // open Forms. Keyed on the file and not on the generated callback: load_wp_build()
391 // only requires build.php on the Forms page, so the callback is legitimately
392 // absent on every other admin screen, which runs this method too.
393 if ( ! file_exists( self::wp_build_index_path() ) ) {
394 _doing_it_wrong(
395 __METHOD__,
396 '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.',
397 ''
398 );
399 }
400
401 // `jetpack_forms_jetpack_forms_responses_wp_admin_render_page` is the callback generated
402 // by the WP build script, named after the page slug. It only exists once `build/build.php`
403 // is loaded. Without it the page has nothing to render, so show an explanation rather
404 // than the legacy mount point, whose bundle load_admin_scripts() does not enqueue here.
405 $callback = function_exists( 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page' )
406 ? 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page'
407 : array( $this, 'render_wp_build_unavailable' );
408
409 Admin_Menu::add_menu(
410 /** "Jetpack Forms" and "Forms" are product names, do not translate. */
411 'Jetpack Forms',
412 'Forms',
413 'edit_pages',
414 self::FORMS_WPBUILD_ADMIN_SLUG,
415 $callback
416 );
417
418 return;
419 }
420
421 Admin_Menu::add_menu(
422 /** "Jetpack Forms" and "Forms" are Product names, do not translate. */
423 'Jetpack Forms',
424 'Forms',
425 'edit_pages',
426 self::ADMIN_SLUG,
427 array( $this, 'render_dashboard' )
428 );
429 }
430
431 /**
432 * Render the dashboard.
433 */
434 public function render_dashboard() {
435 ?>
436 <div id="jp-forms-dashboard"></div>
437 <?php
438 }
439
440 /**
441 * Render an error notice when the wp-build dashboard cannot render.
442 *
443 * The wp-build dashboard renders through a callback generated into `build/build.php`.
444 * That file is missing when the package ships without a complete build, and it is
445 * never loaded when a host application filters `jetpack_forms_load_wp_build` to false.
446 * The legacy bundle is no fallback here: load_admin_scripts() skips it on this screen.
447 * So report the problem instead of rendering a blank page.
448 *
449 * @since 7.25.0
450 */
451 public function render_wp_build_unavailable() {
452 ?>
453 <div class="wrap">
454 <?php /* "Jetpack Forms" is a product name, do not translate. */ ?>
455 <h1>Jetpack Forms</h1>
456 <div class="notice notice-error">
457 <p><?php esc_html_e( 'The Forms dashboard is missing the files it needs to load.', 'jetpack-forms' ); ?></p>
458 <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>
459 </div>
460 </div>
461 <?php
462 }
463
464 /**
465 * Returns true if there are any feedback posts on the site.
466 *
467 * @return boolean
468 */
469 public function has_feedback() {
470 $posts = new \WP_Query(
471 array(
472 'post_type' => 'feedback',
473 'post_status' => array( 'publish', 'draft', 'spam', 'trash' ),
474 'posts_per_page' => 1,
475 'fields' => 'ids',
476 'no_found_rows' => true,
477 'update_post_meta_cache' => false,
478 'update_post_term_cache' => false,
479 'suppress_filters' => true,
480 )
481 );
482 return $posts->have_posts();
483 }
484
485 /**
486 * Option name for storing classic forms state.
487 */
488 const CLASSIC_FORMS_OPTION = 'jetpack_forms_classic_state';
489
490 /**
491 * Classic forms state: site has classic (non-synced) form submissions.
492 */
493 const CLASSIC_FORMS_STATE_CLASSIC = 'classic';
494
495 /**
496 * Classic forms state: no classic form submissions detected.
497 */
498 const CLASSIC_FORMS_STATE_HIDDEN = 'hidden';
499
500 /**
501 * Classic forms state: user dismissed the classic forms notice.
502 */
503 const CLASSIC_FORMS_STATE_DISMISSED = 'dismissed';
504
505 /**
506 * Returns the classic forms state for the current site.
507 *
508 * Returns 'classic' if the site has form submissions (feedback posts) that were not
509 * created by a synced/reusable jetpack_form, 'dismissed' if the user dismissed the
510 * classic forms notice, or 'hidden' otherwise.
511 *
512 * The result is persisted in a WP option so the detection query only runs once per site.
513 * After that, the cached value is returned on every subsequent call. The cache is also
514 * updated eagerly via mark_classic_form_detected() when new classic submissions arrive.
515 *
516 * @since 7.14.0
517 *
518 * @return string 'classic', 'hidden', or 'dismissed'.
519 */
520 public function get_classic_forms_state() {
521 $state = get_option( self::CLASSIC_FORMS_OPTION );
522
523 if ( $state ) {
524 return $state;
525 }
526
527 $state = $this->detect_classic_forms();
528 update_option( self::CLASSIC_FORMS_OPTION, $state, false );
529
530 return $state;
531 }
532
533 /**
534 * Detects whether any feedback posts exist that are not linked to a jetpack_form post,
535 * indicating the site has classic (inline, widget, or template) forms.
536 *
537 * A feedback post is considered "classic" if:
538 * - It has no parent (post_parent = 0), meaning it was created by a form embedded in a
539 * widget, page template, or other non-post context.
540 * - Its parent exists but is not a jetpack_form post, meaning it was created by a form
541 * block or shortcode placed directly in a post or page.
542 *
543 * The query uses a LEFT JOIN on the posts table to find feedback posts with no matching
544 * jetpack_form parent. This leverages the primary key index for the join and the
545 * type_status_date index for filtering by post_type, making it efficient even on large
546 * sites. The LIMIT 1 ensures early exit as soon as one classic form is found.
547 *
548 * Note: An alternative approach would be to search post_content for the form block markup
549 * (<!-- wp:jetpack/contact-form) or shortcode ([contact-form]). However, that requires a
550 * full-text scan of the posts table (LIKE '%...%' on a TEXT column) with no usable index,
551 * making it significantly more expensive. The feedback-based approach also better fits the
552 * use case: we only need to surface the "Not seeing all your forms?" prompt when there are
553 * actual submissions that won't appear under any synced form in the dashboard.
554 *
555 * @since 7.14.0
556 *
557 * @return string 'classic' or 'hidden'.
558 */
559 private function detect_classic_forms() {
560 global $wpdb;
561
562 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
563 $result = $wpdb->get_var(
564 $wpdb->prepare(
565 "SELECT 1 FROM {$wpdb->posts} AS f
566 LEFT JOIN {$wpdb->posts} AS p
567 ON p.ID = f.post_parent AND p.post_type = %s
568 WHERE f.post_type = 'feedback'
569 AND p.ID IS NULL
570 LIMIT 1",
571 Contact_Form::POST_TYPE
572 )
573 );
574
575 return $result ? self::CLASSIC_FORMS_STATE_CLASSIC : self::CLASSIC_FORMS_STATE_HIDDEN;
576 }
577
578 /**
579 * Eagerly marks the site as having classic forms by setting the option to 'classic'.
580 *
581 * Called when a new form submission is saved that does not belong to a synced jetpack_form.
582 * This avoids re-running the detection query — once a classic submission is observed, the
583 * state is permanently set without needing to scan the database again.
584 *
585 * If the user has already dismissed the classic forms notice, the state is left as
586 * 'dismissed' so the notice does not reappear.
587 *
588 * @since 7.14.0
589 */
590 public static function mark_classic_form_detected() {
591 $current = get_option( self::CLASSIC_FORMS_OPTION );
592
593 if ( self::CLASSIC_FORMS_STATE_DISMISSED === $current ) {
594 return;
595 }
596
597 update_option( self::CLASSIC_FORMS_OPTION, self::CLASSIC_FORMS_STATE_CLASSIC, false );
598 }
599
600 /**
601 * Returns url of forms admin page.
602 *
603 * @param string|null $tab Tab to open in the forms admin page.
604 * @param int|null $post_id Post ID of response to open in the forms responses page.
605 *
606 * @return string
607 */
608 public static function get_forms_admin_url( $tab = null, $post_id = null ) {
609 /** This filter is documented in class-dashboard.php::init */
610 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
611 $url = admin_url( 'admin.php' );
612
613 $url .= $is_wp_build_enabled
614 ? '?page=' . self::FORMS_WPBUILD_ADMIN_SLUG
615 : '?page=' . self::ADMIN_SLUG;
616
617 if ( $is_wp_build_enabled ) {
618 $path = self::get_forms_admin_path_wp_build( $tab, $post_id );
619 $url .= '&p=' . rawurlencode( $path );
620 } else {
621 $suffix = self::get_forms_admin_suffix_legacy( $tab, $post_id );
622
623 if ( $suffix !== '' ) {
624 $url .= $suffix;
625 }
626 }
627
628 /**
629 * Filters the Forms admin page URL.
630 *
631 * @module contact-form
632 * @since 7.8.0
633 *
634 * @param string $url The Forms admin page URL.
635 * @param string|null $tab Tab to open in the forms admin page.
636 * @param int|null $post_id Post ID of response to open in the forms responses page.
637 *
638 * @return string The filtered Forms admin page URL.
639 */
640 return apply_filters( 'jetpack_forms_admin_url', $url, $tab, $post_id );
641 }
642
643 /**
644 * Returns the URL of the standalone single response page for a given response.
645 *
646 * The standalone page is a wp-build route (`/response/<id>`). The legacy
647 * dashboard has no equivalent, so it falls back to the responses list with the
648 * response selected — as does a missing/empty post ID.
649 *
650 * @since 7.25.0
651 *
652 * @param int|null $post_id Post ID of the response to open.
653 *
654 * @return string
655 */
656 public static function get_single_response_admin_url( $post_id = null ) {
657 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
658
659 // `get_forms_admin_url()` owns the URL scheme for both dashboards. The
660 // 'response' tab resolves to the standalone page on wp-build, and falls
661 // through to the responses list on legacy, which has no such route.
662 return self::get_forms_admin_url( $post_id ? 'response' : 'inbox', $post_id );
663 }
664
665 /**
666 * WP-Build path for the forms admin URL.
667 *
668 * @param string|null $tab Tab to open.
669 * @param int|null $post_id Post ID of response.
670 * @return string URL path (e.g. '/', '/responses/inbox', '/forms').
671 */
672 private static function get_forms_admin_path_wp_build( $tab, $post_id ) {
673 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
674 $response_ids = ! empty( $post_id ) ? '?responseIds=["' . $post_id . '"]' : '';
675
676 // The standalone single response page, which addresses the response by path
677 // rather than selecting it in a list.
678 if ( $tab === 'response' && ! empty( $post_id ) ) {
679 return '/response/' . $post_id;
680 }
681
682 $path_map = array(
683 'inbox' => '/responses/inbox',
684 'spam' => '/responses/spam',
685 'trash' => '/responses/trash',
686 'forms' => '/forms',
687 'responses/inbox' => '/responses/inbox',
688 );
689
690 if ( $tab !== null && $tab !== '' && isset( $path_map[ $tab ] ) ) {
691 return $path_map[ $tab ] . $response_ids;
692 }
693
694 if ( ! empty( $post_id ) ) {
695 return '/responses/inbox?responseIds=["' . $post_id . '"]';
696 }
697
698 return '/responses/inbox';
699 }
700
701 /**
702 * Legacy (hash-based) URL suffix for the forms admin page.
703 *
704 * @param string|null $tab Tab to open.
705 * @param int|null $post_id Post ID of response.
706 * @return string URL suffix (e.g. '#/responses?status=inbox&r=123', or '#/forms').
707 */
708 private static function get_forms_admin_suffix_legacy( $tab, $post_id ) {
709 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
710 $valid_tabs = array( 'spam', 'inbox', 'trash' );
711 $r_param = ! empty( $post_id ) ? '&r=' . $post_id : '';
712
713 if ( in_array( $tab, $valid_tabs, true ) ) {
714 return '#/responses?status=' . $tab . $r_param;
715 }
716
717 if ( $tab === 'forms' ) {
718 return '#/forms';
719 }
720
721 if ( ! empty( $post_id ) ) {
722 return '#/responses?status=inbox' . $r_param;
723 }
724
725 return '';
726 }
727
728 /**
729 * Returns true if the current screen is the Jetpack Forms admin page.
730 *
731 * @return boolean
732 */
733 public static function is_jetpack_forms_admin_page() {
734 if ( ! function_exists( 'get_current_screen' ) ) {
735 return false;
736 }
737
738 $screen = get_current_screen();
739
740 if ( ! $screen || ! isset( $screen->id ) ) {
741 return false;
742 }
743
744 $forms_admin_screens = array(
745 'jetpack_page_' . self::ADMIN_SLUG,
746 'jetpack_page_' . self::FORMS_WPBUILD_ADMIN_SLUG,
747 );
748
749 return in_array( $screen->id, $forms_admin_screens, true );
750 }
751
752 /**
753 * Returns true if form notes feature is enabled.
754 *
755 * @return boolean
756 */
757 public static function is_notes_enabled() {
758 /**
759 * Enable form notes feature in Jetpack Forms .
760 *
761 * @module contact-form
762 * @since 7.3.0
763 *
764 * @param bool $enabled Should the form notes feature be enabled? Defaults to false.
765 */
766 return apply_filters( 'jetpack_forms_notes_enable', false );
767 }
768
769 /**
770 * Get admin URL for given screen ID.
771 *
772 * @deprecated 7.9.0 Use Dashboard::get_forms_admin_url() instead.
773 *
774 * @param string $screen_id Screen ID.
775 * @return string Admin URL.
776 */
777 public static function get_admin_url( $screen_id ) {
778 _deprecated_function( __METHOD__, 'jetpack-7.9.0', 'Dashboard::get_forms_admin_url' );
779
780 if ( 'edit-jetpack_form' === $screen_id ) {
781 return self::get_forms_admin_url( 'forms' );
782 }
783
784 if ( 'edit-feedback' === $screen_id ) {
785 return self::get_forms_admin_url( 'inbox' );
786 }
787
788 return self::get_forms_admin_url();
789 }
790 }
791