PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 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 All 98 releases
← All changes | inc/generate-form-markup.php +1287 -142 0.0.10 → 2.12.8 View file →
@@ -7,13 +7,11 @@
7 7 */
8 8
9 9 namespace SRFM\Inc;
10 10
11 -use WP_REST_Response;
12 -use WP_Error;
11 +use SRFM\Inc\Compatibility\Multilingual\Multilingual_Manager;
12 +use SRFM\Inc\Compatibility\Multilingual\String_Translator;
13 13 use SRFM\Inc\Traits\Get_Instance;
14 -use SRFM\Inc\Helper;
15 -use SRFM\Inc\Smart_Tags;
16 14
17 15 if ( ! defined( 'ABSPATH' ) ) {
18 16 exit; // Exit if accessed directly.
19 17 }
@@ -26,8 +24,39 @@
26 24 class Generate_Form_Markup {
27 25 use Get_Instance;
28 26
29 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 + /**
30 59 * Constructor
31 60 *
32 61 * @since 0.0.1
33 62 */
@@ -32,11 +61,196 @@
32 61 * @since 0.0.1
33 62 */
34 63 public function __construct() {
35 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 );
36 71 }
37 72
38 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 + /**
39 253 * Add custom API Route to generate form markup.
40 254 *
41 255 * @return void
42 256 * @since 0.0.1
@@ -46,68 +260,308 @@
46 260 'sureforms/v1',
47 261 '/generate-form-markup',
48 262 [
49 263 'methods' => 'GET',
50 - 'callback' => [ $this, 'get_form_markup' ],
51 - 'permission_callback' => '__return_true',
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 + ],
52 276 ]
53 277 );
54 278 }
55 279
56 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 + /**
57 340 * Handle Form status
58 341 *
59 - * @param int|string $id Contains form ID.
60 - * @param boolean $show_title_current_page Boolean to show/hide form title.
61 - * @param string $sf_classname additional class_name.
62 - * @param string $post_type Contains post type.
63 - * @param boolean $do_blocks Boolean to enable/disable parsing dynamic blocks.
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.
64 348 *
65 349 * @return string|false
66 350 * @since 0.0.1
67 351 */
68 - public static function get_form_markup( $id, $show_title_current_page = true, $sf_classname = '', $post_type = 'post', $do_blocks = false ) {
69 - if ( isset( $_GET['id'] ) && isset( $_GET['srfm_form_markup_nonce'] ) ) {
70 - $nonce = isset( $_GET['srfm_form_markup_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['srfm_form_markup_nonce'] ) ) : '';
71 - $id = wp_verify_nonce( $nonce, 'srfm_form_markup' ) && ! empty( $_GET['srfm_form_markup_nonce'] ) ? Helper::get_integer_value( sanitize_text_field( wp_unslash( $_GET['id'] ) ) ) : '';
72 - } else {
73 - $id = Helper::get_integer_value( $id );
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;
74 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 +
75 379 do_action( 'srfm_localize_conditional_logic_data', $id );
76 380 $post = get_post( Helper::get_integer_value( $id ) );
77 381
78 - $content = '';
382 + $content = '';
383 + $form_blocks = [];
79 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 +
80 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 +
81 402 if ( ! empty( $do_blocks ) ) {
82 - $content = do_blocks( $post->post_content );
403 + $content = do_blocks( $post_content );
83 404 } else {
84 - $content = apply_filters( 'the_content', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- wordpress hook
405 + $content = apply_filters( 'the_content', $post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- wordpress hook
85 406 }
86 407 }
87 408
88 - $blocks = parse_blocks( $content );
89 - $block_count = count( $blocks );
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 );
90 413 $current_post_type = get_post_type();
91 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 +
92 425 ob_start();
93 426 if ( '' !== $id && 0 !== $block_count ) {
94 427
95 - $container_id = 'srfm-form-container-' . Helper::get_string_value( $id );
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 : [];
96 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 +
97 522 $form_classes = [
98 523 'srfm-form-container',
99 - $container_id,
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.
100 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,
101 530 ];
102 531
103 - $form_classes[] = Helper::get_string_value( Helper::get_meta_value( $id, '_srfm_additional_classes' ) );
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 + }
104 541
105 - $form_styling = get_post_meta( $id, '_srfm_forms_styling', true );
106 - $form_styling = ! empty( $form_styling ) && is_array( $form_styling ) ? $form_styling : [];
107 - $page_break_settings = defined( 'SRFM_PRO_VER' ) ? get_post_meta( $id, '_srfm_page_break_settings', true ) : [];
108 - $page_break_settings = ! empty( $page_break_settings ) && is_array( $page_break_settings ) ? $page_break_settings : [];
109 - $is_page_break = ! empty( $page_break_settings ) ? $page_break_settings['is_page_break'] : false;
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 + // Auto-advance is read here rather than in Pro's button renderer because
546 + // save & resume replaces that whole container through the
547 + // srfm_page_break_buttons_html filter, which would drop the attributes.
548 + // The form tag is rendered exactly once and is already how both step
549 + // runtimes receive their per-form settings (form-id, ajaxurl,
550 + // data-submit-token).
551 + //
552 + // Two stored settings rather than one because the two layouts are
553 + // mutually exclusive: Pro filters srfm_use_page_break_layout to false
554 + // when the conversational layout is on, so $page_break_settings is
555 + // empty there and its editor panel is hidden. Each layout keeps the
556 + // toggle with the rest of its own settings, and only one can apply.
557 + $conversational_settings = defined( 'SRFM_PRO_VER' ) ? get_post_meta( $id, '_srfm_conversational_form', true ) : [];
558 + $conversational_settings = ! empty( $conversational_settings ) && is_array( $conversational_settings ) ? $conversational_settings : [];
559 + $is_conversational = ! empty( $conversational_settings['is_cf_enabled'] );
560 + $active_step_settings = $is_conversational ? $conversational_settings : ( $is_page_break ? $page_break_settings : [] );
561 + $auto_advance_key = $is_conversational ? 'cf_auto_advance' : 'auto_advance';
562 + $auto_advance = ! empty( $active_step_settings[ $auto_advance_key ] );
563 + $auto_advance_hide_next = $auto_advance && ! empty( $active_step_settings[ $auto_advance_key . '_hide_next' ] );
110 564 $page_break_progress_type = ! empty( $page_break_settings ) ? $page_break_settings['progress_indicator_type'] : 'none';
111 565 $form_confirmation = get_post_meta( $id, '_srfm_form_confirmation' );
112 566 $confirmation_type = '';
113 567 $submission_action = '';
@@ -113,12 +567,12 @@
113 567 $submission_action = '';
114 568 $success_url = '';
115 569 if ( is_array( $form_confirmation ) && isset( $form_confirmation[0][0] ) ) {
116 570 $confirmation_data = $form_confirmation[0][0];
117 - $page_url = isset( $confirmation_data['page_url'] ) ? $confirmation_data['page_url'] : '';
118 - $custom_url = isset( $confirmation_data['custom_url'] ) ? $confirmation_data['custom_url'] : '';
119 - $confirmation_type = isset( $confirmation_data['confirmation_type'] ) ? $confirmation_data['confirmation_type'] : '';
120 - $submission_action = isset( $confirmation_data['submission_action'] ) ? $confirmation_data['submission_action'] : '';
571 + $page_url = $confirmation_data['page_url'] ?? '';
572 + $custom_url = $confirmation_data['custom_url'] ?? '';
573 + $confirmation_type = $confirmation_data['confirmation_type'] ?? '';
574 + $submission_action = $confirmation_data['submission_action'] ?? '';
121 575 $success_url = '';
122 576 if ( 'different page' === $confirmation_type ) {
123 577 $success_url = $page_url;
124 578 } elseif ( 'custom url' === $confirmation_type ) {
@@ -127,15 +581,21 @@
127 581 }
128 582
129 583 // Submit button.
130 584 $button_text = Helper::get_meta_value( $id, '_srfm_submit_button_text' );
131 - $submit_button_alignment = $form_styling['submit_button_alignment'];
132 - $btn_from_theme = Helper::get_meta_value( $id, '_srfm_inherit_theme_button' );
133 - $is_inline_button = Helper::get_meta_value( $id, '_srfm_is_inline_button' );
134 - $security_type = Helper::get_meta_value( $id, '_srfm_captcha_security_type' );
135 - $form_custom_css_meta = Helper::get_meta_value( $id, '_srfm_form_custom_css' );
136 - $custom_css = ! empty( $form_custom_css_meta ) && is_string( $form_custom_css_meta ) ? $form_custom_css_meta : '';
585 + $button_text = String_Translator::get_instance()->translate_submit_button( (int) $id, Helper::get_string_value( $button_text ) );
586 + $submit_button_alignment = ! empty( $form_styling['submit_button_alignment'] ) ? $form_styling['submit_button_alignment'] : 'left';
137 587
588 + if ( is_rtl() && ( 'left' === $submit_button_alignment || 'right' === $submit_button_alignment ) ) {
589 + $submit_button_alignment = 'right' === $submit_button_alignment ? 'left' : 'right';
590 + }
591 +
592 + $btn_from_theme = Helper::get_meta_value( $id, '_srfm_inherit_theme_button' );
593 + $is_inline_button = apply_filters( 'srfm_is_inline_button', Helper::get_meta_value( $id, '_srfm_is_inline_button' ) );
594 + $security_type = Helper::get_meta_value( $id, '_srfm_captcha_security_type' );
595 + $form_custom_css_meta = Helper::get_meta_value( $id, '_srfm_form_custom_css' );
596 + $custom_css = ! empty( $form_custom_css_meta ) && is_string( $form_custom_css_meta ) ? $form_custom_css_meta : '';
597 +
138 598 $full = 'justify' === $submit_button_alignment ? true : false;
139 599 $recaptcha_version = 'g-recaptcha' === $security_type ? Helper::get_meta_value( $id, '_srfm_form_recaptcha' ) : '';
140 600 $srfm_cf_appearance_mode = '';
141 601 $srfm_cf_turnstile_site_key = '';
@@ -149,26 +609,26 @@
149 609 $global_setting_options = [];
150 610 }
151 611
152 612 if ( is_array( $global_setting_options ) && 'cf-turnstile' === $security_type ) {
153 - $srfm_cf_turnstile_site_key = isset( $global_setting_options['srfm_cf_turnstile_site_key'] ) ? $global_setting_options['srfm_cf_turnstile_site_key'] : '';
154 - $srfm_cf_appearance_mode = isset( $global_setting_options['srfm_cf_appearance_mode'] ) ? $global_setting_options['srfm_cf_appearance_mode'] : 'auto';
613 + $srfm_cf_turnstile_site_key = $global_setting_options['srfm_cf_turnstile_site_key'] ?? '';
614 + $srfm_cf_appearance_mode = $global_setting_options['srfm_cf_appearance_mode'] ?? 'auto';
155 615 }
156 616
157 617 if ( is_array( $global_setting_options ) && 'hcaptcha' === $security_type ) {
158 - $srfm_hcaptcha_site_key = isset( $global_setting_options['srfm_hcaptcha_site_key'] ) ? $global_setting_options['srfm_hcaptcha_site_key'] : '';
618 + $srfm_hcaptcha_site_key = $global_setting_options['srfm_hcaptcha_site_key'] ?? '';
159 619 }
160 620
161 621 if ( is_array( $global_setting_options ) && 'g-recaptcha' === $security_type ) {
162 622 switch ( $recaptcha_version ) {
163 623 case 'v2-checkbox':
164 - $google_captcha_site_key = isset( $global_setting_options['srfm_v2_checkbox_site_key'] ) ? $global_setting_options['srfm_v2_checkbox_site_key'] : '';
624 + $google_captcha_site_key = $global_setting_options['srfm_v2_checkbox_site_key'] ?? '';
165 625 break;
166 626 case 'v2-invisible':
167 - $google_captcha_site_key = isset( $global_setting_options['srfm_v2_invisible_site_key'] ) ? $global_setting_options['srfm_v2_invisible_site_key'] : '';
627 + $google_captcha_site_key = $global_setting_options['srfm_v2_invisible_site_key'] ?? '';
168 628 break;
169 629 case 'v3-reCAPTCHA':
170 - $google_captcha_site_key = isset( $global_setting_options['srfm_v3_site_key'] ) ? $global_setting_options['srfm_v3_site_key'] : '';
630 + $google_captcha_site_key = $global_setting_options['srfm_v3_site_key'] ?? '';
171 631 break;
172 632 default:
173 633 break;
174 634 }
@@ -173,13 +633,16 @@
173 633 break;
174 634 }
175 635 }
176 636
177 - $primary_color = $form_styling['primary_color'];
178 - $help_color_var = $form_styling['text_color'];
179 - $label_text_color = $form_styling['text_color_on_primary'];
180 - $field_spacing = $form_styling['field_spacing'];
637 + // Ensure $google_captcha_site_key is not empty, and if not, trim any leading or trailing whitespace.
638 + $google_captcha_site_key = is_string( $google_captcha_site_key ) && ! empty( $google_captcha_site_key ) ? trim( $google_captcha_site_key ) : '';
181 639
640 + $primary_color = $form_styling['primary_color'] ?? '';
641 + $help_color_var = $form_styling['text_color'] ?? '';
642 + $label_text_color = $form_styling['text_color_on_primary'] ?? '';
643 + $field_spacing = $form_styling['field_spacing'] ?? 'small';
644 +
182 645 // New colors.
183 646
184 647 $primary_color_var = $primary_color ? $primary_color : '#046bd2';
185 648 $label_text_color_var = $label_text_color ? $label_text_color : '#111827';
@@ -184,17 +647,35 @@
184 647 $primary_color_var = $primary_color ? $primary_color : '#046bd2';
185 648 $label_text_color_var = $label_text_color ? $label_text_color : '#111827';
186 649
187 650 $selected_size = Helper::get_css_vars( $field_spacing );
651 +
652 + $should_show_submit_button = apply_filters(
653 + 'srfm_show_submit_button',
654 + 0 !== $block_count && ! $is_inline_button || $is_page_break,
655 + $id
656 + );
657 +
658 + if ( ! $should_show_submit_button ) {
659 + $form_classes[] = 'srfm-submit-button-hidden';
660 + }
661 +
662 + // The scoped Custom CSS below is for embedded views only: on the form's own
663 + // single/instant view, templates/single-form.php already outputs the Custom
664 + // CSS (unscoped) in <head> — emitting it here too would duplicate it.
665 + $embed_custom_css = 'sureforms_form' !== $current_post_type ? $custom_css : '';
188 666 ?>
189 667 <div class="<?php echo esc_attr( implode( ' ', array_filter( $form_classes ) ) ); ?>">
668 + <?php if ( ! $disable_default_styles || '' !== $embed_custom_css ) { // Nothing to print otherwise — avoid an empty style block. ?>
190 669 <style>
191 670 /* Need to check and remove the input variables related to the Style Tab. */
192 671 <?php echo esc_html( ".{$container_id}" ); ?> {
672 + <?php if ( ! $disable_default_styles ) { ?>
193 673 /* New test variables */
194 674 --srfm-color-scheme-primary: <?php echo esc_html( $primary_color_var ); ?>;
195 675 --srfm-color-scheme-text-on-primary: <?php echo esc_html( $label_text_color_var ); ?>;
196 676 --srfm-color-scheme-text: <?php echo esc_html( $help_color_var ); ?>;
677 + --srfm-quill-editor-color: <?php echo esc_html( $primary_color_var ); ?>;
197 678
198 679 --srfm-color-input-label: <?php echo esc_html( $help_color_var ); ?>;
199 680 --srfm-color-input-description: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
200 681 --srfm-color-input-placeholder: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.5 );
@@ -201,9 +682,9 @@
201 682 --srfm-color-input-text: <?php echo esc_html( $help_color_var ); ?>;
202 683 --srfm-color-input-prefix: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
203 684 --srfm-color-input-background: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.02 );
204 685 --srfm-color-input-background-hover: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
205 - --srfm-color-input-background-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.05 );
686 + --srfm-color-input-background-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.07 );
206 687 --srfm-color-input-border: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.25 );
207 688 --srfm-color-input-border-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.15 );
208 689 --srfm-color-multi-choice-svg: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.7 );
209 690 --srfm-color-input-border-hover: hsl( from <?php echo esc_html( $primary_color_var ); ?> h s l / 0.65 );
@@ -224,35 +705,205 @@
224 705 --srfm-dropdown-menu-border-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.10 );
225 706 --srfm-dropdown-placeholder-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.50 );
226 707 --srfm-dropdown-icon-color: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.65 );
227 708 --srfm-dropdown-icon-disabled: hsl( from <?php echo esc_html( $help_color_var ); ?> h s l / 0.25 );
228 - <?php
229 - // Echo the CSS variables for the form according to the field spacing selected.
230 - foreach ( $selected_size as $variable => $value ) {
231 - echo esc_html( Helper::get_string_value( $variable ) ) . ': ' . esc_html( Helper::get_string_value( $value ) ) . ';';
232 - }
233 - do_action( 'srfm_form_css_variables', $id, $primary_color_var, $help_color_var );
234 - // echo custom css on page/post.
235 - if ( 'sureforms_form' !== $current_post_type ) :
236 - echo wp_kses_post( $custom_css );
237 - endif;
709 +
710 + /* Background Control Variables */
711 + <?php
712 + // Form Styles.
713 + $styling_vars = [
714 + // Instant Form Padding.
715 + '--srfm-instant-form-padding-top' => sanitize_text_field( "{$instant_form['padding_top']}{$instant_form['padding_unit']}" ),
716 + '--srfm-instant-form-padding-right' => sanitize_text_field( "{$instant_form['padding_right']}{$instant_form['padding_unit']}" ),
717 + '--srfm-instant-form-padding-bottom' => sanitize_text_field( "{$instant_form['padding_bottom']}{$instant_form['padding_unit']}" ),
718 + '--srfm-instant-form-padding-left' => sanitize_text_field( "{$instant_form['padding_left']}{$instant_form['padding_unit']}" ),
719 + // Instant Form Border Radius.
720 + '--srfm-instant-form-border-radius-top' => sanitize_text_field( "{$instant_form['border_radius_top']}{$instant_form['border_radius_unit']}" ),
721 + '--srfm-instant-form-border-radius-right' => sanitize_text_field( "{$instant_form['border_radius_right']}{$instant_form['border_radius_unit']}" ),
722 + '--srfm-instant-form-border-radius-bottom' => sanitize_text_field( "{$instant_form['border_radius_bottom']}{$instant_form['border_radius_unit']}" ),
723 + '--srfm-instant-form-border-radius-left' => sanitize_text_field( "{$instant_form['border_radius_left']}{$instant_form['border_radius_unit']}" ),
724 + // Embed Form Padding.
725 + '--srfm-form-padding-top' => sanitize_text_field( "{$form['padding_top']}{$form['padding_unit']}" ),
726 + '--srfm-form-padding-right' => sanitize_text_field( "{$form['padding_right']}{$form['padding_unit']}" ),
727 + '--srfm-form-padding-bottom' => sanitize_text_field( "{$form['padding_bottom']}{$form['padding_unit']}" ),
728 + '--srfm-form-padding-left' => sanitize_text_field( "{$form['padding_left']}{$form['padding_unit']}" ),
729 + // Embed Form Border Radius.
730 + '--srfm-form-border-radius-top' => sanitize_text_field( "{$form['border_radius_top']}{$form['border_radius_unit']}" ),
731 + '--srfm-form-border-radius-right' => sanitize_text_field( "{$form['border_radius_right']}{$form['border_radius_unit']}" ),
732 + '--srfm-form-border-radius-bottom' => sanitize_text_field( "{$form['border_radius_bottom']}{$form['border_radius_unit']}" ),
733 + '--srfm-form-border-radius-left' => sanitize_text_field( "{$form['border_radius_left']}{$form['border_radius_unit']}" ),
734 + ];
735 + // Background Styles.
736 + if ( 'image' === $bg_type && ! empty( $bg_image ) ) {
737 + $bg_size_merged = 'custom' === $bg_image_size ? "{$bg_image_size_custom}{$bg_image_size_custom_unit}" : $bg_image_size;
738 + $styling_vars += [
739 + '--srfm-bg-image' => 'url(' . esc_url_raw( $bg_image ) . ')',
740 + '--srfm-bg-position' => sanitize_text_field(
741 + ( ( ! empty( $bg_image_position['x'] ) ? $bg_image_position['x'] : 0.5 ) * 100 ) . '% ' .
742 + ( ( ! empty( $bg_image_position['y'] ) ? $bg_image_position['y'] : 0.5 ) * 100 ) . '% '
743 + ),
744 + '--srfm-bg-attachment' => sanitize_text_field( $bg_image_attachment ),
745 + '--srfm-bg-repeat' => sanitize_text_field( $bg_image_repeat ),
746 + '--srfm-bg-size' => sanitize_text_field( $bg_size_merged ),
747 + ];
748 + } elseif ( 'color' === $bg_type && ! empty( $bg_color ) ) {
749 + $styling_vars['--srfm-bg-color'] = sanitize_text_field( $bg_color );
750 + } elseif ( 'gradient' === $bg_type && ! empty( $bg_gradient ) ) {
751 + if ( $is_advanced_gradient ) {
752 + $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 );
753 + }
754 + $styling_vars['--srfm-bg-gradient'] = sanitize_text_field( $bg_gradient );
755 + }
756 + // Overlay Variables.
757 + if ( 'image' === $bg_type && 'image' === $overlay_type && ! empty( $overlay_image ) ) {
758 + $styling_vars += [
759 + '--srfm-bg-overlay-image' => 'url(' . esc_url_raw( $overlay_image ) . ')',
760 + '--srfm-bg-overlay-position' => sanitize_text_field(
761 + ( ( ! empty( $overlay_position['x'] ) ? $overlay_position['x'] : 0.5 ) * 100 ) . '% ' .
762 + ( ( ! empty( $overlay_position['y'] ) ? $overlay_position['y'] : 0.5 ) * 100 ) . '%'
763 + ),
764 + '--srfm-bg-overlay-attachment' => sanitize_text_field( $overlay_attachment ),
765 + '--srfm-bg-overlay-repeat' => sanitize_text_field( $overlay_repeat ),
766 + '--srfm-bg-overlay-size' => sanitize_text_field( $overlay_size ),
767 + '--srfm-bg-overlay-blend-mode' => sanitize_text_field( $overlay_blend_mode ),
768 + ];
769 + } elseif ( 'image' === $bg_type && 'color' === $overlay_type && ! empty( $overlay_color ) ) {
770 + $styling_vars += [
771 + '--srfm-bg-overlay-color' => sanitize_text_field( $overlay_color ),
772 + ];
773 + } elseif ( 'image' === $bg_type && 'gradient' === $overlay_type && ! empty( $bg_overlay_gradient ) ) {
774 + if ( $is_overlay_advanced_gradient ) {
775 + $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 );
776 + }
777 + $styling_vars += [
778 + '--srfm-bg-overlay-gradient' => sanitize_text_field( $bg_overlay_gradient ),
779 + ];
780 + }
781 + $styling_vars['--srfm-bg-overlay-opacity'] = floatval( $overlay_opacity );
782 + // Output the CSS variables.
783 + foreach ( $styling_vars as $key => $value ) {
784 + echo esc_html( Helper::get_string_value( $key ) ) . ': ' . esc_html( Helper::get_string_value( $value ) ) . ';';
785 + }
786 + ?>
787 + <?php
788 + // Echo the CSS variables for the form according to the field spacing selected.
789 + foreach ( $selected_size as $variable => $value ) {
790 + echo esc_html( Helper::get_string_value( $variable ) ) . ': ' . esc_html( Helper::get_string_value( $value ) ) . ';';
791 + }
792 + do_action(
793 + 'srfm_form_css_variables',
794 + [
795 + 'id' => $id,
796 + 'primary_color' => $primary_color_var,
797 + 'help_color' => $help_color_var,
798 + 'form_styling' => $form_styling,
799 + 'block_attrs' => $block_attrs,
800 + ]
801 + );
802 + } // End if default styling is not disabled.
803 + echo wp_kses_post( $embed_custom_css );
238 804 ?>
239 805 }
240 806 </style>
807 + <?php } // End if the style block has content. ?>
241 808 <?php
242 809 if ( 'sureforms_form' !== $current_post_type && true === $show_title_current_page ) {
243 810 $title = ! empty( get_the_title( (int) $id ) ) ? get_the_title( (int) $id ) : '';
811 + $title = String_Translator::get_instance()->translate_form_title( (int) $id, $title );
244 812 ?>
245 813 <h2 class="srfm-form-title"><?php echo esc_html( $title ); ?></h2>
246 814 <?php
247 815 }
816 +
817 + // Password protected form check.
818 + if ( $post && post_password_required( $post ) ) {
819 + // Define allowed HTML tags for password form output.
820 + $allowed_password_form_tags = [
821 + 'form' => [
822 + 'action' => true,
823 + 'method' => true,
824 + 'class' => true,
825 + 'id' => true,
826 + ],
827 + 'label' => [
828 + 'for' => true,
829 + 'class' => true,
830 + ],
831 + 'input' => [
832 + 'type' => true,
833 + 'name' => true,
834 + 'id' => true,
835 + 'class' => true,
836 + 'value' => true,
837 + 'size' => true,
838 + 'placeholder' => true,
839 + 'required' => true,
840 + ],
841 + 'p' => [
842 + 'class' => true,
843 + 'style' => true,
844 + ],
845 + 'button' => [
846 + 'type' => true,
847 + 'name' => true,
848 + 'class' => true,
849 + 'id' => true,
850 + 'style' => true,
851 + ],
852 + 'div' => [
853 + 'class' => true,
854 + 'id' => true,
855 + 'style' => true,
856 + ],
857 + 'span' => [
858 + 'class' => true,
859 + 'aria-hidden' => true,
860 + ],
861 + 'svg' => [
862 + 'xmlns' => true,
863 + 'width' => true,
864 + 'height' => true,
865 + 'viewBox' => true,
866 + 'fill' => true,
867 + ],
868 + 'path' => [
869 + 'd' => true,
870 + 'stroke' => true,
871 + 'stroke-opacity' => true,
872 + 'stroke-width' => true,
873 + 'stroke-linecap' => true,
874 + 'stroke-linejoin' => true,
875 + ],
876 + ];
877 + echo wp_kses( get_the_password_form( $post ), $allowed_password_form_tags );
878 + ?>
879 + </div>
880 + <?php
881 + self::$current_block_attrs = [];
882 + return ob_get_clean();
883 + }
884 + $submit_token = Submit_Token::generate( (int) $id );
885 + // Separately namespaced from the submission token: this one is only good
886 + // for incrementing a view counter, so scraping it from the page buys an
887 + // attacker nothing beyond what the beacon already does, and it cannot be
888 + // replayed against the submit endpoint.
889 + $view_token = Submit_Token::generate( (int) $id, Submit_Token::NAMESPACE_VIEW );
890 +
891 + // Admin-only shortcut into the form editor. Emitted here, immediately
892 + // above the <form>, so it occupies its own row in normal flow and can
893 + // never overlap a field. Already inside the `.srfm-form-container`
894 + // branch, so a zero-block form (no container) never reaches here and
895 + // cannot emit an orphaned pill. Works for every embed method (block,
896 + // shortcode, widget) because they all render through this function.
897 + self::render_edit_form_button( (int) $id );
898 +
248 899 ?>
249 900 <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 ' : '' ); ?>"
250 - 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' ) ); ?>" nonce="<?php echo esc_attr( wp_create_nonce( 'unique_validation_nonce' ) ); ?>"
901 + 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 ); ?>"<?php echo $auto_advance ? ' data-srfm-auto-advance="1"' : ''; ?><?php echo $auto_advance_hide_next ? ' data-srfm-hide-next="1"' : ''; ?>
251 902 >
252 903 <?php
253 - wp_nonce_field( 'srfm-form-submit', 'sureforms_form_submit' );
254 - $global_setting_options = get_option( 'srfm_general_settings_options' );
904 + // Submission security is handled via the HMAC token in data-submit-token.
905 + $global_setting_options = get_option( 'srfm_security_settings_options' );
255 906 $honeypot_spam = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_honeypot'] ) ? $global_setting_options['srfm_honeypot'] : '';
256 907
257 908 if ( $is_page_break && 'none' !== $page_break_progress_type ) {
258 909 do_action( 'srfm_page_break_header', $id );
@@ -259,118 +910,280 @@
259 910 }
260 911 ?>
261 912
262 913 <input type="hidden" value="<?php echo esc_attr( Helper::get_string_value( $id ) ); ?>" name="form-id">
914 + <?php
915 + /*
916 + * Submission language. Captured client-side because the REST submit endpoint
917 + * loses WPML's URL-based language context. The value is baked into the markup
918 + * at render time, so accurate entry-language tagging requires the page cache to
919 + * be language-aware (the default for WPML's language-per-URL modes). At submit
920 + * time the server re-validates this value against the active language list and
921 + * falls back to its own current_language() when it can't be confirmed
922 + * (see Form_Submit::is_known_language()), so a stale/forged value is never
923 + * trusted blindly.
924 + */
925 + ?>
926 + <input type="hidden" value="<?php echo esc_attr( Multilingual_Manager::get_instance()->provider()->current_language() ); ?>" name="srfm-form-language">
263 927 <input type="hidden" value="" name="srfm-sender-email-field" id="srfm-sender-email">
264 928 <input type="hidden" value="<?php echo esc_attr( Helper::get_string_value( $is_page_break ) ); ?>" id="srfm-page-break">
265 - <?php if ( $honeypot_spam ) : ?>
929 + <?php if ( $honeypot_spam ) { ?>
266 930 <input type="hidden" value="" name="srfm-honeypot-field">
267 - <?php endif; ?>
268 - <?php
269 -
931 + <?php
932 + }
933 + self::common_error_message( 'head' );
270 934 if ( $is_page_break ) {
271 935 do_action( 'srfm_page_break_pagination', $post, $id );
272 - } else {
273 - // phpcs:ignore
936 + } elseif ( ! apply_filters( 'srfm_use_custom_field_content', false ) ) {
937 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is filtered and sanitized by WordPress core blocks and filters.
274 938 echo $content;
275 - // phpcs:ignoreEnd
276 939 }
277 - ?>
278 - <?php if ( 0 !== $block_count && ! $is_inline_button || $is_page_break ) : ?>
279 - <?php if ( ! empty( $security_type ) && 'none' !== $security_type ) : ?>
280 - <div class="srfm-captcha-container">
281 - <?php if ( is_string( $google_captcha_site_key ) && ! empty( $google_captcha_site_key ) && 'g-recaptcha' === $security_type ) : ?>
282 940
283 - <?php if ( 'v2-checkbox' === $recaptcha_version ) : ?>
284 - <?php
285 - wp_enqueue_script( 'google-recaptcha', 'https://www.google.com/recaptcha/api.js', [], SRFM_VER, true );
286 - ?>
287 - <div class='g-recaptcha' data-callback="onSuccess" recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>" data-sitekey="<?php echo esc_attr( strval( $google_captcha_site_key ) ); ?>" ></div>
288 - <?php endif; ?>
941 + do_action( 'srfm_after_field_content', $post, $id );
289 942
290 - <?php if ( 'v2-invisible' === $recaptcha_version ) : ?>
291 - <?php
292 - wp_enqueue_script( 'google-recaptcha-invisible', 'https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', [ SRFM_SLUG . '-form-submit' ], SRFM_VER, true );
293 - ?>
294 - <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>
295 - <?php endif; ?>
943 + $captcha_container_hidden_class = ! empty( $google_captcha_site_key ) && ( 'v3-reCAPTCHA' === $recaptcha_version || 'v2-invisible' === $recaptcha_version ) ? 'srfm-display-none' : '';
296 944
297 - <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) : ?>
298 - <?php wp_enqueue_script( 'srfm-google-recaptchaV3', 'https://www.google.com/recaptcha/api.js?render=' . esc_js( $google_captcha_site_key ), [], SRFM_VER, true ); ?>
299 - <?php endif; ?>
945 + ?>
946 + <?php if ( $should_show_submit_button && ! empty( $security_type ) && 'none' !== $security_type ) { ?>
947 + <div class="srfm-captcha-container <?php echo esc_attr( $captcha_container_hidden_class ); ?>">
300 948
301 - <?php endif; ?>
302 949 <?php
303 950
304 - if ( 'cf-turnstile' === $security_type ) :
305 - // Cloudflare Turnstile script.
306 - wp_enqueue_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
307 - SRFM_SLUG . '-cf-turnstile',
308 - 'https://challenges.cloudflare.com/turnstile/v0/api.js',
309 - [],
310 - null,
311 - [
312 - false,
313 - 'defer' => true,
314 - ]
315 - );
316 - ?>
317 - <div id="srfm-cf-sitekey" class="cf-turnstile" data-callback="onSuccess" data-theme="<?php echo esc_attr( $srfm_cf_appearance_mode ); ?>" data-sitekey="<?php echo esc_attr( $srfm_cf_turnstile_site_key ); ?>"></div>
318 - <?php
319 - endif;
951 + if ( 'g-recaptcha' === $security_type ) {
952 + self::get_google_captcha_script( $recaptcha_version, $google_captcha_site_key );
953 + }
320 954
321 - if ( 'hcaptcha' === $security_type ) :
322 - // hCaptcha script.
323 - wp_enqueue_script( 'hcaptcha', 'https://js.hcaptcha.com/1/api.js', [], null, [ 'strategy' => 'defer' ] ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
324 - ?>
325 - <div id="srfm-hcaptcha-sitekey" data-callback="onSuccess" class="h-captcha" data-sitekey="<?php echo esc_attr( $srfm_hcaptcha_site_key ); ?>"></div>
326 - <?php
327 - endif;
955 + if ( 'cf-turnstile' === $security_type ) {
956 + self::get_cf_turnstile_script( $srfm_cf_appearance_mode, $srfm_cf_turnstile_site_key );
957 + }
958 +
959 + if ( 'hcaptcha' === $security_type ) {
960 + self::get_h_captcha_script( $srfm_hcaptcha_site_key );
961 + }
328 962 ?>
329 - <div class="srfm-validation-error" id="captcha-error" style="display: none;"><?php echo esc_attr__( 'Please verify that you are not a robot.', 'sureforms' ); ?></div>
963 + <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>
330 964 </div>
331 - <?php endif; ?>
965 + <?php } ?>
332 966
333 967 <?php
334 968 if ( $is_page_break ) {
335 969 do_action( 'srfm_page_break_btn', $id );
336 970 }
971 + $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 );
337 972 ?>
338 973
339 - <div class="srfm-submit-container <?php echo esc_attr( $is_page_break ? 'hide' : '' ); ?>">
340 - <div style="width: <?php echo esc_attr( $full ? '100%;' : ';' ); ?> text-align: <?php echo esc_attr( $submit_button_alignment ? $submit_button_alignment : 'left' ); ?>" class="wp-block-button">
341 - <button style="width:<?php echo esc_attr( $full ? '100%;' : '' ); ?>" id="srfm-submit-btn"class="<?php echo esc_attr( '1' === $btn_from_theme ? 'wp-block-button__link' : 'srfm-btn-frontend srfm-button srfm-submit-button' ); ?><?php echo 'v3-reCAPTCHA' === $recaptcha_version ? ' g-recaptcha' : ''; ?>"
342 - <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) : ?>
343 - recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>"
344 - data-sitekey="<?php echo esc_attr( $google_captcha_site_key ); ?>"
345 - <?php endif; ?>
346 - >
347 - <div class="srfm-submit-wrap">
348 - <?php echo esc_html( $button_text ); ?>
349 - <div class="srfm-loader"></div>
350 - </div>
351 - </button>
974 + <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;' : ''; ?>">
975 + <div style="width: <?php echo esc_attr( $full ? '100%' : '' ); ?>; text-align: <?php echo esc_attr( $submit_button_alignment ); ?>" class="wp-block-button">
976 + <?php do_action( 'srfm_before_submit_button', $id ); ?>
977 + <?php if ( $should_show_submit_button ) { ?>
978 + <button style="<?php echo esc_attr( $full ? 'width: 100%;' : '' ); ?>" id="srfm-submit-btn" class="<?php echo esc_attr( implode( ' ', array_filter( $srfm_button_classes ) ) ); ?>"
979 + <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) { ?>
980 + data-callback="recaptchaCallback"
981 + data-error-callback="onGCaptchaV3Error"
982 + recaptcha-type="<?php echo esc_attr( $recaptcha_version ); ?>"
983 + data-sitekey="<?php echo esc_attr( $google_captcha_site_key ); ?>"
984 + <?php } ?>
985 + >
986 + <div class="srfm-submit-wrap">
987 + <?php echo esc_html( $button_text ); ?>
988 + <div class="srfm-loader"></div>
989 + </div>
990 + </button>
991 + <?php } ?>
992 + <?php do_action( 'srfm_after_submit_button', $id ); ?>
352 993 </div>
353 994 </div>
354 - <?php endif; ?>
355 - <p id="srfm-error-message" class="srfm-error-message" hidden="true"><?php echo esc_html__( 'There was an error trying to submit your form. Please try again.', 'sureforms' ); ?></p>
995 + <?php
996 +
997 + echo wp_kses_post(
998 + apply_filters(
999 + 'srfm_after_submit_button_content',
1000 + '',
1001 + [
1002 + 'id' => $id,
1003 + 'should_show_submit_button' => $should_show_submit_button,
1004 + 'button_text' => $button_text,
1005 + 'submit_button_alignment' => $submit_button_alignment,
1006 + 'full' => $full,
1007 + 'btn_from_theme' => $btn_from_theme,
1008 + 'is_page_break' => $is_page_break,
1009 + 'recaptcha_version' => $recaptcha_version,
1010 + 'google_captcha_site_key' => $google_captcha_site_key,
1011 + 'srfm_button_classes' => $srfm_button_classes,
1012 + ]
1013 + )
1014 + );
1015 + }
1016 + self::common_error_message( 'footer' );
1017 + ?>
356 1018 </form>
357 1019 <div class="srfm-single-form srfm-success-box in-page">
358 1020 <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>
359 1021 </div>
360 1022 <?php
361 - $page_url = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
362 - $path = Helper::get_string_value( wp_parse_url( $page_url, PHP_URL_PATH ) );
363 - $segments = explode( '/', $path );
364 - $form_path = isset( $segments[1] ) ? $segments[1] : '';
1023 + // Add preview script for real-time styling updates from block editor.
1024 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a preview context, nonce not required.
1025 + if ( isset( $_GET['form_preview'] ) && 'true' === $_GET['form_preview'] && isset( $container_id ) ) {
1026 + self::enqueue_preview_styling_script( $container_id );
1027 + }
1028 + ?>
1029 + </div>
1030 + <?php
1031 + self::$current_block_attrs = [];
1032 + return ob_get_clean();
1033 + }
1034 +
1035 + /**
1036 + * Generate HCaptcha script markup
1037 + *
1038 + * @param string $srfm_hcaptcha_site_key site key.
1039 + * @since 1.7.0
1040 + * @return void
1041 + */
1042 + public static function get_h_captcha_script( $srfm_hcaptcha_site_key ) {
1043 + if ( ! empty( $srfm_hcaptcha_site_key ) ) {
1044 + // hCaptcha script.
1045 + wp_enqueue_script( 'hcaptcha', 'https://js.hcaptcha.com/1/api.js', [], null, [ 'strategy' => 'defer' ] ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1046 + ?>
1047 + <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>
1048 + <?php
1049 + } else {
1050 + Helper::render_missing_sitekey_error( 'HCaptcha' );
365 1051 }
1052 + }
1053 +
1054 + /**
1055 + * Generate Google Recaptcha script markup
1056 + *
1057 + * @param string $recaptcha_version reCAPTCHA version.
1058 + * @param string $google_captcha_site_key site key.
1059 + * @since 1.7.0
1060 + * @return void
1061 + */
1062 + public static function get_google_captcha_script( $recaptcha_version, $google_captcha_site_key ) {
1063 +
1064 + if ( empty( $google_captcha_site_key ) ) {
1065 + Helper::render_missing_sitekey_error( 'Google reCAPTCHA' );
1066 + return;
1067 + }
1068 +
1069 + if ( 'v2-checkbox' === $recaptcha_version ) {
1070 + ?>
1071 + <?php
1072 + wp_enqueue_script( 'google-recaptcha', 'https://www.google.com/recaptcha/api.js', [], SRFM_VER, true );
1073 + ?>
1074 + <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>
1075 + <?php } ?>
1076 +
1077 + <?php if ( 'v2-invisible' === $recaptcha_version ) { ?>
1078 + <?php
1079 + wp_enqueue_script( 'google-recaptcha-invisible', 'https://www.google.com/recaptcha/api.js?onload=recaptchaCallback&render=explicit', [ SRFM_SLUG . '-form-submit' ], SRFM_VER, true );
1080 + ?>
1081 + <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>
1082 + <?php } ?>
1083 +
1084 + <?php if ( 'v3-reCAPTCHA' === $recaptcha_version ) { ?>
1085 + <?php
1086 + // 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.
1087 + wp_enqueue_script(
1088 + 'srfm-google-recaptchaV3',
1089 + 'https://www.google.com/recaptcha/api.js?render=' . $google_captcha_site_key,
1090 + [],
1091 + null,
1092 + true
1093 + );
1094 + // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
1095 + ?>
1096 + <?php
1097 + }
1098 + }
1099 +
1100 + /**
1101 + * Generate Cloudflare Turnstile script markup
1102 + *
1103 + * @param string $srfm_cf_appearance_mode appearance mode.
1104 + * @param string $srfm_cf_turnstile_site_key site key.
1105 + * @since 1.7.0
1106 + * @return void
1107 + */
1108 + public static function get_cf_turnstile_script( $srfm_cf_appearance_mode, $srfm_cf_turnstile_site_key ) {
1109 + if ( ! empty( $srfm_cf_turnstile_site_key ) ) {
1110 + // Cloudflare Turnstile script.
1111 + // 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.
1112 + wp_enqueue_script(
1113 + SRFM_SLUG . '-cf-turnstile',
1114 + 'https://challenges.cloudflare.com/turnstile/v0/api.js',
1115 + [],
1116 + null,
1117 + [
1118 + 'strategy' => 'defer',
1119 + ]
1120 + );
1121 + // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion, PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
1122 + ?>
1123 + <!-- The callback methods below are available on frontend.js. onTurnstileError displays and error in place of recaptcha dialog. -->
1124 + <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>
1125 + <?php
1126 + } else {
1127 + Helper::render_missing_sitekey_error( 'Turnstile' );
1128 + }
1129 + }
1130 +
1131 + /**
1132 + * Generate common error message markup
1133 + *
1134 + * @param string $position position of the error message.
1135 + * @since 1.5.0
1136 + * @return void
1137 + */
1138 + public static function common_error_message( $position = 'footer' ) {
1139 + $icon = Helper::fetch_svg( 'info_circle', '', 'aria-hidden="true"' );
1140 + $classes = "srfm-common-error-message srfm-error-message srfm-{$position}-error";
366 1141 ?>
367 - </div>
1142 + <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>
368 1143 <?php
369 - return ob_get_clean();
370 1144 }
371 1145
372 1146 /**
1147 + * Enqueue the preview styling script for real-time updates from block editor.
1148 + *
1149 + * @param string $container_id The form container ID selector.
1150 + * @since 2.7.0
1151 + * @return void
1152 + */
1153 + public static function enqueue_preview_styling_script( $container_id ) {
1154 + $script_asset_path = SRFM_DIR . 'assets/build/previewStyling.asset.php';
1155 + $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [
1156 + 'dependencies' => [],
1157 + 'version' => SRFM_VER,
1158 + ];
1159 +
1160 + wp_enqueue_script(
1161 + SRFM_SLUG . '-preview-styling',
1162 + SRFM_URL . 'assets/build/previewStyling.js',
1163 + $script_asset['dependencies'],
1164 + $script_asset['version'],
1165 + true
1166 + );
1167 +
1168 + wp_localize_script(
1169 + SRFM_SLUG . '-preview-styling',
1170 + 'srfmPreviewStyling',
1171 + [
1172 + 'containerId' => $container_id,
1173 + 'fieldSpacingVars' => Helper::get_css_vars(),
1174 + ]
1175 + );
1176 +
1177 + /**
1178 + * Action to allow Pro to enqueue additional preview styling scripts.
1179 + *
1180 + * @since 2.7.0
1181 + */
1182 + do_action( 'srfm_enqueue_preview_styling_scripts' );
1183 + }
1184 +
1185 + /**
373 1186 * Generate form confirmation markup
374 1187 *
375 1188 * @param array<mixed> $form_data contains form data.
376 1189 * @param array<mixed> $submission_data contains submission data.
@@ -384,11 +1197,22 @@
384 1197 if ( empty( $form_data ) ) {
385 1198 return $confirmation_message;
386 1199 }
387 1200
388 - $form_confirmation = isset( $form_data['form-id'] ) ?
389 - get_post_meta( Helper::get_integer_value( $form_data['form-id'] ), '_srfm_form_confirmation' ) : null;
1201 + $form_id = isset( $form_data['form-id'] ) ? Helper::get_integer_value( $form_data['form-id'] ) : 0;
1202 + $form_confirmation = get_post_meta( $form_id, '_srfm_form_confirmation' );
390 1203
1204 + /**
1205 + * Filter the form confirmation data.
1206 + * Allows conditional confirmations to override the default confirmation settings.
1207 + *
1208 + * @param mixed $form_confirmation The form confirmation data from post meta.
1209 + * @param int $form_id The form ID.
1210 + * @param array $submission_data The submission data.
1211 + * @since 2.4.0
1212 + */
1213 + $form_confirmation = apply_filters( 'srfm_form_confirmation_data', $form_confirmation, $form_id, $submission_data );
1214 +
391 1215 if ( ! is_array( $form_confirmation ) ) {
392 1216 return $confirmation_message;
393 1217 }
394 1218
@@ -395,15 +1219,336 @@
395 1219 $confirmation_data = is_array( $form_confirmation[0] ) && isset( $form_confirmation[0][0] ) ? $form_confirmation[0][0] : null;
396 1220
397 1221 if ( is_array( $form_confirmation ) && isset( $confirmation_data['message'] ) && is_string( $confirmation_data['message'] ) ) {
398 1222 $confirmation_message = $confirmation_data['message'];
1223 + $confirmation_message = String_Translator::get_instance()->translate_confirmation_message( (int) $form_id, 0, $confirmation_message );
399 1224 }
400 1225 if ( empty( $submission_data ) ) {
401 1226 return $confirmation_message;
402 1227 }
403 1228 $smart_tags = new Smart_Tags();
404 - $confirmation_message = $smart_tags->process_smart_tags( $confirmation_data['message'], $submission_data, $form_data );
1229 + $confirmation_message = $smart_tags->process_smart_tags( $confirmation_message, $submission_data, $form_data );
405 1230
406 - return $confirmation_message;
1231 + /**
1232 + * Filter whether confirmation message links should open in a new tab.
1233 + *
1234 + * @since 2.5.2
1235 + *
1236 + * @param bool $open_in_new_tab Whether links open in a new tab. Default true.
1237 + */
1238 + $open_in_new_tab = (bool) apply_filters( 'srfm_confirmation_links_open_in_new_tab', true );
407 1239
1240 + $markup = Helper::strip_js_attributes(
1241 + apply_filters( 'srfm_after_submit_confirmation_message', $confirmation_message, $form_data, $submission_data ),
1242 + ! $open_in_new_tab
1243 + );
1244 +
1245 + if ( false !== strpos( $markup, 'src="image/svg+xml;base64' ) ) {
1246 + // Handle Form Confirmation SVGs separately. We have planned to improve it in the future replacing it with image URL.
1247 + $normalized_string = preg_replace( '/src="image\/svg\+xml;base64/', 'src="data:image/svg+xml;base64', $markup );
1248 +
1249 + if ( is_string( $normalized_string ) ) {
1250 + $markup = $normalized_string;
1251 + }
1252 + }
1253 +
1254 + return $markup;
1255 + }
1256 +
1257 + /**
1258 + * Get redirect url for form incase of different page or custom url is selected.
1259 + *
1260 + * @param array<mixed> $form_data contains form data.
1261 + * @param array<mixed> $submission_data contains submission data.
1262 + * @since 1.0.2
1263 + * @return string|false
1264 + */
1265 + public static function get_redirect_url( $form_data = [], $submission_data = [] ) {
1266 + $redirect_url = '';
1267 +
1268 + if ( empty( $form_data ) ) {
1269 + return $redirect_url;
1270 + }
1271 +
1272 + $form_id = isset( $form_data['form-id'] ) ? Helper::get_integer_value( $form_data['form-id'] ) : 0;
1273 + $form_confirmation = get_post_meta( $form_id, '_srfm_form_confirmation' );
1274 +
1275 + /**
1276 + * Filter the form confirmation data.
1277 + * Allows conditional confirmations to override the default confirmation settings.
1278 + *
1279 + * @param mixed $form_confirmation The form confirmation data from post meta.
1280 + * @param int $form_id The form ID.
1281 + * @param array $submission_data The submission data.
1282 + * @since 2.4.0
1283 + */
1284 + $form_confirmation = apply_filters( 'srfm_form_confirmation_data', $form_confirmation, $form_id, $submission_data );
1285 +
1286 + if ( ! is_array( $form_confirmation ) ) {
1287 + return $redirect_url;
1288 + }
1289 +
1290 + $confirmation_data = is_array( $form_confirmation[0] ) && isset( $form_confirmation[0][0] ) ? $form_confirmation[0][0] : null;
1291 +
1292 + $page_url = $confirmation_data['page_url'] ?? '';
1293 + $custom_url = $confirmation_data['custom_url'] ?? '';
1294 + $confirmation_type = $confirmation_data['confirmation_type'] ?? '';
1295 +
1296 + if ( 'different page' === $confirmation_type ) {
1297 + $redirect_url = esc_url_raw( $page_url );
1298 + } elseif ( 'custom url' === $confirmation_type ) {
1299 + $redirect_url = esc_url_raw( $custom_url );
1300 + }
1301 +
1302 + if ( empty( $redirect_url ) ) {
1303 + return $redirect_url;
1304 + }
1305 +
1306 + if ( empty( $confirmation_data['enable_query_params'] ) || true !== $confirmation_data['enable_query_params'] ) {
1307 + return $redirect_url;
1308 + }
1309 +
1310 + if ( empty( $confirmation_data['query_params'] ) && ! is_array( $confirmation_data['query_params'] ) ) {
1311 + return $redirect_url;
1312 + }
1313 +
1314 + $query_params = [];
1315 + foreach ( $confirmation_data['query_params'] as $params ) {
1316 + if ( is_array( $params ) && ! empty( array_keys( $params ) ) && ! empty( array_values( $params ) ) ) {
1317 + $query_params[ sanitize_text_field( array_keys( $params )[0] ) ] = sanitize_text_field( array_values( $params )[0] );
1318 + }
1319 + }
1320 +
1321 + $redirect_url = add_query_arg( $query_params, $redirect_url );
1322 +
1323 + if ( ! empty( $submission_data ) ) {
1324 + $smart_tags = new Smart_Tags();
1325 + // Adding upload_format_type = 'raw' to retrieve urls as comma separated values.
1326 + $form_data['upload_format_type'] = 'raw';
1327 + // Skip auto-linking URLs in smart tag values — redirect query params need raw values, not HTML.
1328 + $form_data['smart_tag_context'] = 'redirect';
1329 +
1330 + /*
1331 + * Resolve smart tags in the URL, normalize the multi-value delimiters
1332 + * left behind by the substitution, then decode any HTML entities.
1333 + *
1334 + * Multi-select dropdown values are packed as "Red | Blue" by the frontend
1335 + * (srfmUtility.prepareValue in assets/js/unminified/frontend.js), and
1336 + * checkbox multi-choice values are rendered as "Red<br>Blue" by
1337 + * Smart_Tags::parse_form_input. Neither delimiter is URL-friendly as-is:
1338 + * " | " leaks whitespace into the query string and "<br>" gets mangled
1339 + * by esc_url_raw below. Normalize both to a plain "|" so the final
1340 + * redirect URL carries a clean, URL-safe list that the receiver can
1341 + * split on "|".
1342 + *
1343 + * The str_replace runs before html_entity_decode so that any literal
1344 + * "<br>" character sequence inside an option label — which
1345 + * Smart_Tags::parse_form_input escapes to "&lt;br&gt;" before joining
1346 + * — survives intact. Only the actual delimiter (the unescaped "<br>"
1347 + * emitted by the implode) is converted to a pipe; html_entity_decode
1348 + * then restores the option's original text.
1349 + */
1350 + $resolved_redirect_url = Helper::get_string_value( $smart_tags->process_smart_tags( $redirect_url, $submission_data, $form_data ) );
1351 + $multi_value_delimiters = [ '<br>', ' | ' ];
1352 + $redirect_url = html_entity_decode( str_replace( $multi_value_delimiters, '|', $resolved_redirect_url ) );
1353 + }
1354 +
1355 + return esc_url_raw( apply_filters( 'srfm_after_submit_redirect_url', $redirect_url ) );
1356 + }
1357 +
1358 + /**
1359 + * Print the admin-only "Edit Form" shortcut on an embedded form.
1360 + *
1361 + * Renders a small pill link that opens the block editor for this form, on its
1362 + * own right-aligned row directly above the form.
1363 + *
1364 + * It sits in normal flow rather than being absolutely positioned over the
1365 + * form's top-right corner, which is what it used to do. An overlay can only
1366 + * avoid the fields when the container happens to have enough top padding —
1367 + * with the default theme styling it landed on top of the first row's last
1368 + * field (#3062). Flow layout cannot overlap anything by construction, at any
1369 + * width, with any theme. The cost is that the form shifts down by the pill's
1370 + * height, which happens only for users who can edit the form; the markup and
1371 + * its styles remain entirely absent from the DOM for everyone else, so no
1372 + * regular visitor sees a layout change.
1373 + *
1374 + * Admin-only by construction: the `sureforms_form` CPT registers with
1375 + * `map_meta_cap => false`, so `edit_post` collapses to a blanket
1376 + * `manage_options` check with no per-post component — an editor never sees the
1377 + * pill on any form. For every other viewer the markup and its styles are
1378 + * entirely absent from the DOM.
1379 + *
1380 + * The stylesheet is attached to a registered inline-only handle so `WP_Styles`
1381 + * dedupes it by handle (surviving a discarded `the_content` pass, e.g. an SEO
1382 + * plugin building `og:description` during `wp_head`) and it survives a strict
1383 + * `style-src` CSP. It is not cache-signalled here: the payload is only a
1384 + * `wp-admin/post.php?post=N` link an anonymous visitor cannot act on, and a
1385 + * `DONOTCACHEPAGE` define from a fragment renderer is both inert on the normal
1386 + * (headers-already-sent) path and an irreversible process-global side effect.
1387 + *
1388 + * @param int $form_id Form post ID.
1389 + *
1390 + * @return void
1391 + * @since 2.12.4
1392 + */
1393 + public static function render_edit_form_button( $form_id ) {
1394 + $form_id = absint( $form_id );
1395 +
1396 + // Only for real SureForms forms — the [sureforms] shortcode accepts any
1397 + // post ID, and a non-form target would map `edit_post` normally and leak
1398 + // the pill to an ordinary editor.
1399 + if ( 0 === $form_id || ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $form_id ) ) {
1400 + return;
1401 + }
1402 +
1403 + // Capability gate first, before the suppression filter, so no work is done
1404 + // for the anonymous visitors who make up almost every page view.
1405 + if ( ! current_user_can( 'edit_post', $form_id ) ) {
1406 + return;
1407 + }
1408 +
1409 + // Contexts where the pill is redundant or wrong:
1410 + // - the single-form / Instant Form page, where the form IS the whole page
1411 + // and the admin bar already links to its editor. This is also what
1412 + // suppresses the block editor's preview — that preview is an iframe to
1413 + // the form's own permalink (an ordinary front-end request), NOT a REST
1414 + // render, so `is_singular` is the load-bearing guard there;
1415 + // - any admin / AJAX / REST / JSON request, or a feed (the markup would
1416 + // otherwise land inside `content:encoded` CDATA).
1417 + if (
1418 + is_singular( SRFM_FORMS_POST_TYPE )
1419 + || is_admin()
1420 + || wp_doing_ajax()
1421 + || wp_is_json_request()
1422 + || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
1423 + || is_feed()
1424 + ) {
1425 + return;
1426 + }
1427 +
1428 + // Page-builder editor canvases render the form directly (not over REST),
1429 + // where their own element-edit handles would collide with the pill.
1430 + // `$instance` is checked as well as the class name: Elementor declares
1431 + // `public static $instance = null` and only populates it on boot, so the
1432 + // class can exist while the singleton is still null. Dereferencing it then
1433 + // is a fatal Error, not a warning, and guarding only on class_exists() left
1434 + // that reachable — test-generate-form-markup.php hit it. The bundled stub
1435 + // types $instance as non-nullable, which is why PHPStan reads the isset()
1436 + // as redundant and has to be told otherwise.
1437 + //
1438 + // ->editor is checked for the same reason one level down: Elementor assigns it
1439 + // in init_components() on `init`, while the singleton itself is created on
1440 + // `plugins_loaded`. Between those two hooks $instance is set and ->editor is
1441 + // still null, so checking only the singleton reproduces the original fatal a
1442 + // property later.
1443 + // @phpstan-ignore-next-line -- Stub disagrees with runtime; see above.
1444 + if ( class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
1445 + return;
1446 + }
1447 + if ( function_exists( 'bricks_is_builder' ) && bricks_is_builder() ) {
1448 + return;
1449 + }
1450 +
1451 + /**
1452 + * Allow integrations to suppress the admin "Edit Form" shortcut entirely.
1453 + *
1454 + * @param bool $show Whether to render the shortcut. Default true.
1455 + * @param int $form_id Form post ID.
1456 + *
1457 + * @since 2.12.4
1458 + */
1459 + if ( ! apply_filters( 'srfm_show_edit_form_button', true, $form_id ) ) {
1460 + return;
1461 + }
1462 +
1463 + $edit_link = get_edit_post_link( $form_id, 'raw' );
1464 +
1465 + if ( empty( $edit_link ) ) {
1466 + return;
1467 + }
1468 +
1469 + // Attribution marker read back by Admin::maybe_track_edit_form_button_click()
1470 + // when the editor loads. Added before the filter below so an integration that
1471 + // replaces the link wholesale drops the marker with it, rather than having our
1472 + // query arg appended to a third-party URL.
1473 + // 'url' context, not the default 'display': the latter returns &amp;-escaped
1474 + // separators, and feeding those to add_query_arg() only round-trips because
1475 + // build_query() happens to re-emit the mangled `amp;action` key verbatim. The
1476 + // raw form has no such dependency, and esc_url() below still escapes on output.
1477 + $edit_link = add_query_arg( self::EDIT_FORM_BUTTON_SOURCE_ARG, 'embed', $edit_link );
1478 +
1479 + /**
1480 + * Filter the target of the admin "Edit Form" shortcut.
1481 + *
1482 + * @param string $edit_link Editor URL for the form.
1483 + * @param int $form_id Form post ID.
1484 + *
1485 + * @since 2.12.4
1486 + */
1487 + $edit_link = Helper::get_string_value( apply_filters( 'srfm_edit_form_button_link', $edit_link, $form_id ) );
1488 +
1489 + if ( '' === $edit_link ) {
1490 + return;
1491 + }
1492 +
1493 + // Registered inline-only handle: WP_Styles dedupes by handle across every
1494 + // embedded form and prints via print_late_styles() in the footer even when
1495 + // enqueued this late (during the_content).
1496 + $style_handle = 'srfm-edit-form-btn';
1497 + if ( ! wp_style_is( $style_handle, 'registered' ) ) {
1498 + wp_register_style( $style_handle, false, [], SRFM_VER );
1499 + wp_add_inline_style( $style_handle, self::get_edit_form_button_css() );
1500 + }
1501 + wp_enqueue_style( $style_handle );
1502 + ?>
1503 + <div class="srfm-edit-form-btn-wrap">
1504 + <a class="srfm-edit-form-btn" href="<?php echo esc_url( $edit_link ); ?>" target="_blank" rel="noopener noreferrer">
1505 + <svg width="20" height="20" 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>
1506 + <span><?php esc_html_e( 'Edit Form', 'sureforms' ); ?></span>
1507 + <span class="screen-reader-text"><?php esc_html_e( '(opens in a new tab)', 'sureforms' ); ?></span>
1508 + </a>
1509 + </div>
1510 + <?php
1511 + }
1512 +
1513 + /**
1514 + * Stylesheet for the admin "Edit Form" pill (#3029).
1515 + *
1516 + * The wrapper is a flow-level flex row rather than an absolute overlay, so the
1517 + * pill reserves its own space and cannot cover a field (#3062). `justify-content`
1518 + * uses the logical `flex-end`, which follows the writing direction and is
1519 + * therefore RTL-correct without a separate rule.
1520 + *
1521 + * No `position: relative` on the container any more: that rule existed solely to
1522 + * be the positioning context for the old overlay.
1523 + *
1524 + * @return string
1525 + * @since 2.12.4
1526 + */
1527 + private static function get_edit_form_button_css() {
1528 + return '
1529 + .srfm-edit-form-btn-wrap {
1530 + display: flex;
1531 + justify-content: flex-end;
1532 + margin-block-end: 8px;
1533 + }
1534 + .srfm-edit-form-btn {
1535 + display: inline-flex;
1536 + align-items: center;
1537 + gap: 6px;
1538 + padding: 6px 12px;
1539 + font-size: 13px;
1540 + font-weight: 500;
1541 + line-height: 1;
1542 + color: #1e293b;
1543 + background: #ffffff;
1544 + border: 1px solid #e2e8f0;
1545 + border-radius: 9999px;
1546 + box-shadow: 0 2px 6px rgba( 0, 0, 0, 0.12 );
1547 + text-decoration: none;
1548 + }
1549 + .srfm-edit-form-btn:hover { border-color: #cbd5e1; color: #0f172a; }
1550 + .srfm-edit-form-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
1551 + .srfm-edit-form-btn svg { width: 14px; height: 14px; }
1552 + ';
408 1553 }
409 1554 }