PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | includes/page-form-builder/field-packs/static-text/field-static-text.php +286 -35 11.1 → 11.9 View file →
@@ -1,4 +1,4 @@
1 1 <?php
2 2 /**
3 3 * WPBC BFB Pack: Static Text (label / paragraph / heading) — Schema-driven, Factory Inspector
4 4 *
@@ -3,11 +3,12 @@
3 3 * WPBC BFB Pack: Static Text (label / paragraph / heading) — Schema-driven, Factory Inspector
4 4 *
5 5 * Purpose:
6 6 * - Non-interactive text block for headings, labels, paragraphs, etc.
7 - * - One pack type "static_text" with props: text, tag, align, bold, italic, html_allowed, nl2br, cssclass_extra, html_id, help.
8 - * - Inspector is Factory-driven (no WP templates). Preview is rendered by a pure JS renderer class.
9 - * - Exporter (JS) emits plain HTML (optionally wrapped with id/class) + optional help.
7 + * - One pack type "static_text" with props: text, links, tag, align, bold, italic, html_allowed, nl2br, cssclass_extra, html_id, help.
8 + * - Inspector is Factory-driven (no WP templates). Preview is rendered by a pure JS renderer class.
9 + * - Link tokens such as {terms} are replaced with sanitized link definitions.
10 + * - Exporter (JS) emits escaped HTML (optionally wrapped with id/class) + optional help.
10 11 *
11 12 * Contracts Used:
12 13 * - Filter: wpbc_bfb_register_field_packs
13 14 * - Action: wpbc_enqueue_js_field_pack
@@ -12,17 +13,19 @@
12 13 * - Filter: wpbc_bfb_register_field_packs
13 14 * - Action: wpbc_enqueue_js_field_pack
14 15 * - Action: wpbc_bfb_palette_register_items (optional palette registration)
15 16 *
16 - * Files:
17 - * ../includes/page-form-builder/field-packs/static-text/field-static-text.php (this file)
18 - * ../includes/page-form-builder/field-packs/static-text/_out/static-text.js (renderer + exporter glue)
17 + * Files:
18 + * ../includes/page-form-builder/field-packs/static-text/field-static-text.php (this file)
19 + * ../includes/page-form-builder/field-packs/static-text/_out/static-text-links.js (link editor + token renderer)
20 + * ../includes/page-form-builder/field-packs/static-text/_out/static-text-links.css (link editor layout)
21 + * ../includes/page-form-builder/field-packs/static-text/_out/static-text.js (renderer + exporter glue)
19 22 *
20 23 * @package Booking Calendar
21 24 * @author wpdevelop
22 25 * @since 11.0.0
23 - * @modified 2025-11-08
24 - * @version 1.0.0
26 + * @modified 2026-09-25
27 + * @version 1.1.0
25 28 */
26 29
27 30 if ( ! defined( 'ABSPATH' ) ) {
28 31 exit;
@@ -30,11 +33,12 @@
30 33
31 34 /**
32 35 * Register the "static_text" field pack (Schema-driven Inspector).
33 36 *
34 - * Props stored in schema:
35 - * - text : string — content to show
36 - * - tag : string — allowed: p,label,span,small,div,h1,h2,h3,h4,h5,h6
37 + * Props stored in schema:
38 + * - text : string — content to show
39 + * - links : array — optional token-to-link definitions
40 + * - tag : string — allowed: p,label,span,small,div,h1,h2,h3,h4,h5,h6
37 41 * - align : string — left|center|right
38 42 * - bold : int|bool — 0|1
39 43 * - italic : int|bool — 0|1
40 44 * - html_allowed : int|bool — 0|1
@@ -76,12 +80,13 @@
76 80 'icon' => 'text',
77 81 'usage_key' => 'static_text',
78 82
79 83 // === Schema: defaults + coercion handled by Inspector/Factory ===
80 - 'schema' => array(
81 - 'props' => array(
82 - 'text' => array( 'type' => 'string', 'default' => __( 'Add your message here…', 'booking' ) ),
83 - 'tag' => array( 'type' => 'string', 'enum' => array_keys( $tag_options ), 'default' => 'p' ),
84 + 'schema' => array(
85 + 'props' => array(
86 + 'text' => array( 'type' => 'string', 'default' => __( 'Add your message here…', 'booking' ) ),
87 + 'links' => array( 'type' => 'array', 'default' => array() ),
88 + 'tag' => array( 'type' => 'string', 'enum' => array_keys( $tag_options ), 'default' => 'p' ),
84 89 'align' => array( 'type' => 'string', 'enum' => array_keys( $align_options ), 'default' => 'left' ),
85 90 'bold' => array( 'type' => 'int', 'min' => 0, 'max' => 1, 'default' => 0 ),
86 91 'italic' => array( 'type' => 'int', 'min' => 0, 'max' => 1, 'default' => 0 ),
87 92 'html_allowed' => array( 'type' => 'int', 'min' => 0, 'max' => 1, 'default' => 0 ),
@@ -110,12 +115,12 @@
110 115 'type' => 'textarea',
111 116 'key' => 'text',
112 117 'label' => __( 'Text', 'booking' ),
113 118 'rows' => 3,
114 - 'placeholder' => __( 'Enter text', 'booking' ) . '...',
115 - ),
116 - array(
117 - 'type' => 'select',
119 + 'placeholder' => __( 'Enter text', 'booking' ) . '...',
120 + ),
121 + array(
122 + 'type' => 'select',
118 123 'key' => 'tag',
119 124 'label' => __( 'HTML Tag', 'booking' ),
120 125 'options' => $tag_options,
121 126 ),
@@ -125,12 +130,18 @@
125 130 'label' => __( 'Alignment', 'booking' ),
126 131 'options' => $align_options,
127 132 ),
128 133 array( 'type' => 'checkbox', 'key' => 'bold', 'label' => __( 'Bold', 'booking' ) ),
129 - array( 'type' => 'checkbox', 'key' => 'italic', 'label' => __( 'Italic', 'booking' ) ),
130 - // array( 'type' => 'checkbox', 'key' => 'html_allowed', 'label' => __( 'Allow basic HTML', 'booking' ) ),
131 - array( 'type' => 'checkbox', 'key' => 'nl2br', 'label' => __( 'Convert newlines to <br> tag', 'booking' ) ),
132 - ),
134 + array( 'type' => 'checkbox', 'key' => 'italic', 'label' => __( 'Italic', 'booking' ) ),
135 + // array( 'type' => 'checkbox', 'key' => 'html_allowed', 'label' => __( 'Allow basic HTML', 'booking' ) ),
136 + array( 'type' => 'checkbox', 'key' => 'nl2br', 'label' => __( 'Convert newlines to <br> tag', 'booking' ) ),
137 + array(
138 + 'type' => 'slot',
139 + 'key' => 'links',
140 + 'label' => __( 'Link definitions', 'booking' ),
141 + 'slot' => 'static_text_links',
142 + ),
143 + ),
133 144 ),
134 145 array(
135 146 'key' => 'advanced',
136 147 'label' => __( 'Advanced', 'booking' ),
@@ -154,22 +165,262 @@
154 165 *
155 166 * @param string $page Current admin page slug (unused, provided for parity with other packs).
156 167 * @return void
157 168 */
158 -function wpbc_bfb_enqueue__field_static_text_assets( $page ) {
159 -
160 - wp_enqueue_script(
161 - 'wpbc-bfb_field_static_text',
162 - wpbc_plugin_url( '/includes/page-form-builder/field-packs/static-text/_out/static-text.js' ),
163 - array( 'wpbc-bfb' ),
164 - WP_BK_VERSION_NUM,
165 - true
169 +function wpbc_bfb_enqueue__field_static_text_assets( $page ) {
170 +
171 + wp_enqueue_style(
172 + 'wpbc-bfb_field_static_text_links',
173 + wpbc_plugin_url( '/includes/page-form-builder/field-packs/static-text/_out/static-text-links.css' ),
174 + array(),
175 + WP_BK_VERSION_NUM
176 + );
177 +
178 + wp_enqueue_script(
179 + 'wpbc-bfb_field_static_text_links',
180 + wpbc_plugin_url( '/includes/page-form-builder/field-packs/static-text/_out/static-text-links.js' ),
181 + array( 'wpbc-bfb' ),
182 + WP_BK_VERSION_NUM,
183 + true
184 + );
185 +
186 + wp_localize_script(
187 + 'wpbc-bfb_field_static_text_links',
188 + 'WPBC_BFB_Static_Text_Links_Boot',
189 + array(
190 + 'add_link' => __( 'Add link', 'booking' ),
191 + 'anchor' => __( 'Anchor', 'booking' ),
192 + 'css_class' => __( 'CSS class', 'booking' ),
193 + 'destination' => __( 'Destination', 'booking' ),
194 + 'duplicate' => __( 'Duplicate', 'booking' ),
195 + 'link_type' => __( 'Type', 'booking' ),
196 + 'links_help' => __( 'Use tokens inside braces, for example: {terms} or {privacy_policy}. Each token can link to a URL or an anchor on the same page.', 'booking' ),
197 + 'missing_definitions' => __( 'Add a link definition for: %s', 'booking' ),
198 + 'no_links' => __( 'Add a link definition, then insert its token into the text.', 'booking' ),
199 + 'remove' => __( 'Remove', 'booking' ),
200 + 'target' => __( 'Target', 'booking' ),
201 + 'token_key' => __( 'Token Key', 'booking' ),
202 + 'url' => __( 'URL', 'booking' ),
203 + 'visible_text' => __( 'Text', 'booking' ),
204 + ),
205 + );
206 +
207 + wp_enqueue_script(
208 + 'wpbc-bfb_field_static_text',
209 + wpbc_plugin_url( '/includes/page-form-builder/field-packs/static-text/_out/static-text.js' ),
210 + array( 'wpbc-bfb', 'wpbc-bfb_field_static_text_links' ),
211 + WP_BK_VERSION_NUM,
212 + true
166 213 );
167 214 }
168 -add_action( 'wpbc_enqueue_js_field_pack', 'wpbc_bfb_enqueue__field_static_text_assets', 10, 1 );
169 -
170 -/**
171 - * (Optional) Register a palette item.
215 +add_action( 'wpbc_enqueue_js_field_pack', 'wpbc_bfb_enqueue__field_static_text_assets', 10, 1 );
216 +
217 +/**
218 + * Limit a scalar link-definition value without requiring mbstring.
219 + *
220 + * @param mixed $raw_value Candidate scalar value.
221 + * @param int $max_length Maximum character count.
222 + *
223 + * @return string Bounded string, or an empty string for non-scalar input.
224 + */
225 +function wpbc_bfb_static_text_links_limit_string( $raw_value, $max_length ) {
226 +
227 + if ( ! is_scalar( $raw_value ) ) {
228 + return '';
229 + }
230 +
231 + $raw_value = (string) $raw_value;
232 + $max_length = max( 0, (int) $max_length );
233 +
234 + if ( function_exists( 'mb_substr' ) ) {
235 + return mb_substr( $raw_value, 0, $max_length );
236 + }
237 +
238 + return substr( $raw_value, 0, $max_length );
239 +}
240 +
241 +/**
242 + * Normalize a Static Text link token key to the Builder brace-token grammar.
243 + *
244 + * @param mixed $raw_key Candidate token key.
245 + *
246 + * @return string Lowercase token key containing only letters, numbers, and underscores.
247 + */
248 +function wpbc_bfb_static_text_links_sanitize_token_key( $raw_key ) {
249 +
250 + $token_key = strtolower( wpbc_bfb_static_text_links_limit_string( $raw_key, 64 ) );
251 + $token_key = preg_replace( '/[\s\-]+/', '_', $token_key );
252 + $token_key = preg_replace( '/[^a-z0-9_]/', '', $token_key );
253 + $token_key = preg_replace( '/_+/', '_', $token_key );
254 +
255 + return trim( (string) $token_key, '_' );
256 +}
257 +
258 +/**
259 + * Normalize a space-delimited CSS class list from one link definition.
260 + *
261 + * @param mixed $raw_classes Candidate class list.
262 + *
263 + * @return string Sanitized, de-duplicated class list.
264 + */
265 +function wpbc_bfb_static_text_links_sanitize_css_classes( $raw_classes ) {
266 +
267 + $raw_classes = wpbc_bfb_static_text_links_limit_string( $raw_classes, 200 );
268 + $raw_class_names = preg_split( '/\s+/', trim( $raw_classes ) );
269 + $safe_class_names = array();
270 +
271 + foreach ( (array) $raw_class_names as $raw_class_name ) {
272 + $safe_class_name = sanitize_html_class( $raw_class_name );
273 + if ( '' !== $safe_class_name ) {
274 + $safe_class_names[ $safe_class_name ] = $safe_class_name;
275 + }
276 + }
277 +
278 + return implode( ' ', array_values( $safe_class_names ) );
279 +}
280 +
281 +/**
282 + * Normalize one link destination according to its allow-listed link type.
283 + *
284 + * URL values use WordPress protocol filtering. Anchor and popup identifiers
285 + * are restricted to characters that are safe in an HTML identifier.
286 + *
287 + * @param mixed $raw_destination Candidate destination.
288 + * @param string $link_type Normalized link type.
289 + *
290 + * @return string Sanitized destination.
291 + */
292 +function wpbc_bfb_static_text_links_sanitize_destination( $raw_destination, $link_type ) {
293 +
294 + $destination = trim( wpbc_bfb_static_text_links_limit_string( $raw_destination, 2048 ) );
295 +
296 + if ( 'anchor' === $link_type || 'popup' === $link_type ) {
297 + $destination = ltrim( $destination, '#' );
298 + return (string) preg_replace( '/[^A-Za-z0-9_:\-.]/', '', $destination );
299 + }
300 +
301 + return esc_url_raw( $destination );
302 +}
303 +
304 +/**
305 + * Normalize executable-free link definitions for Static Text and Accept Terms.
306 + *
307 + * The collection is bounded to avoid allowing a crafted Builder request to
308 + * persist an unreasonably large set of link records. Duplicate token keys are
309 + * made deterministic by adding a numeric suffix.
310 + *
311 + * @param mixed $raw_links Candidate array or JSON-encoded link definitions.
312 + *
313 + * @return array Sanitized ordered link definitions.
314 + */
315 +function wpbc_bfb_static_text_links_normalize_definitions( $raw_links ) {
316 +
317 + if ( is_string( $raw_links ) ) {
318 + $decoded_links = json_decode( $raw_links, true );
319 + $raw_links = is_array( $decoded_links ) ? $decoded_links : array();
320 + }
321 +
322 + if ( ! is_array( $raw_links ) ) {
323 + return array();
324 + }
325 +
326 + $normalized_links = array();
327 + $used_keys = array();
328 + $link_index = 0;
329 +
330 + foreach ( array_slice( array_values( $raw_links ), 0, 20 ) as $raw_link ) {
331 + if ( ! is_array( $raw_link ) ) {
332 + continue;
333 + }
334 +
335 + $link_index++;
336 + $token_key = wpbc_bfb_static_text_links_sanitize_token_key( isset( $raw_link['key'] ) ? $raw_link['key'] : '' );
337 +
338 + if ( '' === $token_key ) {
339 + $token_key = 'link_' . $link_index;
340 + }
341 +
342 + $base_key = $token_key;
343 + $key_suffix = 2;
344 + while ( isset( $used_keys[ $token_key ] ) ) {
345 + $token_key = substr( $base_key, 0, 60 ) . '_' . $key_suffix;
346 + $key_suffix++;
347 + }
348 + $used_keys[ $token_key ] = true;
349 +
350 + $link_type = isset( $raw_link['link_type'] ) ? sanitize_key( $raw_link['link_type'] ) : 'url';
351 + if ( ! in_array( $link_type, array( 'url', 'anchor', 'popup' ), true ) ) {
352 + $link_type = 'url';
353 + }
354 +
355 + $visible_text = isset( $raw_link['text'] ) ? sanitize_text_field( wpbc_bfb_static_text_links_limit_string( $raw_link['text'], 300 ) ) : '';
356 + if ( '' === $visible_text ) {
357 + $visible_text = str_replace( '_', ' ', $token_key );
358 + }
359 +
360 + $normalized_links[] = array(
361 + 'key' => $token_key,
362 + 'text' => $visible_text,
363 + 'link_type' => $link_type,
364 + 'destination' => wpbc_bfb_static_text_links_sanitize_destination(
365 + isset( $raw_link['destination'] ) ? $raw_link['destination'] : '',
366 + $link_type
367 + ),
368 + 'target' => isset( $raw_link['target'] ) && '_self' === $raw_link['target'] ? '_self' : '_blank',
369 + 'cssclass' => wpbc_bfb_static_text_links_sanitize_css_classes( isset( $raw_link['cssclass'] ) ? $raw_link['cssclass'] : '' ),
370 + );
371 + }
372 +
373 + return $normalized_links;
374 +}
375 +
376 +/**
377 + * Sanitize stored link definitions throughout a decoded Builder structure.
378 + *
379 + * Missing `links` properties are intentionally left missing so existing
380 + * Static Text fields retain their exact storage shape and Accept Terms can
381 + * continue applying its established defaults. Explicit empty collections
382 + * remain empty.
383 + *
384 + * @param array $structure_branch Decoded Builder structure or nested branch.
385 + *
386 + * @return array Structure with field-owned link definitions normalized.
387 + */
388 +function wpbc_bfb_sanitize_structure__static_text_links( $structure_branch ) {
389 +
390 + if ( ! is_array( $structure_branch ) ) {
391 + return array();
392 + }
393 +
394 + if (
395 + isset( $structure_branch['type'], $structure_branch['data'] )
396 + && 'field' === $structure_branch['type']
397 + && is_array( $structure_branch['data'] )
398 + ) {
399 + $field_type = isset( $structure_branch['data']['type'] ) ? sanitize_key( $structure_branch['data']['type'] ) : '';
400 +
401 + if (
402 + in_array( $field_type, array( 'static_text', 'accept_terms' ), true )
403 + && array_key_exists( 'links', $structure_branch['data'] )
404 + ) {
405 + $structure_branch['data']['links'] = wpbc_bfb_static_text_links_normalize_definitions( $structure_branch['data']['links'] );
406 + }
407 + }
408 +
409 + foreach ( $structure_branch as $branch_key => $branch_value ) {
410 + if ( ! is_array( $branch_value ) ) {
411 + continue;
412 + }
413 +
414 + $structure_branch[ $branch_key ] = wpbc_bfb_sanitize_structure__static_text_links( $branch_value );
415 + }
416 +
417 + return $structure_branch;
418 +}
419 +add_filter( 'wpbc_bfb_sanitize_structure_before_save', 'wpbc_bfb_sanitize_structure__static_text_links', 20, 1 );
420 +
421 +/**
422 + * (Optional) Register a palette item.
172 423 *
173 424 * @param string $group Palette group (e.g., 'general', 'structure').
174 425 * @param string $position Position: 'top' | 'bottom'.
175 426 * @return void