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