PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.5
Post Views Counter v1.7.5
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / includes / class-settings-api.php
post-views-counter / includes Last commit date
class-admin.php 5 months ago class-columns-modal.php 5 months ago class-columns.php 5 months ago class-counter.php 5 months ago class-crawler-detect.php 5 months ago class-cron.php 5 months ago class-dashboard.php 5 months ago class-frontend.php 5 months ago class-functions.php 5 months ago class-import.php 5 months ago class-integration-gutenberg.php 5 months ago class-integrations.php 5 months ago class-query.php 5 months ago class-settings-api.php 5 months ago class-settings-display.php 5 months ago class-settings-general.php 5 months ago class-settings-integrations.php 5 months ago class-settings-other.php 5 months ago class-settings-reports.php 5 months ago class-settings.php 5 months ago class-toolbar.php 5 months ago class-traffic-signals.php 5 months ago class-update.php 5 months ago class-widgets.php 5 months ago functions.php 5 months ago
class-settings-api.php
1116 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Settings_API class.
8 *
9 * @class Post_Views_Counter_Settings_API
10 */
11 class Post_Views_Counter_Settings_API {
12
13 private $settings = [];
14 private $input_settings = [];
15 private $validated_settings = [];
16 private $pages = [];
17 private $page_types = [];
18 private $prefix = '';
19 private $slug = '';
20 private $domain = '';
21 private $plugin = '';
22 private $plugin_url = '';
23 private $object;
24 private $nested = false;
25
26 /**
27 * Class constructor.
28 *
29 * @return void
30 */
31 public function __construct( $args ) {
32 // set initial data
33 $this->prefix = $args['prefix'];
34 $this->domain = $args['domain'];
35 $this->nested = isset( $args['nested'] ) ? (bool) $args['nested'] : false;
36
37 // empty slug?
38 if ( empty( $args['slug'] ) )
39 $this->slug = $args['domain'];
40 else
41 $this->slug = $args['slug'];
42
43 $this->object = $args['object'];
44 $this->plugin = $args['plugin'];
45 $this->plugin_url = $args['plugin_url'];
46
47 // actions
48 add_action( 'admin_menu', [ $this, 'admin_menu_options' ], 11 );
49 add_action( 'admin_init', [ $this, 'register_settings' ], 11 );
50 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
51 }
52
53 /**
54 * Get prefix.
55 *
56 * @return string
57 */
58 public function get_prefix() {
59 return $this->prefix;
60 }
61
62 /**
63 * Get pages.
64 *
65 * @return array
66 */
67 public function get_pages() {
68 return $this->pages;
69 }
70
71 /**
72 * Get settings.
73 *
74 * @return array
75 */
76 public function get_settings() {
77 return $this->settings;
78 }
79
80 /**
81 * Get current input settings during saving.
82 *
83 * @return array
84 */
85 public function get_input_settings() {
86 return $this->input_settings;
87 }
88
89 /**
90 * Get already validated setting fields during saving.
91 *
92 * @return array
93 */
94 public function get_validated_settings() {
95 return $this->validated_settings;
96 }
97
98 /**
99 * Load default scripts and styles.
100 *
101 * @return void
102 */
103 public function admin_enqueue_scripts() {
104 // Legacy inline styles for disabled tabs
105 $handler = $this->prefix . '-settings-api-style';
106 wp_register_style( $handler, false );
107 wp_enqueue_style( $handler );
108
109 wp_add_inline_style( $handler, '.nav-tab-wrapper span.nav-span-disabled {
110 cursor: not-allowed;
111 float: left;
112 }
113 body.rtl .nav-tab-wrapper span.nav-span-disabled {
114 float: right;
115 }
116 .nav-tab-wrapper a.nav-tab.nav-tab-disabled {
117 pointer-events: none;
118 }
119 .nav-tab-wrapper a.nav-tab.nav-tab-disabled:hover {
120 cursor: not-allowed;
121 }' );
122 }
123
124 /**
125 * Add menu pages.
126 *
127 * @return void
128 */
129 public function admin_menu_options() {
130 $this->pages = apply_filters( $this->prefix . '_settings_pages', [] );
131 $types = [
132 'page' => [],
133 'subpage' => [],
134 'settings_page' => []
135 ];
136
137 foreach ( $this->pages as $page => $data ) {
138 // skip invalid page types
139 if ( empty( $data['type'] ) || ! array_key_exists( $data['type'], $types ) )
140 continue;
141
142 if ( $data['type'] === 'page' ) {
143 add_menu_page( $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ], $data['icon'], $data['position'] );
144
145 // add page type
146 $types['page'][$data['menu_slug']] = $page;
147 // menu subpage?
148 } elseif ( $data['type'] === 'subpage' ) {
149 add_submenu_page( $data['parent_slug'], $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ] );
150
151 // add subpage type
152 $types['subpage'][$data['menu_slug']] = $page;
153 // menu settings page?
154 } elseif ( $data['type'] === 'settings_page' ) {
155 add_options_page( $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ] );
156
157 // add settings type
158 $types['settings_page'][$data['menu_slug']] = $page;
159 }
160 }
161
162 // set page types
163 $this->page_types = $types;
164 }
165
166 /**
167 * Render settings.
168 *
169 * @global string $pagenow
170 *
171 * @return void
172 */
173 public function options_page() {
174 global $pagenow;
175
176 $valid_page = false;
177
178 // get current screen
179 $screen = get_current_screen();
180
181 // display top level settings page?
182 if ( $pagenow === 'admin.php' && preg_match( '/^toplevel_page_(' . implode( '|', $this->page_types['page'] ) . ')$/', $screen->base, $matches ) === 1 && ! empty( $matches[1] ) ) {
183 $valid_page = true;
184 $page_type = 'page';
185 $url_page = 'admin.php';
186 }
187
188 // display settings page?
189 if ( ! $valid_page && $pagenow === 'options-general.php' && preg_match( '/^(?:settings_page_)(' . implode( '|', array_keys( $this->page_types['settings_page'] ) ) . ')$/', $screen->base, $matches ) === 1 ) {
190 $valid_page = true;
191 $page_type = 'settings_page';
192 $url_page = 'options-general.php';
193 }
194
195 // skip invalid pages
196 if ( ! $valid_page )
197 return;
198
199 $page_key = $this->page_types[$page_type][$matches[1]];
200 $tab_key = '';
201 $tabs = [];
202
203 // any tabs?
204 if ( array_key_exists( 'tabs', $this->pages[$page_key] ) ) {
205 // get tabs
206 $tabs = $this->pages[$page_key]['tabs'];
207
208 // reset tabs
209 reset( $tabs );
210
211 // get first default tab
212 $first_tab = key( $tabs );
213
214 // get current tab
215 $tab_key = ! empty( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $tabs ) ? $_GET['tab'] : $first_tab;
216
217 // check current tab
218 if ( ! empty( $_GET['tab'] ) )
219 $tab_key = sanitize_key( $_GET['tab'] );
220
221 // invalid tab?
222 if ( ! array_key_exists( $tab_key, $tabs ) )
223 $tab_key = $first_tab;
224
225 $tab_label = ! empty( $tabs[$tab_key]['label'] ) ? $tabs[$tab_key]['label'] : '';
226 $tab_heading = ! empty( $tabs[$tab_key]['heading'] ) ? $tabs[$tab_key]['heading'] : '';
227 } else
228 $tab_label = '';
229
230 if ( empty( $tabs ) )
231 $tab_heading = '';
232
233 $heading = $this->plugin;
234
235 echo '
236 <div class="wrap ' . $this->prefix . '-settings-wrapper' . '" data-settings-prefix="' . esc_attr( $this->prefix ) . '">
237 <div class="header-wrapper">
238 <span class="header-title">' . esc_html( $heading ) . '</span>
239 </div>';
240
241 if ( ! empty( $tabs ) ) {
242 echo '
243 <nav class="nav-tab-wrapper">';
244
245 foreach ( $tabs as $key => $tab ) {
246 if ( ! empty( $tab['disabled'] ) )
247 $url = '';
248 else
249 $url = admin_url( $url_page . '?page=' . $matches[1] . '&tab=' . $key );
250
251 if ( ! empty( $tab['disabled'] ) )
252 echo '<span class="nav-span-disabled">';
253
254 echo '
255 <a class="nav-tab' . ( $tab_key === $key ? ' nav-tab-active' : '' ) . ( ! empty( $tab['disabled'] ) ? ' nav-tab-disabled' : '' ) . ( ! empty( $tab['class'] ) ? ' ' . esc_attr( $tab['class'] ) : '' ) . '" href="' . ( $url !== '' ? esc_url( $url ) : '#' ) . '">' . esc_html( $tab['label'] ) . '</a>';
256
257 if ( ! empty( $tab['disabled'] ) )
258 echo '</span>';
259 }
260
261 echo '
262 </nav>';
263 }
264
265 echo '
266 <div class="content-wrapper">
267 <h1 class="screen-reader-text">' . esc_html( $heading ) . '</h1>';
268
269 // skip for internal options page
270 if ( $page_type !== 'settings_page' )
271 settings_errors();
272
273 // get settings page classes
274 $settings_class = apply_filters( $this->prefix . '_settings_page_class', [ $this->slug . '-settings', $tab_key . '-settings', $this->prefix . '-settings' ] );
275
276 // sanitize settings page classes
277 $settings_class = array_unique( array_filter( array_map( 'sanitize_html_class', $settings_class ) ) );
278
279 // resolve setting group for sidebar/form
280 if ( ! empty( $tab_key ) ) {
281 if ( ! empty( $tabs[$tab_key]['option_name'] ) ) {
282 $setting = $tabs[$tab_key]['option_name'];
283 } else {
284 $setting = $this->prefix . '_' . $tab_key . '_settings';
285 }
286 } else {
287 $setting = $this->prefix . '_' . $matches[1] . '_settings';
288 }
289
290 // capture sidebar output
291 ob_start();
292 do_action( $this->prefix . '_settings_sidebar', $setting, $page_type, $url_page, $tab_key );
293 $sidebar_html = trim( ob_get_clean() );
294
295 // add has-sidebar class if sidebar has content
296 if ( ! empty( $sidebar_html ) ) {
297 $settings_class[] = 'has-sidebar';
298 }
299
300 echo '
301 <div class="' . implode( ' ', array_map( 'esc_attr', $settings_class ) ) . '">';
302
303 $display_form = true;
304
305 // check form attribute
306 if ( ! empty( $tab_key ) && ! empty( $tabs[$tab_key]['form'] ) ) {
307 $form = $tabs[$tab_key]['form'];
308
309 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
310 $display_form = false;
311 } elseif ( ! empty( $tab_key ) && isset( $this->settings[$matches[1]]['form'][$tab_key] ) ) {
312 $form = $this->settings[$matches[1]]['form'][$tab_key];
313
314 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
315 $display_form = false;
316 } elseif ( isset( $this->settings[$matches[1]]['form'] ) ) {
317 $form = $this->settings[$matches[1]]['form'];
318
319 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
320 $display_form = false;
321 }
322
323 if ( $display_form ) {
324 echo '
325 <form action="options.php" method="post" novalidate class="' . $this->prefix . '-settings-form">';
326 }
327
328 settings_fields( $setting );
329
330 if ( $display_form )
331 do_action( $this->prefix . '_settings_form', $setting, $page_type, $url_page, $tab_key );
332
333 // filter sections by tab if tabs are present
334 global $wp_settings_sections;
335
336 $original_sections = $wp_settings_sections;
337
338 if ( ! empty( $tab_key ) && isset( $this->settings[$page_key]['sections'] ) ) {
339 $filtered_sections = [];
340
341 foreach ( $this->settings[$page_key]['sections'] as $section_id => $section ) {
342 // include sections without tab or matching current tab
343 if ( empty( $section['tab'] ) || $section['tab'] === $tab_key ) {
344 if ( isset( $wp_settings_sections[$setting][$section_id] ) ) {
345 $filtered_sections[$section_id] = $wp_settings_sections[$setting][$section_id];
346 }
347 }
348 }
349
350 // replace with filtered sections
351 if ( isset( $wp_settings_sections[$setting] ) ) {
352 $wp_settings_sections[$setting] = $filtered_sections;
353 }
354 }
355
356 do_settings_sections( $setting );
357
358 // restore original sections
359 $wp_settings_sections = $original_sections;
360
361 if ( $display_form ) {
362 $setting_hyphenated = str_replace( '_', '-', $setting );
363 echo '
364 <p class="submit">';
365
366 submit_button( '', 'primary save-' . $setting_hyphenated, 'save_' . $setting, false, [ 'id' => 'save-' . $setting_hyphenated ] );
367
368 submit_button( __( 'Reset to defaults', $this->domain ), 'outline reset-' . $setting_hyphenated, 'reset_' . $setting, false, [ 'id' => 'reset-' . $setting_hyphenated ] );
369
370 echo '
371 </p>
372 </form>';
373 }
374
375 // output sidebar if it has content
376 if ( ! empty( $sidebar_html ) ) {
377 echo '
378 <div class="' . $this->prefix . '-sidebar">' . $sidebar_html . '</div>';
379 }
380
381 echo '
382 </div>
383 </div>';
384
385 echo '
386 <div class="clear"></div>
387 </div>';
388 }
389
390 /**
391 * Register settings.
392 *
393 * @return void
394 */
395 public function register_settings() {
396 $this->settings = apply_filters( $this->prefix . '_settings_data', [] );
397
398 // check settings
399 foreach ( $this->settings as $setting_id => $setting ) {
400 // tabs?
401 if ( is_array( $setting['option_name'] ) ) {
402 foreach ( $setting['option_name'] as $tab => $option_name ) {
403 $this->register_setting_fields( $tab, $setting, $option_name );
404 }
405 } else
406 $this->register_setting_fields( $setting_id, $setting );
407 }
408 }
409
410 /**
411 * Register setting with sections and fields.
412 *
413 * @return void
414 */
415 public function register_setting_fields( $setting_id, $setting, $option_name = '' ) {
416 if ( empty( $option_name ) )
417 $option_name = $setting['option_name'];
418
419 // register setting
420 register_setting( $option_name, $option_name, ! empty( $setting['validate'] ) ? $setting['validate'] : [ $this, 'validate_settings' ] );
421
422 // register setting sections
423 if ( ! empty( $setting['sections'] ) ) {
424 foreach ( $setting['sections'] as $section_id => $section ) {
425 // skip unwanted sections
426 if ( ! empty( $section['tab'] ) && $section['tab'] !== $setting_id )
427 continue;
428
429 // Auto-generate section classes and wrapper
430 $base_slug = sanitize_html_class( str_replace( '_', '-', $section_id ) );
431 $section_prefix = apply_filters( $this->prefix . '_settings_section_prefix', $this->prefix );
432 $section_prefix = sanitize_html_class( $section_prefix );
433 $section_classes = $section_prefix . '-section ' . $section_prefix . '-section-' . $base_slug;
434 $section_args = [
435 'section_class' => $section_classes,
436 'before_section' => '<section id="' . $section_prefix . '-section-' . $base_slug . '" class="%s">',
437 'after_section' => '</section>',
438 ];
439
440 add_settings_section(
441 $section_id,
442 ! empty( $section['title'] ) ? esc_html( $section['title'] ) : '',
443 ! empty( $section['callback'] ) ? $section['callback'] : null,
444 ! empty( $section['page'] ) ? $section['page'] : $option_name,
445 $section_args
446 );
447 }
448 }
449
450 // register setting fields
451 if ( ! empty( $setting['fields'] ) ) {
452 foreach ( $setting['fields'] as $field_key => $field ) {
453 // skip unwanted fields
454 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
455 continue;
456
457 // set field ID
458 $field_id = implode( '_', [ $this->prefix, $setting_id, $field_key ] );
459
460 // skip rendering this field?
461 if ( ! empty( $field['skip_rendering'] ) )
462 continue;
463
464 // prepare field args
465 $args = array_merge( $this->prepare_field_args( $field, $field_id, $field_key, $setting_id, $option_name ), $field );
466 $args['setting_id'] = $setting_id;
467 $class = sanitize_html_class( str_replace( '_', '-', $field_id ) );
468 $classes = [ $class ];
469
470 if ( ! empty( $args['class'] ) ) {
471 $extra_classes = preg_split( '/\s+/', trim( $args['class'] ) );
472 $extra_classes = array_filter( $extra_classes );
473 $extra_classes = array_map( 'sanitize_html_class', $extra_classes );
474 $classes = array_merge( $classes, $extra_classes );
475 }
476
477 $classes = array_values( array_unique( array_filter( $classes ) ) );
478
479 $field_class = implode( ' ', $classes );
480 $wrapper_class = $class !== '' ? $class . '-row' : '';
481
482 // preserve user classes in wrapper
483 if ( ! empty( $field['class'] ) ) {
484 $wrapper_class .= ' ' . $field['class'];
485 }
486
487 $args['class'] = $wrapper_class;
488 $args['field_class'] = $field_class;
489 $args['css_id'] = $class;
490
491 add_settings_field(
492 $field_id,
493 ! empty( $field['title'] ) ? esc_html( $field['title'] ) : '',
494 [ $this, 'render_field' ],
495 $option_name,
496 ! empty( $field['section'] ) ? esc_attr( $field['section'] ) : '',
497 $args
498 );
499 }
500 }
501 }
502
503 /**
504 * Prepare field arguments.
505 *
506 * @param array $args
507 * @return array
508 */
509 public function prepare_field_args( $field, $field_id, $field_key, $setting_id, $setting_name ) {
510 // get field type
511 $field_type = ! empty( $field['type'] ) ? $field['type'] : '';
512
513 // default lookup path
514 $value = null;
515 $default = null;
516 $name = $setting_name . '[' . $field_key . ']';
517
518 // check for parent
519 if ( ! empty( $field['parent'] ) ) {
520 $name = $setting_name . '[' . $field['parent'] . '][' . $field_key . ']';
521
522 // try with setting_id first
523 if ( isset( $this->object->options[$setting_id][$field['parent']][$field_key] ) ) {
524 $value = $this->object->options[$setting_id][$field['parent']][$field_key];
525 } elseif ( isset( $this->object->options[$field['parent']][$field_key] ) ) {
526 // try without setting_id
527 $value = $this->object->options[$field['parent']][$field_key];
528 }
529
530 // defaults
531 if ( isset( $this->object->defaults[$setting_id][$field['parent']][$field_key] ) ) {
532 $default = $this->object->defaults[$setting_id][$field['parent']][$field_key];
533 } elseif ( isset( $this->object->defaults[$field['parent']][$field_key] ) ) {
534 $default = $this->object->defaults[$field['parent']][$field_key];
535 }
536 } else {
537 // nested?
538 if ( $this->nested ) {
539 $name = $setting_name . '[' . $setting_id . '][' . $field_key . ']';
540
541 if ( isset( $this->object->options[$setting_id][$field_key] ) )
542 $value = $this->object->options[$setting_id][$field_key];
543
544 if ( isset( $this->object->defaults[$setting_id][$field_key] ) )
545 $default = $this->object->defaults[$setting_id][$field_key];
546 } else {
547 // flat
548 if ( isset( $this->object->options[$setting_id][$field_key] ) ) {
549 $value = $this->object->options[$setting_id][$field_key];
550 } elseif ( isset( $this->object->options[$field_key] ) ) {
551 $value = $this->object->options[$field_key];
552 }
553
554 // defaults
555 if ( isset( $this->object->defaults[$setting_id][$field_key] ) ) {
556 $default = $this->object->defaults[$setting_id][$field_key];
557 } elseif ( isset( $this->object->defaults[$field_key] ) ) {
558 $default = $this->object->defaults[$field_key];
559 }
560 }
561 }
562
563 if ( $field_type === 'custom' ) {
564 $value = null;
565 $default = null;
566 }
567
568 return [
569 'id' => $field_id,
570 'html_id' => sanitize_html_class( str_replace( '_', '-', $field_id ) ),
571 'name' => $name,
572 'class' => ! empty( $field['class'] ) ? $field['class'] : '',
573 'type' => $field_type,
574 'label' => ! empty( $field['label'] ) ? $field['label'] : '',
575 'description' => ! empty( $field['description'] ) ? $field['description'] : '',
576 'text' => ! empty( $field['text'] ) ? $field['text'] : '',
577 'min' => ! empty( $field['min'] ) ? (int) $field['min'] : 0,
578 'max' => ! empty( $field['max'] ) ? (int) $field['max'] : 0,
579 'options' => ! empty( $field['options'] ) ? $field['options'] : [],
580 'callback' => ! empty( $field['callback'] ) ? $field['callback'] : null,
581 'validate' => ! empty( $field['validate'] ) ? $field['validate'] : null,
582 'callback_args' => ! empty( $field['callback_args'] ) ? $field['callback_args'] : [],
583 'default' => $default,
584 'value' => $value,
585 'setting_id' => $setting_id,
586 'animation' => ! empty( $field['animation'] ) ? $field['animation'] : '',
587 'logic' => ! empty( $field['logic'] ) ? $field['logic'] : null,
588 'fallback_option' => ! empty( $field['fallback_option'] ) ? sanitize_key( $field['fallback_option'] ) : ''
589 /*
590 after_field
591 before_field
592 */
593 ];
594 }
595
596 /**
597 * Render settings field.
598 *
599 * @param array $args
600 * @return void|string
601 */
602 public function render_field( $args ) {
603 if ( empty( $args ) || ! is_array( $args ) )
604 return;
605
606 // build wrapper classes
607 $wrapper_classes = [ $this->prefix . '-field', $this->prefix . '-field-type-' . $args['type'] ];
608 if ( ! empty( $args['class'] ) )
609 $wrapper_classes[] = $args['class'];
610
611 // add disabled class if field is disabled (but not if feature is available)
612 if ( ! empty( $args['disabled'] ) && $args['disabled'] === true && empty( $args['available'] ) )
613 $wrapper_classes[] = $this->prefix . '-disabled';
614
615 // build wrapper attributes
616 $wrapper_attrs = ' id="' . esc_attr( str_replace( '_', '-', $args['id'] ) ) . '-setting" class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '"';
617
618 // add conditional data attributes
619 $data_attrs = '';
620 $conditions = [];
621 $fallback_option = ! empty( $args['fallback_option'] ) ? $args['fallback_option'] : '';
622
623 if ( ! empty( $args['logic'] ) && is_array( $args['logic'] ) ) {
624 if ( isset( $args['logic']['field'] ) ) {
625 $conditions = [ $args['logic'] ];
626 } else {
627 $conditions = $args['logic'];
628 }
629 }
630
631 if ( ! empty( $conditions ) ) {
632 $data_attr_prefix = sanitize_html_class( $this->prefix );
633 $normalized_conditions = [];
634
635 foreach ( $conditions as $condition ) {
636 if ( empty( $condition['field'] ) || empty( $condition['operator'] ) ) {
637 continue;
638 }
639
640 $field = $condition['field'];
641
642 if ( strpos( $field, '-' ) === false && ! empty( $args['setting_id'] ) ) {
643 $field_id = implode( '_', [ $this->prefix, $args['setting_id'], $field ] );
644 $field = sanitize_html_class( str_replace( '_', '-', $field_id ) );
645 }
646
647 $normalized_conditions[] = [
648 'field' => $field,
649 'operator' => $condition['operator'],
650 'value' => isset( $condition['value'] ) ? $condition['value'] : '',
651 'scope' => ! empty( $condition['scope'] ) ? sanitize_key( $condition['scope'] ) : '',
652 'action' => ! empty( $condition['action'] ) ? sanitize_key( $condition['action'] ) : '',
653 'target' => ! empty( $condition['target'] ) ? sanitize_text_field( $condition['target'] ) : '',
654 'container' => ! empty( $condition['container'] ) ? sanitize_text_field( $condition['container'] ) : '',
655 ];
656 }
657
658 if ( ! empty( $normalized_conditions ) && $data_attr_prefix !== '' ) {
659 if ( ! empty( $args['animation'] ) && in_array( $args['animation'], [ 'fade', 'slide' ], true ) ) {
660 $data_attrs .= ' data-' . $data_attr_prefix . '-animation="' . esc_attr( $args['animation'] ) . '"';
661 }
662 $data_attrs .= ' data-' . $data_attr_prefix . '-logic="' . esc_attr( wp_json_encode( $normalized_conditions ) ) . '"';
663 }
664 }
665
666 if ( $fallback_option !== '' ) {
667 $data_attrs .= ' data-' . $this->prefix . '-fallback-option="' . esc_attr( $fallback_option ) . '"';
668 }
669
670 $wrapper_attrs .= $data_attrs;
671
672 $html = '<div' . $wrapper_attrs . '>';
673
674 if ( ! empty ( $args['before_field'] ) )
675 $html .= $args['before_field'];
676
677 switch ( $args['type'] ) {
678 case 'boolean':
679 if ( empty( $args['disabled'] ) )
680 $html .= '<input type="hidden" name="' . esc_attr( $args['name'] ) . '" value="false" />';
681
682 $html .= '<label><input id="' . esc_attr( $args['html_id'] ) . '" type="checkbox" role="switch" name="' . esc_attr( $args['name'] ) . '" value="true" ' . checked( (bool) $args['value'], true, false ) . ' ' . disabled( empty( $args['disabled'] ), false, false ) . ' />' . wp_kses_post( $args['label'] ) . '</label>';
683 break;
684
685 case 'radio':
686 if ( empty( $args['options'] ) || ! is_array( $args['options'] ) )
687 break;
688
689 $display_type = ! empty( $args['display_type'] ) && in_array( $args['display_type'], [ 'horizontal', 'vertical' ], true ) ? $args['display_type'] : 'vertical';
690
691 if ( count( $args['options'] ) > 1 )
692 $html .= '<div class="' . esc_attr( $this->prefix ) . '-field-group ' . esc_attr( $this->prefix ) . '-radio-group ' . $display_type . '">';
693 foreach ( $args['options'] as $key => $name ) {
694 $is_disabled = ! empty( $args['disabled'] ) && ( is_array( $args['disabled'] ) && in_array( $key, $args['disabled'], true ) || $args['disabled'] === true );
695 $label_classes = [];
696
697 if ( $is_disabled && is_array( $args['disabled'] ) )
698 $label_classes[] = $this->prefix . '-disabled';
699
700 // add PRO class for disabled options in pro-extended fields
701 if ( $is_disabled && ! empty( $args['class'] ) && strpos( $args['class'], 'pvc-pro-extended' ) !== false ) {
702 $label_classes[] = $this->prefix . '-pro';
703 }
704
705 $label_class = ! empty( $label_classes ) ? ' class="' . implode( ' ', $label_classes ) . '"' : '';
706
707 $display_name = esc_html( $name );
708
709 $html .= '<label for="' . esc_attr( $args['html_id'] . '-' . $key ) . '"' . $label_class . '><input id="' . esc_attr( $args['html_id'] . '-' . $key ) . '" type="radio" name="' . esc_attr( $args['name'] ) . '" value="' . esc_attr( $key ) . '" ' . checked( $key, $args['value'], false ) . ' ' . disabled( $is_disabled, true, false ) . ' />' . $display_name . '</label> ';
710 }
711 if ( count( $args['options'] ) > 1 )
712 $html .= '</div>';
713 break;
714
715 case 'checkbox':
716 // possible "empty" value
717 if ( $args['value'] === 'empty' )
718 $args['value'] = [];
719
720 // ensure value is an array
721 if ( ! is_array( $args['value'] ) )
722 $args['value'] = [];
723
724 $display_type = ! empty( $args['display_type'] ) && in_array( $args['display_type'], [ 'horizontal', 'vertical' ], true ) ? $args['display_type'] : 'vertical';
725
726 $html .= '<input type="hidden" name="' . esc_attr( $args['name'] ) . '" value="empty" />';
727
728 if ( empty( $args['options'] ) || ! is_array( $args['options'] ) )
729 break;
730 if ( count( $args['options'] ) > 1 )
731 $html .= '<div class="' . esc_attr( $this->prefix ) . '-field-group ' . esc_attr( $this->prefix ) . '-checkbox-group ' . $display_type . '">';
732 foreach ( $args['options'] as $key => $name ) {
733 $is_disabled = ! empty( $args['disabled'] ) && ( is_array( $args['disabled'] ) && in_array( $key, $args['disabled'], true ) || $args['disabled'] === true );
734 $label_classes = [];
735
736 if ( $is_disabled && is_array( $args['disabled'] ) )
737 $label_classes[] = $this->prefix . '-disabled';
738
739 // add PRO styling for disabled options in pro-extended fields
740 if ( $is_disabled && ! empty( $args['class'] ) && strpos( $args['class'], 'pvc-pro-extended' ) !== false )
741 $label_classes[] = $this->prefix . '-pro';
742
743 $label_class = ! empty( $label_classes ) ? ' class="' . implode( ' ', $label_classes ) . '"' : '';
744
745 $display_name = esc_html( $name );
746
747 $html .= '<label' . $label_class . '><input id="' . esc_attr( $args['html_id'] . '-' . $key ) . '" type="checkbox" name="' . esc_attr( $args['name'] ) . '[]" value="' . esc_attr( $key ) . '" ' . checked( in_array( $key, $args['value'] ), true, false ) . ' ' . disabled( $is_disabled, true, false ) . ' />' . $display_name . '</label>';
748 }
749 if ( count( $args['options'] ) > 1 )
750 $html .= '</div>';
751 break;
752
753 case 'select':
754 $html .= '<select id="' . esc_attr( $args['html_id'] ) . '" name="' . esc_attr( $args['name'] ) . '" ' . disabled( empty( $args['disabled'] ), false, false ) . '/>';
755
756 foreach ( $args['options'] as $key => $name ) {
757 $html .= '<option value="' . esc_attr( $key ) . '" ' . selected( $args['value'], $key, false ) . '>' . esc_html( $name ) . '</option>';
758 }
759
760 $html .= '</select>';
761 break;
762
763 case 'range':
764 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '-slider" type="range" name="' . esc_attr( $args['name'] ) . '" value="' . esc_attr( $args['value'] ) . '" min="' . esc_attr( $args['min'] ) . '" max="' . esc_attr( $args['max'] ) . '" data-range-output="' . esc_attr( $args['html_id'] ) . '-output" /><output id="' . esc_attr( $args['html_id'] ) . '-output" class="' . esc_attr( $this->slug ) . '-range">' . ( (int) $args['value'] ) . '</output>';
765 break;
766
767 case 'number':
768 $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' );
769 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '" type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '" />';
770 $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' );
771 break;
772
773 case 'color':
774 $color_value = esc_attr( $args['value'] );
775 $color_name = esc_attr( $args['name'] );
776 $input_id = esc_attr( $args['html_id'] );
777 $input_class = $this->prefix . '-color-input';
778
779 if ( ! empty( $args['subclass'] ) ) {
780 $input_class .= ' ' . esc_attr( $args['subclass'] );
781 }
782
783 $swatch_style = ' style="background-color: ' . $color_value . ';"';
784
785 $html .= '<div class="' . $this->prefix . '-color-control">';
786 // Text input for the hex color value.
787 $html .= '<input id="' . $input_id . '" type="text" name="' . $color_name . '" value="' . $color_value . '" class="' . $input_class . '" />';
788 // Swatch button to toggle the picker.
789 $html .= '<button type="button" class="' . $this->prefix . '-color-swatch"' . $swatch_style . ' aria-label="' . esc_attr__( 'Open color picker', $this->domain ) . '" aria-expanded="false"></button>';
790 // Vanilla-colorful picker (hidden by default).
791 $html .= '<div class="' . $this->prefix . '-color-popover" aria-hidden="true"><hex-color-picker class="' . $this->prefix . '-hex-color-picker" color="' . $color_value . '"></hex-color-picker></div>';
792 $html .= '</div>';
793 break;
794
795 case 'custom':
796 $html .= call_user_func( $args['callback'], $args );
797 break;
798
799 case 'info':
800 $html .= '<span' . ( ! empty( $args['subclass'] ) ? ' class="' . esc_attr( $args['subclass'] ) . '"' : '' ) . '>' . esc_html( $args['text'] ) . '</span>';
801 break;
802
803 case 'class':
804 case 'input':
805 default:
806 $empty_disabled = empty( $args['disabled'] );
807
808 $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' );
809 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '"' . ( ! empty( $args['subclass'] ) ? ' class="' . esc_attr( $args['subclass'] ) . '"' : '' ) . ' type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '" ' . disabled( $empty_disabled, false, false ) . '/>';
810 $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' );
811
812 if ( ! $empty_disabled )
813 $html .= '<input' . ( $empty_disabled ? '' : ' class="hidden"' ) . ' type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '">';
814 }
815
816 if ( ! empty ( $args['after_field'] ) )
817 $html .= $args['after_field'];
818
819 if ( ! empty ( $args['description'] ) )
820 $html .= '<p class="description">' . $args['description'] . '</p>';
821
822 $html .= '</div>';
823
824 if ( ! empty( $args['return'] ) )
825 return $html;
826 else
827 echo $html;
828 }
829
830 /**
831 * Validate settings field.
832 *
833 * @param mixed $value
834 * @param string $type
835 * @param array $args
836 * @return mixed
837 */
838 public function validate_field( $value = null, $type = '', $args = [] ) {
839 if ( is_null( $value ) )
840 return null;
841
842 switch ( $type ) {
843 case 'boolean':
844 // possible value: 'true' or 'false'
845 $value = ( $value === 'true' || $value === true );
846 break;
847
848 case 'radio':
849 $value = is_array( $value ) ? $args['default'] : sanitize_key( $value );
850
851 // disallow disabled radios
852 if ( ! empty( $args['disabled'] ) && in_array( $value, $args['disabled'], true ) )
853 $value = $args['default'];
854 break;
855
856 case 'checkbox':
857 // possible value: 'empty' or array
858 if ( $value === 'empty' )
859 $value = [];
860 else {
861 if ( is_array( $value ) && ! empty( $value ) ) {
862 $value = array_map( 'sanitize_key', $value );
863 $values = [];
864
865 foreach ( $value as $single_value ) {
866 if ( array_key_exists( $single_value, $args['options'] ) )
867 $values[] = $single_value;
868 }
869
870 $value = $values;
871 } else
872 $value = [];
873 }
874 break;
875
876 case 'number':
877 $value = (int) $value;
878
879 // is value lower than?
880 if ( isset( $args['min'] ) && $value < $args['min'] )
881 $value = $args['min'];
882
883 // is value greater than?
884 if ( isset( $args['max'] ) && $value > $args['max'] )
885 $value = $args['max'];
886 break;
887
888 case 'color':
889 $value = sanitize_text_field( $value );
890 if ( ! preg_match( '/^#[a-f0-9]{3,6}$/i', $value ) ) {
891 $value = $args['default'] ?? '#000000';
892 }
893 break;
894
895 case 'info':
896 $value = '';
897 break;
898
899 case 'custom':
900 // do nothing
901 break;
902
903 case 'class':
904 $value = trim( $value );
905
906 // more than 1 class?
907 if ( strpos( $value, ' ' ) !== false ) {
908 // get unique valid HTML classes
909 $value = array_unique( array_filter( array_map( 'sanitize_html_class', explode( ' ', $value ) ) ) );
910
911 if ( ! empty( $value ) )
912 $value = implode( ' ', $value );
913 else
914 $value = '';
915 // single class
916 } else
917 $value = sanitize_html_class( $value, $args['default'] );
918 break;
919
920 case 'input':
921 case 'select':
922 default:
923 $value = is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : sanitize_text_field( $value );
924 break;
925 }
926
927 return stripslashes_deep( $value );
928 }
929
930 /**
931 * Validate settings.
932 *
933 * @param array $input
934 * @return array
935 */
936 public function validate_settings( $input ) {
937 // check capability
938 if ( ! current_user_can( 'manage_options' ) )
939 return $input;
940
941 // check option page
942 if ( empty( $_POST['option_page'] ) )
943 return $input;
944
945 // try to get setting name and ID
946 foreach ( $this->settings as $id => $setting ) {
947 // tabs?
948 if ( is_array( $setting['option_name'] ) ) {
949 foreach ( $setting['option_name'] as $tab => $option_name ) {
950 // found valid setting?
951 if ( $option_name === $_POST['option_page'] ) {
952 // assign setting ID
953 $setting_id = $tab;
954
955 // assign setting name
956 $setting_name = $option_name;
957
958 // assign setting key
959 $setting_key = $id;
960
961 // already found setting, no need to check the rest
962 break 2;
963 }
964 }
965 } else {
966 // found valid setting?
967 if ( $setting['option_name'] === $_POST['option_page'] ) {
968 // assign setting ID and key
969 $setting_key = $setting_id = $id;
970
971 // assign setting name
972 $setting_name = $setting['option_name'];
973
974 // already found setting, no need to check the rest
975 break;
976 }
977 }
978 }
979
980 // check setting id, no need to check $setting_name since it was at the same stage
981 if ( empty( $setting_id ) )
982 return $input;
983
984 // save settings
985 if ( isset( $_POST['save_' . $setting_name] ) ) {
986 $input = $this->validate_input_settings( $setting_id, $setting_key, $input );
987
988 add_settings_error( $setting_name, 'settings_saved', __( 'Settings saved.', $this->domain ), 'updated' );
989 // reset settings
990 } elseif ( isset( $_POST['reset_' . $setting_name] ) ) {
991 // get default values
992 $input = $this->object->defaults[$setting_id];
993
994 // check custom reset functions
995 if ( ! empty( $this->settings[$setting_key]['fields'] ) ) {
996 foreach ( $this->settings[$setting_key]['fields'] as $field_id => $field ) {
997 // skip invalid tab field if any
998 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
999 continue;
1000
1001 // custom reset function?
1002 if ( ! empty( $field['reset'] ) ) {
1003 // valid function? no need to check "else" since all default values are already set
1004 if ( $this->callback_function_exists( $field['reset'] ) ) {
1005 if ( $field['type'] === 'custom' )
1006 $input = call_user_func( $field['reset'], $input, $field );
1007 else
1008 $input[$field_id] = call_user_func( $field['reset'], $input[$field_id], $field );
1009 }
1010 }
1011 }
1012 }
1013
1014 add_settings_error( $setting_name, 'settings_restored', __( 'Settings restored to defaults.', $this->domain ), 'updated' );
1015 }
1016
1017 do_action( $this->prefix . '_configuration_updated', 'settings', $input );
1018
1019 return $input;
1020 }
1021
1022 /**
1023 * Validate input settings.
1024 *
1025 * @param string $setting_id
1026 * @param array $input
1027 * @return array
1028 */
1029 public function validate_input_settings( $setting_id, $setting_key, $input ) {
1030 if ( ! empty( $this->settings[$setting_key]['fields'] ) ) {
1031 foreach ( $this->settings[$setting_key]['fields'] as $field_id => $field ) {
1032 // skip saving this field?
1033 if ( ! empty( $field['skip_saving'] ) )
1034 continue;
1035
1036 // skip invalid tab field if any
1037 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
1038 continue;
1039
1040 // custom validate function?
1041 if ( ! empty( $field['validate'] ) ) {
1042 // valid function?
1043 if ( $this->callback_function_exists( $field['validate'] ) ) {
1044 if ( $field['type'] === 'custom' )
1045 $input = call_user_func( $field['validate'], $input, $field );
1046 else
1047 $input[$field_id] = isset( $input[$field_id] ) ? call_user_func( $field['validate'], $input[$field_id], $field ) : $this->object->defaults[$setting_id][$field_id];
1048 } else
1049 $input[$field_id] = $this->object->defaults[$setting_id][$field_id];
1050 } else {
1051 // field data?
1052 if ( isset( $input[$field_id] ) ) {
1053 // make sure default value is available
1054 if ( ! isset( $field['default'] ) )
1055 $field['default'] = $this->object->defaults[$setting_id][$field_id];
1056
1057 $input[$field_id] = $this->validate_field( $input[$field_id], $field['type'], $field );
1058 } else
1059 $input[$field_id] = $this->object->defaults[$setting_id][$field_id];
1060 }
1061
1062 // update input data
1063 $this->input_settings = $input;
1064
1065 // add this field as validated
1066 $this->validated_settings[] = $field_id;
1067 }
1068 }
1069
1070 return $input;
1071 }
1072
1073 /**
1074 * Check whether callback is a valid function.
1075 *
1076 * @param string|array $callback
1077 * @return bool
1078 */
1079 public function callback_function_exists( $callback ) {
1080 // function as array?
1081 if ( is_array( $callback ) ) {
1082 list( $object, $function ) = $callback;
1083
1084 // check function existence
1085 $function_exists = method_exists( $object, $function );
1086 // function as string?
1087 } elseif ( is_string( $callback ) ) {
1088 // check function existence
1089 $function_exists = function_exists( $callback );
1090 } else
1091 $function_exists = false;
1092
1093 return $function_exists;
1094 }
1095
1096 /**
1097 * Get value based on minimum and maximum.
1098 *
1099 * @param array $data
1100 * @param string $setting_name
1101 * @param int $default
1102 * @param int $min
1103 * @param int $max
1104 * @return void
1105 */
1106 public function get_int_value( $data, $setting_name, $default, $min, $max ) {
1107 // check existence of value
1108 $value = array_key_exists( $setting_name, $data ) ? (int) $data[$setting_name] : $default;
1109
1110 if ( $value > $max || $value < $min )
1111 $value = $default;
1112
1113 return $value;
1114 }
1115 }
1116