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

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

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