PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / trunk
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz vtrunk
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 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 / generate-form-markup.php
sureforms / inc Last commit date
abilities 3 weeks ago admin 3 months ago ai-form-builder 1 month ago blocks 2 months ago compatibility 3 weeks ago database 3 weeks ago email 3 weeks ago fields 3 weeks ago global-settings 1 month ago lib 1 month ago migrator 2 months ago page-builders 3 weeks ago payments 3 weeks ago single-form-settings 2 months ago traits 2 months ago activator.php 1 year ago admin-ajax.php 2 months ago background-process.php 9 months ago create-new-form.php 3 months ago duplicate-form.php 3 months ago entries.php 3 weeks ago events-scheduler.php 2 years ago export.php 3 months ago field-validation.php 3 weeks ago form-restriction.php 2 months ago form-styling.php 1 month ago form-submit.php 4 days ago forms-data.php 5 months ago frontend-assets.php 4 days ago generate-form-markup.php 1 week ago gutenberg-hooks.php 1 week ago helper.php 4 days ago learn.php 4 months ago onboarding.php 2 months ago post-types.php 1 week ago rest-api.php 1 week ago smart-tags.php 4 days ago submit-token.php 4 months ago translatable.php 1 month ago updater-callbacks.php 3 weeks ago updater.php 3 weeks ago
generate-form-markup.php
1490 lines
1 <?php
2 /**
3 * Sureforms Generate Form Class file.
4 *
5 * @package sureforms.
6 * @since 0.0.1
7 */
8
9 namespace SRFM\Inc;
10
11 use SRFM\Inc\Compatibility\Multilingual\Multilingual_Manager;
12 use SRFM\Inc\Compatibility\Multilingual\String_Translator;
13 use SRFM\Inc\Traits\Get_Instance;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Load Defaults Class.
21 *
22 * @since 0.0.1
23 */
24 class Generate_Form_Markup {
25 use Get_Instance;
26
27 /**
28 * Current block attributes for the form being rendered.
29 * Used by child blocks (like inline button) to access parent form's embed styling.
30 *
31 * @var array<string,mixed>
32 * @since 2.7.0
33 */
34 private static $current_block_attrs = [];
35
36 /**
37 * IDs of the forms known to be on the current request, keyed by form ID.
38 *
39 * Seeded at the `wp` hook (collect_queried_form_ids(), before any output) by
40 * parsing the queried post, and added to at render time by get_form_markup().
41 * The seed is load-bearing: on modern themes the admin bar renders at
42 * wp_body_open (priority 0) — BEFORE the_content — so the render-time registry
43 * alone would be empty when the node is built.
44 *
45 * @var array<int,bool>
46 * @since 2.12.3
47 */
48 private static $rendered_form_ids = [];
49
50 /**
51 * Constructor
52 *
53 * @since 0.0.1
54 */
55 public function __construct() {
56 add_action( 'rest_api_init', [ $this, 'register_custom_endpoint' ] );
57 // Seed the form registry from the queried post before any output, so the
58 // admin bar (which renders at wp_body_open, before the_content) has the list.
59 add_action( 'wp', [ $this, 'collect_queried_form_ids' ] );
60 // Frontend admin-bar "Entries" deep-link. Priority 100 mirrors the
61 // existing "Edit Form" node in Post_Types.
62 add_action( 'admin_bar_menu', [ $this, 'add_entries_admin_bar_node' ], 100 );
63 }
64
65 /**
66 * Seed the rendered-form registry from the queried singular post's content,
67 * before any output.
68 *
69 * The admin bar renders at wp_body_open (priority 0) on modern themes — before
70 * the_content — so relying on the render-time registry alone would leave the
71 * node empty on essentially every embed. Parsing the queried post here (srfm/form
72 * blocks incl. reusable/synced patterns, and [sureforms] shortcodes, via the
73 * shared Form_Styling helper) covers those; get_form_markup() then adds anything
74 * a static parse can't see (page builders, FSE template parts).
75 *
76 * @since 2.12.3
77 * @return void
78 */
79 public function collect_queried_form_ids() {
80 if ( is_admin() || ! is_singular() ) {
81 return;
82 }
83
84 // The only consumer is the admin-bar node, which bails for anyone without
85 // manage_options. Without this guard every anonymous front-end request ran
86 // parse_blocks() plus recursive get_post() expansion of synced patterns for a
87 // feature it could never see. The current user is already resolved at `wp`.
88 if ( ! is_admin_bar_showing() || ! Helper::current_user_can() ) {
89 return;
90 }
91
92 $post_id = absint( get_queried_object_id() );
93 if ( 0 === $post_id ) {
94 return;
95 }
96
97 // 'raw' context: the default 'display' context applies the post_content filter,
98 // so the parsed list could disagree with Form_Styling::should_skip_frontend_styles(),
99 // which reads raw.
100 $content = Helper::get_string_value( get_post_field( 'post_content', $post_id, 'raw' ) );
101 foreach ( Form_Styling::get_form_ids_from_content( $content ) as $form_id ) {
102 $fid = absint( $form_id );
103 if ( $fid > 0 ) {
104 self::$rendered_form_ids[ $fid ] = true;
105 }
106 }
107 }
108
109 /**
110 * Get the current block attributes.
111 *
112 * @return array<string,mixed>
113 * @since 2.7.0
114 */
115 public static function get_current_block_attrs() {
116 return self::$current_block_attrs;
117 }
118
119 /**
120 * Add an "Entries" node to the frontend admin bar on any page that contains a
121 * SureForms form, deep-linking to the Entries admin page pre-filtered to that
122 * form. The form list comes from collect_queried_form_ids() (seeded at `wp`)
123 * plus the render-time registry.
124 *
125 * ACTUAL COVERAGE: srfm/form blocks, synced/reusable patterns (core/block) and
126 * [sureforms] shortcodes in the queried post's content, plus a singular form CPT
127 * page. Page builders that store layout outside post_content (Elementor in
128 * _elementor_data, Bricks in _bricks_page_content_*) and FSE template parts are
129 * NOT covered: the render-time registry is written during the_content, which on
130 * block themes runs after wp_admin_bar_render() at wp_body_open, so the node is
131 * already built. On classic themes those paths happen to work via core's wp_footer
132 * fallback, which makes the feature silently theme-dependent. Use the
133 * `srfm_admin_bar_entries_form_ids` filter to contribute builder-sourced IDs until
134 * early builder detection lands. With multiple forms the node becomes a
135 * submenu (one child per form); the parent then links to the unfiltered page.
136 *
137 * Runs on admin_bar_menu, which fires as the bar renders (wp_body_open on modern
138 * themes). Gated to users who can view the Entries page (the same
139 * `manage_options` capability the admin page and entries REST endpoints use).
140 *
141 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance.
142 * @since 2.12.3
143 * @return void
144 */
145 public function add_entries_admin_bar_node( $wp_admin_bar ) {
146 // Frontend only, and only when the bar is actually shown for this user.
147 if ( is_admin() || ! is_admin_bar_showing() || ! $wp_admin_bar instanceof \WP_Admin_Bar ) {
148 return;
149 }
150
151 // Match who can view entries (admin page + entries REST capability).
152 if ( ! Helper::current_user_can() ) {
153 return;
154 }
155
156 $form_ids = array_map( 'absint', array_keys( self::$rendered_form_ids ) );
157
158 // Fallback for a form's own singular page if nothing was recorded.
159 if ( empty( $form_ids ) && is_singular( SRFM_FORMS_POST_TYPE ) ) {
160 $singular_id = absint( get_the_ID() );
161 if ( $singular_id > 0 ) {
162 $form_ids[] = $singular_id;
163 }
164 }
165
166 /**
167 * Filter the form IDs offered in the admin-bar Entries node. Lets sources a
168 * content parse / render can't see contribute — Elementor (_elementor_data),
169 * Bricks (_bricks_page_content_*), FSE template parts, or Pro's
170 * [srfm_show_entries] shortcode.
171 *
172 * @since 2.12.3
173 * @param array<int> $form_ids Form IDs detected on the current request.
174 */
175 $form_ids = array_map( 'absint', (array) apply_filters( 'srfm_admin_bar_entries_form_ids', $form_ids ) );
176
177 // Keep only real SureForms forms. The [sureforms] shortcode accepts any
178 // published post ID, so esc_html() below must not be the only barrier
179 // against a hostile post title (e.g. authored by an Editor with unfiltered_html).
180 $form_ids = array_values(
181 array_unique(
182 array_filter(
183 $form_ids,
184 static function ( $fid ) {
185 return $fid > 0 && SRFM_FORMS_POST_TYPE === get_post_type( $fid );
186 }
187 )
188 )
189 );
190 if ( empty( $form_ids ) ) {
191 return;
192 }
193
194 $entries_base = admin_url( 'admin.php?page=' . SRFM_ENTRIES );
195 $node_id = 'srfm-entries';
196 $icon = '<span class="ab-icon dashicons dashicons-list-view" style="line-height:1.2;margin-right:4px;"></span>';
197
198 // Single form — link straight to its filtered entries.
199 if ( 1 === count( $form_ids ) ) {
200 $wp_admin_bar->add_node(
201 [
202 'id' => $node_id,
203 'title' => $icon . '<span class="ab-label">' . esc_html__( 'Entries', 'sureforms' ) . '</span>',
204 'href' => esc_url( $entries_base . '#/?form=' . $form_ids[0] ),
205 // Core esc_attr()s meta['title'], so pass it unescaped here.
206 'meta' => [ 'title' => __( 'View entries for this form', 'sureforms' ) ],
207 ]
208 );
209 return;
210 }
211
212 // Multiple forms — parent links to unfiltered Entries, one child per form.
213 $wp_admin_bar->add_node(
214 [
215 'id' => $node_id,
216 'title' => $icon . '<span class="ab-label">' . esc_html__( 'Entries', 'sureforms' ) . '</span>',
217 'href' => esc_url( $entries_base ),
218 'meta' => [ 'title' => __( 'View form entries', 'sureforms' ) ],
219 ]
220 );
221
222 // Cap the submenu; the parent's unfiltered link covers the overflow so a page
223 // with many forms can't blow past the (non-scrolling) admin bar.
224 foreach ( array_slice( $form_ids, 0, 10 ) as $form_id ) {
225 $title = get_the_title( $form_id );
226 // get_the_title() runs the_title filters that may inject markup, and
227 // WP_Admin_Bar does not escape node titles — strip tags and escape here.
228 $title = '' !== $title
229 ? esc_html( wp_strip_all_tags( $title ) )
230 /* translators: %d: form ID. */
231 : esc_html( sprintf( __( 'Form #%d', 'sureforms' ), $form_id ) );
232
233 $wp_admin_bar->add_node(
234 [
235 'id' => $node_id . '-' . $form_id,
236 'parent' => $node_id,
237 'title' => $title,
238 'href' => esc_url( $entries_base . '#/?form=' . $form_id ),
239 ]
240 );
241 }
242 }
243
244 /**
245 * Add custom API Route to generate form markup.
246 *
247 * @return void
248 * @since 0.0.1
249 */
250 public function register_custom_endpoint() {
251 register_rest_route(
252 'sureforms/v1',
253 '/generate-form-markup',
254 [
255 'methods' => 'GET',
256 'callback' => [ $this, 'render_form_markup_endpoint' ],
257 'permission_callback' => [ $this, 'render_form_markup_permissions_check' ],
258 'args' => [
259 'id' => [
260 'required' => true,
261 'type' => 'integer',
262 'sanitize_callback' => 'absint',
263 'validate_callback' => static function ( $value ) {
264 return absint( $value ) > 0;
265 },
266 ],
267 ],
268 ]
269 );
270 }
271
272 /**
273 * Permission check for the form-markup endpoint.
274 *
275 * The endpoint exists for one purpose: rendering the editor preview when a user
276 * picks a form in the srfm/form block. So the caller must at least be able to
277 * edit content. A nonce is not sufficient — `srfm_form_markup` is minted in
278 * enqueue_block_editor_assets, so passing it proves only that the caller reached
279 * the editor, never what they are allowed to read.
280 *
281 * @since 2.12.3
282 * @return bool|\WP_Error True when allowed, WP_Error otherwise.
283 */
284 public function render_form_markup_permissions_check() {
285 if ( ! current_user_can( 'edit_posts' ) ) {
286 return new \WP_Error(
287 'srfm_rest_cannot_render_form',
288 __( 'Sorry, you are not allowed to render form markup.', 'sureforms' ),
289 [ 'status' => rest_authorization_required_code() ]
290 );
291 }
292
293 return true;
294 }
295
296 /**
297 * Render the requested form for the block-editor preview.
298 *
299 * Constrains the requested ID to a SureForms form, and to one the caller is
300 * allowed to see: published forms are already public, anything else (draft,
301 * pending, private, trashed) needs the SureForms forms capability.
302 *
303 * @param \WP_REST_Request<array<string,mixed>> $request REST request.
304 *
305 * @since 2.12.3
306 * @return string|\WP_Error Form markup, or WP_Error when the form is not renderable for this caller.
307 */
308 public function render_form_markup_endpoint( $request ) {
309 $form_id = Helper::get_integer_value( $request->get_param( 'id' ) );
310 $form = $form_id > 0 ? get_post( $form_id ) : null;
311
312 if ( ! $form instanceof \WP_Post || SRFM_FORMS_POST_TYPE !== $form->post_type ) {
313 return new \WP_Error(
314 'srfm_rest_form_not_found',
315 __( 'No form was found with the given ID.', 'sureforms' ),
316 [ 'status' => 404 ]
317 );
318 }
319
320 if ( 'publish' !== $form->post_status && ! Helper::current_user_can() ) {
321 return new \WP_Error(
322 'srfm_rest_cannot_render_form',
323 __( 'Sorry, you are not allowed to render this form.', 'sureforms' ),
324 [ 'status' => rest_authorization_required_code() ]
325 );
326 }
327
328 return Helper::get_string_value( self::get_form_markup( $form_id ) );
329 }
330
331 /**
332 * Handle Form status
333 *
334 * @param int|string $id Contains form ID.
335 * @param bool $show_title_current_page Boolean to srfm-show/srfm-hide form title.
336 * @param string $sf_classname additional class_name.
337 * @param string $post_type Contains post type.
338 * @param bool $do_blocks Boolean to enable/disable parsing dynamic blocks.
339 * @param array<mixed> $block_attrs Block attributes for per-embed styling.
340 *
341 * @return string|false
342 * @since 0.0.1
343 */
344 public static function get_form_markup( $id, $show_title_current_page = true, $sf_classname = '', $post_type = 'post', $do_blocks = false, $block_attrs = [] ) {
345 // SECURITY INVARIANT — a renderer must never read the request to decide what to
346 // render. The caller's `$id` is the only source of truth here; the REST route
347 // owns request parsing (see render_form_markup_endpoint). Reintroducing any
348 // query-string override would let a URL change which form a page renders.
349 $id = Helper::get_integer_value( $id );
350
351 // Check for any form restrictions.
352 $form_id = Helper::get_integer_value( $id );
353
354 // Additively record the form for the admin-bar "Entries" node. The registry
355 // is primarily seeded at `wp` (collect_queried_form_ids) because the bar
356 // renders before the_content; this render-time write is what covers paths a
357 // content parse can't see — page builders (Elementor/Bricks) and FSE template
358 // parts. Recorded before the restriction check: a restricted form is still on
359 // the page, and its admin still wants its entries link.
360 if ( $form_id > 0 ) {
361 self::$rendered_form_ids[ $form_id ] = true;
362 }
363
364 if ( Form_Restriction::is_form_restricted( $form_id ) ) {
365 return Form_Restriction::display_form_restriction_message( $form_id );
366 }
367
368 // Store block_attrs for child blocks (like inline button) to access.
369 self::$current_block_attrs = $block_attrs;
370
371 do_action( 'srfm_localize_conditional_logic_data', $id );
372 $post = get_post( Helper::get_integer_value( $id ) );
373
374 $content = '';
375 $form_blocks = [];
376
377 $active_plugins = Helper::get_array_value( get_option( 'active_plugins', [] ) );
378 $is_learndash_active = in_array( 'sfwd-lms/sfwd_lms.php', $active_plugins, true );
379
380 if ( $is_learndash_active ) {
381 $do_blocks = true;
382 }
383
384 if ( $post && ! empty( $post->post_content ) ) {
385 // Filter to get the post content for the form.
386 $post_content = apply_filters( 'srfm_get_form_post_content', $post->post_content, $id );
387
388 // Pre-translate block-attribute strings (labels, placeholders, options, etc.)
389 // before rendering, so the visitor's chosen language is honoured. Returns the
390 // translated markup plus the parsed top-level blocks so we can derive the block
391 // count without re-parsing the rendered HTML. No-op when no provider is active.
392 [ $post_content, $form_blocks ] = String_Translator::get_instance()->translate_form_content_with_blocks( (int) $id, Helper::get_string_value( $post_content ), $post );
393
394 if ( ! empty( $do_blocks ) ) {
395 $content = do_blocks( $post_content );
396 } else {
397 $content = apply_filters( 'the_content', $post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- wordpress hook
398 }
399 }
400
401 // Reuse the translator's parse on the multilingual path; otherwise parse once here
402 // (single-language path). Either way the content is parsed exactly once, never three times.
403 $form_blocks = ! empty( $form_blocks ) ? $form_blocks : parse_blocks( $content );
404 $block_count = count( $form_blocks );
405 $current_post_type = get_post_type();
406
407 // When enabled, the form renders without the SureForms inline CSS variables so
408 // the site's own CSS fully controls its appearance. Per-form Custom CSS still applies.
409 // Read ONCE through the canonical checker so the `srfm_disable_default_styles`
410 // filter runs a single time per render and governs the enqueue path, the
411 // marker class and the inline CSS guard alike.
412 $disable_default_styles = Form_Styling::is_default_styling_disabled( $id );
413
414 // load all the frontend assets. Skips the SureForms stylesheets when the form has default styling disabled.
415 Frontend_Assets::enqueue_scripts_and_styles( $disable_default_styles );
416
417 ob_start();
418 if ( '' !== $id && 0 !== $block_count ) {
419
420 // Create unique container ID using blockId if available (for multiple embeds of same form).
421 // Base class (without blockId) is needed for JS compatibility - frontend.js and phone.js use form-id attribute to construct selectors.
422 $base_container_class = 'srfm-form-container-' . Helper::get_string_value( $id );
423 $block_id_suffix = ! empty( $block_attrs['blockId'] ) ? '-' . Helper::get_string_value( $block_attrs['blockId'] ) : '';
424 $container_id = $base_container_class . $block_id_suffix;
425 $form_styling = get_post_meta( $id, '_srfm_forms_styling', true );
426 $form_styling = ! empty( $form_styling ) && is_array( $form_styling ) ? $form_styling : [];
427
428 // Apply per-embed styling customization when formTheme is not 'inherit'.
429 if ( Form_Styling::has_custom_styling( $block_attrs ) ) {
430 $form_styling = Form_Styling::map_block_attrs_to_styling( $form_styling, $block_attrs );
431 }
432
433 // Background Settings.
434 $bg_type = $form_styling['bg_type'] ?? 'color';
435 $bg_color = $form_styling['bg_color'] ?? '';
436 $bg_image = $form_styling['bg_image'] ?? '';
437 $bg_image_position = $form_styling['bg_image_position'] ?? [];
438 $bg_image_attachment = $form_styling['bg_image_attachment'] ?? 'scroll';
439 $bg_image_repeat = $form_styling['bg_image_repeat'] ?? 'no-repeat';
440 $bg_image_size = $form_styling['bg_image_size'] ?? 'cover';
441 $bg_image_size_custom = $form_styling['bg_image_size_custom'] ?? 100;
442 $bg_image_size_custom_unit = $form_styling['bg_image_size_custom_unit'] ?? '%';
443 $bg_gradient = $form_styling['bg_gradient'] ?? 'linear-gradient(90deg, #FFC9B2 0%, #C7CBFF 100%)';
444 $gradient_type = $form_styling['gradient_type'] ?? 'basic'; // Basic or advanced.
445 $is_advanced_gradient = 'advanced' === $gradient_type ? true : false;
446 $bg_gradient_type = $is_advanced_gradient && isset( $form_styling['bg_gradient_type'] ) ? $form_styling['bg_gradient_type'] : 'linear'; // linear or radial gradient.
447 $bg_gradient_color_1 = $is_advanced_gradient && isset( $form_styling['bg_gradient_color_1'] ) ? $form_styling['bg_gradient_color_1'] : '';
448 $bg_gradient_color_2 = $is_advanced_gradient && isset( $form_styling['bg_gradient_color_2'] ) ? $form_styling['bg_gradient_color_2'] : '';
449 $bg_gradient_location_1 = $is_advanced_gradient && isset( $form_styling['bg_gradient_location_1'] ) ? $form_styling['bg_gradient_location_1'] : '';
450 $bg_gradient_location_2 = $is_advanced_gradient && isset( $form_styling['bg_gradient_location_2'] ) ? $form_styling['bg_gradient_location_2'] : '';
451 $bg_gradient_angle = $is_advanced_gradient && isset( $form_styling['bg_gradient_angle'] ) ? $form_styling['bg_gradient_angle'] : '';
452 // Overlay Settings.
453 $overlay_type = $form_styling['bg_gradient_overlay_type'] ?? '';
454 $overlay_size = $form_styling['bg_overlay_size'] ?? 'cover';
455 $overlay_opacity = $form_styling['bg_overlay_opacity'] ?? 1;
456 $overlay_color = $form_styling['bg_image_overlay_color'] ?? '';
457 $overlay_image = $form_styling['bg_overlay_image'] ?? '';
458 $overlay_position = $form_styling['bg_overlay_position'] ?? [];
459 $overlay_attachment = $form_styling['bg_overlay_attachment'] ?? 'scroll';
460 $overlay_repeat = $form_styling['bg_overlay_repeat'] ?? 'no-repeat';
461 $overlay_blend_mode = $form_styling['bg_overlay_blend_mode'] ?? 'normal';
462 // Gradient Overlay.
463 $bg_overlay_gradient = $form_styling['bg_overlay_gradient'] ?? 'linear-gradient(90deg, #FFC9B2 0%, #C7CBFF 100%)';
464 $overlay_gradient_type = $form_styling['overlay_gradient_type'] ?? 'basic'; // Basic or advanced.
465 $is_overlay_advanced_gradient = 'advanced' === $overlay_gradient_type ? true : false;
466 $bg_overlay_gradient_type = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_type'] ) ? $form_styling['bg_overlay_gradient_type'] : 'linear';
467 $bg_overlay_gradient_color_1 = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_color_1'] ) ? $form_styling['bg_overlay_gradient_color_1'] : '';
468 $bg_overlay_gradient_color_2 = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_color_2'] ) ? $form_styling['bg_overlay_gradient_color_2'] : '';
469 $bg_overlay_gradient_location_1 = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_location_1'] ) ? $form_styling['bg_overlay_gradient_location_1'] : '';
470 $bg_overlay_gradient_location_2 = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_location_2'] ) ? $form_styling['bg_overlay_gradient_location_2'] : '';
471 $bg_overlay_gradient_angle = $is_overlay_advanced_gradient && isset( $form_styling['bg_overlay_gradient_angle'] ) ? $form_styling['bg_overlay_gradient_angle'] : '';
472 // Embed Form Settings.
473 $form = [
474 // Padding.
475 'padding_top' => isset( $form_styling['form_padding_top'] ) ? floatval( $form_styling['form_padding_top'] ) : 0,
476 'padding_right' => isset( $form_styling['form_padding_right'] ) ? floatval( $form_styling['form_padding_right'] ) : 0,
477 'padding_bottom' => isset( $form_styling['form_padding_bottom'] ) ? floatval( $form_styling['form_padding_bottom'] ) : 0,
478 'padding_left' => isset( $form_styling['form_padding_left'] ) ? floatval( $form_styling['form_padding_left'] ) : 0,
479 'padding_unit' => isset( $form_styling['form_padding_unit'] ) ? Helper::get_string_value( $form_styling['form_padding_unit'] ) : 'px',
480 // Border Radius.
481 'border_radius_top' => isset( $form_styling['form_border_radius_top'] ) ? floatval( $form_styling['form_border_radius_top'] ) : 0,
482 'border_radius_right' => isset( $form_styling['form_border_radius_right'] ) ? floatval( $form_styling['form_border_radius_right'] ) : 0,
483 'border_radius_bottom' => isset( $form_styling['form_border_radius_bottom'] ) ? floatval( $form_styling['form_border_radius_bottom'] ) : 0,
484 'border_radius_left' => isset( $form_styling['form_border_radius_left'] ) ? floatval( $form_styling['form_border_radius_left'] ) : 0,
485 'border_radius_unit' => isset( $form_styling['form_border_radius_unit'] ) ? Helper::get_string_value( $form_styling['form_border_radius_unit'] ) : 'px',
486 ];
487 // Instant Form Settings.
488 $instant_form = [
489 // Padding.
490 'padding_top' => isset( $form_styling['instant_form_padding_top'] ) ? floatval( $form_styling['instant_form_padding_top'] ) : 32,
491 'padding_right' => isset( $form_styling['instant_form_padding_right'] ) ? floatval( $form_styling['instant_form_padding_right'] ) : 32,
492 'padding_bottom' => isset( $form_styling['instant_form_padding_bottom'] ) ? floatval( $form_styling['instant_form_padding_bottom'] ) : 32,
493 'padding_left' => isset( $form_styling['instant_form_padding_left'] ) ? floatval( $form_styling['instant_form_padding_left'] ) : 32,
494 'padding_unit' => isset( $form_styling['instant_form_padding_unit'] ) ? Helper::get_string_value( $form_styling['instant_form_padding_unit'] ) : 'px',
495 // Border Radius.
496 'border_radius_top' => isset( $form_styling['instant_form_border_radius_top'] ) ? floatval( $form_styling['instant_form_border_radius_top'] ) : 12,
497 'border_radius_right' => isset( $form_styling['instant_form_border_radius_right'] ) ? floatval( $form_styling['instant_form_border_radius_right'] ) : 12,
498 'border_radius_bottom' => isset( $form_styling['instant_form_border_radius_bottom'] ) ? floatval( $form_styling['instant_form_border_radius_bottom'] ) : 12,
499 'border_radius_left' => isset( $form_styling['instant_form_border_radius_left'] ) ? floatval( $form_styling['instant_form_border_radius_left'] ) : 12,
500 'border_radius_unit' => isset( $form_styling['instant_form_border_radius_unit'] ) ? Helper::get_string_value( $form_styling['instant_form_border_radius_unit'] ) : 'px',
501 ];
502
503 if ( 'custom' === $overlay_size ) {
504 $bg_overlay_custom_size = $form_styling['bg_overlay_custom_size'] ?? 100;
505 $bg_overlay_custom_size_unit = $form_styling['bg_overlay_custom_size_unit'] ?? '%';
506 $overlay_size = $bg_overlay_custom_size . $bg_overlay_custom_size_unit;
507 }
508
509 $background_classes = apply_filters( 'srfm_add_background_classes', Helper::get_background_classes( $bg_type, $overlay_type, $bg_image ), $id, $block_attrs );
510
511 $neve_theme_margin_class_name = 'srfm-neve-theme-add-margin-bottom';
512 $theme_name = wp_get_theme()->get( 'Name' );
513
514 $form_classes = [
515 'srfm-form-container',
516 $base_container_class, // Base class for JS compatibility (frontend.js, phone.js).
517 ! empty( $block_id_suffix ) ? $container_id : '', // Unique class for CSS scoping when blockId exists.
518 $sf_classname,
519 'Neve' === $theme_name ? $neve_theme_margin_class_name : '', // compatibility with Neve theme for margin between main content and footer.
520 $disable_default_styles ? 'srfm-styling-none' : '', // Marker class when default styling is disabled, so custom CSS can target the state.
521 $background_classes,
522 ];
523
524 $custom_added_classes = Helper::get_meta_value( $id, '_srfm_additional_classes' );
525 if ( ! empty( $custom_added_classes ) && is_string( $custom_added_classes ) ) {
526 $custom_added_classes = explode( ' ', $custom_added_classes );
527 foreach ( $custom_added_classes as $class ) {
528 if ( Helper::is_valid_css_class_name( $class ) ) {
529 $form_classes[] = $class;
530 }
531 }
532 }
533
534 $page_break_settings = defined( 'SRFM_PRO_VER' ) && apply_filters( 'srfm_use_page_break_layout', true ) ? get_post_meta( $id, '_srfm_page_break_settings', true ) : [];
535 $page_break_settings = ! empty( $page_break_settings ) && is_array( $page_break_settings ) ? $page_break_settings : [];
536 $is_page_break = ! empty( $page_break_settings ) ? $page_break_settings['is_page_break'] : false;
537 $page_break_progress_type = ! empty( $page_break_settings ) ? $page_break_settings['progress_indicator_type'] : 'none';
538 $form_confirmation = get_post_meta( $id, '_srfm_form_confirmation' );
539 $confirmation_type = '';
540 $submission_action = '';
541 $success_url = '';
542 if ( is_array( $form_confirmation ) && isset( $form_confirmation[0][0] ) ) {
543 $confirmation_data = $form_confirmation[0][0];
544 $page_url = $confirmation_data['page_url'] ?? '';
545 $custom_url = $confirmation_data['custom_url'] ?? '';
546 $confirmation_type = $confirmation_data['confirmation_type'] ?? '';
547 $submission_action = $confirmation_data['submission_action'] ?? '';
548 $success_url = '';
549 if ( 'different page' === $confirmation_type ) {
550 $success_url = $page_url;
551 } elseif ( 'custom url' === $confirmation_type ) {
552 $success_url = $custom_url;
553 }
554 }
555
556 // Submit button.
557 $button_text = Helper::get_meta_value( $id, '_srfm_submit_button_text' );
558 $button_text = String_Translator::get_instance()->translate_submit_button( (int) $id, Helper::get_string_value( $button_text ) );
559 $submit_button_alignment = ! empty( $form_styling['submit_button_alignment'] ) ? $form_styling['submit_button_alignment'] : 'left';
560
561 if ( is_rtl() && ( 'left' === $submit_button_alignment || 'right' === $submit_button_alignment ) ) {
562 $submit_button_alignment = 'right' === $submit_button_alignment ? 'left' : 'right';
563 }
564
565 $btn_from_theme = Helper::get_meta_value( $id, '_srfm_inherit_theme_button' );
566 $is_inline_button = apply_filters( 'srfm_is_inline_button', Helper::get_meta_value( $id, '_srfm_is_inline_button' ) );
567 $security_type = Helper::get_meta_value( $id, '_srfm_captcha_security_type' );
568 $form_custom_css_meta = Helper::get_meta_value( $id, '_srfm_form_custom_css' );
569 $custom_css = ! empty( $form_custom_css_meta ) && is_string( $form_custom_css_meta ) ? $form_custom_css_meta : '';
570
571 $full = 'justify' === $submit_button_alignment ? true : false;
572 $recaptcha_version = 'g-recaptcha' === $security_type ? Helper::get_meta_value( $id, '_srfm_form_recaptcha' ) : '';
573 $srfm_cf_appearance_mode = '';
574 $srfm_cf_turnstile_site_key = '';
575 $srfm_hcaptcha_site_key = '';
576
577 $google_captcha_site_key = '';
578
579 if ( 'none' !== $security_type ) {
580 $global_setting_options = get_option( 'srfm_security_settings_options' );
581 } else {
582 $global_setting_options = [];
583 }
584
585 if ( is_array( $global_setting_options ) && 'cf-turnstile' === $security_type ) {
586 $srfm_cf_turnstile_site_key = $global_setting_options['srfm_cf_turnstile_site_key'] ?? '';
587 $srfm_cf_appearance_mode = $global_setting_options['srfm_cf_appearance_mode'] ?? 'auto';
588 }
589
590 if ( is_array( $global_setting_options ) && 'hcaptcha' === $security_type ) {
591 $srfm_hcaptcha_site_key = $global_setting_options['srfm_hcaptcha_site_key'] ?? '';
592 }
593
594 if ( is_array( $global_setting_options ) && 'g-recaptcha' === $security_type ) {
595 switch ( $recaptcha_version ) {
596 case 'v2-checkbox':
597 $google_captcha_site_key = $global_setting_options['srfm_v2_checkbox_site_key'] ?? '';
598 break;
599 case 'v2-invisible':
600 $google_captcha_site_key = $global_setting_options['srfm_v2_invisible_site_key'] ?? '';
601 break;
602 case 'v3-reCAPTCHA':
603 $google_captcha_site_key = $global_setting_options['srfm_v3_site_key'] ?? '';
604 break;
605 default:
606 break;
607 }
608 }
609
610 // Ensure $google_captcha_site_key is not empty, and if not, trim any leading or trailing whitespace.
611 $google_captcha_site_key = is_string( $google_captcha_site_key ) && ! empty( $google_captcha_site_key ) ? trim( $google_captcha_site_key ) : '';
612
613 $primary_color = $form_styling['primary_color'] ?? '';
614 $help_color_var = $form_styling['text_color'] ?? '';
615 $label_text_color = $form_styling['text_color_on_primary'] ?? '';
616 $field_spacing = $form_styling['field_spacing'] ?? 'small';
617
618 // New colors.
619
620 $primary_color_var = $primary_color ? $primary_color : '#046bd2';
621 $label_text_color_var = $label_text_color ? $label_text_color : '#111827';
622
623 $selected_size = Helper::get_css_vars( $field_spacing );
624
625 $should_show_submit_button = apply_filters(
626 'srfm_show_submit_button',
627 0 !== $block_count && ! $is_inline_button || $is_page_break,
628 $id
629 );
630
631 if ( ! $should_show_submit_button ) {
632 $form_classes[] = 'srfm-submit-button-hidden';
633 }
634
635 // The scoped Custom CSS below is for embedded views only: on the form's own
636 // single/instant view, templates/single-form.php already outputs the Custom
637 // CSS (unscoped) in <head> — emitting it here too would duplicate it.
638 $embed_custom_css = 'sureforms_form' !== $current_post_type ? $custom_css : '';
639 ?>
640 <div class="<?php echo esc_attr( implode( ' ', array_filter( $form_classes ) ) ); ?>">
641 <?php if ( ! $disable_default_styles || '' !== $embed_custom_css ) { // Nothing to print otherwise — avoid an empty style block. ?>
642 <style>
643 /* Need to check and remove the input variables related to the Style Tab. */
644 <?php echo esc_html( ".{$container_id}" ); ?> {
645 <?php if ( ! $disable_default_styles ) { ?>
646 /* New test variables */
647 --srfm-color-scheme-primary: <?php echo esc_html( $primary_color_var ); ?>;
648 --srfm-color-scheme-text-on-primary: <?php echo esc_html( $label_text_color_var ); ?>;
649 --srfm-color-scheme-text: <?php echo esc_html( $help_color_var ); ?>;
650 --srfm-quill-editor-color: <?php echo esc_html( $primary_color_var ); ?>;
651
652 --srfm-color-input-label: <?php echo esc_html( $help_color_var ); ?>;
653 --srfm-color-input-description: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
654 --srfm-color-input-placeholder: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.5 );
655 --srfm-color-input-text: <?php echo esc_html( $help_color_var ); ?>;
656 --srfm-color-input-prefix: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
657 --srfm-color-input-background: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.02 );
658 --srfm-color-input-background-hover: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
659 --srfm-color-input-background-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.07 );
660 --srfm-color-input-border: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.25 );
661 --srfm-color-input-border-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.15 );
662 --srfm-color-multi-choice-svg: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.7 );
663 --srfm-color-input-border-hover: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.65 );
664 --srfm-color-input-border-focus-glow: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.15 );
665 --srfm-color-input-selected: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.1 );
666 --srfm-btn-color-hover: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.9 );
667 --srfm-btn-color-disabled: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.25 );
668
669 /* Dropdown Variables */
670 --srfm-dropdown-input-background-hover: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
671 --srfm-dropdown-option-background-hover: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.10 );
672 --srfm-dropdown-option-background-selected: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
673 --srfm-dropdown-option-selected-icon: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
674 --srfm-dropdown-option-text-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.80 );
675 --srfm-dropdown-option-selected-text: <?php echo esc_html( $help_color_var ); ?>;
676 --srfm-dropdown-badge-background: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
677 --srfm-dropdown-badge-background-hover: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.10 );
678 --srfm-dropdown-menu-border-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.10 );
679 --srfm-dropdown-placeholder-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.50 );
680 --srfm-dropdown-icon-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
681 --srfm-dropdown-icon-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.25 );
682
683 /* Background Control Variables */
684 <?php
685 // Form Styles.
686 $styling_vars = [
687 // Instant Form Padding.
688 '--srfm-instant-form-padding-top' => sanitize_text_field( "{$instant_form['padding_top']}{$instant_form['padding_unit']}" ),
689 '--srfm-instant-form-padding-right' => sanitize_text_field( "{$instant_form['padding_right']}{$instant_form['padding_unit']}" ),
690 '--srfm-instant-form-padding-bottom' => sanitize_text_field( "{$instant_form['padding_bottom']}{$instant_form['padding_unit']}" ),
691 '--srfm-instant-form-padding-left' => sanitize_text_field( "{$instant_form['padding_left']}{$instant_form['padding_unit']}" ),
692 // Instant Form Border Radius.
693 '--srfm-instant-form-border-radius-top' => sanitize_text_field( "{$instant_form['border_radius_top']}{$instant_form['border_radius_unit']}" ),
694 '--srfm-instant-form-border-radius-right' => sanitize_text_field( "{$instant_form['border_radius_right']}{$instant_form['border_radius_unit']}" ),
695 '--srfm-instant-form-border-radius-bottom' => sanitize_text_field( "{$instant_form['border_radius_bottom']}{$instant_form['border_radius_unit']}" ),
696 '--srfm-instant-form-border-radius-left' => sanitize_text_field( "{$instant_form['border_radius_left']}{$instant_form['border_radius_unit']}" ),
697 // Embed Form Padding.
698 '--srfm-form-padding-top' => sanitize_text_field( "{$form['padding_top']}{$form['padding_unit']}" ),
699 '--srfm-form-padding-right' => sanitize_text_field( "{$form['padding_right']}{$form['padding_unit']}" ),
700 '--srfm-form-padding-bottom' => sanitize_text_field( "{$form['padding_bottom']}{$form['padding_unit']}" ),
701 '--srfm-form-padding-left' => sanitize_text_field( "{$form['padding_left']}{$form['padding_unit']}" ),
702 // Embed Form Border Radius.
703 '--srfm-form-border-radius-top' => sanitize_text_field( "{$form['border_radius_top']}{$form['border_radius_unit']}" ),
704 '--srfm-form-border-radius-right' => sanitize_text_field( "{$form['border_radius_right']}{$form['border_radius_unit']}" ),
705 '--srfm-form-border-radius-bottom' => sanitize_text_field( "{$form['border_radius_bottom']}{$form['border_radius_unit']}" ),
706 '--srfm-form-border-radius-left' => sanitize_text_field( "{$form['border_radius_left']}{$form['border_radius_unit']}" ),
707 ];
708 // Background Styles.
709 if ( 'image' === $bg_type && ! empty( $bg_image ) ) {
710 $bg_size_merged = 'custom' === $bg_image_size ? "{$bg_image_size_custom}{$bg_image_size_custom_unit}" : $bg_image_size;
711 $styling_vars += [
712 '--srfm-bg-image' => 'url(' . esc_url_raw( $bg_image ) . ')',
713 '--srfm-bg-position' => sanitize_text_field(
714 ( ( ! empty( $bg_image_position['x'] ) ? $bg_image_position['x'] : 0.5 ) * 100 ) . '% ' .
715 ( ( ! empty( $bg_image_position['y'] ) ? $bg_image_position['y'] : 0.5 ) * 100 ) . '% '
716 ),
717 '--srfm-bg-attachment' => sanitize_text_field( $bg_image_attachment ),
718 '--srfm-bg-repeat' => sanitize_text_field( $bg_image_repeat ),
719 '--srfm-bg-size' => sanitize_text_field( $bg_size_merged ),
720 ];
721 } elseif ( 'color' === $bg_type && ! empty( $bg_color ) ) {
722 $styling_vars['--srfm-bg-color'] = sanitize_text_field( $bg_color );
723 } elseif ( 'gradient' === $bg_type && ! empty( $bg_gradient ) ) {
724 if ( $is_advanced_gradient ) {
725 $bg_gradient = Helper::get_gradient_css( $bg_gradient_type, $bg_gradient_color_1, $bg_gradient_color_2, $bg_gradient_location_1, $bg_gradient_location_2, $bg_gradient_angle );
726 }
727 $styling_vars['--srfm-bg-gradient'] = sanitize_text_field( $bg_gradient );
728 }
729 // Overlay Variables.
730 if ( 'image' === $bg_type && 'image' === $overlay_type && ! empty( $overlay_image ) ) {
731 $styling_vars += [
732 '--srfm-bg-overlay-image' => 'url(' . esc_url_raw( $overlay_image ) . ')',
733 '--srfm-bg-overlay-position' => sanitize_text_field(
734 ( ( ! empty( $overlay_position['x'] ) ? $overlay_position['x'] : 0.5 ) * 100 ) . '% ' .
735 ( ( ! empty( $overlay_position['y'] ) ? $overlay_position['y'] : 0.5 ) * 100 ) . '%'
736 ),
737 '--srfm-bg-overlay-attachment' => sanitize_text_field( $overlay_attachment ),
738 '--srfm-bg-overlay-repeat' => sanitize_text_field( $overlay_repeat ),
739 '--srfm-bg-overlay-size' => sanitize_text_field( $overlay_size ),
740 '--srfm-bg-overlay-blend-mode' => sanitize_text_field( $overlay_blend_mode ),
741 ];
742 } elseif ( 'image' === $bg_type && 'color' === $overlay_type && ! empty( $overlay_color ) ) {
743 $styling_vars += [
744 '--srfm-bg-overlay-color' => sanitize_text_field( $overlay_color ),
745 ];
746 } elseif ( 'image' === $bg_type && 'gradient' === $overlay_type && ! empty( $bg_overlay_gradient ) ) {
747 if ( $is_overlay_advanced_gradient ) {
748 $bg_overlay_gradient = Helper::get_gradient_css( $bg_overlay_gradient_type, $bg_overlay_gradient_color_1, $bg_overlay_gradient_color_2, $bg_overlay_gradient_location_1, $bg_overlay_gradient_location_2, $bg_overlay_gradient_angle );
749 }
750 $styling_vars += [
751 '--srfm-bg-overlay-gradient' => sanitize_text_field( $bg_overlay_gradient ),
752 ];
753 }
754 $styling_vars['--srfm-bg-overlay-opacity'] = floatval( $overlay_opacity );
755 // Output the CSS variables.
756 foreach ( $styling_vars as $key => $value ) {
757 echo esc_html( Helper::get_string_value( $key ) ) . ': ' . esc_html( Helper::get_string_value( $value ) ) . ';';
758 }
759 ?>
760 <?php
761 // Echo the CSS variables for the form according to the field spacing selected.
762 foreach ( $selected_size as $variable => $value ) {
763 echo esc_html( Helper::get_string_value( $variable ) ) . ': ' . esc_html( Helper::get_string_value( $value ) ) . ';';
764 }
765 do_action(
766 'srfm_form_css_variables',
767 [
768 'id' => $id,
769 'primary_color' => $primary_color_var,
770 'help_color' => $help_color_var,
771 'form_styling' => $form_styling,
772 'block_attrs' => $block_attrs,
773 ]
774 );
775 } // End if default styling is not disabled.
776 echo wp_kses_post( $embed_custom_css );
777 ?>
778 }
779 </style>
780 <?php } // End if the style block has content. ?>
781 <?php
782 if ( 'sureforms_form' !== $current_post_type && true === $show_title_current_page ) {
783 $title = ! empty( get_the_title( (int) $id ) ) ? get_the_title( (int) $id ) : '';
784 $title = String_Translator::get_instance()->translate_form_title( (int) $id, $title );
785 ?>
786 <h2 class="srfm-form-title"><?php echo esc_html( $title ); ?></h2>
787 <?php
788 }
789
790 // Password protected form check.
791 if ( $post && post_password_required( $post ) ) {
792 // Define allowed HTML tags for password form output.
793 $allowed_password_form_tags = [
794 'form' => [
795 'action' => true,
796 'method' => true,
797 'class' => true,
798 'id' => true,
799 ],
800 'label' => [
801 'for' => true,
802 'class' => true,
803 ],
804 'input' => [
805 'type' => true,
806 'name' => true,
807 'id' => true,
808 'class' => true,
809 'value' => true,
810 'size' => true,
811 'placeholder' => true,
812 'required' => true,
813 ],
814 'p' => [
815 'class' => true,
816 'style' => true,
817 ],
818 'button' => [
819 'type' => true,
820 'name' => true,
821 'class' => true,
822 'id' => true,
823 'style' => true,
824 ],
825 'div' => [
826 'class' => true,
827 'id' => true,
828 'style' => true,
829 ],
830 'span' => [
831 'class' => true,
832 'aria-hidden' => true,
833 ],
834 'svg' => [
835 'xmlns' => true,
836 'width' => true,
837 'height' => true,
838 'viewBox' => true,
839 'fill' => true,
840 ],
841 'path' => [
842 'd' => true,
843 'stroke' => true,
844 'stroke-opacity' => true,
845 'stroke-width' => true,
846 'stroke-linecap' => true,
847 'stroke-linejoin' => true,
848 ],
849 ];
850 echo wp_kses( get_the_password_form( $post ), $allowed_password_form_tags );
851 ?>
852 </div>
853 <?php
854 self::$current_block_attrs = [];
855 return ob_get_clean();
856 }
857 $submit_token = Submit_Token::generate( (int) $id );
858
859 ?>
860 <form method="post" enctype="multipart/form-data" id="srfm-form-<?php echo esc_attr( Helper::get_string_value( $id ) ); ?>" class="srfm-form <?php echo esc_attr( 'sureforms_form' === $post_type ? 'srfm-single-form ' : '' ); ?>"
861 form-id="<?php echo esc_attr( Helper::get_string_value( $id ) ); ?>" after-submission="<?php echo esc_attr( $submission_action ); ?>" message-type="<?php echo esc_attr( $confirmation_type ? $confirmation_type : 'same page' ); ?>" success-url="<?php echo esc_attr( $success_url ? $success_url : '' ); ?>" ajaxurl="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" data-submit-token="<?php echo esc_attr( $submit_token ); ?>"
862 >
863 <?php
864 // Submission security is handled via the HMAC token in data-submit-token.
865 $global_setting_options = get_option( 'srfm_security_settings_options' );
866 $honeypot_spam = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_honeypot'] ) ? $global_setting_options['srfm_honeypot'] : '';
867
868 if ( $is_page_break && 'none' !== $page_break_progress_type ) {
869 do_action( 'srfm_page_break_header', $id );
870 }
871 ?>
872
873 <input type="hidden" value="<?php echo esc_attr( Helper::get_string_value( $id ) ); ?>" name="form-id">
874 <?php
875 /*
876 * Submission language. Captured client-side because the REST submit endpoint
877 * loses WPML's URL-based language context. The value is baked into the markup
878 * at render time, so accurate entry-language tagging requires the page cache to
879 * be language-aware (the default for WPML's language-per-URL modes). At submit
880 * time the server re-validates this value against the active language list and
881 * falls back to its own current_language() when it can't be confirmed
882 * (see Form_Submit::is_known_language()), so a stale/forged value is never
883 * trusted blindly.
884 */
885 ?>
886 <input type="hidden" value="<?php echo esc_attr( Multilingual_Manager::get_instance()->provider()->current_language() ); ?>" name="srfm-form-language">
887 <input type="hidden" value="" name="srfm-sender-email-field" id="srfm-sender-email">
888 <input type="hidden" value="<?php echo esc_attr( Helper::get_string_value( $is_page_break ) ); ?>" id="srfm-page-break">
889 <?php if ( $honeypot_spam ) { ?>
890 <input type="hidden" value="" name="srfm-honeypot-field">
891 <?php
892 }
893 self::common_error_message( 'head' );
894 if ( $is_page_break ) {
895 do_action( 'srfm_page_break_pagination', $post, $id );
896 } elseif ( ! apply_filters( 'srfm_use_custom_field_content', false ) ) {
897 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is filtered and sanitized by WordPress core blocks and filters.
898 echo $content;
899 }
900
901 do_action( 'srfm_after_field_content', $post, $id );
902
903 $captcha_container_hidden_class = ! empty( $google_captcha_site_key ) && ( 'v3-reCAPTCHA' === $recaptcha_version || 'v2-invisible' === $recaptcha_version ) ? 'srfm-display-none' : '';
904
905 ?>
906 <?php if ( $should_show_submit_button && ! empty( $security_type ) && 'none' !== $security_type ) { ?>
907 <div class="srfm-captcha-container <?php echo esc_attr( $captcha_container_hidden_class ); ?>">
908
909 <?php
910
911 if ( 'g-recaptcha' === $security_type ) {
912 self::get_google_captcha_script( $recaptcha_version, $google_captcha_site_key );
913 }
914
915 if ( 'cf-turnstile' === $security_type ) {
916 self::get_cf_turnstile_script( $srfm_cf_appearance_mode, $srfm_cf_turnstile_site_key );
917 }
918
919 if ( 'hcaptcha' === $security_type ) {
920 self::get_h_captcha_script( $srfm_hcaptcha_site_key );
921 }
922 ?>
923 <div class="srfm-validation-error" id="captcha-error" style="display: none;"><?php echo esc_html__( 'Please verify that you are not a robot.', 'sureforms' ); ?></div>
924 </div>
925 <?php } ?>
926
927 <?php
928 if ( $is_page_break ) {
929 do_action( 'srfm_page_break_btn', $id );
930 }
931 $srfm_button_classes = apply_filters( 'srfm_add_button_classes', [ '1' === $btn_from_theme ? 'wp-block-button__link' : 'srfm-btn-frontend srfm-button srfm-submit-button', 'v3-reCAPTCHA' === $recaptcha_version ? ' g-recaptcha' : '' ], $id, $block_attrs );
932 ?>
933
934 <div class="srfm-submit-container <?php echo esc_attr( $is_page_break ? 'srfm-hide' : '' ); ?>" style="<?php echo ! $should_show_submit_button ? 'visibility:hidden;position:absolute;' : ''; ?>">
935 <div style="width: <?php echo esc_attr( $full ? '100%' : '' ); ?>; text-align: <?php echo esc_attr( $submit_button_alignment ); ?>" class="wp-block-button">
936 <?php do_action( 'srfm_before_submit_button', $id ); ?>
937 <?php if ( $should_show_submit_button ) { ?>
938 <button style="<?php echo esc_attr( $full ? 'width: 100%;' : '' ); ?>" id="srfm-submit-btn" class="<?php echo esc_attr( implode( ' ', array_filter( $srfm_button_classes ) ) ); ?>"
939 <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) { ?>
940 data-callback="recaptchaCallback"
941 data-error-callback="onGCaptchaV3Error"
942 recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>"
943 data-sitekey="<?php echo esc_attr( $google_captcha_site_key ); ?>"
944 <?php } ?>
945 >
946 <div class="srfm-submit-wrap">
947 <?php echo esc_html( $button_text ); ?>
948 <div class="srfm-loader"></div>
949 </div>
950 </button>
951 <?php } ?>
952 <?php do_action( 'srfm_after_submit_button', $id ); ?>
953 </div>
954 </div>
955 <?php
956
957 echo wp_kses_post(
958 apply_filters(
959 'srfm_after_submit_button_content',
960 '',
961 [
962 'id' => $id,
963 'should_show_submit_button' => $should_show_submit_button,
964 'button_text' => $button_text,
965 'submit_button_alignment' => $submit_button_alignment,
966 'full' => $full,
967 'btn_from_theme' => $btn_from_theme,
968 'is_page_break' => $is_page_break,
969 'recaptcha_version' => $recaptcha_version,
970 'google_captcha_site_key' => $google_captcha_site_key,
971 'srfm_button_classes' => $srfm_button_classes,
972 ]
973 )
974 );
975 }
976 self::common_error_message( 'footer' );
977 ?>
978 </form>
979 <div class="srfm-single-form srfm-success-box in-page">
980 <div aria-live="polite" aria-atomic="true" role="alert" id="srfm-success-message-page-<?php echo esc_attr( Helper::get_string_value( $id ) ); ?>" class="srfm-success-box-description"></div>
981 </div>
982 <?php
983 // Admin-only shortcut into the form editor, overlaid at the top-right of
984 // the embedded form. Rendered only for users who can edit THIS form, so
985 // it is fully absent from the DOM for everyone else and, being absolutely
986 // positioned, never affects the layout or submission for regular
987 // visitors. Works for every embed method (block, shortcode, widget)
988 // because they all render through this function. Gated on the same
989 // condition as the `.srfm-form-container` open above, so a zero-block
990 // form (no container) never emits an orphaned, unpositioned pill.
991 if ( '' !== $id && 0 !== $block_count ) {
992 self::render_edit_form_button( (int) $id );
993 }
994
995 // Add preview script for real-time styling updates from block editor.
996 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a preview context, nonce not required.
997 if ( isset( $_GET['form_preview'] ) && 'true' === $_GET['form_preview'] && isset( $container_id ) ) {
998 self::enqueue_preview_styling_script( $container_id );
999 }
1000 ?>
1001 </div>
1002 <?php
1003 self::$current_block_attrs = [];
1004 return ob_get_clean();
1005 }
1006
1007 /**
1008 * Generate HCaptcha script markup
1009 *
1010 * @param string $srfm_hcaptcha_site_key site key.
1011 * @since 1.7.0
1012 * @return void
1013 */
1014 public static function get_h_captcha_script( $srfm_hcaptcha_site_key ) {
1015 if ( ! empty( $srfm_hcaptcha_site_key ) ) {
1016 // hCaptcha script.
1017 wp_enqueue_script( 'hcaptcha', 'https://js.hcaptcha.com/1/api.js', [], null, [ 'strategy' => 'defer' ] ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1018 ?>
1019 <div id="srfm-hcaptcha-sitekey" data-callback="onSuccess" data-error-callback="onHCaptchaError" class="h-captcha" data-sitekey="<?php echo esc_attr( $srfm_hcaptcha_site_key ); ?>"></div>
1020 <?php
1021 } else {
1022 Helper::render_missing_sitekey_error( 'HCaptcha' );
1023 }
1024 }
1025
1026 /**
1027 * Generate Google Recaptcha script markup
1028 *
1029 * @param string $recaptcha_version reCAPTCHA version.
1030 * @param string $google_captcha_site_key site key.
1031 * @since 1.7.0
1032 * @return void
1033 */
1034 public static function get_google_captcha_script( $recaptcha_version, $google_captcha_site_key ) {
1035
1036 if ( empty( $google_captcha_site_key ) ) {
1037 Helper::render_missing_sitekey_error( 'Google reCAPTCHA' );
1038 return;
1039 }
1040
1041 if ( 'v2-checkbox' === $recaptcha_version ) {
1042 ?>
1043 <?php
1044 wp_enqueue_script( 'google-recaptcha', 'https://www.google.com/recaptcha/api.js', [], SRFM_VER, true );
1045 ?>
1046 <div class='g-recaptcha' data-callback="onSuccess" data-error-callback="onGCaptchaV2CheckBoxError" recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>" data-sitekey="<?php echo esc_attr( strval( $google_captcha_site_key ) ); ?>" ></div>
1047 <?php } ?>
1048
1049 <?php if ( 'v2-invisible' === $recaptcha_version ) { ?>
1050 <?php
1051 wp_enqueue_script( 'google-recaptcha-invisible', 'https://www.google.com/recaptcha/api.js?onload=recaptchaCallback&render=explicit', [ SRFM_SLUG . '-form-submit' ], SRFM_VER, true );
1052 ?>
1053 <div class='g-recaptcha' recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>" data-sitekey="<?php echo esc_attr( $google_captcha_site_key ); ?>" data-size="invisible"></div>
1054 <?php } ?>
1055
1056 <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) { ?>
1057 <?php
1058 // phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Google reCAPTCHA must be loaded from Google's servers for token verification; the version is controlled by Google, and passing null avoids stale caching.
1059 wp_enqueue_script(
1060 'srfm-google-recaptchaV3',
1061 'https://www.google.com/recaptcha/api.js?render=' . $google_captcha_site_key,
1062 [],
1063 null,
1064 true
1065 );
1066 // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
1067 ?>
1068 <?php
1069 }
1070 }
1071
1072 /**
1073 * Generate Cloudflare Turnstile script markup
1074 *
1075 * @param string $srfm_cf_appearance_mode appearance mode.
1076 * @param string $srfm_cf_turnstile_site_key site key.
1077 * @since 1.7.0
1078 * @return void
1079 */
1080 public static function get_cf_turnstile_script( $srfm_cf_appearance_mode, $srfm_cf_turnstile_site_key ) {
1081 if ( ! empty( $srfm_cf_turnstile_site_key ) ) {
1082 // Cloudflare Turnstile script.
1083 // phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Cloudflare Turnstile must be loaded from Cloudflare's servers for token verification; the version is controlled by Cloudflare.
1084 wp_enqueue_script(
1085 SRFM_SLUG . '-cf-turnstile',
1086 'https://challenges.cloudflare.com/turnstile/v0/api.js',
1087 [],
1088 null,
1089 [
1090 'strategy' => 'defer',
1091 ]
1092 );
1093 // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
1094 ?>
1095 <!-- The callback methods below are available on frontend.js. onTurnstileError displays and error in place of recaptcha dialog. -->
1096 <div id="srfm-cf-sitekey" class="cf-turnstile" data-callback="onSuccess" data-error-callback="onTurnstileError" data-theme="<?php echo esc_attr( $srfm_cf_appearance_mode ); ?>" data-sitekey="<?php echo esc_attr( $srfm_cf_turnstile_site_key ); ?>"></div>
1097 <?php
1098 } else {
1099 Helper::render_missing_sitekey_error( 'Turnstile' );
1100 }
1101 }
1102
1103 /**
1104 * Generate common error message markup
1105 *
1106 * @param string $position position of the error message.
1107 * @since 1.5.0
1108 * @return void
1109 */
1110 public static function common_error_message( $position = 'footer' ) {
1111 $icon = Helper::fetch_svg( 'info_circle', '', 'aria-hidden="true"' );
1112 $classes = "srfm-common-error-message srfm-error-message srfm-{$position}-error";
1113 ?>
1114 <p id="srfm-error-message" class="<?php echo esc_attr( $classes ); ?>" hidden><?php echo wp_kses( $icon, Helper::$allowed_tags_svg ); ?><span class="srfm-error-content"><?php echo esc_html( String_Translator::get_instance()->translate_validation_message( 'srfm_submit_error', __( 'There was an error trying to submit your form. Please try again.', 'sureforms' ) ) ); ?></span></p>
1115 <?php
1116 }
1117
1118 /**
1119 * Enqueue the preview styling script for real-time updates from block editor.
1120 *
1121 * @param string $container_id The form container ID selector.
1122 * @since 2.7.0
1123 * @return void
1124 */
1125 public static function enqueue_preview_styling_script( $container_id ) {
1126 $script_asset_path = SRFM_DIR . 'assets/build/previewStyling.asset.php';
1127 $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [
1128 'dependencies' => [],
1129 'version' => SRFM_VER,
1130 ];
1131
1132 wp_enqueue_script(
1133 SRFM_SLUG . '-preview-styling',
1134 SRFM_URL . 'assets/build/previewStyling.js',
1135 $script_asset['dependencies'],
1136 $script_asset['version'],
1137 true
1138 );
1139
1140 wp_localize_script(
1141 SRFM_SLUG . '-preview-styling',
1142 'srfmPreviewStyling',
1143 [
1144 'containerId' => $container_id,
1145 'fieldSpacingVars' => Helper::get_css_vars(),
1146 ]
1147 );
1148
1149 /**
1150 * Action to allow Pro to enqueue additional preview styling scripts.
1151 *
1152 * @since 2.7.0
1153 */
1154 do_action( 'srfm_enqueue_preview_styling_scripts' );
1155 }
1156
1157 /**
1158 * Generate form confirmation markup
1159 *
1160 * @param array<mixed> $form_data contains form data.
1161 * @param array<mixed> $submission_data contains submission data.
1162 * @since 0.0.3
1163 * @return string|false
1164 */
1165 public static function get_confirmation_markup( $form_data = [], $submission_data = [] ) {
1166
1167 $confirmation_message = '';
1168
1169 if ( empty( $form_data ) ) {
1170 return $confirmation_message;
1171 }
1172
1173 $form_id = isset( $form_data['form-id'] ) ? Helper::get_integer_value( $form_data['form-id'] ) : 0;
1174 $form_confirmation = get_post_meta( $form_id, '_srfm_form_confirmation' );
1175
1176 /**
1177 * Filter the form confirmation data.
1178 * Allows conditional confirmations to override the default confirmation settings.
1179 *
1180 * @param mixed $form_confirmation The form confirmation data from post meta.
1181 * @param int $form_id The form ID.
1182 * @param array $submission_data The submission data.
1183 * @since 2.4.0
1184 */
1185 $form_confirmation = apply_filters( 'srfm_form_confirmation_data', $form_confirmation, $form_id, $submission_data );
1186
1187 if ( ! is_array( $form_confirmation ) ) {
1188 return $confirmation_message;
1189 }
1190
1191 $confirmation_data = is_array( $form_confirmation[0] ) && isset( $form_confirmation[0][0] ) ? $form_confirmation[0][0] : null;
1192
1193 if ( is_array( $form_confirmation ) && isset( $confirmation_data['message'] ) && is_string( $confirmation_data['message'] ) ) {
1194 $confirmation_message = $confirmation_data['message'];
1195 $confirmation_message = String_Translator::get_instance()->translate_confirmation_message( (int) $form_id, 0, $confirmation_message );
1196 }
1197 if ( empty( $submission_data ) ) {
1198 return $confirmation_message;
1199 }
1200 $smart_tags = new Smart_Tags();
1201 $confirmation_message = $smart_tags->process_smart_tags( $confirmation_message, $submission_data, $form_data );
1202
1203 /**
1204 * Filter whether confirmation message links should open in a new tab.
1205 *
1206 * @since 2.5.2
1207 *
1208 * @param bool $open_in_new_tab Whether links open in a new tab. Default true.
1209 */
1210 $open_in_new_tab = (bool) apply_filters( 'srfm_confirmation_links_open_in_new_tab', true );
1211
1212 $markup = Helper::strip_js_attributes(
1213 apply_filters( 'srfm_after_submit_confirmation_message', $confirmation_message, $form_data, $submission_data ),
1214 ! $open_in_new_tab
1215 );
1216
1217 if ( false !== strpos( $markup, 'src="image/svg+xml;base64' ) ) {
1218 // Handle Form Confirmation SVGs separately. We have planned to improve it in the future replacing it with image URL.
1219 $normalized_string = preg_replace( '/src="image\/svg\+xml;base64/', 'src="data:image/svg+xml;base64', $markup );
1220
1221 if ( is_string( $normalized_string ) ) {
1222 $markup = $normalized_string;
1223 }
1224 }
1225
1226 return $markup;
1227 }
1228
1229 /**
1230 * Get redirect url for form incase of different page or custom url is selected.
1231 *
1232 * @param array<mixed> $form_data contains form data.
1233 * @param array<mixed> $submission_data contains submission data.
1234 * @since 1.0.2
1235 * @return string|false
1236 */
1237 public static function get_redirect_url( $form_data = [], $submission_data = [] ) {
1238 $redirect_url = '';
1239
1240 if ( empty( $form_data ) ) {
1241 return $redirect_url;
1242 }
1243
1244 $form_id = isset( $form_data['form-id'] ) ? Helper::get_integer_value( $form_data['form-id'] ) : 0;
1245 $form_confirmation = get_post_meta( $form_id, '_srfm_form_confirmation' );
1246
1247 /**
1248 * Filter the form confirmation data.
1249 * Allows conditional confirmations to override the default confirmation settings.
1250 *
1251 * @param mixed $form_confirmation The form confirmation data from post meta.
1252 * @param int $form_id The form ID.
1253 * @param array $submission_data The submission data.
1254 * @since 2.4.0
1255 */
1256 $form_confirmation = apply_filters( 'srfm_form_confirmation_data', $form_confirmation, $form_id, $submission_data );
1257
1258 if ( ! is_array( $form_confirmation ) ) {
1259 return $redirect_url;
1260 }
1261
1262 $confirmation_data = is_array( $form_confirmation[0] ) && isset( $form_confirmation[0][0] ) ? $form_confirmation[0][0] : null;
1263
1264 $page_url = $confirmation_data['page_url'] ?? '';
1265 $custom_url = $confirmation_data['custom_url'] ?? '';
1266 $confirmation_type = $confirmation_data['confirmation_type'] ?? '';
1267
1268 if ( 'different page' === $confirmation_type ) {
1269 $redirect_url = esc_url_raw( $page_url );
1270 } elseif ( 'custom url' === $confirmation_type ) {
1271 $redirect_url = esc_url_raw( $custom_url );
1272 }
1273
1274 if ( empty( $redirect_url ) ) {
1275 return $redirect_url;
1276 }
1277
1278 if ( empty( $confirmation_data['enable_query_params'] ) || true !== $confirmation_data['enable_query_params'] ) {
1279 return $redirect_url;
1280 }
1281
1282 if ( empty( $confirmation_data['query_params'] ) && ! is_array( $confirmation_data['query_params'] ) ) {
1283 return $redirect_url;
1284 }
1285
1286 $query_params = [];
1287 foreach ( $confirmation_data['query_params'] as $params ) {
1288 if ( is_array( $params ) && ! empty( array_keys( $params ) ) && ! empty( array_values( $params ) ) ) {
1289 $query_params[ sanitize_text_field( array_keys( $params )[0] ) ] = sanitize_text_field( array_values( $params )[0] );
1290 }
1291 }
1292
1293 $redirect_url = add_query_arg( $query_params, $redirect_url );
1294
1295 if ( ! empty( $submission_data ) ) {
1296 $smart_tags = new Smart_Tags();
1297 // Adding upload_format_type = 'raw' to retrieve urls as comma separated values.
1298 $form_data['upload_format_type'] = 'raw';
1299 // Skip auto-linking URLs in smart tag values — redirect query params need raw values, not HTML.
1300 $form_data['smart_tag_context'] = 'redirect';
1301
1302 /*
1303 * Resolve smart tags in the URL, normalize the multi-value delimiters
1304 * left behind by the substitution, then decode any HTML entities.
1305 *
1306 * Multi-select dropdown values are packed as "Red | Blue" by the frontend
1307 * (srfmUtility.prepareValue in assets/js/unminified/frontend.js), and
1308 * checkbox multi-choice values are rendered as "Red<br>Blue" by
1309 * Smart_Tags::parse_form_input. Neither delimiter is URL-friendly as-is:
1310 * " | " leaks whitespace into the query string and "<br>" gets mangled
1311 * by esc_url_raw below. Normalize both to a plain "|" so the final
1312 * redirect URL carries a clean, URL-safe list that the receiver can
1313 * split on "|".
1314 *
1315 * The str_replace runs before html_entity_decode so that any literal
1316 * "<br>" character sequence inside an option label — which
1317 * Smart_Tags::parse_form_input escapes to "&lt;br&gt;" before joining
1318 * — survives intact. Only the actual delimiter (the unescaped "<br>"
1319 * emitted by the implode) is converted to a pipe; html_entity_decode
1320 * then restores the option's original text.
1321 */
1322 $resolved_redirect_url = Helper::get_string_value( $smart_tags->process_smart_tags( $redirect_url, $submission_data, $form_data ) );
1323 $multi_value_delimiters = [ '<br>', ' | ' ];
1324 $redirect_url = html_entity_decode( str_replace( $multi_value_delimiters, '|', $resolved_redirect_url ) );
1325 }
1326
1327 return esc_url_raw( apply_filters( 'srfm_after_submit_redirect_url', $redirect_url ) );
1328 }
1329
1330 /**
1331 * Print the admin-only "Edit Form" shortcut on an embedded form.
1332 *
1333 * Renders a small pill link overlaid at the top-right of the form container
1334 * (Elementor/Beaver-Builder style) that opens the block editor for this form.
1335 * Being absolutely positioned, it never affects the form's layout.
1336 *
1337 * Admin-only by construction: the `sureforms_form` CPT registers with
1338 * `map_meta_cap => false`, so `edit_post` collapses to a blanket
1339 * `manage_options` check with no per-post component — an editor never sees the
1340 * pill on any form. For every other viewer the markup and its styles are
1341 * entirely absent from the DOM.
1342 *
1343 * The stylesheet is attached to a registered inline-only handle so `WP_Styles`
1344 * dedupes it by handle (surviving a discarded `the_content` pass, e.g. an SEO
1345 * plugin building `og:description` during `wp_head`) and it survives a strict
1346 * `style-src` CSP. It is not cache-signalled here: the payload is only a
1347 * `wp-admin/post.php?post=N` link an anonymous visitor cannot act on, and a
1348 * `DONOTCACHEPAGE` define from a fragment renderer is both inert on the normal
1349 * (headers-already-sent) path and an irreversible process-global side effect.
1350 *
1351 * @param int $form_id Form post ID.
1352 *
1353 * @return void
1354 * @since 2.12.4
1355 */
1356 public static function render_edit_form_button( $form_id ) {
1357 $form_id = absint( $form_id );
1358
1359 // Only for real SureForms forms — the [sureforms] shortcode accepts any
1360 // post ID, and a non-form target would map `edit_post` normally and leak
1361 // the pill to an ordinary editor.
1362 if ( 0 === $form_id || ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $form_id ) ) {
1363 return;
1364 }
1365
1366 // Capability gate first, before the suppression filter, so no work is done
1367 // for the anonymous visitors who make up almost every page view.
1368 if ( ! current_user_can( 'edit_post', $form_id ) ) {
1369 return;
1370 }
1371
1372 // Contexts where the pill is redundant or wrong:
1373 // - the single-form / Instant Form page, where the form IS the whole page
1374 // and the admin bar already links to its editor. This is also what
1375 // suppresses the block editor's preview — that preview is an iframe to
1376 // the form's own permalink (an ordinary front-end request), NOT a REST
1377 // render, so `is_singular` is the load-bearing guard there;
1378 // - any admin / AJAX / REST / JSON request, or a feed (the markup would
1379 // otherwise land inside `content:encoded` CDATA).
1380 if (
1381 is_singular( SRFM_FORMS_POST_TYPE )
1382 || is_admin()
1383 || wp_doing_ajax()
1384 || wp_is_json_request()
1385 || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
1386 || is_feed()
1387 ) {
1388 return;
1389 }
1390
1391 // Page-builder editor canvases render the form directly (not over REST),
1392 // where their own element-edit handles would collide with the pill.
1393 if ( class_exists( '\Elementor\Plugin' ) && \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
1394 return;
1395 }
1396 if ( function_exists( 'bricks_is_builder' ) && bricks_is_builder() ) {
1397 return;
1398 }
1399
1400 /**
1401 * Allow integrations to suppress the admin "Edit Form" shortcut entirely.
1402 *
1403 * @param bool $show Whether to render the shortcut. Default true.
1404 * @param int $form_id Form post ID.
1405 *
1406 * @since 2.12.4
1407 */
1408 if ( ! apply_filters( 'srfm_show_edit_form_button', true, $form_id ) ) {
1409 return;
1410 }
1411
1412 $edit_link = get_edit_post_link( $form_id );
1413
1414 if ( empty( $edit_link ) ) {
1415 return;
1416 }
1417
1418 /**
1419 * Filter the target of the admin "Edit Form" shortcut.
1420 *
1421 * @param string $edit_link Editor URL for the form.
1422 * @param int $form_id Form post ID.
1423 *
1424 * @since 2.12.4
1425 */
1426 $edit_link = Helper::get_string_value( apply_filters( 'srfm_edit_form_button_link', $edit_link, $form_id ) );
1427
1428 if ( '' === $edit_link ) {
1429 return;
1430 }
1431
1432 // Registered inline-only handle: WP_Styles dedupes by handle across every
1433 // embedded form and prints via print_late_styles() in the footer even when
1434 // enqueued this late (during the_content).
1435 $style_handle = 'srfm-edit-form-btn';
1436 if ( ! wp_style_is( $style_handle, 'registered' ) ) {
1437 wp_register_style( $style_handle, false, [], SRFM_VER );
1438 wp_add_inline_style( $style_handle, self::get_edit_form_button_css() );
1439 }
1440 wp_enqueue_style( $style_handle );
1441 ?>
1442 <a class="srfm-edit-form-btn" href="<?php echo esc_url( $edit_link ); ?>" target="_blank" rel="noopener noreferrer">
1443 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>
1444 <span><?php esc_html_e( 'Edit Form', 'sureforms' ); ?></span>
1445 <span class="screen-reader-text"><?php esc_html_e( '(opens in a new tab)', 'sureforms' ); ?></span>
1446 </a>
1447 <?php
1448 }
1449
1450 /**
1451 * Stylesheet for the admin "Edit Form" pill (#3029).
1452 *
1453 * `position: relative` on the container is scoped to the `srfm-styling-none`
1454 * case: with default styling on, the shipped CSS already sets it, so a global
1455 * rule here would only risk overriding a site that deliberately set it static.
1456 * Offsets use a small positive inset (`inset-inline-end`) so the pill sits
1457 * inside the box — no mobile horizontal overflow — and is RTL-correct.
1458 *
1459 * @return string
1460 * @since 2.12.4
1461 */
1462 private static function get_edit_form_button_css() {
1463 return '
1464 .srfm-form-container.srfm-styling-none { position: relative; }
1465 .srfm-edit-form-btn {
1466 position: absolute;
1467 top: 8px;
1468 inset-inline-end: 8px;
1469 z-index: 5;
1470 display: inline-flex;
1471 align-items: center;
1472 gap: 6px;
1473 padding: 6px 12px;
1474 font-size: 13px;
1475 font-weight: 500;
1476 line-height: 1;
1477 color: #1e293b;
1478 background: #ffffff;
1479 border: 1px solid #e2e8f0;
1480 border-radius: 9999px;
1481 box-shadow: 0 2px 6px rgba( 0, 0, 0, 0.12 );
1482 text-decoration: none;
1483 }
1484 .srfm-edit-form-btn:hover { border-color: #cbd5e1; color: #0f172a; }
1485 .srfm-edit-form-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
1486 .srfm-edit-form-btn svg { width: 14px; height: 14px; }
1487 ';
1488 }
1489 }
1490