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

1,359 lines 38.0 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 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
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 $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
768 $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
769 if ( $new_style == $previous_style ) {
770 continue;
771 }
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 <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'] ); ?>">
1264 <h3 class="accordion-section-title hndle" tabindex="0">
1265 <?php
1266 FrmAppHelper::icon_by_class( 'frmfont ' . $icon_id );
1267 echo esc_html( $box['title'] );
1268 FrmAppHelper::icon_by_class( 'frmfont frm_arrowdown8_icon' );
1269 ?>
1270 </h3>
1271 <div class="accordion-section-content <?php postbox_classes( $box['id'], $page ); ?>">
1272 <div class="inside">
1273 <?php call_user_func( $box['callback'], $data_object, $box ); ?>
1274 </div><!-- .inside -->
1275 </div><!-- .accordion-section-content -->
1276 </li><!-- .accordion-section -->
1277 <?php
1278 }//end foreach
1279 }//end if
1280 }//end foreach
1281 }//end if
1282 ?>
1283 </ul><!-- .outer-border -->
1284 </div><!-- .accordion-container -->
1285 <?php
1286 return $i;
1287 }
1288
1289 /**
1290 * Rename a style via an AJAX action.
1291 *
1292 * @since 6.0
1293 *
1294 * @return void
1295 */
1296 public static function rename_style() {
1297 $permission_error = FrmAppHelper::permission_nonce_error( 'frm_edit_forms', 'nonce', 'frm_ajax' );
1298 if ( $permission_error !== false ) {
1299 $data = array(
1300 'message' => __( 'Unable to rename style', 'formidable' ),
1301 );
1302 wp_send_json_error( $data, 403 );
1303 die();
1304 }
1305
1306 $style_id = FrmAppHelper::get_post_param( 'style_id', 0, 'absint' );
1307 $style_name = FrmAppHelper::get_post_param( 'style_name', '', 'sanitize_text_field' );
1308
1309 if ( ! $style_id || ! $style_name ) {
1310 $data = array(
1311 'message' => __( 'Invalid route', 'formidable' ),
1312 );
1313 wp_send_json_error( $data, 400 );
1314 die();
1315 }
1316
1317 $post = get_post( $style_id );
1318 if ( ! $post || $post->post_type !== self::$post_type ) {
1319 $data = array(
1320 'message' => __( 'The style you are renaming either does not exist or it is not a style', 'formidable' ),
1321 );
1322 wp_send_json_error( $data, 404 );
1323 die();
1324 }
1325
1326 global $wpdb;
1327 $wpdb->update( $wpdb->posts, array( 'post_title' => $style_name ), array( 'ID' => $post->ID ) );
1328
1329 $data = array();
1330 wp_send_json_success( $data );
1331 }
1332
1333 /**
1334 * Prevent the WordPress edit.css file from loading on the visual styler page.
1335 * This way .form-field elements do not have border styles applied to them.
1336 *
1337 * @since 6.0
1338 *
1339 * @param WP_Styles $styles
1340 * @return void
1341 */
1342 public static function disable_conflicting_wp_admin_css( $styles ) {
1343 if ( ! FrmAppHelper::is_style_editor_page() ) {
1344 return;
1345 }
1346
1347 FrmStylesPreviewHelper::disable_conflicting_wp_admin_css( $styles );
1348 }
1349
1350 /**
1351 * @deprecated 6.1 Saving custom CSS has been moved into Global Settings.
1352 *
1353 * @return void
1354 */
1355 public static function save_css() {
1356 _deprecated_function( __METHOD__, '6.1' );
1357 }
1358 }
1359