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

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

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