PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.32.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.32.1
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 6.32.1, at classes/helpers/FrmAppHelper.php

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