PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.8.2
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.8.2
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.8.2, at classes/controllers/FrmStylesController.php

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