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

1,363 lines 38.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
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 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_empty_array' );
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 FrmStyleComponent::register_assets();
124 self::load_pro_hooks();
125
126 $version = FrmAppHelper::plugin_version();
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 /**
269 * @param string $tag
270 * @param string $handle
271 * @return string
272 */
273 public static function add_tags_to_css( $tag, $handle ) {
274 if ( ( 'formidable' === $handle || 'jquery-theme' === $handle ) && strpos( $tag, ' property=' ) === false ) {
275 $frm_settings = FrmAppHelper::get_settings();
276 $tag = str_replace( ' type="', ' property="stylesheet" type="', $tag );
277 }
278
279 return $tag;
280 }
281
282 /**
283 * Route the edit route to the style function.
284 *
285 * @since 6.0 this function no longer has any parameter values.
286 *
287 * @return void
288 */
289 public static function edit() {
290 self::load_styler();
291 }
292
293 /**
294 * 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.
295 *
296 * @return void
297 */
298 public static function new_style() {
299 self::load_styler();
300 }
301
302 /**
303 * 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.
304 *
305 * @return void
306 */
307 public static function duplicate() {
308 self::load_styler();
309 }
310
311 /**
312 * Render the style page for a form for assigning a style to a form, and for updating a target style.
313 *
314 * @since 6.0 this function no longer takes parameters.
315 *
316 * @return void
317 */
318 public static function load_styler() {
319 if ( 'assign_style' === FrmAppHelper::get_post_param( 'frm_action' ) ) {
320 self::save_form_style();
321 }
322
323 self::setup_styles_and_scripts_for_styler();
324
325 $style_id = self::get_style_id_for_styler();
326 if ( ! $style_id ) {
327 $error_args = array(
328 'title' => __( 'No styles', 'formidable' ),
329 'body' => __( 'You must have a style to use the Visual Styler.', 'formidable' ),
330 'cancel_url' => admin_url( 'admin.php?page=formidable' ),
331 );
332 FrmAppController::show_error_modal( $error_args );
333 return;
334 }
335
336 $form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );
337 if ( ! $form_id ) {
338 $form_id = self::get_form_id_for_style( $style_id );
339 }
340
341 $form = FrmForm::getOne( $form_id );
342
343 if ( ! is_object( $form ) ) {
344 $error_args = array(
345 'title' => __( 'No forms', 'formidable' ),
346 'body' => __( 'You must have a form to use the Visual Styler.', 'formidable' ),
347 'cancel_url' => admin_url( 'admin.php?page=formidable' ),
348 );
349 FrmAppController::show_error_modal( $error_args );
350 return;
351 }
352
353 $frm_style = new FrmStyle( $style_id );
354 $active_style = $frm_style->get_one();
355 $default_style = self::get_default_style();
356
357 self::disable_admin_page_styling_on_submit_buttons();
358
359 /**
360 * @since 6.0
361 *
362 * @param array{form:\stdClass} $data
363 */
364 do_action( 'frm_before_render_style_page', compact( 'form' ) );
365
366 self::render_style_page( $active_style, $form, $default_style );
367 }
368
369 /**
370 * @since 6.0
371 *
372 * @return int
373 */
374 private static function get_style_id_for_styler() {
375 $action = FrmAppHelper::simple_get( 'frm_action' );
376 if ( 'duplicate' === $action ) {
377 // The duplicate action uses style_id instead of id for better backward compatibility.
378 return FrmAppHelper::simple_get( 'style_id', 'absint', 0 );
379 }
380
381 $style_id = FrmAppHelper::simple_get( 'id', 'absint', 0 );
382 if ( $style_id ) {
383 // Always use the style ID from the URL if one is specified.
384 return $style_id;
385 }
386
387 $request_form_id = FrmAppHelper::simple_get( 'form', 'absint', 0 );
388 if ( $request_form_id && is_callable( 'FrmProStylesController::get_active_style_for_form' ) ) {
389 return FrmProStylesController::get_active_style_for_form( $request_form_id )->ID;
390 }
391
392 return self::get_default_style()->ID;
393 }
394
395 /**
396 * If a form ID is not being passed in the URL, try to get the best match.
397 *
398 * @since 6.0
399 *
400 * @param int $style_id
401 * @return int
402 */
403 private static function get_form_id_for_style( $style_id ) {
404 $check = serialize( array( 'custom_style' => (string) $style_id ) );
405 $check = substr( $check, 5, -1 );
406 $form_id = FrmDb::get_var(
407 'frm_forms',
408 array(
409 'options LIKE' => $check,
410 'status' => 'published',
411 )
412 );
413
414 if ( ! $form_id ) {
415 // TODO: Show a message why a random form is being shown (because no form is assigned to the style).
416 // Fallback to any form.
417 $where = array(
418 'status' => 'published',
419 // Make sure it's not a repeater.
420 'parent_form_id' => array( null, 0 ),
421 );
422 $form_id = FrmDb::get_var( 'frm_forms', $where, 'id' );
423 }
424
425 return $form_id;
426 }
427
428 /**
429 * Add a frm_no_style_button class to all buttons to avoid some style rules like border-radius: 30px.
430 *
431 * @return void
432 */
433 private static function disable_admin_page_styling_on_submit_buttons() {
434 add_filter(
435 'frm_submit_button_class',
436 function ( $classes ) {
437 $classes[] = 'frm_no_style_button';
438 return $classes;
439 }
440 );
441 }
442
443 /**
444 * @since 6.0
445 *
446 * @return WP_Post
447 */
448 private static function get_default_style() {
449 $frm_style = new FrmStyle( 'default' );
450 $default_style = $frm_style->get_one();
451 return $default_style;
452 }
453
454 /**
455 * Save style for form (from Styler list page) via a POST action.
456 *
457 * @since 6.0
458 *
459 * @return void
460 */
461 private static function save_form_style() {
462 $permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'frm_save_form_style', 'frm_save_form_style_nonce' );
463 if ( $permission_error !== false ) {
464 wp_die( 'Unable to save form', '', 403 );
465 }
466
467 $style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
468 if ( $style_id && ! self::confirm_style_exists_before_setting( $style_id ) ) {
469 wp_die( esc_html__( 'Invalid target style', 'formidable' ), esc_html__( 'Invalid target style', 'formidable' ), 400 );
470 return;
471 }
472
473 /**
474 * Hook into the saved style ID so Pro can import a style template by its key and return a new style ID.
475 *
476 * @since 6.0
477 *
478 * @param int $style_id
479 */
480 $style_id = apply_filters( 'frm_saved_form_style_id', $style_id );
481
482 if ( ! $style_id && '0' !== FrmAppHelper::get_post_param( 'style_id', '', 'sanitize_text_field' ) ) {
483 // "0" is a special value used for the enable/disable toggle.
484 wp_die( esc_html__( 'Invalid style value', 'formidable' ), esc_html__( 'Invalid style value', 'formidable' ), 400 );
485 return;
486 }
487
488 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
489 if ( ! $form_id ) {
490 wp_die( esc_html__( 'No form specified', 'formidable' ), esc_html__( 'No form specified', 'formidable' ), 400 );
491 return;
492 }
493
494 $form = FrmForm::getOne( $form_id );
495 if ( ! $form ) {
496 wp_die( esc_html__( 'Form does not exist', 'formidable' ), esc_html__( 'Form does not exist', 'formidable' ), 400 );
497 return;
498 }
499
500 // If the default style is selected, use the "Always use default" legacy option instead of the default style.
501 // There's also a check here for conversational forms.
502 // Without the check it isn't possible to select "Default" because "Always use default" will convert to "Lines" dynamically.
503 $default_style = self::get_default_style();
504 if ( $style_id === $default_style->ID && empty( $form->options['chat'] ) ) {
505 $style_id = 1;
506 }
507
508 // We want to save a string for consistency. FrmStylesHelper::get_form_count_for_style expects the custom style ID is a string.
509 $form->options['custom_style'] = (string) $style_id;
510
511 global $wpdb;
512 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );
513
514 FrmForm::clear_form_cache();
515
516 self::$message = __( 'Successfully updated style.', 'formidable' );
517 }
518
519 /**
520 * Validate that we're assigning a form to a style that actually exists before assigning it to a form.
521 *
522 * @param int $style_id
523 * @return bool True if the style actually exists.
524 */
525 private static function confirm_style_exists_before_setting( $style_id ) {
526 global $wpdb;
527 $post_type = FrmDb::get_var( $wpdb->posts, array( 'ID' => $style_id ), 'post_type' );
528 return self::$post_type === $post_type;
529 }
530
531 /**
532 * Register and enqueue styles and scripts for the style tab page.
533 *
534 * @since 6.0
535 *
536 * @return void
537 */
538 private static function setup_styles_and_scripts_for_styler() {
539 $plugin_url = FrmAppHelper::plugin_url();
540 $version = FrmAppHelper::plugin_version();
541 $js_dependencies = array( 'wp-i18n', 'wp-hooks', 'formidable_dom' );
542
543 if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::use_jquery_datepicker' ) && FrmProAppHelper::use_jquery_datepicker() ) {
544 $js_dependencies[] = 'jquery-ui-datepicker';
545 }
546
547 wp_register_script( 'formidable_style', $plugin_url . '/js/admin/style.js', $js_dependencies, $version );
548 wp_register_style( 'formidable_style', $plugin_url . '/css/admin/style.css', array(), $version );
549 wp_print_styles( 'formidable_style' );
550
551 wp_print_styles( 'formidable' );
552 wp_enqueue_script( 'formidable_style' );
553 wp_set_script_translations( 'formidable_style', 'formidable' );
554 }
555
556 /**
557 * Render the style page (with a more limited and typed scope than calling it from self::style directly).
558 *
559 * @since 6.0
560 *
561 * @param stdClass|WP_Post $active_style
562 * @param stdClass $form
563 * @param WP_Post $default_style
564 * @return void
565 */
566 private static function render_style_page( $active_style, $form, $default_style ) {
567 $style_views_path = self::get_views_path();
568 // Edit, list (default), new_style.
569 $view = FrmAppHelper::simple_get( 'frm_action', 'sanitize_text_field', 'list' );
570 $frm_style = new FrmStyle( $active_style->ID );
571
572 if ( 'new_style' !== $view && ! FrmAppHelper::simple_get( 'form' ) && ! FrmAppHelper::simple_get( 'style_id' ) ) {
573 // Have the Appearance > Forms link fallback to the edit view. Otherwise we want to use 'list' as the default.
574 $view = 'edit';
575 }
576
577 if ( in_array( $view, array( 'edit', 'new_style', 'duplicate' ), true ) ) {
578 self::add_meta_boxes();
579 }
580
581 switch ( $view ) {
582 case 'edit':
583 $style = $active_style;
584 break;
585
586 case 'duplicate':
587 $style = clone $active_style;
588 $new_style = $frm_style->get_new();
589 $style->ID = $new_style->ID;
590 $style->post_name = $new_style->post_name;
591 unset( $new_style );
592 break;
593
594 case 'new_style':
595 $style = $frm_style->get_new();
596 break;
597 }
598
599 if ( in_array( $view, array( 'duplicate', 'new_style' ), true ) ) {
600 $style->post_title = FrmAppHelper::simple_get( 'style_name' );
601 $style->menu_order = 0;
602 }
603
604 if ( ! isset( $style ) ) {
605 $style = $active_style;
606 }
607
608 self::force_form_style( $style );
609
610 if ( isset( self::$message ) ) {
611 $message = self::$message;
612 }
613
614 $preview_helper = new FrmStylesPreviewHelper( $form->id );
615 $preview_helper->adjust_form_for_preview();
616
617 // Get form HTML before displaying warnings and notes so we can check global $frm_vars data without adding extra database calls.
618 $target_form_preview_html = $preview_helper->get_html_for_form_preview();
619 $warnings = $preview_helper->get_warnings_for_styler_preview( $style, $default_style, $view );
620 $notes = $preview_helper->get_notes_for_styler_preview();
621
622 include $style_views_path . 'show.php';
623 }
624
625 /**
626 * @since 6.0
627 *
628 * @return string
629 */
630 private static function get_views_path() {
631 return FrmAppHelper::plugin_path() . '/classes/views/styles/';
632 }
633
634 /**
635 * Filter form classes so the form uses the preview style, not the form's active style.
636 *
637 * @since 6.0
638 *
639 * @param stdClass|WP_Post $style A new style is not a WP_Post object.
640 * @return void
641 */
642 private static function force_form_style( $style ) {
643 add_filter(
644 'frm_add_form_style_class',
645 function ( $class ) use ( $style ) {
646 $split = array_filter(
647 explode( ' ', $class ),
648 /**
649 * @param string $class
650 */
651 function ( $class ) {
652 return $class && 0 !== strpos( $class, 'frm_style_' );
653 }
654 );
655 $split[] = 'frm_style_' . $style->post_name;
656 return implode( ' ', $split );
657 }
658 );
659 }
660
661 /**
662 * Save style post object via a POST request submitted from the Visual styler "edit" page.
663 *
664 * @since 6.0
665 *
666 * @return void
667 */
668 public static function save_style() {
669 $frm_style = new FrmStyle();
670 $post_id = FrmAppHelper::get_post_param( 'ID', false, 'sanitize_title' );
671 $style_nonce = FrmAppHelper::get_post_param( 'frm_style', '', 'sanitize_text_field' );
672
673 if ( $post_id === false || ! wp_verify_nonce( $style_nonce, 'frm_style_nonce' ) ) {
674 // Exit early if the request isn't valid.
675 // Since we're not dying, it should just reload the visual styler without any message.
676 return;
677 }
678
679 $id = $frm_style->update( $post_id );
680 if ( ! $post_id && $id ) {
681 self::maybe_redirect_after_save( $id );
682 // Set the post id to the new style so it will be loaded for editing.
683 $post_id = reset( $id );
684 }
685
686 self::$message = __( 'Your styling settings have been saved.', 'formidable' );
687 }
688
689 /**
690 * Show the edit view after saving.
691 * The save event is triggered earlier, on admin init where self::save_style is called.
692 * This happens earlier because there is a possible redirect (to adjust the URL for a new or duplicated style).
693 *
694 * @return void
695 */
696 public static function save() {
697 self::edit();
698 }
699
700 /**
701 * Force a redirect after duplicating or creating a new style to avoid an old stale URL that could result in more styles than intended.
702 *
703 * @since 6.0
704 *
705 * @param array $ids
706 * @return void
707 */
708 private static function maybe_redirect_after_save( $ids ) {
709 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
710 $parsed = parse_url( $referer );
711 $query = $parsed['query'];
712
713 $current_action = false;
714 $actions_to_redirect = array( 'duplicate', 'new_style' );
715 foreach ( $actions_to_redirect as $action ) {
716 if ( false !== strpos( $query, 'frm_action=' . $action ) ) {
717 $current_action = $action;
718 break;
719 }
720 }
721
722 if ( false === $current_action ) {
723 // Do not redirect as the referer URL did not match $actions_to_redirect.
724 return;
725 }
726
727 parse_str( $query, $parsed_query );
728 $form_id = ! empty( $parsed_query['form'] ) ? absint( $parsed_query['form'] ) : 0;
729
730 $style = new stdClass();
731 $style->ID = end( $ids );
732 wp_safe_redirect( esc_url_raw( FrmStylesHelper::get_edit_url( $style, $form_id ) ) );
733 die();
734 }
735
736 /**
737 * @since 6.0
738 *
739 * @param string $message
740 * @param array|object $forms
741 * @return void
742 */
743 public static function manage( $message = '', $forms = array() ) {
744 $frm_style = new FrmStyle();
745 $styles = $frm_style->get_all();
746 $default_style = $frm_style->get_default_style( $styles );
747 $frm_settings = FrmAppHelper::get_settings();
748
749 if ( ! $forms ) {
750 $forms = FrmForm::get_published_forms();
751 }
752
753 include FrmAppHelper::plugin_path() . '/classes/views/styles/manage.php';
754 }
755
756 /**
757 * Handle saving for the page rendered in self::manage which is included in Global Settings in the "Manage Styles" tab.
758 * This gets called from the frm_update_settings hook which is called after saving Global settings.
759 *
760 * @return void
761 */
762 private static function manage_styles() {
763 global $wpdb;
764
765 $forms = FrmForm::get_published_forms();
766 foreach ( $forms as $form ) {
767 if ( ! isset( $_POST['style'] ) || ! isset( $_POST['style'][ $form->id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
768 continue;
769 }
770
771 $new_style = sanitize_text_field( wp_unslash( $_POST['style'][ $form->id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
772
773 $form->options['custom_style'] = $new_style;
774 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $form->options ) ), array( 'id' => $form->id ) );
775 unset( $form );
776 }
777 }
778
779 /**
780 * Echo content for the Custom CSS page.
781 *
782 * @param string $message
783 * @return void
784 */
785 public static function custom_css( $message = '' ) {
786 $settings = self::enqueue_codemirror();
787 $id = $settings ? 'frm_codemirror_box' : 'frm_custom_css_box';
788 $custom_css = self::get_custom_css();
789
790 include FrmAppHelper::plugin_path() . '/classes/views/styles/custom_css.php';
791 }
792
793 /**
794 * Get custom CSS code entered in the Custom CSS page.
795 *
796 * @since 6.0
797 *
798 * @return string
799 */
800 public static function get_custom_css() {
801 $settings = FrmAppHelper::get_settings();
802 if ( is_string( $settings->custom_css ) ) {
803 return $settings->custom_css;
804 }
805
806 // If it does not exist, check the default style as a fallback.
807 $frm_style = new FrmStyle();
808 $style = $frm_style->get_default_style();
809 $custom_css = $style->post_content['custom_css'];
810
811 return $custom_css;
812 }
813
814 /**
815 * Enqueue assets for codemirror, built into WordPress since 4.9.
816 * The Custom CSS page uses codemirror.
817 *
818 * @since 6.0 Previously this code was embedded in self::custom_css.
819 *
820 * @return array|false
821 */
822 private static function enqueue_codemirror() {
823 if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
824 // The WordPress version is likely older than 4.9.
825 return false;
826 }
827
828 $settings = wp_enqueue_code_editor(
829 array(
830 'type' => 'text/css',
831 'codemirror' => array(
832 'indentUnit' => 2,
833 'tabSize' => 2,
834 // As the codemirror box only appears once you click into the Custom CSS tab, we need to auto-refresh.
835 // Otherwise the line numbers all end up with a 1px width causing overlap issues with the text in the content.
836 'autoRefresh' => true,
837 ),
838 )
839 );
840
841 if ( $settings ) {
842 wp_add_inline_script(
843 'code-editor',
844 sprintf(
845 'jQuery( function() { wp.codeEditor.initialize( \'frm_codemirror_box\', %s ); } );',
846 wp_json_encode( $settings )
847 )
848 );
849 }
850
851 return $settings;
852 }
853
854 /**
855 * Handling routing for the visual styler.
856 *
857 * @return void
858 */
859 public static function route() {
860 $action = FrmAppHelper::get_param( 'frm_action', '', 'get', 'sanitize_title' );
861 FrmAppHelper::include_svg();
862
863 switch ( $action ) {
864 case 'edit':
865 case 'save':
866 self::$action();
867 return;
868 default:
869 do_action( 'frm_style_action_route', $action );
870
871 if ( apply_filters( 'frm_style_stop_action_route', false, $action ) ) {
872 return;
873 }
874
875 if ( in_array( $action, array( 'new_style', 'duplicate' ), true ) ) {
876 self::$action();
877 return;
878 }
879
880 self::edit();
881 return;
882 }
883 }
884
885 /**
886 * Handle AJAX routing for frm_settings_reset for resetting styles to the default settings.
887 * From the edit view, it will return default styles and not actually update the style.
888 * 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.
889 *
890 * @since 6.0 When a style_id is passed to this action, the style will actually be reset.
891 *
892 * @return void
893 */
894 public static function reset_styling() {
895 FrmAppHelper::permission_check( 'frm_change_settings' );
896 check_ajax_referer( 'frm_ajax', 'nonce' );
897
898 $style_id = FrmAppHelper::get_post_param( 'style_id', '', 'absint' );
899 if ( ! $style_id ) {
900 // A style ID is not sent when resetting on the edit page.
901 // Instead of resetting the style, send the defaults back so the inputs can be updated with JavaScript.
902 $frm_style = new FrmStyle();
903 $defaults = $frm_style->get_defaults();
904 echo json_encode( $defaults );
905 wp_die();
906 }
907
908 $frm_style = new FrmStyle();
909 $default_template_style = $frm_style->get_default_template_style( $style_id );
910 $where = array(
911 'ID' => $style_id,
912 'post_type' => self::$post_type,
913 );
914
915 $style_object = $frm_style->get_new();
916 $style_object->post_content = json_decode( $default_template_style, true );
917
918 global $wpdb;
919 $wpdb->update( $wpdb->posts, array( 'post_content' => $default_template_style ), $where );
920
921 // Save the settings after resetting to default or the old style will still appear.
922 $frm_style->save_settings();
923
924 $data = array(
925 'style' => FrmStylesCardHelper::get_style_param_for_card( $style_object ),
926 );
927 wp_send_json_success( $data );
928 wp_die();
929 }
930
931 /**
932 * Handle routing for the frm_change_styling AJAX action.
933 * This doesn't actually change styling. It just handles the events when someone changes a style.
934 * It responds with the new CSS required for the updated styler preview in the edit page.
935 *
936 * @return void
937 */
938 public static function change_styling() {
939 FrmAppHelper::permission_check( 'frm_change_settings' );
940 check_ajax_referer( 'frm_ajax', 'nonce' );
941
942 $is_loaded_via_ajax = true;
943 $frm_style = new FrmStyle();
944
945 // Intentionally avoid defaults here so nothing gets removed from our style.
946 $defaults = array();
947 $style = '';
948
949 echo '<style type="text/css">';
950 include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
951 echo '</style>';
952 wp_die();
953 }
954
955 /**
956 * @return void
957 */
958 public static function add_meta_boxes() {
959 // setup meta boxes
960 $meta_boxes = array(
961 'general' => __( 'General', 'formidable' ),
962 'form-title' => __( 'Form Title', 'formidable' ),
963 'form-description' => __( 'Form Description', 'formidable' ),
964 'field-labels' => __( 'Field Labels', 'formidable' ),
965 'field-description' => __( 'Field Description', 'formidable' ),
966 'field-colors' => __( 'Field Colors', 'formidable' ),
967 'field-sizes' => __( 'Field Settings', 'formidable' ),
968 'check-box-radio-fields' => __( 'Check Box & Radio Fields', 'formidable' ),
969 'buttons' => __( 'Buttons', 'formidable' ),
970 'form-messages' => __( 'Form Messages', 'formidable' ),
971 );
972
973 /**
974 * Add custom boxes to the styling settings
975 *
976 * @since 2.3
977 *
978 * @param array $meta_boxes
979 */
980 $meta_boxes = apply_filters( 'frm_style_boxes', $meta_boxes );
981
982 foreach ( $meta_boxes as $nicename => $name ) {
983 add_meta_box( $nicename . '-style', $name, 'FrmStylesController::include_style_section', self::$screen, 'side', 'default', $nicename );
984 unset( $nicename, $name );
985 }
986 }
987
988 /**
989 * @param array $atts
990 * @param array $sec
991 * @return void
992 */
993 public static function include_style_section( $atts, $sec ) {
994 extract( $atts ); // phpcs:ignore WordPress.PHP.DontExtract
995 $style = $atts['style'];
996 FrmStylesHelper::prepare_color_output( $style->post_content, false );
997
998 $current_tab = FrmAppHelper::simple_get( 'page-tab', 'sanitize_title', 'default' );
999 $file_name = FrmAppHelper::plugin_path() . '/classes/views/styles/_' . $sec['args'] . '.php';
1000
1001 /**
1002 * Set the location of custom styling settings right before
1003 * loading onto the page. If your style box was named "progress",
1004 * this hook name will be frm_style_settings_progress.
1005 *
1006 * @since 2.3
1007 */
1008 $file_name = apply_filters( 'frm_style_settings_' . $sec['args'], $file_name );
1009
1010 echo '<div class="frm_grid_container">';
1011 include $file_name;
1012 echo '</div>';
1013 }
1014
1015 public static function load_css() {
1016 header( 'Content-type: text/css' );
1017
1018 $frm_style = new FrmStyle();
1019 $defaults = $frm_style->get_defaults();
1020 $style = '';
1021
1022 include FrmAppHelper::plugin_path() . '/css/_single_theme.css.php';
1023 wp_die();
1024 }
1025
1026 /**
1027 * @return void
1028 */
1029 public static function load_saved_css() {
1030 $css = get_transient( 'frmpro_css' );
1031
1032 ob_start();
1033 include FrmAppHelper::plugin_path() . '/css/custom_theme.css.php';
1034 $output = ob_get_clean();
1035 $output = self::replace_relative_url( $output );
1036
1037 /**
1038 * The API needs to load font icons through a custom URL.
1039 *
1040 * @since 5.2
1041 *
1042 * @param string $output
1043 */
1044 $output = apply_filters( 'frm_saved_css', $output );
1045
1046 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1047
1048 self::maybe_hide_sample_form_error_message();
1049
1050 wp_die();
1051 }
1052
1053 /**
1054 * Add an extra style rule to hide a broken style warning.
1055 * To avoid cluttering the front end with any unnecessary styles this is only added when the referer URL matches the styler.
1056 *
1057 * @since 6.2.3
1058 *
1059 * @return void
1060 */
1061 public static function maybe_hide_sample_form_error_message() {
1062 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
1063 if ( false !== strpos( $referer, 'admin.php?page=formidable-styles' ) ) {
1064 echo '#frm_broken_styles_warning { display: none; }';
1065 }
1066 }
1067
1068 /**
1069 * Replaces relative URL with absolute URL.
1070 *
1071 * @since 4.11.03
1072 *
1073 * @param string $css CSS content.
1074 * @return string
1075 */
1076 public static function replace_relative_url( $css ) {
1077 $plugin_url = trailingslashit( FrmAppHelper::plugin_url() );
1078 return str_replace(
1079 array(
1080 'url(../',
1081 "url('../",
1082 'url("../',
1083 ),
1084 array(
1085 'url(' . $plugin_url,
1086 "url('" . $plugin_url,
1087 'url("' . $plugin_url,
1088 ),
1089 $css
1090 );
1091 }
1092
1093 /**
1094 * Check if the Formidable styling should be loaded,
1095 * then enqueue it for the footer
1096 *
1097 * @since 2.0
1098 */
1099 public static function enqueue_style() {
1100 global $frm_vars;
1101
1102 if ( isset( $frm_vars['css_loaded'] ) && $frm_vars['css_loaded'] ) {
1103 // The CSS has already been loaded.
1104 return;
1105 }
1106
1107 $frm_settings = FrmAppHelper::get_settings();
1108 if ( $frm_settings->load_style !== 'none' ) {
1109 wp_enqueue_style( 'formidable' );
1110 $frm_vars['css_loaded'] = true;
1111 }
1112 }
1113
1114 /**
1115 * Get the stylesheets for the form settings page
1116 *
1117 * @return array<WP_Post>
1118 */
1119 public static function get_style_opts() {
1120 $frm_style = new FrmStyle();
1121 $styles = $frm_style->get_all();
1122
1123 return $styles;
1124 }
1125
1126 /**
1127 * Get the style post object for a target form.
1128 *
1129 * @param bool|object|string $form
1130 * @return WP_Post|null
1131 */
1132 public static function get_form_style( $form = 'default' ) {
1133 $style = FrmFormsHelper::get_form_style( $form );
1134
1135 if ( empty( $style ) || 1 == $style ) {
1136 $style = 'default';
1137 }
1138
1139 $frm_style = new FrmStyle( $style );
1140 return $frm_style->get_one();
1141 }
1142
1143 /**
1144 * @param string $class
1145 * @param string $style
1146 */
1147 public static function get_form_style_class( $class, $style ) {
1148 if ( 1 == $style ) {
1149 $style = 'default';
1150 }
1151
1152 $frm_style = new FrmStyle( $style );
1153 $style = $frm_style->get_one();
1154
1155 if ( $style ) {
1156 $class .= ' frm_style_' . $style->post_name;
1157 self::maybe_add_rtl_class( $style, $class );
1158 }
1159
1160 return $class;
1161 }
1162
1163 /**
1164 * @since 3.0
1165 * @param object $style
1166 * @param string $class
1167 */
1168 private static function maybe_add_rtl_class( $style, &$class ) {
1169 $is_rtl = isset( $style->post_content['direction'] ) && 'rtl' === $style->post_content['direction'];
1170 if ( $is_rtl ) {
1171 $class .= ' frm_rtl';
1172 }
1173 }
1174
1175 /**
1176 * @param string $val
1177 */
1178 public static function get_style_val( $val, $form = 'default' ) {
1179 $style = self::get_form_style( $form );
1180 if ( $style && isset( $style->post_content[ $val ] ) ) {
1181 return $style->post_content[ $val ];
1182 }
1183 }
1184
1185 public static function show_entry_styles( $default_styles ) {
1186 $frm_style = new FrmStyle( 'default' );
1187 $style = $frm_style->get_one();
1188
1189 if ( ! $style ) {
1190 return $default_styles;
1191 }
1192
1193 foreach ( $default_styles as $name => $val ) {
1194 $setting = $name;
1195 if ( 'border_width' == $name ) {
1196 $setting = 'field_border_width';
1197 } elseif ( 'alt_bg_color' == $name ) {
1198 $setting = 'bg_color_active';
1199 }
1200 $default_styles[ $name ] = $style->post_content[ $setting ];
1201 unset( $name, $val );
1202 }
1203
1204 return $default_styles;
1205 }
1206
1207 public static function &important_style( $important, $field ) {
1208 $important = self::get_style_val( 'important_style', $field['form_id'] );
1209
1210 return $important;
1211 }
1212
1213 /**
1214 * Duplicate of WordPress do_accordion_section function, it adds an additional svg icon support.
1215 *
1216 * @since 6.8.3
1217 *
1218 * @return int
1219 */
1220 public static function do_accordion_sections( $screen, $context, $data_object ) {
1221 global $wp_meta_boxes;
1222
1223 // the symbol id from icons.svg
1224 $icon_ids = array(
1225 'ranking-fields-style' => 'frm_chart_bar_icon',
1226 'section-fields-style' => 'frm-form-title-style',
1227 );
1228
1229 wp_enqueue_script( 'accordion' );
1230
1231 if ( empty( $screen ) ) {
1232 $screen = get_current_screen();
1233 } elseif ( is_string( $screen ) ) {
1234 $screen = convert_to_screen( $screen );
1235 }
1236
1237 $page = $screen->id;
1238
1239 ?>
1240 <div id="side-sortables" class="accordion-container">
1241 <ul class="outer-border">
1242 <?php
1243 $i = 0;
1244 $first_open = false;
1245
1246 if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
1247 foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
1248 if ( isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
1249 foreach ( $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) {
1250 if ( false === $box || ! $box['title'] ) {
1251 continue;
1252 }
1253
1254 ++$i;
1255 $icon_id = array_key_exists( $box['id'], $icon_ids ) ? $icon_ids[ $box['id'] ] : 'frm-' . $box['id'];
1256
1257 $open_class = '';
1258 if ( ! $first_open ) {
1259 $first_open = true;
1260 $open_class = 'open';
1261 }
1262
1263 $accordion_content_id = 'frm_style_section_' . $box['id'];
1264 ?>
1265 <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'] ); ?>">
1266 <h3 class="accordion-section-title hndle">
1267 <?php
1268 FrmAppHelper::icon_by_class( 'frmfont ' . $icon_id . ' frm_svg24' );
1269 echo esc_html( $box['title'] );
1270 ?>
1271 <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'] ); ?>">
1272 <?php FrmAppHelper::icon_by_class( 'frmfont frm_arrowdown8_icon' ); ?>
1273 </button>
1274 </h3>
1275 <div class="accordion-section-content <?php postbox_classes( $box['id'], $page ); ?>" id="<?php echo esc_attr( $accordion_content_id ); ?>">
1276 <div class="inside">
1277 <?php call_user_func( $box['callback'], $data_object, $box ); ?>
1278 </div><!-- .inside -->
1279 </div><!-- .accordion-section-content -->
1280 </li><!-- .accordion-section -->
1281 <?php
1282 }//end foreach
1283 }//end if
1284 }//end foreach
1285 }//end if
1286 ?>
1287 </ul><!-- .outer-border -->
1288 </div><!-- .accordion-container -->
1289 <?php
1290 return $i;
1291 }
1292
1293 /**
1294 * Rename a style via an AJAX action.
1295 *
1296 * @since 6.0
1297 *
1298 * @return void
1299 */
1300 public static function rename_style() {
1301 $permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'nonce', 'frm_ajax' );
1302 if ( $permission_error !== false ) {
1303 $data = array(
1304 'message' => __( 'Unable to rename style', 'formidable' ),
1305 );
1306 wp_send_json_error( $data, 403 );
1307 die();
1308 }
1309
1310 $style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
1311 $style_name = FrmAppHelper::get_post_param( 'style_name', '', 'sanitize_text_field' );
1312
1313 if ( ! $style_id || ! $style_name ) {
1314 $data = array(
1315 'message' => __( 'Invalid route', 'formidable' ),
1316 );
1317 wp_send_json_error( $data, 400 );
1318 die();
1319 }
1320
1321 $post = get_post( $style_id );
1322 if ( ! $post || $post->post_type !== self::$post_type ) {
1323 $data = array(
1324 'message' => __( 'The style you are renaming either does not exist or it is not a style', 'formidable' ),
1325 );
1326 wp_send_json_error( $data, 404 );
1327 die();
1328 }
1329
1330 global $wpdb;
1331 $wpdb->update( $wpdb->posts, array( 'post_title' => $style_name ), array( 'ID' => $post->ID ) );
1332
1333 $data = array();
1334 wp_send_json_success( $data );
1335 }
1336
1337 /**
1338 * Prevent the WordPress edit.css file from loading on the visual styler page.
1339 * This way .form-field elements do not have border styles applied to them.
1340 *
1341 * @since 6.0
1342 *
1343 * @param WP_Styles $styles
1344 * @return void
1345 */
1346 public static function disable_conflicting_wp_admin_css( $styles ) {
1347 if ( ! FrmAppHelper::is_style_editor_page() ) {
1348 return;
1349 }
1350
1351 FrmStylesPreviewHelper::disable_conflicting_wp_admin_css( $styles );
1352 }
1353
1354 /**
1355 * @deprecated 6.1 Saving custom CSS has been moved into Global Settings.
1356 *
1357 * @return void
1358 */
1359 public static function save_css() {
1360 _deprecated_function( __METHOD__, '6.1' );
1361 }
1362 }
1363