PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.33
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.33
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / helpers / FrmStylesHelper.php

FrmStylesHelper.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.33, at classes/helpers/FrmStylesHelper.php

1,266 lines 32.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmStylesHelper {
7
8 /**
9 * @return array
10 */
11 public static function get_upload_base() {
12 $uploads = wp_upload_dir();
13
14 if ( is_ssl() && ! preg_match( '/^https:\/\/.*\..*$/', $uploads['baseurl'] ) ) {
15 $uploads['baseurl'] = str_replace( 'http://', 'https://', $uploads['baseurl'] );
16 }
17
18 return $uploads;
19 }
20
21 /**
22 * Called from the admin header.
23 *
24 * @since 4.0
25 *
26 * @return void
27 */
28 public static function save_button() {
29 // phpcs:disable Generic.WhiteSpace.ScopeIndent
30 ?>
31 <input type="submit" name="submit" class="button button-primary frm-button-primary" value="<?php esc_attr_e( 'Update', 'formidable' ); ?>" />
32 <?php
33 // phpcs:enable Generic.WhiteSpace.ScopeIndent
34 }
35
36 /**
37 * @since 2.05
38 *
39 * @return array
40 */
41 public static function get_css_label_positions() {
42 return array(
43 'none' => __( 'top', 'formidable' ),
44 'left' => __( 'left', 'formidable' ),
45 'right' => __( 'right', 'formidable' ),
46 'no_label' => __( 'none', 'formidable' ),
47 'inside' => __( 'inside', 'formidable' ),
48 );
49 }
50
51 /**
52 * @since 6.11 Added $field param.
53 *
54 * @param array|object $field
55 *
56 * @return array
57 */
58 public static function get_single_label_positions( $field = array() ) {
59 $label_positions = array(
60 'top' => __( 'Top', 'formidable' ),
61 'left' => __( 'Left', 'formidable' ),
62 'right' => __( 'Right', 'formidable' ),
63 'inline' => __( 'Inline (left without a set width)', 'formidable' ),
64 'none' => __( 'None', 'formidable' ),
65 'hidden' => __( 'Hidden (but leave the space)', 'formidable' ),
66 'inside' => __( 'Placeholder inside the field', 'formidable' ),
67 );
68
69 /**
70 * Allows updating label positions in field settings.
71 *
72 * @since 6.11
73 *
74 * @param array $label_positions
75 * @param array|object $field
76 */
77 return apply_filters( 'frm_single_label_positions', $label_positions, $field );
78 }
79
80 /**
81 * @return array
82 */
83 public static function minus_icons() {
84 return array(
85 0 => array(
86 '-' => '62e',
87 '+' => '62f',
88 ),
89 1 => array(
90 '-' => '600',
91 '+' => '602',
92 ),
93 2 => array(
94 '-' => '604',
95 '+' => '603',
96 ),
97 3 => array(
98 '-' => '633',
99 '+' => '632',
100 ),
101 4 => array(
102 '-' => '613',
103 '+' => '60f',
104 ),
105 );
106 }
107
108 /**
109 * @return array
110 */
111 public static function arrow_icons() {
112 $minus_icons = self::minus_icons();
113
114 return array(
115 6 => array(
116 '-' => '62d',
117 '+' => '62a',
118 ),
119 0 => array(
120 '-' => '60d',
121 '+' => '609',
122 ),
123 1 => array(
124 '-' => '60e',
125 '+' => '60c',
126 ),
127 2 => array(
128 '-' => '630',
129 '+' => '631',
130 ),
131 3 => array(
132 '-' => '62b',
133 '+' => '628',
134 ),
135 4 => array(
136 '-' => '62c',
137 '+' => '629',
138 ),
139 5 => array(
140 '-' => '635',
141 '+' => '634',
142 ),
143 'p0' => $minus_icons[0],
144 'p1' => $minus_icons[1],
145 'p2' => $minus_icons[2],
146 'p3' => $minus_icons[3],
147 'p4' => $minus_icons[4],
148 );
149 }
150
151 /**
152 * @since 2.0
153 *
154 * @param int|string $key Icon key.
155 * @param string $icon Icon state, `+` or `-`.
156 * @param string $type Icon type, such as `arrow`.
157 *
158 * @return string The class for this icon.
159 */
160 public static function icon_key_to_class( $key, $icon = '+', $type = 'arrow' ) {
161 if ( 'arrow' === $type && is_numeric( $key ) ) {
162 // frm_arrowup6_icon.
163 $arrow = array(
164 '-' => 'down',
165 '+' => 'up',
166 );
167 $class = 'frm_arrow' . $arrow[ $icon ];
168 } else {
169 // frm_minus1_icon.
170 $key = str_replace( 'p', '', $key );
171 $plus = array(
172 '-' => 'minus',
173 '+' => 'plus',
174 );
175 $class = 'frm_' . $plus[ $icon ];
176 }
177
178 if ( $key ) {
179 $class .= $key;
180 }
181
182 return $class . '_icon';
183 }
184
185 /**
186 * @param WP_Post $style
187 * @param FrmStyle $frm_style
188 * @param string $type
189 *
190 * @return void
191 */
192 public static function bs_icon_select( $style, $frm_style, $type = 'arrow' ) {
193 $function_name = $type . '_icons';
194 $icons = self::$function_name();
195 unset( $function_name );
196
197 $name = 'arrow' === $type ? 'collapse_icon' : 'repeat_icon';
198 // phpcs:disable Generic.WhiteSpace.ScopeIndent
199 ?>
200 <div class="btn-group" id="frm_<?php echo esc_attr( $name ); ?>_select">
201 <button class="multiselect dropdown-toggle btn btn-default" data-toggle="dropdown" type="button">
202 <?php FrmAppHelper::icon_by_class( 'frmfont ' . self::icon_key_to_class( $style->post_content[ $name ], '+', $type ) ); ?>
203 <?php FrmAppHelper::icon_by_class( 'frmfont ' . self::icon_key_to_class( $style->post_content[ $name ], '-', $type ) ); ?>
204 <b class="caret"></b>
205 </button>
206 <ul class="multiselect-container frm-dropdown-menu">
207 <?php foreach ( $icons as $key => $icon ) { ?>
208 <li <?php echo $style->post_content['collapse_icon'] == $key ? 'class="active"' : ''; // phpcs:ignore Universal.Operators.StrictComparisons ?>>
209 <a href="javascript:void(0);">
210 <label>
211 <input type="radio" value="<?php echo esc_attr( $key ); ?>" name="<?php echo esc_attr( $frm_style->get_field_name( $name ) ); ?>" <?php checked( $style->post_content[ $name ], $key ); ?> /><?php // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong ?>
212 <span>
213 <?php
214 FrmAppHelper::icon_by_class( 'frmfont ' . self::icon_key_to_class( $key, '+', $type ) );
215 FrmAppHelper::icon_by_class( 'frmfont ' . self::icon_key_to_class( $key, '-', $type ) );
216 ?>
217 </span>
218 </label>
219 </a>
220 </li>
221 <?php } ?>
222 </ul>
223 </div>
224 <?php
225 // phpcs:enable Generic.WhiteSpace.ScopeIndent
226 }
227
228 /**
229 * Convert a color setting to a RGB CSV (without the rgb()/rgba() wrapper).
230 *
231 * @since 2.0
232 *
233 * @param string $color Color setting value. This could be hex or rgb.
234 *
235 * @return string RGB value without the rgb() wrapper.
236 */
237 public static function hex2rgb( $color ) {
238 $rgb = str_starts_with( $color, 'rgb' ) ? self::get_rgb_array_from_rgb( $color ) : self::get_rgb_array_from_hex( $color );
239 return implode( ',', $rgb );
240 }
241
242 /**
243 * Remove the rgb()/rgba() wrapper from a RGB color and return its R, G and B values as an array.
244 *
245 * @since 6.8.3
246 *
247 * @param string $rgb RGB value including the rgb() or rgba() wrapper.
248 *
249 * @return array<string> including three numeric values for R, G, and B.
250 */
251 private static function get_rgb_array_from_rgb( $rgb ) {
252 $rgb = str_replace( array( 'rgb(', 'rgba(', ')' ), '', $rgb );
253 $rgb = explode( ',', $rgb );
254
255 if ( 4 === count( $rgb ) ) {
256 // Drop the alpha. The function is expected to only return r,g,b with no alpha.
257 array_pop( $rgb );
258 }
259
260 return $rgb;
261 }
262
263 /**
264 * Get the R, G, and B array values from a Hex color code.
265 *
266 * @since 6.8.3
267 *
268 * @param string $hex A hex color string.
269 *
270 * @return array<string> Including three numeric values for R, G, and B.
271 */
272 private static function get_rgb_array_from_hex( $hex ) {
273 $hex = str_replace( '#', '', $hex );
274 list( $r, $g, $b ) = sscanf( $hex, '%02x%02x%02x' );
275 return array( $r, $g, $b );
276 }
277
278 /**
279 * @since 4.0
280 *
281 * @param string $hex Hex color string.
282 * @param float|int|string $a Alpha channel value.
283 *
284 * @return string
285 */
286 public static function hex2rgba( $hex, $a ) {
287 $rgb = self::hex2rgb( $hex );
288 return 'rgba(' . $rgb . ',' . $a . ')';
289 }
290
291 /**
292 * @since 6.0
293 *
294 * @param string $rgba Color setting value. This could be hex or rgb.
295 *
296 * @return string Hex color value.
297 */
298 private static function rgb_to_hex( $rgba ) {
299 if ( str_starts_with( $rgba, '#' ) ) {
300 // Color is already hex.
301 return $rgba;
302 }
303 preg_match( '/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i', $rgba, $by_color );
304 return sprintf( '%02x%02x%02x', $by_color[1], $by_color[2], $by_color[3] );
305 }
306
307 /**
308 * @since 6.8
309 *
310 * @param string $hsl
311 *
312 * @return string|null Null if it fails to parse the HSL string.
313 */
314 private static function hsl_to_hex( $hsl ) {
315 // Convert hsla to hsl.
316 $hsl = preg_replace( '/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/', 'hsl($1, $2%, $3%)', $hsl );
317
318 // Extract HSL components from the color string.
319 preg_match( '/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/', $hsl, $matches );
320
321 if ( count( $matches ) !== 4 ) {
322 // Invalid HSL string format.
323 return null;
324 }
325
326 // Extract HSL values.
327 $h = (int) $matches[1];
328 $s = (int) $matches[2] / 100;
329 $l = (int) $matches[3] / 100;
330
331 // Calculate RGB values.
332 $c = ( 1 - abs( 2 * $l - 1 ) ) * $s;
333 $x = $c * ( 1 - abs( ( (int) ( $h / 60 ) % 2 ) - 1 ) );
334 $m = $l - $c / 2;
335 $r = 0;
336 $g = 0;
337 $b = 0;
338
339 if ( $h >= 0 && $h < 60 ) {
340 $r = $c;
341 $g = $x;
342 } elseif ( $h >= 60 && $h < 120 ) {
343 $r = $x;
344 $g = $c;
345 } elseif ( $h >= 120 && $h < 180 ) {
346 $g = $c;
347 $b = $x;
348 } elseif ( $h >= 180 && $h < 240 ) {
349 $g = $x;
350 $b = $c;
351 } elseif ( $h >= 240 && $h < 300 ) {
352 $r = $x;
353 $b = $c;
354 } elseif ( $h >= 300 && $h < 360 ) {
355 $r = $c;
356 $b = $x;
357 }//end if
358
359 // Convert RGB to 8-bit values
360 $r = round( ( $r + $m ) * 255 );
361 $g = round( ( $g + $m ) * 255 );
362 $b = round( ( $b + $m ) * 255 );
363
364 // Convert RGB to hex
365 return sprintf( '%02x%02x%02x', $r, $g, $b );
366 }
367
368 /**
369 * @since 2.3
370 *
371 * @param string $hex string The original color in hex format #ffffff.
372 * @param int $steps integer Should be between -255 and 255. Negative = darker, positive = lighter.
373 *
374 * @return string
375 */
376 public static function adjust_brightness( $hex, $steps ) {
377 $steps = max( - 255, min( 255, $steps ) );
378
379 if ( str_starts_with( $hex, 'rgba(' ) ) {
380 $rgba = str_replace( ')', '', str_replace( 'rgba(', '', $hex ) );
381 list ( $r, $g, $b, $a ) = array_map( 'trim', explode( ',', $rgba ) );
382 $r = max( 0, min( 255, $r + $steps ) );
383 $g = max( 0, min( 255, $g + $steps ) );
384 $b = max( 0, min( 255, $b + $steps ) );
385 return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $a . ')';
386 }
387
388 // Normalize into a six character long hex string
389 $hex = str_replace( '#', '', $hex );
390 self::fill_hex( $hex );
391
392 // Split into three parts: R, G and B
393 $color_parts = str_split( $hex, 2 );
394 $return = '#';
395
396 foreach ( $color_parts as $color ) {
397 // Convert to decimal.
398 $color = hexdec( $color );
399 // Adjust color.
400 $color = max( 0, min( 255, $color + $steps ) );
401
402 // Make two char hex code.
403 $return .= str_pad( dechex( $color ), 2, '0', STR_PAD_LEFT );
404 }
405
406 return $return;
407 }
408
409 /**
410 * @since 6.0
411 *
412 * @param string $color
413 *
414 * @return int
415 */
416 public static function get_color_brightness( $color ) {
417 if ( str_starts_with( $color, 'rgb' ) ) {
418 $color = self::rgb_to_hex( $color );
419 }
420
421 if ( str_starts_with( $color, 'hsl' ) ) {
422 $hsl_to_hex = self::hsl_to_hex( $color );
423
424 if ( is_null( $hsl_to_hex ) ) {
425 // Fallback if we cannot convert the HSL value.
426 return 0;
427 }
428
429 $color = $hsl_to_hex;
430 }
431
432 self::fill_hex( $color );
433
434 $c_r = hexdec( substr( $color, 0, 2 ) );
435 $c_g = hexdec( substr( $color, 2, 2 ) );
436 $c_b = hexdec( substr( $color, 4, 2 ) );
437
438 return ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000;
439 }
440
441 /**
442 * Change a 3 character hex color to a 6 character one.
443 *
444 * @since 6.0
445 *
446 * @param string $color Color value, passed by reference.
447 *
448 * @return void
449 */
450 private static function fill_hex( &$color ) {
451 if ( 3 === strlen( $color ) ) {
452 $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
453 }
454 }
455
456 /**
457 * @since 4.05.02
458 *
459 * @param array $vars CSS variable keys.
460 *
461 * @return array
462 */
463 public static function get_css_vars( $vars = array() ) {
464 $vars = apply_filters( 'frm_css_vars', $vars );
465 return array_unique( $vars );
466 }
467
468 /**
469 * @since 4.05.02
470 *
471 * @param array $settings Style settings.
472 * @param array $defaults Default style settings.
473 * @param array $vars CSS variable keys to output.
474 *
475 * @return void
476 */
477 public static function output_vars( $settings, $defaults = array(), $vars = array() ) {
478 if ( ! $vars ) {
479 $vars = self::get_css_vars( array_keys( $settings ) );
480 }
481
482 $vars = array_diff( $vars, self::get_style_keys_to_remove_from_output_vars() );
483
484 foreach ( $vars as $var ) {
485 if ( ! isset( $settings[ $var ] ) || ! self::css_key_is_valid( $var ) ) {
486 continue;
487 }
488
489 if ( ! isset( $defaults[ $var ] ) ) {
490 $defaults[ $var ] = '';
491 }
492
493 $prepared_value = '';
494
495 if ( self::should_add_css_var( $settings, $defaults, $var, $prepared_value ) ) {
496 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
497 echo '--' . esc_html( self::clean_var_name( str_replace( '_', '-', $var ) ) ) . ':' . $prepared_value . ';';
498 }
499 }
500 }
501
502 /**
503 * None of these style settings are used as CSS variables, so we want to exclude them to keep the CSS output clean.
504 *
505 * @since 6.26.1
506 *
507 * @return array<string>
508 */
509 private static function get_style_keys_to_remove_from_output_vars() {
510 return array(
511 'remove_box_shadow',
512 'remove_box_shadow_active',
513 'theme_css',
514 'theme_name',
515 'theme_selector',
516 'important_style',
517 'submit_style',
518 'collapse_icon',
519 'center_form',
520 'custom_css',
521 'style_class',
522 'submit_bg_img',
523 'change_margin',
524 'repeat_icon',
525 'use_base_font_size',
526 'field_shape_type',
527 'bg_image_id',
528 'single_style_custom_css',
529 'enable_style_custom_css',
530 );
531 }
532
533 /**
534 * Check if a CSS variable setting is not blank, doesn't match the default, and doesn't include invalid substrings.
535 *
536 * @since 6.24
537 *
538 * @param array $settings Array of setting values.
539 * @param array $defaults Array of default values.
540 * @param string $var The setting key name.
541 * @param string $prepared_value The value from calling css_var_prepare_value. This is set by reference so it can be used after this function is called.
542 *
543 * @return bool True if the CSS value should be printed.
544 */
545 private static function should_add_css_var( $settings, $defaults, $var, &$prepared_value ) {
546 $prepared_value = self::css_var_prepare_value( $settings, $var );
547
548 if ( $prepared_value === '' ) {
549 return false;
550 }
551
552 if ( $defaults && $defaults[ $var ] === $prepared_value ) {
553 return false;
554 }
555
556 return self::css_value_is_valid( $prepared_value );
557 }
558
559 /**
560 * Prevent invalid CSS keys from getting added to the generated CSS.
561 *
562 * @since 6.20
563 *
564 * @param string $key
565 *
566 * @return bool
567 */
568 private static function css_key_is_valid( $key ) {
569 // Any key that is abnormally large is not valid.
570 // Any key that contains a '{' is not valid.
571 return strlen( $key ) < 100 && ! str_contains( $key, '{' );
572 }
573
574 /**
575 * Confirm a CSS value is valid.
576 * If it appears to contain JavaScript, it will not be added.
577 *
578 * @since 6.20
579 *
580 * @param string $var
581 *
582 * @return bool
583 */
584 private static function css_value_is_valid( $var ) {
585 if ( is_numeric( $var ) ) {
586 return true;
587 }
588
589 // None of these substrings should be present in any CSS value.
590 $invalid_substrings = array(
591 'function(',
592 ';userAgent',
593 ';stopPropagation',
594 '{const',
595 'window[',
596 'navigator[',
597 'Array;',
598 );
599
600 foreach ( $invalid_substrings as $substring ) {
601 if ( str_contains( $var, $substring ) ) {
602 return false;
603 }
604 }
605
606 return true;
607 }
608
609 /**
610 * Remove anything that isn't used as a CSS variable name.
611 *
612 * @param string $var_name
613 *
614 * @return string
615 */
616 private static function clean_var_name( $var_name ) {
617 return preg_replace( '/[^a-zA-Z0-9_-]/', '', $var_name );
618 }
619
620 /**
621 * Prepare the value for a CSS variable.
622 *
623 * @since 6.14
624 *
625 * @param array $settings An array of css style.
626 * @param string $key
627 *
628 * @return string
629 */
630 private static function css_var_prepare_value( $settings, $key ) {
631 $value = $settings[ $key ];
632
633 if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
634 return '';
635 }
636
637 switch ( $key ) {
638 case 'font':
639 return safecss_filter_attr( $value );
640
641 case 'border_width_error':
642 case 'field_border_width':
643 if ( ! empty( $settings['field_shape_type'] ) && 'underline' === $settings['field_shape_type'] ) {
644 return safecss_filter_attr( '0px 0px ' . $value . ' 0px' );
645 }
646 break;
647
648 case 'box_shadow':
649 if ( ! empty( $settings['field_shape_type'] ) && 'underline' === $settings['field_shape_type'] ) {
650 return safecss_filter_attr( 'none' );
651 }
652 break;
653
654 case 'border_radius':
655 if ( ! empty( $settings['field_shape_type'] ) ) {
656 switch ( $settings['field_shape_type'] ) {
657 case 'underline':
658 case 'regular':
659 return safecss_filter_attr( '0px' );
660 case 'circle':
661 return safecss_filter_attr( '30px' );
662 }
663 }
664 break;
665 }//end switch
666
667 return esc_html( $value );
668 }
669
670 /**
671 * @since 2.3
672 *
673 * @param WP_Post $style
674 *
675 * @return array
676 */
677 public static function get_settings_for_output( $style ) {
678 if ( self::previewing_style() ) {
679 $frm_style = new FrmStyle();
680
681 if ( isset( $_POST['frm_style_setting'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
682
683 // Sanitizing is done later.
684 //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
685 $posted = wp_unslash( $_POST['frm_style_setting'] );
686
687 if ( is_array( $posted ) ) {
688 $settings = $frm_style->sanitize_post_content( $posted['post_content'] );
689 $style_name = FrmAppHelper::get_post_param( 'style_name', '', 'sanitize_title' );
690 } else {
691 $posted = json_decode( $posted, true );
692 FrmAppHelper::format_form_data( $posted );
693 $settings = $frm_style->sanitize_post_content( $posted['frm_style_setting']['post_content'] );
694 $style_name = sanitize_title( $posted['style_name'] );
695 }
696 } else {
697 $settings = $frm_style->sanitize_post_content( wp_unslash( $_GET ) );
698 $style_name = FrmAppHelper::get_param( 'style_name', '', 'get', 'sanitize_title' );
699 }
700
701 $settings = self::update_base_font_size( $settings, $frm_style->get_defaults() );
702
703 FrmAppHelper::sanitize_value( 'sanitize_text_field', $settings );
704
705 $settings['style_class'] = '';
706
707 if ( ! empty( $style_name ) ) {
708 $settings['style_class'] = $style_name . '.';
709 }
710 } else {
711 $settings = $style->post_content;
712 $settings['style_class'] = 'frm_style_' . $style->post_name . '.';
713 }//end if
714
715 $settings['style_class'] .= 'with_frm_style';
716 $settings['font'] = stripslashes( $settings['font'] );
717 $settings['change_margin'] = self::description_margin_for_screensize( $settings['width'] );
718
719 $checkbox_opts = array( 'important_style', 'auto_width', 'submit_style', 'collapse_icon', 'center_form' );
720
721 foreach ( $checkbox_opts as $opt ) {
722 if ( ! isset( $settings[ $opt ] ) ) {
723 $settings[ $opt ] = 0;
724 }
725 }
726
727 self::prepare_color_output( $settings );
728
729 $settings['field_height'] = $settings['field_height'] === '' ? 'auto' : $settings['field_height'];
730 $settings['field_width'] = $settings['field_width'] === '' ? 'auto' : $settings['field_width'];
731 $settings['auto_width'] = $settings['auto_width'] ? 'auto' : $settings['field_width'];
732 $settings['box_shadow'] = ! empty( $settings['remove_box_shadow'] ) ? 'none' : '0 1px 2px 0 rgba(18, 18, 23, 0.05)';
733
734 if ( ! isset( $settings['repeat_icon'] ) ) {
735 $settings['repeat_icon'] = 1;
736 }
737
738 return $settings;
739 }
740
741 /**
742 * Update the "Base Font Size" value from "Quick Settings across multiple settings values".
743 *
744 * @since 6.14
745 *
746 * @param array $settings An array of css style.
747 * @param array $defaults Default style settings.
748 *
749 * @return array
750 */
751 public static function update_base_font_size( $settings, $defaults ) {
752 if ( empty( $settings['base_font_size'] ) || empty( $settings['use_base_font_size'] ) || 'false' === $settings['use_base_font_size'] ) {
753 return $settings;
754 }
755
756 $base_font_size = (int) $settings['base_font_size'];
757 $font_size = $defaults['font_size'];
758 $font_sizes_to_update = array(
759 'font_size',
760 'field_font_size',
761 'check_font_size',
762 'title_size',
763 'form_desc_size',
764 'description_font_size',
765 'section_font_size',
766 'submit_font_size',
767 'success_font_size',
768 'error_font_size',
769 'progress_size',
770 );
771
772 array_map(
773 function ( $key ) use ( $defaults, $font_size, $base_font_size, &$settings ) {
774 if ( isset( $settings[ $key ] ) ) {
775 $settings[ $key ] = round( self::get_base_font_size_scale( $key, $font_size, $defaults ) * $base_font_size ) . 'px';
776 }
777 },
778 $font_sizes_to_update
779 );
780
781 return $settings;
782 }
783
784 /**
785 * Get style font size scale value.
786 *
787 * @since 6.14
788 *
789 * @param string $key Setting key.
790 * @param int|string $value Base font size value.
791 * @param array $defaults Default style settings.
792 *
793 * @return float
794 */
795 private static function get_base_font_size_scale( $key, $value, $defaults ) {
796 if ( empty( $defaults[ $key ] ) || ! is_numeric( (int) $defaults[ $key ] ) || ! is_numeric( (int) $value ) || 0 === (int) $value ) {
797 return 1;
798 }
799
800 return round( (int) $defaults[ $key ] / (int) $value, 2 );
801 }
802
803 /**
804 * @since 2.3
805 *
806 * @param array $settings Style settings, passed by reference.
807 * @param bool $allow_transparent Whether transparent colors are allowed.
808 *
809 * @return void
810 */
811 public static function prepare_color_output( &$settings, $allow_transparent = true ) {
812 $colors = self::allow_color_override();
813
814 foreach ( $colors as $css => $opts ) {
815 if ( $css === 'transparent' && ! $allow_transparent ) {
816 $css = '';
817 }
818
819 foreach ( $opts as $opt ) {
820 self::get_color_output( $css, $settings[ $opt ] );
821 }
822 }
823 }
824
825 /**
826 * @since 2.3
827 *
828 * @return array
829 */
830 private static function allow_color_override() {
831 $frm_style = new FrmStyle();
832 $colors = $frm_style->get_color_settings();
833
834 $transparent = array(
835 'fieldset_color',
836 'fieldset_bg_color',
837 'bg_color',
838 'bg_color_active',
839 'bg_color_disabled',
840 'section_bg_color',
841 'error_bg',
842 'success_bg_color',
843 'progress_bg_color',
844 'progress_active_bg_color',
845 'submit_border_color',
846 'submit_hover_border_color',
847 'submit_active_border_color',
848 'submit_hover_bg_color',
849 'submit_active_bg_color',
850 );
851
852 return array(
853 'transparent' => $transparent,
854 '' => array_diff( $colors, $transparent ),
855 );
856 }
857
858 /**
859 * @since 2.3
860 *
861 * @param string $default Default color value.
862 * @param string $color Color value, passed by reference.
863 *
864 * @return void
865 */
866 private static function get_color_output( $default, &$color ) {
867 $color = trim( $color );
868
869 if ( ! $color ) {
870 $color = $default;
871 } elseif ( ! str_contains( $color, '#' ) && self::is_hex( $color ) ) {
872 $color = '#' . $color;
873 }
874 }
875
876 /**
877 * If a color looks like a hex code without the #, prepend the #.
878 * A color looks like a hex code if it does not contain the substrings "rgb", "rgba", "hsl", "hsla", or "hwb".
879 *
880 * @since 6.8
881 *
882 * @param string $color
883 *
884 * @return bool
885 */
886 private static function is_hex( $color ) {
887 $non_hex_substrings = array(
888 'rgb(',
889 'rgba(',
890 'hsl(',
891 'hsla(',
892 'hwb(',
893 );
894
895 foreach ( $non_hex_substrings as $substring ) {
896 if ( str_contains( $color, $substring ) ) {
897 return false;
898 }
899 }
900
901 return true;
902 }
903
904 /**
905 * If left/right label is over a certain size,
906 * adjust the field description margin at a different screen size
907 *
908 * @since 2.3
909 *
910 * @param string $width Label width value.
911 *
912 * @return false|string
913 */
914 private static function description_margin_for_screensize( $width ) {
915 $temp_label_width = str_replace( 'px', '', $width );
916 $change_margin = false;
917
918 if ( $temp_label_width >= 230 ) {
919 $change_margin = '800px';
920 } elseif ( $width >= 215 ) {
921 $change_margin = '700px';
922 } elseif ( $width >= 180 ) {
923 $change_margin = '650px';
924 }
925
926 return $change_margin;
927 }
928
929 /**
930 * Get the raw alignment value stored in the active style for a radio or checkbox field.
931 *
932 * @since 6.32
933 *
934 * @param array|int $field The 'field' array.
935 *
936 * @return string
937 */
938 public static function get_align_from_active_style( $field ) {
939 $field_type = FrmField::is_checkbox( $field ) ? 'checkbox' : 'radio';
940 $key = FrmStylesController::get_align_key_for_style_settings( $field_type );
941
942 return FrmStylesController::get_active_style( $field )->post_content[ $key ] ?? '';
943 }
944
945 /**
946 * @since 2.3
947 *
948 * @return bool
949 */
950 public static function previewing_style() {
951 // phpcs:ignore WordPress.Security.NonceVerification.Missing
952 $ajax_change = isset( $_POST['action'] ) && $_POST['action'] === 'frm_change_styling' && isset( $_POST['frm_style_setting'] );
953
954 return $ajax_change || isset( $_GET['flat'] );
955 }
956
957 /**
958 * Get the URL for the Styler page list view (where you can assign styles to a form and view style templates) for a target form.
959 *
960 * @since 6.0
961 *
962 * @param int|string $form_id
963 *
964 * @return string
965 */
966 public static function get_list_url( $form_id ) {
967 return admin_url( 'admin.php?page=formidable-styles&form=' . absint( $form_id ) );
968 }
969
970 /**
971 * Get the back button args from Style settings.
972 *
973 * @since 6.14
974 *
975 * @param stdClass|WP_Post $style
976 * @param int $form_id
977 *
978 * @return array
979 */
980 public static function get_style_options_back_button_args( $style, $form_id ) {
981 if ( self::is_advanced_settings() ) {
982 return array(
983 'title' => __( 'Quick Settings', 'formidable' ),
984 'id' => 'frm_style_back_to_quick_settings',
985 );
986 }
987 return array(
988 'href' => self::get_list_url( $form_id ),
989 'title' => $style->post_title,
990 );
991 }
992
993 /**
994 * Get a link to edit a target style post object in the visual styler.
995 *
996 * @param stdClass|WP_Post $style
997 * @param int|string $form_id Used for the back button and preview form target.
998 * @param string $section The url param section.
999 *
1000 * @return string
1001 */
1002 public static function get_edit_url( $style, $form_id = 0, $section = '' ) {
1003 $query_args = array(
1004 'page' => 'formidable-styles',
1005 'frm_action' => 'edit',
1006 'id' => $style->ID,
1007 'section' => $section,
1008 );
1009
1010 if ( $form_id ) {
1011 // We include &form_id for the back button to know where to point to.
1012 $query_args['form'] = $form_id;
1013 }
1014
1015 return add_query_arg( $query_args, admin_url( 'admin.php' ) );
1016 }
1017
1018 /**
1019 * Get a count of the number of forms assigned to a target style ID.
1020 * All unassigned forms are included in the count for the default style.
1021 *
1022 * @since 6.0
1023 *
1024 * @param int|string $style_id
1025 * @param bool $is_default
1026 *
1027 * @return int
1028 */
1029 public static function get_form_count_for_style( $style_id, $is_default ) {
1030 $serialized = serialize( array( 'custom_style' => (string) $style_id ) );
1031 // Chop off the "a:1:{" from the front and the "}" from the back.
1032 $substring = substr( $serialized, 5, -1 );
1033 $number_of_forms = FrmDb::get_count(
1034 'frm_forms',
1035 array(
1036 'status' => 'published',
1037 'options LIKE' => $substring,
1038 )
1039 );
1040
1041 if ( ! $is_default ) {
1042 // Exit early as the rest of the code is about including the default count.
1043 return $number_of_forms;
1044 }
1045
1046 $conversational_style_id = FrmDb::get_var( 'posts', array( 'post_name' => 'lines-no-boxes' ), 'ID' );
1047
1048 return $number_of_forms + self::get_default_style_count( $style_id, $conversational_style_id );
1049 }
1050
1051 /**
1052 * Get the number of forms that use the default style.
1053 *
1054 * @since 6.0
1055 *
1056 * @param int|string $style_id
1057 * @param mixed $conversational_style_id
1058 *
1059 * @return int
1060 */
1061 private static function get_default_style_count( $style_id, $conversational_style_id ) {
1062 $substrings = array_map(
1063 function ( $value ) {
1064 $substring = serialize( array( 'custom_style' => $value ) );
1065 return substr( $substring, 5, -1 );
1066 },
1067 array( '1', 1 )
1068 );
1069 $where = array(
1070 'status' => 'published',
1071 0 => array(
1072 'options NOT LIKE' => 'custom_style',
1073 'or' => 1,
1074 'options LIKE' => $substrings,
1075 ),
1076 );
1077
1078 if ( $conversational_style_id ) {
1079 // When a conversational style is set, check for it in the query by wrapping the where and adding a conversational option check.
1080 $is_conversational_style = (int) $style_id === (int) $conversational_style_id;
1081 $where[0] = array(
1082 // The chat option doesn't exist if it isn't on.
1083 ( $is_conversational_style ? 'options LIKE' : 'options NOT LIKE' ) => ';s:4:"chat";',
1084 $where[0],
1085 );
1086 }
1087
1088 return FrmDb::get_count( 'frm_forms', $where );
1089 }
1090
1091 /**
1092 * Check if the current page is the advanced settings page.
1093 *
1094 * @since 6.14
1095 *
1096 * @return bool True if is advanced settings, false otherwise.
1097 */
1098 public static function is_advanced_settings() {
1099 return FrmAppHelper::get_param( 'section' ) === 'advanced-settings' && FrmAppHelper::get_param( 'page' ) === 'formidable-styles';
1100 }
1101
1102 /**
1103 * Get wrapper classname for style editor sections.
1104 *
1105 * @since 6.16
1106 *
1107 * @param string $section_type
1108 *
1109 * @return string The style editor wrapper classname.
1110 */
1111 public static function style_editor_get_wrapper_classname( $section_type ) {
1112 $is_quick_settings = 'quick-settings' === $section_type;
1113 $classname = 'frm-style-editor-form';
1114 $classname .= ( ! self::is_advanced_settings() xor $is_quick_settings ) ? ' frm_hidden' : '';
1115
1116 return $classname . ( FrmAppHelper::pro_is_installed() ? ' frm-pro' : '' );
1117 }
1118
1119 /**
1120 * Retrieve the background image URL of the submit button.
1121 * It may be either a full URL string (used in versions prior to 6.14) or a numeric attachment ID (introduced in version 6.14).
1122 *
1123 * @since 6.14
1124 *
1125 * @param array $settings
1126 *
1127 * @return false|string Return image url or false.
1128 */
1129 public static function get_submit_image_bg_url( $settings ) {
1130 $background_image = $settings['submit_bg_img'];
1131
1132 if ( ! $background_image ) {
1133 return false;
1134 }
1135
1136 // Handle the case where the submit_bg_img is a full URL string. If the
1137 // Settings were saved with the older styler version prior to 6.14, the
1138 // submit_bg_img will be a full URL string.
1139 if ( ! is_numeric( $background_image ) ) {
1140 return $background_image;
1141 }
1142
1143 return wp_get_attachment_url( (int) $background_image );
1144 }
1145
1146 /**
1147 * Determines if the chosen JavaScript library should be used.
1148 *
1149 * @since 6.13
1150 *
1151 * @return bool
1152 */
1153 public static function use_chosen_js() {
1154 if ( ! FrmAppHelper::pro_is_installed() ) {
1155 return false;
1156 }
1157
1158 return is_callable( 'FrmProAppHelper::use_chosen_js' ) ? FrmProAppHelper::use_chosen_js() : true;
1159 }
1160
1161 /**
1162 * Returns the bottom part from a margin/padding value.
1163 *
1164 * @since 6.17
1165 *
1166 * @param string $value The margin/padding value.
1167 *
1168 * @return string
1169 */
1170 public static function get_bottom_value( $value ) {
1171 if ( ! $value ) {
1172 return $value;
1173 }
1174
1175 $parts = explode( ' ', $value );
1176
1177 return count( $parts ) < 3 ? $parts[0] : $parts[2];
1178 }
1179
1180 /**
1181 * Scope CSS to .frm_forms on admin pages to prevent style conflicts.
1182 * On front-end pages, the CSS is returned unchanged.
1183 *
1184 * @since 6.30
1185 *
1186 * @param string $css The CSS to scope.
1187 *
1188 * @return string
1189 */
1190 public static function maybe_scope_css_for_admin( $css ) {
1191 if ( ! self::should_scope_custom_css() ) {
1192 return $css;
1193 }
1194
1195 /**
1196 * Filter the CSS selector used for @scope when scoping custom CSS on admin pages.
1197 *
1198 * @since 6.30
1199 *
1200 * @param string $selector The CSS selector to scope to. Default '.frm_forms'.
1201 */
1202 $selector = apply_filters( 'frm_scope_custom_css_selector', '.frm_forms' );
1203
1204 return '@scope (' . $selector . ') {' . $css . '}';
1205 }
1206
1207 /**
1208 * Scope only the custom CSS portion of the full cached stylesheet for admin pages.
1209 * Extracts the global custom CSS from the cached CSS, outputs the theme CSS unchanged,
1210 * and returns only the custom CSS portion scoped.
1211 *
1212 * @since 6.30
1213 *
1214 * @param string $css The full cached CSS containing both theme and custom CSS.
1215 *
1216 * @return string The theme CSS with only the custom CSS portion scoped.
1217 */
1218 public static function maybe_scope_custom_css_in_cached_output( $css ) {
1219 if ( ! self::should_scope_custom_css() ) {
1220 return $css;
1221 }
1222
1223 $custom_css = strip_tags( FrmStylesController::get_custom_css() );
1224
1225 if ( ! $custom_css ) {
1226 return $css;
1227 }
1228
1229 // The cached CSS is minified. Minify the custom CSS the same way to find it.
1230 $minified_custom_css = self::minify_css( $custom_css );
1231 $custom_css_pos = strrpos( $css, $minified_custom_css );
1232
1233 if ( false !== $custom_css_pos ) {
1234 $css = substr( $css, 0, $custom_css_pos );
1235 }
1236
1237 return $css . self::maybe_scope_css_for_admin( $custom_css );
1238 }
1239
1240 /**
1241 * Minify CSS by stripping comments and whitespace.
1242 * Matches the minification used when saving the cached CSS file.
1243 *
1244 * @since 6.30
1245 *
1246 * @param string $css The CSS to minify.
1247 *
1248 * @return string
1249 */
1250 private static function minify_css( $css ) {
1251 return preg_replace( '/\/\*(.|\s)*?\*\//', '', str_replace( array( "\r\n", "\r", "\n", "\t", ' ' ), '', $css ) );
1252 }
1253
1254 /**
1255 * Check if custom CSS should be scoped for admin context.
1256 *
1257 * @since 6.30
1258 *
1259 * @return bool
1260 */
1261 private static function should_scope_custom_css() {
1262 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, used to scope CSS output for admin context.
1263 return isset( $_GET['frm_scope_custom_css'] );
1264 }
1265 }
1266