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

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

1,491 lines 40.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmStylesController {
7
8 /**
9 * @var string
10 */
11 public static $post_type = 'frm_styles';
12
13 /**
14 * @var string
15 */
16 public static $screen = 'formidable_page_formidable-styles';
17
18 /**
19 * @var string|null
20 */
21 private static $message;
22
23 /**
24 * @return void
25 */
26 public static function load_pro_hooks() {
27 if ( FrmAppHelper::pro_is_installed() ) {
28 FrmProStylesController::load_pro_hooks();
29 }
30 }
31
32 /**
33 * @return void
34 */
35 public static function register_post_types() {
36 register_post_type(
37 self::$post_type,
38 array(
39 'label' => __( 'Styles', 'formidable' ),
40 'public' => false,
41 'show_ui' => false,
42 'capability_type' => 'page',
43 'capabilities' => array(
44 'edit_post' => 'frm_change_settings',
45 'edit_posts' => 'frm_change_settings',
46 'edit_others_posts' => 'frm_change_settings',
47 'publish_posts' => 'frm_change_settings',
48 'delete_post' => 'frm_change_settings',
49 'delete_posts' => 'frm_change_settings',
50 'read_private_posts' => 'read_private_posts',
51 ),
52 'supports' => array(
53 'title',
54 ),
55 'has_archive' => false,
56 'labels' => array(
57 'name' => __( 'Styles', 'formidable' ),
58 'singular_name' => __( 'Style', 'formidable' ),
59 'menu_name' => __( 'Style', 'formidable' ),
60 'edit' => __( 'Edit', 'formidable' ),
61 'add_new_item' => __( 'Create a New Style', 'formidable' ),
62 'edit_item' => __( 'Edit Style', 'formidable' ),
63 ),
64 )
65 );
66 }
67
68 /**
69 * Add two links for the visual styler.
70 * There's a "Styles" submenu in the Formidable menu.
71 * There's a second alternative "Forms" submenu in the Appearance menu.
72 * This submenu links to a page to edit a form with the default style.
73 *
74 * @return void
75 */
76 public static function menu() {
77 add_submenu_page( 'formidable', 'Formidable | ' . __( 'Styles', 'formidable' ), __( 'Styles', 'formidable' ), 'frm_change_settings', 'formidable-styles', 'FrmStylesController::route' );
78 add_submenu_page( 'themes.php', 'Formidable | ' . __( 'Styles', 'formidable' ), __( 'Forms', 'formidable' ), 'frm_change_settings', 'formidable-styles2', 'FrmStylesController::route' );
79 }
80
81 /**
82 * Remove filters for the visual styler preview.
83 * This triggers earlier than self::admin_init which is called too late.
84 *
85 * @return void
86 */
87 public static function plugins_loaded() {
88 if ( ! FrmAppHelper::is_style_editor_page() ) {
89 return;
90 }
91
92 self::disable_form_css();
93 self::prevent_form_scripts_from_loading();
94 }
95
96 /**
97 * Avoid loading CSS the normal way with the preview in the styler.
98 * It gets loaded instead with the frmpro_css action set to the #frm-custom-theme-css element.
99 *
100 * @since 6.0
101 *
102 * @return void
103 */
104 private static function disable_form_css() {
105 add_filter( 'get_frm_stylesheet', '__return_empty_array' );
106 }
107
108 /**
109 * Removing this action prevents front end JavaScript from loading.
110 *
111 * @since 6.0
112 *
113 * @return void
114 */
115 private static function prevent_form_scripts_from_loading() {
116 remove_action( 'init', 'FrmFormsController::front_head' );
117 }
118
119 /**
120 * @return void
121 */
122 public static function admin_init() {
123 self::maybe_hook_into_global_settings_save();
124
125 if ( ! FrmAppHelper::is_style_editor_page() ) {
126 return;
127 }
128
129 FrmStyleComponent::register_assets();
130 self::load_pro_hooks();
131
132 $version = FrmAppHelper::plugin_version();
133
134 if ( FrmAppHelper::is_style_editor_page( 'edit' ) ) {
135 wp_enqueue_style( 'wp-color-picker' );
136 }
137
138 wp_enqueue_style( 'frm-custom-theme', admin_url( 'admin-ajax.php?action=frmpro_css' ), array(), $version );
139
140 $style = apply_filters( 'frm_style_head', false );
141
142 if ( $style ) {
143 wp_enqueue_style( 'frm-single-custom-theme', admin_url( 'admin-ajax.php?action=frmpro_load_css&flat=1' ) . '&' . http_build_query( $style->post_content ), array(), $version );
144 }
145 }
146
147 /**
148 * @since 6.0
149 *
150 * @return void
151 */
152 private static function maybe_hook_into_global_settings_save() {
153 if ( empty( $_POST ) || ! isset( $_POST['style'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
154 // Avoid changing any style data if the style array is not sent in the request.
155 return;
156 }
157
158 add_action(
159 'frm_update_settings',
160 /**
161 * Update the form data on the "Manage Styles" tab after global settings are saved.
162 */
163 function () {
164 self::manage_styles();
165 }
166 );
167 }
168
169 /**
170 * @param string $register Either 'enqueue' or 'register'.
171 * @param bool $force True to enqueue/register the style if a form has not been loaded.
172 *
173 * @return void
174 */
175 public static function enqueue_css( $register = 'enqueue', $force = false ) {
176 global $frm_vars;
177
178 $register_css = $register === 'register';
179 $should_load = $force || ( ( $frm_vars['load_css'] || $register_css ) && ! FrmAppHelper::is_admin() );
180
181 if ( ! $should_load ) {
182 return;
183 }
184
185 $frm_settings = FrmAppHelper::get_settings();
186
187 if ( $frm_settings->load_style === 'none' ) {
188 return;
189 }
190
191 $css = apply_filters( 'get_frm_stylesheet', self::custom_stylesheet() );
192
193 if ( $css ) {
194 $css = (array) $css;
195 $version = FrmAppHelper::plugin_version();
196
197 foreach ( $css as $css_key => $file ) {
198 if ( $register_css ) {
199 $this_version = self::get_css_version( $css_key, $version );
200 wp_register_style( $css_key, $file, array(), $this_version );
201 }
202
203 $load_on_all = ! FrmAppHelper::is_admin() && 'all' === $frm_settings->load_style;
204
205 if ( $load_on_all || $register !== 'register' ) {
206 wp_enqueue_style( $css_key );
207 }
208 unset( $css_key, $file );
209 }
210
211 if ( $frm_settings->load_style === 'all' ) {
212 $frm_vars['css_loaded'] = true;
213 }
214 }//end if
215 unset( $css );
216
217 add_filter( 'style_loader_tag', 'FrmStylesController::add_tags_to_css', 10, 2 );
218 }
219
220 /**
221 * @return array<string,string>
222 */
223 public static function custom_stylesheet() {
224 global $frm_vars;
225 $stylesheet_urls = array();
226
227 if ( empty( $frm_vars['css_loaded'] ) ) {
228 // Include css in head.
229 self::get_url_to_custom_style( $stylesheet_urls );
230 }
231
232 return $stylesheet_urls;
233 }
234
235 /**
236 * @param array $stylesheet_urls
237 *
238 * @return void
239 */
240 private static function get_url_to_custom_style( &$stylesheet_urls ) {
241 $file_name = '/css/' . self::get_file_name();
242
243 if ( is_readable( FrmAppHelper::plugin_path() . $file_name ) ) {
244 $url = FrmAppHelper::plugin_url() . $file_name;
245 } else {
246 $url = admin_url( 'admin-ajax.php?action=frmpro_css' );
247 }
248 $stylesheet_urls['formidable'] = $url;
249 }
250
251 /**
252 * Use a different stylesheet per site in a multisite install
253 *
254 * @since 3.0.03
255 *
256 * @return string
257 */
258 public static function get_file_name() {
259 if ( is_multisite() ) {
260 $blog_id = get_current_blog_id();
261 $name = 'formidableforms' . absint( $blog_id ) . '.css';
262 } else {
263 $name = 'formidableforms.css';
264 }
265
266 return $name;
267 }
268
269 /**
270 * @param string $css_key
271 * @param string $version
272 *
273 * @return string
274 */
275 private static function get_css_version( $css_key, $version ) {
276 if ( 'formidable' === $css_key ) {
277 $this_version = get_option( 'frm_last_style_update' );
278
279 if ( ! $this_version ) {
280 $this_version = $version;
281 }
282 } else {
283 $this_version = $version;
284 }
285
286 return $this_version;
287 }
288
289 /**
290 * @param string $tag
291 * @param string $handle
292 *
293 * @return string
294 */
295 public static function add_tags_to_css( $tag, $handle ) {
296 if ( ( 'formidable' === $handle || 'jquery-theme' === $handle ) && strpos( $tag, ' property=' ) === false ) {
297 $frm_settings = FrmAppHelper::get_settings();
298 $tag = str_replace( ' type="', ' property="stylesheet" type="', $tag );
299 }
300
301 return $tag;
302 }
303
304 /**
305 * Route the edit route to the style function.
306 *
307 * @since 6.0 this function no longer has any parameter values.
308 *
309 * @return void
310 */
311 public static function edit() {
312 self::load_styler();
313 }
314
315 /**
316 * When a new style route is hit, show a new style with the defaults. There is no ID yet, it is created after the first save event.
317 *
318 * @return void
319 */
320 public static function new_style() {
321 self::load_styler();
322 }
323
324 /**
325 * When the duplicate route is hit, it just shows the visual styler with a copy of the target style. There is no ID yet, it is created after the first save event.
326 *
327 * @return void
328 */
329 public static function duplicate() {
330 self::load_styler();
331 }
332
333 /**
334 * Render the style page for a form for assigning a style to a form, and for updating a target style.
335 *
336 * @since 6.0 this function no longer takes parameters.
337 *
338 * @return void
339 */
340 public static function load_styler() {
341 if ( 'assign_style' === FrmAppHelper::get_post_param( 'frm_action' ) ) {
342 self::save_form_style();
343 }
344
345 self::setup_styles_and_scripts_for_styler();
346
347 $style_id = self::get_style_id_for_styler();
348
349 if ( ! $style_id ) {
350 $error_args = array(
351 'title' => __( 'No styles', 'formidable' ),
352 'body' => __( 'You must have a style to use the Visual Styler.', 'formidable' ),
353 'cancel_url' => admin_url( 'admin.php?page=formidable' ),
354 );
355 FrmAppController::show_error_modal( $error_args );
356 return;
357 }
358
359 $form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );
360
361 if ( ! $form_id ) {
362 $form_id = self::get_form_id_for_style( $style_id );
363 }
364
365 $form = FrmForm::getOne( $form_id );
366
367 if ( ! is_object( $form ) ) {
368 $error_args = array(
369 'title' => __( 'No forms', 'formidable' ),
370 'body' => __( 'You must have a form to use the Visual Styler.', 'formidable' ),
371 'cancel_url' => admin_url( 'admin.php?page=formidable' ),
372 );
373 FrmAppController::show_error_modal( $error_args );
374 return;
375 }
376
377 $frm_style = new FrmStyle( $style_id );
378 $active_style = $frm_style->get_one();
379 $default_style = self::get_default_style();
380
381 self::disable_admin_page_styling_on_submit_buttons();
382
383 /**
384 * @since 6.0
385 *
386 * @param array{form:\stdClass} $data
387 */
388 do_action( 'frm_before_render_style_page', compact( 'form' ) );
389
390 self::render_style_page( $active_style, $form, $default_style );
391 }
392
393 /**
394 * @since 6.0
395 *
396 * @return int
397 */
398 private static function get_style_id_for_styler() {
399 $action = FrmAppHelper::simple_get( 'frm_action' );
400
401 if ( 'duplicate' === $action ) {
402 // The duplicate action uses style_id instead of id for better backward compatibility.
403 return FrmAppHelper::simple_get( 'style_id', 'absint', 0 );
404 }
405
406 $style_id = FrmAppHelper::simple_get( 'id', 'absint', 0 );
407
408 if ( $style_id ) {
409 // Always use the style ID from the URL if one is specified.
410 return $style_id;
411 }
412
413 $request_form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );
414
415 if ( $request_form_id && is_callable( 'FrmProStylesController::get_active_style_for_form' ) ) {
416 return FrmProStylesController::get_active_style_for_form( $request_form_id )->ID;
417 }
418
419 return self::get_default_style()->ID;
420 }
421
422 /**
423 * If a form ID is not being passed in the URL, try to get the best match.
424 *
425 * @since 6.0
426 *
427 * @param int $style_id
428 *
429 * @return int
430 */
431 private static function get_form_id_for_style( $style_id ) {
432 $check = serialize( array( 'custom_style' => (string) $style_id ) );
433 $check = substr( $check, 5, -1 );
434 $form_id = FrmDb::get_var(
435 'frm_forms',
436 array(
437 'options LIKE' => $check,
438 'status' => 'published',
439 )
440 );
441
442 if ( ! $form_id ) {
443 // TODO: Show a message why a random form is being shown (because no form is assigned to the style).
444 // Fallback to any form.
445 $where = array(
446 'status' => 'published',
447 // Make sure it's not a repeater.
448 'parent_form_id' => array( null, 0 ),
449 );
450 $form_id = FrmDb::get_var( 'frm_forms', $where, 'id' );
451 }
452
453 return $form_id;
454 }
455
456 /**
457 * Add a frm_no_style_button class to all buttons to avoid some style rules like border-radius: 30px.
458 *
459 * @return void
460 */
461 private static function disable_admin_page_styling_on_submit_buttons() {
462 add_filter(
463 'frm_submit_button_class',
464 function ( $classes ) {
465 $classes[] = 'frm_no_style_button';
466 return $classes;
467 }
468 );
469 }
470
471 /**
472 * @since 6.0
473 *
474 * @return WP_Post
475 */
476 private static function get_default_style() {
477 $frm_style = new FrmStyle( 'default' );
478 $default_style = $frm_style->get_one();
479 return $default_style;
480 }
481
482 /**
483 * Save style for form (from Styler list page) via a POST action.
484 *
485 * @since 6.0
486 *
487 * @return void
488 */
489 private static function save_form_style() {
490 $permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'frm_save_form_style', 'frm_save_form_style_nonce' );
491
492 if ( $permission_error !== false ) {
493 wp_die( 'Unable to save form', '', 403 );
494 }
495
496 $style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
497
498 if ( $style_id && ! self::confirm_style_exists_before_setting( $style_id ) ) {
499 wp_die( esc_html__( 'Invalid target style', 'formidable' ), esc_html__( 'Invalid target style', 'formidable' ), 400 );
500 return;
501 }
502
503 /**
504 * Hook into the saved style ID so Pro can import a style template by its key and return a new style ID.
505 *
506 * @since 6.0
507 *
508 * @param int $style_id
509 */
510 $style_id = apply_filters( 'frm_saved_form_style_id', $style_id );
511
512 if ( ! $style_id && '0' !== FrmAppHelper::get_post_param( 'style_id', '', 'sanitize_text_field' ) ) {
513 // "0" is a special value used for the enable/disable toggle.
514 wp_die( esc_html__( 'Invalid style value', 'formidable' ), esc_html__( 'Invalid style value', 'formidable' ), 400 );
515 return;
516 }
517
518 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
519
520 if ( ! $form_id ) {
521 wp_die( esc_html__( 'No form specified', 'formidable' ), esc_html__( 'No form specified', 'formidable' ), 400 );
522 return;
523 }
524
525 $form = FrmForm::getOne( $form_id );
526
527 if ( ! $form ) {
528 wp_die( esc_html__( 'Form does not exist', 'formidable' ), esc_html__( 'Form does not exist', 'formidable' ), 400 );
529 return;
530 }
531
532 $previous_style_id = $form->options['custom_style'];
533
534 // If the default style is selected, use the "Always use default" legacy option instead of the default style.
535 // There's also a check here for conversational forms.
536 // Without the check it isn't possible to select "Default" because "Always use default" will convert to "Lines" dynamically.
537 $default_style = self::get_default_style();
538
539 if ( $style_id === $default_style->ID && empty( $form->options['chat'] ) ) {
540 $style_id = 1;
541 }
542
543 // We want to save a string for consistency. FrmStylesHelper::get_form_count_for_style expects the custom style ID is a string.
544 $form->options['custom_style'] = (string) $style_id;
545
546 if ( $previous_style_id === $form->options['custom_style'] ) {
547 // Exit early before updating DB if nothing actually changed.
548 self::$message = __( 'Successfully updated style.', 'formidable' );
549 return;
550 }
551
552 global $wpdb;
553 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );
554
555 FrmForm::clear_form_cache();
556
557 /**
558 * @since 6.25.1
559 *
560 * @param int $form_id
561 * @param int $style_id
562 */
563 do_action( 'frm_after_changed_form_style', absint( $form_id ), absint( $style_id ) );
564
565 self::$message = __( 'Successfully updated style.', 'formidable' );
566 }
567
568 /**
569 * Validate that we're assigning a form to a style that actually exists before assigning it to a form.
570 *
571 * @param int $style_id
572 *
573 * @return bool True if the style actually exists.
574 */
575 private static function confirm_style_exists_before_setting( $style_id ) {
576 global $wpdb;
577 $post_type = FrmDb::get_var( $wpdb->posts, array( 'ID' => $style_id ), 'post_type' );
578 return self::$post_type === $post_type;
579 }
580
581 /**
582 * Register and enqueue styles and scripts for the style tab page.
583 *
584 * @since 6.0
585 *
586 * @return void
587 */
588 private static function setup_styles_and_scripts_for_styler() {
589 $plugin_url = FrmAppHelper::plugin_url();
590 $version = FrmAppHelper::plugin_version();
591 $js_dependencies = array( 'wp-i18n', 'wp-hooks', 'formidable_dom' );
592
593 if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::use_jquery_datepicker' ) && FrmProAppHelper::use_jquery_datepicker() ) {
594 $js_dependencies[] = 'jquery-ui-datepicker';
595 }
596
597 wp_register_script( 'formidable_style', $plugin_url . '/js/admin/style.js', $js_dependencies, $version );
598 wp_register_style( 'formidable_style', $plugin_url . '/css/admin/style.css', array(), $version );
599 wp_print_styles( 'formidable_style' );
600
601 wp_print_styles( 'formidable' );
602 wp_enqueue_script( 'formidable_style' );
603 wp_set_script_translations( 'formidable_style', 'formidable' );
604 }
605
606 /**
607 * Render the style page (with a more limited and typed scope than calling it from self::style directly).
608 *
609 * @since 6.0
610 *
611 * @param stdClass|WP_Post $active_style
612 * @param stdClass $form
613 * @param WP_Post $default_style
614 *
615 * @return void
616 */
617 private static function render_style_page( $active_style, $form, $default_style ) {
618 $style_views_path = self::get_views_path();
619 // Edit, list (default), new_style.
620 $view = FrmAppHelper::simple_get( 'frm_action', 'sanitize_text_field', 'list' );
621 $frm_style = new FrmStyle( $active_style->ID );
622
623 if ( 'new_style' !== $view && ! FrmAppHelper::simple_get( 'form' ) && ! FrmAppHelper::simple_get( 'style_id' ) ) {
624 // Have the Appearance > Forms link fallback to the edit view. Otherwise we want to use 'list' as the default.
625 $view = 'edit';
626 }
627
628 if ( in_array( $view, array( 'edit', 'new_style', 'duplicate' ), true ) ) {
629 self::add_meta_boxes();
630 }
631
632 switch ( $view ) {
633 case 'edit':
634 $style = $active_style;
635 break;
636
637 case 'duplicate':
638 $style = clone $active_style;
639 $new_style = $frm_style->get_new();
640 $style->ID = $new_style->ID;
641 $style->post_name = $new_style->post_name;
642 unset( $new_style );
643 break;
644
645 case 'new_style':
646 $style = $frm_style->get_new();
647 break;
648 }
649
650 if ( in_array( $view, array( 'duplicate', 'new_style' ), true ) ) {
651 $style->post_title = FrmAppHelper::simple_get( 'style_name' );
652 $style->menu_order = 0;
653 }
654
655 if ( ! isset( $style ) ) {
656 $style = $active_style;
657 }
658
659 self::force_form_style( $style );
660
661 if ( isset( self::$message ) ) {
662 $message = self::$message;
663 }
664
665 $preview_helper = new FrmStylesPreviewHelper( $form->id );
666 $preview_helper->adjust_form_for_preview();
667
668 // Get form HTML before displaying warnings and notes so we can check global $frm_vars data without adding extra database calls.
669 $target_form_preview_html = $preview_helper->get_html_for_form_preview();
670 $warnings = $preview_helper->get_warnings_for_styler_preview( $style, $default_style, $view );
671 $notes = $preview_helper->get_notes_for_styler_preview();
672
673 include $style_views_path . 'show.php';
674 }
675
676 /**
677 * @since 6.0
678 *
679 * @return string
680 */
681 private static function get_views_path() {
682 return FrmAppHelper::plugin_path() . '/classes/views/styles/';
683 }
684
685 /**
686 * Filter form classes so the form uses the preview style, not the form's active style.
687 *
688 * @since 6.0
689 *
690 * @param stdClass|WP_Post $style A new style is not a WP_Post object.
691 *
692 * @return void
693 */
694 private static function force_form_style( $style ) {
695 add_filter(
696 'frm_add_form_style_class',
697 function ( $class ) use ( $style ) {
698 $split = array_filter(
699 explode( ' ', $class ),
700 /**
701 * @param string $class
702 */
703 function ( $class ) {
704 return $class && 0 !== strpos( $class, 'frm_style_' );
705 }
706 );
707 $split[] = 'frm_style_' . $style->post_name;
708 return implode( ' ', $split );
709 }
710 );
711 }
712
713 /**
714 * Save style post object via a POST request submitted from the Visual styler "edit" page.
715 *
716 * @since 6.0
717 *
718 * @return void
719 */
720 public static function save_style() {
721 $frm_style = new FrmStyle();
722 $post_id = FrmAppHelper::get_post_param( 'ID', false, 'sanitize_title' );
723 $style_nonce = FrmAppHelper::get_post_param( 'frm_style', '', 'sanitize_text_field' );
724
725 if ( $post_id === false || ! wp_verify_nonce( $style_nonce, 'frm_style_nonce' ) ) {
726 // Exit early if the request isn't valid.
727 // Since we're not dying, it should just reload the visual styler without any message.
728 return;
729 }
730
731 $id = $frm_style->update( $post_id );
732
733 if ( ! $post_id && $id ) {
734 self::maybe_redirect_after_save( $id );
735 // Set the post id to the new style so it will be loaded for editing.
736 $post_id = reset( $id );
737 }
738
739 /**
740 * @since 6.25.1
741 *
742 * @param int $post_id
743 */
744 do_action( 'frm_after_saved_style', absint( $post_id ) );
745
746 self::$message = __( 'Your styling settings have been saved.', 'formidable' );
747 }
748
749 /**
750 * Show the edit view after saving.
751 * The save event is triggered earlier, on admin init where self::save_style is called.
752 * This happens earlier because there is a possible redirect (to adjust the URL for a new or duplicated style).
753 *
754 * @return void
755 */
756 public static function save() {
757 self::edit();
758 }
759
760 /**
761 * Force a redirect after duplicating or creating a new style to avoid an old stale URL that could result in more styles than intended.
762 *
763 * @since 6.0
764 *
765 * @param array $ids
766 *
767 * @return void
768 */
769 private static function maybe_redirect_after_save( $ids ) {
770 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
771 $parsed = parse_url( $referer );
772 $query = $parsed['query'];
773
774 $current_action = false;
775 $actions_to_redirect = array( 'duplicate', 'new_style' );
776
777 foreach ( $actions_to_redirect as $action ) {
778 if ( false !== strpos( $query, 'frm_action=' . $action ) ) {
779 $current_action = $action;
780 break;
781 }
782 }
783
784 if ( false === $current_action ) {
785 // Do not redirect as the referer URL did not match $actions_to_redirect.
786 return;
787 }
788
789 parse_str( $query, $parsed_query );
790 $form_id = ! empty( $parsed_query['form'] ) ? absint( $parsed_query['form'] ) : 0;
791
792 $style = new stdClass();
793 $style->ID = end( $ids );
794 wp_safe_redirect( esc_url_raw( FrmStylesHelper::get_edit_url( $style, $form_id ) ) );
795 die();
796 }
797
798 /**
799 * @since 6.0
800 *
801 * @param string $message
802 * @param array|object $forms
803 *
804 * @return void
805 */
806 public static function manage( $message = '', $forms = array() ) {
807 $frm_style = new FrmStyle();
808 $styles = $frm_style->get_all();
809 $default_style = $frm_style->get_default_style( $styles );
810 $frm_settings = FrmAppHelper::get_settings();
811
812 if ( ! $forms ) {
813 $forms = FrmForm::get_published_forms();
814 }
815
816 include FrmAppHelper::plugin_path() . '/classes/views/styles/manage.php';
817 }
818
819 /**
820 * Handle saving for the page rendered in self::manage which is included in Global Settings in the "Manage Styles" tab.
821 * This gets called from the frm_update_settings hook which is called after saving Global settings.
822 *
823 * @return void
824 */
825 private static function manage_styles() {
826 global $wpdb;
827
828 $forms = FrmForm::get_published_forms();
829
830 foreach ( $forms as $form ) {
831 if ( ! isset( $_POST['style'] ) || ! isset( $_POST['style'][ $form->id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
832 continue;
833 }
834
835 $new_style = sanitize_text_field( wp_unslash( $_POST['style'][ $form->id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
836
837 $form->options['custom_style'] = $new_style;
838 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );
839 unset( $form );
840 }
841 }
842
843 /**
844 * Echo content for the Custom CSS page.
845 *
846 * @param string $message
847 * @param array $extra_args An array of extra arguments.
848 *
849 * @return void
850 */
851 public static function custom_css( $message = '', $extra_args = array() ) {
852 $id = $extra_args['id'] ?? 'frm_codemirror_box';
853 $settings = self::enqueue_codemirror( $id, $extra_args['placeholder'] ?? '' );
854 $id = $settings ? $id : 'frm_custom_css_box';
855 $show_errors = $extra_args['show_errors'] ?? true;
856 $custom_css = $extra_args['custom_css'] ?? self::get_custom_css();
857 $heading = $extra_args['heading'] ?? __( 'You can add custom css here or in your theme style.css. Any CSS added here will be used anywhere the Formidable CSS is loaded.', 'formidable' );
858 $textarea_params = ! empty( $extra_args['textarea_params'] ) ? $extra_args['textarea_params'] : array(
859 'name' => 'frm_custom_css',
860 'id' => $id,
861 );
862
863 if ( ! empty( $settings ) ) {
864 $textarea_params['class'] = 'hide-if-js';
865 }
866 include FrmAppHelper::plugin_path() . '/classes/views/styles/custom_css.php';
867 }
868
869 /**
870 * Get custom CSS code entered in the Custom CSS page.
871 *
872 * @since 6.0
873 *
874 * @param array|null $single_style_settings The single style settings.
875 *
876 * @return string
877 */
878 public static function get_custom_css( $single_style_settings = null ) {
879 // If the single style settings are passed, return the custom CSS from the single style settings.
880 if ( ! empty( $single_style_settings['single_style_custom_css'] ) && ! empty( $single_style_settings['enable_style_custom_css'] ) ) {
881 return $single_style_settings['single_style_custom_css'];
882 }
883
884 $settings = FrmAppHelper::get_settings();
885
886 if ( is_string( $settings->custom_css ) ) {
887 return $settings->custom_css;
888 }
889
890 // If it does not exist, check the default style as a fallback.
891 $frm_style = new FrmStyle();
892 $style = $frm_style->get_default_style();
893 $custom_css = $style->post_content['custom_css'];
894
895 return $custom_css;
896 }
897
898 /**
899 * Enqueue assets for codemirror, built into WordPress since 4.9.
900 * The Custom CSS page uses codemirror.
901 *
902 * @since 6.0 Previously this code was embedded in self::custom_css.
903 *
904 * @param string $id The ID of the codemirror box.
905 * @param string $placeholder The placeholder text for the codemirror box.
906 *
907 * @return array|false
908 */
909 private static function enqueue_codemirror( $id = 'frm_codemirror_box', $placeholder = '' ) {
910 if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
911 // The WordPress version is likely older than 4.9.
912 return false;
913 }
914
915 $settings = wp_enqueue_code_editor(
916 array(
917 'type' => 'text/css',
918 'codemirror' => array(
919 'indentUnit' => 2,
920 'tabSize' => 2,
921 // As the codemirror box only appears once you click into the Custom CSS tab, we need to auto-refresh.
922 // Otherwise the line numbers all end up with a 1px width causing overlap issues with the text in the content.
923 'autoRefresh' => true,
924 'placeholder' => $placeholder,
925 ),
926 )
927 );
928
929 if ( $settings ) {
930 wp_add_inline_script(
931 'code-editor',
932 sprintf(
933 'jQuery( function() { window.%s_wp_editor = wp.codeEditor.initialize( \'%s\', %s ); } );',
934 $id,
935 $id,
936 wp_json_encode( $settings )
937 )
938 );
939 }
940
941 return $settings;
942 }
943
944 /**
945 * Handling routing for the visual styler.
946 *
947 * @return void
948 */
949 public static function route() {
950 $action = FrmAppHelper::get_param( 'frm_action', '', 'get', 'sanitize_title' );
951 FrmAppHelper::include_svg();
952
953 switch ( $action ) {
954 case 'edit':
955 case 'save':
956 self::$action();
957 return;
958 default:
959 do_action( 'frm_style_action_route', $action );
960
961 if ( apply_filters( 'frm_style_stop_action_route', false, $action ) ) {
962 return;
963 }
964
965 if ( in_array( $action, array( 'new_style', 'duplicate' ), true ) ) {
966 self::$action();
967 return;
968 }
969
970 self::edit();
971 return;
972 }
973 }
974
975 /**
976 * Handle AJAX routing for frm_settings_reset for resetting styles to the default settings.
977 * From the edit view, it will return default styles and not actually update the style.
978 * On the list view, it does update the style immediately, and returns the default card style attributes so the style card can be reset as well.
979 *
980 * @since 6.0 When a style_id is passed to this action, the style will actually be reset.
981 *
982 * @return void
983 */
984 public static function reset_styling() {
985 FrmAppHelper::permission_check( 'frm_change_settings' );
986 check_ajax_referer( 'frm_ajax', 'nonce' );
987
988 $style_id = FrmAppHelper::get_post_param( 'style_id', '', 'absint' );
989
990 if ( ! $style_id ) {
991 // A style ID is not sent when resetting on the edit page.
992 // Instead of resetting the style, send the defaults back so the inputs can be updated with JavaScript.
993 $frm_style = new FrmStyle();
994 $defaults = $frm_style->get_defaults();
995 echo json_encode( $defaults );
996 wp_die();
997 }
998
999 $frm_style = new FrmStyle();
1000 $default_template_style = $frm_style->get_default_template_style( $style_id );
1001 $where = array(
1002 'ID' => $style_id,
1003 'post_type' => self::$post_type,
1004 );
1005
1006 $style_object = $frm_style->get_new();
1007 $style_object->post_content = json_decode( $default_template_style, true );
1008
1009 global $wpdb;
1010 $wpdb->update( $wpdb->posts, array( 'post_content' => $default_template_style ), $where );
1011
1012 // Save the settings after resetting to default or the old style will still appear.
1013 $frm_style->save_settings();
1014
1015 $data = array(
1016 'style' => FrmStylesCardHelper::get_style_param_for_card( $style_object ),
1017 );
1018 wp_send_json_success( $data );
1019 wp_die();
1020 }
1021
1022 /**
1023 * Handle routing for the frm_change_styling AJAX action.
1024 * This doesn't actually change styling. It just handles the events when someone changes a style.
1025 * It responds with the new CSS required for the updated styler preview in the edit page.
1026 *
1027 * @return void
1028 */
1029 public static function change_styling() {
1030 FrmAppHelper::permission_check( 'frm_change_settings' );
1031 check_ajax_referer( 'frm_ajax', 'nonce' );
1032
1033 $is_loaded_via_ajax = true;
1034 $frm_style = new FrmStyle();
1035
1036 // Intentionally avoid defaults here so nothing gets removed from our style.
1037 $defaults = array();
1038 $style = '';
1039
1040 echo '<style type="text/css">';
1041 include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
1042 echo '</style>';
1043 wp_die();
1044 }
1045
1046 /**
1047 * @return void
1048 */
1049 public static function add_meta_boxes() {
1050 // setup meta boxes
1051 $meta_boxes = array(
1052 'general' => __( 'General', 'formidable' ),
1053 'form-title' => __( 'Form Title', 'formidable' ),
1054 'form-description' => __( 'Form Description', 'formidable' ),
1055 'field-labels' => __( 'Field Labels', 'formidable' ),
1056 'field-description' => __( 'Field Description', 'formidable' ),
1057 'field-colors' => __( 'Field Colors', 'formidable' ),
1058 'field-sizes' => __( 'Field Settings', 'formidable' ),
1059 'check-box-radio-fields' => __( 'Check Box & Radio Fields', 'formidable' ),
1060 'buttons' => __( 'Buttons', 'formidable' ),
1061 'form-messages' => __( 'Form Messages', 'formidable' ),
1062 );
1063
1064 /**
1065 * Add custom boxes to the styling settings
1066 *
1067 * @since 2.3
1068 *
1069 * @param array $meta_boxes
1070 */
1071 $meta_boxes = apply_filters( 'frm_style_boxes', $meta_boxes );
1072
1073 foreach ( $meta_boxes as $nicename => $name ) {
1074 add_meta_box( $nicename . '-style', $name, 'FrmStylesController::include_style_section', self::$screen, 'side', 'default', $nicename );
1075 unset( $nicename, $name );
1076 }
1077 }
1078
1079 /**
1080 * @param array $atts
1081 * @param array $sec
1082 *
1083 * @return void
1084 */
1085 public static function include_style_section( $atts, $sec ) {
1086 extract( $atts ); // phpcs:ignore WordPress.PHP.DontExtract
1087 $style = $atts['style'];
1088 FrmStylesHelper::prepare_color_output( $style->post_content, false );
1089
1090 $current_tab = FrmAppHelper::simple_get( 'page-tab', 'sanitize_title', 'default' );
1091 $file_name = FrmAppHelper::plugin_path() . '/classes/views/styles/_' . $sec['args'] . '.php';
1092
1093 /**
1094 * Set the location of custom styling settings right before
1095 * loading onto the page. If your style box was named "progress",
1096 * this hook name will be frm_style_settings_progress.
1097 *
1098 * @since 2.3
1099 */
1100 $file_name = apply_filters( 'frm_style_settings_' . $sec['args'], $file_name );
1101
1102 echo '<div class="frm_grid_container">';
1103 include $file_name;
1104 echo '</div>';
1105 }
1106
1107 public static function load_css() {
1108 header( 'Content-type: text/css' );
1109
1110 $frm_style = new FrmStyle();
1111 $defaults = $frm_style->get_defaults();
1112 $style = '';
1113
1114 include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
1115 wp_die();
1116 }
1117
1118 /**
1119 * @return void
1120 */
1121 public static function load_saved_css() {
1122 $css = get_transient( 'frmpro_css' );
1123
1124 ob_start();
1125 include FrmAppHelper::plugin_path() . '/css/custom_theme.css.php';
1126 $output = ob_get_clean();
1127 $output = self::replace_relative_url( $output );
1128
1129 /**
1130 * The API needs to load font icons through a custom URL.
1131 *
1132 * @since 5.2
1133 *
1134 * @param string $output
1135 */
1136 $output = apply_filters( 'frm_saved_css', $output );
1137
1138 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1139
1140 self::maybe_hide_sample_form_error_message();
1141
1142 wp_die();
1143 }
1144
1145 /**
1146 * Add an extra style rule to hide a broken style warning.
1147 * To avoid cluttering the front end with any unnecessary styles this is only added when the referer URL matches the styler.
1148 *
1149 * @since 6.2.3
1150 *
1151 * @return void
1152 */
1153 public static function maybe_hide_sample_form_error_message() {
1154 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
1155
1156 if ( false !== strpos( $referer, 'admin.php?page=formidable-styles' ) ) {
1157 echo '#frm_broken_styles_warning { display: none; }';
1158 }
1159 }
1160
1161 /**
1162 * Replaces relative URL with absolute URL.
1163 *
1164 * @since 4.11.03
1165 *
1166 * @param string $css CSS content.
1167 *
1168 * @return string
1169 */
1170 public static function replace_relative_url( $css ) {
1171 $plugin_url = trailingslashit( FrmAppHelper::plugin_url() );
1172 return str_replace(
1173 array(
1174 'url(../',
1175 "url('../",
1176 'url("../',
1177 ),
1178 array(
1179 'url(' . $plugin_url,
1180 "url('" . $plugin_url,
1181 'url("' . $plugin_url,
1182 ),
1183 $css
1184 );
1185 }
1186
1187 /**
1188 * Check if the Formidable styling should be loaded,
1189 * then enqueue it for the footer
1190 *
1191 * @since 2.0
1192 *
1193 * @return void
1194 */
1195 public static function enqueue_style() {
1196 global $frm_vars;
1197
1198 if ( isset( $frm_vars['css_loaded'] ) && $frm_vars['css_loaded'] ) {
1199 // The CSS has already been loaded.
1200 return;
1201 }
1202
1203 $frm_settings = FrmAppHelper::get_settings();
1204
1205 if ( $frm_settings->load_style !== 'none' ) {
1206 wp_enqueue_style( 'formidable' );
1207 $frm_vars['css_loaded'] = true;
1208 }
1209 }
1210
1211 /**
1212 * Get the stylesheets for the form settings page
1213 *
1214 * @return array<WP_Post>
1215 */
1216 public static function get_style_opts() {
1217 $frm_style = new FrmStyle();
1218 $styles = $frm_style->get_all();
1219
1220 return $styles;
1221 }
1222
1223 /**
1224 * Get the style post object for a target form.
1225 *
1226 * @param bool|object|string $form
1227 *
1228 * @return WP_Post|null
1229 */
1230 public static function get_form_style( $form = 'default' ) {
1231 $style = FrmFormsHelper::get_form_style( $form );
1232
1233 if ( empty( $style ) || 1 == $style ) {
1234 $style = 'default';
1235 }
1236
1237 $frm_style = new FrmStyle( $style );
1238 return $frm_style->get_one();
1239 }
1240
1241 /**
1242 * @param string $class
1243 * @param string $style
1244 *
1245 * @return string
1246 */
1247 public static function get_form_style_class( $class, $style ) {
1248 if ( 1 == $style ) {
1249 $style = 'default';
1250 }
1251
1252 $frm_style = new FrmStyle( $style );
1253 $style = $frm_style->get_one();
1254
1255 if ( $style ) {
1256 $class .= ' frm_style_' . $style->post_name;
1257 self::maybe_add_rtl_class( $style, $class );
1258 }
1259
1260 return $class;
1261 }
1262
1263 /**
1264 * @since 3.0
1265 *
1266 * @param object $style
1267 * @param string $class
1268 *
1269 * @return void
1270 */
1271 private static function maybe_add_rtl_class( $style, &$class ) {
1272 $is_rtl = isset( $style->post_content['direction'] ) && 'rtl' === $style->post_content['direction'];
1273
1274 if ( $is_rtl ) {
1275 $class .= ' frm_rtl';
1276 }
1277 }
1278
1279 /**
1280 * @param string $val Style setting key.
1281 * @param int|string $form Form ID or 'default'.
1282 *
1283 * @return mixed
1284 */
1285 public static function get_style_val( $val, $form = 'default' ) {
1286 $style = self::get_form_style( $form );
1287
1288 if ( $style && isset( $style->post_content[ $val ] ) ) {
1289 return $style->post_content[ $val ];
1290 }
1291 }
1292
1293 /**
1294 * @param array $default_styles
1295 *
1296 * @return array
1297 */
1298 public static function show_entry_styles( $default_styles ) {
1299 $frm_style = new FrmStyle( 'default' );
1300 $style = $frm_style->get_one();
1301
1302 if ( ! $style ) {
1303 return $default_styles;
1304 }
1305
1306 foreach ( $default_styles as $name => $val ) {
1307 $setting = $name;
1308
1309 if ( 'border_width' == $name ) {
1310 $setting = 'field_border_width';
1311 } elseif ( 'alt_bg_color' == $name ) {
1312 $setting = 'bg_color_active';
1313 }
1314 $default_styles[ $name ] = $style->post_content[ $setting ];
1315 unset( $name, $val );
1316 }
1317
1318 return $default_styles;
1319 }
1320
1321 /**
1322 * @param string $important
1323 * @param array $field
1324 *
1325 * @return string
1326 */
1327 public static function &important_style( $important, $field ) {
1328 $important = self::get_style_val( 'important_style', $field['form_id'] );
1329
1330 return $important;
1331 }
1332
1333 /**
1334 * Duplicate of WordPress do_accordion_section function, it adds an additional svg icon support.
1335 *
1336 * @since 6.8.3
1337 *
1338 * @param string|WP_Screen $screen Screen ID or screen object.
1339 * @param string $context Meta box context.
1340 * @param mixed $data_object Data object passed to callbacks.
1341 *
1342 * @return int
1343 */
1344 public static function do_accordion_sections( $screen, $context, $data_object ) {
1345 global $wp_meta_boxes;
1346
1347 // the symbol id from icons.svg
1348 $icon_ids = array(
1349 'ranking-fields-style' => 'frm_chart_bar_icon',
1350 'section-fields-style' => 'frm-form-title-style',
1351 );
1352
1353 wp_enqueue_script( 'accordion' );
1354
1355 if ( empty( $screen ) ) {
1356 $screen = get_current_screen();
1357 } elseif ( is_string( $screen ) ) {
1358 $screen = convert_to_screen( $screen );
1359 }
1360
1361 $page = $screen->id;
1362
1363 ?>
1364 <div id="side-sortables" class="accordion-container">
1365 <ul class="outer-border">
1366 <?php
1367 $i = 0;
1368 $first_open = false;
1369
1370 if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
1371 foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
1372 if ( isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
1373 foreach ( $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) {
1374 if ( false === $box || ! $box['title'] ) {
1375 continue;
1376 }
1377
1378 ++$i;
1379 $icon_id = array_key_exists( $box['id'], $icon_ids ) ? $icon_ids[ $box['id'] ] : 'frm-' . $box['id'];
1380
1381 $open_class = '';
1382
1383 if ( ! $first_open ) {
1384 $first_open = true;
1385 $open_class = 'open';
1386 }
1387
1388 $accordion_content_id = 'frm_style_section_' . $box['id'];
1389 ?>
1390 <li class="control-section accordion-section <?php echo esc_attr( $open_class ); ?> <?php echo esc_attr( $box['id'] ); ?>" id="<?php echo esc_attr( $box['id'] ); ?>">
1391 <h3 class="accordion-section-title hndle">
1392 <?php
1393 FrmAppHelper::icon_by_class( 'frmfont ' . $icon_id . ' frm_svg24' );
1394 echo esc_html( $box['title'] );
1395 ?>
1396 <button type="button" aria-expanded="<?php echo esc_attr( 'open' === $open_class ? 'true' : 'false' ); ?>" aria-controls="<?php echo esc_attr( $accordion_content_id ); ?>" aria-label="<?php echo esc_attr( $box['title'] ); ?>">
1397 <?php FrmAppHelper::icon_by_class( 'frmfont frm_arrowdown8_icon' ); ?>
1398 </button>
1399 </h3>
1400 <div class="accordion-section-content <?php postbox_classes( $box['id'], $page ); ?>" id="<?php echo esc_attr( $accordion_content_id ); ?>">
1401 <div class="inside">
1402 <?php call_user_func( $box['callback'], $data_object, $box ); ?>
1403 </div><!-- .inside -->
1404 </div><!-- .accordion-section-content -->
1405 </li><!-- .accordion-section -->
1406 <?php
1407 }//end foreach
1408 }//end if
1409 }//end foreach
1410 }//end if
1411 ?>
1412 </ul><!-- .outer-border -->
1413 </div><!-- .accordion-container -->
1414 <?php
1415 return $i;
1416 }
1417
1418 /**
1419 * Rename a style via an AJAX action.
1420 *
1421 * @since 6.0
1422 *
1423 * @return void
1424 */
1425 public static function rename_style() {
1426 $permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'nonce', 'frm_ajax' );
1427
1428 if ( $permission_error !== false ) {
1429 $data = array(
1430 'message' => __( 'Unable to rename style', 'formidable' ),
1431 );
1432 wp_send_json_error( $data, 403 );
1433 die();
1434 }
1435
1436 $style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
1437 $style_name = FrmAppHelper::get_post_param( 'style_name', '', 'sanitize_text_field' );
1438
1439 if ( ! $style_id || ! $style_name ) {
1440 $data = array(
1441 'message' => __( 'Invalid route', 'formidable' ),
1442 );
1443 wp_send_json_error( $data, 400 );
1444 die();
1445 }
1446
1447 $post = get_post( $style_id );
1448
1449 if ( ! $post || $post->post_type !== self::$post_type ) {
1450 $data = array(
1451 'message' => __( 'The style you are renaming either does not exist or it is not a style', 'formidable' ),
1452 );
1453 wp_send_json_error( $data, 404 );
1454 die();
1455 }
1456
1457 global $wpdb;
1458 $wpdb->update( $wpdb->posts, array( 'post_title' => $style_name ), array( 'ID' => $post->ID ) );
1459
1460 $data = array();
1461 wp_send_json_success( $data );
1462 }
1463
1464 /**
1465 * Prevent the WordPress edit.css file from loading on the visual styler page.
1466 * This way .form-field elements do not have border styles applied to them.
1467 *
1468 * @since 6.0
1469 *
1470 * @param WP_Styles $styles
1471 *
1472 * @return void
1473 */
1474 public static function disable_conflicting_wp_admin_css( $styles ) {
1475 if ( ! FrmAppHelper::is_style_editor_page() ) {
1476 return;
1477 }
1478
1479 FrmStylesPreviewHelper::disable_conflicting_wp_admin_css( $styles );
1480 }
1481
1482 /**
1483 * @deprecated 6.1 Saving custom CSS has been moved into Global Settings.
1484 *
1485 * @return void
1486 */
1487 public static function save_css() {
1488 _deprecated_function( __METHOD__, '6.1' );
1489 }
1490 }
1491