PluginProbe ʕ •ᴥ•ʔ
SureForms – Drag & Drop Contact Form & Form Builder, Payment Form, Survey, Quiz & Calculator / 2.12.0
SureForms – Drag & Drop Contact Form & Form Builder, Payment Form, Survey, Quiz & Calculator v2.12.0
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 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / admin / editor-nudge.php
sureforms / inc / admin Last commit date
editor-nudge.php 1 month ago html-form-detector.php 1 month ago
editor-nudge.php
291 lines
1 <?php
2 /**
3 * Editor Nudge.
4 *
5 * Marketing/education banner shown inside the block editor when a user is
6 * editing a post/page whose title hints at a form ("contact", "form",
7 * "forms"). Encourages them to create a SureForms form.
8 *
9 * Script is enqueued when:
10 * - User can manage SureForms forms (the `sureforms_form` CPT requires
11 * `manage_options` for create/edit, so the nudge is gated on the same
12 * cap — otherwise we would surface a "Create Form" CTA to users who
13 * cannot reach the form-creation flow).
14 * - Current screen is the block editor and not the SureForms form CPT.
15 * - The current post does not have an active dismissal recorded against
16 * it (dismissals are stored as post meta so each post tracks its own
17 * state — dismissing on one page does not silence the nudge on others).
18 *
19 * The remaining rule — "current post must not already contain a srfm/form
20 * block" — is evaluated in JS against live editor state.
21 *
22 * @package sureforms.
23 */
24
25 namespace SRFM\Inc\Admin;
26
27 use SRFM\Inc\Helper;
28 use SRFM\Inc\Traits\Get_Instance;
29
30 if ( ! defined( 'ABSPATH' ) ) {
31 exit;
32 }
33
34 /**
35 * Editor nudge handler.
36 *
37 * @since 2.8.2
38 */
39 class Editor_Nudge {
40 use Get_Instance;
41
42 /**
43 * Post meta key that stores the dismissed timestamp for a given post.
44 *
45 * Per-post (rather than per-user) so dismissing the nudge on one
46 * page does not silence it on every other page in the same site.
47 *
48 * @since 2.8.2
49 */
50 public const DISMISS_META_KEY = 'srfm_editor_nudge_dismissed';
51
52 /**
53 * Nonce action used by the dismiss AJAX endpoint.
54 *
55 * @since 2.8.2
56 */
57 public const NONCE_ACTION = 'srfm_editor_nudge_dismiss';
58
59 /**
60 * How long a dismissal suppresses the nudge on a single post, in seconds.
61 *
62 * Stored as a timestamp in post meta; once `time() - dismissed_at`
63 * exceeds this value the nudge can surface again on that post.
64 * Prevents a single mis-click from hiding the banner forever on a
65 * post with no recovery path.
66 *
67 * @since 2.8.2
68 */
69 public const DISMISS_EXPIRY_SECONDS = 90 * DAY_IN_SECONDS;
70
71 /**
72 * Constructor.
73 *
74 * @since 2.8.2
75 */
76 public function __construct() {
77 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
78 add_action( 'wp_ajax_srfm_editor_nudge_dismiss', [ $this, 'handle_dismiss' ] );
79 }
80
81 /**
82 * Decide whether the nudge script should be loaded for the current request.
83 *
84 * @since 2.8.2
85 * @return bool
86 */
87 public function allow_load() {
88 if ( ! is_admin() ) {
89 return false;
90 }
91
92 // Gate on the cap required to actually manage SureForms forms
93 // (`manage_options` per the CPT registration in inc/post-types.php).
94 // Pass the cap explicitly rather than relying on the helper default
95 // so the rule survives any future change to Helper::current_user_can().
96 // Editor / Author / Subscriber roles do NOT have manage_options on a
97 // stock WordPress install, so they will always fail this check.
98 if ( ! Helper::current_user_can( 'manage_options' ) ) {
99 return false;
100 }
101
102 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
103
104 if ( ! $screen || ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
105 return false;
106 }
107
108 // Skip on the SureForms form editor itself.
109 if ( SRFM_FORMS_POST_TYPE === $screen->post_type ) {
110 return false;
111 }
112
113 // Skip if the current post has an active dismissal recorded against it.
114 // Best-effort — when no post is in scope (e.g. the Site Editor) we
115 // still enqueue and let the JS layer surface or hide based on live
116 // editor state.
117 $post_id = $this->get_current_post_id();
118 if ( $post_id > 0 && $this->is_dismissal_active( $post_id ) ) {
119 return false;
120 }
121
122 return true;
123 }
124
125 /**
126 * Whether the given post has an active dismissal within the expiry window.
127 *
128 * Treats any non-numeric / zero / future-timestamp value as inactive,
129 * and rolls over once the recorded timestamp is older than
130 * `DISMISS_EXPIRY_SECONDS`.
131 *
132 * @since 2.8.2
133 * @param int $post_id Post ID to check.
134 * @return bool
135 */
136 public function is_dismissal_active( $post_id ) {
137 $dismissed_at = Helper::get_integer_value( get_post_meta( $post_id, self::DISMISS_META_KEY, true ) );
138
139 if ( $dismissed_at <= 0 ) {
140 return false;
141 }
142
143 return time() - $dismissed_at < self::DISMISS_EXPIRY_SECONDS;
144 }
145
146 /**
147 * Enqueue the nudge script when allowed.
148 *
149 * @since 2.8.2
150 * @return void
151 */
152 public function enqueue_scripts() {
153 if ( ! $this->allow_load() ) {
154 return;
155 }
156
157 $handle = SRFM_SLUG . '-editor-nudge';
158 $asset_path = SRFM_DIR . 'assets/build/editorNudge.asset.php';
159 $asset = file_exists( $asset_path )
160 ? include $asset_path
161 : [
162 'dependencies' => [ 'wp-data', 'wp-i18n', 'wp-dom-ready' ],
163 'version' => SRFM_VER,
164 ];
165
166 wp_enqueue_script(
167 $handle,
168 SRFM_URL . 'assets/build/editorNudge.js',
169 $asset['dependencies'],
170 $asset['version'],
171 true
172 );
173
174 $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min';
175 $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified';
176
177 wp_enqueue_style(
178 $handle,
179 SRFM_URL . 'assets/css/' . $dir_name . '/editor-nudge' . $file_prefix . '.css',
180 [],
181 SRFM_VER
182 );
183
184 // Editor nudge UTM attribution — start.
185 // Tag the "Create Form" CTA with the same UTM scheme used by other
186 // SureForms nudge links so the destination flow can attribute the
187 // click. Only fills keys not already present in the URL.
188 $create_form_url = admin_url( 'admin.php?page=add-new-form' );
189 $nudge_utm = [
190 'utm_source' => 'sureforms_plugin',
191 'utm_medium' => 'editor_nudge',
192 'utm_campaign' => 'core_plugin',
193 ];
194 $existing_args = [];
195 $query_string = wp_parse_url( $create_form_url, PHP_URL_QUERY );
196 if ( is_string( $query_string ) && '' !== $query_string ) {
197 parse_str( $query_string, $existing_args );
198 }
199 $missing_utm = array_diff_key( $nudge_utm, $existing_args );
200 if ( ! empty( $missing_utm ) ) {
201 $create_form_url = add_query_arg( $missing_utm, $create_form_url );
202 }
203 // Editor nudge UTM attribution — end.
204
205 wp_localize_script(
206 $handle,
207 'srfm_editor_nudge',
208 [
209 'ajax_url' => admin_url( 'admin-ajax.php' ),
210 'nonce' => wp_create_nonce( self::NONCE_ACTION ),
211 'create_form_url' => $create_form_url,
212 'message' => __( 'Hey! It looks like you\'re creating a form. Build a ready-to-use form in under 30 seconds with SureForms AI, with no extra setup required.', 'sureforms' ),
213 'button_label' => __( 'Create Form', 'sureforms' ),
214 'logo_url' => SRFM_URL . 'admin/assets/sureforms-logo.png',
215 ]
216 );
217
218 Helper::register_script_translations( $handle );
219 }
220
221 /**
222 * AJAX handler to persist the dismissed state for the post being edited.
223 *
224 * @since 2.8.2
225 * @return void
226 */
227 public function handle_dismiss() {
228 if ( ! Helper::current_user_can( 'manage_options' ) ) {
229 wp_send_json_error(
230 [ 'message' => __( 'You are not allowed to perform this action.', 'sureforms' ) ],
231 403
232 );
233 }
234
235 if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
236 wp_send_json_error(
237 [ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
238 400
239 );
240 }
241
242 $post_id = isset( $_POST['post_id'] )
243 ? Helper::get_integer_value( sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) )
244 : 0;
245
246 $post = $post_id > 0 ? get_post( $post_id ) : null;
247 if ( ! $post ) {
248 wp_send_json_error(
249 [ 'message' => __( 'Invalid post.', 'sureforms' ) ],
250 400
251 );
252 }
253
254 // The nudge never surfaces on the SureForms form CPT, so dismissals
255 // for it are never legitimate — reject before touching post meta.
256 if ( SRFM_FORMS_POST_TYPE === $post->post_type ) {
257 wp_send_json_error(
258 [ 'message' => __( 'Invalid post.', 'sureforms' ) ],
259 400
260 );
261 }
262
263 // Granular per-post authorization — `manage_options` alone is not
264 // enough; the requesting user must also be able to edit THIS post.
265 if ( ! current_user_can( 'edit_post', $post_id ) ) {
266 wp_send_json_error(
267 [ 'message' => __( 'You cannot edit this post.', 'sureforms' ) ],
268 403
269 );
270 }
271
272 update_post_meta( $post_id, self::DISMISS_META_KEY, time() );
273
274 wp_send_json_success();
275 }
276
277 /**
278 * Resolve the post ID being edited on the current admin request.
279 *
280 * @since 2.8.2
281 * @return int Post ID, or 0 when no post is in scope.
282 */
283 protected function get_current_post_id() {
284 global $post;
285 if ( $post instanceof \WP_Post && $post->ID > 0 ) {
286 return (int) $post->ID;
287 }
288 return 0;
289 }
290 }
291