PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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 / FrmAppHelper.php

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

5,192 lines 140.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 FrmAppHelper {
7
8 /**
9 * Version of the database we are moving to.
10 *
11 * @var int
12 */
13 public static $db_version = 106;
14
15 /**
16 * Used by the API add-on.
17 *
18 * @var float
19 */
20 public static $font_version = 7;
21
22 /**
23 * @var bool
24 */
25 private static $added_gmt_offset_filter = false;
26
27 /**
28 * @since 2.0
29 *
30 * @var string
31 */
32 public static $plug_version = '6.35';
33
34 /**
35 * @var bool
36 */
37 private static $included_svg = false;
38
39 /**
40 * @since 1.07.02
41 *
42 * @return string The version of this plugin
43 */
44 public static function plugin_version() {
45 return self::$plug_version;
46 }
47
48 /**
49 * @return string
50 */
51 public static function plugin_folder() {
52 return basename( self::plugin_path() );
53 }
54
55 /**
56 * @return string
57 */
58 public static function plugin_path() {
59 return dirname( __DIR__, 2 );
60 }
61
62 /**
63 * @return string
64 */
65 public static function plugin_url() {
66 // Previously FRM_URL constant.
67 return plugins_url( '', self::plugin_path() . '/formidable.php' );
68 }
69
70 /**
71 * @return string
72 */
73 public static function relative_plugin_url() {
74 return str_replace( array( 'https:', 'http:' ), '', self::plugin_url() );
75 }
76
77 /**
78 * @return string Site URL
79 */
80 public static function site_url() {
81 return site_url();
82 }
83
84 /**
85 * Get the name of this site
86 * Used for [sitename] shortcode
87 *
88 * @since 2.0
89 *
90 * @return string
91 */
92 public static function site_name() {
93 return get_option( 'blogname' );
94 }
95
96 /**
97 * @param string $url
98 *
99 * @return string
100 */
101 public static function make_affiliate_url( $url ) {
102 $affiliate_id = self::get_affiliate();
103
104 if ( $affiliate_id ) {
105 $url = str_replace( array( 'http://', 'https://' ), '', $url );
106 $url = 'http://www.shareasale.com/r.cfm?u=' . absint( $affiliate_id ) . '&b=841990&m=64739&afftrack=plugin&urllink=' . urlencode( $url );
107 }
108
109 return $url;
110 }
111
112 /**
113 * @return int
114 */
115 public static function get_affiliate() {
116 return absint( apply_filters( 'frm_affiliate_id', 0 ) );
117 }
118
119 /**
120 * @since 3.04.02
121 *
122 * @param array|string $args If a string is passed, it is used for the utm_campaign attribute.
123 * @param string $page
124 */
125 public static function admin_upgrade_link( $args, $page = '' ) {
126 if ( $page ) {
127 $page = str_replace( 'https://formidableforms.com/', '', $page );
128 $page = 'https://formidableforms.com/' . $page;
129 } else {
130 $page = 'https://formidableforms.com/lite-upgrade/';
131 }
132
133 $args = is_array( $args ) ? self::adjust_legacy_utm_args( $args ) : array( 'campaign' => $args );
134
135 $query_args = array(
136 'utm_source' => 'plugin',
137 'utm_medium' => self::get_utm_medium(),
138 );
139 $query_args = self::maybe_add_utm_license( $query_args );
140
141 if ( isset( $args['campaign'] ) ) {
142 $query_args['utm_campaign'] = $args['campaign'];
143 }
144
145 if ( isset( $args['content'] ) ) {
146 $query_args['utm_content'] = $args['content'];
147 }
148
149 if ( isset( $args['param'] ) ) {
150 $query_args['f'] = $args['param'];
151 }
152
153 if ( ! empty( $args['plan'] ) ) {
154 $query_args['plan'] = $args['plan'];
155 }
156
157 $link = add_query_arg( $query_args, $page );
158
159 if ( isset( $args['anchor'] ) ) {
160 $link .= '#' . $args['anchor'];
161 }
162
163 return self::make_affiliate_url( $link );
164 }
165
166 /**
167 * If medium is "pro", add an additional utm_license param with their active license type.
168 *
169 * @since 6.25.1
170 *
171 * @param array $query_args
172 * @param string $link
173 *
174 * @return array
175 */
176 private static function maybe_add_utm_license( $query_args, $link = '' ) {
177 $medium = $query_args['utm_medium'] ?? self::pull_medium_from_link( $link );
178
179 if ( 'pro' === $medium && is_callable( 'FrmProAddonsController::get_readable_license_type' ) ) {
180 $query_args['utm_license'] = strtolower( FrmProAddonsController::get_readable_license_type() );
181 }
182
183 return $query_args;
184 }
185
186 /**
187 * @since 6.26
188 *
189 * @param string $link
190 *
191 * @return string
192 */
193 private static function pull_medium_from_link( $link ) {
194 if ( ! $link ) {
195 return '';
196 }
197
198 $parsed = parse_url( $link );
199
200 if ( ! is_array( $parsed ) || ! isset( $parsed['query'] ) ) {
201 return '';
202 }
203
204 $query_args = wp_parse_args( $parsed['query'] );
205
206 return ! empty( $query_args['utm_medium'] ) ? $query_args['utm_medium'] : '';
207 }
208
209 /**
210 * @since 6.25.1
211 *
212 * @return string
213 */
214 private static function get_utm_medium() {
215 return self::pro_is_connected() ? 'pro' : 'lite';
216 }
217
218 /**
219 * Change campaign from "liteplugin" to what we're currently using for medium.
220 *
221 * @since 6.25.1
222 *
223 * @param array $args
224 *
225 * @return array
226 */
227 private static function adjust_legacy_utm_args( $args ) {
228 if ( isset( $args['medium'] ) ) {
229 $args['campaign'] = $args['medium'];
230 unset( $args['medium'] );
231 }
232
233 return $args;
234 }
235
236 /**
237 * @since 6.21
238 *
239 * @param string $cta_link
240 * @param array $utm
241 */
242 public static function maybe_add_missing_utm( $cta_link, $utm ) {
243 $utm = self::adjust_legacy_utm_args( $utm );
244 $query_args = array();
245
246 if ( ! str_contains( $cta_link, 'utm_source' ) ) {
247 $query_args['utm_source'] = 'plugin';
248 }
249
250 if ( ! str_contains( $cta_link, 'utm_medium' ) ) {
251 $query_args['utm_medium'] = self::get_utm_medium();
252 }
253
254 if ( ! str_contains( $cta_link, 'utm_campaign' ) && isset( $utm['campaign'] ) ) {
255 $query_args['utm_campaign'] = $utm['campaign'];
256 }
257
258 if ( ! str_contains( $cta_link, 'utm_content' ) && isset( $utm['content'] ) ) {
259 $query_args['utm_content'] = $utm['content'];
260 }
261
262 $query_args = self::maybe_add_utm_license( $query_args, $cta_link );
263
264 return $query_args ? add_query_arg( $query_args, $cta_link ) : $cta_link;
265 }
266
267 /**
268 * Get the Formidable settings
269 *
270 * @since 2.0
271 *
272 * @param array $args - May include the form id when values need translation.
273 *
274 * @return FrmSettings
275 */
276 public static function get_settings( $args = array() ) {
277 global $frm_settings;
278
279 if ( ! $frm_settings ) {
280 $frm_settings = new FrmSettings( $args );
281 } elseif ( isset( $args['current_form'] ) ) {
282 // If the global has already been set, allow strings to be filtered.
283 $frm_settings->maybe_filter_for_form( $args );
284 }
285
286 return $frm_settings;
287 }
288
289 /**
290 * @return string
291 */
292 public static function get_menu_name() {
293 if ( ! self::pro_is_installed() || FrmAddonsController::is_license_expired() ) {
294 return 'Formidable';
295 }
296
297 return self::get_settings()->menu;
298 }
299
300 /**
301 * Determine if the current branding is set to 'formidable'.
302 * Checks the menu icon, and verifies if it matches the formidable branding.
303 *
304 * @since 6.4.2
305 *
306 * @return bool True if the menu icon is the logo, false otherwise.
307 */
308 public static function is_formidable_branding() {
309 if ( ! self::pro_is_installed() ) {
310 return true;
311 }
312
313 return str_contains( self::get_menu_icon_class(), 'frm_logo_icon' );
314 }
315
316 /**
317 * @since 3.05
318 *
319 * @param array $atts
320 *
321 * @return string
322 */
323 public static function svg_logo( $atts = array() ) {
324 $defaults = array(
325 'height' => 18,
326 'width' => 18,
327 'fill' => '#4d4d4d',
328 'orange' => '#f05a24',
329 );
330 $atts = array_merge( $defaults, $atts );
331
332 // phpcs:disable SlevomatCodingStandard.Files.LineLength.LineTooLong
333 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 599.68 601.37" width="' . esc_attr( $atts['width'] ) . '" height="' . esc_attr( $atts['height'] ) . '">
334 <path fill="' . esc_attr( $atts['orange'] ) . '" d="M289.6 384h140v76h-140z"/>
335 <path fill="' . esc_attr( $atts['fill'] ) . '" d="M400.2 147h-200c-17 0-30.6 12.2-30.6 29.3V218h260v-71zM397.9 264H169.6v196h75V340H398a32.2 32.2 0 0 0 30.1-21.4 24.3 24.3 0 0 0 1.7-8.7V264zM299.8 601.4A300.3 300.3 0 0 1 0 300.7a299.8 299.8 0 1 1 511.9 212.6 297.4 297.4 0 0 1-212 88zm0-563A262 262 0 0 0 38.3 300.7a261.6 261.6 0 1 0 446.5-185.5 259.5 259.5 0 0 0-185-76.8z"/>
336 </svg>';
337 // phpcs:enable SlevomatCodingStandard.Files.LineLength.LineTooLong
338 }
339
340 /**
341 * @since 4.0
342 *
343 * @param array $atts
344 *
345 * @return void
346 */
347 public static function show_logo( $atts = array() ) {
348 self::kses_echo( self::svg_logo( $atts ), 'all' );
349 }
350
351 /**
352 * @since 4.03.02
353 *
354 * @return void
355 */
356 public static function show_header_logo() {
357 $icon = self::svg_logo(
358 array(
359 'height' => 35,
360 'width' => 35,
361 )
362 );
363
364 $new_icon = apply_filters( 'frm_icon', $icon, true );
365
366 if ( $new_icon !== $icon ) {
367 if ( str_starts_with( $new_icon, '<svg' ) ) {
368 $icon = str_replace( 'viewBox="0 0 20', 'width="30" height="35" style="color:#929699" viewBox="0 0 20', $new_icon );
369 } else {
370 // Show nothing if it isn't an SVG.
371 $icon = '<div style="height:39px"></div>';
372 }
373 }
374 self::kses_echo( $icon, 'all' );
375 }
376
377 /**
378 * @since 2.02.04
379 *
380 * @return bool
381 */
382 public static function ips_saved() {
383 $frm_settings = self::get_settings();
384 return ! $frm_settings->no_ips;
385 }
386
387 /**
388 * @return bool
389 */
390 public static function pro_is_installed() {
391 return (bool) apply_filters( 'frm_pro_installed', false );
392 }
393
394 /**
395 * Check if the Pro plugin is installed, whether authorized or not.
396 *
397 * @since 6.8.3
398 *
399 * @return bool
400 */
401 public static function pro_is_included() {
402 return function_exists( 'load_formidable_pro' );
403 }
404
405 /**
406 * @since 4.06.02
407 *
408 * @return bool
409 */
410 public static function pro_is_connected() {
411 global $frm_vars;
412 return self::pro_is_installed() && $frm_vars['pro_is_authorized'];
413 }
414
415 /**
416 * @since 4.06
417 * @since 6.16.2 Added $check_for_settings parameter
418 *
419 * @param bool $check_for_settings
420 *
421 * @return bool
422 */
423 public static function is_form_builder_page( $check_for_settings = true ) {
424 $action = self::simple_get( 'frm_action', 'sanitize_title' );
425 $check_actions = array( 'edit', 'duplicate' );
426
427 if ( $check_for_settings ) {
428 $check_actions[] = 'settings';
429 }
430
431 return self::is_admin_page( 'formidable' ) && in_array( $action, $check_actions, true );
432 }
433
434 /**
435 * @return bool
436 */
437 public static function is_formidable_admin() {
438 $page = self::simple_get( 'page', 'sanitize_title' );
439
440 if ( ! $page ) {
441 return self::is_view_builder_page();
442 }
443
444 return str_contains( $page, 'formidable' );
445 }
446
447 /**
448 * Checks if is a list page.
449 *
450 * @since 6.19
451 *
452 * @param string $page The name of the page to check.
453 *
454 * @return bool
455 */
456 public static function is_admin_list_page( $page = 'formidable' ) {
457 if ( 'formidable' === $page ) {
458 return self::on_form_listing_page();
459 }
460
461 if ( ! self::is_admin_page( $page ) ) {
462 return false;
463 }
464
465 if ( 'formidable-entries' === $page ) {
466 $action = self::simple_get( 'frm_action' );
467
468 if ( ! $action || in_array( $action, self::get_entries_listing_page_form_actions(), true ) ) {
469 return true;
470 }
471 }
472
473 // Check edit or settings page.
474 return ! self::simple_get( 'frm_action' );
475 }
476
477 /**
478 * @since 6.20
479 *
480 * @return array<string>
481 */
482 private static function get_entries_listing_page_form_actions() {
483 return array( 'list', 'destroy' );
484 }
485
486 /**
487 * Check for certain page in Formidable settings
488 *
489 * @since 2.0
490 *
491 * @param string $page The name of the page to check.
492 *
493 * @return bool
494 */
495 public static function is_admin_page( $page = 'formidable' ) {
496 global $pagenow;
497 $get_page = self::simple_get( 'page', 'sanitize_title' );
498
499 if ( $pagenow ) {
500 // Allow this to be true during ajax load i.e. ajax form builder loading
501 $is_page = ( $pagenow === 'admin.php' || $pagenow === 'admin-ajax.php' ) && $get_page === $page;
502
503 if ( $is_page ) {
504 return true;
505 }
506 }
507
508 return is_admin() && $get_page === $page;
509 }
510
511 /**
512 * If the current page is for editing or creating a view.
513 * Returns false for the views listing page.
514 *
515 * @since 4.0
516 *
517 * @return bool
518 */
519 public static function is_view_builder_page() {
520 global $pagenow;
521
522 if ( ! in_array( $pagenow, array( 'post.php', 'post-new.php', 'edit.php' ), true ) ) {
523 return false;
524 }
525
526 $post_type = self::simple_get( 'post_type', 'sanitize_title' );
527
528 if ( ! $post_type ) {
529 $post_id = self::simple_get( 'post', 'absint' );
530 $post = get_post( $post_id );
531 $post_type = $post ? $post->post_type : '';
532 }
533
534 return $post_type === 'frm_display';
535 }
536
537 /**
538 * Check for the form preview page
539 *
540 * @since 2.0
541 *
542 * @return bool
543 */
544 public static function is_preview_page() {
545 global $pagenow;
546 $action = self::simple_get( 'action', 'sanitize_title' );
547
548 return $pagenow === 'admin-ajax.php' && $action === 'frm_forms_preview';
549 }
550
551 /**
552 * Check for ajax except the form preview page
553 *
554 * @since 2.0
555 *
556 * @return bool
557 */
558 public static function doing_ajax() {
559 return wp_doing_ajax() && ! self::is_preview_page();
560 }
561
562 /**
563 * @return string
564 */
565 public static function js_suffix() {
566 return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
567 }
568
569 /**
570 * @since 2.0.8
571 *
572 * @return bool
573 */
574 public static function prevent_caching() {
575 global $frm_vars;
576 return ! empty( $frm_vars['prevent_caching'] );
577 }
578
579 /**
580 * Check if on an admin page
581 *
582 * @since 2.0
583 *
584 * @return bool
585 */
586 public static function is_admin() {
587 $is_admin = is_admin() && ! wp_doing_ajax();
588
589 /**
590 * @since 6.0
591 *
592 * @param bool $is_admin
593 */
594 return apply_filters( 'frm_is_admin', $is_admin );
595 }
596
597 /**
598 * Check if value contains blank value or empty array
599 *
600 * @since 2.0
601 *
602 * @param mixed $value Value to check.
603 * @param string $empty
604 *
605 * @return bool
606 */
607 public static function is_empty_value( $value, $empty = '' ) {
608 return $value === array() || $value === $empty;
609 }
610
611 /**
612 * @param mixed $value
613 * @param string $empty
614 *
615 * @return bool
616 */
617 public static function is_not_empty_value( $value, $empty = '' ) {
618 return ! self::is_empty_value( $value, $empty );
619 }
620
621 /**
622 * Get any value from the $_SERVER
623 *
624 * @since 2.0
625 *
626 * @param string $value
627 *
628 * @return string
629 */
630 public static function get_server_value( $value ) {
631 return isset( $_SERVER[ $value ] ) ? wp_strip_all_tags( wp_unslash( $_SERVER[ $value ] ) ) : '';
632 }
633
634 /**
635 * Get the server OS
636 *
637 * @since 6.4.2
638 *
639 * @return string
640 */
641 public static function get_server_os() {
642 if ( function_exists( 'php_uname' ) ) {
643 return php_uname( 's' );
644 }
645
646 if ( ! defined( 'PHP_OS' ) ) {
647 return '';
648 }
649
650 // match the same response for Windows server as php_uname('s')
651 return in_array( PHP_OS, array( 'WIN32', 'WINNT', 'Windows_NT' ), true ) ? 'Windows NT' : PHP_OS;
652 }
653
654 /**
655 * Check for the IP address in several places (when custom headers are enabled).
656 * Used by [ip] shortcode.
657 *
658 * @return string The IP address of the current user
659 */
660 public static function get_ip_address() {
661 $ip_options = self::should_use_custom_header_ip() ? self::get_custom_header_keys_for_ip() : array( 'REMOTE_ADDR' );
662 $ip = '';
663
664 foreach ( $ip_options as $key ) {
665 if ( ! isset( $_SERVER[ $key ] ) ) {
666 continue;
667 }
668
669 $key = self::get_server_value( $key );
670
671 foreach ( explode( ',', $key ) as $ip ) {
672 // Just to be safe.
673 $ip = trim( $ip );
674
675 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) {
676 return sanitize_text_field( $ip );
677 }
678 }
679 }
680
681 return sanitize_text_field( $ip );
682 }
683
684 /**
685 * @since 6.1
686 *
687 * @return array
688 */
689 public static function get_custom_header_keys_for_ip() {
690 return array(
691 'HTTP_CLIENT_IP',
692 'HTTP_CF_CONNECTING_IP',
693 'HTTP_X_FORWARDED_FOR',
694 'HTTP_X_FORWARDED',
695 'HTTP_X_CLUSTER_CLIENT_IP',
696 'HTTP_X_REAL_IP',
697 'HTTP_FORWARDED_FOR',
698 'HTTP_FORWARDED',
699 'REMOTE_ADDR',
700 );
701 }
702
703 /**
704 * Check if we should check every HTTP header or just $_SERVER['REMOTE_ADDR'].
705 * The other HTTP headers can be spoofed so this isn't recommended.
706 * But in some cases (like reverse proxies), the IP may be empty if you use $_SERVER['REMOTE_ADDR'].
707 *
708 * @since 6.1
709 *
710 * @return bool
711 */
712 private static function should_use_custom_header_ip() {
713 $settings = self::get_settings();
714 $should_use_custom_header_ip = ! $settings->no_ips && $settings->custom_header_ip;
715
716 /**
717 * Filter whether to check spoofable HTTP headers.
718 * This uses the custom_header_ip setting, but it is hidden if the GDPR option is also on.
719 * As the IP is still checked for blacklist checks, someone with the GDPR option may still want to enable this when behind a reverse proxy.
720 *
721 * @since 6.1
722 *
723 * @param bool $should_use_custom_header_ip
724 */
725 return apply_filters( 'frm_use_custom_header_ip', $should_use_custom_header_ip );
726 }
727
728 /**
729 * @param string $param
730 * @param mixed $default
731 * @param string $src
732 * @param callable|string $sanitize
733 *
734 * @return mixed
735 */
736 public static function get_param( $param, $default = '', $src = 'get', $sanitize = '' ) {
737 if ( str_contains( $param, '[' ) ) {
738 $params = explode( '[', $param );
739 $param = $params[0];
740 }
741
742 if ( $src === 'get' ) {
743 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
744 $value = isset( $_POST[ $param ] ) ? wp_unslash( $_POST[ $param ] ) : ( isset( $_GET[ $param ] ) ? wp_unslash( $_GET[ $param ] ) : $default );
745
746 if ( ! isset( $_POST[ $param ] ) && isset( $_GET[ $param ] ) && ! is_array( $value ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
747 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
748 $value = htmlspecialchars_decode( wp_unslash( $_GET[ $param ] ) );
749 }
750 self::sanitize_value( $sanitize, $value );
751 } else {
752 $value = self::get_simple_request(
753 array(
754 'type' => $src,
755 'param' => $param,
756 'default' => $default,
757 'sanitize' => $sanitize,
758 )
759 );
760 }
761
762 if ( ! isset( $params ) || ! is_array( $value ) || ! $value ) {
763 return $value;
764 }
765
766 foreach ( $params as $k => $p ) {
767 if ( ! $k || ! is_array( $value ) ) {
768 continue;
769 }
770
771 $p = trim( $p, ']' );
772 $value = $value[ $p ] ?? $default;
773 }
774
775 return $value;
776 }
777
778 /**
779 * Get a value from $_POST data.
780 *
781 * @param string $param The key we are trying to access data from in $_POST.
782 * @param mixed $default The default if nothing is being sent.
783 * @param callable|string $sanitize Make sure to pass a sanitize method here. This function will NOT sanitize by default.
784 * @param bool $serialized
785 *
786 * @return mixed
787 */
788 public static function get_post_param( $param, $default = '', $sanitize = '', $serialized = false ) {
789 return self::get_simple_request(
790 array(
791 'type' => 'post',
792 'param' => $param,
793 'default' => $default,
794 'sanitize' => $sanitize,
795 'serialized' => $serialized,
796 )
797 );
798 }
799
800 /**
801 * @since 2.0
802 *
803 * @param string $param
804 * @param string $sanitize
805 * @param string $default
806 *
807 * @return array|int|string
808 */
809 public static function simple_get( $param, $sanitize = 'sanitize_text_field', $default = '' ) {
810 return self::get_simple_request(
811 array(
812 'type' => 'get',
813 'param' => $param,
814 'default' => $default,
815 'sanitize' => $sanitize,
816 )
817 );
818 }
819
820 /**
821 * Get a GET/POST/REQUEST value and sanitize it
822 *
823 * @since 2.0.6
824 *
825 * @param array $args
826 *
827 * @return array|string
828 */
829 public static function get_simple_request( $args ) {
830 $defaults = array(
831 'param' => '',
832 'default' => '',
833 'type' => 'get',
834 'sanitize' => 'sanitize_text_field',
835 'serialized' => false,
836 );
837 $args = wp_parse_args( $args, $defaults );
838 $value = $args['default'];
839
840 if ( $args['type'] === 'get' ) {
841 if ( $_GET && isset( $_GET[ $args['param'] ] ) ) {
842 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
843 $value = wp_unslash( $_GET[ $args['param'] ] );
844 }
845 } elseif ( $args['type'] === 'post' ) {
846 if ( isset( $_POST[ $args['param'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
847 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
848 $value = wp_unslash( $_POST[ $args['param'] ] );
849
850 if ( $args['serialized'] === true && is_serialized_string( $value ) && is_serialized( $value ) ) {
851 self::unserialize_or_decode( $value );
852 }
853 }
854 } elseif ( isset( $_REQUEST[ $args['param'] ] ) ) {
855 // phpcs:ignore phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
856 $value = wp_unslash( $_REQUEST[ $args['param'] ] );
857 }
858
859 self::sanitize_value( $args['sanitize'], $value );
860
861 return $value;
862 }
863
864 /**
865 * Preserve backslashes in a value, but make sure value doesn't get compounding slashes
866 *
867 * @since 2.0.8
868 *
869 * @param string $value
870 *
871 * @return string Value.
872 */
873 public static function preserve_backslashes( $value ) {
874 // If backslashes have already been added, don't add them again
875 return str_contains( $value, '\\\\' ) ? $value : addslashes( $value );
876 }
877
878 /**
879 * Sanitize a value in-place.
880 * If $value is an array, the sanitize function will get called for each item.
881 *
882 * @param callable $sanitize
883 * @param mixed $value
884 *
885 * @return void
886 */
887 public static function sanitize_value( $sanitize, &$value ) {
888 if ( ! $sanitize ) {
889 return;
890 }
891
892 if ( is_object( $value ) ) {
893 $value = '';
894 return;
895 }
896
897 if ( is_array( $value ) ) {
898 $temp_values = $value;
899
900 foreach ( $temp_values as $k => $v ) {
901 self::sanitize_value( $sanitize, $value[ $k ] );
902 }
903
904 return;
905 }
906
907 $value = call_user_func( $sanitize, $value );
908 }
909
910 /**
911 * @param array $sanitize_method
912 * @param array $values
913 *
914 * @return void
915 */
916 public static function sanitize_request( $sanitize_method, &$values ) {
917 $temp_values = $values;
918
919 foreach ( $temp_values as $k => $val ) {
920 if ( isset( $sanitize_method[ $k ] ) ) {
921 $values[ $k ] = call_user_func( $sanitize_method[ $k ], $val );
922 }
923 }
924 }
925
926 /**
927 * @since 4.0.04
928 *
929 * @param mixed $value
930 *
931 * @return void
932 */
933 public static function sanitize_with_html( &$value ) {
934 if ( current_user_can( 'frm_edit_entries' ) || current_user_can( 'administrator' ) ) {
935 // Only strip unsafe HTML like scripts for a privileged user submitting a form.
936 self::sanitize_value( 'wp_kses_post', $value );
937 } else {
938 self::sanitize_value( self::class . '::strip_most_html', $value );
939 }
940 self::decode_specialchars( $value );
941 self::sanitize_value( 'FrmHtmlSanitizer::sanitize_url_attributes', $value );
942 }
943
944 /**
945 * Allow only a small set of very basic HTML for unprivileged users.
946 *
947 * @since 6.7.1
948 *
949 * @param string $value
950 */
951 public static function strip_most_html( $value ) {
952 if ( '' === $value ) {
953 return $value;
954 }
955
956 $allowed_html = array(
957 'b' => array(),
958 'br' => array(),
959 'strong' => array(),
960 'p' => array(),
961 'i' => array(),
962 'ul' => array(),
963 'ol' => array(),
964 'li' => array(),
965 );
966
967 /**
968 * @since 6.7.1
969 *
970 * @param array $allowed_html
971 */
972 $allowed_html = apply_filters( 'frm_allowed_form_input_html', $allowed_html );
973
974 return wp_kses( $value, $allowed_html );
975 }
976
977 /**
978 * Do wp_specialchars_decode to get back '&' that wp_kses_post might have turned to '&amp;'
979 * this MUST be done, else we'll be back to the '& entity' problem.
980 *
981 * @since 4.0.04
982 *
983 * @param mixed $value Value to decode, passed by reference.
984 */
985 public static function decode_specialchars( &$value ) {
986 if ( is_array( $value ) ) {
987 $temp_values = $value;
988
989 foreach ( $temp_values as $k => $v ) {
990 self::decode_specialchars( $value[ $k ] );
991 }
992 } else {
993 self::decode_amp( $value );
994 }
995 }
996
997 /**
998 * The wp_specialchars_decode function changes too much.
999 * This will leave HTML as is, but still convert &.
1000 * Adapted from wp_specialchars_decode().
1001 *
1002 * @since 4.03.01
1003 *
1004 * @param string $string The string to prep, passed by reference.
1005 */
1006 private static function decode_amp( &$string ) {
1007 // Don't bother if there are no entities - saves a lot of processing
1008 if ( ! $string || ! str_contains( $string, '&' ) ) {
1009 return;
1010 }
1011
1012 $translation = array(
1013 '&quot;' => '"',
1014 '&#034;' => '"',
1015 '&#x22;' => '"',
1016 // The space preserves the HTML.
1017 '&lt; ' => '< ',
1018 // The space preserves the HTML.
1019 '&#060; ' => '< ',
1020 '&gt;' => '>',
1021 '&#062;' => '>',
1022 '&amp;' => '&',
1023 '&#038;' => '&',
1024 '&#x26;' => '&',
1025 );
1026
1027 $translation_preg = array(
1028 '/&#0*34;/' => '&#034;',
1029 '/&#x0*22;/i' => '&#x22;',
1030 '/&#0*60;/' => '&#060;',
1031 '/&#0*62;/' => '&#062;',
1032 '/&#0*38;/' => '&#038;',
1033 '/&#x0*26;/i' => '&#x26;',
1034 );
1035
1036 // Remove zero padding on numeric entities
1037 $string = preg_replace( array_keys( $translation_preg ), array_values( $translation_preg ), $string );
1038
1039 // Replace characters according to translation table
1040 $string = strtr( $string, $translation );
1041 }
1042
1043 /**
1044 * Sanitize the value, and allow some HTML
1045 *
1046 * @since 2.0
1047 *
1048 * @param string $value The value to sanitize.
1049 * @param array|string $allowed Allowed HTML tags and attributes, or 'all' for defaults.
1050 *
1051 * @return string
1052 */
1053 public static function kses( $value, $allowed = array() ) {
1054 return wp_kses( $value, self::allowed_html( $allowed ) );
1055 }
1056
1057 /**
1058 * Sanitizes and echoes a given value.
1059 *
1060 * @since 6.18
1061 *
1062 * @param string $value The value to sanitize and output.
1063 * @param array|string $allowed Allowed HTML tags and attributes.
1064 *
1065 * @return void
1066 */
1067 public static function kses_echo( $value, $allowed = array() ) {
1068 echo self::kses( $value, $allowed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1069 }
1070
1071 /**
1072 * The regular kses function strips [button_action] from submit button HTML.
1073 *
1074 * @since 5.0.13
1075 *
1076 * @param string $html
1077 *
1078 * @return string
1079 */
1080 public static function kses_submit_button( $html ) {
1081 $included_button_action = str_contains( $html, '[button_action]' );
1082 $included_back_hook = str_contains( $html, '[back_hook]' );
1083 $included_draft_hook = str_contains( $html, '[draft_hook]' );
1084 add_filter( 'safe_style_css', 'FrmAppHelper::allow_visibility_style' );
1085 add_filter( 'frm_striphtml_allowed_tags', 'FrmAppHelper::add_allowed_submit_button_tags' );
1086 $html = self::kses( $html, 'all' );
1087 remove_filter( 'safe_style_css', 'FrmAppHelper::allow_visibility_style' );
1088 remove_filter( 'frm_striphtml_allowed_tags', 'FrmAppHelper::add_allowed_submit_button_tags' );
1089
1090 if ( $included_button_action ) {
1091 if ( str_contains( $html, '<input type="submit"' ) ) {
1092 $pattern = '/(<input type="submit")([^>]*)(\/>)/';
1093 $html = preg_replace( $pattern, '$1$2[button_action] $3', $html, 1 );
1094 } else {
1095 $pattern = '/(<button)(.*)(class=")(.*)(frm_button_submit)(.*)(")(.*)([^>]+)(>)/';
1096 $html = preg_replace( $pattern, '$1$2$3$4$5$6$7 [button_action]$8$9$10', $html, 1 );
1097 }
1098 }
1099
1100 if ( $included_back_hook ) {
1101 $html = str_replace( 'class="frm_prev_page"', 'class="frm_prev_page" [back_hook]', $html );
1102 }
1103
1104 if ( $included_draft_hook ) {
1105 return str_replace( 'class="frm_save_draft"', 'class="frm_save_draft" [draft_hook]', $html );
1106 }
1107
1108 return $html;
1109 }
1110
1111 /**
1112 * @since 5.0.13
1113 *
1114 * @param array $allowed_attr
1115 *
1116 * @return array
1117 */
1118 public static function allow_visibility_style( $allowed_attr ) {
1119 $allowed_attr[] = 'visibility';
1120 return $allowed_attr;
1121 }
1122
1123 /**
1124 * @since 5.0.13
1125 *
1126 * @param array $allowed_html
1127 *
1128 * @return array
1129 */
1130 public static function add_allowed_submit_button_tags( $allowed_html ) {
1131 $allowed_html['input'] = array(
1132 'type' => true,
1133 'value' => true,
1134 'formnovalidate' => true,
1135 'name' => true,
1136 'class' => true,
1137 );
1138 $allowed_html['button']['formnovalidate'] = true;
1139 $allowed_html['button']['name'] = true;
1140 $allowed_html['img']['style'] = true;
1141 return $allowed_html;
1142 }
1143
1144 /**
1145 * @since 2.05.03
1146 *
1147 * @param array|string $allowed
1148 *
1149 * @return array
1150 */
1151 private static function allowed_html( $allowed ) {
1152 $html = self::safe_html();
1153 $allowed_html = array();
1154
1155 if ( $allowed === 'all' ) {
1156 $allowed_html = $html;
1157 } elseif ( $allowed ) {
1158 foreach ( (array) $allowed as $a ) {
1159 $allowed_html[ $a ] = $html[ $a ] ?? array();
1160 }
1161 }
1162
1163 return apply_filters( 'frm_striphtml_allowed_tags', $allowed_html );
1164 }
1165
1166 /**
1167 * @since 2.05.03
1168 *
1169 * @return array
1170 */
1171 private static function safe_html() {
1172 $allow_class = array(
1173 'class' => true,
1174 'id' => true,
1175 );
1176
1177 return array(
1178 'a' => array(
1179 'class' => true,
1180 'href' => true,
1181 'id' => true,
1182 'rel' => true,
1183 'target' => true,
1184 'title' => true,
1185 'tabindex' => true,
1186 ),
1187 'abbr' => array(
1188 'title' => true,
1189 ),
1190 'aside' => $allow_class,
1191 'b' => array(),
1192 'blockquote' => array(
1193 'cite' => true,
1194 ),
1195 'br' => array(),
1196 'cite' => array(
1197 'title' => true,
1198 ),
1199 'code' => array(),
1200 'defs' => array(),
1201 'del' => array(
1202 'datetime' => true,
1203 'title' => true,
1204 ),
1205 'dd' => array(),
1206 'div' => array(
1207 'class' => true,
1208 'id' => true,
1209 'title' => true,
1210 'style' => true,
1211 'role' => true,
1212 ),
1213 'dl' => array(),
1214 'dt' => array(),
1215 'em' => array(),
1216 'h1' => $allow_class,
1217 'h2' => $allow_class,
1218 'h3' => $allow_class,
1219 'h4' => $allow_class,
1220 'h5' => $allow_class,
1221 'h6' => $allow_class,
1222 'i' => array(
1223 'class' => true,
1224 'id' => true,
1225 'icon' => true,
1226 'style' => true,
1227 ),
1228 'img' => array(
1229 'alt' => true,
1230 'class' => true,
1231 'height' => true,
1232 'id' => true,
1233 'src' => true,
1234 'width' => true,
1235 ),
1236 'li' => $allow_class,
1237 'ol' => $allow_class,
1238 'p' => $allow_class,
1239 'path' => array(
1240 'd' => true,
1241 'fill' => true,
1242 ),
1243 'pre' => array(),
1244 'q' => array(
1245 'cite' => true,
1246 'title' => true,
1247 ),
1248 'rect' => array(
1249 'class' => true,
1250 'fill' => true,
1251 'height' => true,
1252 'width' => true,
1253 'x' => true,
1254 'y' => true,
1255 'rx' => true,
1256 'stroke' => true,
1257 'stroke-opacity' => true,
1258 'stroke-width' => true,
1259 ),
1260 'section' => $allow_class,
1261 'span' => array(
1262 'class' => true,
1263 'id' => true,
1264 'title' => true,
1265 'style' => true,
1266 'aria-hidden' => true,
1267 ),
1268 'strike' => array(),
1269 'strong' => array(),
1270 'symbol' => array(
1271 'class' => true,
1272 'id' => true,
1273 'viewbox' => true,
1274 ),
1275 'svg' => array(
1276 'class' => true,
1277 'id' => true,
1278 'xmlns' => true,
1279 'viewbox' => true,
1280 'width' => true,
1281 'height' => true,
1282 'style' => true,
1283 'fill' => true,
1284 'aria-label' => true,
1285 'aria-hidden' => true,
1286 ),
1287 'use' => array(
1288 'href' => true,
1289 'xlink:href' => true,
1290 ),
1291 'ul' => $allow_class,
1292 'label' => array(
1293 'for' => true,
1294 'class' => true,
1295 'id' => true,
1296 ),
1297 'button' => array(
1298 'class' => true,
1299 'type' => true,
1300 ),
1301 'legend' => array(
1302 'class' => true,
1303 ),
1304 'option' => array(
1305 'class' => true,
1306 'value' => true,
1307 'selected' => true,
1308 ),
1309 );
1310 }
1311
1312 /**
1313 * Used when switching the action for a bulk action
1314 *
1315 * @since 2.0
1316 */
1317 public static function remove_get_action() {
1318 if ( empty( $_GET ) ) {
1319 return;
1320 }
1321
1322 $action_name = isset( $_GET['action'] ) ? 'action' : ( isset( $_GET['action2'] ) ? 'action2' : '' );
1323
1324 if ( ! $action_name ) {
1325 return;
1326 }
1327
1328 $new_action = self::get_param( $action_name, '', 'get', 'sanitize_text_field' );
1329
1330 if ( $new_action ) {
1331 $_SERVER['REQUEST_URI'] = str_replace( '&action=' . $new_action, '', self::get_server_value( 'REQUEST_URI' ) );
1332 }
1333 }
1334
1335 /**
1336 * Check the WP query for a parameter
1337 *
1338 * @since 2.0
1339 *
1340 * @param array|string $value
1341 * @param string $param
1342 *
1343 * @return array|string
1344 */
1345 public static function get_query_var( $value, $param ) {
1346 // phpcs:ignore Universal.Operators.StrictComparisons
1347 if ( $value != '' ) {
1348 return $value;
1349 }
1350
1351 global $wp_query;
1352
1353 return $wp_query->query_vars[ $param ] ?? $value;
1354 }
1355
1356 /**
1357 * Try to show the SVG if possible. Otherwise, use the font icon.
1358 *
1359 * @since 4.0.02
1360 *
1361 * @param string $class
1362 * @param array $atts
1363 *
1364 * @return string|null
1365 */
1366 public static function icon_by_class( $class, $atts = array() ) {
1367 $echo = ! isset( $atts['echo'] ) || $atts['echo'];
1368
1369 if ( isset( $atts['echo'] ) ) {
1370 unset( $atts['echo'] );
1371 }
1372
1373 $icon = trim( str_replace( array( 'frm_icon_font', 'frmfont ' ), '', $class ) );
1374
1375 // Replace icons that have been removed or renamed.
1376 $deprecated = array(
1377 'frm_clone_solid_icon' => 'frm_clone_icon',
1378 'frm_keyalt_icon' => 'frm_key_icon',
1379 'frm_keyalt_solid_icon' => 'frm_key_solid_icon',
1380 );
1381
1382 if ( isset( $deprecated[ $icon ] ) ) {
1383 $icon = $deprecated[ $icon ];
1384 $class = str_replace( $icon, $deprecated[ $icon ], $class );
1385 }
1386
1387 $is_font_icon = $icon === $class;
1388
1389 if ( ! $is_font_icon ) {
1390 $class = str_contains( $icon, ' ' ) ? ' ' . $icon : '';
1391
1392 if ( str_contains( $icon, ' ' ) ) {
1393 $icon = explode( ' ', $icon );
1394 $icon = reset( $icon );
1395 }
1396 }
1397
1398 if ( $atts ) {
1399 // A caller passed attributes, so kses still has to decide which of them survive. Its
1400 // allowlist comes from safe_html() through the frm_striphtml_allowed_tags filter, and
1401 // add-ons widen it around their own icons, so there is no fixed list to check against.
1402 $html_atts = self::array_to_html_params( $atts );
1403 $markup = $is_font_icon
1404 ? '<i class="' . esc_attr( $class ) . '"' . $html_atts . '></i>'
1405 : '<svg class="frmsvg' . esc_attr( $class ) . '"' . $html_atts . '><use href="#' . esc_attr( $icon ) . '" /></svg>';
1406
1407 if ( ! $echo ) {
1408 return $markup;
1409 }
1410
1411 echo self::kses_icon( $markup ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1412 return null;
1413 }
1414
1415 /**
1416 * With no attributes from the caller, the tag is nothing but this method's own markup
1417 * around an escaped class and icon id, so there is nothing left for kses to decide and it
1418 * can be skipped. Echoing the pieces rather than a finished string keeps that safe to the
1419 * escaping sniff without an annotation.
1420 *
1421 * This is the path the form builder takes for most of the tens of thousands of icons it
1422 * renders on a large form, and the kses pass was most of what each one cost.
1423 */
1424 $callback = function () use ( $is_font_icon, $class, $icon ) {
1425 if ( $is_font_icon ) {
1426 echo '<i class="' . esc_attr( $class ) . '"></i>';
1427 return;
1428 }
1429
1430 echo '<svg class="frmsvg' . esc_attr( $class ) . '"><use href="#' . esc_attr( $icon ) . '" /></svg>';
1431 };
1432
1433 return self::clip( $callback, $echo );
1434 }
1435
1436 /**
1437 * Run kses for icons. It needs to add a few filters first in order to preserve some custom style values.
1438 *
1439 * @since 5.0.13
1440 *
1441 * @param string $icon
1442 *
1443 * @return string
1444 */
1445 public static function kses_icon( $icon ) {
1446 add_filter( 'safe_style_css', 'FrmAppHelper::allow_vars_in_styles' );
1447 add_filter( 'safecss_filter_attr_allow_css', 'FrmAppHelper::allow_style', 10, 2 );
1448 add_filter( 'frm_striphtml_allowed_tags', 'FrmAppHelper::add_allowed_icon_tags' );
1449 $icon = self::kses( $icon, 'all' );
1450 remove_filter( 'safe_style_css', 'FrmAppHelper::allow_vars_in_styles' );
1451 remove_filter( 'safecss_filter_attr_allow_css', 'FrmAppHelper::allow_style' );
1452 remove_filter( 'frm_striphtml_allowed_tags', 'FrmAppHelper::add_allowed_icon_tags' );
1453 return $icon;
1454 }
1455
1456 /**
1457 * @since 5.0.13.1
1458 *
1459 * @param array $allowed_html
1460 *
1461 * @return array
1462 */
1463 public static function add_allowed_icon_tags( $allowed_html ) {
1464 $allowed_html['svg']['data-open'] = true;
1465 $allowed_html['svg']['title'] = true;
1466 $allowed_html['svg']['tabindex'] = true;
1467 return $allowed_html;
1468 }
1469
1470 /**
1471 * @since 5.0.13
1472 *
1473 * @param array $allowed_attr
1474 *
1475 * @return array
1476 */
1477 public static function allow_vars_in_styles( $allowed_attr ) {
1478 $allowed_attr[] = '--primary-700';
1479 return $allowed_attr;
1480 }
1481
1482 /**
1483 * @since 5.0.13
1484 *
1485 * @param bool $allow_css
1486 * @param string $css_string
1487 */
1488 public static function allow_style( $allow_css, $css_string ) {
1489 if ( ! $allow_css && str_starts_with( $css_string, '--primary-700:' ) ) {
1490 $split = explode( ':', $css_string, 2 );
1491 $allow_css = 2 === count( $split ) && self::is_a_valid_color( $split[1] );
1492 }
1493 return $allow_css;
1494 }
1495
1496 /**
1497 * @since 5.0.13
1498 *
1499 * @param string $value
1500 *
1501 * @return bool
1502 */
1503 private static function is_a_valid_color( $value ) {
1504 $match = 0;
1505
1506 if ( str_starts_with( $value, 'rgba(' ) ) {
1507 $match = preg_match( '/^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*(?:\.\d+)?)\)$/', $value );
1508 } elseif ( str_starts_with( $value, 'rgb(' ) ) {
1509 $match = preg_match( '/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/', $value );
1510 } elseif ( str_starts_with( $value, '#' ) ) {
1511 $match = preg_match( '/^#([a-f0-9]{6}|[a-f0-9]{3})\b$/', $value );
1512 }
1513
1514 return (bool) $match;
1515 }
1516
1517 /**
1518 * Include svg images.
1519 *
1520 * @since 4.0.02
1521 *
1522 * @return void
1523 */
1524 public static function include_svg() {
1525 if ( self::$included_svg ) {
1526 return;
1527 }
1528
1529 // Use readfile instead of include_once because of a default security rule in Snuffleupagus.
1530 readfile( self::plugin_path() . '/images/icons.svg' );
1531 self::$included_svg = true;
1532 }
1533
1534 /**
1535 * Convert an associative array to HTML values.
1536 *
1537 * @since 4.0.02
1538 * @since 5.0.13 added $echo parameter.
1539 *
1540 * @param array $atts
1541 * @param bool $echo
1542 *
1543 * @return string|void
1544 */
1545 public static function array_to_html_params( $atts, $echo = false ) {
1546 $callback = function () use ( $atts ) {
1547 if ( ! $atts ) {
1548 return;
1549 }
1550
1551 foreach ( $atts as $key => $value ) {
1552 echo ' ' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
1553 }
1554 };
1555 return self::clip( $callback, $echo );
1556 }
1557
1558 /**
1559 * Call an echo function and either echo it or return the result as a string.
1560 *
1561 * @since 5.0.13
1562 *
1563 * @param Closure $echo_function
1564 * @param bool $echo
1565 *
1566 * @return string|null
1567 *
1568 * @psalm-return ($echo is true ? null : string)
1569 */
1570 public static function clip( $echo_function, $echo = false ) {
1571 if ( ! $echo ) {
1572 ob_start();
1573 }
1574
1575 if ( is_callable( $echo_function ) ) {
1576 $echo_function();
1577 }
1578
1579 return $echo ? null : ob_get_clean();
1580 }
1581
1582 /**
1583 * @since 3.0
1584 *
1585 * @param array $atts
1586 *
1587 * @return void
1588 */
1589 public static function get_admin_header( $atts ) {
1590 $has_nav = ! empty( $atts['form'] ) && empty( $atts['is_template'] );
1591
1592 if ( empty( $atts['close'] ) ) {
1593 $atts['close'] = admin_url( 'admin.php?page=formidable' );
1594 }
1595
1596 if ( ! isset( $atts['import_link'] ) ) {
1597 $atts['import_link'] = false;
1598 }
1599
1600 include self::plugin_path() . '/classes/views/shared/admin-header.php';
1601 }
1602
1603 /**
1604 * @since 6.0
1605 *
1606 * @param string $type
1607 *
1608 * @return void
1609 */
1610 public static function import_link( $type = 'secondary' ) {
1611 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1612 ?>
1613 <a href="<?php echo esc_url( admin_url( 'admin.php?page=formidable-import' ) ); ?>" class="button frm-button-<?php echo esc_attr( $type ); ?> frm_animate_bg">
1614 <?php esc_html_e( 'Import', 'formidable' ); ?>
1615 </a>
1616 <?php
1617 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1618 }
1619
1620 /**
1621 * Print applicable admin banner.
1622 *
1623 * @since 5.4.2
1624 *
1625 * @param bool $should_show_lite_upgrade
1626 *
1627 * @return void
1628 */
1629 public static function print_admin_banner( $should_show_lite_upgrade ) {
1630 if ( ! current_user_can( 'administrator' ) ) {
1631 FrmInbox::maybe_show_banner();
1632 return;
1633 }
1634
1635 if ( FrmSalesApi::maybe_show_banner() || self::maybe_show_license_warning() || FrmInbox::maybe_show_banner() || ! $should_show_lite_upgrade || self::pro_is_installed() ) {
1636 // Print license warning or inbox banner and exit if either prints.
1637 // And exit before printing the upgrade bar if it shouldn't be shown.
1638 return;
1639 }
1640 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1641 ?>
1642 <div class="frm-upgrade-bar">
1643 <div class="frm-upgrade-bar-inner">
1644 <?php
1645 $cta_text = FrmSalesApi::get_best_sale_value( 'lite_banner_cta_text' );
1646
1647 if ( ! $cta_text ) {
1648 $cta_text = __( 'upgrading to PRO', 'formidable' );
1649 }
1650
1651 $upgrade_link = FrmSalesApi::get_best_sale_value( 'lite_banner_cta_link' );
1652 $utm = array(
1653 'campaign' => 'settings-license',
1654 'content' => 'lite-banner',
1655 );
1656
1657 $upgrade_link = $upgrade_link ? self::maybe_add_missing_utm( $upgrade_link, $utm ) : self::admin_upgrade_link( $utm );
1658
1659 printf(
1660 /* translators: %1$s: Start link HTML, %2$s: CTA text ("upgrading to PRO" by default), %3$s: End link HTML */
1661 esc_html__( 'You\'re using Formidable Forms Lite. To unlock more features consider %1$s%2$s%3$s.', 'formidable' ),
1662 '<a href="' . esc_url( $upgrade_link ) . '" target="_blank" rel="noopener">',
1663 esc_html( $cta_text ),
1664 '</a>'
1665 );
1666 ?>
1667 </div>
1668 </div>
1669 <?php
1670 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1671 }
1672
1673 /**
1674 * @since 5.4.2
1675 *
1676 * @return bool True if a banner is available and shown.
1677 */
1678 private static function maybe_show_license_warning() {
1679 return is_callable( 'FrmProAddonsController::admin_banner' ) && FrmProAddonsController::admin_banner();
1680 }
1681
1682 /**
1683 * Render a button for a new item (Form, Application, etc).
1684 *
1685 * @since 3.0
1686 *
1687 * @param array $atts {
1688 * Details about the button.
1689 *
1690 * @type array $link_hook Custom link hook, calls do_action and exits early.
1691 * @type string $new_link Href value, default #.
1692 * @type string $class Custom class names, space separated.
1693 * @type string $button_text Button text. Default "Add New".
1694 * }
1695 *
1696 * @return void
1697 */
1698 public static function add_new_item_link( $atts ) {
1699 if ( isset( $atts['link_hook'] ) ) {
1700 do_action( $atts['link_hook']['hook'], $atts['link_hook']['param'] );
1701 return;
1702 }
1703
1704 if ( empty( $atts['new_link'] ) && empty( $atts['create_form'] ) && empty( $atts['class'] ) ) {
1705 // Do not render a button if none of these attributes are set.
1706 return;
1707 }
1708
1709 $href = ! empty( $atts['new_link'] ) ? esc_url( $atts['new_link'] ) : '#';
1710 $class = 'button button-primary frm-button-primary';
1711
1712 if ( ! empty( $atts['class'] ) ) {
1713 $class .= ' ' . $atts['class'];
1714 }
1715
1716 $button_text = ! empty( $atts['button_text'] ) ? $atts['button_text'] : __( 'Add New', 'formidable' );
1717
1718 require self::plugin_path() . '/classes/views/shared/add-button.php';
1719 }
1720
1721 /**
1722 * @since 3.06
1723 *
1724 * @param array $atts
1725 *
1726 * @return void
1727 */
1728 public static function show_search_box( $atts ) {
1729 $defaults = array(
1730 'placeholder' => '',
1731 'tosearch' => '',
1732 'text' => __( 'Search', 'formidable' ),
1733 'input_id' => '',
1734 'value' => false,
1735 'class' => '',
1736 );
1737 $atts = array_merge( $defaults, $atts );
1738
1739 if ( $atts['input_id'] === 'template' && empty( $atts['tosearch'] ) ) {
1740 $atts['tosearch'] = 'frm-card';
1741 }
1742
1743 $class = 'frm-search-input';
1744
1745 if ( ! empty( $atts['tosearch'] ) ) {
1746 $class .= ' frm-auto-search';
1747 }
1748
1749 $input_id = $atts['input_id'] . '-search-input';
1750
1751 $input_atts = array(
1752 'type' => 'search',
1753 'id' => $input_id,
1754 'name' => 's',
1755 'placeholder' => $atts['placeholder'],
1756 'class' => $class,
1757 'data-tosearch' => $atts['tosearch'],
1758 );
1759
1760 if ( is_string( $atts['value'] ) ) {
1761 $input_atts['value'] = $atts['value'];
1762 } elseif ( isset( $_REQUEST['s'] ) ) {
1763 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1764 $input_atts['value'] = wp_unslash( $_REQUEST['s'] );
1765 }
1766
1767 if ( ! empty( $atts['tosearch'] ) ) {
1768 $input_atts['autocomplete'] = 'off';
1769 }
1770 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1771 ?>
1772 <p class="frm-search <?php echo esc_attr( $atts['class'] ); ?>">
1773 <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>">
1774 <?php echo esc_html( $atts['text'] ); ?>:
1775 </label>
1776 <?php self::icon_by_class( 'frmfont frm_search_icon frm_svg20' ); ?>
1777 <input <?php self::array_to_html_params( $input_atts, true ); ?> />
1778 <?php
1779 if ( empty( $atts['tosearch'] ) ) {
1780 submit_button( $atts['text'], 'button-secondary', '', false, array( 'id' => 'search-submit' ) );
1781 }
1782 ?>
1783 </p>
1784 <?php
1785 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1786 }
1787
1788 /**
1789 * @param string $type Hook type slug.
1790 * @param object|null $object Optional related object.
1791 *
1792 * @return void
1793 */
1794 public static function trigger_hook_load( $type, $object = null ) {
1795 // Only load the form hooks once.
1796 $hooks_loaded = apply_filters( 'frm_' . $type . '_hooks_loaded', false, $object );
1797
1798 if ( ! $hooks_loaded ) {
1799 do_action( 'frm_load_' . $type . '_hooks' );
1800 }
1801 }
1802
1803 /**
1804 * Save all front-end js scripts into a single file.
1805 * And save an additional single file of all front-end Stripe JS scripts.
1806 *
1807 * @since 3.0
1808 *
1809 * @return void
1810 */
1811 public static function save_combined_js() {
1812 $file_atts = apply_filters(
1813 'frm_js_location',
1814 array(
1815 'file_name' => 'frm.min.js',
1816 'new_file_path' => self::plugin_path() . '/js',
1817 )
1818 );
1819 $new_file = new FrmCreateFile( $file_atts );
1820
1821 $files = array(
1822 self::plugin_path() . '/js/formidable.min.js',
1823 );
1824 /**
1825 * @param array $files
1826 */
1827 $files = apply_filters( 'frm_combined_js_files', $files );
1828 $new_file->combine_files( $files );
1829
1830 // Create the minified Stripe Script.
1831 $file_atts = apply_filters(
1832 'frm_stripe_js_location',
1833 array(
1834 'file_name' => 'frmstrp.min.js',
1835 'new_file_path' => self::plugin_path() . '/js',
1836 )
1837 );
1838 $new_file = new FrmCreateFile( $file_atts );
1839 $files = array(
1840 FrmStrpLiteAppHelper::plugin_path() . 'js/frmstrp.min.js',
1841 );
1842
1843 /**
1844 * @since 6.5
1845 *
1846 * @param array $files
1847 */
1848 $files = apply_filters( 'frm_stripe_combined_js_files', $files );
1849 $new_file->combine_files( $files );
1850 }
1851
1852 /**
1853 * Check a value from a shortcode to see if true or false.
1854 * True when value is 1, true, 'true', 'yes'
1855 *
1856 * @since 1.07.10
1857 *
1858 * @param bool|int|string $value The value to compare.
1859 *
1860 * @return bool
1861 */
1862 public static function is_true( $value ) {
1863 return true === $value || '1' === (string) $value || 'true' === $value || 'yes' === $value;
1864 }
1865
1866 /**
1867 * Gets all post from a specific post type.
1868 * This gets the entire WP_Post object so it can require a lot of memory. When only id and title are needed, consider using FrmAppHelper::get_post_ids_and_titles instead.
1869 *
1870 * @since 4.10.01 Add `$post_type` argument.
1871 *
1872 * @param string $post_type Post type to query. Default is `page`.
1873 *
1874 * @return WP_Post[]
1875 */
1876 public static function get_pages( $post_type = 'page' ) {
1877 $query = array(
1878 'post_type' => $post_type,
1879 'post_status' => array( 'publish', 'private' ),
1880 'numberposts' => - 1,
1881 'orderby' => 'title',
1882 'order' => 'ASC',
1883 );
1884
1885 return get_posts( $query );
1886 }
1887
1888 /**
1889 * Gets post ids and titles for a specific post type.
1890 *
1891 * @since 5.0.09
1892 *
1893 * @param string $post_type Post type to query. Default is `page`.
1894 *
1895 * @return array
1896 */
1897 public static function get_post_ids_and_titles( $post_type = 'page' ) {
1898 return FrmDb::get_results(
1899 'posts',
1900 array(
1901 'post_type' => $post_type,
1902 'post_status' => array( 'publish', 'private' ),
1903 ),
1904 'ID, post_title',
1905 array(
1906 'order_by' => 'post_title ASC',
1907 )
1908 );
1909 }
1910
1911 /**
1912 * Renders an autocomplete page selection or a regular dropdown depending on
1913 * the total page count
1914 *
1915 * @since 4.03.06
1916 * @since 4.10.01 Added `post_type` and `autocomplete_placeholder` to the arguments array.
1917 *
1918 * @param array $args Selection arguments.
1919 */
1920 public static function maybe_autocomplete_pages_options( $args ) {
1921 $args = self::preformat_selection_args( $args );
1922 $pages_count = wp_count_posts( $args['post_type'] );
1923
1924 if ( ! isset( $pages_count->publish ) || $pages_count->publish <= 50 ) {
1925 self::wp_pages_dropdown( $args );
1926 return;
1927 }
1928
1929 wp_enqueue_script( 'jquery-ui-autocomplete' );
1930
1931 $selected = self::get_post_param( $args['field_name'], $args['page_id'], 'absint' );
1932 $title = '';
1933
1934 if ( $selected ) {
1935 $title = get_the_title( $selected );
1936 }
1937
1938 ?>
1939 <input type="text" class="frm-page-search"
1940 data-post-type="<?php echo esc_attr( $args['post_type'] ); ?>"
1941 placeholder="<?php echo esc_attr( $args['autocomplete_placeholder'] ); ?>"
1942 value="<?php echo esc_attr( $title ); ?>" />
1943 <input type="hidden" name="<?php echo esc_attr( $args['field_name'] ); ?>"
1944 class="frm_autocomplete_value_input"
1945 value="<?php echo esc_attr( $selected ); ?>" />
1946 <?php
1947 }
1948
1949 /**
1950 * Maybe show an HTML select or autocomplete input based on the number of options.
1951 *
1952 * @since 6.21
1953 *
1954 * @param array $args Args. See the method for details.
1955 */
1956 public static function maybe_autocomplete_options( $args ) {
1957 $defaults = array(
1958 'truncate' => false,
1959 'placeholder' => ' ',
1960 'name' => '',
1961 'id' => '',
1962 'selected' => '',
1963 'source' => array(),
1964 'dropdown_limit' => 50,
1965 'autocomplete_placeholder' => __( 'Select an option', 'formidable' ),
1966 'value_key' => 'value',
1967 'label_key' => 'label',
1968 );
1969
1970 $args = wp_parse_args( $args, $defaults );
1971 $html_attrs = array();
1972
1973 if ( ! empty( $args['name'] ) ) {
1974 $html_attrs['name'] = $args['name'];
1975 }
1976
1977 if ( ! empty( $args['id'] ) ) {
1978 $html_attrs['id'] = $args['id'];
1979 }
1980
1981 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1982 if ( count( $args['source'] ) <= $args['dropdown_limit'] ) {
1983 ?>
1984 <select <?php self::array_to_html_params( $html_attrs, true ); ?>>
1985 <option value=""><?php echo esc_html( $args['placeholder'] ); ?></option>
1986 <?php
1987 foreach ( $args['source'] as $key => $source ) :
1988 $value_label = self::get_dropdown_value_and_label_from_option( $source, $key, $args );
1989
1990 if ( ! empty( $args['truncate'] ) ) {
1991 $value_label['label'] = self::truncate( $value_label['label'], $args['truncate'] );
1992 }
1993 ?>
1994 <option value="<?php echo esc_attr( $value_label['value'] ); ?>" <?php selected( $value_label['value'], $args['selected'] ); ?>><?php echo esc_html( $value_label['label'] ); ?></option><?php // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong ?>
1995 <?php endforeach; ?>
1996 </select>
1997 <?php
1998 return;
1999 }
2000 // phpcs:enable Generic.WhiteSpace.ScopeIndent
2001
2002 $options = array();
2003 $autocomplete_value = '';
2004
2005 foreach ( $args['source'] as $key => $source ) {
2006 $value_label = self::get_dropdown_value_and_label_from_option( $source, $key, $args );
2007
2008 if ( $value_label['value'] === $args['selected'] ) {
2009 $autocomplete_value = $value_label['label'];
2010 }
2011
2012 $options[] = $value_label;
2013 }
2014
2015 $html_attrs['type'] = 'hidden';
2016 $html_attrs['class'] = 'frm_autocomplete_value_input';
2017 $html_attrs['value'] = $args['selected'];
2018 ?>
2019 <input type="text" class="frm-custom-search"
2020 data-source="<?php echo esc_attr( wp_json_encode( $options ) ); ?>"
2021 placeholder="<?php echo esc_attr( $args['autocomplete_placeholder'] ); ?>"
2022 value="<?php echo esc_attr( $autocomplete_value ); ?>" />
2023 <input <?php self::array_to_html_params( $html_attrs, true ); ?> />
2024 <?php
2025 }
2026
2027 /**
2028 * Gets dropdown value and label from autodropdown option.
2029 *
2030 * @since 6.21
2031 *
2032 * @param array|string $option Autocomplete option.
2033 * @param string $key Array key of the option.
2034 * @param array $args See {@see FrmAppHelper::maybe_autocomplete_options()}.
2035 *
2036 * @return array
2037 */
2038 private static function get_dropdown_value_and_label_from_option( $option, $key, $args ) {
2039 if ( is_array( $option ) ) {
2040 $value = $option[ $args['value_key'] ] ?? '';
2041 $label = $option[ $args['label_key'] ] ?? '';
2042 } else {
2043 $value = $key;
2044 $label = $option;
2045 }
2046
2047 return compact( 'value', 'label' );
2048 }
2049
2050 /**
2051 * @param array $args
2052 * @param string $page_id Deprecated.
2053 * @param bool $truncate Deprecated.
2054 */
2055 public static function wp_pages_dropdown( $args = array(), $page_id = '', $truncate = false ) {
2056 self::prep_page_dropdown_params( $page_id, $truncate, $args );
2057
2058 $pages = self::get_post_ids_and_titles( $args['post_type'] );
2059 $selected = self::get_post_param( $args['field_name'], $args['page_id'], 'absint' );
2060 // phpcs:disable Generic.WhiteSpace.ScopeIndent
2061 ?>
2062 <select name="<?php echo esc_attr( $args['field_name'] ); ?>" id="<?php echo esc_attr( $args['field_name'] ); ?>" class="frm-pages-dropdown">
2063 <option value=""><?php echo esc_html( $args['placeholder'] ); ?></option>
2064 <?php foreach ( $pages as $page ) { ?>
2065 <option value="<?php echo esc_attr( $page->ID ); ?>" <?php selected( $selected, $page->ID ); ?>>
2066 <?php echo esc_html( $args['truncate'] ? self::truncate( $page->post_title, $args['truncate'] ) : $page->post_title ); ?>
2067 </option>
2068 <?php } ?>
2069 </select>
2070 <?php
2071 // phpcs:enable Generic.WhiteSpace.ScopeIndent
2072 }
2073
2074 /**
2075 * Fill in missing parameters passed to wp_pages_dropdown().
2076 * This is for reverse compatibility with switching 3 params to 1.
2077 *
2078 * @since 4.03.06
2079 *
2080 * @param string $page_id Deprecated.
2081 * @param bool $truncate Deprecated.
2082 * @param mixed $args
2083 *
2084 * @return void
2085 */
2086 private static function prep_page_dropdown_params( $page_id, $truncate, &$args ) {
2087 if ( ! is_array( $args ) ) {
2088 $args = array(
2089 'field_name' => $args,
2090 'page_id' => $page_id,
2091 'truncate' => $truncate,
2092 );
2093 }
2094
2095 $args = self::preformat_selection_args( $args );
2096 }
2097
2098 /**
2099 * Filter to format args for page dropdown or autocomplete
2100 *
2101 * @since 4.03.06
2102 * @since 4.10.01 Added `post_type` and `autocomplete_placeholder` to the arguments array.
2103 *
2104 * @param array $args
2105 *
2106 * @return array
2107 */
2108 private static function preformat_selection_args( $args ) {
2109 $defaults = array(
2110 'truncate' => false,
2111 'placeholder' => ' ',
2112 'field_name' => '',
2113 'page_id' => '',
2114 'post_type' => 'page',
2115 'autocomplete_placeholder' => __( 'Select a Page', 'formidable' ),
2116 );
2117
2118 return array_merge( $defaults, $args );
2119 }
2120
2121 /**
2122 * @param int $post_id
2123 *
2124 * @return string
2125 */
2126 public static function post_edit_link( $post_id ) {
2127 $post = get_post( $post_id );
2128
2129 if ( $post ) {
2130 $post_url = admin_url( 'post.php?post=' . $post_id . '&action=edit' );
2131 return '<a href="' . esc_url( $post_url ) . '">' . self::truncate( $post->post_title, 50 ) . '</a>';
2132 }
2133
2134 return '';
2135 }
2136
2137 /**
2138 * Hide the WordPress menus on some pages.
2139 *
2140 * @since 4.0
2141 *
2142 * @return bool
2143 */
2144 public static function is_full_screen() {
2145 return self::is_form_builder_page() || self::is_style_editor_page() || self::is_full_screen_view_builder_page();
2146 }
2147
2148 /**
2149 * Check if user is on the style editor or its alternative URL.
2150 * The first URL is a submenu "Styles" in the Formidable menu /wp-admin/admin.php?page=formidable-styles.
2151 * The alternative URL is linked as a submenu "Forms" item of the Appearance menu /wp-admin/themes.php?page=formidable-styles2.
2152 *
2153 * @since 5.5.3
2154 * @since 6.0 Added the $view parameter. Previously there was only a 'edit' view.
2155 *
2156 * @param string $view Supports 'edit', 'list', and ''. If '', both 'edit' and 'list' will match.
2157 *
2158 * @return bool
2159 */
2160 public static function is_style_editor_page( $view = '' ) {
2161 if ( ! self::is_admin_page( 'formidable-styles' ) && ! self::is_admin_page( 'formidable-styles2' ) ) {
2162 return false;
2163 }
2164
2165 if ( ! in_array( $view, array( 'list', 'edit' ), true ) ) {
2166 return true;
2167 }
2168
2169 $action = self::simple_get( 'frm_action' );
2170 $is_edit_mode = 'edit' === $action || ( ! $action && ! self::simple_get( 'id' ) && ! self::simple_get( 'form' ) );
2171
2172 if ( ! $is_edit_mode && class_exists( 'FrmProStylesController' ) && in_array( $action, array( 'new_style', 'duplicate' ), true ) ) {
2173 $is_edit_mode = true;
2174 }
2175
2176 $checking_for_edit_mode = 'edit' === $view;
2177
2178 return $is_edit_mode === $checking_for_edit_mode;
2179 }
2180
2181 /**
2182 * @since 5.5.3
2183 *
2184 * @return bool
2185 */
2186 private static function is_full_screen_view_builder_page() {
2187 return self::is_admin_page( 'formidable-views-editor' );
2188 }
2189
2190 /**
2191 * @param string $field_name
2192 * @param array|string $capability
2193 * @param string $multiple 'single' and 'multiple'.
2194 */
2195 public static function wp_roles_dropdown( $field_name, $capability, $multiple = 'single' ) {
2196 // phpcs:disable Generic.WhiteSpace.ScopeIndent
2197 ?>
2198 <select name="<?php echo esc_attr( $field_name ); ?>" id="<?php echo esc_attr( $field_name ); ?>"
2199 <?php echo 'multiple' === $multiple ? 'multiple="multiple"' : ''; ?>
2200 class="frm_multiselect">
2201 <?php self::roles_options( $capability ); ?>
2202 </select>
2203 <?php
2204 // phpcs:enable Generic.WhiteSpace.ScopeIndent
2205 }
2206
2207 /**
2208 * @since 4.07
2209 *
2210 * @param array|string $selected
2211 * @param string $current
2212 */
2213 private static function selected( $selected, $current ) {
2214 if ( is_callable( 'FrmProAppHelper::selected' ) ) {
2215 FrmProAppHelper::selected( $selected, $current );
2216 } else {
2217 selected( in_array( $current, (array) $selected, true ) );
2218 }
2219 }
2220
2221 /**
2222 * @param array|string $capability
2223 */
2224 public static function roles_options( $capability ) {
2225 global $frm_vars;
2226
2227 if ( isset( $frm_vars['editable_roles'] ) ) {
2228 $editable_roles = $frm_vars['editable_roles'];
2229 } else {
2230 $editable_roles = get_editable_roles();
2231 $frm_vars['editable_roles'] = $editable_roles;
2232 }
2233
2234 foreach ( $editable_roles as $role => $details ) {
2235 $name = translate_user_role( $details['name'] );
2236 ?>
2237 <option value="<?php echo esc_attr( $role ); ?>" <?php self::selected( $capability, $role ); ?>><?php echo esc_html( $name ); ?></option>
2238 <?php
2239 unset( $role, $details );
2240 }
2241 }
2242
2243 /**
2244 * Gets the list of capabilities.
2245 *
2246 * @since 5.0 Parameter `$type` supports `pro_only` value.
2247 *
2248 * @param string $type Supports `auto`, `pro`, or `pro_only`.
2249 *
2250 * @return array
2251 */
2252 public static function frm_capabilities( $type = 'auto' ) {
2253 if ( ! self::pro_is_installed() && ! in_array( $type, array( 'pro', 'pro_only' ), true ) ) {
2254 return self::get_lite_capabilities();
2255 }
2256
2257 $pro_cap = array(
2258 'frm_create_entries' => __( 'Add Entries from Admin Area', 'formidable' ),
2259 'frm_edit_entries' => __( 'Edit Entries from Admin Area', 'formidable' ),
2260 'frm_view_reports' => __( 'View Reports', 'formidable' ),
2261 );
2262 /**
2263 * @since 5.3.1
2264 *
2265 * @param array<string,string> $pro_cap
2266 */
2267 $pro_cap = apply_filters( 'frm_pro_capabilities', $pro_cap );
2268
2269 if ( 'pro_only' === $type ) {
2270 return $pro_cap;
2271 }
2272
2273 return self::get_lite_capabilities() + $pro_cap;
2274 }
2275
2276 /**
2277 * Get the list of lite plugin capabilities.
2278 *
2279 * @since 5.3.1
2280 *
2281 * @return array<string,string>
2282 */
2283 private static function get_lite_capabilities() {
2284 return array(
2285 'frm_view_forms' => __( 'View Forms List', 'formidable' ),
2286 'frm_edit_forms' => __( 'Add and Edit Forms', 'formidable' ),
2287 'frm_delete_forms' => __( 'Delete Forms', 'formidable' ),
2288 'frm_change_settings' => __( 'Access this Settings Page', 'formidable' ),
2289 'frm_view_entries' => __( 'View Entries from Admin Area', 'formidable' ),
2290 'frm_delete_entries' => __( 'Delete Entries from Admin Area', 'formidable' ),
2291 );
2292 }
2293
2294 /**
2295 * Call the WordPress current_user_can but also validate empty strings as true for any logged in user
2296 *
2297 * @since 4.06.03
2298 *
2299 * @param string $role
2300 *
2301 * @return bool
2302 */
2303 public static function current_user_can( $role ) {
2304 if ( $role === '-1' ) {
2305 return false;
2306 }
2307
2308 if ( $role === 'loggedout' ) {
2309 return ! is_user_logged_in();
2310 }
2311
2312 if ( $role === 'loggedin' || ! $role ) {
2313 return is_user_logged_in();
2314 }
2315
2316 if ( (int) $role === 1 ) {
2317 $role = 'administrator';
2318 }
2319
2320 return is_user_logged_in() ? current_user_can( $role ) : false;
2321 }
2322
2323 /**
2324 * @param array|string $needed_role
2325 *
2326 * @return bool
2327 */
2328 public static function user_has_permission( $needed_role ) {
2329 if ( is_array( $needed_role ) ) {
2330 foreach ( $needed_role as $role ) {
2331 if ( self::current_user_can( $role ) ) {
2332 return true;
2333 }
2334 }
2335
2336 return false;
2337 }
2338
2339 $can = self::current_user_can( $needed_role );
2340
2341 if ( $can || in_array( $needed_role, array( '-1', 'loggedout' ), true ) ) {
2342 return $can;
2343 }
2344
2345 $roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
2346
2347 foreach ( $roles as $role ) {
2348 if ( current_user_can( $role ) ) {
2349 return true;
2350 }
2351
2352 if ( $role === $needed_role ) {
2353 break;
2354 }
2355 }
2356
2357 return false;
2358 }
2359
2360 /**
2361 * Make sure administrators can see Formidable menu
2362 *
2363 * @since 2.0
2364 */
2365 public static function maybe_add_permissions() {
2366 self::force_capability( 'frm_view_entries' );
2367
2368 if ( ! current_user_can( 'administrator' ) || current_user_can( 'frm_view_forms' ) ) {
2369 return;
2370 }
2371
2372 $user = new WP_User( get_current_user_id() );
2373 $frm_roles = self::frm_capabilities();
2374
2375 foreach ( $frm_roles as $frm_role => $frm_role_description ) {
2376 $user->add_cap( $frm_role );
2377 unset( $frm_role, $frm_role_description );
2378 }
2379 }
2380
2381 /**
2382 * Make sure admins have permission to see the menu items
2383 *
2384 * @since 2.0.6
2385 *
2386 * @param string $cap
2387 *
2388 * @return void
2389 */
2390 public static function force_capability( $cap = 'frm_change_settings' ) {
2391 if ( ! current_user_can( 'administrator' ) || current_user_can( $cap ) ) {
2392 return;
2393 }
2394
2395 $role = get_role( 'administrator' );
2396 $frm_roles = self::frm_capabilities();
2397
2398 foreach ( $frm_roles as $frm_role => $frm_role_description ) {
2399 $role->add_cap( $frm_role );
2400 }
2401 }
2402
2403 /**
2404 * Check if the user has permission for action.
2405 * Return permission message and stop the action if no permission
2406 *
2407 * @since 2.0
2408 *
2409 * @param string $permission
2410 * @param string $show_message
2411 */
2412 public static function permission_check( $permission, $show_message = 'show' ) {
2413 $permission_error = self::permission_nonce_error( $permission );
2414
2415 if ( $permission_error === false ) {
2416 return;
2417 }
2418
2419 if ( 'hide' === $show_message ) {
2420 $permission_error = '';
2421 }
2422
2423 wp_die( esc_html( $permission_error ) );
2424 }
2425
2426 /**
2427 * Check user permission and nonce
2428 *
2429 * @since 2.0
2430 *
2431 * @param string $permission
2432 * @param string $nonce_name
2433 * @param string $nonce
2434 *
2435 * @return false|string The permission message or false if allowed
2436 */
2437 public static function permission_nonce_error( $permission, $nonce_name = '', $nonce = '' ) {
2438 if ( $permission && ! current_user_can( $permission ) && ! current_user_can( 'administrator' ) ) {
2439 $frm_settings = self::get_settings();
2440 return $frm_settings->admin_permission;
2441 }
2442
2443 $error = false;
2444
2445 if ( ! $nonce_name ) {
2446 return $error;
2447 }
2448
2449 $nonce_value = $_REQUEST && isset( $_REQUEST[ $nonce_name ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_name ] ) ) : '';
2450
2451 if ( $_REQUEST && ( ! isset( $_REQUEST[ $nonce_name ] ) || ! wp_verify_nonce( $nonce_value, $nonce ) ) ) {
2452 $frm_settings = self::get_settings();
2453 $error = $frm_settings->admin_permission;
2454 }
2455
2456 return $error;
2457 }
2458
2459 /**
2460 * @param array|string $values
2461 * @param string $current
2462 *
2463 * @return void
2464 */
2465 public static function checked( $values, $current ) {
2466 if ( self::check_selected( $values, $current ) ) {
2467 echo ' checked="checked"';
2468 }
2469 }
2470
2471 /**
2472 * @param array|string $values
2473 * @param string|null $current
2474 *
2475 * @return bool
2476 */
2477 public static function check_selected( $values, $current ) {
2478 $values = self::recursive_function_map( $values, 'trim' );
2479 $values = self::recursive_function_map( $values, 'htmlspecialchars_decode' );
2480 $current = is_null( $current ) ? '' : htmlspecialchars_decode( trim( $current ) );
2481
2482 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict, Universal.Operators.StrictComparisons
2483 return is_array( $values ) ? in_array( $current, $values ) : $values == $current;
2484 }
2485
2486 /**
2487 * @param array|string $value
2488 * @param callable|string $function
2489 *
2490 * @return array|string
2491 */
2492 public static function recursive_function_map( $value, $function ) {
2493 if ( is_array( $value ) ) {
2494 $original_function = $function;
2495 $function = count( $value ) ? explode( ', ', FrmDb::prepare_array_values( $value, $function ) ) : array( $function );
2496
2497 if ( self::is_assoc( $value ) ) {
2498 foreach ( $value as $k => $v ) {
2499 if ( ! is_array( $v ) ) {
2500 $value[ $k ] = call_user_func( $original_function, $v );
2501 }
2502 }
2503 } else {
2504 $value = array_map( array( 'FrmAppHelper', 'recursive_function_map' ), $value, $function );
2505 }
2506 } else {
2507 $value = self::maybe_update_value_if_null( $value, $function );
2508 $value = call_user_func( $function, $value );
2509 }//end if
2510
2511 return $value;
2512 }
2513
2514 /**
2515 * Updates value to empty string if it is null and being passed to a string function.
2516 *
2517 * @since 6.8.4
2518 *
2519 * @param mixed $value
2520 * @param string $function
2521 *
2522 * @return mixed
2523 */
2524 private static function maybe_update_value_if_null( $value, $function ) {
2525 if ( null === $value && in_array( $function, array( 'trim', 'strlen' ), true ) ) {
2526 return '';
2527 }
2528
2529 return $value;
2530 }
2531
2532 /**
2533 * @param array $array
2534 *
2535 * @return bool
2536 */
2537 public static function is_assoc( $array ) {
2538 return (bool) count( array_filter( array_keys( $array ), 'is_string' ) );
2539 }
2540
2541 /**
2542 * Flatten a multi-dimensional array
2543 *
2544 * @param array $array
2545 * @param string $keys
2546 *
2547 * @return array
2548 */
2549 public static function array_flatten( $array, $keys = 'keep' ) {
2550 $return = array();
2551
2552 foreach ( $array as $key => $value ) {
2553 if ( is_array( $value ) ) {
2554 $return = array_merge( $return, self::array_flatten( $value, $keys ) );
2555 } elseif ( $keys === 'keep' ) {
2556 $return[ $key ] = $value;
2557 } else {
2558 $return[] = $value;
2559 }
2560 }
2561
2562 return $return;
2563 }
2564
2565 /**
2566 * Flatten an array before imploding it to avoid Array to string conversion warnings.
2567 *
2568 * @since 6.16.1
2569 *
2570 * @param string $sep
2571 * @param array $array
2572 *
2573 * @return string
2574 */
2575 public static function safe_implode( $sep, $array ) {
2576 $array = self::array_flatten( $array );
2577 return implode( $sep, $array );
2578 }
2579
2580 /**
2581 * @param string $text
2582 * @param bool $is_rich_text
2583 *
2584 * @return string
2585 */
2586 public static function esc_textarea( $text, $is_rich_text = false ) {
2587 $safe_text = str_replace( '&quot;', '"', $text );
2588
2589 if ( ! $is_rich_text ) {
2590 $safe_text = htmlspecialchars( $safe_text, ENT_NOQUOTES );
2591 }
2592
2593 $safe_text = str_replace( '&amp; ', '& ', $safe_text );
2594
2595 /**
2596 * @param string $safe_text
2597 * @param string $text
2598 */
2599 return (string) apply_filters( 'esc_textarea', $safe_text, $text );
2600 }
2601
2602 /**
2603 * Add auto paragraphs to text areas
2604 *
2605 * @since 2.0
2606 *
2607 * @param mixed $content
2608 *
2609 * @return mixed
2610 */
2611 public static function use_wpautop( $content ) {
2612 if ( apply_filters( 'frm_use_wpautop', true ) && is_string( $content ) ) {
2613 return wpautop( str_replace( '<br>', '<br />', $content ) );
2614 }
2615
2616 return $content;
2617 }
2618
2619 /**
2620 * Replace quotes with their HTML entities.
2621 *
2622 * @param string $val
2623 *
2624 * @return string
2625 */
2626 public static function replace_quotes( $val ) {
2627 // Replace double quotes.
2628 $val = str_replace( array( '&#8220;', '&#8221;', '&#8243;' ), '"', $val );
2629
2630 // Replace single quotes.
2631 return str_replace( array( '&#8216;', '&#8217;', '&#8242;', '&prime;', '&rsquo;', '&lsquo;' ), "'", $val );
2632 }
2633
2634 /**
2635 * @param string $handle
2636 * @param int|string $default
2637 *
2638 * @return int|string
2639 */
2640 public static function script_version( $handle, $default = 0 ) {
2641 global $wp_scripts;
2642
2643 if ( ! $wp_scripts ) {
2644 return $default;
2645 }
2646
2647 $ver = $default;
2648
2649 if ( ! isset( $wp_scripts->registered[ $handle ] ) ) {
2650 return $ver;
2651 }
2652
2653 $query = $wp_scripts->registered[ $handle ];
2654
2655 if ( is_object( $query ) && ! empty( $query->ver ) ) {
2656 return $query->ver;
2657 }
2658
2659 return $ver;
2660 }
2661
2662 /**
2663 * @since 5.0.13 added $echo param.
2664 *
2665 * @param string $url
2666 * @param bool $echo
2667 *
2668 * @return string|null
2669 */
2670 public static function js_redirect( $url, $echo = false ) {
2671 $callback = function () use ( $url ) {
2672 echo '<script type="text/javascript">window.location="' . esc_url_raw( $url ) . '"</script>';
2673 };
2674 return self::clip( $callback, $echo );
2675 }
2676
2677 /**
2678 * @param int|string $user_id
2679 *
2680 * @return int|string
2681 */
2682 public static function get_user_id_param( $user_id ) {
2683 if ( ! $user_id || is_numeric( $user_id ) ) {
2684 return $user_id;
2685 }
2686
2687 $user_id = sanitize_text_field( $user_id );
2688
2689 if ( $user_id === 'current' ) {
2690 $user_id = get_current_user_id();
2691 } else {
2692 $user = is_email( $user_id ) ? get_user_by( 'email', $user_id ) : get_user_by( 'login', $user_id );
2693
2694 if ( $user ) {
2695 $user_id = $user->ID;
2696 }
2697 unset( $user );
2698 }
2699
2700 return $user_id;
2701 }
2702
2703 /**
2704 * @param string $filename
2705 * @param array $atts
2706 *
2707 * @return false|string
2708 */
2709 public static function get_file_contents( $filename, $atts = array() ) {
2710 if ( ! is_file( $filename ) ) {
2711 return false;
2712 }
2713
2714 extract( $atts ); // phpcs:ignore WordPress.PHP.DontExtract
2715 ob_start();
2716 include $filename;
2717 return ob_get_clean();
2718 }
2719
2720 /**
2721 * @param string $name
2722 * @param string $table_name
2723 * @param string $column
2724 * @param int $id
2725 * @param int $num_chars
2726 */
2727 public static function get_unique_key( $name, $table_name, $column, $id = 0, $num_chars = 5 ) {
2728 $key = '';
2729
2730 if ( $name ) {
2731 $key = sanitize_key( $name );
2732 $key = self::maybe_clear_long_key( $key, $column );
2733 }
2734
2735 if ( ! $key ) {
2736 $key = self::generate_new_key( $num_chars );
2737 }
2738
2739 $key = self::prevent_numeric_and_reserved_keys( $key );
2740
2741 $similar_keys = FrmDb::get_col(
2742 $table_name,
2743 array(
2744 $column . ' like%' => $key,
2745 'ID !' => $id,
2746 ),
2747 $column
2748 );
2749
2750 // Create a unique field id if it has already been used.
2751 if ( ! in_array( $key, $similar_keys, true ) ) {
2752 return $key;
2753 }
2754
2755 $key = self::maybe_truncate_key_before_appending( $column, $key );
2756
2757 /**
2758 * Allow for a custom separator between the attempted key and the generated suffix.
2759 *
2760 * @since 5.2.03
2761 *
2762 * @param string $separator. Default empty.
2763 * @param string $key the key without the added suffix.
2764 */
2765 $separator = apply_filters( 'frm_unique_' . $column . '_separator', '', $key );
2766
2767 $suffix = 2;
2768 do {
2769 $key_check = $key . $separator . $suffix;
2770 ++$suffix;
2771 } while ( in_array( $key_check, $similar_keys, true ) );
2772
2773 return $key_check;
2774 }
2775
2776 /**
2777 * Avoid trying to append to a really long key,
2778 * The database limit is 100 for form and field keys so we want to avoid getting too close.
2779 *
2780 * @param string $column
2781 * @param string $key
2782 *
2783 * @return string
2784 */
2785 private static function maybe_truncate_key_before_appending( $column, $key ) {
2786 if ( ! in_array( $column, array( 'form_key', 'field_key' ), true ) ) {
2787 return $key;
2788 }
2789
2790 $max_key_length_before_truncating = 60;
2791
2792 if ( strlen( $key ) > $max_key_length_before_truncating ) {
2793 $key = substr( $key, 0, $max_key_length_before_truncating );
2794
2795 if ( is_numeric( $key ) ) {
2796 $key .= 'a';
2797 }
2798 }
2799
2800 return $key;
2801 }
2802
2803 /**
2804 * Possibly reset a key to avoid conflicts with column size limits.
2805 *
2806 * @param string $key
2807 * @param string $column
2808 *
2809 * @return string either the original key value, or an empty string if the key was too long.
2810 */
2811 private static function maybe_clear_long_key( $key, $column ) {
2812 if ( 'field_key' === $column && strlen( $key ) >= 70 ) {
2813 return '';
2814 }
2815 return $key;
2816 }
2817
2818 /**
2819 * @since 6.21 This is changed from `private` to `public`.
2820 *
2821 * @param int $num_chars
2822 *
2823 * @return string
2824 */
2825 public static function generate_new_key( $num_chars ) {
2826 $max_slug_value = 36 ** $num_chars;
2827
2828 // We want to have at least 2 characters in the slug.
2829 $min_slug_value = 37;
2830 return base_convert( random_int( $min_slug_value, $max_slug_value ), 10, 36 );
2831 }
2832
2833 /**
2834 * @param string $key
2835 *
2836 * @return string
2837 */
2838 private static function prevent_numeric_and_reserved_keys( $key ) {
2839 if ( is_numeric( $key ) ) {
2840 $key .= 'a';
2841 } else {
2842 $not_allowed = array(
2843 'id',
2844 'key',
2845 'created-at',
2846 'detaillink',
2847 'editlink',
2848 'siteurl',
2849 'evenodd',
2850 );
2851
2852 if ( in_array( $key, $not_allowed, true ) ) {
2853 $key .= 'a';
2854 }
2855 }
2856
2857 return $key;
2858 }
2859
2860 /**
2861 * Editing a Form or Entry
2862 *
2863 * @param object $record
2864 * @param string $table
2865 * @param array|string $fields
2866 * @param bool $default
2867 * @param array $post_values
2868 * @param array $args
2869 *
2870 * @return array|bool
2871 */
2872 public static function setup_edit_vars( $record, $table, $fields = '', $default = false, $post_values = array(), $args = array() ) {
2873 if ( ! $record ) {
2874 return false;
2875 }
2876
2877 if ( ! $post_values ) {
2878 $post_values = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
2879 }
2880
2881 $values = array(
2882 'id' => $record->id,
2883 'fields' => array(),
2884 );
2885
2886 foreach ( array( 'name', 'description' ) as $var ) {
2887 $default_val = $record->{$var} ?? '';
2888 $values[ $var ] = self::get_param( $var, $default_val, 'get', 'wp_kses_post' );
2889 unset( $var, $default_val );
2890 }
2891
2892 $values['description'] = self::use_wpautop( $values['description'] );
2893
2894 self::fill_form_opts( $record, $table, $post_values, $values );
2895
2896 self::prepare_field_arrays( $fields, $record, $values, array_merge( $args, compact( 'default', 'post_values' ) ) );
2897
2898 if ( $table === 'entries' ) {
2899 $values = FrmEntriesHelper::setup_edit_vars( $values, $record );
2900 } elseif ( $table === 'forms' ) {
2901 $values = FrmFormsHelper::setup_edit_vars( $values, $record, $post_values );
2902 }
2903
2904 return $values;
2905 }
2906
2907 /**
2908 * @param array|string $fields
2909 * @param object $record
2910 * @param array $values
2911 * @param array $args
2912 *
2913 * @return void
2914 */
2915 private static function prepare_field_arrays( $fields, $record, array &$values, $args ) {
2916 if ( ! $fields ) {
2917 return;
2918 }
2919
2920 foreach ( (array) $fields as $field ) {
2921 if ( ! self::is_admin_page() ) {
2922 // Don't prep default values on the form settings page.
2923 $field->default_value = apply_filters( 'frm_get_default_value', $field->default_value, $field, true );
2924 }
2925
2926 $args['parent_form_id'] = $args['parent_form_id'] ?? $field->form_id;
2927 self::fill_field_defaults( $field, $record, $values, $args );
2928 }
2929 }
2930
2931 /**
2932 * @param object $field
2933 * @param object $record
2934 * @param array $values
2935 * @param array $args
2936 *
2937 * @return void
2938 */
2939 private static function fill_field_defaults( $field, $record, array &$values, $args ) {
2940 $post_values = $args['post_values'];
2941
2942 if ( $args['default'] ) {
2943 $meta_value = $field->default_value;
2944 } elseif ( $record->post_id && self::pro_is_installed() && ! empty( $field->field_options['post_field'] ) ) {
2945 if ( ! isset( $field->field_options['custom_field'] ) ) {
2946 $field->field_options['custom_field'] = '';
2947 }
2948
2949 $meta_value = FrmProEntryMetaHelper::get_post_value(
2950 $record->post_id,
2951 $field->field_options['post_field'],
2952 $field->field_options['custom_field'],
2953 array(
2954 'truncate' => false,
2955 'type' => $field->type,
2956 'form_id' => $field->form_id,
2957 'field' => $field,
2958 )
2959 );
2960 } else {
2961 $meta_value = FrmEntryMeta::get_meta_value( $record, $field->id );
2962 }//end if
2963
2964 $field_type = $post_values['field_options'][ 'type_' . $field->id ] ?? $field->type;
2965
2966 if ( isset( $post_values['item_meta'][ $field->id ] ) ) {
2967 $new_value = $post_values['item_meta'][ $field->id ];
2968 self::unserialize_or_decode( $new_value );
2969 } else {
2970 $new_value = $meta_value;
2971 }
2972
2973 $field_array = self::start_field_array( $field );
2974 $field_array['value'] = $new_value;
2975 $field_array['type'] = apply_filters( 'frm_field_type', $field_type, $field, $new_value );
2976 $field_array['parent_form_id'] = $args['parent_form_id'];
2977
2978 $args['field_type'] = $field_type;
2979
2980 FrmFieldsHelper::prepare_edit_front_field( $field_array, $field, $values['id'], $args );
2981
2982 if ( empty( $field_array['unique'] ) ) {
2983 $field_array['unique_msg'] = '';
2984 }
2985
2986 $field_array = array_merge( (array) $field->field_options, $field_array );
2987
2988 $values['fields'][ $field->id ] = $field_array;
2989 }
2990
2991 /**
2992 * @since 3.0
2993 *
2994 * @param object $field
2995 *
2996 * @return array
2997 */
2998 public static function start_field_array( $field ) {
2999 return array(
3000 'id' => $field->id,
3001 'default_value' => $field->default_value,
3002 'name' => $field->name,
3003 'description' => $field->description,
3004 'options' => $field->options,
3005 'required' => $field->required,
3006 'field_key' => $field->field_key,
3007 'field_order' => $field->field_order,
3008 'form_id' => $field->form_id,
3009 );
3010 }
3011
3012 /**
3013 * @param object $record
3014 * @param string $table
3015 * @param array $post_values
3016 * @param array $values
3017 */
3018 private static function fill_form_opts( $record, $table, $post_values, array &$values ) {
3019 if ( $table === 'entries' ) {
3020 $form = $record->form_id;
3021 FrmForm::maybe_get_form( $form );
3022 } else {
3023 $form = $record;
3024 }
3025
3026 if ( ! $form ) {
3027 return;
3028 }
3029
3030 $values['form_name'] = isset( $record->form_id ) ? $form->name : '';
3031 $values['parent_form_id'] = isset( $record->form_id ) ? $form->parent_form_id : 0;
3032
3033 if ( ! is_array( $form->options ) ) {
3034 return;
3035 }
3036
3037 foreach ( $form->options as $opt => $value ) {
3038 if ( isset( $post_values[ $opt ] ) ) {
3039 $values[ $opt ] = $post_values[ $opt ];
3040 self::unserialize_or_decode( $values[ $opt ] );
3041 } else {
3042 $values[ $opt ] = $value;
3043 }
3044 }
3045
3046 self::fill_form_defaults( $post_values, $values );
3047 }
3048
3049 /**
3050 * Set to POST value or default
3051 *
3052 * @param array $post_values
3053 * @param array $values
3054 *
3055 * @return void
3056 */
3057 private static function fill_form_defaults( $post_values, array &$values ) {
3058 $form_defaults = FrmFormsHelper::get_default_opts();
3059
3060 foreach ( $form_defaults as $opt => $default ) {
3061 // phpcs:ignore Universal.Operators.StrictComparisons
3062 if ( ! isset( $values[ $opt ] ) || $values[ $opt ] == '' ) {
3063 $values[ $opt ] = $post_values['options'][ $opt ] ?? $default;
3064 }
3065
3066 unset( $opt, $default );
3067 }
3068
3069 if ( ! isset( $values['custom_style'] ) ) {
3070 $values['custom_style'] = self::custom_style_value( $post_values );
3071 }
3072
3073 foreach ( array( 'before', 'after', 'submit' ) as $h ) {
3074 if ( ! isset( $values[ $h . '_html' ] ) ) {
3075 $values[ $h . '_html' ] = $post_values['options'][ $h . '_html' ] ?? FrmFormsHelper::get_default_html( $h );
3076 }
3077 unset( $h );
3078 }
3079 }
3080
3081 /**
3082 * @since 2.2.10
3083 *
3084 * @param array $post_values
3085 *
3086 * @return bool|int
3087 */
3088 public static function custom_style_value( $post_values ) {
3089 if ( $post_values && isset( $post_values['options']['custom_style'] ) ) {
3090 return absint( $post_values['options']['custom_style'] );
3091 }
3092
3093 $frm_settings = self::get_settings();
3094 return $frm_settings->load_style !== 'none';
3095 }
3096
3097 /**
3098 * Truncate a string.
3099 *
3100 * Note: By default, this function will allow for a few additional characters more than $length.
3101 * If a string has no spaces, it allows up to 50 additional characters. To force a true length limit,
3102 * use $force_length_limit = true.
3103 *
3104 * @param mixed $original_string
3105 * @param int|string $length
3106 * @param int $minword
3107 * @param string $continue
3108 * @param bool $force_length_limit Force the final string to never exceed the length limit.
3109 *
3110 * @return string
3111 */
3112 public static function truncate( $original_string, $length, $minword = 3, $continue = '...', $force_length_limit = false ) {
3113 if ( ! is_string( $original_string ) && ! is_int( $original_string ) ) {
3114 return '';
3115 }
3116
3117 $length = (int) $length;
3118
3119 if ( $length === 0 ) {
3120 return '';
3121 }
3122
3123 $str = wp_strip_all_tags( (string) $original_string );
3124 $original_len = self::mb_function( array( 'mb_strlen', 'strlen' ), array( $str ) );
3125
3126 if ( $length <= 10 ) {
3127 $sub = self::mb_function( array( 'mb_substr', 'substr' ), array( $str, 0, $length ) );
3128
3129 if ( $force_length_limit ) {
3130 return $sub;
3131 }
3132
3133 return $sub . ( $length < $original_len ? $continue : '' );
3134 }
3135
3136 $sub = '';
3137 $len = 0;
3138 $words = self::mb_function( array( 'mb_split', 'explode' ), array( ' ', $str ) );
3139
3140 if ( ! is_array( $words ) ) {
3141 return $original_string;
3142 }
3143
3144 foreach ( $words as $word ) {
3145 $part = ( $sub !== '' ? ' ' : '' ) . $word;
3146 $total_len = self::mb_function( array( 'mb_strlen', 'strlen' ), array( $sub . $part ) );
3147
3148 if ( $total_len > $length && substr_count( $sub, ' ' ) ) {
3149 break;
3150 }
3151
3152 $sub .= $part;
3153 $len += self::mb_function( array( 'mb_strlen', 'strlen' ), array( $part ) );
3154
3155 if ( substr_count( $sub, ' ' ) > $minword && $total_len >= $length ) {
3156 break;
3157 }
3158
3159 unset( $total_len, $word );
3160 }
3161
3162 $sub = self::maybe_force_truncate_on_string_with_no_spaces( $sub, $length, $force_length_limit );
3163
3164 if ( $force_length_limit ) {
3165 // Ensure the final string doesn't exceed the length limit.
3166 $final_len = self::mb_function( array( 'mb_strlen', 'strlen' ), array( $sub ) );
3167
3168 if ( $final_len > $length ) {
3169 return self::mb_function( array( 'mb_substr', 'substr' ), array( $sub, 0, $length ) );
3170 }
3171
3172 return $sub;
3173 }
3174
3175 return $sub . ( $len < $original_len ? $continue : '' );
3176 }
3177
3178 /**
3179 * If the string is still too long because there may not have been any spaces, force truncate.
3180 *
3181 * @since 6.5.4
3182 *
3183 * @param string $sub Current substring.
3184 * @param int $length The length limit.
3185 * @param bool $force_length_limit Force the string to not exceed the length limit.
3186 *
3187 * @return string
3188 */
3189 private static function maybe_force_truncate_on_string_with_no_spaces( $sub, $length, $force_length_limit = false ) {
3190 if ( ! $force_length_limit ) {
3191 if ( strlen( $sub ) < $length + 50 ) {
3192 // If the string isn't way over the limit, leave it.
3193 return $sub;
3194 }
3195
3196 $first_space = strpos( $sub, ' ', $length );
3197
3198 if ( false !== $first_space ) {
3199 // Ignore anything with spaces.
3200 return $sub;
3201 }
3202
3203 return substr( $sub, 0, $length + 10 );
3204 }
3205
3206 // When force_length_limit is true, ensure the string doesn't exceed the length.
3207 $final_len = self::mb_function( array( 'mb_strlen', 'strlen' ), array( $sub ) );
3208
3209 if ( $final_len > $length ) {
3210 return self::mb_function( array( 'mb_substr', 'substr' ), array( $sub, 0, $length ) );
3211 }
3212
3213 return $sub;
3214 }
3215
3216 /**
3217 * @param array $function_names
3218 * @param array $args
3219 *
3220 * @return mixed
3221 */
3222 public static function mb_function( $function_names, $args ) {
3223 $mb_function_name = $function_names[0];
3224 $function_name = $function_names[1];
3225
3226 if ( function_exists( $mb_function_name ) ) {
3227 $function_name = $mb_function_name;
3228 }
3229
3230 return call_user_func_array( $function_name, $args );
3231 }
3232
3233 /**
3234 * @param string $date
3235 * @param string $date_format
3236 * @param string $time_format
3237 *
3238 * @return string
3239 */
3240 public static function get_formatted_time( $date, $date_format = '', $time_format = '' ) {
3241 if ( ! $date ) {
3242 return $date;
3243 }
3244
3245 if ( ! $date_format ) {
3246 $date_format = get_option( 'date_format' );
3247 }
3248
3249 if ( preg_match( '/^\d{1-2}\/\d{1-2}\/\d{4}$/', $date ) && self::pro_is_installed() ) {
3250 $frmpro_settings = new FrmProSettings();
3251 $date = FrmProAppHelper::convert_date( $date, $frmpro_settings->date_format, 'Y-m-d' );
3252 }
3253
3254 $formatted = self::get_localized_date( $date_format, $date );
3255 $do_time = gmdate( 'H:i:s', strtotime( $date ) ) !== '00:00:00';
3256
3257 if ( $do_time ) {
3258 $formatted .= self::add_time_to_date( $time_format, $date );
3259 }
3260
3261 return $formatted;
3262 }
3263
3264 /**
3265 * @param string $time_format
3266 * @param string $date
3267 *
3268 * @return string
3269 */
3270 private static function add_time_to_date( $time_format, $date ) {
3271 if ( ! $time_format ) {
3272 $time_format = get_option( 'time_format' );
3273 }
3274
3275 $trimmed_format = trim( $time_format );
3276
3277 if ( $time_format && $trimmed_format ) {
3278 return ' ' . __( 'at', 'formidable' ) . ' ' . self::get_localized_date( $time_format, $date );
3279 }
3280
3281 return '';
3282 }
3283
3284 /**
3285 * @since 2.0.8
3286 *
3287 * @param string $date_format
3288 * @param string $date
3289 *
3290 * @return string
3291 */
3292 public static function get_localized_date( $date_format, $date ) {
3293 $date = get_date_from_gmt( $date );
3294 return date_i18n( $date_format, strtotime( $date ) );
3295 }
3296
3297 /**
3298 * Gets the time ago in words.
3299 *
3300 * @param int $from In seconds.
3301 * @param int|string $to In seconds.
3302 * @param int|string $levels Number of time units to include or a specific unit.
3303 *
3304 * @return string Time ago.
3305 */
3306 public static function human_time_diff( $from, $to = '', $levels = 1 ) {
3307 $now = ! $to && 0 !== $to ? new DateTime() : new DateTime( '@' . $to );
3308 $ago = new DateTime( '@' . $from );
3309
3310 // Get the time difference
3311 $diff_object = $now->diff( $ago );
3312 $diff = get_object_vars( $diff_object );
3313
3314 // Add week amount and update day amount
3315 $diff['w'] = floor( $diff['d'] / 7 );
3316 $diff['d'] -= $diff['w'] * 7;
3317
3318 $time_strings = self::get_time_strings();
3319
3320 if ( ! is_numeric( $levels ) ) {
3321 // Show time in specified unit.
3322 $levels = self::get_unit( $levels );
3323
3324 if ( isset( $time_strings[ $levels ] ) ) {
3325 $diff = array(
3326 $levels => self::time_format( $levels, $diff ),
3327 );
3328 $time_strings = array(
3329 $levels => $time_strings[ $levels ],
3330 );
3331 }
3332
3333 $levels = 1;
3334 }
3335
3336 foreach ( $time_strings as $k => $v ) {
3337 if ( ! empty( $diff[ $k ] ) ) {
3338 $time_strings[ $k ] = $diff[ $k ] . ' ' . ( $diff[ $k ] > 1 ? $v[1] : $v[0] );
3339 } elseif ( isset( $diff[ $k ] ) && count( $time_strings ) === 1 ) {
3340 // Account for 0.
3341 $time_strings[ $k ] = $diff[ $k ] . ' ' . $v[1];
3342 } else {
3343 unset( $time_strings[ $k ] );
3344 }
3345 }
3346
3347 $levels_deep = apply_filters( 'frm_time_ago_levels', $levels, compact( 'time_strings', 'from', 'to' ) );
3348 $time_strings = array_slice( $time_strings, 0, absint( $levels_deep ) );
3349
3350 return implode( ' ', $time_strings );
3351 }
3352
3353 /**
3354 * @since 4.05.01
3355 *
3356 * @param string $unit
3357 * @param array $diff
3358 *
3359 * @return int
3360 */
3361 private static function time_format( $unit, $diff ) {
3362 $return = array(
3363 'y' => 'y',
3364 'd' => 'days',
3365 );
3366
3367 if ( isset( $return[ $unit ] ) ) {
3368 return $diff[ $return[ $unit ] ];
3369 }
3370
3371 $total = $diff['days'] * self::convert_time( 'd', $unit );
3372 $times = array( 'h', 'i', 's' );
3373
3374 foreach ( $times as $time ) {
3375 if ( ! isset( $diff[ $time ] ) ) {
3376 continue;
3377 }
3378
3379 $total += $diff[ $time ] * self::convert_time( $time, $unit );
3380 }
3381
3382 return floor( $total );
3383 }
3384
3385 /**
3386 * @since 4.05.01
3387 *
3388 * @param string $from
3389 * @param string $to
3390 *
3391 * @return int
3392 */
3393 private static function convert_time( $from, $to ) {
3394 $convert = array(
3395 's' => 1,
3396 'i' => MINUTE_IN_SECONDS,
3397 'h' => HOUR_IN_SECONDS,
3398 'd' => DAY_IN_SECONDS,
3399 'w' => WEEK_IN_SECONDS,
3400 'm' => DAY_IN_SECONDS * 30.42,
3401 'y' => DAY_IN_SECONDS * 365.25,
3402 );
3403
3404 return $convert[ $from ] / $convert[ $to ];
3405 }
3406
3407 /**
3408 * @since 4.05.01
3409 *
3410 * @param string $unit
3411 *
3412 * @return int|string
3413 */
3414 private static function get_unit( $unit ) {
3415 $units = self::get_time_strings();
3416
3417 if ( isset( $units[ $unit ] ) || is_numeric( $unit ) ) {
3418 return $unit;
3419 }
3420
3421 foreach ( $units as $u => $strings ) {
3422 if ( in_array( $unit, $strings, true ) ) {
3423 return $u;
3424 }
3425 }
3426
3427 return 1;
3428 }
3429
3430 /**
3431 * Get the translatable time strings. The untranslated version is a failsafe
3432 * in case languages are changing for the unit set in the shortcode.
3433 *
3434 * @since 2.0.20
3435 *
3436 * @return array
3437 */
3438 private static function get_time_strings() {
3439 return array(
3440 'y' => array(
3441 __( 'year', 'formidable' ),
3442 __( 'years', 'formidable' ),
3443 'year',
3444 ),
3445 'm' => array(
3446 __( 'month', 'formidable' ),
3447 __( 'months', 'formidable' ),
3448 'month',
3449 ),
3450 'w' => array(
3451 __( 'week', 'formidable' ),
3452 __( 'weeks', 'formidable' ),
3453 'week',
3454 ),
3455 'd' => array(
3456 __( 'day', 'formidable' ),
3457 __( 'days', 'formidable' ),
3458 'day',
3459 ),
3460 'h' => array(
3461 __( 'hour', 'formidable' ),
3462 __( 'hours', 'formidable' ),
3463 'hour',
3464 ),
3465 'i' => array(
3466 __( 'minute', 'formidable' ),
3467 __( 'minutes', 'formidable' ),
3468 'minute',
3469 ),
3470 's' => array(
3471 __( 'second', 'formidable' ),
3472 __( 'seconds', 'formidable' ),
3473 'second',
3474 ),
3475 );
3476 }
3477
3478 // Pagination Methods.
3479
3480 /**
3481 * @param int $r_count
3482 * @param int $current_p
3483 * @param int $p_size
3484 *
3485 * @return int
3486 */
3487 public static function get_last_record_num( $r_count, $current_p, $p_size ) {
3488 return $r_count < $current_p * $p_size ? $r_count : $current_p * $p_size;
3489 }
3490
3491 /**
3492 * @param int $r_count
3493 * @param int $current_p
3494 * @param int $p_size
3495 *
3496 * @return int
3497 */
3498 public static function get_first_record_num( $r_count, $current_p, $p_size ) {
3499 // phpcs:ignore Universal.Operators.StrictComparisons
3500 if ( $current_p == 1 ) {
3501 return 1;
3502 }
3503 return self::get_last_record_num( $r_count, $current_p - 1, $p_size ) + 1;
3504 }
3505
3506 /**
3507 * @param array $json_vars
3508 *
3509 * @return array
3510 */
3511 public static function json_to_array( $json_vars ) {
3512 $vars = array();
3513
3514 foreach ( $json_vars as $jv ) {
3515 $jv_name = explode( '[', $jv['name'] );
3516 $last = count( $jv_name ) - 1;
3517
3518 foreach ( $jv_name as $p => $n ) {
3519 $name = trim( $n, ']' );
3520
3521 if ( ! isset( $l1 ) ) {
3522 $l1 = $name;
3523 }
3524
3525 if ( ! isset( $l2 ) ) {
3526 $l2 = $name;
3527 }
3528
3529 if ( ! isset( $l3 ) ) {
3530 $l3 = $name;
3531 }
3532
3533 $this_val = $p === $last ? $jv['value'] : array();
3534
3535 switch ( $p ) {
3536 case 0:
3537 $l1 = $name;
3538 self::add_value_to_array( $name, $l1, $this_val, $vars );
3539 break;
3540
3541 case 1:
3542 $l2 = $name;
3543 self::add_value_to_array( $name, $l2, $this_val, $vars[ $l1 ] );
3544 break;
3545
3546 case 2:
3547 $l3 = $name;
3548 self::add_value_to_array( $name, $l3, $this_val, $vars[ $l1 ][ $l2 ] );
3549 break;
3550
3551 case 3:
3552 $l4 = $name;
3553 self::add_value_to_array( $name, $l4, $this_val, $vars[ $l1 ][ $l2 ][ $l3 ] );
3554 }
3555
3556 unset( $this_val, $n );
3557 }//end foreach
3558
3559 unset( $last, $jv );
3560 }//end foreach
3561
3562 return $vars;
3563 }
3564
3565 /**
3566 * @param string $name
3567 * @param string $l1
3568 * @param mixed $val
3569 * @param array $vars
3570 *
3571 * @return void
3572 */
3573 public static function add_value_to_array( $name, $l1, $val, &$vars ) {
3574 // phpcs:ignore Universal.Operators.StrictComparisons
3575 if ( $name == '' ) {
3576 $vars[] = $val;
3577 } elseif ( ! isset( $vars[ $l1 ] ) ) {
3578 $vars[ $l1 ] = $val;
3579 }
3580 }
3581
3582 /**
3583 * @param string $name
3584 * @param string $class
3585 * @param string $form_name
3586 *
3587 * @return void
3588 */
3589 public static function maybe_add_tooltip( $name, $class = 'closed', $form_name = '' ) {
3590 $tooltips = array(
3591 'action_title' => __( 'Give this action a label for easy reference.', 'formidable' ),
3592 'email_to' => __( 'Add one or more recipient addresses separated by a ",". FORMAT: Name <[email protected]> or [email protected]. [default-email] is the address set in the global "Default Email Address" settings.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3593 'cc' => __( 'Add CC addresses separated by a ",". FORMAT: Name <[email protected]> or [email protected].', 'formidable' ),
3594 'bcc' => __( 'Add BCC addresses separated by a ",". FORMAT: Name <[email protected]> or [email protected].', 'formidable' ),
3595 'reply_to' => __( 'If you would like a different reply to address than the "from" address, add a single address here. FORMAT: Name <[email protected]> or [email protected].', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3596 'from' => __( 'Enter the name and/or email address of the sender. FORMAT: John Bates <[email protected]> or [email protected].', 'formidable' ),
3597 /* translators: %1$s: Form name, %2$s: Date */
3598 'email_subject' => esc_attr( sprintf( __( 'If you leave the subject blank, the default will be used: %1$s Form submitted on %2$s', 'formidable' ), $form_name, self::site_name() ) ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3599 'new_tab' => __( 'This option will open the link in a new browser tab. Please note that some popup blockers may prevent this from happening, in which case the link will be displayed.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3600 );
3601
3602 if ( ! isset( $tooltips[ $name ] ) ) {
3603 return;
3604 }
3605
3606 if ( 'open' === $class ) {
3607 echo ' frm_help"';
3608 } else {
3609 echo ' class="frm_help"';
3610 }
3611
3612 echo ' title="' . esc_attr( $tooltips[ $name ] );
3613
3614 if ( 'open' !== $class ) {
3615 echo '"';
3616 }
3617 }
3618
3619 /**
3620 * Add the current_page class to that page in the form nav
3621 *
3622 * @param int|string $page
3623 * @param int|string $current_page
3624 * @param array $action
3625 *
3626 * @return void
3627 */
3628 public static function select_current_page( $page, $current_page, $action = array() ) {
3629 // phpcs:ignore Universal.Operators.StrictComparisons
3630 if ( $current_page != $page ) {
3631 return;
3632 }
3633
3634 $frm_action = self::simple_get( 'frm_action', 'sanitize_title' );
3635
3636 if ( 'lite-reports' === $frm_action ) {
3637 $frm_action = 'reports';
3638 }
3639
3640 if ( ! $action || ( $frm_action && in_array( $frm_action, $action, true ) ) ) {
3641 echo ' class="current_page"';
3642 }
3643 }
3644
3645 /**
3646 * Prepare and json_encode post content
3647 *
3648 * @since 2.0
3649 *
3650 * @param array $post_content
3651 *
3652 * @return string $post_content ( json encoded array )
3653 */
3654 public static function prepare_and_encode( $post_content ) {
3655 // Loop through array to strip slashes and add only the needed ones.
3656 foreach ( $post_content as $key => $val ) {
3657 // Replace problematic characters (like &quot;)
3658 if ( is_string( $val ) ) {
3659 $val = str_replace( '&quot;', '"', $val );
3660 }
3661
3662 self::prepare_action_slashes( $val, $key, $post_content );
3663 unset( $key, $val );
3664 }
3665
3666 // json_encode the array.
3667 $post_content = json_encode( $post_content );
3668
3669 // Add extra slashes for \r\n since WP strips them.
3670 $post_content = str_replace( array( '\\r', '\\n', '\\u', '\\t' ), array( '\\\\r', '\\\\n', '\\\\u', '\\\\t' ), $post_content );
3671
3672 // Allow for &quot
3673 return str_replace( '&quot;', '\\"', $post_content );
3674 }
3675
3676 /**
3677 * @param array|string $val
3678 * @param int|string $key
3679 * @param array $post_content
3680 *
3681 * @return void
3682 */
3683 private static function prepare_action_slashes( $val, $key, &$post_content ) {
3684 if ( ! isset( $post_content[ $key ] ) || is_numeric( $val ) ) {
3685 return;
3686 }
3687
3688 if ( is_array( $val ) ) {
3689 foreach ( $val as $k1 => $v1 ) {
3690 self::prepare_action_slashes( $v1, $k1, $post_content[ $key ] );
3691 unset( $k1, $v1 );
3692 }
3693
3694 return;
3695 }
3696
3697 // Strip all slashes so everything is the same, no matter where the value is coming from
3698 $val = stripslashes( $val );
3699
3700 // Add backslashes before double quotes and forward slashes only
3701 $post_content[ $key ] = addcslashes( $val, '"\\/' );
3702 }
3703
3704 /**
3705 * Check for either json or serialized data. This is temporary while transitioning
3706 * all data to json.
3707 *
3708 * @since 4.02.03
3709 *
3710 * @param array|string $value
3711 *
3712 * @return void
3713 */
3714 public static function unserialize_or_decode( &$value ) {
3715 if ( is_array( $value ) ) {
3716 return;
3717 }
3718
3719 $value = is_serialized( $value ) ? self::maybe_unserialize_array( $value ) : self::maybe_json_decode( $value, false );
3720 }
3721
3722 /**
3723 * Safely unserialize an array if necessary.
3724 * This function doesn't actually use unserialize. The string is parsed instead.
3725 *
3726 * @since 6.2
3727 *
3728 * @param mixed $value
3729 *
3730 * @return mixed
3731 */
3732 public static function maybe_unserialize_array( $value ) {
3733 if ( ! is_string( $value ) ) {
3734 return $value;
3735 }
3736
3737 // Since we only expect an array, skip anything that doesn't start with a:.
3738 if ( ! is_serialized( $value ) || ! str_starts_with( $value, 'a:' ) ) {
3739 return $value;
3740 }
3741
3742 $parsed = FrmSerializedStringParserHelper::get()->parse( $value );
3743
3744 return is_array( $parsed ) ? $parsed : $value;
3745 }
3746
3747 /**
3748 * Decode a JSON string.
3749 * Do not switch shortcodes like [24] to array unless intentional ie XML values.
3750 *
3751 * @param array|string|null $string
3752 * @param bool $single_to_array
3753 *
3754 * @return array|string|null
3755 */
3756 public static function maybe_json_decode( $string, $single_to_array = true ) {
3757 if ( is_array( $string ) || is_null( $string ) ) {
3758 return $string;
3759 }
3760
3761 $new_string = json_decode( $string, true );
3762 $single_value = false;
3763
3764 if ( ! $single_to_array ) {
3765 $single_value = is_array( $new_string ) && count( $new_string ) === 1 && isset( $new_string[0] );
3766 }
3767
3768 if ( json_last_error() === JSON_ERROR_NONE && is_array( $new_string ) && ! $single_value ) {
3769 return $new_string;
3770 }
3771
3772 return $string;
3773 }
3774
3775 /**
3776 * @since 6.2.3
3777 *
3778 * @param string $value
3779 *
3780 * @return string
3781 */
3782 public static function maybe_utf8_encode( $value ) {
3783 $from_format = 'ISO-8859-1';
3784 $to_format = 'UTF-8';
3785
3786 if ( function_exists( 'mb_check_encoding' ) && function_exists( 'mb_convert_encoding' ) ) {
3787 if ( mb_check_encoding( $value, $from_format ) ) {
3788 return mb_convert_encoding( $value, $to_format, $from_format );
3789 }
3790
3791 return $value;
3792 }
3793
3794 if ( function_exists( 'iconv' ) ) {
3795 $converted = iconv( $from_format, $to_format, $value );
3796
3797 // Value is false if $value is not ISO-8859-1.
3798 if ( false !== $converted ) {
3799 return $converted;
3800 }
3801 }
3802
3803 return $value;
3804 }
3805
3806 /**
3807 * Reformat the json serialized array in name => value array.
3808 *
3809 * @since 4.02.03
3810 *
3811 * @param array $form
3812 *
3813 * @return void
3814 */
3815 public static function format_form_data( &$form ) {
3816 $formatted = array();
3817
3818 foreach ( $form as $input ) {
3819 if ( ! isset( $input['name'] ) ) {
3820 continue;
3821 }
3822
3823 $key = $input['name'];
3824
3825 if ( ! isset( $formatted[ $key ] ) ) {
3826 $formatted[ $key ] = $input['value'];
3827 continue;
3828 }
3829
3830 if ( is_array( $formatted[ $key ] ) ) {
3831 $formatted[ $key ][] = $input['value'];
3832 } else {
3833 $formatted[ $key ] = array( $formatted[ $key ], $input['value'] );
3834 }
3835 }
3836
3837 parse_str( http_build_query( $formatted ), $form );
3838 }
3839
3840 /**
3841 * @since 4.02.03
3842 *
3843 * @param array|string $value
3844 *
3845 * @return string
3846 */
3847 public static function maybe_json_encode( $value ) {
3848 return is_array( $value ) ? wp_json_encode( $value ) : $value;
3849 }
3850
3851 /**
3852 * Echo The javascript to open and highlight the Formidable menu
3853 *
3854 * @since 1.07.10
3855 *
3856 * @param string $post_type The name of the post type that may need to be highlighted.
3857 *
3858 * @return void
3859 */
3860 public static function maybe_highlight_menu( $post_type ) {
3861 global $post;
3862
3863 if ( isset( $_REQUEST['post_type'] ) && $_REQUEST['post_type'] !== $post_type ) {
3864 return;
3865 }
3866
3867 if ( is_object( $post ) && $post->post_type !== $post_type ) {
3868 return;
3869 }
3870
3871 self::load_admin_wide_js();
3872 echo '<script type="text/javascript">jQuery(document).ready(function(){frmSelectSubnav();});</script>';
3873 }
3874
3875 /**
3876 * Load the JS file on non-Formidable pages in the admin area
3877 *
3878 * @since 2.0
3879 *
3880 * @param bool $load
3881 *
3882 * @return void
3883 */
3884 public static function load_admin_wide_js( $load = true ) {
3885 wp_register_script( 'formidable_admin_global', self::plugin_url() . '/js/formidable_admin_global.js', array( 'jquery' ), self::plugin_version() );
3886
3887 $global_strings = array(
3888 'updating_msg' => __( 'Please wait while your site updates.', 'formidable' ),
3889 'deauthorize' => __( 'Are you sure you want to deauthorize Formidable Forms on this site?', 'formidable' ),
3890 'url' => self::plugin_url(),
3891 'app_url' => 'https://formidableforms.com/',
3892 'applicationsUrl' => admin_url( 'admin.php?page=formidable-applications' ),
3893 'canAccessApplicationDashboard' => current_user_can( is_callable( 'FrmProApplicationsHelper::get_required_templates_capability' ) ? FrmProApplicationsHelper::get_required_templates_capability() : 'frm_edit_forms' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3894 'loading' => __( 'Loading&hellip;', 'formidable' ),
3895 'nonce' => wp_create_nonce( 'frm_ajax' ),
3896 'proIncludesSliderJs' => is_callable( 'FrmProFormsHelper::prepare_custom_currency' ),
3897 'inboxSlideIn' => FrmInbox::get_inbox_slide_in_value_for_js(),
3898 );
3899 wp_localize_script( 'formidable_admin_global', 'frmGlobal', $global_strings );
3900
3901 if ( $load ) {
3902 wp_enqueue_script( 'formidable_admin_global' );
3903 }
3904 }
3905
3906 /**
3907 * @since 2.0.9
3908 *
3909 * @return void
3910 */
3911 public static function load_font_style() {
3912 wp_enqueue_style( 'frm_fonts', self::plugin_url() . '/css/frm_fonts.css', array(), self::plugin_version() );
3913 }
3914
3915 /**
3916 * @param string $location
3917 *
3918 * @return void
3919 */
3920 public static function localize_script( $location ) {
3921 global $wp_scripts, $wp_version;
3922
3923 $script_strings = array(
3924 'ajax_url' => esc_url_raw( self::get_ajax_url() ),
3925 'images_url' => self::plugin_url() . '/images',
3926 'loading' => __( 'Loading&hellip;', 'formidable' ),
3927 'remove' => __( 'Remove', 'formidable' ),
3928 'offset' => apply_filters( 'frm_scroll_offset', 4 ),
3929 'nonce' => wp_create_nonce( 'frm_ajax' ),
3930 'id' => __( 'ID', 'formidable' ),
3931 'no_results' => __( 'No results match', 'formidable' ),
3932 'file_spam' => __( 'That file looks like Spam.', 'formidable' ),
3933 'calc_error' => __( 'There is an error in the calculation in the field with key', 'formidable' ),
3934 'empty_fields' => __( 'Please complete the preceding required fields before uploading a file.', 'formidable' ),
3935 'focus_first_error' => self::should_focus_first_error(),
3936 'include_alert_role' => self::should_include_alert_role_on_field_errors(),
3937 // We need to keep this setting for a few versions because Pro checks for this.
3938 'include_resend_email' => false,
3939 );
3940
3941 $data = $wp_scripts->get_data( 'formidable', 'data' );
3942
3943 if ( ! $data ) {
3944 wp_localize_script( 'formidable', 'frm_js', $script_strings );
3945 }
3946
3947 if ( $location === 'admin' ) {
3948 $admin_script_strings = array(
3949 'desc' => __( '(Click to add description)', 'formidable' ),
3950 'blank' => __( '(Blank)', 'formidable' ),
3951 'no_label' => self::get_no_label_text(),
3952 'ok' => __( 'OK', 'formidable' ),
3953 'cancel' => __( 'Cancel', 'formidable' ),
3954 'default_label' => __( 'Default', 'formidable' ),
3955 'clear_default' => __( 'Clear default value when typing', 'formidable' ),
3956 'no_clear_default' => __( 'Do not clear default value when typing', 'formidable' ),
3957 'valid_default' => __( 'Default value will pass form validation', 'formidable' ),
3958 'no_valid_default' => __( 'Default value will NOT pass form validation', 'formidable' ),
3959 'confirm' => __( 'Are you sure?', 'formidable' ),
3960 'conf_delete' => __( 'Are you sure you want to delete this field and all data associated with it?', 'formidable' ),
3961 'conf_delete_sec' => __( 'All fields inside this Section will be deleted along with their data. Are you sure you want to delete this group of fields?', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3962 'conf_no_repeat' => __( 'Warning: If you have entries with multiple rows, all but the first row will be lost.', 'formidable' ),
3963 'default_unique' => FrmFieldsHelper::default_unique_msg(),
3964 'default_conf' => __( 'The entered values do not match', 'formidable' ),
3965 'enter_email' => __( 'Enter Email', 'formidable' ),
3966 'confirm_email' => __( 'Confirm Email', 'formidable' ),
3967 'conditional_text' => __( 'Conditional content here', 'formidable' ),
3968 'new_option' => __( 'New Option', 'formidable' ),
3969 'css_invalid_size' => __( 'In certain browsers (e.g. Firefox) text will not display correctly if the field height is too small relative to the field padding and text size. Please increase your field height or decrease your field padding.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3970 'enter_password' => __( 'Enter Password', 'formidable' ),
3971 'confirm_password' => __( 'Confirm Password', 'formidable' ),
3972 'import_complete' => __( 'Import Complete', 'formidable' ),
3973 'updating' => __( 'Please wait while your site updates.', 'formidable' ),
3974 'no_save_warning' => __( 'Warning: There is no way to retrieve unsaved entries.', 'formidable' ),
3975 'private_label' => __( 'Private', 'formidable' ),
3976 'jquery_ui_url' => '',
3977 'pro_url' => is_callable( 'FrmProAppHelper::plugin_url' ) ? FrmProAppHelper::plugin_url() : '',
3978 'no_licenses' => __( 'No new licenses were found', 'formidable' ),
3979 'unmatched_parens' => __( 'This calculation has at least one unmatched ( ) { } [ ].', 'formidable' ),
3980 'view_shortcodes' => __( 'This calculation may have shortcodes that work in Views but not forms.', 'formidable' ),
3981 'text_shortcodes' => __( 'This calculation may have shortcodes that work in text calculations but not numeric calculations.', 'formidable' ),
3982 /* translators: %d is the number of allowed actions per form */
3983 'only_one_action' => sprintf( __( 'This form action is limited to %d per form.', 'formidable' ), 1 ),
3984 'edit_action_text' => __( 'Please edit the existing form action.', 'formidable' ),
3985 'only_one_payment_action' => __( 'This form already has a payment action, and is currently limited to a single payment action.', 'formidable' ),
3986 'only_one_stripe_action' => __( 'This form already has a payment action. Multiple Stripe actions are available when using the Stripe add-on, available for Formidable Business licenses and higher.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3987 'unsafe_params' => FrmFormsHelper::reserved_words(),
3988 /* Translators: %s is the name of a Detail Page Slug that is a reserved word.*/
3989 'slug_is_reserved' => sprintf( __( 'The Detail Page Slug "%s" is reserved by WordPress. This may cause problems. Is this intentional?', 'formidable' ), '****' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3990 /* Translators: %s is the name of a parameter that is a reserved word. More than one word could be listed here, though that would not be common. */
3991 'param_is_reserved' => sprintf( __( 'The parameter "%s" is reserved by WordPress. This may cause problems when included in the URL. Is this intentional? ', 'formidable' ), '****' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
3992 'reserved_words' => __( 'See the list of reserved words in WordPress.', 'formidable' ),
3993 'repeat_limit_min' => __( 'Please enter a Repeat Limit that is greater than 1.', 'formidable' ),
3994 'checkbox_limit' => __( 'Please select a limit between 0 and 200.', 'formidable' ),
3995 'install' => __( 'Install', 'formidable' ),
3996 'active' => __( 'Active', 'formidable' ),
3997 'installed' => __( 'Installed', 'formidable' ),
3998 'not_installed' => __( 'Not Installed', 'formidable' ),
3999 'select_a_field' => __( 'Select a Field', 'formidable' ),
4000 'no_items_found' => __( 'No items found.', 'formidable' ),
4001 'field_already_used' => __( 'Oops. You have already used that field.', 'formidable' ),
4002
4003 // Deprecated in 6.0.
4004 'saving' => '',
4005
4006 // Deprecated in 6.0.
4007 'saved' => '',
4008
4009 // translators: %1$s: HTML open tag, %2$s: HTML end tag.
4010 'holdShiftMsg' => esc_html__( 'You can hold %1$sShift%2$s on your keyboard to select multiple fields', 'formidable' ),
4011 'noTitleText' => FrmFormsHelper::get_no_title_text(),
4012
4013 // In older versions this event listener causes the section to immediately close again
4014 // When the h3 element is clicked. It's only required in WP 6.7+.
4015 'requireAccordionTitleClickListener' => version_compare( $wp_version, '6.7', '>=' ),
4016 );
4017
4018 self::add_form_builder_modal_data( $admin_script_strings );
4019
4020 /**
4021 * @param array $admin_script_strings
4022 */
4023 $admin_script_strings = apply_filters( 'frm_admin_script_strings', $admin_script_strings );
4024
4025 $data = $wp_scripts->get_data( 'formidable_admin', 'data' );
4026
4027 if ( ! $data ) {
4028 wp_localize_script( 'formidable_admin', 'frm_admin_js', $admin_script_strings );
4029 }
4030 }//end if
4031 }
4032
4033 /**
4034 * @param array $admin_script_strings
4035 *
4036 * @return void
4037 */
4038 private static function add_form_builder_modal_data( &$admin_script_strings ) {
4039 if ( ! self::is_form_builder_page() || self::pro_is_installed() ) {
4040 return;
4041 }
4042
4043 $stripe_connected = FrmStrpLiteConnectHelper::at_least_one_mode_is_setup();
4044 $square_connected = FrmSquareLiteConnectHelper::at_least_one_mode_is_setup();
4045 $paypal_connected = FrmPayPalLiteConnectHelper::at_least_one_mode_is_setup();
4046 $gateway_connected = $stripe_connected || $square_connected || $paypal_connected;
4047 $payments_settings_url = FrmStrpLiteAppController::get_payments_settings_url();
4048
4049 if ( ! $gateway_connected ) {
4050 // This modal shows when user clicks on one of the pricing fields and no payment gateways configured.
4051 $admin_script_strings['paymentsSettingsModal'] = array(
4052 'title' => __( 'Setup a Payment Gateway first', 'formidable' ),
4053 'msg' => __( 'To use the payment fields, please install and configure a payment gateway in your account settings.', 'formidable' ),
4054 'closeText' => __( 'Close', 'formidable' ),
4055 'actionUrl' => $payments_settings_url,
4056 'actionText' => __( 'Go to Payment Settings', 'formidable' ),
4057 'noCenter' => true,
4058 );
4059 }
4060
4061 // This modal shows on load once after upgrading the plugin.
4062 $show_pricing_fields_modal = get_option( 'frm_show_pricing_fields_modal' );
4063
4064 if ( ! $show_pricing_fields_modal ) {
4065 return;
4066 }
4067
4068 $admin_script_strings['pricingFieldsModal'] = array(
4069 'title' => esc_html__( 'Start Accepting Payments Today!', 'formidable' ),
4070 'img' => esc_url( self::plugin_url() . '/images/upsell/pricing-fields.png' ),
4071 'noCenter' => true,
4072 );
4073
4074 if ( $gateway_connected ) {
4075 $gateway_texts = array();
4076
4077 if ( $stripe_connected ) {
4078 $gateway_texts['stripe'] = esc_html__( 'Stripe', 'formidable' );
4079 }
4080
4081 if ( $square_connected ) {
4082 $gateway_texts['square'] = esc_html__( 'Square', 'formidable' );
4083 }
4084
4085 if ( $paypal_connected ) {
4086 $gateway_texts['paypal'] = esc_html__( 'PayPal', 'formidable' );
4087 }
4088
4089 $admin_script_strings['pricingFieldsModal']['msg'] = sprintf(
4090 // translators: %s: Stripe, Square, or PayPal.
4091 esc_html__( 'You already have %s connected, so these have already been unlocked.', 'formidable' ),
4092 esc_html( implode( ' ' . esc_html__( 'and', 'formidable' ) . ' ', $gateway_texts ) )
4093 );
4094 } else {
4095 $admin_script_strings['pricingFieldsModal']['closeText'] = __( 'I\'ll do it later!', 'formidable' );
4096 $admin_script_strings['pricingFieldsModal']['actionText'] = __( 'Setup Payments Now', 'formidable' );
4097 $admin_script_strings['pricingFieldsModal']['actionUrl'] = $payments_settings_url;
4098 // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
4099 $admin_script_strings['pricingFieldsModal']['msg'] = __( 'We\'ve unlocked Product, Quantity, and Total fields for Lite users! You can now transform your forms into checkout pages. To start collecting revenue, simply connect your preferred payment gateway (Stripe, Square, or PayPal) in your settings.', 'formidable' );
4100 }//end if
4101
4102 delete_option( 'frm_show_pricing_fields_modal' );
4103 }
4104
4105 /**
4106 * Get the no label text.
4107 *
4108 * @since 6.25.1
4109 *
4110 * @return string
4111 */
4112 public static function get_no_label_text() {
4113 return __( '(no label)', 'formidable' );
4114 }
4115
4116 /**
4117 * @since 6.5
4118 *
4119 * @return string
4120 */
4121 public static function get_ajax_url() {
4122 $ajax_url = admin_url( 'admin-ajax.php', is_ssl() ? 'admin' : 'http' );
4123
4124 /**
4125 * @since 2.0.13
4126 *
4127 * @param string $ajax_url
4128 */
4129 return apply_filters( 'frm_ajax_url', $ajax_url );
4130 }
4131
4132 /**
4133 * Returns whether or not the first errored input should be auto-focused (default true).
4134 *
4135 * @since 5.2.05
4136 *
4137 * @return bool
4138 */
4139 private static function should_focus_first_error() {
4140 return (bool) apply_filters( 'frm_focus_first_error', true );
4141 }
4142
4143 /**
4144 * Returns whether or not field errors should include role="alert" (default true).
4145 *
4146 * @since 5.2.05
4147 *
4148 * @return bool
4149 */
4150 public static function should_include_alert_role_on_field_errors() {
4151 return (bool) apply_filters( 'frm_include_alert_role_on_field_errors', true );
4152 }
4153
4154 /**
4155 * Echo the message on the plugins listing page
4156 *
4157 * @since 1.07.10
4158 *
4159 * @param float $min_version The version the add-on requires.
4160 *
4161 * @return void
4162 */
4163 public static function min_version_notice( $min_version ) {
4164 $frm_version = self::plugin_version();
4165
4166 // Check if Formidable meets minimum requirements.
4167 if ( version_compare( $frm_version, $min_version, '>=' ) ) {
4168 return;
4169 }
4170
4171 $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
4172 echo '<tr class="plugin-update-tr active"><th colspan="' . absint( $wp_list_table->get_column_count() ) . '" class="check-column plugin-update colspanchange"><div class="update-message">' . // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
4173 esc_html__( 'You are running an outdated version of Formidable. This plugin may not work correctly if you do not update Formidable.', 'formidable' ) .
4174 '</div></td></tr>';
4175 }
4176
4177 /**
4178 * If Pro is far outdated, show a message.
4179 *
4180 * @since 4.0.01
4181 *
4182 * @param string $min_version
4183 *
4184 * @return void
4185 */
4186 public static function min_pro_version_notice( $min_version ) {
4187 if ( ! self::is_formidable_admin() ) {
4188 // Don't show admin-wide.
4189 return;
4190 }
4191
4192 self::php_version_notice();
4193
4194 $is_pro = self::pro_is_installed() && class_exists( 'FrmProDb' );
4195
4196 if ( ! $is_pro || self::meets_min_pro_version( $min_version ) ) {
4197 return;
4198 }
4199
4200 include self::plugin_path() . '/classes/views/addons/min-version-notice.php';
4201 }
4202
4203 /**
4204 * If Pro is installed, check the version number.
4205 *
4206 * @since 4.0.01
4207 *
4208 * @param string $min_version
4209 *
4210 * @return bool
4211 */
4212 public static function meets_min_pro_version( $min_version ) {
4213 return ! class_exists( 'FrmProDb' ) || version_compare( FrmProDb::$plug_version, $min_version, '>=' );
4214 }
4215
4216 /**
4217 * Show a message if the PHP version is below the recommendations.
4218 *
4219 * @since 4.0.02
4220 *
4221 * @return void
4222 */
4223 private static function php_version_notice() {
4224 $message = array();
4225
4226 if ( version_compare( phpversion(), '7.0', '<' ) ) {
4227 $message[] = __( 'The version of PHP on your server is too low. If this is not corrected, you may see issues with Formidable Forms. Please contact your web host and ask to be updated to PHP 7.0+.', 'formidable' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
4228 }
4229
4230 // phpcs:disable Generic.WhiteSpace.ScopeIndent
4231 foreach ( $message as $m ) {
4232 ?>
4233 <div class="frm-banner-alert frm_error_style frm_previous_install">
4234 <?php echo esc_html( $m ); ?>
4235 </div>
4236 <?php
4237 }
4238 // phpcs:enable Generic.WhiteSpace.ScopeIndent
4239 }
4240
4241 /**
4242 * @param string $type
4243 *
4244 * @return array<string,string>
4245 */
4246 public static function locales( $type = 'date' ) {
4247 $locales = array(
4248 'en' => __( 'English', 'formidable' ),
4249 'af' => __( 'Afrikaans', 'formidable' ),
4250 'sq' => __( 'Albanian', 'formidable' ),
4251 'ar-DZ' => __( 'Algerian Arabic', 'formidable' ),
4252 'am' => __( 'Amharic', 'formidable' ),
4253 'ar' => __( 'Arabic', 'formidable' ),
4254 'hy' => __( 'Armenian', 'formidable' ),
4255 'az' => __( 'Azerbaijani', 'formidable' ),
4256 'eu' => __( 'Basque', 'formidable' ),
4257 'be' => __( 'Belarusian', 'formidable' ),
4258 'bn' => __( 'Bengali', 'formidable' ),
4259 'bs' => __( 'Bosnian', 'formidable' ),
4260 'bg' => __( 'Bulgarian', 'formidable' ),
4261 'ca' => __( 'Catalan', 'formidable' ),
4262 'zh-HK' => __( 'Chinese Hong Kong', 'formidable' ),
4263 'zh-CN' => __( 'Chinese Simplified', 'formidable' ),
4264 'zh-TW' => __( 'Chinese Traditional', 'formidable' ),
4265 'hr' => __( 'Croatian', 'formidable' ),
4266 'cs' => __( 'Czech', 'formidable' ),
4267 'da' => __( 'Danish', 'formidable' ),
4268 'nl' => __( 'Dutch', 'formidable' ),
4269 'en-GB' => __( 'English/UK', 'formidable' ),
4270 'eo' => __( 'Esperanto', 'formidable' ),
4271 'et' => __( 'Estonian', 'formidable' ),
4272 'fo' => __( 'Faroese', 'formidable' ),
4273 'fa' => __( 'Farsi/Persian', 'formidable' ),
4274 'fil' => __( 'Filipino', 'formidable' ),
4275 'fi' => __( 'Finnish', 'formidable' ),
4276 'fr' => __( 'French', 'formidable' ),
4277 'fr-CA' => __( 'French/Canadian', 'formidable' ),
4278 'fr-CH' => __( 'French/Swiss', 'formidable' ),
4279 'gl' => __( 'Galician', 'formidable' ),
4280 'ka' => __( 'Georgian', 'formidable' ),
4281 'de' => __( 'German', 'formidable' ),
4282 'de-AT' => __( 'German/Austria', 'formidable' ),
4283 'de-CH' => __( 'German/Switzerland', 'formidable' ),
4284 'el' => __( 'Greek', 'formidable' ),
4285 'gu' => __( 'Gujarati', 'formidable' ),
4286 'he' => __( 'Hebrew', 'formidable' ),
4287 'iw' => __( 'Hebrew', 'formidable' ),
4288 'hi' => __( 'Hindi', 'formidable' ),
4289 'hu' => __( 'Hungarian', 'formidable' ),
4290 'is' => __( 'Icelandic', 'formidable' ),
4291 'id' => __( 'Indonesian', 'formidable' ),
4292 'it' => __( 'Italian', 'formidable' ),
4293 'ja' => __( 'Japanese', 'formidable' ),
4294 'kn' => __( 'Kannada', 'formidable' ),
4295 'kk' => __( 'Kazakh', 'formidable' ),
4296 'km' => __( 'Khmer', 'formidable' ),
4297 'ko' => __( 'Korean', 'formidable' ),
4298 'ky' => __( 'Kyrgyz', 'formidable' ),
4299 'lo' => __( 'Laothian', 'formidable' ),
4300 'lv' => __( 'Latvian', 'formidable' ),
4301 'lt' => __( 'Lithuanian', 'formidable' ),
4302 'lb' => __( 'Luxembourgish', 'formidable' ),
4303 'mk' => __( 'Macedonian', 'formidable' ),
4304 'ml' => __( 'Malayalam', 'formidable' ),
4305 'ms' => __( 'Malaysian', 'formidable' ),
4306 'mr' => __( 'Marathi', 'formidable' ),
4307 'no' => __( 'Norwegian', 'formidable' ),
4308 'nb' => __( 'Norwegian Bokmål', 'formidable' ),
4309 'nn' => __( 'Norwegian Nynorsk', 'formidable' ),
4310 'pl' => __( 'Polish', 'formidable' ),
4311 'pt' => __( 'Portuguese', 'formidable' ),
4312 'pt-BR' => __( 'Portuguese/Brazilian', 'formidable' ),
4313 'pt-PT' => __( 'Portuguese/Portugal', 'formidable' ),
4314 'rm' => __( 'Romansh', 'formidable' ),
4315 'ro' => __( 'Romanian', 'formidable' ),
4316 'ru' => __( 'Russian', 'formidable' ),
4317 'sr' => __( 'Serbian', 'formidable' ),
4318 'sr-SR' => __( 'Serbian', 'formidable' ),
4319 'si' => __( 'Sinhalese', 'formidable' ),
4320 'sk' => __( 'Slovak', 'formidable' ),
4321 'sl' => __( 'Slovenian', 'formidable' ),
4322 'es' => __( 'Spanish', 'formidable' ),
4323 'es-419' => __( 'Spanish/Latin America', 'formidable' ),
4324 'sw' => __( 'Swahili', 'formidable' ),
4325 'sv' => __( 'Swedish', 'formidable' ),
4326 'ta' => __( 'Tamil', 'formidable' ),
4327 'te' => __( 'Telugu', 'formidable' ),
4328 'th' => __( 'Thai', 'formidable' ),
4329 'tj' => __( 'Tajiki', 'formidable' ),
4330 'tr' => __( 'Turkish', 'formidable' ),
4331 'uk' => __( 'Ukrainian', 'formidable' ),
4332 'ur' => __( 'Urdu', 'formidable' ),
4333 'vi' => __( 'Vietnamese', 'formidable' ),
4334 'cy-GB' => __( 'Welsh', 'formidable' ),
4335 'zu' => __( 'Zulu', 'formidable' ),
4336 );
4337
4338 if ( $type === 'captcha' ) {
4339 // Remove the languages unavailable for the captcha
4340 $unset = array( 'sq', 'bs', 'eo', 'fo', 'fr-CH', 'sr-SR', 'ar-DZ', 'be', 'cy-GB', 'kk', 'km', 'ky', 'lb', 'mk', 'nb', 'nn', 'rm', 'tj' );
4341 } else {
4342 // Remove the languages unavailable for the datepicker
4343 $unset = array( 'fil', 'fr-CA', 'de-AT', 'de-CH', 'iw', 'hi', 'pt', 'pt-PT', 'es-419', 'mr', 'lo', 'kn', 'si', 'gu', 'bn', 'zu', 'ur', 'te', 'sw', 'am' );
4344 }
4345
4346 $locales = array_diff_key( $locales, array_flip( $unset ) );
4347
4348 /**
4349 * Filter available locale options.
4350 *
4351 * @since 5.4.5 Added $args parameter with type.
4352 *
4353 * @param array<string,string> $locales
4354 * @param array $args {
4355 *
4356 * @type string $type
4357 * }
4358 */
4359 return apply_filters( 'frm_locales', $locales, compact( 'type' ) );
4360 }
4361
4362 /**
4363 * @return string
4364 */
4365 public static function get_menu_icon_class() {
4366 if ( is_callable( 'FrmProAppHelper::get_settings' ) ) {
4367 $settings = FrmProAppHelper::get_settings();
4368
4369 if ( is_object( $settings ) && ! empty( $settings->menu_icon ) ) {
4370 return $settings->menu_icon;
4371 }
4372 }
4373
4374 return 'frmfont frm_logo_icon';
4375 }
4376
4377 /**
4378 * Shows the images dropdown.
4379 *
4380 * @since 5.0.04
4381 *
4382 * @param array $args {
4383 * Arguments.
4384 *
4385 * @type string $selected Selected value.
4386 * @type array[] $options Array of options with keys are option values and values are array.
4387 * The option array contains `text`, `svg` and `custom_atts`.
4388 * @type string $classes Custom CSS classes for the wrapper element.
4389 * @type array $input_attrs Attributes of value input.
4390 * }
4391 */
4392 public static function images_dropdown( $args ) {
4393 $args = self::fill_default_images_dropdown_args( $args );
4394 $input_attrs_str = self::get_images_dropdown_input_attrs( $args );
4395 ob_start();
4396 include self::plugin_path() . '/classes/views/shared/images-dropdown.php';
4397 $output = ob_get_clean();
4398
4399 /**
4400 * Allows modifying the output of FrmAppHelper::images_dropdown() method.
4401 *
4402 * @since 5.0.04
4403 *
4404 * @param string $output The output.
4405 * @param array $args Passed arguments.
4406 */
4407 echo apply_filters( 'frm_images_dropdown_output', $output, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
4408 }
4409
4410 /**
4411 * Fills the default images_dropdown() arguments.
4412 *
4413 * @since 5.0.04
4414 *
4415 * @param array $args The arguments.
4416 *
4417 * @return array
4418 */
4419 private static function fill_default_images_dropdown_args( $args ) {
4420 $defaults = array(
4421 'selected' => '',
4422 'options' => array(),
4423 'classes' => '',
4424 'input_attrs' => array(),
4425 );
4426 $new_args = wp_parse_args( $args, $defaults );
4427
4428 $new_args['options'] = (array) $new_args['options'];
4429 $new_args['input_attrs'] = (array) $new_args['input_attrs'];
4430
4431 /**
4432 * Allows modifying the arguments of images_dropdown() method.
4433 *
4434 * @since 5.0.04
4435 *
4436 * @param array $new_args Arguments after filling the defaults.
4437 * @param array $args Arguments passed to the method, before filling the defaults.
4438 */
4439 return apply_filters( 'frm_images_dropdown_args', $new_args, $args );
4440 }
4441
4442 /**
4443 * Gets HTML attributes of the input in images_dropdown() method.
4444 *
4445 * @since 5.0.04
4446 *
4447 * @param array $args The arguments.
4448 *
4449 * @return string
4450 */
4451 private static function get_images_dropdown_input_attrs( $args ) {
4452 $input_attrs = $args['input_attrs'];
4453 $input_attrs['type'] = 'radio';
4454 $input_attrs['name'] = $args['name'];
4455
4456 $input_attrs_str = '';
4457
4458 foreach ( $input_attrs as $key => $input_attr ) {
4459 $input_attrs_str .= ' ' . sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $input_attr ) );
4460 }
4461
4462 /**
4463 * Allows modifying the HTML attributes of the input in images_dropdown() method.
4464 *
4465 * @since 5.0.04
4466 *
4467 * @param string $input_attrs_str HTML attributes string.
4468 * @param array $args The arguments of images_dropdown() method.
4469 */
4470 return apply_filters( 'frm_images_dropdown_input_attrs', $input_attrs_str, $args );
4471 }
4472
4473 /**
4474 * @since 6.7.1
4475 *
4476 * @param array $option Option data.
4477 * @param array $args The arguments of images_dropdown() method.
4478 *
4479 * @return array
4480 */
4481 public static function get_images_dropdown_atts( $option, $args ) {
4482 $image = self::get_images_dropdown_option_image( $option, $args );
4483 $classes = self::get_images_dropdown_option_classes( $option, $args );
4484 $custom_attrs = self::get_images_dropdown_option_html_attrs( $option, $args );
4485 return compact( 'image', 'classes', 'custom_attrs' );
4486 }
4487
4488 /**
4489 * Gets the image of each option in images_dropdown() method.
4490 *
4491 * @since 5.0.04
4492 *
4493 * @param array $option Option data.
4494 * @param array $args The arguments of images_dropdown() method.
4495 *
4496 * @return string
4497 */
4498 private static function get_images_dropdown_option_image( $option, $args ) {
4499 $image = self::icon_by_class(
4500 'frmfont ' . $option['svg'],
4501 array(
4502 'echo' => false,
4503 )
4504 );
4505
4506 $args['option'] = $option;
4507
4508 /**
4509 * Allows modifying the image of each option in images_dropdown() method.
4510 *
4511 * @since 5.0.04
4512 *
4513 * @param string $image The image HTML.
4514 * @param array $args The arguments of images_dropdown() method, with `option` array is added.
4515 */
4516 return apply_filters( 'frm_images_dropdown_option_image', $image, $args );
4517 }
4518
4519 /**
4520 * Gets the HTML classes of each option in images_dropdown() method.
4521 *
4522 * @since 5.0.04
4523 *
4524 * @param array $option Option data.
4525 * @param array $args The arguments of images_dropdown() method.
4526 *
4527 * @return string
4528 */
4529 private static function get_images_dropdown_option_classes( $option, $args ) {
4530 $classes = '';
4531
4532 if ( ! empty( $option['custom_attrs']['class'] ) ) {
4533 $classes .= ' ' . $option['custom_attrs']['class'];
4534 }
4535
4536 $args['option'] = $option;
4537
4538 /**
4539 * Allows modifying the CSS classes of each option in images_dropdown() method.
4540 *
4541 * @since 5.0.04
4542 *
4543 * @param string $classes CSS classes.
4544 * @param array $args The arguments of images_dropdown() method, with `option` array is added.
4545 */
4546 return apply_filters( 'frm_images_dropdown_option_classes', $classes, $args );
4547 }
4548
4549 /**
4550 * Gets the custom HTML attributes of each option in images_dropdown() method.
4551 *
4552 * @since 5.0.04
4553 *
4554 * @param array $option Option data.
4555 * @param array $args The arguments of images_dropdown() method.
4556 *
4557 * @return string
4558 */
4559 private static function get_images_dropdown_option_html_attrs( $option, $args ) {
4560 $html_attrs = '';
4561
4562 if ( ! empty( $option['custom_attrs'] ) && is_array( $option['custom_attrs'] ) ) {
4563 $html_attrs_arr = array();
4564
4565 foreach ( $option['custom_attrs'] as $key => $value ) {
4566 if ( in_array( $key, array( 'type', 'class', 'data-value' ), true ) ) {
4567 continue;
4568 }
4569
4570 $html_attrs_arr[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
4571 }
4572
4573 $html_attrs = implode( ' ', $html_attrs_arr );
4574 }
4575
4576 $args['option'] = $option;
4577
4578 /**
4579 * Allows modifying the custom HTML attributes of each option in images_dropdown() method.
4580 *
4581 * @since 5.0.04
4582 *
4583 * @param string $html_attrs The HTML attributes string.
4584 * @param array $args The arguments of images_dropdown() method, with `option` array is added.
4585 */
4586 return apply_filters( 'frm_images_dropdown_option_html_attrs', $html_attrs, $args );
4587 }
4588
4589 /**
4590 * @since 5.0.07
4591 *
4592 * @return bool true if the current user is allowed to save unfiltered HTML.
4593 */
4594 public static function allow_unfiltered_html() {
4595 if ( self::should_never_allow_unfiltered_html() ) {
4596 return false;
4597 }
4598 return current_user_can( 'unfiltered_html' );
4599 }
4600
4601 /**
4602 * @since 5.0.13
4603 *
4604 * @return bool
4605 */
4606 public static function should_never_allow_unfiltered_html() {
4607 if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
4608 return true;
4609 }
4610
4611 /**
4612 * Formidable will check DISALLOW_UNFILTERED_HTML to determine if some form HTML should be filtered or not.
4613 * In many cases, scripts are added intentionally to forms and will not be stripped if DISALLOW_UNFILTERED_HTML is not set.
4614 * It is also possible to filter Formidable without defining DISALLOW_UNFILTERED_HTML, with add_filter( 'frm_disallow_unfiltered_html', '__return_true' );
4615 *
4616 * @since 5.0.13
4617 */
4618 return apply_filters( 'frm_disallow_unfiltered_html', false );
4619 }
4620
4621 /**
4622 * @since 5.0.07
4623 *
4624 * @param array $values
4625 * @param array $keys
4626 *
4627 * @return array
4628 */
4629 public static function maybe_filter_array( $values, $keys ) {
4630 if ( self::allow_unfiltered_html() ) {
4631 return $values;
4632 }
4633
4634 foreach ( $keys as $key ) {
4635 if ( isset( $values[ $key ] ) ) {
4636 $values[ $key ] = self::kses( $values[ $key ], 'all' );
4637 }
4638 }
4639
4640 return $values;
4641 }
4642
4643 /**
4644 * Some back end fields allow privileged users to add scripts.
4645 * A site that uses the DISALLOW_UNFILTERED_HTML always remove scripts on echo.
4646 *
4647 * @since 5.0.13
4648 *
4649 * @param string $value
4650 * @param array|string $allowed 'all' for everything included as defaults.
4651 *
4652 * @return string
4653 */
4654 public static function maybe_kses( $value, $allowed = 'all' ) {
4655 if ( self::should_never_allow_unfiltered_html() ) {
4656 return self::kses( $value, $allowed );
4657 }
4658 return $value;
4659 }
4660
4661 /**
4662 * Check if an option attribute used in an [input] shortcode is safe.
4663 *
4664 * @since 6.11.2
4665 *
4666 * @param string $key
4667 * @param string $context Either 'display' or 'update'. On update, we want to allow a few keys that are never displayed.
4668 *
4669 * @return bool
4670 */
4671 public static function input_key_is_safe( $key, $context = 'display' ) {
4672 if ( 'update' === $context && in_array( $key, array( 'opt', 'label' ), true ) ) {
4673 $safe = true;
4674 } elseif ( str_starts_with( $key, 'data-' ) ) {
4675 // Allow all data attributes.
4676 $safe = true;
4677 } elseif ( str_starts_with( $key, 'aria-' ) ) {
4678 // Allow all aria attributes.
4679 $safe = true;
4680 } else {
4681 $safe_keys = array(
4682 'class',
4683 'required',
4684 'title',
4685 'placeholder',
4686 'value',
4687 'readonly',
4688 'disabled',
4689 'size',
4690 'maxlength',
4691 'min',
4692 'max',
4693 'pattern',
4694 'step',
4695 'autofocus',
4696 'width',
4697 'height',
4698 'autocomplete',
4699 'tabindex',
4700 'role',
4701 'style',
4702 );
4703 $safe = in_array( $key, $safe_keys, true );
4704 }//end if
4705
4706 /**
4707 * Filter the $safe value so additional keys can be allowed or disallowed.
4708 *
4709 * @since 6.11.2
4710 *
4711 * @param bool $safe True if the key is considered safe.
4712 * @param string $key
4713 * @param string $context Either 'display' or 'update'.
4714 */
4715 return (bool) apply_filters( 'frm_input_key_is_safe', $safe, $key, $context );
4716 }
4717
4718 /**
4719 * @since 5.0.16
4720 *
4721 * @param string $medium
4722 *
4723 * @return array
4724 */
4725 public static function get_landing_page_upgrade_data_params( $medium = 'landing' ) {
4726 $params = array(
4727 'medium' => $medium,
4728 'upgrade' => __( 'Form Landing Pages', 'formidable' ),
4729 'message' => __( 'Easily manage a landing page for your form. Upgrade to get form landing pages.', 'formidable' ),
4730 'screenshot' => 'landing.png',
4731 'learn-more' => self::get_doc_url( 'landing-page-forms', 'form-landing-page-settings', false ),
4732 );
4733 return self::get_upgrade_data_params( 'landing', $params );
4734 }
4735
4736 /**
4737 * @since 5.0.17
4738 *
4739 * @param string $feature
4740 *
4741 * @return bool
4742 */
4743 public static function show_new_feature( $feature ) {
4744 $link = FrmAddonsController::install_link( $feature );
4745 return array_key_exists( 'status', $link ) || array_key_exists( 'class', $link );
4746 }
4747
4748 /**
4749 * Enhances upgrade data parameters with installation link and plan requirement information.
4750 *
4751 * @since 5.0.17
4752 *
4753 * @param string $plugin The plugin slug to get installation data for.
4754 * @param array $params Initial parameters for the upgrade data.
4755 * @param bool $detailed Whether to include detailed information.
4756 *
4757 * @return array Modified parameters with installation data.
4758 */
4759 public static function get_upgrade_data_params( $plugin, $params, $detailed = false ) {
4760 $link = FrmAddonsController::install_link( $plugin );
4761
4762 if ( ! $link ) {
4763 return $params;
4764 }
4765
4766 if ( ! empty( $link['url'] ) && self::pro_is_installed() ) {
4767 $params['oneclick'] = json_encode( $link );
4768 unset( $params['message'] );
4769
4770 if ( ! isset( $params['medium'] ) ) {
4771 $params['medium'] = $plugin;
4772 }
4773 } else {
4774 $params['requires'] = $params['requires'] ?? FrmFormsHelper::get_plan_required( $link );
4775 }
4776
4777 if ( $detailed ) {
4778 $params['plugin-status'] = $link['status'] ?? '';
4779 }
4780
4781 return $params;
4782 }
4783
4784 /**
4785 * Returns true if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f], false otherwise.
4786 * Not every server installs the ctype extension, so use a fallback if the function does not exist.
4787 *
4788 * @since 5.0.17
4789 *
4790 * @param string $text
4791 *
4792 * @return bool
4793 */
4794 public static function ctype_xdigit( $text ) {
4795 if ( function_exists( 'ctype_xdigit' ) ) {
4796 return ctype_xdigit( $text );
4797 }
4798 return is_string( $text ) && '' !== $text && ! preg_match( '/[^A-Fa-f0-9]/', $text );
4799 }
4800
4801 /**
4802 * Set the current screen to avoid undefined notices.
4803 *
4804 * @since 5.2.01
4805 */
4806 public static function set_current_screen_and_hook_suffix() {
4807 global $hook_suffix;
4808
4809 if ( is_null( $hook_suffix ) ) {
4810 // $hook_suffix gets used in substr so make sure it's not null. PHP 8.1 deprecates null in substr.
4811 $hook_suffix = ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
4812 }
4813
4814 set_current_screen();
4815 }
4816
4817 /**
4818 * Shows pill text.
4819 *
4820 * @since 5.2.02
4821 *
4822 * @param string|null $text Text in the pill. Default is NEW.
4823 */
4824 public static function show_pill_text( $text = null ) {
4825 if ( null === $text ) {
4826 $text = __( 'NEW', 'formidable' );
4827 }
4828 echo '<span class="frm-meta-tag frm-new-pill">' . esc_html( $text ) . '</span>';
4829 }
4830
4831 /**
4832 * Count the number of decimals digits.
4833 *
4834 * @since 5.2.07
4835 *
4836 * @param mixed $num Number.
4837 *
4838 * @return false|int Returns `false` if the passed parameter is not number.
4839 */
4840 public static function count_decimals( $num ) {
4841 if ( ! is_numeric( $num ) ) {
4842 return false;
4843 }
4844
4845 $num = (string) $num;
4846 $parts = explode( '.', $num );
4847
4848 if ( 1 === count( $parts ) ) {
4849 return 0;
4850 }
4851
4852 return strlen( $parts[ count( $parts ) - 1 ] );
4853 }
4854
4855 /**
4856 * Prevent a fatal error in PHP8 if gmt_offset happens to be set an empty string.
4857 * This is a bug in WordPress. It isn't safe to call current_time( 'timestamp' ) without this with an empty string offset.
4858 * In the future this might be safe to remove. Keep an eye on the current_time function in functions.php.
4859 *
4860 * @since 5.3.1
4861 *
4862 * @return void
4863 */
4864 public static function filter_gmt_offset() {
4865 if ( self::$added_gmt_offset_filter ) {
4866 // Avoid adding twice.
4867 return;
4868 }
4869
4870 add_filter(
4871 'option_gmt_offset',
4872 function ( $offset ) {
4873 if ( ! is_string( $offset ) || is_numeric( $offset ) ) {
4874 // Leave a valid value alone.
4875 return $offset;
4876 }
4877
4878 return 0;
4879 }
4880 );
4881 self::$added_gmt_offset_filter = true;
4882 }
4883
4884 /**
4885 * @since 5.3.1
4886 *
4887 * @return bool
4888 */
4889 public static function on_form_listing_page() {
4890 if ( ! self::is_admin_page( 'formidable' ) ) {
4891 return false;
4892 }
4893
4894 $action = self::simple_get( 'frm_action', 'sanitize_title' );
4895 return ! $action || in_array( $action, self::get_form_listing_page_actions(), true );
4896 }
4897
4898 /**
4899 * Get all actions that also display the forms list.
4900 *
4901 * @since 5.3.1
4902 *
4903 * @return array<string>
4904 */
4905 private static function get_form_listing_page_actions() {
4906 return array( 'list', 'trash', 'untrash', 'destroy' );
4907 }
4908
4909 /**
4910 * Safely call get_plugins, importing the required files if they are not yet loaded.
4911 *
4912 * @since 5.5
4913 *
4914 * @return array
4915 */
4916 public static function get_plugins() {
4917 if ( ! function_exists( 'get_plugins' ) ) {
4918 require_once ABSPATH . 'wp-admin/includes/plugin.php';
4919 }
4920 return get_plugins();
4921 }
4922
4923 /**
4924 * Make sure that the file we're trying to load is in fact the expected file type, and that it's coming from our S3 bucket.
4925 * This is to make sure that the URL can't be exploited for a SSRF attack.
4926 *
4927 * @since 5.5.5
4928 *
4929 * @param string $url
4930 * @param string $expected_extension
4931 *
4932 * @return bool
4933 */
4934 public static function validate_url_is_in_s3_bucket( $url, $expected_extension ) {
4935 $file_is_in_expected_s3_bucket = str_starts_with( $url, 'https://s3.amazonaws.com/fp.strategy11.com' );
4936
4937 if ( ! $file_is_in_expected_s3_bucket ) {
4938 return false;
4939 }
4940
4941 $parsed = parse_url( $url );
4942
4943 if ( ! is_array( $parsed ) ) {
4944 // URL is malformed.
4945 return false;
4946 }
4947
4948 $path = $parsed['path'];
4949 $ext = pathinfo( $path, PATHINFO_EXTENSION );
4950 // The URL isn't to an XML file.
4951 return $expected_extension === $ext;
4952 }
4953
4954 /**
4955 * Display a dismissable warning message and save its dismissal state.
4956 *
4957 * @since 6.3
4958 *
4959 * @param string $message The warning message to display.
4960 * @param string $option The unique identifier for the dismissal state of the message and the WP Ajax action.
4961 *
4962 * @return void
4963 */
4964 public static function add_dismissable_warning_message( $message = '', $option = '' ) {
4965 if ( ! $message || ! $option ) {
4966 return;
4967 }
4968
4969 $ajax_callback = function () use ( $option ) {
4970 self::dismiss_warning_message( $option );
4971 };
4972
4973 // We're handling JS codes with `doJsonPost` and it adds 'frm_' to the beginning of the action.
4974 // To prevent any issues, we add 'frm_' from the beginning of the action.
4975 add_action( 'wp_ajax_frm_' . $option, $ajax_callback );
4976
4977 add_filter(
4978 'frm_message_list',
4979 function ( $show_messages ) use ( $message, $option ) {
4980 if ( get_option( $option, false ) ) {
4981 return $show_messages;
4982 }
4983
4984 $dismiss_icon = self::icon_by_class(
4985 'frmfont frm_close_icon',
4986 array(
4987 'aria-label' => _x( 'Dismiss', 'warning message: close icon label', 'formidable' ),
4988 'echo' => false,
4989 )
4990 );
4991
4992 $show_messages[] = $message;
4993 $show_messages[] = '<span class="frm-warning-dismiss frmsvg" data-action="' . esc_attr( $option ) . '">' . $dismiss_icon . '</span>';
4994
4995 return $show_messages;
4996 }
4997 );
4998 }
4999
5000 /**
5001 * Dismiss a warning message and update the dismissal state.
5002 *
5003 * @since 6.3
5004 *
5005 * @param string $option The unique identifier for the dismissal state of the message.
5006 *
5007 * @return void
5008 */
5009 public static function dismiss_warning_message( $option = '' ) {
5010 self::permission_check( 'frm_change_settings' );
5011 check_ajax_referer( 'frm_ajax', 'nonce' );
5012
5013 if ( $option ) {
5014 update_option( $option, true, false );
5015 }
5016
5017 wp_send_json_success();
5018 }
5019
5020 /**
5021 * Lite license copy.
5022 * Used in FrmDashboardController & FrmSettingsController
5023 *
5024 * @since 6.8
5025 *
5026 * @return string
5027 */
5028 public static function copy_for_lite_license() {
5029 $message = __( 'You\'re using Formidable Forms Lite - no license needed. Enjoy!', 'formidable' ) . ' 🙂';
5030 return apply_filters( 'frm_license_type_text', $message );
5031 }
5032
5033 /**
5034 * Removes scripts that are unnecessarily loaded across the pages!
5035 *
5036 * @since 6.9
5037 *
5038 * @return void
5039 */
5040 public static function dequeue_extra_global_scripts() {
5041 wp_dequeue_script( 'frm-surveys-admin' );
5042 wp_dequeue_script( 'frm-quizzes-form-action' );
5043 }
5044
5045 /**
5046 * Shows tooltip icon.
5047 *
5048 * @since 6.12
5049 *
5050 * @param string $tooltip_text Tooltip text.
5051 * @param array $atts Tooltip wrapper HTML attributes.
5052 *
5053 * @return void
5054 */
5055 public static function tooltip_icon( $tooltip_text, $atts = array() ) {
5056 $atts['title'] = $tooltip_text;
5057
5058 if ( isset( $atts['class'] ) ) {
5059 $atts['class'] .= ' frm_help';
5060 } else {
5061 $atts['class'] = 'frm_help';
5062 }
5063 // phpcs:disable Generic.WhiteSpace.ScopeIndent
5064 ?>
5065 <span <?php self::array_to_html_params( $atts, true ); ?>>
5066 <?php self::icon_by_class( 'frmfont frm_tooltip_icon' ); ?>
5067 </span>
5068 <?php
5069 // phpcs:enable Generic.WhiteSpace.ScopeIndent
5070 }
5071
5072 /**
5073 * Prints errors for settings in onboarding wizard or template settings.
5074 *
5075 * @since 6.15
5076 *
5077 * @param array $args Args.
5078 *
5079 * @return void
5080 */
5081 public static function print_setting_error( $args ) {
5082 $args = wp_parse_args(
5083 $args,
5084 array(
5085 'id' => '',
5086 'errors' => array(),
5087 'class' => '',
5088 )
5089 );
5090
5091 $args['class'] .= ' frm-validation-error frm-mt-xs frm_hidden';
5092
5093 // phpcs:disable Generic.WhiteSpace.ScopeIndent
5094 ?>
5095 <span id="<?php echo esc_attr( $args['id'] ); ?>" class="<?php echo esc_attr( $args['class'] ); ?>">
5096 <?php
5097 if ( is_array( $args['errors'] ) ) {
5098 foreach ( $args['errors'] as $key => $msg ) {
5099 ?>
5100 <span frm-error="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $msg ); ?></span>
5101 <?php
5102 }
5103 } else {
5104 echo '<span>' . esc_html( $args['errors'] ) . '</span>';
5105 }
5106 ?>
5107 </span>
5108 <?php
5109 // phpcs:enable Generic.WhiteSpace.ScopeIndent
5110 }
5111
5112 /**
5113 * Check if GDPR is enabled.
5114 *
5115 * @since 6.19
5116 *
5117 * @return bool
5118 */
5119 public static function is_gdpr_enabled() {
5120 $frm_settings = self::get_settings();
5121 return $frm_settings->enable_gdpr || $frm_settings->no_ips || $frm_settings->custom_header_ip || $frm_settings->no_gdpr_cookies;
5122 }
5123
5124 /**
5125 * Check if GDPR cookies are disabled.
5126 *
5127 * @since 6.19
5128 *
5129 * @return bool
5130 */
5131 public static function no_gdpr_cookies() {
5132 $frm_settings = self::get_settings();
5133 return $frm_settings->enable_gdpr && $frm_settings->no_gdpr_cookies;
5134 }
5135
5136 /**
5137 * Check if a string is valid UTF-8.
5138 *
5139 * @since 6.24
5140 *
5141 * @param string|null $string The string to check.
5142 *
5143 * @return bool
5144 */
5145 public static function is_valid_utf8( $string ) {
5146 if ( is_null( $string ) ) {
5147 // Return true so we do not attempt to change encoding on a null value.
5148 return true;
5149 }
5150
5151 // wp_is_valid_utf8 is added in WP 6.9.
5152 if ( function_exists( 'wp_is_valid_utf8' ) ) {
5153 return wp_is_valid_utf8( $string );
5154 }
5155
5156 // As of WP 6.9, seems_utf8 is deprecated.
5157 return function_exists( 'seems_utf8' ) ? seems_utf8( $string ) : false;
5158 }
5159
5160 /**
5161 * Get a documentation URL with UTM parameters and affiliate tracking.
5162 *
5163 * @since 6.26
5164 *
5165 * @param string $path The relative path to append to the base URL.
5166 * @param string $campaign The campaign to use for UTM parameters.
5167 * @param bool $add_kb_base Whether to prepend 'knowledgebase/' to the path. Default true.
5168 *
5169 * @return string The processed URL with UTM parameters and affiliate tracking.
5170 */
5171 public static function get_doc_url( $path, $campaign, $add_kb_base = true ) {
5172 $path = trim( $path, '/' );
5173
5174 if ( $add_kb_base ) {
5175 $path = 'knowledgebase/' . $path;
5176 }
5177
5178 return self::maybe_add_missing_utm( 'https://formidableforms.com/' . $path, array( 'campaign' => $campaign ) );
5179 }
5180
5181 /**
5182 * @since 5.0.16
5183 * @deprecated 6.26
5184 *
5185 * @return bool
5186 */
5187 public static function show_landing_pages() {
5188 _deprecated_function( __METHOD__, '6.26' );
5189 return true;
5190 }
5191 }
5192