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