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

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