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

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