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

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