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

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

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