PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.9.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.9.1
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
← All changes | admin/admin.php +290 -3231 trunk1.9.1 View file →
@@ -6,29 +6,20 @@
6 6 */
7 7
8 8 namespace SRFM\Admin;
9 9
10 -use Astra_Notices;
10 +use SRFM\Admin\Views\Entries_List_Table;
11 +use SRFM\Admin\Views\Single_Entry;
11 12 use SRFM\Inc\AI_Form_Builder\AI_Helper;
12 -use SRFM\Inc\Client_Logger;
13 -use SRFM\Inc\Database\Register;
14 13 use SRFM\Inc\Database\Tables\Entries;
15 -use SRFM\Inc\Generate_Form_Markup;
16 -use SRFM\Inc\Global_Settings\Global_Settings;
17 14 use SRFM\Inc\Helper;
18 15 use SRFM\Inc\Onboarding;
19 -use SRFM\Inc\Payments\Payment_Helper;
20 -use SRFM\Inc\Payments\Stripe\Stripe_Helper;
21 -use SRFM\Inc\Smart_Tags;
16 +use SRFM\Inc\Post_Types;
22 17 use SRFM\Inc\Traits\Get_Instance;
23 18
24 19 if ( ! defined( 'ABSPATH' ) ) {
25 20 exit; // Exit if accessed directly.
26 21 }
27 -
28 -if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
29 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
30 -}
31 22 /**
32 23 * Admin handler class.
33 24 *
34 25 * @since 0.0.1
@@ -36,60 +27,8 @@
36 27 class Admin {
37 28 use Get_Instance;
38 29
39 30 /**
40 - * Minimum number of forms or entries required to show the rating notice.
41 - *
42 - * @since 2.5.2
43 - */
44 - public const RATING_NOTICE_THRESHOLD = 3;
45 -
46 - /**
47 - * Post meta the Starter Templates (Astra Sites) plugin stamps on every post it
48 - * imports. The "Finish setting up" Thank You prompt (#3030) scopes to these
49 - * forms only. Owned by a plugin that is NOT a SureForms dependency: on installs
50 - * without Starter Templates nothing carries this meta and the prompt never shows.
51 - *
52 - * @since 2.12.4
53 - */
54 - public const ASTRA_SITES_IMPORT_META = '_astra_sites_imported_post';
55 -
56 - /**
57 - * Negative-cache transient: no form on this site carries the import marker.
58 - *
59 - * Set only when the marker query itself returns zero posts, which is a
60 - * site-wide fact rather than a per-user one, and cleared as soon as any post is
61 - * stamped with the marker (see invalidate_starter_template_cache()). This keeps
62 - * the query off the majority of installs without tying the features to whether
63 - * Starter Templates happens to still be active — the marker outlives it.
64 - *
65 - * @since 2.12.4
66 - */
67 - public const NO_IMPORTED_FORMS_TRANSIENT = 'srfm_no_starter_template_forms';
68 -
69 - /**
70 - * Inline CSS for Quill 1.x (react-quill) list markers.
71 - *
72 - * Quill 1.x renders bullet/numbered list markers via CSS ::before pseudo-elements,
73 - * whereas the vendor quill.snow.css targets .ql-ui child elements (Quill 2.x approach).
74 - * This constant is shared by enqueue_styles() and enqueue_scripts() to prevent drift.
75 - *
76 - * @since 2.5.2
77 - */
78 - public const QUILL_1X_INLINE_CSS = '.ql-editor ul,.ql-editor ol{padding-left:1.5em}.ql-editor ul>li,.ql-editor ol>li{list-style-type:none}.ql-editor ol li:not(.ql-direction-rtl),.ql-editor ul li:not(.ql-direction-rtl){padding-left:1.5em}.ql-editor ol li.ql-direction-rtl,.ql-editor ul li.ql-direction-rtl{padding-right:1.5em}.ql-editor ul>li::before{content:"\2022"}.ql-editor li::before{display:inline-block;white-space:nowrap;width:1.2em}.ql-editor li:not(.ql-direction-rtl)::before{margin-left:-1.5em;margin-right:.3em;text-align:right}.ql-editor li.ql-direction-rtl::before{margin-left:.3em;margin-right:-1.5em}.ql-editor ol li{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;counter-increment:list-0}.ql-editor ol li::before{content:counter(list-0,decimal) ". "}.ql-editor ol li.ql-indent-1{counter-increment:list-1;counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-1::before{content:counter(list-1,lower-alpha) ". "}.ql-editor ol li.ql-indent-2{counter-increment:list-2;counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-2::before{content:counter(list-2,lower-roman) ". "}.ql-editor ol li.ql-indent-3{counter-increment:list-3;counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-3::before{content:counter(list-3,decimal) ". "}.ql-editor ol li.ql-indent-4{counter-increment:list-4;counter-reset:list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-4::before{content:counter(list-4,lower-alpha) ". "}.ql-editor ol li.ql-indent-5{counter-increment:list-5;counter-reset:list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-5::before{content:counter(list-5,lower-roman) ". "}.ql-editor ol li.ql-indent-6{counter-increment:list-6;counter-reset:list-7 list-8 list-9}.ql-editor ol li.ql-indent-6::before{content:counter(list-6,decimal) ". "}.ql-editor ol li.ql-indent-7{counter-increment:list-7;counter-reset:list-8 list-9}.ql-editor ol li.ql-indent-7::before{content:counter(list-7,lower-alpha) ". "}.ql-editor ol li.ql-indent-8{counter-increment:list-8;counter-reset:list-9}.ql-editor ol li.ql-indent-8::before{content:counter(list-8,lower-roman) ". "}.ql-editor ol li.ql-indent-9{counter-increment:list-9}.ql-editor ol li.ql-indent-9::before{content:counter(list-9,decimal) ". "}';
79 -
80 - /**
81 - * Notice id for the "Finish setting up" Thank You prompt (#3030).
82 - *
83 - * A single stable id (not per-form): keeps both the autoloaded
84 - * `allowed_astra_notices` option and the per-user dismissal meta bounded to one
85 - * row, and lets a dismissed user short-circuit before the query runs.
86 - *
87 - * @since 2.12.6
88 - */
89 - public const THANKYOU_PROMPT_NOTICE_ID = 'srfm-thankyou-prompt';
90 -
91 - /**
92 31 * Dashboard widget entries data.
93 32 *
94 33 * @var array
95 34 * @since 1.9.1
@@ -96,50 +35,8 @@
96 35 */
97 36 private $dashboard_widget_data = [];
98 37
99 38 /**
100 - * Cached result for whether the rating notice should display.
101 - *
102 - * @var bool|null
103 - * @since 2.5.2
104 - */
105 - private $should_show_rating = null;
106 -
107 - /**
108 - * SureForms Page Default permission.
109 - *
110 - * @var string
111 - * @since 1.12.2
112 - */
113 - private static $sureforms_page_default_capability = 'manage_options';
114 -
115 - /**
116 - * Request memo for the "Finish setting up" Thank You prompt (#3030).
117 - *
118 - * A static property (not a function-local static) so tests can reset it via
119 - * reflection / reset_thankyou_prompt_cache() — otherwise the first call pins
120 - * the value for the whole process and the feature is untestable.
121 - *
122 - * @var array<int,array<string,mixed>>|null
123 - * @since 2.12.4
124 - */
125 - private static $thankyou_prompt_cache = null;
126 -
127 - /**
128 - * Request memo for the dashboard setup-checklist card (#3031).
129 - *
130 - * A static property (not a function-local static) so tests can reset it via
131 - * reset_form_setup_card_cache() and exercise the populated path — a
132 - * function-local static pins the first result for the whole process. Keyed by
133 - * user id since the payload derives from that user's capabilities.
134 - * `false` means "not computed yet"; `null`/array is a computed result.
135 - *
136 - * @var array<int,array<string,mixed>|null>
137 - * @since 2.12.4
138 - */
139 - private static $setup_card_cache = [];
140 -
141 - /**
142 39 * Class constructor.
143 40 *
144 41 * @return void
145 42 * @since 0.0.1
@@ -147,15 +44,11 @@
147 44 public function __construct() {
148 45 add_action( 'admin_menu', [ $this, 'add_menu_page' ], 9 );
149 46 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
150 47 add_action( 'admin_menu', [ $this, 'settings_page' ] );
151 - add_action( 'admin_menu', [ $this, 'add_learn_page' ] );
152 48 add_action( 'admin_menu', [ $this, 'add_new_form' ] );
153 49 add_action( 'admin_menu', [ $this, 'add_suremail_page' ] );
154 50 if ( ! Helper::has_pro() ) {
155 - add_action( 'admin_menu', [ $this, 'add_quiz_page' ] );
156 - add_action( 'admin_menu', [ $this, 'add_survey_reports_page' ] );
157 - add_action( 'admin_menu', [ $this, 'add_partial_entries_page' ] );
158 51 add_action( 'admin_menu', [ $this, 'add_upgrade_to_pro' ] );
159 52 add_action( 'admin_footer', [ $this, 'add_upgrade_to_pro_target_attr' ] );
160 53 }
161 54
@@ -167,21 +60,24 @@
167 60 // this action is used to restrict Spectra's quick action bar on SureForms CPTS.
168 61 add_action( 'uag_enable_quick_action_sidebar', [ $this, 'restrict_spectra_quick_action_bar' ] );
169 62
170 63 add_action( 'current_screen', [ $this, 'enable_gutenberg_for_sureforms' ], 100 );
171 - // Register notices early for React pages (before admin_enqueue_scripts).
172 - add_action( 'admin_init', [ $this, 'register_pro_compatibility_notices' ], 5 );
64 + add_action( 'admin_notices', [ $this, 'srfm_pro_version_compatibility' ] );
65 + add_action( 'admin_notices', [ $this, 'add_smtp_warning_notice' ] );
173 66
174 - // Database maintenance notice: the entries table is missing, so submissions
175 - // cannot be saved. Registered at admin_init priority 5 so the React notice is
176 - // in place before admin_enqueue_scripts localizes it.
177 - add_action( 'admin_init', [ $this, 'register_database_repair_notice' ], 5 );
178 - add_action( 'admin_notices', [ $this, 'render_action_item_notices' ] );
179 - add_action( 'admin_notices', [ $this, 'render_database_repair_notice' ] );
180 - add_action( 'admin_post_srfm_repair_entries_table', [ $this, 'handle_database_repair' ] );
181 - // Display notices on traditional WordPress admin pages.
182 - add_action( 'admin_notices', [ $this, 'srfm_pro_version_compatibility' ] );
67 + // Handle entry actions.
68 + add_action( 'admin_init', [ $this, 'handle_entry_actions' ] );
69 + add_action( 'admin_notices', [ Entries_List_Table::class, 'display_bulk_action_notice' ] );
183 70
71 + // This action enqueues translations for NPS Survey library.
72 + // A better solution will be required from library to resolve plugin conflict.
73 + add_action(
74 + 'admin_footer',
75 + static function() {
76 + Helper::register_script_translations( 'nps-survey-script' );
77 + },
78 + 1000
79 + );
184 80 // Enfold theme compatibility to enable block editor for SureForms post type.
185 81 add_filter( 'avf_use_block_editor_for_post', [ $this, 'enable_block_editor_in_enfold_theme' ] );
186 82
187 83 // Add action links to the plugin page.
@@ -192,9 +88,8 @@
192 88
193 89 if ( $admin_notification_on ) {
194 90 add_action( 'admin_menu', [ $this, 'maybe_add_entries_badge' ], 99 );
195 91 }
196 -
197 92 add_filter( 'wpforms_current_user_can', [ $this, 'disable_wpforms_capabilities' ], 10, 3 );
198 93
199 94 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_pointer' ] );
200 95 // Ajax callbacks for wp-pointer functionality.
@@ -200,865 +95,14 @@
200 95 // Ajax callbacks for wp-pointer functionality.
201 96 add_action( 'wp_ajax_should_show_pointer', [ $this, 'pointer_should_show' ] );
202 97 add_action( 'wp_ajax_sureforms_dismiss_pointer', [ $this, 'pointer_dismissed' ] );
203 98 add_action( 'wp_ajax_sureforms_accept_cta', [ $this, 'pointer_accepted_cta' ] );
204 - add_action( 'wp_ajax_srfm_notice_response', [ $this, 'handle_notice_response' ] );
205 - add_action( 'wp_ajax_srfm_dismiss_action_item', [ $this, 'handle_dismiss_action_item' ] );
206 - add_action( 'admin_post_srfm_dismiss_action_item_link', [ $this, 'handle_dismiss_action_item_link' ] );
207 - add_action( 'wp_ajax_srfm_ai_widget_usage', [ $this, 'track_ai_widget_usage' ] );
208 - add_action( 'load-post.php', [ $this, 'maybe_track_edit_form_button_click' ] );
209 - add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] );
210 99
211 100 // Register dashboard widget only if there are recent entries.
212 101 add_action( 'admin_init', [ $this, 'maybe_register_dashboard_widget' ] );
213 -
214 - // Enqueue the AI quick draft widget script on the dashboard screen.
215 - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_ai_dashboard_widget_assets' ] );
216 -
217 - // "Finish setting up" checklist widget on the main WP dashboard (#3031).
218 - add_action( 'wp_dashboard_setup', [ $this, 'register_form_setup_widget' ] );
219 - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_form_setup_widget_assets' ] );
220 -
221 - // Drop the "no imported forms" negative cache as soon as a post is stamped
222 - // with the import marker, so a template imported after the cache was written
223 - // surfaces immediately instead of waiting for the transient to expire.
224 - add_action( 'added_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 );
225 - add_action( 'updated_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 );
226 -
227 - // Save first form creation time stamp.
228 - add_action( 'admin_init', [ $this, 'save_first_form_creation_time_stamp' ] );
229 - add_action( 'admin_notices', [ $this, 'display_srfm_rating_notice' ] );
230 - add_action( 'admin_notices', [ $this, 'display_srfm_getting_started_notice' ] );
231 -
232 - // "Finish setting up" prompt, shown as an Astra Notices admin notice on
233 - // every admin screen except the dashboard (#3030).
234 - add_action( 'admin_notices', [ $this, 'render_thankyou_prompt_notice' ] );
235 -
236 - /**
237 - * Suppress foreign (third-party) admin notices on SureForms admin screens.
238 - *
239 - * Some plugins (e.g. Ninja Forms) print large promotional banners on every
240 - * admin page via the admin_notices / all_admin_notices / network_admin_notices
241 - * hooks. These bleed onto SureForms' own React admin screens and break the UI.
242 - * We run at the EARLIEST priority on each notice hook (all third-party
243 - * callbacks are registered before these hooks fire, during admin_init /
244 - * plugin load) and strip the foreign ones before they are echoed, while
245 - * preserving SureForms' own notices. Scoped strictly to SureForms screens.
246 - */
247 - add_action( 'admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
248 - add_action( 'all_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
249 - add_action( 'network_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
250 102 }
251 103
252 104 /**
253 - * Remove third-party admin notices on SureForms admin screens.
254 - *
255 - * Iterates over the callbacks registered on the admin notice hooks and
256 - * removes any that are not owned by SureForms. A callback is considered
257 - * owned by SureForms when it belongs to a class in the `SRFM` / `SRFM_PRO`
258 - * namespaces or to the bundled `BSF_Admin_Notices` / `Astra_Notices`
259 - * notices library. SureForms' own notices are therefore preserved while
260 - * foreign promotional banners are suppressed.
261 - *
262 - * This callback is hooked at `PHP_INT_MIN` so that it runs first on each
263 - * notice hook and removes the foreign callbacks before WordPress echoes
264 - * them (WP_Hook honours removals made during iteration). It is strictly
265 - * scoped to SureForms admin screens via {@see Helper::is_sureforms_admin_page()}
266 - * so no other admin page is affected.
267 - *
268 - * @since 2.10.0
269 - * @return void
270 - */
271 - public function suppress_foreign_admin_notices() {
272 - // Bail early if we are not on a SureForms admin screen. This keeps the
273 - // suppression strictly scoped and avoids touching any other admin page.
274 - // is_sureforms_admin_page() covers the core screens (dashboard, add-new,
275 - // settings, entries, the form CPT); we additionally match any admin page
276 - // whose `page` slug is SureForms-owned (sureforms_* / srfm_*) so the
277 - // suppression also applies to the payments/quiz/survey/learn/SMTP screens.
278 - if ( ! Helper::is_sureforms_admin_page() && ! $this->is_sureforms_owned_admin_page() ) {
279 - return;
280 - }
281 -
282 - global $wp_filter;
283 -
284 - // The hook currently being fired (admin_notices, all_admin_notices or network_admin_notices).
285 - $current_hook = current_action();
286 -
287 - if ( empty( $current_hook ) || empty( $wp_filter[ $current_hook ] ) || ! ( $wp_filter[ $current_hook ] instanceof \WP_Hook ) ) {
288 - return;
289 - }
290 -
291 - foreach ( $wp_filter[ $current_hook ]->callbacks as $priority => $callbacks ) {
292 - foreach ( $callbacks as $callback ) {
293 - $function = $callback['function'] ?? null;
294 -
295 - // Never remove our own suppression callback.
296 - if ( is_array( $function ) && isset( $function[0] ) && $function[0] === $this && 'suppress_foreign_admin_notices' === $function[1] ) {
297 - continue;
298 - }
299 -
300 - // Preserve SureForms-owned notices, remove everything else.
301 - if ( $this->is_sureforms_owned_notice_callback( $function ) ) {
302 - continue;
303 - }
304 -
305 - remove_action( $current_hook, $function, $priority );
306 - }
307 - }
308 - }
309 -
310 - /**
311 - * Get the first form creation time stamp.
312 - *
313 - * @since 1.10.1
314 - * @return int|false
315 - */
316 - public static function get_first_form_creation_time_stamp() {
317 - return Helper::get_srfm_option( 'first_form_created_at', false );
318 - }
319 -
320 - /**
321 - * Check if the first form has been created.
322 - *
323 - * @since 1.10.1
324 - * @return bool
325 - */
326 - public static function is_first_form_created() {
327 - // Convert the first form creation time stamp to a boolean. If it exists, it will return true, otherwise false.
328 - $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp();
329 -
330 - // If the first form creation time stamp is not set, return false.
331 - if ( ! $first_form_creation_time_stamp ) {
332 - return false; // No forms created yet.
333 - }
334 -
335 - // Check if the first form creation time stamp is a valid integer and greater than zero.
336 - return is_int( $first_form_creation_time_stamp ) && $first_form_creation_time_stamp > 0;
337 - }
338 -
339 - /**
340 - * Whether a form's confirmation message is still the shipped default.
341 - *
342 - * Compared on tag-stripped, entity-decoded, whitespace-collapsed text rather
343 - * than raw HTML: the default is stored with a base64 icon on creation but
344 - * regenerated with a URL icon, so the markup differs while the wording does
345 - * not, and a starter-template import can store a literal apostrophe where the
346 - * generated default carries the encoded `&#039;` — decoding entities makes both
347 - * compare equal. Any real edit to the heading or body text changes the text and
348 - * flips this to false, which is exactly when the prompt should stop showing.
349 - *
350 - * Locale caveat: the comparison target is translated at call time, so a form
351 - * whose default was stored under a different active locale won't match. That
352 - * fails safe — the prompt simply doesn't show — never a false nag.
353 - *
354 - * @param int $form_id Form post ID.
355 - *
356 - * @since 2.12.4
357 - * @return bool
358 - */
359 - public static function is_default_confirmation_message( $form_id ) {
360 - $confirmation = get_post_meta( (int) $form_id, '_srfm_form_confirmation', true );
361 -
362 - if ( ! is_array( $confirmation ) || ! isset( $confirmation[0]['message'] ) || ! is_string( $confirmation[0]['message'] ) ) {
363 - return false;
364 - }
365 -
366 - // The default message is only ever shown for a "same page" confirmation.
367 - // A redirect ("different page" / "custom url") never renders it, yet the
368 - // stored settings still seed the default message string — so without this
369 - // guard a redirect form would be nagged forever about a message no visitor
370 - // sees, with no way to clear the prompt by doing what it asks.
371 - if ( ! isset( $confirmation[0]['confirmation_type'] ) || 'same page' !== $confirmation[0]['confirmation_type'] ) {
372 - return false;
373 - }
374 -
375 - $message = $confirmation[0]['message'];
376 -
377 - if ( '' === trim( $message ) ) {
378 - return false;
379 - }
380 -
381 - $normalize = static function ( $html ) {
382 - // Decode entities too, so an encoded apostrophe (&#039;) in the generated
383 - // default matches a literal one stored by a template import.
384 - $text = html_entity_decode( wp_strip_all_tags( (string) $html ), ENT_QUOTES, 'UTF-8' );
385 - return trim( (string) preg_replace( '/\s+/', ' ', $text ) );
386 - };
387 -
388 - return $normalize( $message ) === $normalize( Global_Settings::get_default_confirmation_message() );
389 - }
390 -
391 - /**
392 - * Whether a form has somewhere to send replies (an enabled email notification
393 - * with a non-empty recipient).
394 - *
395 - * @param int $form_id Form post ID.
396 - *
397 - * @since 2.12.4
398 - * @return bool
399 - */
400 - public static function form_has_reply_destination( $form_id ) {
401 - $notifications = get_post_meta( (int) $form_id, '_srfm_email_notification', true );
402 -
403 - if ( ! is_array( $notifications ) ) {
404 - return false;
405 - }
406 -
407 - foreach ( $notifications as $notification ) {
408 - if ( is_array( $notification ) && ! empty( $notification['status'] ) && ! empty( $notification['email_to'] ) ) {
409 - return true;
410 - }
411 - }
412 -
413 - return false;
414 - }
415 -
416 - /**
417 - * The most recently created form still needing setup (default Thank You
418 - * message, or no reply destination).
419 - *
420 - * Powers the "Finish setting up" prompt (#3030). Limited to the single latest
421 - * such form to avoid clutter, and to forms the current user may actually edit.
422 - * A form is a candidate when it still has an unfinished step (default Thank You
423 - * message, or no reply destination). Dismissal is enforced by the caller,
424 - * before this query runs.
425 - *
426 - * @since 2.12.4
427 - * @return array<int,array<string,mixed>> One entry, or none.
428 - */
429 - public static function get_thankyou_prompt_forms() {
430 - // Memoized for the request so repeated reads (e.g. the notice render plus
431 - // any add-on consumer) share a single query. Sentinel is null, not false,
432 - // so a filter returning false (__return_false to disable) still memoizes.
433 - if ( null !== self::$thankyou_prompt_cache ) {
434 - return self::$thankyou_prompt_cache;
435 - }
436 -
437 - /**
438 - * Filter the forms the "Finish setting up" Thank You notice may surface.
439 - *
440 - * @param array<int,array<string,mixed>> $prompts Candidate prompt payloads.
441 - *
442 - * @since 2.12.4
443 - */
444 - $filtered = apply_filters( 'srfm_thankyou_prompt_forms', self::compute_thankyou_prompt_forms() );
445 - self::$thankyou_prompt_cache = is_array( $filtered ) ? $filtered : [];
446 -
447 - return self::$thankyou_prompt_cache;
448 - }
449 -
450 - /**
451 - * Clear the request memo for the Thank You prompt (#3030).
452 - *
453 - * Lets tests exercise the memoized public path, and is a safe hook for anything
454 - * that changes which form qualifies (e.g. a form save).
455 - *
456 - * @since 2.12.4
457 - * @return void
458 - */
459 - public static function reset_thankyou_prompt_cache() {
460 - self::$thankyou_prompt_cache = null;
461 - }
462 -
463 - /**
464 - * Setup-checklist data for the newest starter-template form (#3031).
465 - *
466 - * Picks the most recent form the current user can edit that was created from an
467 - * Astra Sites starter template. The widget
468 - * lists a fixed set of optional next-steps for it — their completion is not
469 - * computed — so the payload carries only the form and the CTA targets. Memoized
470 - * for the request so the widget register/enqueue/render passes share one query.
471 - *
472 - * @since 2.12.4
473 - * @return array<string,mixed>|null Card payload, or null when there is no candidate form.
474 - */
475 - public static function get_form_setup_card() {
476 - $user_id = get_current_user_id();
477 -
478 - // Request memo, keyed per user — the payload derives from that user's
479 - // capabilities. Reset via reset_form_setup_card_cache().
480 - if ( array_key_exists( $user_id, self::$setup_card_cache ) ) {
481 - return self::$setup_card_cache[ $user_id ];
482 - }
483 -
484 - self::$setup_card_cache[ $user_id ] = self::compute_form_setup_card();
485 -
486 - return self::$setup_card_cache[ $user_id ];
487 - }
488 -
489 - /**
490 - * Drop the "no imported forms" negative cache when the marker is written.
491 - *
492 - * Hooked to added_post_meta/updated_post_meta. Without this, a starter template
493 - * imported after the negative cache was written would show neither the Thank You
494 - * prompt nor the setup widget until the transient expired.
495 - *
496 - * Arguments are read from func_get_args() rather than declared: the hook passes
497 - * ( $meta_id, $post_id, $meta_key ) and the meta id is never needed, so declaring
498 - * it would leave an unused parameter that the coding-standards gate rejects.
499 - *
500 - * @since 2.12.4
501 - * @return void
502 - */
503 - public function invalidate_starter_template_cache() {
504 - $args = func_get_args();
505 - $post_id = isset( $args[1] ) ? (int) $args[1] : 0;
506 - $meta_key = isset( $args[2] ) ? (string) $args[2] : '';
507 -
508 - if ( self::ASTRA_SITES_IMPORT_META !== $meta_key ) {
509 - return;
510 - }
511 -
512 - // A full-site import stamps this marker on every post it creates, so narrow to
513 - // our own post type: both features only ever query sureforms_form, and this
514 - // avoids clearing the cache repeatedly for pages and products during an import.
515 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $post_id ) ) {
516 - return;
517 - }
518 -
519 - delete_transient( self::NO_IMPORTED_FORMS_TRANSIENT );
520 - }
521 -
522 - /**
523 - * Clear the setup-card request memo (#3031).
524 - *
525 - * Lets tests exercise the populated path, and is a safe hook for anything that
526 - * changes which form qualifies (e.g. a form save).
527 - *
528 - * @since 2.12.4
529 - * @return void
530 - */
531 - public static function reset_form_setup_card_cache() {
532 - self::$setup_card_cache = [];
533 - }
534 -
535 - /**
536 - * REST handler: record a "Finish setting up" widget interaction (#3031).
537 - *
538 - * Records the clicked CTA/view action as an analytics event. The request
539 - * carries the displayed form id: capability is re-checked against it here —
540 - * beyond the route's generic permission callback — so only a genuine editor of
541 - * that form can act.
542 - *
543 - * @param \WP_REST_Request<array<string,mixed>> $request Request.
544 - *
545 - * @since 2.12.4
546 - * @return \WP_REST_Response|\WP_Error
547 - */
548 - public function dismiss_form_setup_card( $request ) {
549 - $form_id = absint( $request->get_param( 'form_id' ) );
550 -
551 - if ( $form_id <= 0 || ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $form_id ) || ! current_user_can( 'edit_post', $form_id ) ) {
552 - return new \WP_Error( 'srfm_setup_card_forbidden', __( 'You are not allowed to update this prompt.', 'sureforms' ), [ 'status' => 403 ] );
553 - }
554 -
555 - $action = sanitize_key( (string) $request->get_param( 'action' ) );
556 -
557 - // Interaction analytics for the setup widget (#3031). Each event carries a
558 - // date automatically (see BSF_Analytics_Events::track()) and dedupes per
559 - // event name, matching the sibling notice's telemetry.
560 - $events = [
561 - 'edit_form' => 'form_setup_widget_edit_form',
562 - 'edit_thankyou' => 'form_setup_widget_edit_thankyou',
563 - 'set_up_email' => 'form_setup_widget_set_up_email',
564 - 'view_form' => 'form_setup_widget_view_form',
565 - ];
566 -
567 - if ( isset( $events[ $action ] ) ) {
568 - Analytics::events()->track( $events[ $action ], (string) $form_id );
569 - }
570 -
571 - return new \WP_REST_Response( [ 'success' => true ], 200 );
572 - }
573 -
574 - /**
575 - * Register the "Finish setting up" checklist widget on the main WP dashboard (#3031).
576 - *
577 - * Only for capable users, and only when there is a form still needing setup —
578 - * so the widget never appears empty. The data is memoized in get_form_setup_card()
579 - * and reused by the enqueue and render passes.
580 - *
581 - * @since 2.12.4
582 - * @return void
583 - */
584 - public function register_form_setup_widget() {
585 - if ( ! Helper::current_user_can() ) {
586 - return;
587 - }
588 -
589 - if ( null === self::get_form_setup_card() ) {
590 - return;
591 - }
592 -
593 - wp_add_dashboard_widget(
594 - 'srfm_form_setup_checklist',
595 - __( 'Finish setting up your form', 'sureforms' ),
596 - [ $this, 'render_form_setup_widget' ],
597 - null,
598 - null,
599 - 'normal',
600 - 'high'
601 - );
602 - }
603 -
604 - /**
605 - * Render the setup-checklist widget content (#3031).
606 - *
607 - * A heading (with a link to view the form), a subtitle, and a fixed list of
608 - * optional next-steps — each always shown with its CTA; completion is not
609 - * computed. Each CTA deep-links into the editor (edit form / Thank You message /
610 - * email notification) and records an analytics event via the REST endpoint
611 - * wired in the enqueued inline script.
612 - *
613 - * @since 2.12.4
614 - * @return void
615 - */
616 - public function render_form_setup_widget() {
617 - $card = self::get_form_setup_card();
618 -
619 - if ( null === $card ) {
620 - return;
621 - }
622 -
623 - // Optional next-steps — always offered, their completion is not computed.
624 - // 'event' is the analytics action key beaconed on click (see the widget JS).
625 - $rows = [
626 - [
627 - 'label' => __( 'Review or edit your form', 'sureforms' ),
628 - 'cta' => __( 'Edit form', 'sureforms' ),
629 - 'url' => $card['edit_url'],
630 - 'event' => 'edit_form',
631 - ],
632 - [
633 - 'label' => __( 'Personalize the Thank You message', 'sureforms' ),
634 - 'cta' => __( 'Edit message', 'sureforms' ),
635 - 'url' => $card['thankyou_url'],
636 - 'event' => 'edit_thankyou',
637 - ],
638 - [
639 - 'label' => __( 'Choose who gets notified of new replies', 'sureforms' ),
640 - 'cta' => __( 'Set up email', 'sureforms' ),
641 - 'url' => $card['email_url'],
642 - 'event' => 'set_up_email',
643 - ],
644 - ];
645 -
646 - // Fall back to a generic label for an untitled form so the heading never
647 - // renders "Finish setting up " with a dangling space.
648 - $card_title = '' !== trim( (string) $card['title'] ) ? $card['title'] : __( 'your form', 'sureforms' );
649 - $heading = sprintf(
650 - /* translators: %s: form title. */
651 - __( 'Finish setting up %s', 'sureforms' ),
652 - $card_title
653 - );
654 - ?>
655 - <div class="srfm-setup-checklist" id="srfm-setup-checklist">
656 - <p class="srfm-setup-checklist__title">
657 - <?php echo esc_html( $heading ); ?>
658 - <?php if ( ! empty( $card['view_url'] ) ) { ?>
659 - <a class="srfm-setup-checklist__view" data-srfm-event="view_form" href="<?php echo esc_url( $card['view_url'] ); ?>" target="_blank" rel="noopener noreferrer" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: form title. */ __( 'View %s (opens in a new tab)', 'sureforms' ), $card_title ) ); ?>">
660 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
661 - </a>
662 - <?php } ?>
663 - </p>
664 - <p class="srfm-setup-checklist__subtitle"><?php esc_html_e( 'Customize your form to get it ready for real submissions:', 'sureforms' ); ?></p>
665 -
666 - <ul class="srfm-setup-checklist__steps">
667 - <?php foreach ( $rows as $row ) { ?>
668 - <li class="srfm-setup-checklist__step">
669 - <span class="srfm-setup-checklist__label"><?php echo esc_html( $row['label'] ); ?></span>
670 - <a class="srfm-setup-checklist__cta" data-srfm-event="<?php echo esc_attr( $row['event'] ); ?>" href="<?php echo esc_url( $row['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $row['cta'] ); ?></a>
671 - </li>
672 - <?php } ?>
673 - </ul>
674 - </div>
675 - <?php
676 - }
677 -
678 - /**
679 - * Enqueue the setup-checklist widget's styles and behavior on the dashboard (#3031).
680 - *
681 - * Mirrors the AI widget convention: an inline-only handle carries the CSS and the
682 - * behavior (CTA click analytics), with server values —
683 - * the REST URL, nonce and form id — passed through wp_localize_script rather than
684 - * printed into the markup, so it stays Plugin-Check clean.
685 - *
686 - * @param string $hook_suffix Current admin page hook suffix.
687 - *
688 - * @since 2.12.4
689 - * @return void
690 - */
691 - public function enqueue_form_setup_widget_assets( $hook_suffix ) {
692 - if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) {
693 - return;
694 - }
695 -
696 - $card = self::get_form_setup_card();
697 -
698 - if ( null === $card ) {
699 - return;
700 - }
701 -
702 - $css = <<<'CSS'
703 -#srfm_form_setup_checklist .inside { margin: 0; padding: 0; }
704 -.srfm-setup-checklist { padding: 12px 16px 16px; }
705 -.srfm-setup-checklist__title { margin: 0 0 4px; font-size: 15px; font-weight: 600; color: #1e1e1e; }
706 -.srfm-setup-checklist__view { display: inline-flex; align-items: center; margin-left: 6px; color: #d54e21; vertical-align: middle; }
707 -.srfm-setup-checklist__view:hover, .srfm-setup-checklist__view:focus { color: #b83c14; }
708 -.srfm-setup-checklist__subtitle { margin: 0 0 12px; color: #646970; font-size: 13px; }
709 -.srfm-setup-checklist__steps { margin: 0; padding: 0; list-style: none; }
710 -.srfm-setup-checklist__step { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; }
711 -.srfm-setup-checklist__step + .srfm-setup-checklist__step { margin-top: 6px; }
712 -.srfm-setup-checklist__step { background: #f6f7f7; }
713 -.srfm-setup-checklist__label { flex: 1 1 auto; font-size: 14px; color: #1e1e1e; }
714 -.srfm-setup-checklist__cta { margin-left: auto; border: 0; background: transparent; padding: 0; font-size: 14px; font-weight: 600; color: #d54e21; text-decoration: underline; cursor: pointer; }
715 -.srfm-setup-checklist__cta:hover { color: #b83c14; }
716 -/* Keep visited links on-brand — WP admin's a:visited would otherwise turn them blue. */
717 -.srfm-setup-checklist a:visited { color: #d54e21; }
718 -.srfm-setup-checklist a:visited:hover, .srfm-setup-checklist a:visited:focus { color: #b83c14; }
719 -/* Drop WP's blue focus ring on the widget's links; keep an accessible, on-brand keyboard outline. */
720 -.srfm-setup-checklist a:focus { outline: none; box-shadow: none; }
721 -.srfm-setup-checklist a:focus-visible { outline: 2px solid #d54e21; outline-offset: 2px; box-shadow: none; }
722 -CSS;
723 -
724 - wp_register_style( 'srfm-setup-checklist-widget', false, [], SRFM_VER );
725 - wp_enqueue_style( 'srfm-setup-checklist-widget' );
726 - wp_add_inline_style( 'srfm-setup-checklist-widget', $css );
727 -
728 - wp_register_script( 'srfm-setup-checklist-widget', '', [], SRFM_VER, true );
729 - wp_enqueue_script( 'srfm-setup-checklist-widget' );
730 -
731 - wp_localize_script(
732 - 'srfm-setup-checklist-widget',
733 - 'srfmSetupChecklist',
734 - [
735 - 'restUrl' => esc_url_raw( rest_url( 'sureforms/v1/dismiss-form-setup-card' ) ),
736 - 'nonce' => wp_create_nonce( 'wp_rest' ),
737 - 'formId' => $card['id'],
738 - ]
739 - );
740 -
741 - $inline_script = <<<'JS'
742 -( function () {
743 - const cfg = window.srfmSetupChecklist || {};
744 - const widget = document.getElementById( 'srfm-setup-checklist' );
745 - if ( ! widget ) {
746 - return;
747 - }
748 -
749 - const persist = function ( action ) {
750 - return fetch( cfg.restUrl, {
751 - method: 'POST',
752 - credentials: 'same-origin',
753 - keepalive: true,
754 - headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce },
755 - body: JSON.stringify( { form_id: cfg.formId, action: action } ),
756 - } ).catch( function () {} );
757 - };
758 -
759 - // Beacon the CTA / view-form clicks for analytics. keepalive on the fetch lets
760 - // the request finish even though the CTA immediately navigates away.
761 - widget.addEventListener( 'click', function ( e ) {
762 - const target = e.target?.closest?.( '[data-srfm-event]' );
763 - if ( target ) {
764 - persist( target.getAttribute( 'data-srfm-event' ) );
765 - }
766 - } );
767 -}() );
768 -JS;
769 -
770 - wp_add_inline_script( 'srfm-setup-checklist-widget', $inline_script );
771 - }
772 -
773 - /**
774 - * Register the "Finish setting up" prompt as an Astra Notices admin notice (#3030).
775 - *
776 - * Hooked to admin_notices so it registers before the Astra Notices library
777 - * renders (priority 30). Shown on every admin screen EXCEPT the main dashboard,
778 - * for the newest form the current user can edit that still has an unfinished
779 - * step (default Thank You message, or no reply destination). Uses a single
780 - * stable notice id so the library's built-in ✕ dismissal is one persistent
781 - * choice ("stop nudging me"), not a per-form row.
782 - *
783 - * Split out from the renderer so the decision has exactly one home. The Getting
784 - * Started notice suppresses itself when this returns a form, and duplicating the
785 - * conditions there would have meant two copies drifting apart. Reading it costs
786 - * nothing extra — get_thankyou_prompt_forms() memoizes its query per request.
787 - *
788 - * @since 2.12.6
789 - * @return array<string,mixed>|null The form to prompt for, or null when no prompt should render.
790 - */
791 - public function get_displayable_thankyou_prompt() {
792 - if ( ! Helper::current_user_can() || ! class_exists( 'Astra_Notices' ) ) {
793 - return null;
794 - }
795 -
796 - /**
797 - * Short-circuit the "Finish setting up" Thank You notice.
798 - *
799 - * @param bool $show Whether to show the notice. Default true.
800 - *
801 - * @since 2.12.4
802 - */
803 - if ( ! apply_filters( 'srfm_show_thankyou_prompt', true ) ) {
804 - return null;
805 - }
806 -
807 - // Everywhere in wp-admin except the main dashboard. A null screen fails
808 - // closed (return) rather than registering the notice on an unknown screen.
809 - $screen = get_current_screen();
810 -
811 - if ( ! $screen || 'dashboard' === $screen->id ) {
812 - return null;
813 - }
814 -
815 - // The library only checks dismissal at render (priority 30, after this
816 - // query would already have run). Check it up front so a user who dismissed
817 - // the prompt never pays for the WP_Query on subsequent admin page views.
818 - if ( 'notice-dismissed' === get_user_meta( get_current_user_id(), self::THANKYOU_PROMPT_NOTICE_ID, true ) ) {
819 - return null;
820 - }
821 -
822 - // array_values so a filter returning a key-preserving array (e.g. the
823 - // result of array_filter()) still exposes the newest prompt at index 0.
824 - $prompts = array_values( (array) self::get_thankyou_prompt_forms() );
825 -
826 - // Validate every key build_thankyou_notice_markup() reads, not just id/edit_url
827 - // — a filter returning a partial payload would otherwise trip "Undefined array
828 - // key" warnings and esc_url( null ) deprecations on every admin page.
829 - if (
830 - empty( $prompts[0] ) || ! is_array( $prompts[0] )
831 - || empty( $prompts[0]['id'] ) || empty( $prompts[0]['edit_url'] )
832 - || empty( $prompts[0]['thankyou_url'] ) || empty( $prompts[0]['replies_url'] )
833 - || ! isset( $prompts[0]['title'] )
834 - ) {
835 - return null;
836 - }
837 -
838 - return $prompts[0];
839 - }
840 -
841 - /**
842 - * Render the "Finish setting up" Thank You notice (#3030).
843 - *
844 - * @since 2.12.4
845 - * @return void
846 - */
847 - public function render_thankyou_prompt_notice() {
848 - $notice_id = self::THANKYOU_PROMPT_NOTICE_ID;
849 - $form = $this->get_displayable_thankyou_prompt();
850 -
851 - if ( null === $form ) {
852 - return;
853 - }
854 -
855 - // A broken form outranks a setup prompt. This is the top of the existing
856 - // precedence chain, so the action-item check goes here rather than the
857 - // action items standing down for an engagement notice.
858 - if ( $this->has_action_item_warnings() ) {
859 - return;
860 - }
861 -
862 - \Astra_Notices::add_notice(
863 - [
864 - 'id' => $notice_id,
865 - 'type' => 'info',
866 - 'message' => self::build_thankyou_notice_markup( $form ),
867 - 'class' => 'srfm-notice srfm-thankyou-notice',
868 - 'is_dismissible' => true,
869 - 'display-with-other-notices' => true,
870 - // Render late so this nudge never pre-empts higher-priority notices
871 - // (e.g. Astra's minimum-version warnings, which are display-with-
872 - // other-notices => false and would be skipped once ours renders).
873 - 'priority' => 100,
874 - ]
875 - );
876 -
877 - // The message is wp_kses_post'd by the library, so the brand-orange styling
878 - // is printed through the notice's pre-markup hook instead of inline.
879 - add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] );
880 -
881 - // Track clicks on the CTAs and the dismiss ✕ via the shared notice-response
882 - // endpoint, enqueued only when the notice actually renders.
883 - add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_thankyou_notice_tracking' ] );
884 - }
885 -
886 - /**
887 - * Enqueue the click-tracking for the Thank You notice (#3030).
888 - *
889 - * Sends an analytics beacon to the shared `srfm_notice_response` AJAX handler
890 - * when a CTA or the dismiss ✕ is clicked. Uses `keepalive` so the beacon
891 - * survives the navigation the CTA links trigger.
892 - *
893 - * @since 2.12.4
894 - * @return void
895 - */
896 - public function enqueue_thankyou_notice_tracking() {
897 - if ( wp_script_is( 'srfm-thankyou-notice-track', 'enqueued' ) ) {
898 - return;
899 - }
900 -
901 - wp_register_script( 'srfm-thankyou-notice-track', '', [], SRFM_VER, true );
902 - wp_enqueue_script( 'srfm-thankyou-notice-track' );
903 -
904 - $config = wp_json_encode(
905 - [
906 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
907 - 'nonce' => wp_create_nonce( 'srfm_notice_response' ),
908 - ]
909 - );
910 -
911 - wp_add_inline_script( 'srfm-thankyou-notice-track', 'window.srfmThankYouNoticeTrack = ' . $config . ';', 'before' );
912 -
913 - $inline_script = <<<'JS'
914 -( function () {
915 - const cfg = window.srfmThankYouNoticeTrack || {};
916 - const wrap = document.querySelector( '.srfm-thankyou-notice' );
917 - if ( ! wrap ) {
918 - return;
919 - }
920 - const noticeId = wrap.id || '';
921 - const send = function ( button ) {
922 - const body = new URLSearchParams();
923 - body.append( 'action', 'srfm_notice_response' );
924 - body.append( 'nonce', cfg.nonce );
925 - body.append( 'notice_id', noticeId );
926 - body.append( 'button', button );
927 - fetch( cfg.ajaxurl, {
928 - method: 'POST',
929 - credentials: 'same-origin',
930 - keepalive: true,
931 - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
932 - body: body.toString(),
933 - } ).catch( function () {} );
934 - };
935 - // Delegate from the wrapper: this inline script runs at parse time, before
936 - // core's common.js injects the .notice-dismiss ✕ (on DOMContentLoaded), so a
937 - // direct querySelector for it would find nothing and the dismiss beacon would
938 - // never fire. Delegation catches the ✕ and the CTAs whenever they exist.
939 - const ctas = [
940 - [ '.srfm-ty-edit-form', 'edit_form' ],
941 - [ '.srfm-ty-set-replies', 'set_replies' ],
942 - [ '.srfm-ty-edit-thankyou', 'edit_thankyou' ],
943 - ];
944 - wrap.addEventListener( 'click', function ( e ) {
945 - if ( e.target.closest( '.notice-dismiss' ) ) {
946 - send( 'dismissed' );
947 - return;
948 - }
949 - for ( let i = 0; i < ctas.length; i++ ) {
950 - if ( e.target.closest( ctas[ i ][ 0 ] ) ) {
951 - send( ctas[ i ][ 1 ] );
952 - return;
953 - }
954 - }
955 - } );
956 -}() );
957 -JS;
958 -
959 - wp_add_inline_script( 'srfm-thankyou-notice-track', $inline_script );
960 - }
961 -
962 - /**
963 - * Print the Thank You notice's brand-orange styling (#3030).
964 - *
965 - * Fired via astra_notice_before_markup_{id} so it lands right before the notice
966 - * and only when the notice actually renders.
967 - *
968 - * @since 2.12.4
969 - * @return void
970 - */
971 - public function print_srfm_notice_styles() {
972 - // The library wp_kses_post()'s the message, which strips <svg> and data:
973 - // image srcs, so the SureForms mark is painted as a CSS background here
974 - // (this hook fires outside that kses call). URL-encoded, not base64, so the
975 - // value is fully percent-encoded and safe to pass through esc_url.
976 - $icon = 'data:image/svg+xml,' . rawurlencode(
977 - '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 32 32"><path fill="#D54407" fill-rule="evenodd" clip-rule="evenodd" d="M32 0H0V32H32V0ZM22.8573 6.85728H9.14304V11.4287V13.7144L11.4288 11.4287H22.8573V6.85728ZM20.5717 13.7146H9.14314V18.286V20.5714V20.5718V25.1428H16.0003V20.5714H9.14351L11.4289 18.286H20.5717V13.7146Z"/></svg>'
978 - );
979 - ?>
980 - <style id="srfm-notice-styles">
981 - .srfm-notice.notice { border-left-color: #D54407; }
982 - /* Stack our blocks (the library lays the container out as a flex row) and reserve room on the left for the SureForms mark. */
983 - .srfm-notice .astra-notice-container { display: block; padding: 4px 0 4px 52px; background: url('<?php echo esc_url( $icon, [ 'data' ] ); ?>') no-repeat 4px 6px; background-size: 32px 32px; }
984 - .srfm-notice .srfm-notice__title { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: #1d2327; }
985 - .srfm-notice .srfm-notice__text { margin: 0 0 10px; color: #50575e; }
986 - .srfm-notice .srfm-notice__actions { margin: 12px 0 2px; display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center; }
987 - .srfm-notice .button-primary { background: #D54407; border-color: #D54407; color: #fff; box-shadow: none; text-shadow: none; }
988 - .srfm-notice .button-primary:hover, .srfm-notice .button-primary:focus { background: #C83B00; border-color: #C83B00; color: #fff; box-shadow: none; }
989 - .srfm-notice .button:not(.button-primary) { background: transparent; border-color: transparent; color: #D54407; box-shadow: none; padding: 0; }
990 - .srfm-notice .button:not(.button-primary):hover, .srfm-notice .button:not(.button-primary):focus { background: transparent; border-color: transparent; color: #C83B00; box-shadow: none; }
991 - .srfm-notice .button-primary:focus { outline: 2px solid #D54407; outline-offset: 1px; }
992 - </style>
993 - <?php
994 - }
995 -
996 - /**
997 - * Check and save the first form creation time stamp.
998 - * If not already saved.
999 - *
1000 - * @since 1.10.1
1001 - * @return void
1002 - */
1003 - public static function save_first_form_creation_time_stamp() {
1004 - if ( ! Helper::current_user_can() || self::is_first_form_created() || ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
1005 - return;
1006 - }
1007 -
1008 - // Get the first form creation time from the database that is published.
1009 - $query = new \WP_Query(
1010 - [
1011 - 'post_type' => SRFM_FORMS_POST_TYPE,
1012 - 'posts_per_page' => 1,
1013 - 'orderby' => 'date',
1014 - 'order' => 'ASC',
1015 - 'fields' => 'ids',
1016 - 'post_status' => 'publish',
1017 - ]
1018 - );
1019 -
1020 - if ( ! empty( $query->posts ) && isset( $query->posts[0] ) ) {
1021 - // Get the first post from the query result.
1022 - $post_id = $query->posts[0];
1023 - // Get the post creation time in GMT.
1024 - $creation_time = get_post_field( 'post_date_gmt', $post_id );
1025 - // Convert the creation time to a timestamp.
1026 - $timestamp = strtotime( $creation_time );
1027 -
1028 - if ( ! $timestamp ) {
1029 - return;
1030 - }
1031 -
1032 - Helper::update_srfm_option( 'first_form_created_at', $timestamp );
1033 - }
1034 - }
1035 -
1036 - /**
1037 - * Check if n days have passed since the first form creation.
1038 - * This is used to determine if the dynamic nudges should be shown.
1039 - *
1040 - * @param int $days Number of days to check.
1041 - * @since 1.10.1
1042 - * @return bool
1043 - */
1044 - public static function check_first_form_creation_threshold( $days = 3 ) {
1045 - $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp();
1046 -
1047 - if ( ! $first_form_creation_time_stamp ) {
1048 - return false; // No forms created yet.
1049 - }
1050 -
1051 - /**
1052 - * Calculate the number of days since the first form was created.
1053 - */
1054 - $days_from_creation = ( strtotime( current_time( 'mysql' ) ) - $first_form_creation_time_stamp ) / DAY_IN_SECONDS;
1055 -
1056 - // Return a boolean indicating if the number of days since creation is greater than the specified days.
1057 - return $days_from_creation > $days;
1058 - }
1059 -
1060 - /**
1061 105 * Show action on plugin page.
1062 106 *
1063 107 * @param array $links links.
1064 108 * @return array
@@ -1066,17 +110,15 @@
1066 110 */
1067 111 public function add_action_links( $links ) {
1068 112 if ( ! Helper::has_pro() ) {
1069 113 // Display upsell link if SureForms Pro is not installed.
1070 - $upsell_link = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'plugin-list' ] );
1071 -
1072 - ob_start();
1073 - ?>
1074 - <a href="<?php echo esc_url( $upsell_link ); ?>" target="_blank" rel="noreferrer" class="sureforms-plugins-go-pro">
1075 - <?php echo esc_html__( 'Get SureForms Pro', 'sureforms' ); ?>
1076 - </a>
1077 - <?php
1078 - $links[] = trim( ob_get_clean() );
114 + $upsell_link = add_query_arg(
115 + [
116 + 'utm_medium' => 'plugin-list',
117 + ],
118 + Helper::get_sureforms_website_url( 'pricing' )
119 + );
120 + $links[] = '<a href="' . esc_url( $upsell_link ) . '" target="_blank" rel="noreferrer" class="sureforms-plugins-go-pro">' . esc_html__( 'Get SureForms Pro', 'sureforms' ) . '</a>';
1079 121 }
1080 122
1081 123 return $links;
1082 124 }
@@ -1144,15 +186,20 @@
1144 186 * @return void
1145 187 * @since 0.0.1
1146 188 */
1147 189 public function add_menu_page() {
1148 - $menu_slug = 'sureforms_menu';
190 + if ( ! current_user_can( 'manage_options' ) ) {
191 + return;
192 + }
1149 193
194 + $capability = 'manage_options';
195 + $menu_slug = 'sureforms_menu';
196 +
1150 197 $logo = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/icon.svg' );
1151 198 add_menu_page(
1152 199 __( 'SureForms', 'sureforms' ),
1153 200 __( 'SureForms', 'sureforms' ),
1154 - self::$sureforms_page_default_capability,
201 + 'edit_others_posts',
1155 202 $menu_slug,
1156 203 static function () {
1157 204 },
1158 205 'data:image/svg+xml;base64,' . base64_encode( $logo ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
@@ -1163,9 +210,9 @@
1163 210 add_submenu_page(
1164 211 $menu_slug,
1165 212 __( 'Dashboard', 'sureforms' ),
1166 213 __( 'Dashboard', 'sureforms' ),
1167 - self::$sureforms_page_default_capability,
214 + $capability,
1168 215 $menu_slug,
1169 216 [ $this, 'render_dashboard' ]
1170 217 );
1171 218 }
@@ -1181,9 +228,9 @@
1181 228 add_submenu_page(
1182 229 'sureforms_menu',
1183 230 __( 'Settings', 'sureforms' ),
1184 231 __( 'Settings', 'sureforms' ),
1185 - self::$sureforms_page_default_capability,
232 + 'edit_others_posts',
1186 233 'sureforms_form_settings',
1187 234 $callback
1188 235 );
1189 236
@@ -1228,119 +275,25 @@
1228 275 */
1229 276 public function add_upgrade_to_pro() {
1230 277 // The url used here is used as a selector for css to style the upgrade to pro submenu.
1231 278 // If you are changing this url, please make sure to update the css as well.
1232 - $upgrade_url = Helper::get_sureforms_website_url( 'upgrade', [ 'utm_medium' => 'submenu_link_upgrade' ] );
279 + $upgrade_url = add_query_arg(
280 + [
281 + 'utm_medium' => 'submenu_link_upgrade',
282 + ],
283 + Helper::get_sureforms_website_url( 'upgrade' )
284 + );
1233 285
1234 286 add_submenu_page(
1235 287 'sureforms_menu',
1236 288 __( 'Upgrade', 'sureforms' ),
1237 289 __( 'Upgrade', 'sureforms' ),
1238 - self::$sureforms_page_default_capability,
290 + 'edit_others_posts',
1239 291 $upgrade_url
1240 292 );
1241 293 }
1242 294
1243 295 /**
1244 - * Add Quiz empty state submenu page for free users.
1245 - *
1246 - * @return void
1247 - * @since 2.7.0
1248 - */
1249 - public function add_quiz_page() {
1250 - add_submenu_page(
1251 - 'sureforms_menu',
1252 - __( 'Quiz Entries', 'sureforms' ),
1253 - __( 'Quizzes', 'sureforms' ) .
1254 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1255 - esc_html__( 'New', 'sureforms' ) .
1256 - '</span>',
1257 - self::$sureforms_page_default_capability,
1258 - 'sureforms_quiz_entries',
1259 - [ $this, 'render_quiz_empty_state' ],
1260 - 5
1261 - );
1262 - }
1263 -
1264 - /**
1265 - * Quiz empty state page callback.
1266 - *
1267 - * @return void
1268 - * @since 2.7.0
1269 - */
1270 - public function render_quiz_empty_state() {
1271 - ?>
1272 - <div id="srfm-quiz-entries-root" class="srfm-admin-wrapper"></div>
1273 - <?php
1274 - }
1275 -
1276 - /**
1277 - * Add Survey Reports promotional submenu page for free users.
1278 - *
1279 - * @return void
1280 - * @since 2.8.0
1281 - */
1282 - public function add_survey_reports_page() {
1283 - add_submenu_page(
1284 - 'sureforms_menu',
1285 - __( 'Survey Reports', 'sureforms' ),
1286 - __( 'Survey Reports', 'sureforms' ) .
1287 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1288 - esc_html__( 'New', 'sureforms' ) .
1289 - '</span>',
1290 - self::$sureforms_page_default_capability,
1291 - 'sureforms_survey_reports',
1292 - [ $this, 'render_survey_empty_state' ],
1293 - 6
1294 - );
1295 - }
1296 -
1297 - /**
1298 - * Survey empty state page callback.
1299 - *
1300 - * @return void
1301 - * @since 2.8.0
1302 - */
1303 - public function render_survey_empty_state() {
1304 - ?>
1305 - <div id="srfm-survey-empty-state-root" class="srfm-admin-wrapper"></div>
1306 - <?php
1307 - }
1308 -
1309 - /**
1310 - * Add Partial Entries promotional submenu page for free users.
1311 - *
1312 - * @return void
1313 - * @since 2.9.0
1314 - */
1315 - public function add_partial_entries_page() {
1316 - add_submenu_page(
1317 - 'sureforms_menu',
1318 - __( 'Partial Entries', 'sureforms' ),
1319 - __( 'Partial Entries', 'sureforms' ) .
1320 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1321 - esc_html__( 'New', 'sureforms' ) .
1322 - '</span>',
1323 - self::$sureforms_page_default_capability,
1324 - 'sureforms_partial_entries',
1325 - [ $this, 'render_partial_entries_empty_state' ],
1326 - 7
1327 - );
1328 - }
1329 -
1330 - /**
1331 - * Partial Entries empty state page callback.
1332 - *
1333 - * @return void
1334 - * @since 2.9.0
1335 - */
1336 - public function render_partial_entries_empty_state() {
1337 - ?>
1338 - <div id="srfm-partial-entries-empty-state-root" class="srfm-admin-wrapper"></div>
1339 - <?php
1340 - }
1341 -
1342 - /**
1343 296 * Add SMTP promotional submenu page.
1344 297 *
1345 298 * @return void
1346 299 * @since 1.7.1
@@ -1349,9 +302,9 @@
1349 302 add_submenu_page(
1350 303 'sureforms_menu',
1351 304 __( 'SMTP', 'sureforms' ),
1352 305 __( 'SMTP', 'sureforms' ),
1353 - self::$sureforms_page_default_capability,
306 + 'edit_others_posts',
1354 307 'sureforms_smtp',
1355 308 [ $this, 'suremail_page_callback' ]
1356 309 );
1357 310
@@ -1372,11 +325,9 @@
1372 325 * @return void
1373 326 * @since 1.7.1
1374 327 */
1375 328 public function suremail_page_callback() {
1376 - ?>
1377 - <div id="srfm-suremail-container" class="srfm-admin-wrapper"></div>
1378 - <?php
329 + echo '<div id="srfm-suremail-container" class="srfm-admin-wrapper"></div>';
1379 330 }
1380 331
1381 332 /**
1382 333 * Render Admin Dashboard.
@@ -1384,11 +335,9 @@
1384 335 * @return void
1385 336 * @since 0.0.1
1386 337 */
1387 338 public function render_dashboard() {
1388 - ?>
1389 - <div id="srfm-dashboard-container" class="srfm-admin-wrapper"></div>
1390 - <?php
339 + echo '<div id="srfm-dashboard-container" class="srfm-admin-wrapper"></div>';
1391 340 }
1392 341
1393 342 /**
1394 343 * Settings page callback.
@@ -1396,43 +345,12 @@
1396 345 * @return void
1397 346 * @since 0.0.1
1398 347 */
1399 348 public function settings_page_callback() {
1400 - ?>
1401 - <div id="srfm-settings-container" class="srfm-admin-wrapper"></div>
1402 - <?php
349 + echo '<div id="srfm-settings-container" class="srfm-admin-wrapper"></div>';
1403 350 }
1404 351
1405 352 /**
1406 - * Add Learn submenu page.
1407 - *
1408 - * @return void
1409 - * @since 2.5.2
1410 - */
1411 - public function add_learn_page() {
1412 - add_submenu_page(
1413 - 'sureforms_menu',
1414 - __( 'Learn', 'sureforms' ),
1415 - __( 'Learn', 'sureforms' ),
1416 - self::$sureforms_page_default_capability,
1417 - 'sureforms_learn',
1418 - [ $this, 'render_learn' ]
1419 - );
1420 - }
1421 -
1422 - /**
1423 - * Learn page callback.
1424 - *
1425 - * @return void
1426 - * @since 2.5.2
1427 - */
1428 - public function render_learn() {
1429 - ?>
1430 - <div id="srfm-learn-root" class="srfm-admin-wrapper"></div>
1431 - <?php
1432 - }
1433 -
1434 - /**
1435 353 * Add new form menu item.
1436 354 *
1437 355 * @return void
1438 356 * @since 0.0.1
@@ -1439,20 +357,11 @@
1439 357 */
1440 358 public function add_new_form() {
1441 359 add_submenu_page(
1442 360 'sureforms_menu',
1443 - __( 'Forms', 'sureforms' ),
1444 - __( 'Forms', 'sureforms' ),
1445 - self::$sureforms_page_default_capability,
1446 - 'sureforms_forms',
1447 - [ $this, 'render_forms' ],
1448 - 1
1449 - );
1450 - add_submenu_page(
1451 - 'sureforms_menu',
1452 361 __( 'New Form', 'sureforms' ),
1453 362 __( 'New Form', 'sureforms' ),
1454 - self::$sureforms_page_default_capability,
363 + 'edit_others_posts',
1455 364 'add-new-form',
1456 365 [ $this, 'add_new_form_callback' ],
1457 366 2
1458 367 );
@@ -1459,24 +368,14 @@
1459 368 $entries_hook = add_submenu_page(
1460 369 'sureforms_menu',
1461 370 __( 'Entries', 'sureforms' ),
1462 371 __( 'Entries', 'sureforms' ),
1463 - self::$sureforms_page_default_capability,
372 + 'edit_others_posts',
1464 373 SRFM_ENTRIES,
1465 374 [ $this, 'render_entries' ],
1466 375 3
1467 376 );
1468 377
1469 - add_submenu_page(
1470 - 'sureforms_menu',
1471 - __( 'Payments', 'sureforms' ),
1472 - __( 'Payments', 'sureforms' ),
1473 - self::$sureforms_page_default_capability,
1474 - SRFM_PAYMENTS,
1475 - [ $this, 'render_payments' ],
1476 - 4
1477 - );
1478 -
1479 378 if ( $entries_hook ) {
1480 379 add_action( 'load-' . $entries_hook, [ $this, 'mark_entries_page_visit' ] );
1481 380 }
1482 381 }
@@ -1481,20 +380,8 @@
1481 380 }
1482 381 }
1483 382
1484 383 /**
1485 - * Payments page callback.
1486 - *
1487 - * @return void
1488 - * @since 2.0.0
1489 - */
1490 - public function render_payments() {
1491 - ?>
1492 - <div id="srfm-payments-react-container" class="srfm-admin-wrapper"></div>
1493 - <?php
1494 - }
1495 -
1496 - /**
1497 384 * Add new form mentu item callback.
1498 385 *
1499 386 * @return void
1500 387 * @since 0.0.1
@@ -1499,34 +386,41 @@
1499 386 * @return void
1500 387 * @since 0.0.1
1501 388 */
1502 389 public function add_new_form_callback() {
1503 - ?>
1504 - <div id="srfm-add-new-form-container" class="srfm-admin-wrapper"></div>
1505 - <?php
390 + echo '<div id="srfm-add-new-form-container" class="srfm-admin-wrapper"></div>';
1506 391 }
1507 392
1508 393 /**
1509 - * Forms page callback.
1510 - *
1511 - * @return void
1512 - * @since 2.0.0
1513 - */
1514 - public function render_forms() {
1515 - ?>
1516 - <div id="srfm-forms-root" class="srfm-admin-wrapper"></div>
1517 - <?php
1518 - }
1519 -
1520 - /**
1521 394 * Entries page callback.
1522 395 *
1523 396 * @since 0.0.13
1524 - * @since 2.0.0 - Updated the entries UI and the function definition.
1525 397 * @return void
1526 398 */
1527 399 public function render_entries() {
1528 - echo '<div id="srfm-entries-root"></div>';
400 + // Render single entry view.
401 + // Adding the phpcs ignore nonce verification as no database operations are performed in this function, it is used to display the single entry view.
402 + if ( isset( $_GET['entry_id'] ) && is_numeric( $_GET['entry_id'] ) && isset( $_GET['view'] ) && 'details' === $_GET['view'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here and explained in the comments above as well.
403 + $single_entry_view = new Single_Entry();
404 + $single_entry_view->render();
405 + return;
406 + }
407 +
408 + // Render all entries view.
409 + $entries_table = new Entries_List_Table();
410 + $entries_table->prepare_items();
411 + echo '<div class="wrap"><h1 class="wp-heading-inline">' . esc_html__( 'Entries', 'sureforms' ) . '</h1>';
412 + if ( empty( $entries_table->all_entries_count ) && empty( $entries_table->trash_entries_count ) ) {
413 + $instance = Post_Types::get_instance();
414 + $instance->sureforms_render_blank_state( SRFM_ENTRIES );
415 + $instance->get_blank_state_styles();
416 + return;
417 + }
418 + echo '<form method="get">';
419 + echo '<input type="hidden" name="page" value="sureforms_entries">';
420 + $entries_table->display();
421 + echo '</form>';
422 + echo '</div>';
1529 423 }
1530 424
1531 425 /**
1532 426 * Add notification badge to SureForms menu when there are new entries.
@@ -1534,9 +428,9 @@
1534 428 * @since 1.7.3
1535 429 * @return void
1536 430 */
1537 431 public function maybe_add_entries_badge() {
1538 - if ( ! Helper::current_user_can() ) {
432 + if ( ! current_user_can( 'edit_others_posts' ) ) {
1539 433 return;
1540 434 }
1541 435
1542 436 // If currently viewing the entries listing page, mark it as visited and skip the badge.
@@ -1555,15 +449,10 @@
1555 449
1556 450 global $menu;
1557 451 foreach ( $menu as $index => $item ) {
1558 452 if ( isset( $item[2] ) && 'sureforms_menu' === $item[2] ) {
1559 - ob_start();
1560 - ?>
1561 - <span class="srfm-update-dot"></span>
1562 - <?php
1563 - $dot_html = ob_get_clean();
1564 453 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for menu item.
1565 - $menu[ $index ][0] .= $dot_html;
454 + $menu[ $index ][0] .= ' <span class="srfm-update-dot"></span>';
1566 455 break;
1567 456 }
1568 457 }
1569 458
@@ -1570,17 +459,12 @@
1570 459 global $submenu;
1571 460 if ( isset( $submenu['sureforms_menu'] ) ) {
1572 461 foreach ( $submenu['sureforms_menu'] as $index => $sub_item ) {
1573 462 if ( isset( $sub_item[2] ) && SRFM_ENTRIES === $sub_item[2] ) {
1574 - ob_start();
1575 - ?>
1576 - <span class="update-plugins count-<?php echo absint( $new_entries ); ?>">
1577 - <span class="plugin-count"><?php echo absint( $new_entries ); ?></span>
1578 - </span>
1579 - <?php
1580 - $badge_html = ob_get_clean();
1581 - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for submenu item.
1582 - $submenu['sureforms_menu'][ $index ][0] .= $badge_html;
463 + $submenu['sureforms_menu'][ $index ][0] .= sprintf( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for submenu item.
464 + ' <span class="update-plugins count-%1$d"><span class="plugin-count">%1$d</span></span>',
465 + absint( $new_entries )
466 + );
1583 467 break;
1584 468 }
1585 469 }
1586 470 }
@@ -1592,9 +476,9 @@
1592 476 * @since 1.7.3
1593 477 * @return void
1594 478 */
1595 479 public function mark_entries_page_visit() {
1596 - if ( Helper::current_user_can() ) {
480 + if ( current_user_can( 'edit_others_posts' ) ) {
1597 481 $srfm_options = get_option( 'srfm_options', [] );
1598 482 $srfm_options['entries_last_visited'] = time();
1599 483 \SRFM\Inc\Helper::update_admin_settings_option( 'srfm_options', $srfm_options );
1600 484 }
@@ -1609,22 +493,15 @@
1609 493 * @since 0.0.1
1610 494 */
1611 495 public function add_settings_link( $links, $file ) {
1612 496 if ( 'sureforms/sureforms.php' === $file ) {
1613 - ob_start();
1614 - ?>
1615 - <a href="<?php echo esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ); ?>">
1616 - <?php echo esc_html__( 'Settings', 'sureforms' ); ?>
1617 - </a>
1618 - <?php
1619 - $settings_link_html = ob_get_clean();
1620 - $plugin_links = apply_filters(
497 + $plugin_links = apply_filters(
1621 498 'sureforms_plugin_action_links',
1622 499 [
1623 - 'sureforms_settings' => $settings_link_html,
500 + 'sureforms_settings' => '<a href="' . esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ) . '">' . esc_html__( 'Settings', 'sureforms' ) . '</a>',
1624 501 ]
1625 502 );
1626 - $links = array_merge( $plugin_links, $links );
503 + $links = array_merge( $plugin_links, $links );
1627 504 }
1628 505 return $links;
1629 506 }
1630 507
@@ -1654,9 +531,8 @@
1654 531 wp_enqueue_style( SRFM_SLUG . '-backend-blocks', $css_uri . 'blocks/default/backend' . $file_prefix . '.css', [], SRFM_VER );
1655 532 wp_enqueue_style( SRFM_SLUG . '-intl', $vendor_css_uri . 'intl/intlTelInput-backend.min.css', [], SRFM_VER );
1656 533 wp_enqueue_style( SRFM_SLUG . '-common', $css_uri . 'common' . $file_prefix . '.css', [], SRFM_VER );
1657 534 wp_enqueue_style( SRFM_SLUG . '-reactQuill', $vendor_css_uri . 'quill/quill.snow.css', [], SRFM_VER );
1658 - wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS );
1659 535 wp_enqueue_style( SRFM_SLUG . '-single-form-modal', $css_uri . 'single-form-setting' . $file_prefix . '.css', [], SRFM_VER );
1660 536
1661 537 // if version is equal to or lower than 6.6.2 then add compatibility css.
1662 538 if ( version_compare( $wp_version, '6.6.2', '<=' ) ) {
@@ -1735,8 +611,9 @@
1735 611 global $wp_version;
1736 612
1737 613 $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min';
1738 614 $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified';
615 + $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/';
1739 616 $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/';
1740 617 $is_rtl = is_rtl();
1741 618 $rtl = $is_rtl ? '-rtl' : '';
1742 619
@@ -1744,138 +621,44 @@
1744 621 * List of the handles in which we need to add translation compatibility.
1745 622 */
1746 623 $script_translations_handlers = [];
1747 624 $onboarding_instance = Onboarding::get_instance();
1748 - $current_user = wp_get_current_user();
1749 625
1750 626 $localization_data = [
1751 - 'site_url' => get_site_url(),
1752 - 'current_user_login' => $current_user->user_login ?? '',
1753 - 'website_lead_details' => [
1754 - 'first_name' => $current_user->first_name ?? '',
1755 - 'last_name' => $current_user->last_name ?? '',
1756 - 'email' => $current_user->user_email ?? '',
1757 - ],
1758 - 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(),
1759 - 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ),
1760 - 'plugin_version' => SRFM_VER,
1761 - 'global_settings_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '',
1762 - 'is_pro_active' => Helper::has_pro(),
1763 - 'is_first_form_created' => self::is_first_form_created(),
1764 - 'check_three_days_threshold' => self::check_first_form_creation_threshold(),
1765 - 'check_eight_days_threshold' => self::check_first_form_creation_threshold( 8 ),
1766 - 'pro_plugin_version' => Helper::has_pro() ? SRFM_PRO_VER : '',
1767 - 'pro_plugin_name' => Helper::has_pro() && defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro',
1768 - 'sureforms_pricing_page' => Helper::get_sureforms_website_url( 'pricing' ),
1769 - 'field_spacing_vars' => Helper::get_css_vars(),
1770 - 'is_ver_lower_than_6_7' => version_compare( $wp_version, '6.6.2', '<=' ),
1771 - 'integrations' => Helper::sureforms_get_integration(),
1772 - 'rotating_plugin_banner' => Helper::get_rotating_plugin_banner(),
1773 - 'ajax_url' => admin_url( 'admin-ajax.php' ),
1774 - 'client_logs_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_client_logs' ) : '',
1775 - 'action_items' => $this->get_action_items(),
1776 - 'notice_response_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_notice_response' ) : '',
1777 - 'dismiss_action_item_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_dismiss_action_item' ) : '',
1778 - 'sf_plugin_manager_nonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ),
1779 - 'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
1780 - 'plugin_activating_text' => __( 'Activating...', 'sureforms' ),
1781 - 'plugin_activated_text' => __( 'Activated', 'sureforms' ),
1782 - 'plugin_activate_text' => __( 'Activate', 'sureforms' ),
1783 - 'plugin_installing_text' => __( 'Installing...', 'sureforms' ),
1784 - 'plugin_installed_text' => __( 'Installed', 'sureforms' ),
1785 - 'privacy_policy_url' => Helper::get_sureforms_website_url( 'privacy-policy/' ),
1786 - 'is_rtl' => $is_rtl,
1787 - 'onboarding_completed' => method_exists( $onboarding_instance, 'get_onboarding_status' ) ? $onboarding_instance->get_onboarding_status() : false,
1788 - 'migration_banner_dismissed' => method_exists( $onboarding_instance, 'is_migration_banner_dismissed' ) ? $onboarding_instance->is_migration_banner_dismissed() : false,
1789 - 'migration_settings_url' => admin_url( 'admin.php?page=sureforms_form_settings&tab=migration-settings' ),
1790 - 'onboarding_redirect' => isset( $_GET['srfm-activation-redirect'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required for the activation redirection.
1791 - 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ),
1792 - 'general_settings_url' => admin_url( '/options-general.php' ),
1793 - 'additional_header_nav_items' => [],
1794 - // Smart tags for the Global Defaults email notification fields.
1795 - // srfm_block_data is only available in the block editor; these keys
1796 - // make the same data accessible on the settings page via srfm_admin.
1797 - 'smart_tags_array' => Smart_Tags::smart_tag_list(),
1798 - 'smart_tags_array_email' => Smart_Tags::email_smart_tag_list(),
1799 - // Default confirmation message HTML (icon + heading + text) used as
1800 - // the initial React state before the settings API response arrives.
1801 - 'default_confirmation_message' => Global_Settings::get_default_confirmation_message(),
1802 - 'payments' => apply_filters(
1803 - 'srfm_admin_localize_payments_data',
1804 - [
1805 - 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
1806 - 'stripe_mode' => Stripe_Helper::get_stripe_mode(),
1807 - 'stripe_connect_url' => Stripe_Helper::get_stripe_settings_url(),
1808 - 'currencies_data' => Payment_Helper::get_all_currencies_data(),
1809 - 'zero_decimal_currencies' => Payment_Helper::get_zero_decimal_currencies(),
1810 - 'webhook_url' => Stripe_Helper::get_webhook_url(),
1811 - 'webhook_test_connected' => Stripe_Helper::is_webhook_configured( 'test', true ),
1812 - 'webhook_live_connected' => Stripe_Helper::is_webhook_configured( 'live', true ),
1813 - 'is_transaction_present' => Stripe_Helper::is_transaction_present(),
1814 - 'payment_currency' => Payment_Helper::get_currency(),
1815 - 'currency_sign_position' => Payment_Helper::get_currency_sign_position(),
1816 - ]
1817 - ),
1818 - 'mcp_adapter_status' => file_exists( WP_PLUGIN_DIR . '/mcp-adapter/mcp-adapter.php' )
1819 - ? ( is_plugin_active( 'mcp-adapter/mcp-adapter.php' ) ? 'active' : 'installed' )
1820 - : 'not_installed',
1821 - 'mcp_endpoint_url' => esc_url_raw( rest_url( 'sureforms/v1/mcp' ) ),
627 + 'site_url' => get_site_url(),
628 + 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(),
629 + 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ),
630 + 'plugin_version' => SRFM_VER,
631 + 'global_settings_nonce' => current_user_can( 'manage_options' ) ? wp_create_nonce( 'wp_rest' ) : '',
632 + 'is_pro_active' => Helper::has_pro(),
633 + 'pro_plugin_version' => Helper::has_pro() ? SRFM_PRO_VER : '',
634 + 'pro_plugin_name' => Helper::has_pro() && defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro',
635 + 'sureforms_pricing_page' => Helper::get_sureforms_website_url( 'pricing' ),
636 + 'field_spacing_vars' => Helper::get_css_vars(),
637 + 'is_ver_lower_than_6_7' => version_compare( $wp_version, '6.6.2', '<=' ),
638 + 'integrations' => Helper::sureforms_get_integration(),
639 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
640 + 'sf_plugin_manager_nonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ),
641 + 'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
642 + 'plugin_activating_text' => __( 'Activating...', 'sureforms' ),
643 + 'plugin_activated_text' => __( 'Activated', 'sureforms' ),
644 + 'plugin_activate_text' => __( 'Activate', 'sureforms' ),
645 + 'plugin_installing_text' => __( 'Installing...', 'sureforms' ),
646 + 'plugin_installed_text' => __( 'Installed', 'sureforms' ),
647 + 'is_rtl' => $is_rtl,
648 + 'onboarding_completed' => method_exists( $onboarding_instance, 'get_onboarding_status' ) ? $onboarding_instance->get_onboarding_status() : false,
649 + 'onboarding_redirect' => isset( $_GET['srfm-activation-redirect'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required for the activation redirection.
650 + 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ),
651 + 'srfm_ai_details' => AI_Helper::get_current_usage_details(),
1822 652 ];
1823 653
1824 - $is_screen_sureforms_menu = Helper::validate_request_context( 'sureforms_menu', 'page' );
1825 - $is_screen_add_new_form = Helper::validate_request_context( 'add-new-form', 'page' );
1826 - $is_screen_sureforms_forms = Helper::validate_request_context( 'sureforms_forms', 'page' );
1827 - $is_screen_sureforms_form_settings = Helper::validate_request_context( 'sureforms_form_settings', 'page' );
1828 - $is_screen_sureforms_payments = Helper::validate_request_context( 'sureforms_payments', 'page' );
1829 - $is_screen_sureforms_entries = Helper::validate_request_context( SRFM_ENTRIES, 'page' );
1830 - $is_screen_sureforms_learn = Helper::validate_request_context( 'sureforms_learn', 'page' );
1831 - $is_screen_quiz_empty_state = Helper::validate_request_context( 'sureforms_quiz_entries', 'page' );
1832 - $is_screen_survey_empty_state = Helper::validate_request_context( 'sureforms_survey_reports', 'page' );
1833 - $is_screen_partial_entries_empty_state = Helper::validate_request_context( 'sureforms_partial_entries', 'page' );
1834 - $is_post_type_sureforms_form = SRFM_FORMS_POST_TYPE === $current_screen->post_type;
654 + $is_screen_sureforms_menu = Helper::validate_request_context( 'sureforms_menu', 'page' );
655 + $is_screen_add_new_form = Helper::validate_request_context( 'add-new-form', 'page' );
656 + $is_screen_sureforms_form_settings = Helper::validate_request_context( 'sureforms_form_settings', 'page' );
657 + $is_screen_sureforms_entries = Helper::validate_request_context( SRFM_ENTRIES, 'page' );
658 + $is_post_type_sureforms_form = SRFM_FORMS_POST_TYPE === $current_screen->post_type;
1835 659
1836 - /**
1837 - * Check if the current screen is the SureForms Menu and AI Auth Email is present then we will add user type as registered.
1838 - * Compatibility with existing UI code that checks for this condition.
1839 - */
1840 - if ( $is_screen_sureforms_menu ) {
1841 - // If email is stored send the user type as registered else non-registered.
1842 - $localization_data['srfm_ai_details'] = [
1843 - 'type' => ! empty( get_option( 'srfm_ai_auth_user_email' ) ) ? 'registered' : 'non-registered',
1844 - ];
1845 - }
1846 -
1847 - // Add the Quizzes and Survey Reports nav items when pro is not active.
1848 - if ( ! Helper::has_pro() ) {
1849 - $localization_data['additional_header_nav_items'][] = [
1850 - 'slug' => 'sureforms_quiz_entries',
1851 - 'text' => __( 'Quizzes', 'sureforms' ),
1852 - 'link' => admin_url( 'admin.php?page=sureforms_quiz_entries' ),
1853 - ];
1854 - $localization_data['additional_header_nav_items'][] = [
1855 - 'slug' => 'sureforms_survey_reports',
1856 - 'text' => __( 'Survey Reports', 'sureforms' ),
1857 - 'link' => admin_url( 'admin.php?page=sureforms_survey_reports' ),
1858 - ];
1859 - $localization_data['additional_header_nav_items'][] = [
1860 - 'slug' => 'sureforms_partial_entries',
1861 - 'text' => __( 'Partial Entries', 'sureforms' ),
1862 - 'link' => admin_url( 'admin.php?page=sureforms_partial_entries' ),
1863 - ];
1864 - }
1865 -
1866 - $is_sureforms_screen = $is_screen_sureforms_menu || $is_post_type_sureforms_form || $is_screen_add_new_form || $is_screen_sureforms_forms || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_screen_sureforms_payments || $is_screen_sureforms_learn || $is_screen_quiz_empty_state || $is_screen_survey_empty_state || $is_screen_partial_entries_empty_state;
1867 -
1868 - /**
1869 - * Filter to allow extending the SureForms dashboard screen check.
1870 - *
1871 - * @since 2.6.0
1872 - *
1873 - * @param bool $is_sureforms_screen Whether the current screen is a SureForms dashboard screen.
1874 - */
1875 - $is_sureforms_screen = apply_filters( 'srfm_is_dashboard_screen', $is_sureforms_screen );
1876 -
1877 - if ( $is_sureforms_screen ) {
660 + if ( $is_screen_sureforms_menu || $is_post_type_sureforms_form || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries ) {
1878 661 $asset_handle = '-dashboard';
1879 662
1880 663 wp_enqueue_style( SRFM_SLUG . $asset_handle . '-font', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap', [], SRFM_VER );
1881 664
@@ -1903,9 +686,9 @@
1903 686 update_option( 'srfm_pro_license_status', $current_license_status );
1904 687 }
1905 688 }
1906 689
1907 - $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings&subpage=recaptcha' );
690 + $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings' );
1908 691 $localization_data['integration_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=integration-settings' );
1909 692 wp_localize_script(
1910 693 SRFM_SLUG . $asset_handle,
1911 694 SRFM_SLUG . '_admin',
@@ -1914,11 +697,12 @@
1914 697 $localization_data
1915 698 )
1916 699 );
1917 700 wp_enqueue_style( SRFM_SLUG . '-dashboard', SRFM_URL . 'assets/build/dashboard.css', [], SRFM_VER, 'all' );
701 +
1918 702 }
1919 703
1920 - if ( $is_screen_sureforms_form_settings || $is_screen_sureforms_forms ) {
704 + if ( $is_screen_sureforms_form_settings ) {
1921 705 wp_enqueue_style( SRFM_SLUG . '-settings', $css_uri . 'backend/settings' . $file_prefix . $rtl . '.css', [], SRFM_VER );
1922 706 }
1923 707
1924 708 // Enqueue styles for the entries page.
@@ -1925,61 +709,11 @@
1925 709 if ( $is_screen_sureforms_entries ) {
1926 710 $asset_handle = '-entries';
1927 711 wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/entries.js', $script_info['dependencies'], SRFM_VER, true );
1928 712
1929 - wp_localize_script(
1930 - SRFM_SLUG . $asset_handle,
1931 - SRFM_SLUG . '_admin',
1932 - apply_filters(
1933 - SRFM_SLUG . '_admin_filter',
1934 - $localization_data
1935 - )
1936 - );
1937 713 $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1938 714 }
1939 715
1940 - // Enqueue scripts for the learn page.
1941 - if ( $is_screen_sureforms_learn ) {
1942 - $asset_handle = '-learn';
1943 - wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/learn.js', $script_info['dependencies'], SRFM_VER, true );
1944 -
1945 - wp_localize_script(
1946 - SRFM_SLUG . $asset_handle,
1947 - SRFM_SLUG . '_admin',
1948 - apply_filters(
1949 - SRFM_SLUG . '_admin_filter',
1950 - $localization_data
1951 - )
1952 - );
1953 - $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1954 - }
1955 -
1956 - // Enqueue scripts for the forms page.
1957 - if ( $is_screen_sureforms_forms ) {
1958 - $asset_handle = '-forms';
1959 -
1960 - $script_asset_path = SRFM_DIR . 'assets/build/forms.asset.php';
1961 - $script_info = file_exists( $script_asset_path )
1962 - ? include $script_asset_path
1963 - : [
1964 - 'dependencies' => [],
1965 - 'version' => SRFM_VER,
1966 - ];
1967 -
1968 - wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.js', $script_info['dependencies'], SRFM_VER, true );
1969 - wp_localize_script(
1970 - SRFM_SLUG . $asset_handle,
1971 - SRFM_SLUG . '_admin',
1972 - apply_filters(
1973 - SRFM_SLUG . '_admin_filter',
1974 - $localization_data
1975 - )
1976 - );
1977 - wp_enqueue_style( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.css', [], SRFM_VER, 'all' );
1978 -
1979 - $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1980 - }
1981 -
1982 716 // Enqueue scripts for the SureMail promotional page.
1983 717 $is_screen_sureforms_smtp = Helper::validate_request_context( 'sureforms_smtp', 'page' );
1984 718 if ( $is_screen_sureforms_smtp ) {
1985 719 $asset_handle = 'suremail';
@@ -2018,65 +752,27 @@
2018 752
2019 753 $script_translations_handlers[] = SRFM_SLUG . '-suremail';
2020 754 }
2021 755
2022 - // Enqueue scripts for the Quiz empty state page (free users only).
2023 - if ( $is_screen_quiz_empty_state && ! Helper::has_pro() ) {
2024 - $asset_handle = 'quizEmptyState';
756 + // Admin Submenu Styles.
757 + wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . $rtl . '.css', [], SRFM_VER );
2025 758
2026 - $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
2027 - $script_info = file_exists( $script_asset_path )
2028 - ? include $script_asset_path
2029 - : [
2030 - 'dependencies' => [],
2031 - 'version' => SRFM_VER,
2032 - ];
759 + if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id ) {
760 + $asset_handle = 'page_header';
2033 761
2034 - wp_enqueue_script( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2035 - wp_enqueue_style( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
2036 -
2037 - $script_translations_handlers[] = SRFM_SLUG . '-quiz-empty-state';
2038 - }
2039 -
2040 - // Enqueue scripts for the Survey Reports empty state page (free users only).
2041 - if ( $is_screen_survey_empty_state && ! Helper::has_pro() ) {
2042 - $asset_handle = 'surveyEmptyState';
2043 -
2044 762 $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
2045 763 $script_info = file_exists( $script_asset_path )
2046 - ? include $script_asset_path
2047 - : [
2048 - 'dependencies' => [],
2049 - 'version' => SRFM_VER,
2050 - ];
764 + ? include $script_asset_path
765 + : [
766 + 'dependencies' => [],
767 + 'version' => SRFM_VER,
768 + ];
769 + wp_enqueue_script( SRFM_SLUG . '-form-page-header', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
770 + wp_enqueue_style( SRFM_SLUG . '-form-archive-styles', $css_uri . 'form-archive-styles' . $file_prefix . $rtl . '.css', [], SRFM_VER );
2051 771
2052 - wp_enqueue_script( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2053 - wp_enqueue_style( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
2054 -
2055 - $script_translations_handlers[] = SRFM_SLUG . '-survey-empty-state';
772 + $script_translations_handlers[] = SRFM_SLUG . '-form-page-header';
2056 773 }
2057 774
2058 - // Enqueue scripts for the Partial Entries empty state page (free users only).
2059 - if ( $is_screen_partial_entries_empty_state && ! Helper::has_pro() ) {
2060 - $asset_handle = 'partialEntriesEmptyState';
2061 -
2062 - $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
2063 - $script_info = file_exists( $script_asset_path )
2064 - ? include $script_asset_path
2065 - : [
2066 - 'dependencies' => [],
2067 - 'version' => SRFM_VER,
2068 - ];
2069 -
2070 - wp_enqueue_script( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2071 - wp_enqueue_style( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
2072 -
2073 - $script_translations_handlers[] = SRFM_SLUG . '-partial-entries-empty-state';
2074 - }
2075 -
2076 - // Admin Submenu Styles.
2077 - wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . $rtl . '.css', [], SRFM_VER );
2078 -
2079 775 if ( $is_screen_sureforms_form_settings ) {
2080 776 $asset_handle = 'settings';
2081 777
2082 778 $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
@@ -2090,20 +786,40 @@
2090 786 wp_enqueue_script( SRFM_SLUG . '-settings', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2091 787 wp_localize_script(
2092 788 SRFM_SLUG . '-settings',
2093 789 SRFM_SLUG . '_admin',
2094 - apply_filters(
2095 - SRFM_SLUG . '_admin_filter',
2096 - $localization_data
2097 - )
790 + $localization_data
2098 791 );
2099 792
2100 - // Enqueue Tailwind and Quill editor styles for the settings page.
2101 - wp_enqueue_style( SRFM_SLUG . '-settings-build', SRFM_URL . 'assets/build/settings.css', [], SRFM_VER, 'all' );
2102 - wp_enqueue_style( SRFM_SLUG . '-reactQuill', SRFM_URL . 'assets/css/minified/deps/quill/quill.snow.css', [], SRFM_VER );
2103 - wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS );
793 + $script_translations_handlers[] = SRFM_SLUG . '-settings';
794 + }
795 + if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id ) {
796 + wp_enqueue_script( SRFM_SLUG . '-form-archive', $js_uri . 'form-archive' . $file_prefix . '.js', [], SRFM_VER, true );
797 + wp_enqueue_script( SRFM_SLUG . '-export', $js_uri . 'export' . $file_prefix . '.js', [ 'wp-i18n' ], SRFM_VER, true );
798 + wp_localize_script(
799 + SRFM_SLUG . '-export',
800 + SRFM_SLUG . '_export',
801 + [
802 + 'ajaxurl' => admin_url( 'admin-ajax.php' ),
803 + 'srfm_export_nonce' => wp_create_nonce( 'export_form_nonce' ),
804 + 'site_url' => get_site_url(),
805 + 'import_form_nonce' => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'wp_rest' ) : '',
806 + 'import_btn_string' => __( 'Import Form', 'sureforms' ),
807 + ]
808 + );
2104 809
2105 - $script_translations_handlers[] = SRFM_SLUG . '-settings';
810 + wp_enqueue_script( SRFM_SLUG . '-backend', $js_uri . 'backend' . $file_prefix . '.js', [], SRFM_VER, true );
811 + wp_localize_script(
812 + SRFM_SLUG . '-backend',
813 + SRFM_SLUG . '_backend',
814 + [
815 + 'site_url' => get_site_url(),
816 + ]
817 + );
818 +
819 + $script_translations_handlers[] = SRFM_SLUG . '-form-archive';
820 + $script_translations_handlers[] = SRFM_SLUG . '-export';
821 + $script_translations_handlers[] = SRFM_SLUG . '-backend';
2106 822 }
2107 823
2108 824 if ( $is_screen_add_new_form ) {
2109 825 wp_enqueue_style( SRFM_SLUG . '-template-picker', $css_uri . 'template-picker' . $file_prefix . $rtl . '.css', [], SRFM_VER );
@@ -2124,12 +840,13 @@
2124 840 SRFM_SLUG . '_admin',
2125 841 [
2126 842 'site_url' => get_site_url(),
2127 843 'plugin_url' => SRFM_URL,
844 + 'preview_images_url' => SRFM_URL . 'images/template-previews/',
2128 845 'admin_url' => admin_url( 'admin.php' ),
2129 846 'new_template_picker_base_url' => admin_url( 'post-new.php?post_type=sureforms_form' ),
2130 - 'capability' => Helper::current_user_can(),
2131 - 'template_picker_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '',
847 + 'capability' => current_user_can( 'edit_posts' ),
848 + 'template_picker_nonce' => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'wp_rest' ) : '',
2132 849 'is_pro_active' => Helper::has_pro(),
2133 850 'srfm_ai_usage_details' => AI_Helper::get_current_usage_details(),
2134 851 'is_pro_license_active' => AI_Helper::is_pro_license_active(),
2135 852 'srfm_ai_auth_user_email' => get_option( 'srfm_ai_auth_user_email' ),
@@ -2150,9 +867,8 @@
2150 867 'srfm/checkbox',
2151 868 'srfm/number',
2152 869 'srfm/inline-button',
2153 870 'srfm/advanced-heading',
2154 - 'srfm/payment',
2155 871 ]
2156 872 );
2157 873 if ( ! is_array( $default_allowed_quick_sidebar_blocks ) ) {
2158 874 $default_allowed_quick_sidebar_blocks = [];
@@ -2164,25 +880,22 @@
2164 880 }
2165 881 $quick_sidebar_allowed_blocks = get_option( 'srfm_quick_sidebar_allowed_blocks' );
2166 882 $quick_sidebar_allowed_blocks = ! empty( $quick_sidebar_allowed_blocks ) && is_array( $quick_sidebar_allowed_blocks ) ? $quick_sidebar_allowed_blocks : $default_allowed_quick_sidebar_blocks;
2167 883 $srfm_ajax_nonce = wp_create_nonce( 'srfm_ajax_nonce' );
884 + wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true );
885 + wp_localize_script(
886 + SRFM_SLUG . '-quick-action-siderbar',
887 + SRFM_SLUG . '_quick_sidebar_blocks',
888 + [
889 + 'allowed_blocks' => $quick_sidebar_allowed_blocks,
890 + 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar,
891 + 'srfm_ajax_nonce' => $srfm_ajax_nonce,
892 + 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ),
893 + ]
894 + );
2168 895
2169 - if ( Helper::is_sureforms_admin_page() ) {
2170 - wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true );
2171 - wp_localize_script(
2172 - SRFM_SLUG . '-quick-action-siderbar',
2173 - SRFM_SLUG . '_quick_sidebar_blocks',
2174 - [
2175 - 'allowed_blocks' => $quick_sidebar_allowed_blocks,
2176 - 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar,
2177 - 'srfm_ajax_nonce' => $srfm_ajax_nonce,
2178 - 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ),
2179 - ]
2180 - );
896 + $script_translations_handlers[] = SRFM_SLUG . '-quick-action-siderbar';
2181 897
2182 - $script_translations_handlers[] = SRFM_SLUG . '-quick-action-siderbar';
2183 - }
2184 -
2185 898 /**
2186 899 * Enqueuing SureTriggers Integration script.
2187 900 * This script loads suretriggers iframe in Intergations tab.
2188 901 */
@@ -2254,282 +967,43 @@
2254 967
2255 968 return $status;
2256 969 }
2257 970
2258 - /**
2259 - * Register Pro compatibility notices early for React pages.
2260 - *
2261 - * This method runs on admin_init (priority 5) to ensure notices are
2262 - * registered BEFORE admin_enqueue_scripts, so they're available when
2263 - * wp_localize_script runs.
2264 - *
2265 - * Hooked - admin_init (priority 5)
2266 - *
2267 - * @return void
2268 - * @since 2.5.0
2269 - */
2270 - public function register_pro_compatibility_notices() {
2271 - // Early exit if Pro is not active, user lacks permissions, or Notice_Manager is unavailable.
2272 - if ( ! Helper::has_pro() || ! Helper::current_user_can() || ! class_exists( 'SRFM\Admin\Notice_Manager' ) ) {
2273 - return;
2274 - }
971 + // Entries methods.
2275 972
2276 - // Register version outdated notice for React pages.
2277 - if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) {
2278 - $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro';
2279 - $react_outdated_message = sprintf(
2280 - // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version.
2281 - esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version.', 'sureforms' ),
2282 - esc_html( SRFM_VER ),
2283 - esc_html( $pro_plugin_name ),
2284 - esc_html( SRFM_PRO_RECOMMENDED_VER )
2285 - );
2286 -
2287 - \SRFM\Admin\Notice_Manager::register_notice(
2288 - [
2289 - 'id' => 'sureforms-pro-version-outdated',
2290 - 'variant' => 'warning',
2291 - 'message' => $react_outdated_message,
2292 - 'actions' => [
2293 - [
2294 - 'label' => esc_html__( 'Update Now', 'sureforms' ),
2295 - 'url' => admin_url( 'update-core.php' ),
2296 - 'variant' => 'primary',
2297 - ],
2298 - ],
2299 - 'pages' => [ 'all' ],
2300 - ]
2301 - );
2302 - }
2303 - }
2304 -
2305 973 /**
2306 - * Register the React notice when the entries table is missing.
974 + * Handle entry actions.
2307 975 *
2308 - * Hooked - admin_init, priority 5.
2309 - *
2310 - * Priority 5 is load-bearing: Notice_Manager hands notices to the front end
2311 - * through the `srfm_admin_filter` applied during admin_enqueue_scripts, so
2312 - * anything registering later never reaches the page.
2313 - *
2314 - * @since 2.12.6
976 + * @since 0.0.13
2315 977 * @return void
2316 978 */
2317 - public function register_database_repair_notice() {
2318 - // admin_init also fires on admin-ajax.php. Nothing there renders a notice, so
2319 - // skip the work rather than reading a transient on every AJAX request.
2320 - if ( wp_doing_ajax() ) {
2321 - return;
2322 - }
979 + public function handle_entry_actions() {
980 + Entries_List_Table::process_bulk_actions();
2323 981
2324 - if ( ! Helper::current_user_can() ) {
982 + if ( ! isset( $_GET['page'] ) || SRFM_ENTRIES !== $_GET['page'] ) {
2325 983 return;
2326 984 }
2327 -
2328 - if ( ! class_exists( 'SRFM\Admin\Notice_Manager' ) ) {
985 + if ( ! isset( $_GET['entry_id'] ) || ! isset( $_GET['action'] ) ) {
2329 986 return;
2330 987 }
2331 -
2332 - // A just-completed repair reports its outcome instead of the warning.
2333 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display flag; the repair itself is nonce-checked in handle_database_repair().
2334 - $result = isset( $_GET['srfm_db_repair'] ) ? sanitize_key( wp_unslash( $_GET['srfm_db_repair'] ) ) : '';
2335 -
2336 - if ( 'done' === $result ) {
2337 - Notice_Manager::register_notice(
2338 - [
2339 - 'id' => 'srfm-database-repaired',
2340 - 'variant' => 'success',
2341 - 'message' => __( 'Your SureForms database is up to date. New form entries will be saved as usual.', 'sureforms' ),
2342 - 'pages' => [ 'all' ],
2343 - ]
2344 - );
2345 - return;
988 + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'srfm_entries_action' ) ) {
989 + wp_die( esc_html__( 'Nonce verification failed.', 'sureforms' ) );
2346 990 }
2347 -
2348 - if ( 'failed' === $result ) {
2349 - Notice_Manager::register_notice(
2350 - [
2351 - 'id' => 'srfm-database-repair-failed',
2352 - // Still a warning, not an error: a host that does not allow
2353 - // SureForms to create tables is not the user's mistake.
2354 - 'variant' => 'warning',
2355 - 'message' => __( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ),
2356 - 'actions' => [
2357 - [
2358 - 'label' => __( 'Contact support', 'sureforms' ),
2359 - 'url' => 'https://sureforms.com/contact/',
2360 - ],
2361 - ],
2362 - 'pages' => [ 'all' ],
2363 - ]
2364 - );
2365 - return;
991 + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
992 + $entry_id = Helper::get_integer_value( sanitize_text_field( wp_unslash( $_GET['entry_id'] ) ) );
993 + $view = isset( $_GET['view'] ) ? sanitize_text_field( wp_unslash( $_GET['view'] ) ) : '';
994 + if ( $entry_id > 0 ) {
995 + if ( 'read' === $action && 'details' === $view ) {
996 + $entry_status = Entries::get( $entry_id )['status'];
997 + if ( 'trash' === $entry_status ) {
998 + wp_die( esc_html__( 'You cannot view this entry because it is in trash.', 'sureforms' ) );
999 + }
1000 + }
1001 + Entries_List_Table::handle_entry_status( $entry_id, $action, $view );
2366 1002 }
2367 -
2368 - if ( ! Register::is_entries_table_missing() ) {
2369 - return;
2370 - }
2371 -
2372 - $this->track_database_notice_impression();
2373 -
2374 - Notice_Manager::register_notice(
2375 - [
2376 - 'id' => 'srfm-database-maintenance',
2377 - 'variant' => 'warning',
2378 - 'title' => __( 'Database update needed', 'sureforms' ),
2379 - // Plain text only. AdminNotice.js renders this as a React child, so
2380 - // any markup here would show up as literal characters.
2381 - 'message' => $this->get_database_notice_message(),
2382 - 'actions' => [
2383 - [
2384 - 'label' => __( 'Fix now', 'sureforms' ),
2385 - // Opaque identifier, resolved to a handler in AdminNotice.js.
2386 - // Deliberately not a URL or endpoint: the server never tells
2387 - // the browser which address to call.
2388 - 'action' => 'repair-entries-table',
2389 - 'url' => $this->get_database_repair_url(),
2390 - ],
2391 - ],
2392 - 'pages' => [ 'all' ],
2393 - ]
2394 - );
2395 1003 }
2396 1004
2397 1005 /**
2398 - * Render the classic warning on the WordPress dashboard.
2399 - *
2400 - * Hooked - admin_notices.
2401 - *
2402 - * Scoped to index.php on purpose. The React notice already covers the SureForms
2403 - * screens, so leaving this one admin-wide would stack two warnings on the same
2404 - * page and nag on every screen in wp-admin.
2405 - *
2406 - * Registered as [ $this, 'method' ] rather than a closure because
2407 - * suppress_foreign_admin_notices() strips any callback it cannot attribute to a
2408 - * SureForms class — a closure here would be silently removed.
2409 - *
2410 - * @since 2.12.6
2411 - * @return void
2412 - */
2413 - public function render_database_repair_notice() {
2414 - if ( ! Helper::current_user_can() ) {
2415 - return;
2416 - }
2417 -
2418 - $screen = get_current_screen();
2419 -
2420 - if ( ! $screen || 'dashboard' !== $screen->base ) {
2421 - return;
2422 - }
2423 -
2424 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display flag; the repair itself is nonce-checked in handle_database_repair().
2425 - $result = isset( $_GET['srfm_db_repair'] ) ? sanitize_key( wp_unslash( $_GET['srfm_db_repair'] ) ) : '';
2426 -
2427 - if ( 'done' === $result ) {
2428 - ?>
2429 - <div class="notice notice-success is-dismissible">
2430 - <p><?php esc_html_e( 'Your SureForms database is up to date. New form entries will be saved as usual.', 'sureforms' ); ?></p>
2431 - </div>
2432 - <?php
2433 - return;
2434 - }
2435 -
2436 - if ( 'failed' === $result ) {
2437 - ?>
2438 - <div class="notice notice-warning is-dismissible">
2439 - <p><?php esc_html_e( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ); ?></p>
2440 - </div>
2441 - <?php
2442 - return;
2443 - }
2444 -
2445 - if ( ! Register::is_entries_table_missing() ) {
2446 - return;
2447 - }
2448 -
2449 - $this->track_database_notice_impression();
2450 - ?>
2451 - <div class="notice notice-warning">
2452 - <p>
2453 - <strong><?php esc_html_e( 'SureForms — database update needed', 'sureforms' ); ?></strong>
2454 - </p>
2455 - <p><?php echo esc_html( $this->get_database_notice_message() ); ?></p>
2456 - <p>
2457 - <a href="<?php echo esc_url( $this->get_database_repair_url() ); ?>" class="button button-primary">
2458 - <?php esc_html_e( 'Update database', 'sureforms' ); ?>
2459 - </a>
2460 - </p>
2461 - </div>
2462 - <?php
2463 - }
2464 -
2465 - /**
2466 - * Repair the entries table, then redirect back with the outcome.
2467 - *
2468 - * Hooked - admin_post_srfm_repair_entries_table.
2469 - *
2470 - * A nonce-protected GET that changes state matches how core's own plugin
2471 - * activate / deactivate / delete links work.
2472 - *
2473 - * @since 2.12.6
2474 - * @return void
2475 - */
2476 - public function handle_database_repair() {
2477 - if ( ! Helper::current_user_can() ) {
2478 - wp_die( esc_html__( 'You do not have permission to update the database.', 'sureforms' ), 403 );
2479 - }
2480 -
2481 - check_admin_referer( 'srfm_repair_entries_table' );
2482 -
2483 - $repaired = $this->do_database_repair();
2484 - $referer = wp_get_referer();
2485 -
2486 - wp_safe_redirect(
2487 - add_query_arg(
2488 - 'srfm_db_repair',
2489 - $repaired ? 'done' : 'failed',
2490 - $referer ? $referer : admin_url()
2491 - )
2492 - );
2493 - exit;
2494 - }
2495 -
2496 - /**
2497 - * Repair the entries table and record what happened.
2498 - *
2499 - * The single place the repair is performed and counted, shared by the
2500 - * admin-post handler and the REST endpoint. One user action reaches exactly one
2501 - * of those, so the click counter cannot double-count across the two surfaces.
2502 - *
2503 - * @since 2.12.6
2504 - * @return bool True when the table exists afterwards.
2505 - */
2506 - public function do_database_repair() {
2507 - // Cumulative counter, so $force = true: each new count is a new value and is
2508 - // re-sent, while an identical repeat short-circuits inside track().
2509 - $attempts = Helper::get_integer_value( Helper::get_srfm_option( 'db_repair_attempts', 0 ) ) + 1;
2510 - Helper::update_srfm_option( 'db_repair_attempts', $attempts );
2511 -
2512 - // Event name is the `database_error` => `fix_now` entry in the $valid
2513 - // allowlist in handle_notice_response(). Kept in sync by hand; that array is
2514 - // where the team looks notice event names up.
2515 - Analytics::events()->track( 'database_error_notice_cta', (string) $attempts, [], true );
2516 -
2517 - $repaired = Register::repair_entries_table();
2518 -
2519 - // The failure case is the more valuable signal: it means the host refuses to
2520 - // let SureForms create tables, which no amount of retrying will fix.
2521 - Analytics::events()->track(
2522 - 'database_repair_result',
2523 - $repaired ? 'success' : 'failed',
2524 - [],
2525 - true
2526 - );
2527 -
2528 - return $repaired;
2529 - }
2530 -
2531 - /**
2532 1006 * Admin Notice Callback if sureforms pro is out of date.
2533 1007 *
2534 1008 * Hooked - admin_notices
2535 1009 *
@@ -2544,9 +1018,9 @@
2544 1018 if ( empty( get_current_screen() ) ) {
2545 1019 return;
2546 1020 }
2547 1021
2548 - if ( ! Helper::current_user_can() ) {
1022 + if ( ! current_user_can( 'update_plugins' ) ) {
2549 1023 return;
2550 1024 }
2551 1025
2552 1026 $srfm_pro_license_status = get_option( 'srfm_pro_license_status', '' );
@@ -2562,311 +1036,124 @@
2562 1036 $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro';
2563 1037 $message = '';
2564 1038 $url = admin_url( 'admin.php?page=sureforms_form_settings&tab=account-settings' );
2565 1039 if ( 'unlicensed' === $srfm_pro_license_status ) {
2566 - ob_start();
2567 - ?>
2568 - <p>
2569 - <?php
2570 - printf(
2571 - // translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
2572 - esc_html__( 'Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more.', 'sureforms' ),
2573 - '<a href="' . esc_url( $url ) . '">',
2574 - '</a>',
2575 - '<i>' . esc_html( $pro_plugin_name ) . '</i>'
2576 - );
2577 - ?>
2578 - </p>
2579 - <?php
2580 - $message = ob_get_clean();
1040 + $message = '<p>' . sprintf(
1041 + // translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
1042 + esc_html__( 'Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more.', 'sureforms' ),
1043 + '<a href="' . esc_url( $url ) . '">',
1044 + '</a>',
1045 + '<i>' . esc_html( $pro_plugin_name ) . '</i>'
1046 + ) . '</p>';
2581 1047 }
2582 1048
2583 1049 if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) {
2584 - ob_start();
2585 - ?>
2586 - <p>
2587 - <?php
2588 - printf(
2589 - // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
2590 - esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s.', 'sureforms' ),
2591 - esc_html( SRFM_VER ),
2592 - esc_html( $pro_plugin_name ),
2593 - esc_html( SRFM_PRO_RECOMMENDED_VER ),
2594 - '<a href="' . esc_url( admin_url( 'update-core.php' ) ) . '">',
2595 - '</a>'
2596 - );
2597 - ?>
2598 - </p>
2599 - <?php
2600 - $message .= ob_get_clean();
1050 + $message .= '<p>' . sprintf(
1051 + // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
1052 + esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s.', 'sureforms' ),
1053 + esc_html( SRFM_VER ),
1054 + esc_html( $pro_plugin_name ),
1055 + esc_html( SRFM_PRO_RECOMMENDED_VER ),
1056 + '<a href=' . esc_url( admin_url( 'update-core.php' ) ) . '>',
1057 + '</a>'
1058 + ) . '</p>';
2601 1059 }
2602 1060
2603 1061 if ( ! empty( $message ) ) {
2604 1062 // Phpcs ignore comment is required as $message variable is already escaped.
2605 - ?>
2606 - <div class="notice notice-warning"><?php echo wp_kses_post( $message ); ?></div>
2607 - <?php
1063 + echo '<div class="notice notice-warning">' . wp_kses_post( $message ) . '</div>';
2608 1064 }
2609 1065 }
2610 1066
2611 1067 /**
2612 - * Display a notice to the user about providing a review.
1068 + * Add SMTP warning admin notice if no SMTP plugin is active
2613 1069 *
2614 - * @since 2.5.2
2615 - * @return void
2616 - */
2617 - public function display_srfm_rating_notice() {
2618 - // Only show to admins.
2619 - if ( ! Helper::current_user_can() ) {
2620 - return;
2621 - }
2622 -
2623 - // Allow the notice to be disabled.
2624 - if ( ! apply_filters( 'srfm_show_rating_notice', true ) ) {
2625 - return;
2626 - }
2627 -
2628 - $notice_id = 'srfm-plugin-review-notice';
2629 -
2630 - Astra_Notices::add_notice(
2631 - [
2632 - 'id' => $notice_id,
2633 - 'type' => '',
2634 - 'message' => self::build_srfm_notice_markup(
2635 - __( 'Amazing! SureForms is powering your forms and submissions - let\'s keep growing together!', 'sureforms' ),
2636 - __( 'If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?', 'sureforms' ),
2637 - [
2638 - [
2639 - 'text' => __( 'Rate SureForms', 'sureforms' ),
2640 - 'url' => esc_url( 'https://wordpress.org/support/plugin/sureforms/reviews/' ),
2641 - 'primary' => true,
2642 - // Leaves wp-admin, so it also dismisses on the way out.
2643 - 'dismiss' => true,
2644 - 'external' => true,
2645 - ],
2646 - [
2647 - 'text' => __( 'Maybe later', 'sureforms' ),
2648 - 'url' => '#',
2649 - 'dismiss' => true,
2650 - 'snooze' => WEEK_IN_SECONDS,
2651 - ],
2652 - [
2653 - 'text' => __( 'I already did', 'sureforms' ),
2654 - 'url' => '#',
2655 - 'dismiss' => true,
2656 - ],
2657 - ]
2658 - ),
2659 - 'class' => 'srfm-notice srfm-rating-notice',
2660 - 'repeat-notice-after' => WEEK_IN_SECONDS,
2661 - // Yields to the Thank You prompt for the same reason the Getting Started
2662 - // notice does: a specific form to finish beats a recurring review ask,
2663 - // and a user with three forms who then imports a template would
2664 - // otherwise see both at once.
2665 - 'show_if' => $this->maybe_display_rating_notice() && null === $this->get_displayable_thankyou_prompt() && ! $this->has_action_item_warnings(),
2666 - 'display-with-other-notices' => true,
2667 - ]
2668 - );
2669 -
2670 - add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] );
2671 - add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_notice_response_script' ] );
2672 - }
2673 -
2674 - /**
2675 - * Display a "Getting Started" admin notice for new users who haven't yet
2676 - * reached the rating-notice milestone (3+ forms or 3+ entries).
1070 + * Hooked - admin_notices
2677 1071 *
2678 - * The Astra Notices library handles the 7-day delay via the
2679 - * `display-notice-after` parameter.
2680 - *
2681 - * @since 2.5.2
2682 1072 * @return void
1073 + * @since 1.9.1
2683 1074 */
2684 - public function display_srfm_getting_started_notice() {
2685 - // Only show to admins.
2686 - if ( ! Helper::current_user_can() ) {
1075 + public function add_smtp_warning_notice() {
1076 + if ( ! current_user_can( 'manage_options' ) ) {
2687 1077 return;
2688 1078 }
2689 -
2690 - // Allow the notice to be disabled programmatically.
2691 - if ( ! apply_filters( 'srfm_show_getting_started_notice', true ) ) {
1079 + // Only show on dashboard or SureForms admin pages.
1080 + $screen = get_current_screen();
1081 + if ( ! $screen || ( false === strpos( $screen->id, 'sureforms' ) && 'dashboard' !== $screen->id ) ) {
2692 1082 return;
2693 1083 }
2694 -
2695 - $notice_id = 'srfm-getting-started-notice';
2696 -
2697 - Astra_Notices::add_notice(
2698 - [
2699 - 'id' => $notice_id,
2700 - 'type' => '',
2701 - 'message' => self::build_srfm_notice_markup(
2702 - __( 'SureForms is ready to power your forms — explore what\'s possible!', 'sureforms' ),
2703 - __( 'Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard.', 'sureforms' ),
2704 - [
2705 - [
2706 - 'text' => __( 'Go to Dashboard', 'sureforms' ),
2707 - 'url' => esc_url( admin_url( 'admin.php?page=sureforms_menu' ) ),
2708 - 'primary' => true,
2709 - ],
2710 - [
2711 - 'text' => __( 'Maybe later', 'sureforms' ),
2712 - 'url' => '#',
2713 - 'dismiss' => true,
2714 - 'snooze' => WEEK_IN_SECONDS,
2715 - ],
2716 - [
2717 - 'text' => __( 'I already know', 'sureforms' ),
2718 - 'url' => '#',
2719 - 'dismiss' => true,
2720 - ],
2721 - ]
2722 - ),
2723 - 'class' => 'srfm-notice srfm-getting-started-notice',
2724 - 'repeat-notice-after' => WEEK_IN_SECONDS,
2725 - // Yields to both of the other SureForms notices, so only one of ours is
2726 - // ever on screen. The rating notice supersedes it once the user has real
2727 - // usage; the Thank You prompt supersedes it because "finish this specific
2728 - // form" is a concrete next step and this is a generic tour invitation.
2729 - 'show_if' => ! $this->maybe_display_rating_notice() && null === $this->get_displayable_thankyou_prompt() && ! $this->has_action_item_warnings(),
2730 - 'display-notice-after' => WEEK_IN_SECONDS,
2731 - 'display-with-other-notices' => true,
2732 - ]
2733 - );
2734 -
2735 - // Same pre-markup hook the Thank You prompt uses, so both notices are painted
2736 - // by one stylesheet instead of two that drift apart.
2737 - add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] );
2738 - add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_notice_response_script' ] );
2739 - }
2740 -
2741 - /**
2742 - * Enqueue the notice response analytics script.
2743 - *
2744 - * Called via the astra_notice_after_markup_{id} hook so the script
2745 - * only loads when a SureForms notice is actually rendered.
2746 - *
2747 - * @since 2.5.2
2748 - * @return void
2749 - */
2750 - public function enqueue_notice_response_script() {
2751 - if ( wp_script_is( 'srfm-notice-response', 'enqueued' ) ) {
1084 + // Dismiss logic.
1085 + if ( isset( $_GET['srfm_dismiss_smtp_notice'] ) && '1' === $_GET['srfm_dismiss_smtp_notice'] ) {
1086 + // Verify nonce for security.
1087 + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'srfm_dismiss_smtp_notice' ) ) {
1088 + return;
1089 + }
1090 + update_user_meta( get_current_user_id(), 'srfm_dismiss_smtp_notice', 1 );
2752 1091 return;
2753 1092 }
2754 -
2755 - wp_enqueue_script(
2756 - 'srfm-notice-response',
2757 - SRFM_URL . 'admin/assets/js/notice-response.js',
2758 - [],
2759 - SRFM_VER,
2760 - true
2761 - );
2762 -
2763 - wp_localize_script(
2764 - 'srfm-notice-response',
2765 - 'srfmNoticeResponse',
2766 - [
2767 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
2768 - 'nonce' => wp_create_nonce( 'srfm_notice_response' ),
2769 - ]
2770 - );
2771 - }
2772 -
2773 - /**
2774 - * Handle the notice response AJAX request.
2775 - *
2776 - * Validates the request and records the analytics event
2777 - * for the notice button that was clicked.
2778 - *
2779 - * @since 2.5.2
2780 - * @return void
2781 - */
2782 - public function handle_notice_response() {
2783 - if ( ! check_ajax_referer( 'srfm_notice_response', 'nonce', false ) ) {
2784 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
1093 + if ( get_user_meta( get_current_user_id(), 'srfm_dismiss_smtp_notice', true ) ) {
2785 1094 return;
2786 1095 }
2787 -
2788 - if ( ! Helper::current_user_can() ) {
2789 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
1096 + // Determine SureMail plugin status and link.
1097 + $plugin_file = 'suremails/suremails.php';
1098 + if ( file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' ) && is_plugin_active( $plugin_file ) ) {
1099 + // SureMail is active, do not show the notice.
2790 1100 return;
2791 1101 }
1102 + if ( ! Helper::is_any_smtp_plugin_active() ) {
1103 + if ( file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' ) ) {
1104 + if ( is_plugin_active( $plugin_file ) ) {
1105 + $suremail_url = admin_url( 'options-general.php?page=suremail#/dashboard' );
1106 + } else {
1107 + $suremail_url = wp_nonce_url( admin_url( 'plugins.php?action=activate&plugin=' . $plugin_file ), 'activate-plugin_' . $plugin_file );
1108 + }
1109 + } else {
1110 + $suremail_url = admin_url( 'plugin-install.php?s=suremail&tab=search&type=term' );
1111 + }
1112 + $dismiss_url = wp_nonce_url( add_query_arg( 'srfm_dismiss_smtp_notice', '1' ), 'srfm_dismiss_smtp_notice' );
1113 + printf(
1114 + '<div class="notice notice-warning is-dismissible srfm-smtp-warning" data-dismiss-url="%1$s"><p>%2$s</p></div>',
1115 + esc_url( $dismiss_url ),
1116 + sprintf(
1117 + /* translators: 1: line break, 2: SureMail link opening tag, 3: SureMail link closing tag */
1118 + esc_html__( 'It looks like there\'s no SMTP plugin running on your site. That means emails sent from SureForms might not go through.%1$sYou can use %2$sSureMail%3$s to get email delivery working.', 'sureforms' ),
1119 + '<br />',
1120 + '<a href="' . esc_url( $suremail_url ) . '" target="_blank">',
1121 + '</a>'
1122 + )
1123 + );
1124 + ?>
1125 + <script type="text/javascript">
1126 + document.addEventListener('DOMContentLoaded', function() {
1127 + // Handle SMTP notice dismissal using event delegation
1128 + // This works even if the dismiss button is added dynamically by WordPress
1129 + document.addEventListener('click', function(e) {
1130 + // Check if clicked element is a dismiss button within our SMTP notice
1131 + if (e.target.classList.contains('notice-dismiss') && e.target.closest('.srfm-smtp-warning')) {
1132 + const smtpNotice = e.target.closest('.srfm-smtp-warning');
1133 + const dismissUrl = smtpNotice.dataset.dismissUrl;
2792 1134
2793 - $notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ) : '';
2794 - $button = isset( $_POST['button'] ) ? sanitize_text_field( wp_unslash( $_POST['button'] ) ) : '';
2795 -
2796 - $valid = [
2797 - 'srfm-getting-started-notice' => [
2798 - 'go_to_dashboard' => 'getting_started_notice_cta',
2799 - 'maybe_later' => 'getting_started_notice_snooze',
2800 - 'dismissed' => 'getting_started_notice_dismiss',
2801 - ],
2802 - 'srfm-plugin-review-notice' => [
2803 - 'rate_sureforms' => 'rating_notice_cta',
2804 - 'maybe_later' => 'rating_notice_snooze',
2805 - 'dismissed' => 'rating_notice_dismiss',
2806 - ],
2807 - // Database maintenance notice. Keyed `database_error` for the warehouse;
2808 - // the user-facing copy deliberately reads as a routine update, not an
2809 - // error. `dismissed` is registered but unreachable today — a missing
2810 - // entries table is not something we let people dismiss.
2811 - 'database_error' => [
2812 - 'fix_now' => 'database_error_notice_cta',
2813 - 'dismissed' => 'database_error_notice_dismiss',
2814 - ],
2815 - // The "Finish setting up" prompt (#3030): three CTAs, plus the ✕.
2816 - 'form_submission_error' => [
2817 - 'contact_support' => 'submission_failure_notice_cta',
2818 - 'dismissed' => 'submission_failure_notice_dismiss',
2819 - ],
2820 - 'notification_error' => [
2821 - 'contact_support' => 'notification_failure_notice_cta',
2822 - 'dismissed' => 'notification_failure_notice_dismiss',
2823 - ],
2824 - 'integration_error' => [
2825 - 'contact_support' => 'integration_failure_notice_cta',
2826 - 'dismissed' => 'integration_failure_notice_dismiss',
2827 - ],
2828 - 'caching_plugin' => [
2829 - 'help_me_fix' => 'caching_plugin_notice_cta',
2830 - 'dismissed' => 'caching_plugin_notice_dismiss',
2831 - ],
2832 - 'srfm-thankyou-prompt' => [
2833 - 'edit_form' => 'thankyou_notice_edit_form',
2834 - 'set_replies' => 'thankyou_notice_set_replies',
2835 - 'edit_thankyou' => 'thankyou_notice_edit_thankyou',
2836 - 'dismissed' => 'thankyou_notice_dismiss',
2837 - ],
2838 - ];
2839 -
2840 - if ( ! isset( $valid[ $notice_id ][ $button ] ) ) {
2841 - wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 );
2842 - // wp_send_json_error() ends the request in production. The explicit return
2843 - // keeps the guard a guard rather than something that only works because of
2844 - // a side effect in a function elsewhere.
2845 - return;
1135 + if (dismissUrl) {
1136 + // Security: Validate URL is safe before redirecting
1137 + try {
1138 + const url = new URL(dismissUrl, window.location.origin);
1139 + // Only allow same-origin URLs for security
1140 + if (url.origin === window.location.origin && url.protocol === window.location.protocol) {
1141 + window.location.href = url.href;
1142 + }
1143 + } catch (error) {
1144 + // Invalid URL - ignore the redirect for security
1145 + console.warn('Invalid dismiss URL detected:', dismissUrl);
1146 + }
1147 + e.preventDefault();
1148 + e.stopPropagation();
1149 + }
1150 + }
1151 + });
1152 + });
1153 + </script>
1154 + <?php
2846 1155 }
2847 -
2848 - $event_name = $valid[ $notice_id ][ $button ];
2849 - Analytics::events()->track( $event_name, $button );
2850 -
2851 - // Reporting the failures retires the notice until something new fails.
2852 - // Handled here rather than in the browser so it holds for the classic
2853 - // wp-admin notice too, which is a plain link with no JavaScript.
2854 - $categories = [
2855 - 'form_submission_error' => 'submission',
2856 - 'notification_error' => 'notification',
2857 - 'integration_error' => 'integration',
2858 - ];
2859 -
2860 - if ( 'contact_support' === $button && isset( $categories[ $notice_id ] ) ) {
2861 - Client_Logger::acknowledge_category( $categories[ $notice_id ] );
2862 -
2863 - if ( 'form_submission_error' === $notice_id ) {
2864 - Client_Logger::acknowledge_failures();
2865 - }
2866 - }
2867 -
2868 - wp_send_json_success();
2869 1156 }
2870 1157
2871 1158 /**
2872 1159 * Disables the capabilities for WPForms to avoid conflicts when enqueueing
@@ -2928,9 +1215,9 @@
2928 1215 * @since 1.8.0
2929 1216 */
2930 1217 public function pointer_should_show() {
2931 1218 // Security: Check user capability.
2932 - if ( ! Helper::current_user_can() ) {
1219 + if ( ! current_user_can( 'manage_options' ) ) {
2933 1220 wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2934 1221 }
2935 1222 // Security: Nonce check.
2936 1223 if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
@@ -2966,9 +1253,9 @@
2966 1253 * @since 1.8.0
2967 1254 */
2968 1255 public function pointer_dismissed() {
2969 1256 // Security: Check user capability.
2970 - if ( ! Helper::current_user_can() ) {
1257 + if ( ! current_user_can( 'manage_options' ) ) {
2971 1258 wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2972 1259 }
2973 1260 // Security: Nonce check.
2974 1261 if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
@@ -2987,9 +1274,9 @@
2987 1274 * @since 1.8.0
2988 1275 */
2989 1276 public function pointer_accepted_cta() {
2990 1277 // Security: Check user capability.
2991 - if ( ! Helper::current_user_can() ) {
1278 + if ( ! current_user_can( 'manage_options' ) ) {
2992 1279 wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2993 1280 }
2994 1281 // Security: Nonce check.
2995 1282 if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
@@ -3007,17 +1294,8 @@
3007 1294 * @return void
3008 1295 * @since 1.9.1
3009 1296 */
3010 1297 public function maybe_register_dashboard_widget() {
3011 -
3012 - // Only for users with manage_options capability.
3013 - if ( ! Helper::current_user_can() ) {
3014 - return;
3015 - }
3016 -
3017 - // Register the AI quick draft widget for capable users (the capability gate above applies); unlike the recent-entries widget below, it is not conditional on having entries.
3018 - add_action( 'wp_dashboard_setup', [ $this, 'register_ai_dashboard_widget' ] );
3019 -
3020 1298 // Quick check if there are any entries in the last 7 days.
3021 1299 $seven_days_ago = strtotime( '-7 days' );
3022 1300 $total_entries = Entries::get_entries_count_after( $seven_days_ago );
3023 1301
@@ -3052,290 +1330,8 @@
3052 1330 );
3053 1331 }
3054 1332
3055 1333 /**
3056 - * Register the AI quick draft dashboard widget.
3057 - *
3058 - * @return void
3059 - * @since 2.12.1
3060 - */
3061 - public function register_ai_dashboard_widget() {
3062 - wp_add_dashboard_widget(
3063 - 'sureforms_ai_quick_draft',
3064 - __( 'SureForms AI Quick Draft', 'sureforms' ),
3065 - [ $this, 'render_ai_dashboard_widget' ],
3066 - null,
3067 - null,
3068 - 'normal',
3069 - 'high'
3070 - );
3071 - }
3072 -
3073 - /**
3074 - * Render AI quick draft dashboard widget content.
3075 - *
3076 - * @return void
3077 - * @since 2.12.1
3078 - */
3079 - public function render_ai_dashboard_widget() {
3080 - ?>
3081 - <div class="srfm-ai-dashboard-widget">
3082 - <p>
3083 - <?php esc_html_e( 'Describe the form and let SureForms AI generate it for you.', 'sureforms' ); ?>
3084 - </p>
3085 - <label for="srfm-ai-dashboard-prompt" class="screen-reader-text">
3086 - <?php esc_html_e( 'Describe your form', 'sureforms' ); ?>
3087 - </label>
3088 - <textarea
3089 - id="srfm-ai-dashboard-prompt"
3090 - class="widefat"
3091 - rows="5"
3092 - maxlength="2000"
3093 - placeholder="<?php esc_attr_e( 'Example: Create a contact form with name, email, phone, and message fields.', 'sureforms' ); ?>"
3094 - ></textarea>
3095 - <p style="margin-top:10px;margin-bottom:0;display:flex;align-items:center;gap:10px;">
3096 - <button type="button" class="button button-primary" id="srfm-ai-dashboard-generate" disabled>
3097 - <?php esc_html_e( 'Create New Form', 'sureforms' ); ?>
3098 - </button>
3099 - <span id="srfm-ai-dashboard-char-count" style="color:#646970;">0/2000</span>
3100 - </p>
3101 - </div>
3102 - <?php
3103 - }
3104 -
3105 - /**
3106 - * Enqueue the AI quick draft dashboard widget script on the dashboard screen.
3107 - *
3108 - * The widget's behavior lives here (attached via wp_add_inline_script) rather than as an
3109 - * inline <script> in the render callback, so it passes Plugin Check and keeps server values
3110 - * out of the markup. Server values are passed through wp_localize_script.
3111 - *
3112 - * @param string $hook_suffix The current admin page hook suffix.
3113 - * @return void
3114 - * @since 2.12.1
3115 - */
3116 - public function enqueue_ai_dashboard_widget_assets( $hook_suffix ) {
3117 - // Only on the main dashboard, and only for capable users (matches the widget gate).
3118 - if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) {
3119 - return;
3120 - }
3121 -
3122 - // Register an inline-only handle (empty src) — the WordPress-core pattern for attaching
3123 - // localized data plus an inline script without shipping a separate asset file.
3124 - wp_register_script( 'srfm-ai-dashboard-widget', '', [], SRFM_VER, true );
3125 - wp_enqueue_script( 'srfm-ai-dashboard-widget' );
3126 -
3127 - wp_localize_script(
3128 - 'srfm-ai-dashboard-widget',
3129 - 'srfmAiDashboardWidget',
3130 - [
3131 - 'redirectUrl' => admin_url( 'admin.php?page=add-new-form' ),
3132 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
3133 - 'nonce' => wp_create_nonce( 'srfm_ai_widget_usage' ),
3134 - 'redirectingTxt' => __( 'Redirecting...', 'sureforms' ),
3135 - ]
3136 - );
3137 -
3138 - $inline_script = <<<'JS'
3139 -( function () {
3140 - const config = window.srfmAiDashboardWidget || {};
3141 - const generateButton = document.getElementById( 'srfm-ai-dashboard-generate' );
3142 - const promptField = document.getElementById( 'srfm-ai-dashboard-prompt' );
3143 - const charCount = document.getElementById( 'srfm-ai-dashboard-char-count' );
3144 - if ( ! generateButton || ! promptField ) {
3145 - return;
3146 - }
3147 -
3148 - const updateWidgetState = function () {
3149 - const promptValue = promptField.value.trim();
3150 - generateButton.disabled = ! promptValue;
3151 - if ( charCount ) {
3152 - charCount.textContent = `${ promptField.value.length }/2000`;
3153 - }
3154 - };
3155 -
3156 - const triggerGeneration = function () {
3157 - const prompt = promptField.value.trim();
3158 - if ( ! prompt ) {
3159 - promptField.focus();
3160 - return;
3161 - }
3162 -
3163 - generateButton.disabled = true;
3164 - generateButton.textContent = config.redirectingTxt;
3165 -
3166 - const redirectUrl = new URL( config.redirectUrl, window.location.origin );
3167 - redirectUrl.searchParams.set( 'srfm_ai_dashboard_prompt', prompt );
3168 -
3169 - const requestBody = new URLSearchParams();
3170 - requestBody.append( 'action', 'srfm_ai_widget_usage' );
3171 - requestBody.append( 'nonce', config.nonce );
3172 -
3173 - fetch( config.ajaxUrl, {
3174 - method: 'POST',
3175 - credentials: 'same-origin',
3176 - headers: {
3177 - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
3178 - },
3179 - body: requestBody.toString(),
3180 - } ).finally( function () {
3181 - window.location.href = redirectUrl.toString();
3182 - } );
3183 - };
3184 -
3185 - promptField.addEventListener( 'input', updateWidgetState );
3186 - generateButton.addEventListener( 'click', triggerGeneration );
3187 - promptField.addEventListener( 'keydown', function ( event ) {
3188 - if ( event.key === 'Enter' && ( event.metaKey || event.ctrlKey ) ) {
3189 - event.preventDefault();
3190 - triggerGeneration();
3191 - }
3192 - } );
3193 -
3194 - updateWidgetState();
3195 -}() );
3196 -JS;
3197 -
3198 - wp_add_inline_script( 'srfm-ai-dashboard-widget', $inline_script );
3199 - }
3200 -
3201 - /**
3202 - * Count an editor visit that came from the front-end "Edit Form" pill.
3203 - *
3204 - * The pill is a plain link, so the click is attributed by the marker query arg
3205 - * it carries rather than by a front-end click handler. That keeps the front end
3206 - * script-free and adds no AJAX endpoint: the only thing on the page is still an
3207 - * anchor. It also measures the outcome that matters — the editor actually
3208 - * opening — instead of a click that may never land.
3209 - *
3210 - * Every decision here comes from server state. The query arg selects the code
3211 - * path; what gets counted is derived from the resolved post and the current
3212 - * user's capability on it. An absent, empty, misspelled or reused arg, a post
3213 - * that is not a SureForms form, and a user without `edit_post` on that form all
3214 - * fall through to no-op without an explicit branch.
3215 - *
3216 - * No nonce, deliberately: the pill is rendered into front-end HTML that may be
3217 - * page-cached, so a nonce would either be baked into the cache or be stale on
3218 - * arrival. The effect is a private usage counter for a user who can already edit
3219 - * the form, and nothing attacker-controlled reaches the analytics payload — the
3220 - * value sent is an integer read back from stored state.
3221 - *
3222 - * Because the marker is just a query arg, the invariant that bounds this is the
3223 - * dedup transient below, not the arg: a given editor moves the counter at most
3224 - * once per form per hour, no matter how many times the URL is requested. That is
3225 - * also what keeps the metric honest — without it a refresh or a back-navigation
3226 - * would count again, and each count is a read-modify-write of the whole
3227 - * `srfm_options` row, which holds unrelated settings.
3228 - *
3229 - * @return void
3230 - * @since 2.12.6
3231 - */
3232 - public function maybe_track_edit_form_button_click() {
3233 - // is_string() before sanitize_key(): `?srfm_edit_src[]=x` satisfies isset(),
3234 - // and wp_unslash() hands the array straight through. sanitize_key() only grew
3235 - // its is_scalar() guard after this plugin's minimum WordPress, so on the older
3236 - // supported versions that reaches strtolower( array ) — a TypeError on PHP 8,
3237 - // i.e. the one input shape that ended in a fatal rather than in the no-op the
3238 - // rest of this method guarantees.
3239 - $arg = Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG;
3240 -
3241 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only attribution marker; see docblock for why a nonce is neither possible nor needed.
3242 - $source = isset( $_GET[ $arg ] ) && is_string( $_GET[ $arg ] ) ? sanitize_key( wp_unslash( $_GET[ $arg ] ) ) : '';
3243 -
3244 - if ( 'embed' !== $source ) {
3245 - return;
3246 - }
3247 -
3248 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Same read-only path as above.
3249 - $post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
3250 -
3251 - // Resolve the post type from the stored post, never from the request.
3252 - //
3253 - // The capability below reads as per-post but is not: sureforms_form is
3254 - // registered with an explicit capabilities map and no `map_meta_cap`
3255 - // (inc/post-types.php), so core short-circuits `edit_post` to the post type's
3256 - // `edit_post` capability — `manage_options` — without ever consulting $post_id.
3257 - // The real gate is therefore "site administrator", which is stricter than a
3258 - // per-form check, not weaker. Written down because a later `map_meta_cap` on
3259 - // the CPT would silently change what this line means with no diff here.
3260 - if ( 0 === $post_id || SRFM_FORMS_POST_TYPE !== get_post_type( $post_id ) ) {
3261 - return;
3262 - }
3263 -
3264 - if ( ! current_user_can( 'edit_post', $post_id ) ) {
3265 - return;
3266 - }
3267 -
3268 - // One count per editor per form per hour. Without this the metric measures
3269 - // "editor loads carrying the marker" rather than pill clicks — a refresh or a
3270 - // back-navigation re-counts — and a forged page could drive the counter, and
3271 - // the writes behind it, without bound.
3272 - $dedup_key = 'srfm_pill_click_' . get_current_user_id() . '_' . $post_id;
3273 -
3274 - if ( false !== get_transient( $dedup_key ) ) {
3275 - return;
3276 - }
3277 -
3278 - set_transient( $dedup_key, 1, HOUR_IN_SECONDS );
3279 -
3280 - $count = Helper::get_integer_value( Helper::get_srfm_option( 'edit_form_button_clicks', 0 ) ) + 1;
3281 - Helper::update_srfm_option( 'edit_form_button_clicks', $count );
3282 -
3283 - // $force = true because this is a cumulative counter, not a one-time event —
3284 - // it must re-send the latest count each cycle (bypasses one-time dedup).
3285 - Analytics::events()->track( 'edit_form_button_clicked', (string) $count, [], true );
3286 - }
3287 -
3288 - /**
3289 - * Let core strip the edit-attribution marker from the admin URL.
3290 - *
3291 - * Core's wp_admin_canonical_url() rewrites the address bar via replaceState() on
3292 - * admin_head, which runs after load-post.php — so the marker has already been
3293 - * counted by the time it is removed and no attribution is lost. Without this it
3294 - * lingers in the address bar, in bookmarks, and in the Referer header sent to
3295 - * every subresource the editor loads.
3296 - *
3297 - * @param array<string> $args Query args core already removes.
3298 - * @since 2.12.6
3299 - * @return array<string> Args with the marker appended.
3300 - */
3301 - public function add_removable_query_args( $args ) {
3302 - if ( ! is_array( $args ) ) {
3303 - return [ Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG ];
3304 - }
3305 -
3306 - $args[] = Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG;
3307 -
3308 - return $args;
3309 - }
3310 -
3311 - /**
3312 - * Track AI dashboard widget usage.
3313 - *
3314 - * @return void
3315 - * @since 2.12.1
3316 - */
3317 - public function track_ai_widget_usage() {
3318 - if ( ! check_ajax_referer( 'srfm_ai_widget_usage', 'nonce', false ) ) {
3319 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
3320 - }
3321 -
3322 - if ( ! Helper::current_user_can() ) {
3323 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
3324 - }
3325 -
3326 - $current_count = (int) Helper::get_srfm_option( 'ai_dashboard_widget_uses', 0 ) + 1;
3327 - Helper::update_srfm_option( 'ai_dashboard_widget_uses', $current_count );
3328 -
3329 - // Emit an analytics event so usage lands in the warehouse via events_record.
3330 - // $force = true because this is a cumulative counter, not a one-time event —
3331 - // it must re-send the latest count each cycle (bypasses one-time dedup).
3332 - Analytics::events()->track( 'ai_dashboard_widget_used', (string) $current_count, [], true );
3333 -
3334 - wp_send_json_success();
3335 - }
3336 -
3337 - /**
3338 1334 * Render the dashboard widget content.
3339 1335 *
3340 1336 * @return void
3341 1337 * @since 1.9.1
@@ -3384,739 +1380,8 @@
3384 1380 <?php
3385 1381 }
3386 1382
3387 1383 /**
3388 - * Classic dashboard notice when submissions keep failing.
3389 - *
3390 - * Hooked - admin_notices.
3391 - *
3392 - * Gated to the WP dashboard. The React notice already covers SureForms' own
3393 - * screens, so leaving this admin-wide would stack two warnings on one page.
3394 - *
3395 - * Registered as [ $this, 'method' ] rather than a closure because
3396 - * suppress_foreign_admin_notices() strips any callback it cannot attribute to
3397 - * a SureForms class -- a closure here would be silently removed.
3398 - *
3399 - * @since 2.12.6
3400 - * @return void
3401 - */
3402 - public function render_action_item_notices() {
3403 - // Shown across wp-admin, because someone whose forms are silently failing
3404 - // may not open the WP dashboard or SureForms for days.
3405 - //
3406 - // The one exclusion is SureForms' own dashboard: the Form Checks panel in
3407 - // its sidebar already lists these, and a banner above it would say the same
3408 - // thing twice on one screen.
3409 - if ( Helper::validate_request_context( 'sureforms_menu', 'page' ) ) {
3410 - return;
3411 - }
3412 -
3413 - $this->enqueue_notice_response_script();
3414 -
3415 - foreach ( $this->get_action_items() as $item ) {
3416 - $status = Helper::get_string_value( $item['status'] ?? '' );
3417 -
3418 - // Passing checks belong in the SureForms panel, not in wp-admin. A
3419 - // notice that says nothing is wrong is noise on every page load.
3420 - if ( 'success' === $status || '' === $status ) {
3421 - continue;
3422 - }
3423 -
3424 - // A fault reads as an error; advice reads as a warning. Both are shown,
3425 - // but they are not the same kind of message and should not look alike.
3426 - $class = 'error' === $status ? 'notice-error' : 'notice-warning';
3427 - ?>
3428 - <div class="notice <?php echo esc_attr( $class ); ?>">
3429 - <p><strong><?php echo esc_html( $item['title'] ); ?></strong></p>
3430 - <p><?php echo esc_html( $item['message'] ); ?></p>
3431 - <p>
3432 - <a
3433 - href="<?php echo esc_url( Helper::get_string_value( $item['cta_url'] ) ); ?>"
3434 - class="button button-primary"
3435 - data-srfm-notice-id="<?php echo esc_attr( Helper::get_string_value( $item['id'] ) ); ?>"
3436 - data-srfm-button="<?php echo esc_attr( Helper::get_string_value( $item['cta_action'] ?? '' ) ); ?>"
3437 - <?php echo 0 === strpos( Helper::get_string_value( $item['cta_url'] ), 'mailto:' ) ? '' : 'target="_blank" rel="noopener noreferrer"'; ?>
3438 - >
3439 - <?php echo esc_html( $item['cta_label'] ); ?>
3440 - </a>
3441 - <?php if ( ! empty( $item['dismissible'] ) ) { ?>
3442 - <a href="<?php echo esc_url( $this->get_dismiss_action_item_url( Helper::get_string_value( $item['id'] ) ) ); ?>" class="button">
3443 - <?php esc_html_e( 'Dismiss', 'sureforms' ); ?>
3444 - </a>
3445 - <?php } ?>
3446 - </p>
3447 - </div>
3448 - <?php
3449 - }
3450 - }
3451 -
3452 - /**
3453 - * Dismiss an action item from the classic notice's link.
3454 - *
3455 - * Hooked - admin_post_srfm_dismiss_action_item_link.
3456 - *
3457 - * @since 2.12.6
3458 - * @return void
3459 - */
3460 - public function handle_dismiss_action_item_link() {
3461 - if ( ! Helper::current_user_can() ) {
3462 - wp_die( esc_html__( 'You do not have permission to do this.', 'sureforms' ), 403 );
3463 - }
3464 -
3465 - check_admin_referer( 'srfm_dismiss_action_item' );
3466 -
3467 - $item_id = isset( $_GET['item'] ) ? sanitize_key( wp_unslash( $_GET['item'] ) ) : '';
3468 -
3469 - $this->dismiss_action_item( $item_id );
3470 -
3471 - $referer = wp_get_referer();
3472 -
3473 - wp_safe_redirect( $referer ? $referer : admin_url() );
3474 - exit;
3475 - }
3476 -
3477 - /**
3478 - * Whether anything is currently wrong enough to warrant a notice.
3479 - *
3480 - * Deliberately re-derives the two conditions rather than calling
3481 - * get_action_items(), which records an impression as a side effect and must not
3482 - * run from a show_if callback.
3483 - *
3484 - * @since 2.12.6
3485 - * @return bool
3486 - */
3487 - public function has_action_item_warnings() {
3488 - if ( Client_Logger::has_persistent_failures() ) {
3489 - return true;
3490 - }
3491 -
3492 - if ( '' === Helper::get_active_caching_plugin() ) {
3493 - return false;
3494 - }
3495 -
3496 - $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) );
3497 -
3498 - return ! in_array( 'caching_plugin', $dismissed, true );
3499 - }
3500 -
3501 - /**
3502 - * Things on this site that need the owner's attention, newest concern first.
3503 - *
3504 - * Fed to the dashboard sidebar carousel. Each entry is self-describing so the
3505 - * front end has no rules of its own to keep in sync -- adding a new item here
3506 - * makes it appear with no JavaScript change.
3507 - *
3508 - * `dismissible` separates a fault from advice. A run of failed submissions is
3509 - * not something to wave away, and clears itself when a submission succeeds. A
3510 - * caching plugin being present is information, so it can be dismissed.
3511 - *
3512 - * @since 2.12.6
3513 - * @return array<int,array<string,mixed>>
3514 - */
3515 - public function get_action_items() {
3516 - if ( ! Helper::current_user_can() ) {
3517 - return [];
3518 - }
3519 -
3520 - $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) );
3521 - $warnings = [];
3522 - $passing = [];
3523 -
3524 - $open = Client_Logger::get_open_failures();
3525 -
3526 - // One item per category. They read differently to a site owner and must not
3527 - // be collapsed: submissions failing means visitors cannot reach you, a
3528 - // notification failing means you are not hearing about entries that did
3529 - // save, an integration failing means a third party is not receiving them.
3530 - $categories = [
3531 - 'submission' => [
3532 - 'id' => 'form_submission_error',
3533 - /* translators: %s: form title. */
3534 - 'title' => __( 'We noticed a form submission failure on %s.', 'sureforms' ),
3535 - 'generic' => __( 'We noticed a form submission failure.', 'sureforms' ),
3536 - 'message' => __( 'Visitors may be unable to reach you, and those entries were not saved.', 'sureforms' ),
3537 - 'passing' => __( 'Form submissions are completing normally.', 'sureforms' ),
3538 - ],
3539 - 'notification' => [
3540 - 'id' => 'notification_error',
3541 - /* translators: %s: form title. */
3542 - 'title' => __( 'We noticed a notification failure on %s.', 'sureforms' ),
3543 - 'generic' => __( 'We noticed a notification failure.', 'sureforms' ),
3544 - 'message' => __( 'The entry was saved, but the email telling you about it could not be sent — so new entries may be arriving without you hearing about them.', 'sureforms' ),
3545 - 'passing' => __( 'Notification emails are sending normally.', 'sureforms' ),
3546 - ],
3547 - 'integration' => [
3548 - 'id' => 'integration_error',
3549 - /* translators: %s: form title. */
3550 - 'title' => __( 'We noticed an integration failure on %s.', 'sureforms' ),
3551 - 'generic' => __( 'We noticed an integration failure.', 'sureforms' ),
3552 - 'message' => __( 'The entry was saved, but it could not be passed on to a connected service.', 'sureforms' ),
3553 - 'passing' => __( 'Integrations are running normally.', 'sureforms' ),
3554 - ],
3555 - ];
3556 -
3557 - foreach ( $categories as $category => $copy ) {
3558 - if ( ! isset( $open[ $category ] ) ) {
3559 - $passing[] = [
3560 - 'id' => $copy['id'],
3561 - 'status' => 'success',
3562 - 'title' => $copy['passing'],
3563 - 'message' => '',
3564 - 'cta_label' => '',
3565 - 'cta_url' => '',
3566 - 'dismissible' => false,
3567 - ];
3568 - continue;
3569 - }
3570 -
3571 - // Name the form. "A form is failing" is not actionable on a site with
3572 - // twenty of them, and the title is the first thing anyone asks for.
3573 - $form_title = Helper::get_string_value( $open[ $category ]['form_title'] ?? '' );
3574 -
3575 - $warnings[] = [
3576 - 'id' => $copy['id'],
3577 - 'status' => 'error',
3578 - 'title' => '' !== $form_title
3579 - ? sprintf( $copy['title'], $form_title )
3580 - : $copy['generic'],
3581 - 'message' => $copy['message'],
3582 - 'cta_label' => __( 'Contact Support', 'sureforms' ),
3583 - 'cta_url' => $this->get_support_mailto_url(),
3584 - 'cta_action' => 'contact_support',
3585 - 'dismissible' => false,
3586 - ];
3587 - }
3588 -
3589 - $caching_plugin = Helper::get_active_caching_plugin();
3590 -
3591 - if ( '' === $caching_plugin ) {
3592 - $passing[] = [
3593 - 'id' => 'caching_plugin',
3594 - 'status' => 'success',
3595 - 'title' => __( 'No caching plugin that needs configuring was found.', 'sureforms' ),
3596 - 'message' => '',
3597 - 'cta_label' => '',
3598 - 'cta_url' => '',
3599 - 'dismissible' => false,
3600 - ];
3601 - } elseif ( ! in_array( 'caching_plugin', $dismissed, true ) ) {
3602 - $warnings[] = [
3603 - 'id' => 'caching_plugin',
3604 - 'status' => 'warning',
3605 - 'title' => sprintf(
3606 - /* translators: %s: caching plugin name. */
3607 - __( '%s may interfere with your forms.', 'sureforms' ),
3608 - $caching_plugin
3609 - ),
3610 - 'message' => __( 'Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order.', 'sureforms' ),
3611 - 'cta_label' => __( 'Help Me Fix', 'sureforms' ),
3612 - 'cta_url' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-caching-plugins/',
3613 - 'cta_action' => 'help_me_fix',
3614 - 'dismissible' => true,
3615 - ];
3616 - }
3617 -
3618 - // Warnings first: the point of the panel is what needs attention, with the
3619 - // passing checks below as reassurance rather than as the headline.
3620 - $items = array_merge( $warnings, $passing );
3621 -
3622 - $this->track_action_item_impressions( $warnings );
3623 -
3624 - /**
3625 - * Filter the dashboard action items.
3626 - *
3627 - * Each entry needs id, status ('warning' or 'success'), title, message,
3628 - * cta_label, cta_url and dismissible. Only ids in
3629 - * handle_dismiss_action_item()'s allowlist can actually be dismissed, so
3630 - * adding a dismissible item here also needs a line there.
3631 - *
3632 - * @since 2.12.6
3633 - *
3634 - * @param array<int,array<string,mixed>> $items Action items.
3635 - */
3636 - return Helper::apply_filters_as_array( 'srfm_action_items', $items );
3637 - }
3638 -
3639 - /**
3640 - * Dismiss one action item.
3641 - *
3642 - * Hooked - wp_ajax_srfm_dismiss_action_item.
3643 - *
3644 - * Only items get_action_items() marks dismissible can be dismissed, so a
3645 - * crafted request cannot silence a genuine fault.
3646 - *
3647 - * @since 2.12.6
3648 - * @return void
3649 - */
3650 - public function handle_dismiss_action_item() {
3651 - if ( ! Helper::current_user_can() ) {
3652 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
3653 - return;
3654 - }
3655 -
3656 - if ( ! check_ajax_referer( 'srfm_dismiss_action_item', 'nonce', false ) ) {
3657 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
3658 - return;
3659 - }
3660 -
3661 - $item_id = isset( $_POST['item_id'] ) ? sanitize_key( wp_unslash( $_POST['item_id'] ) ) : '';
3662 -
3663 - if ( ! $this->dismiss_action_item( $item_id ) ) {
3664 - wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 );
3665 - return;
3666 - }
3667 -
3668 - wp_send_json_success();
3669 - }
3670 -
3671 - /**
3672 - * Nonce-protected URL that repairs the entries table.
3673 - *
3674 - * Shared by both notice surfaces so there is one repair route, one nonce and one
3675 - * place that counts the click. Private, so it stays off the public API and out of
3676 - * the test-coverage gate.
3677 - *
3678 - * @since 2.12.6
3679 - * @return string
3680 - */
3681 - private function get_database_repair_url() {
3682 - return wp_nonce_url(
3683 - admin_url( 'admin-post.php?action=srfm_repair_entries_table' ),
3684 - 'srfm_repair_entries_table'
3685 - );
3686 - }
3687 -
3688 - /**
3689 - * The database notice body, which differs by what the repair will actually do.
3690 - *
3691 - * Two outcomes are possible and they are not equivalent to the person clicking:
3692 - * when the entries table exists under a different prefix — a changed
3693 - * `$table_prefix`, a restored dump, a security plugin that renamed tables and
3694 - * skipped ours — the repair renames it back and every stored entry comes with
3695 - * it. When there is nothing to adopt, the repair creates an empty table and the
3696 - * old submissions are not recoverable from here.
3697 - *
3698 - * Promising the wrong one is how a maintenance prompt turns into a complaint, so
3699 - * the copy states which is about to happen.
3700 - *
3701 - * @since 2.12.6
3702 - * @return string
3703 - */
3704 - private function get_database_notice_message() {
3705 - if ( '' !== Register::get_adoptable_entries_table() ) {
3706 - return __( 'SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept.', 'sureforms' );
3707 - }
3708 -
3709 - return __( 'SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here.', 'sureforms' );
3710 - }
3711 -
3712 - /**
3713 - * Count one sighting of the database notice, at most once per user per day.
3714 - *
3715 - * While the table is missing the notice renders on every admin page load, on two
3716 - * surfaces. Counting each render would rewrite the autoloaded `srfm_options` blob
3717 - * on every pageview of a site that is already broken, and one site left unfixed
3718 - * would dominate the aggregate. Throttling to a day per user answers the question
3719 - * that matters — how many people are seeing this — for one write.
3720 - *
3721 - * @since 2.12.6
3722 - * @return void
3723 - */
3724 - private function track_database_notice_impression() {
3725 - $user_id = get_current_user_id();
3726 -
3727 - if ( ! $user_id ) {
3728 - return;
3729 - }
3730 -
3731 - $key = 'srfm_db_notice_seen_' . $user_id;
3732 -
3733 - if ( get_transient( $key ) ) {
3734 - return;
3735 - }
3736 -
3737 - set_transient( $key, 1, DAY_IN_SECONDS );
3738 -
3739 - Analytics::events()->track( 'database_error_notice_shown', 'entries' );
3740 - }
3741 -
3742 - /**
3743 - * Build the setup-card payload (uncached). See get_form_setup_card().
3744 - *
3745 - * @since 2.12.4
3746 - * @return array<string,mixed>|null Card payload, or null when there is no candidate.
3747 - */
3748 - private static function compute_form_setup_card() {
3749 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
3750 - return null;
3751 - }
3752 -
3753 - // Negative cache. Deliberately not a `defined( 'ASTRA_SITES_VER' )` check:
3754 - // Starter Templates defines that constant in its main plugin file, so it only
3755 - // exists while the plugin is active, yet neither its uninstall.php nor its
3756 - // deactivation hook removes the import marker. Gating on the constant would
3757 - // silently switch this feature off for the very people it targets — anyone who
3758 - // imported a starter template and then removed the one-shot import plugin.
3759 - if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) {
3760 - return null;
3761 - }
3762 -
3763 - // Only forms created from an Astra Sites starter template — those carry the
3764 - // marker Starter Templates stamps on imported posts (self::ASTRA_SITES_IMPORT_META).
3765 - // Prime post + meta caches (the loop reads title, permalink and edit link
3766 - // per candidate) so this is a single query, not a follow-up per form.
3767 - $query = new \WP_Query(
3768 - [
3769 - 'post_type' => SRFM_FORMS_POST_TYPE,
3770 - 'post_status' => [ 'publish', 'draft', 'pending' ],
3771 - 'posts_per_page' => 10,
3772 - // ID breaks the tie: a starter-template import creates several forms
3773 - // within the same second, so post_date alone leaves "the newest form"
3774 - // up to MySQL and it can differ between page loads.
3775 - 'orderby' => [
3776 - 'date' => 'DESC',
3777 - 'ID' => 'DESC',
3778 - ],
3779 - 'no_found_rows' => true,
3780 - 'update_post_meta_cache' => true,
3781 - 'update_post_term_cache' => false,
3782 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; dashboard-only.
3783 - [
3784 - 'key' => self::ASTRA_SITES_IMPORT_META,
3785 - 'compare' => 'EXISTS',
3786 - ],
3787 - ],
3788 - ]
3789 - );
3790 -
3791 - // Nothing on this site carries the marker — remember that, so the query does
3792 - // not repeat on every load. Keyed on the query result rather than on anything
3793 - // user-specific, so it is safe to share, and invalidated the moment a post is
3794 - // stamped (see invalidate_starter_template_cache()).
3795 - if ( empty( $query->posts ) ) {
3796 - set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS );
3797 - }
3798 -
3799 - foreach ( $query->posts as $post ) {
3800 - $form_id = (int) $post->ID;
3801 -
3802 - if ( ! current_user_can( 'edit_post', $form_id ) ) {
3803 - continue;
3804 - }
3805 -
3806 - $edit_link = get_edit_post_link( $form_id, 'raw' );
3807 -
3808 - if ( empty( $edit_link ) ) {
3809 - continue;
3810 - }
3811 -
3812 - // The steps are shown as optional next-steps — their completion is not
3813 - // computed, so the widget simply lists the actions the owner can take.
3814 - return [
3815 - 'id' => $form_id,
3816 - 'title' => get_the_title( $form_id ),
3817 - 'edit_url' => $edit_link,
3818 - // Deep-links to the email-notification panel where supported; falls
3819 - // back to opening the editor when the focus handler isn't present.
3820 - 'email_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ),
3821 - // Deep-links to the Form Confirmation panel (the Thank You message).
3822 - 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ),
3823 - // Front-end instant-form page. get_permalink() only yields a working
3824 - // URL for published forms; a draft/pending form has no public URL, so
3825 - // omit the view link there (the empty() guard hides the icon).
3826 - 'view_url' => 'publish' === $post->post_status ? (string) get_permalink( $form_id ) : '',
3827 - ];
3828 - }
3829 -
3830 - return null;
3831 - }
3832 -
3833 - /**
3834 - * Build the Thank You prompt payload (uncached). See get_thankyou_prompt_forms().
3835 - *
3836 - * @since 2.12.4
3837 - * @return array<int,array<string,mixed>> One entry, or none.
3838 - */
3839 - private static function compute_thankyou_prompt_forms() {
3840 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
3841 - return [];
3842 - }
3843 -
3844 - // Negative cache — this notice renders on every admin screen, so keeping the
3845 - // query off installs that can never match is what matters here. See
3846 - // self::NO_IMPORTED_FORMS_TRANSIENT for why this is not gated on whether
3847 - // Starter Templates is still active: the marker outlives the plugin.
3848 - if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) {
3849 - return [];
3850 - }
3851 -
3852 - // Only forms imported from a Starter Templates (Astra Sites) starter
3853 - // template — see self::ASTRA_SITES_IMPORT_META. Prime post + meta caches
3854 - // (the loop reads meta, title and creation time per candidate) so this is a
3855 - // single query rather than the main query plus a follow-up per form.
3856 - $query = new \WP_Query(
3857 - [
3858 - 'post_type' => SRFM_FORMS_POST_TYPE,
3859 - 'post_status' => 'publish',
3860 - 'posts_per_page' => 10,
3861 - // ID breaks the tie — an import creates several forms in the same
3862 - // second, so post_date alone makes "newest" MySQL-dependent.
3863 - 'orderby' => [
3864 - 'date' => 'DESC',
3865 - 'ID' => 'DESC',
3866 - ],
3867 - 'no_found_rows' => true,
3868 - 'update_post_meta_cache' => true,
3869 - 'update_post_term_cache' => false,
3870 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; admin-notice only.
3871 - [
3872 - 'key' => self::ASTRA_SITES_IMPORT_META,
3873 - 'compare' => 'EXISTS',
3874 - ],
3875 - ],
3876 - ]
3877 - );
3878 -
3879 - // Nothing on this site carries the marker — remember that, so the query does
3880 - // not repeat on every load. Keyed on the query result rather than on anything
3881 - // user-specific, so it is safe to share, and invalidated the moment a post is
3882 - // stamped (see invalidate_starter_template_cache()).
3883 - if ( empty( $query->posts ) ) {
3884 - set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS );
3885 - }
3886 -
3887 - $prompts = [];
3888 - $now = time();
3889 -
3890 - foreach ( $query->posts as $post ) {
3891 - $form_id = (int) $post->ID;
3892 -
3893 - if ( ! current_user_can( 'edit_post', $form_id ) ) {
3894 - continue;
3895 - }
3896 -
3897 - $steps = [
3898 - // A destination for replies: an enabled notification with a recipient.
3899 - 'replies' => ! self::form_has_reply_destination( $form_id ),
3900 - // The thank-you message is still the shipped default.
3901 - 'thankyou' => self::is_default_confirmation_message( $form_id ),
3902 - ];
3903 -
3904 - // Nothing left to finish — no card for this form.
3905 - if ( ! $steps['replies'] && ! $steps['thankyou'] ) {
3906 - continue;
3907 - }
3908 -
3909 - $edit_link = get_edit_post_link( $form_id, 'raw' );
3910 -
3911 - if ( empty( $edit_link ) ) {
3912 - continue;
3913 - }
3914 -
3915 - $created = get_post_time( 'U', true, $form_id );
3916 - $days_ago = is_int( $created ) ? (int) floor( ( $now - $created ) / DAY_IN_SECONDS ) : 0;
3917 -
3918 - $prompts[] = [
3919 - 'id' => $form_id,
3920 - 'title' => get_the_title( $form_id ),
3921 - 'days_ago' => max( 0, $days_ago ),
3922 - 'steps' => $steps,
3923 - 'edit_url' => $edit_link,
3924 - // The editor reads srfm_focus to open the matching settings tab:
3925 - // "notifications" lands on Email Notification (where the reply
3926 - // destination is set, so the CTA can actually clear that step) and
3927 - // "thankyou" on Form Confirmation.
3928 - 'replies_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ),
3929 - 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ),
3930 - ];
3931 -
3932 - // One card is enough — surface only the latest form needing setup.
3933 - break;
3934 - }
3935 -
3936 - return $prompts;
3937 - }
3938 -
3939 - /**
3940 - * Build the Thank You notice's inner markup (title, sentence, action buttons).
3941 - *
3942 - * @param array<string,mixed> $form Prompt payload from get_thankyou_prompt_forms().
3943 - *
3944 - * @since 2.12.4
3945 - * @return string
3946 - */
3947 - private static function build_thankyou_notice_markup( $form ) {
3948 - // The prompt only surfaces starter-template imports (see the meta gate), so
3949 - // the form was created for the user rather than by them. Kept generic — no
3950 - // per-step claim — so it is always accurate whatever the user has since
3951 - // changed, while the action buttons point to the specific things to finish.
3952 - $sentence = __( 'We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions.', 'sureforms' );
3953 -
3954 - return self::build_srfm_notice_markup(
3955 - sprintf(
3956 - /* translators: %s: form name. */
3957 - __( 'Finish setting up “%s”', 'sureforms' ),
3958 - $form['title']
3959 - ),
3960 - $sentence,
3961 - [
3962 - [
3963 - 'text' => __( 'Edit form', 'sureforms' ),
3964 - 'url' => $form['edit_url'],
3965 - 'primary' => true,
3966 - 'class' => 'srfm-ty-edit-form',
3967 - 'external' => true,
3968 - ],
3969 - [
3970 - 'text' => __( 'Edit the Thank You message', 'sureforms' ),
3971 - 'url' => $form['thankyou_url'],
3972 - 'class' => 'srfm-ty-edit-thankyou',
3973 - 'external' => true,
3974 - ],
3975 - [
3976 - 'text' => __( 'Set where replies go', 'sureforms' ),
3977 - 'url' => $form['replies_url'],
3978 - 'class' => 'srfm-ty-set-replies',
3979 - 'external' => true,
3980 - ],
3981 - ]
3982 - );
3983 - }
3984 -
3985 - /**
3986 - * Build the shared SureForms admin-notice body: title, sentence, action row.
3987 - *
3988 - * One builder for every SureForms notice so they cannot drift into looking like
3989 - * two different plugins. Everything is escaped here rather than by the caller —
3990 - * the notices library runs the result through wp_kses_post(), which would strip
3991 - * anything richer anyway.
3992 - *
3993 - * @param string $title Notice heading.
3994 - * @param string $text Supporting sentence.
3995 - * @param array<int,array<string,mixed>> $actions Action links. Each accepts
3996 - * text, url, and optionally
3997 - * primary, class, external,
3998 - * dismiss and snooze (seconds).
3999 - * @since 2.12.6
4000 - * @return string
4001 - */
4002 - private static function build_srfm_notice_markup( $title, $text, $actions ) {
4003 - ob_start();
4004 - ?>
4005 - <p class="srfm-notice__title"><?php echo esc_html( $title ); ?></p>
4006 - <p class="srfm-notice__text"><?php echo esc_html( $text ); ?></p>
4007 - <p class="srfm-notice__actions">
4008 - <?php
4009 - foreach ( $actions as $action ) {
4010 - if ( empty( $action['text'] ) || ! isset( $action['url'] ) ) {
4011 - continue;
4012 - }
4013 -
4014 - $classes = [ 'button' ];
4015 -
4016 - if ( ! empty( $action['primary'] ) ) {
4017 - $classes[] = 'button-primary';
4018 - }
4019 -
4020 - // astra-notice-close is what the library binds its dismiss handler to.
4021 - if ( ! empty( $action['dismiss'] ) ) {
4022 - $classes[] = 'astra-notice-close';
4023 - }
4024 -
4025 - if ( ! empty( $action['class'] ) ) {
4026 - $classes[] = $action['class'];
4027 - }
4028 - ?>
4029 - <a
4030 - class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
4031 - href="<?php echo esc_url( $action['url'] ); ?>"
4032 - <?php echo empty( $action['snooze'] ) ? '' : ' data-repeat-notice-after="' . esc_attr( (string) $action['snooze'] ) . '"'; ?>
4033 - <?php echo empty( $action['external'] ) ? '' : ' target="_blank" rel="noopener noreferrer"'; ?>
4034 - ><?php echo esc_html( $action['text'] ); ?></a>
4035 - <?php
4036 - }
4037 - ?>
4038 - </p>
4039 - <?php
4040 - return (string) ob_get_clean();
4041 - }
4042 -
4043 - /**
4044 - * Determine whether a notice callback is owned by SureForms.
4045 - *
4046 - * Recognises object methods on classes in the `SRFM` / `SRFM_PRO` namespaces
4047 - * as well as the bundled notices libraries (`BSF_Admin_Notices` and the
4048 - * legacy `Astra_Notices` alias). Everything else is treated as foreign.
4049 - *
4050 - * @param callable|array|string|null $function The registered callback function.
4051 - * @since 2.10.0
4052 - * @return bool True when the callback belongs to SureForms, false otherwise.
4053 - */
4054 - private function is_sureforms_owned_notice_callback( $function ) {
4055 - $class_name = '';
4056 -
4057 - if ( is_array( $function ) && isset( $function[0] ) ) {
4058 - // Object or static method callback represented as an array. The first
4059 - // element is either the object instance or the fully qualified class name.
4060 - $class_name = is_object( $function[0] ) ? get_class( $function[0] ) : (string) $function[0];
4061 - } elseif ( is_string( $function ) && false !== strpos( $function, '::' ) ) {
4062 - // Static method passed as "Class::method".
4063 - $class_name = strstr( $function, '::', true );
4064 - }
4065 -
4066 - if ( '' === $class_name ) {
4067 - // Plain function callbacks are never owned by SureForms.
4068 - return false;
4069 - }
4070 -
4071 - // SureForms (free and pro) namespaced classes. Pro's real namespace is
4072 - // `SRFM_Pro\` (case-sensitive) — not the all-caps `SRFM_PRO_` constant
4073 - // prefix — so match it case-insensitively to be safe.
4074 - if ( 0 === strpos( $class_name, 'SRFM\\' ) || 0 === stripos( $class_name, 'SRFM_Pro\\' ) ) {
4075 - return true;
4076 - }
4077 -
4078 - // Bundled notices library shipped with SureForms.
4079 - return in_array( $class_name, [ 'BSF_Admin_Notices', 'Astra_Notices' ], true );
4080 - }
4081 -
4082 - /**
4083 - * Whether the current admin page is a SureForms-owned screen identified by a
4084 - * `sureforms_*` / `srfm_*` `page` query slug. Complements
4085 - * {@see Helper::is_sureforms_admin_page()} so foreign-notice suppression also
4086 - * covers the payments / quiz / survey / learn / SMTP / partial-entries screens
4087 - * that the core helper does not enumerate. Read-only screen check.
4088 - *
4089 - * @since 2.10.0
4090 - * @return bool
4091 - */
4092 - private function is_sureforms_owned_admin_page() {
4093 - if ( ! is_admin() || empty( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change.
4094 - return false;
4095 - }
4096 - $page = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change.
4097 - return 0 === strpos( $page, 'sureforms' ) || 0 === strpos( $page, 'srfm' );
4098 - }
4099 -
4100 - /**
4101 - * Callback for displaying the rating notice conditionally.
4102 - *
4103 - * Returns true if the user has 3 or more published forms or 3 or more form entries.
4104 - *
4105 - * @since 2.5.2
4106 - * @return bool
4107 - */
4108 - private function maybe_display_rating_notice() {
4109 - if ( null === $this->should_show_rating ) {
4110 - $entries_count = Entries::get_total_entries_by_status( 'all' );
4111 - $form_count = wp_count_posts( SRFM_FORMS_POST_TYPE );
4112 - $this->should_show_rating = $entries_count >= self::RATING_NOTICE_THRESHOLD || Helper::get_integer_value( $form_count->publish ?? 0 ) >= self::RATING_NOTICE_THRESHOLD;
4113 - }
4114 -
4115 - return $this->should_show_rating;
4116 - }
4117 -
4118 - /**
4119 1384 * Get random premium feature text.
4120 1385 *
4121 1386 * @return string Random feature text.
4122 1387 * @since 1.9.1
@@ -4178,9 +1443,14 @@
4178 1443 </svg>
4179 1444 <span><?php echo esc_html( $this->get_random_premium_feature_text() ); ?></span>
4180 1445 </div>
4181 1446 <?php
4182 - $upgrade_url = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'dashboard-widget' ] );
1447 + $upgrade_url = add_query_arg(
1448 + [
1449 + 'utm_medium' => 'dashboard-widget',
1450 + ],
1451 + Helper::get_sureforms_website_url( 'pricing' )
1452 + );
4183 1453 ?>
4184 1454 <a href="<?php echo esc_url( $upgrade_url ); ?>" class="srfm-upgrade-link" target="_blank">
4185 1455 <?php esc_html_e( 'Upgrade', 'sureforms' ); ?>
4186 1456 </a>
@@ -4214,216 +1484,5 @@
4214 1484
4215 1485 return false;
4216 1486 }
4217 1487
4218 - /**
4219 - * Nonced URL that dismisses one action item without JavaScript.
4220 - *
4221 - * The classic notice cannot use the AJAX dismissal the carousel uses, and
4222 - * WordPress's own `is-dismissible` only hides the notice for that pageview.
4223 - *
4224 - * @param string $item_id Item to dismiss.
4225 - * @since 2.12.6
4226 - * @return string
4227 - */
4228 - private function get_dismiss_action_item_url( $item_id ) {
4229 - return wp_nonce_url(
4230 - add_query_arg(
4231 - [
4232 - 'action' => 'srfm_dismiss_action_item_link',
4233 - 'item' => $item_id,
4234 - ],
4235 - admin_url( 'admin-post.php' )
4236 - ),
4237 - 'srfm_dismiss_action_item'
4238 - );
4239 - }
4240 -
4241 - /**
4242 - * Count one sighting of each warning, at most once per user per day.
4243 - *
4244 - * Throttled because the classic notice renders on every admin page: counting
4245 - * each render would measure how much wp-admin someone browses, not how many
4246 - * sites are affected. A day per user answers the question that matters -- how
4247 - * many people are seeing this -- for one option write.
4248 - *
4249 - * Passing checks are not counted. "Nothing is wrong" is not an impression.
4250 - *
4251 - * @param array<int,array<string,mixed>> $warnings Warning items only.
4252 - * @since 2.12.6
4253 - * @return void
4254 - */
4255 - private function track_action_item_impressions( $warnings ) {
4256 - if ( empty( $warnings ) || wp_doing_ajax() ) {
4257 - return;
4258 - }
4259 -
4260 - $user_id = get_current_user_id();
4261 -
4262 - if ( ! $user_id ) {
4263 - return;
4264 - }
4265 -
4266 - $counts = Helper::get_array_value( Helper::get_srfm_option( 'action_item_impressions', [] ) );
4267 - $changed = false;
4268 -
4269 - foreach ( $warnings as $warning ) {
4270 - $item_id = Helper::get_string_value( $warning['id'] ?? '' );
4271 -
4272 - if ( '' === $item_id ) {
4273 - continue;
4274 - }
4275 -
4276 - $seen_key = 'srfm_action_item_seen_' . $item_id . '_' . $user_id;
4277 -
4278 - if ( get_transient( $seen_key ) ) {
4279 - continue;
4280 - }
4281 -
4282 - set_transient( $seen_key, 1, DAY_IN_SECONDS );
4283 -
4284 - $counts[ $item_id ] = Helper::get_integer_value( $counts[ $item_id ] ?? 0 ) + 1;
4285 - $changed = true;
4286 -
4287 - // Cumulative, so $force = true: each new count is a new value and is
4288 - // re-sent, while an identical repeat short-circuits inside track().
4289 - Analytics::events()->track(
4290 - $item_id . '_notice_shown',
4291 - (string) $counts[ $item_id ],
4292 - [],
4293 - true
4294 - );
4295 - }
4296 -
4297 - if ( $changed ) {
4298 - Helper::update_srfm_option( 'action_item_impressions', $counts );
4299 - }
4300 - }
4301 -
4302 - /**
4303 - * Pre-addressed support email for a run of failed submissions.
4304 - *
4305 - * Carries the details support would otherwise have to ask for, so the first
4306 - * reply can be an answer rather than a questionnaire, along with the recent log
4307 - * entries inline.
4308 - *
4309 - * The log is pasted into the body rather than attached because mailto has no
4310 - * attachment parameter -- browsers drop any attempt to add one -- and it is a
4311 - * tail rather than the whole file because a megabyte of JSON would exceed the
4312 - * URL length every mail client enforces.
4313 - *
4314 - * @since 2.12.6
4315 - * @return string
4316 - */
4317 - private function get_support_mailto_url() {
4318 - $subject = sprintf(
4319 - /* translators: %s: site host. */
4320 - __( 'SureForms: form submissions are failing on %s', 'sureforms' ),
4321 - Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) )
4322 - );
4323 -
4324 - $log = Client_Logger::get_tail();
4325 - $body = $this->get_support_message();
4326 - $body .= "\r\n\r\n" . '---' . "\r\n";
4327 -
4328 - if ( '' === $log['text'] ) {
4329 - $body .= __( 'Debug log: no entries recorded.', 'sureforms' );
4330 - } else {
4331 - $body .= sprintf(
4332 - /* translators: 1: entries shown, 2: entries recorded. */
4333 - __( 'Debug log (most recent %1$d of %2$d entries)', 'sureforms' ),
4334 - $log['shown'],
4335 - $log['total']
4336 - ) . "\r\n";
4337 -
4338 - // Fenced so it survives a reply and reads as data rather than prose in
4339 - // clients that render Markdown.
4340 - $body .= '```' . "\r\n" . str_replace( "\n", "\r\n", $log['text'] ) . "\r\n" . '```';
4341 -
4342 - if ( $log['shown'] < $log['total'] ) {
4343 - $body .= "\r\n\r\n" . __( 'Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' );
4344 - }
4345 - }
4346 -
4347 - return 'mailto:support@sureforms.com?' . http_build_query(
4348 - [
4349 - 'subject' => $subject,
4350 - 'body' => $body,
4351 - ],
4352 - '',
4353 - '&',
4354 - PHP_QUERY_RFC3986
4355 - );
4356 - }
4357 -
4358 - /**
4359 - * Diagnostics block for the support email.
4360 - *
4361 - * Carries what support would otherwise have to ask for, so the first reply can
4362 - * be an answer rather than a questionnaire.
4363 - *
4364 - * @since 2.12.6
4365 - * @return string
4366 - */
4367 - private function get_support_message() {
4368 - global $wp_version;
4369 -
4370 - $count = Client_Logger::get_fault_streak();
4371 -
4372 - $lines = [
4373 - __( 'Hello SureForms support,', 'sureforms' ),
4374 - '',
4375 - sprintf(
4376 - /* translators: %d: number of consecutive failed submissions. */
4377 - _n(
4378 - 'SureForms has recorded %d form submission in a row that could not be completed.',
4379 - 'SureForms has recorded %d form submissions in a row that could not be completed.',
4380 - $count,
4381 - 'sureforms'
4382 - ),
4383 - $count
4384 - ),
4385 - '',
4386 - '---',
4387 - __( 'Site details', 'sureforms' ),
4388 - 'Site: ' . home_url(),
4389 - 'SureForms: ' . SRFM_VER,
4390 - 'SureForms Pro: ' . ( Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) ),
4391 - 'WordPress: ' . Helper::get_string_value( $wp_version ),
4392 - 'PHP: ' . PHP_VERSION,
4393 - 'Caching: ' . ( '' !== Helper::get_active_caching_plugin() ? Helper::get_active_caching_plugin() : __( 'none detected', 'sureforms' ) ),
4394 - 'Consecutive failures: ' . $count,
4395 - ];
4396 -
4397 - return implode( "\r\n", $lines );
4398 - }
4399 -
4400 - /**
4401 - * Record one dismissal, shared by the AJAX and no-JS entry points.
4402 - *
4403 - * Allowlisted, so only advisory items can be dismissed. A run of failed
4404 - * submissions is a fault and must stay put until it actually resolves --
4405 - * otherwise a crafted request could silence the one message that matters.
4406 - *
4407 - * @param string $item_id Item to dismiss.
4408 - * @since 2.12.6
4409 - * @return bool False when the id is not dismissible.
4410 - */
4411 - private function dismiss_action_item( $item_id ) {
4412 - if ( ! in_array( $item_id, [ 'caching_plugin' ], true ) ) {
4413 - return false;
4414 - }
4415 -
4416 - $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) );
4417 -
4418 - if ( ! in_array( $item_id, $dismissed, true ) ) {
4419 - $dismissed[] = $item_id;
4420 - Helper::update_srfm_option( 'dismissed_action_items', $dismissed );
4421 -
4422 - // Recorded here rather than at each caller: both the cross in the
4423 - // dashboard panel and the no-JS link in the classic notice land here.
4424 - Analytics::events()->track( $item_id . '_notice_dismiss', 'dismissed' );
4425 - }
4426 -
4427 - return true;
4428 - }
4429 1488 }