PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.2
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.2
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-forms / src / dashboard / class-dashboard.php
jetpack / jetpack_vendor / automattic / jetpack-forms / src / dashboard Last commit date
class-dashboard-view-switch.php 7 months ago class-dashboard.php 3 weeks ago
class-dashboard.php
696 lines
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 = 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 * Script handle for the JS file we enqueue in the Feedback admin page.
82 *
83 * @var string
84 */
85 const SCRIPT_HANDLE = 'jp-forms-dashboard';
86
87 const ADMIN_SLUG = 'jetpack-forms-admin';
88
89 /**
90 * Slug for the wp-admin integrated Responses UI (wp-build page).
91 *
92 * Note: This must be a valid submenu slug (sanitize_key compatible), not a full URL.
93 *
94 * @var string
95 */
96 const FORMS_WPBUILD_ADMIN_SLUG = 'jetpack-forms-responses-wp-admin';
97
98 /**
99 * Priority for the dashboard menu.
100 * Needs to be high enough for us to be able to unregister the default edit.php menu item.
101 *
102 * @var int
103 */
104 const MENU_PRIORITY = 999;
105
106 /**
107 * Initialize the dashboard.
108 */
109 public function init() {
110 add_action( 'admin_menu', array( $this, 'add_admin_submenu' ), self::MENU_PRIORITY );
111 add_action( 'admin_menu', array( __CLASS__, 'redirect_dashboard_url_cross_variant' ), 1 );
112
113 /**
114 * Filter to enable or disable the wp-build-based Forms dashboard.
115 *
116 * Enabled by default since Central Forms Management is now available for all sites.
117 * Can be disabled by returning false from this filter.
118 *
119 * @since 7.18.0
120 *
121 * @param bool $enabled Whether the wp-build dashboard is enabled. Default true.
122 */
123 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
124
125 if ( $is_wp_build_enabled ) {
126 self::load_wp_build();
127 }
128
129 add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
130
131 // Removed all admin notices on the Jetpack Forms admin page.
132 if ( self::get_admin_query_page() === self::ADMIN_SLUG ) {
133 remove_all_actions( 'admin_notices' );
134 }
135 }
136
137 /**
138 * Redirect dashboard URLs when the wp-build flag has changed since the link was generated.
139 *
140 * Email links may point to the legacy or wp-build dashboard. If the flag has toggled,
141 * the requested page may not exist. This redirects to the correct variant.
142 */
143 public static function redirect_dashboard_url_cross_variant() {
144 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
145 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
146
147 if ( $page !== self::ADMIN_SLUG && $page !== self::FORMS_WPBUILD_ADMIN_SLUG ) {
148 return;
149 }
150
151 /** This filter is documented in class-dashboard.php::init */
152 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
153
154 // Legacy URL requested but wp-build is now active → redirect to wp-build.
155 if ( $page === self::ADMIN_SLUG && $is_wp_build_enabled ) {
156 // The hash is never sent to the server. "inbox" used as default tab so we end up specifically in the responses
157 // route, where the client-side router will handle the redirect to the correct status in its beforeLoad hook.
158 $redirect = self::get_forms_admin_url( 'inbox' );
159 wp_safe_redirect( $redirect );
160 exit;
161 }
162
163 // WP-Build URL requested but legacy is now active → redirect to legacy.
164 if ( $page === self::FORMS_WPBUILD_ADMIN_SLUG && ! $is_wp_build_enabled ) {
165 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
166 $p = isset( $_GET['p'] ) ? rawurldecode( sanitize_text_field( wp_unslash( $_GET['p'] ) ) ) : '';
167 $tab = 'inbox';
168 $post_id = null;
169 $has_mark_as_spam = false;
170
171 // Check if mark_as_spam is a separate query parameter (old email format).
172 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
173 if ( isset( $_GET['mark_as_spam'] ) ) {
174 $has_mark_as_spam = true;
175 }
176
177 if ( $p !== '' ) {
178 // Parse path like /responses/inbox?responseIds=["2879"] or /responses/inbox?responseIds=["2879"]&mark_as_spam or /forms.
179 if ( preg_match( '#^/responses/(inbox|spam|trash)(?:\?responseIds=\["(\d+)"\])?(.*)$#', $p, $m ) ) {
180 $tab = $m[1];
181 $post_id = ! empty( $m[2] ) ? absint( $m[2] ) : null;
182
183 // Check if mark_as_spam parameter is present inside the path.
184 if ( ! empty( $m[3] ) && strpos( $m[3], 'mark_as_spam' ) !== false ) {
185 $has_mark_as_spam = true;
186 }
187 } elseif ( preg_match( '#^/forms#', $p ) ) {
188 $tab = 'forms';
189 }
190 }
191
192 $redirect = self::get_forms_admin_url( $tab, $post_id );
193
194 // Add mark_as_spam parameter if it was present in the original URL (either format).
195 if ( $has_mark_as_spam ) {
196 $redirect .= '&mark_as_spam';
197 }
198
199 wp_safe_redirect( $redirect );
200 exit;
201 }
202 }
203
204 /**
205 * Get the current query 'page' parameter.
206 *
207 * @return string
208 */
209 private static function get_admin_query_page() {
210 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
211 return isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
212 }
213
214 /**
215 * Load JavaScript for the dashboard.
216 */
217 public function load_admin_scripts() {
218 if ( ! self::is_jetpack_forms_admin_page() ) {
219 return;
220 }
221
222 // The wp-build (script-module) dashboard renders its own UI from build/pages/…,
223 // so the legacy SPA bundle is dead weight there. Only enqueue it on the legacy
224 // dashboard. The shared inline data below (connection initial state + REST
225 // preload) is instead attached to the always-present wp-api-fetch handle so the
226 // wp-build app still receives it.
227 if ( self::is_wp_build_dashboard_page() ) {
228 $inline_handle = 'wp-api-fetch';
229 $preload_position = 'after';
230
231 // The i18n loader is registered on every admin page by jetpack-assets but
232 // only enqueued when depended on; the esbuild bundles don't pull it in.
233 // Enqueue it so the wp-build dashboard's init module can download its JS
234 // translation catalogs.
235 if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
236 wp_enqueue_script( 'wp-jp-i18n-loader' );
237 }
238 } else {
239 $inline_handle = self::SCRIPT_HANDLE;
240 $preload_position = 'before';
241
242 Assets::register_script(
243 self::SCRIPT_HANDLE,
244 '../../dist/dashboard/jetpack-forms-dashboard.js',
245 __FILE__,
246 array(
247 'in_footer' => true,
248 'textdomain' => 'jetpack-forms',
249 'enqueue' => true,
250 )
251 );
252 }
253
254 if ( Contact_Form_Plugin::can_use_analytics() ) {
255 Tracking::register_tracks_functions_scripts( true );
256 }
257
258 // Adds Connection package initial state.
259 Connection_Initial_State::render_script( $inline_handle );
260
261 // Preload Forms endpoints needed in dashboard context.
262 // Pre-fetch the first inbox page so the UI renders instantly on first load.
263 $preload_params = array(
264 'context' => 'edit',
265 'fields_format' => 'collection',
266 'order' => 'desc',
267 'orderby' => 'date',
268 'page' => 1,
269 'per_page' => 20,
270 'status' => 'draft,publish',
271 );
272 \ksort( $preload_params );
273 $initial_responses_path = \add_query_arg( $preload_params, '/wp/v2/feedback' );
274 $initial_responses_locale_path = \add_query_arg(
275 \array_merge(
276 $preload_params,
277 array( '_locale' => 'user' )
278 ),
279 '/wp/v2/feedback'
280 );
281 $filters_path = '/wp/v2/feedback/filters';
282 $filters_locale_path = \add_query_arg( array( '_locale' => 'user' ), $filters_path );
283 $preload_paths = array(
284 '/wp/v2/types?context=view',
285 '/wp/v2/feedback/config',
286 '/wp/v2/feedback/integrations-metadata',
287 '/wp/v2/feedback/counts',
288 $filters_path,
289 $filters_locale_path,
290 $initial_responses_path,
291 $initial_responses_locale_path,
292 );
293
294 // Only preload the Forms list endpoint when centralized form management is enabled.
295 if ( Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ) ) {
296 $forms_preload_params = array(
297 'context' => 'edit',
298 'page' => 1,
299 'jetpack_forms_context' => 'dashboard',
300 'order' => 'desc',
301 'orderby' => 'modified',
302 'per_page' => 20,
303 'status' => 'publish,draft,pending,future,private',
304 );
305 ksort( $forms_preload_params );
306 $preload_paths[] = add_query_arg( $forms_preload_params, '/wp/v2/jetpack-forms' );
307 $preload_paths[] = add_query_arg(
308 array_merge(
309 $forms_preload_params,
310 array( '_locale' => 'user' )
311 ),
312 '/wp/v2/jetpack-forms'
313 );
314 $preload_paths[] = '/wp/v2/jetpack-forms/status-counts';
315 $preload_paths[] = add_query_arg( array( '_locale' => 'user' ), '/wp/v2/jetpack-forms/status-counts' );
316 }
317 $preload_data_raw = array_reduce( $preload_paths, 'rest_preload_api_request', array() );
318
319 // Normalize keys to match what apiFetch will request (without domain).
320 $preload_data = array();
321 foreach ( $preload_data_raw as $key => $value ) {
322 $normalized_key = preg_replace( '#^https?://[^/]+/wp-json#', '', $key );
323 $preload_data[ $normalized_key ] = $value;
324 }
325
326 wp_add_inline_script(
327 $inline_handle,
328 sprintf(
329 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
330 wp_json_encode( $preload_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
331 ),
332 $preload_position
333 );
334 }
335
336 /**
337 * Whether the current request targets the wp-build (script-module) Forms dashboard,
338 * as opposed to the legacy SPA dashboard.
339 *
340 * When true, the legacy dashboard bundle should not be enqueued: the wp-build page
341 * (build/pages/jetpack-forms-responses/…) provides its own UI and asset loading.
342 *
343 * @return bool
344 */
345 public static function is_wp_build_dashboard_page() {
346 /** This filter is documented in class-dashboard.php::init */
347 return apply_filters( 'jetpack_forms_alpha', true )
348 && self::get_admin_query_page() === self::FORMS_WPBUILD_ADMIN_SLUG;
349 }
350
351 /**
352 * Register the dashboard admin submenu Forms under Jetpack menu.
353 */
354 public function add_admin_submenu() {
355
356 /** This filter is documented in class-dashboard.php::init */
357 if ( apply_filters( 'jetpack_forms_alpha', true ) ) {
358
359 // `jetpack_forms_jetpack_forms_responses_wp_admin_render_page` is the callback generated by WP build script.
360 $callback = function_exists( 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page' )
361 ? 'jetpack_forms_jetpack_forms_responses_wp_admin_render_page'
362 : array( $this, 'render_dashboard' );
363
364 Admin_Menu::add_menu(
365 /** "Jetpack Forms" and "Forms" are product names, do not translate. */
366 'Jetpack Forms',
367 'Forms',
368 'edit_pages',
369 self::FORMS_WPBUILD_ADMIN_SLUG,
370 $callback,
371 10
372 );
373
374 return;
375 }
376
377 Admin_Menu::add_menu(
378 /** "Jetpack Forms" and "Forms" are Product names, do not translate. */
379 'Jetpack Forms',
380 'Forms',
381 'edit_pages',
382 self::ADMIN_SLUG,
383 array( $this, 'render_dashboard' ),
384 10
385 );
386 }
387
388 /**
389 * Render the dashboard.
390 */
391 public function render_dashboard() {
392 ?>
393 <div id="jp-forms-dashboard"></div>
394 <?php
395 }
396
397 /**
398 * Returns true if there are any feedback posts on the site.
399 *
400 * @return boolean
401 */
402 public function has_feedback() {
403 $posts = new \WP_Query(
404 array(
405 'post_type' => 'feedback',
406 'post_status' => array( 'publish', 'draft', 'spam', 'trash' ),
407 'posts_per_page' => 1,
408 'fields' => 'ids',
409 'no_found_rows' => true,
410 'update_post_meta_cache' => false,
411 'update_post_term_cache' => false,
412 'suppress_filters' => true,
413 )
414 );
415 return $posts->have_posts();
416 }
417
418 /**
419 * Option name for storing classic forms state.
420 */
421 const CLASSIC_FORMS_OPTION = 'jetpack_forms_classic_state';
422
423 /**
424 * Classic forms state: site has classic (non-synced) form submissions.
425 */
426 const CLASSIC_FORMS_STATE_CLASSIC = 'classic';
427
428 /**
429 * Classic forms state: no classic form submissions detected.
430 */
431 const CLASSIC_FORMS_STATE_HIDDEN = 'hidden';
432
433 /**
434 * Classic forms state: user dismissed the classic forms notice.
435 */
436 const CLASSIC_FORMS_STATE_DISMISSED = 'dismissed';
437
438 /**
439 * Returns the classic forms state for the current site.
440 *
441 * Returns 'classic' if the site has form submissions (feedback posts) that were not
442 * created by a synced/reusable jetpack_form, 'dismissed' if the user dismissed the
443 * classic forms notice, or 'hidden' otherwise.
444 *
445 * The result is persisted in a WP option so the detection query only runs once per site.
446 * After that, the cached value is returned on every subsequent call. The cache is also
447 * updated eagerly via mark_classic_form_detected() when new classic submissions arrive.
448 *
449 * @since 7.14.0
450 *
451 * @return string 'classic', 'hidden', or 'dismissed'.
452 */
453 public function get_classic_forms_state() {
454 $state = get_option( self::CLASSIC_FORMS_OPTION );
455
456 if ( $state ) {
457 return $state;
458 }
459
460 $state = $this->detect_classic_forms();
461 update_option( self::CLASSIC_FORMS_OPTION, $state, false );
462
463 return $state;
464 }
465
466 /**
467 * Detects whether any feedback posts exist that are not linked to a jetpack_form post,
468 * indicating the site has classic (inline, widget, or template) forms.
469 *
470 * A feedback post is considered "classic" if:
471 * - It has no parent (post_parent = 0), meaning it was created by a form embedded in a
472 * widget, page template, or other non-post context.
473 * - Its parent exists but is not a jetpack_form post, meaning it was created by a form
474 * block or shortcode placed directly in a post or page.
475 *
476 * The query uses a LEFT JOIN on the posts table to find feedback posts with no matching
477 * jetpack_form parent. This leverages the primary key index for the join and the
478 * type_status_date index for filtering by post_type, making it efficient even on large
479 * sites. The LIMIT 1 ensures early exit as soon as one classic form is found.
480 *
481 * Note: An alternative approach would be to search post_content for the form block markup
482 * (<!-- wp:jetpack/contact-form) or shortcode ([contact-form]). However, that requires a
483 * full-text scan of the posts table (LIKE '%...%' on a TEXT column) with no usable index,
484 * making it significantly more expensive. The feedback-based approach also better fits the
485 * use case: we only need to surface the "Not seeing all your forms?" prompt when there are
486 * actual submissions that won't appear under any synced form in the dashboard.
487 *
488 * @since 7.14.0
489 *
490 * @return string 'classic' or 'hidden'.
491 */
492 private function detect_classic_forms() {
493 global $wpdb;
494
495 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
496 $result = $wpdb->get_var(
497 $wpdb->prepare(
498 "SELECT 1 FROM {$wpdb->posts} AS f
499 LEFT JOIN {$wpdb->posts} AS p
500 ON p.ID = f.post_parent AND p.post_type = %s
501 WHERE f.post_type = 'feedback'
502 AND p.ID IS NULL
503 LIMIT 1",
504 Contact_Form::POST_TYPE
505 )
506 );
507
508 return $result ? self::CLASSIC_FORMS_STATE_CLASSIC : self::CLASSIC_FORMS_STATE_HIDDEN;
509 }
510
511 /**
512 * Eagerly marks the site as having classic forms by setting the option to 'classic'.
513 *
514 * Called when a new form submission is saved that does not belong to a synced jetpack_form.
515 * This avoids re-running the detection query — once a classic submission is observed, the
516 * state is permanently set without needing to scan the database again.
517 *
518 * If the user has already dismissed the classic forms notice, the state is left as
519 * 'dismissed' so the notice does not reappear.
520 *
521 * @since 7.14.0
522 */
523 public static function mark_classic_form_detected() {
524 $current = get_option( self::CLASSIC_FORMS_OPTION );
525
526 if ( self::CLASSIC_FORMS_STATE_DISMISSED === $current ) {
527 return;
528 }
529
530 update_option( self::CLASSIC_FORMS_OPTION, self::CLASSIC_FORMS_STATE_CLASSIC, false );
531 }
532
533 /**
534 * Returns url of forms admin page.
535 *
536 * @param string|null $tab Tab to open in the forms admin page.
537 * @param int|null $post_id Post ID of response to open in the forms responses page.
538 *
539 * @return string
540 */
541 public static function get_forms_admin_url( $tab = null, $post_id = null ) {
542 /** This filter is documented in class-dashboard.php::init */
543 $is_wp_build_enabled = apply_filters( 'jetpack_forms_alpha', true );
544 $url = admin_url( 'admin.php' );
545
546 $url .= $is_wp_build_enabled
547 ? '?page=' . self::FORMS_WPBUILD_ADMIN_SLUG
548 : '?page=' . self::ADMIN_SLUG;
549
550 if ( $is_wp_build_enabled ) {
551 $path = self::get_forms_admin_path_wp_build( $tab, $post_id );
552 $url .= '&p=' . rawurlencode( $path );
553 } else {
554 $suffix = self::get_forms_admin_suffix_legacy( $tab, $post_id );
555
556 if ( $suffix !== '' ) {
557 $url .= $suffix;
558 }
559 }
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 * WP-Build path for the forms admin URL.
578 *
579 * @param string|null $tab Tab to open.
580 * @param int|null $post_id Post ID of response.
581 * @return string URL path (e.g. '/', '/responses/inbox', '/forms').
582 */
583 private static function get_forms_admin_path_wp_build( $tab, $post_id ) {
584 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
585 $response_ids = ! empty( $post_id ) ? '?responseIds=["' . $post_id . '"]' : '';
586
587 $path_map = array(
588 'inbox' => '/responses/inbox',
589 'spam' => '/responses/spam',
590 'trash' => '/responses/trash',
591 'forms' => '/forms',
592 'responses/inbox' => '/responses/inbox',
593 );
594
595 if ( $tab !== null && $tab !== '' && isset( $path_map[ $tab ] ) ) {
596 return $path_map[ $tab ] . $response_ids;
597 }
598
599 if ( ! empty( $post_id ) ) {
600 return '/responses/inbox?responseIds=["' . $post_id . '"]';
601 }
602
603 return '/responses/inbox';
604 }
605
606 /**
607 * Legacy (hash-based) URL suffix for the forms admin page.
608 *
609 * @param string|null $tab Tab to open.
610 * @param int|null $post_id Post ID of response.
611 * @return string URL suffix (e.g. '#/responses?status=inbox&r=123', or '#/forms').
612 */
613 private static function get_forms_admin_suffix_legacy( $tab, $post_id ) {
614 $post_id = ! empty( $post_id ) ? absint( $post_id ) : null;
615 $valid_tabs = array( 'spam', 'inbox', 'trash' );
616 $r_param = ! empty( $post_id ) ? '&r=' . $post_id : '';
617
618 if ( in_array( $tab, $valid_tabs, true ) ) {
619 return '#/responses?status=' . $tab . $r_param;
620 }
621
622 if ( $tab === 'forms' ) {
623 return '#/forms';
624 }
625
626 if ( ! empty( $post_id ) ) {
627 return '#/responses?status=inbox' . $r_param;
628 }
629
630 return '';
631 }
632
633 /**
634 * Returns true if the current screen is the Jetpack Forms admin page.
635 *
636 * @return boolean
637 */
638 public static function is_jetpack_forms_admin_page() {
639 if ( ! function_exists( 'get_current_screen' ) ) {
640 return false;
641 }
642
643 $screen = get_current_screen();
644
645 if ( ! $screen || ! isset( $screen->id ) ) {
646 return false;
647 }
648
649 $forms_admin_screens = array(
650 'jetpack_page_' . self::ADMIN_SLUG,
651 'jetpack_page_' . self::FORMS_WPBUILD_ADMIN_SLUG,
652 );
653
654 return in_array( $screen->id, $forms_admin_screens, true );
655 }
656
657 /**
658 * Returns true if form notes feature is enabled.
659 *
660 * @return boolean
661 */
662 public static function is_notes_enabled() {
663 /**
664 * Enable form notes feature in Jetpack Forms .
665 *
666 * @module contact-form
667 * @since 7.3.0
668 *
669 * @param bool $enabled Should the form notes feature be enabled? Defaults to false.
670 */
671 return apply_filters( 'jetpack_forms_notes_enable', false );
672 }
673
674 /**
675 * Get admin URL for given screen ID.
676 *
677 * @deprecated 7.9.0 Use Dashboard::get_forms_admin_url() instead.
678 *
679 * @param string $screen_id Screen ID.
680 * @return string Admin URL.
681 */
682 public static function get_admin_url( $screen_id ) {
683 _deprecated_function( __METHOD__, 'jetpack-7.9.0', 'Dashboard::get_forms_admin_url' );
684
685 if ( 'edit-jetpack_form' === $screen_id ) {
686 return self::get_forms_admin_url( 'forms' );
687 }
688
689 if ( 'edit-feedback' === $screen_id ) {
690 return self::get_forms_admin_url( 'inbox' );
691 }
692
693 return self::get_forms_admin_url();
694 }
695 }
696