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

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