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

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