PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.2
WebberZone Top 10 — Popular Posts v4.4.2
4.5.2 4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 All 118 releases
top-10 / includes / admin / settings / class-settings-api.php

class-settings-api.php in WebberZone Top 10 — Popular Posts 4.4.2, at includes/admin/settings/class-settings-api.php

1,330 lines 39.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings API.
4 *
5 * Functions to register, read, write and update settings.
6 * Portions of this code have been inspired by Easy Digital Downloads, WordPress Settings Sandbox, WordPress Settings API class, etc.
7 *
8 * @package WebberZone\Top_Ten
9 */
10
11 namespace WebberZone\Top_Ten\Admin\Settings;
12
13 // If this file is called directly, abort.
14 if ( ! defined( 'WPINC' ) ) {
15 die;
16 }
17
18 /**
19 * Settings API wrapper class
20 *
21 * @version 2.9.0
22 * @since 4.0.0
23 */
24 class Settings_API {
25
26 /**
27 * Current version number
28 *
29 * @var string
30 */
31 public const VERSION = '2.11.0';
32
33 /**
34 * Settings Key.
35 *
36 * @var string Settings Key.
37 */
38 public $settings_key;
39
40 /**
41 * Prefix which is used for creating the unique filters and actions.
42 *
43 * @var string Prefix.
44 */
45 public $prefix;
46
47 /**
48 * Translation strings.
49 *
50 * @see set_translation_strings()
51 *
52 * @var array Translation strings.
53 */
54 public $translation_strings;
55
56 /**
57 * Menus.
58 *
59 * @var array Menus.
60 */
61 public $menus = array();
62
63 /**
64 * Menu pages.
65 *
66 * @var array Menu pages.
67 */
68 public $menu_pages = array();
69
70 /**
71 * Default navigation tab.
72 *
73 * @var string Default navigation tab.
74 */
75 protected $default_tab;
76
77 /**
78 * Version used for cache-busting enqueued assets. Pass the plugin's own
79 * version via props so every plugin release refreshes browser caches.
80 *
81 * @var string
82 */
83 protected $version = self::VERSION;
84
85 /**
86 * Settings page.
87 *
88 * @var string Settings page.
89 */
90 public $settings_page = '';
91
92 /**
93 * Admin Footer Text. Displayed at the bottom of the plugin settings page.
94 *
95 * @var string Admin Footer Text.
96 */
97 protected $admin_footer_text;
98
99 /**
100 * Array containing the settings' sections.
101 *
102 * @var array Settings sections array.
103 */
104 protected $settings_sections = array();
105
106 /**
107 * Array containing the settings' fields.
108 *
109 * @var array Settings fields array.
110 */
111 protected $registered_settings = array();
112
113 /**
114 * Array containing the settings' fields that need to be upgraded to the current Settings API.
115 *
116 * @var array Settings fields array.
117 */
118 protected $upgraded_settings = array();
119
120 /**
121 * Help sidebar content.
122 *
123 * @var string Admin Footer Text.
124 */
125 protected $help_sidebar;
126
127 /**
128 * Array of help tabs.
129 *
130 * @var array Settings sections array.
131 */
132 protected $help_tabs = array();
133
134 /**
135 * Settings form.
136 *
137 * @var object Settings form.
138 */
139 public $settings_form;
140
141 /**
142 * Main constructor class.
143 *
144 * @param string $settings_key Settings key.
145 * @param string $prefix Prefix. Used for actions and filters.
146 * @param mixed $args {
147 * Array or string of arguments. Default is blank array.
148 * @type array $translation_strings Translation strings.
149 * @type array $settings_sections Settings sections.
150 * @type array $props Properties.
151 * @type array $registered_settings Registered settings.
152 * @type array $upgraded_settings Upgraded settings.
153 * }
154 */
155 public function __construct( $settings_key, $prefix, $args ) {
156
157 if ( ! defined( 'WZ_SETTINGS_API_VERSION' ) ) {
158 define( 'WZ_SETTINGS_API_VERSION', self::VERSION ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
159 }
160
161 $this->settings_key = $settings_key;
162 $this->prefix = $prefix;
163
164 $defaults = array(
165 'translation_strings' => array(),
166 'props' => array(),
167 'settings_sections' => array(),
168 'registered_settings' => array(),
169 'upgraded_settings' => array(),
170 );
171 $args = wp_parse_args( $args, $defaults );
172
173 $this->hooks();
174 $this->set_translation_strings( $args['translation_strings'] );
175 $this->set_props( $args['props'] );
176 $this->set_sections( $args['settings_sections'] );
177 $this->set_registered_settings( $args['registered_settings'] );
178 $this->set_upgraded_settings( $args['upgraded_settings'] );
179 }
180
181 /**
182 * Adds the functions to the appropriate WordPress hooks.
183 */
184 public function hooks() {
185 add_action( 'admin_menu', array( $this, 'admin_menu' ), 11 );
186 add_action( 'admin_init', array( $this, 'admin_init' ) );
187 add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) );
188 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
189 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
190 }
191
192 /**
193 * Filters the CSS classes for the body tag in the admin.
194 *
195 * @param string $classes Space-separated list of CSS classes.
196 * @return string Space-separated list of CSS classes.
197 */
198 public function admin_body_class( $classes ) {
199 $current_screen = get_current_screen();
200
201 if ( in_array( $current_screen->id, $this->menu_pages, true ) ) {
202 $classes .= " {$this->prefix}-dashboard-page";
203 }
204 return $classes;
205 }
206
207 /**
208 * Sets properties.
209 *
210 * @param array|string $args {
211 * Array or string of arguments. Default is blank array.
212 *
213 * @type array $menus Array of admin menus. See add_custom_menu_page() for more info.
214 * @type string $default_tab Default tab.
215 * @type string $admin_footer_text Admin footer text.
216 * @type string $help_sidebar Help sidebar.
217 * @type array $help_tabs Help tabs.
218 * }
219 */
220 public function set_props( $args ) {
221
222 $defaults = array(
223 'menus' => array(),
224 'default_tab' => 'general',
225 'admin_footer_text' => '',
226 'help_sidebar' => '',
227 'help_tabs' => array(),
228 'version' => self::VERSION,
229 );
230
231 $args = wp_parse_args( $args, $defaults );
232
233 foreach ( $args as $name => $value ) {
234 $this->$name = $value;
235 }
236 }
237
238 /**
239 * Sets translation strings.
240 *
241 * @param array $strings {
242 * Array of translation strings.
243 *
244 * @type string $page_title Page title.
245 * @type string $menu_title Menu title.
246 * @type string $page_header Page header.
247 * @type string $reset_message Reset message.
248 * @type string $success_message Success message.
249 * @type string $save_changes Save changes button label.
250 * @type string $reset_settings Reset settings button label.
251 * @type string $reset_button_confirm Reset button confirmation message.
252 * }
253 *
254 * @return void
255 */
256 public function set_translation_strings( $strings ) {
257
258 // Args prefixed with an underscore are reserved for internal use.
259 $defaults = array(
260 'page_header' => '',
261 'reset_message' => 'Settings have been reset to their default values. Reload this page to view the updated settings.',
262 'success_message' => 'Settings updated.',
263 'save_changes' => 'Save Changes',
264 'reset_settings' => 'Reset all settings',
265 'reset_button_confirm' => 'Do you really want to reset all these settings to their default values?',
266 'button_label' => 'Choose File',
267 'previous_saved' => 'Previously saved',
268 'repeater_new_item' => 'New Item',
269 'required_label' => 'Required',
270 'tom_select_no_results' => 'No results found for "%s"',
271 'search_placeholder' => 'Search settings',
272 'search_no_results' => 'No settings found. Try a different search term.',
273 'search_clear' => 'Clear search',
274 'search_results_single' => '%d setting found.',
275 'search_results_plural' => '%d settings found.',
276 'search_matches_label' => 'matching settings',
277 );
278
279 $strings = wp_parse_args( $strings, $defaults );
280
281 $this->translation_strings = $strings;
282 }
283
284 /**
285 * Set settings sections
286 *
287 * @param array $sections Setting sections array in the format of: id => Title.
288 * @return object Class object.
289 */
290 public function set_sections( $sections ) {
291 $this->settings_sections = (array) $sections;
292
293 return $this;
294 }
295
296 /**
297 * Add a single section
298 *
299 * @param array $section New Section.
300 * @return object Object of the class instance.
301 */
302 public function add_section( $section ) {
303 $this->settings_sections[] = $section;
304
305 return $this;
306 }
307
308 /**
309 * Set the settings fields for registered settings.
310 *
311 * @param array $registered_settings {
312 * Array of settings in format id => attributes.
313 * @type string $section Section title.
314 * @type string $id Field ID.
315 * @type string $name Field name.
316 * @type string $desc Field description.
317 * @type string $type Field type.
318 * @type string $options Field default option(s).
319 * @type string $max Field max. Applicable for numbers.
320 * @type string $min Field min. Applicable for numbers.
321 * @type string $step Field step. Applicable for numbers.
322 * @type string $size Field size. Applicable for text and textarea.
323 * @type string $field_class CSS class.
324 * @type array $field_attributes HTML Attributes in the form of attribute => value.
325 * @type string $placeholder Placeholder. Applicable for text and textarea.
326 * @type string $sanitize_callback Sanitize callback.
327 * }
328 * }
329 * }
330 * @return object Object of the class instance.
331 */
332 public function set_registered_settings( $registered_settings ) {
333 $this->registered_settings = (array) $registered_settings;
334
335 return $this;
336 }
337
338 /**
339 * Set the settings fields for settings to upgrade.
340 *
341 * @param array $upgraded_settings Settings array.
342 * @return object Object of the class instance.
343 */
344 public function set_upgraded_settings( $upgraded_settings = array() ) {
345 $this->upgraded_settings = (array) $upgraded_settings;
346
347 return $this;
348 }
349
350 /**
351 * Add a menu page to the WordPress admin area.
352 *
353 * @param array $menu Array of settings for the menu page.
354 *
355 * @return string|false The resulting page’s hook_suffix, or false if the user does not have the capability required.
356 */
357 public function add_custom_menu_page( $menu ) {
358 $defaults = array(
359
360 // Modes: submenu, management, options, theme, plugins, users, dashboard, posts, media, links, pages, comments.
361 'type' => 'submenu',
362
363 // Submenu default settings.
364 'parent_slug' => 'options-general.php',
365 'page_title' => '',
366 'menu_title' => '',
367 'capability' => $this->get_capability_for_menu(),
368 'menu_slug' => '',
369 'function' => array( $this, 'plugin_settings' ),
370
371 // Menu default settings.
372 'icon_url' => 'dashicons-admin-generic',
373 'position' => null,
374
375 );
376 $menu = wp_parse_args( $menu, $defaults );
377
378 $menu_page = false;
379
380 switch ( $menu['type'] ) {
381 case 'submenu':
382 $menu_page = add_submenu_page(
383 $menu['parent_slug'],
384 $menu['page_title'],
385 $menu['menu_title'],
386 $menu['capability'],
387 $menu['menu_slug'],
388 $menu['function'],
389 $menu['position']
390 );
391 break;
392 case 'management':
393 case 'options':
394 case 'theme':
395 case 'plugins':
396 case 'users':
397 case 'dashboard':
398 case 'posts':
399 case 'media':
400 case 'links':
401 case 'pages':
402 case 'comments':
403 $f = 'add_' . $menu['type'] . '_page';
404 if ( function_exists( $f ) ) {
405 $menu_page = $f(
406 $menu['page_title'],
407 $menu['menu_title'],
408 $menu['capability'],
409 $menu['menu_slug'],
410 $menu['function'],
411 $menu['position']
412 );
413 }
414 break;
415 default:
416 $menu_page = add_menu_page(
417 $menu['page_title'],
418 $menu['menu_title'],
419 $menu['capability'],
420 $menu['menu_slug'],
421 $menu['function'],
422 $menu['icon_url'],
423 $menu['position']
424 );
425 break;
426 }
427
428 return $menu_page;
429 }
430
431
432 /**
433 * Add admin menu.
434 */
435 public function admin_menu() {
436 foreach ( $this->menus as $menu ) {
437 $menu_page = $this->add_custom_menu_page( $menu );
438
439 $this->menu_pages[ $menu['menu_slug'] ] = $menu_page;
440 if ( isset( $menu['settings_page'] ) && $menu['settings_page'] ) {
441 $this->settings_page = $menu_page;
442 }
443 }
444
445 // Load the settings contextual help.
446 add_action( 'load-' . $this->settings_page, array( $this, 'settings_help' ) );
447 }
448
449 /**
450 * Get the appropriate capability for the menu based on the user's roles and settings.
451 *
452 * @param array $roles Array of roles to check.
453 * @param string $base_capability The default capability.
454 * @param \WP_User $current_user The current user object.
455 * @param array $role_capabilities Array of role capabilities.
456 * @return string The capability to use for the menu.
457 */
458 public static function get_capability_for_menu( $roles = array(), $base_capability = 'manage_options', $current_user = null, $role_capabilities = array() ) {
459 if ( ! $current_user ) {
460 $current_user = wp_get_current_user();
461 }
462
463 if ( empty( $roles ) || in_array( 'administrator', $current_user->roles, true ) ) {
464 return $base_capability;
465 }
466
467 if ( empty( $role_capabilities ) ) {
468 $role_capabilities = array(
469 'editor' => 'edit_others_posts',
470 'author' => 'publish_posts',
471 'contributor' => 'edit_posts',
472 'subscriber' => 'read',
473 );
474 }
475
476 foreach ( $current_user->roles as $role ) {
477 if ( in_array( $role, $roles, true ) && isset( $role_capabilities[ $role ] ) ) {
478 return $role_capabilities[ $role ];
479 }
480 }
481
482 return $base_capability;
483 }
484
485 /**
486 * Enqueue scripts and styles.
487 *
488 * @param string $hook The current admin page.
489 */
490 public function admin_enqueue_scripts( $hook ) {
491
492 $minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
493
494 // Settings API scripts.
495 wp_register_script(
496 'wz-' . $this->prefix . '-admin',
497 plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ),
498 array( 'jquery', 'wp-color-picker', 'jquery-ui-tabs' ),
499 $this->version,
500 true
501 );
502 wp_register_script(
503 'wz-' . $this->prefix . '-codemirror',
504 plugins_url( 'js/apply-cm' . $minimize . '.js', __FILE__ ),
505 array( 'jquery', 'underscore', 'code-editor' ),
506 $this->version,
507 true
508 );
509 wp_register_script(
510 'wz-' . $this->prefix . '-media-selector',
511 plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ),
512 array( 'jquery', 'media-editor', 'media-views' ),
513 $this->version,
514 true
515 );
516 wp_register_style(
517 'wz-' . $this->prefix . '-admin',
518 plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ),
519 array( 'wp-color-picker' ),
520 $this->version
521 );
522
523 // Tom Select scripts and styles.
524 wp_register_style(
525 'wz-' . $this->prefix . '-tom-select',
526 plugins_url( 'css/tom-select.min.css', __FILE__ ),
527 array(),
528 $this->version
529 );
530 wp_register_script(
531 'wz-' . $this->prefix . '-tom-select',
532 plugins_url( 'js/tom-select.complete.min.js', __FILE__ ),
533 array( 'jquery' ),
534 $this->version,
535 true
536 );
537 wp_register_script(
538 'wz-' . $this->prefix . '-tom-select-init',
539 plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js',
540 array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ),
541 $this->version,
542 true
543 );
544 wp_localize_script(
545 "wz-{$this->prefix}-admin",
546 'WZSettingsAdmin',
547 array(
548 'prefix' => $this->prefix,
549 'settings_key' => $this->settings_key,
550 'strings' => array(
551 'search_no_results' => esc_html( $this->translation_strings['search_no_results'] ?? 'No settings found. Try a different search term.' ),
552 'search_results_single' => esc_html( $this->translation_strings['search_results_single'] ?? '%d setting found.' ),
553 'search_results_plural' => esc_html( $this->translation_strings['search_results_plural'] ?? '%d settings found.' ),
554 'search_matches_label' => esc_html( $this->translation_strings['search_matches_label'] ?? 'matching settings' ),
555 ),
556 )
557 );
558
559 if ( $hook === $this->settings_page ) {
560 $args = array(
561 'strings' => array(
562 'no_results' => isset( $this->translation_strings['tom_select_no_results'] ) ? esc_html( $this->translation_strings['tom_select_no_results'] ) : 'No results found for "%s"',
563 ),
564 );
565 self::enqueue_scripts_styles( $this->prefix, $args );
566 }
567 }
568
569 /**
570 * Enqueues all scripts, styles, settings, and templates necessary to use the Settings API.
571 *
572 * @param string $prefix Prefix which is used for creating the unique filters and actions.
573 * @param array $args Array of arguments.
574 */
575 public static function enqueue_scripts_styles( $prefix, $args = array() ) {
576
577 wp_enqueue_media();
578
579 wp_enqueue_code_editor(
580 array(
581 'type' => 'text/html',
582 'codemirror' => array(
583 'indentUnit' => 2,
584 'tabSize' => 2,
585 ),
586 )
587 );
588
589 wp_enqueue_script( "wz-{$prefix}-admin" );
590 wp_enqueue_script( "wz-{$prefix}-codemirror" );
591 wp_enqueue_script( "wz-{$prefix}-media-selector" );
592
593 // Enqueue Tom Select.
594 wp_enqueue_style( "wz-{$prefix}-tom-select" );
595 wp_enqueue_script( "wz-{$prefix}-tom-select" );
596
597 $defaults = array(
598 'endpoint' => 'category',
599 'strings' => array(
600 'no_results' => 'No results found for "%s"',
601 ),
602 );
603
604 $args = wp_parse_args( $args, $defaults );
605
606 // Localize Tom Select settings.
607 wp_localize_script(
608 "wz-{$prefix}-tom-select-init",
609 'WZTomSelectSettings',
610 $args
611 );
612 wp_enqueue_script( "wz-{$prefix}-tom-select-init" );
613
614 wp_enqueue_style( 'wz-' . $prefix . '-admin' );
615 }
616
617 /**
618 * Initialize and registers the settings sections and fields to WordPress
619 *
620 * Usually this should be called at `admin_init` hook.
621 *
622 * This public function gets the initiated settings sections and fields. Then
623 * registers them to WordPress and ready for use.
624 */
625 public function admin_init() {
626
627 $settings_key = $this->settings_key;
628
629 if ( false === get_option( $settings_key ) ) {
630 add_option( $settings_key, $this->settings_defaults() );
631 }
632
633 $this->settings_form = new Settings_Form(
634 array(
635 'settings_key' => $settings_key,
636 'prefix' => $this->prefix,
637 'translation_strings' => $this->translation_strings,
638 )
639 );
640
641 foreach ( $this->registered_settings as $section => $settings ) {
642
643 add_settings_section(
644 "{$settings_key}_{$section}", // ID used to identify this section and with which to register options.
645 '', // No title, we will handle this via a separate function.
646 '__return_false', // No callback function needed. We'll process this separately.
647 "{$settings_key}_{$section}" // Page on which these options will be added.
648 );
649
650 foreach ( $settings as $setting ) {
651
652 $args = self::parse_field_args( $setting, $section );
653
654 $id = $args['id'];
655 $name = $args['name'];
656 $type = isset( $args['type'] ) ? $args['type'] : 'text';
657 $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' );
658
659 // Tag header rows so the settings search can group fields under them.
660 if ( 'header' === $type ) {
661 $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row' );
662 }
663
664 add_settings_field(
665 "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array.
666 $name, // Label of the setting.
667 $callback, // Function to handle the setting.
668 "{$settings_key}_{$section}", // Page to display the setting. In our case it is the section as defined above.
669 "{$settings_key}_{$section}", // Name of the section.
670 $args
671 );
672 }
673 }
674
675 // Register the settings into the options table.
676 register_setting(
677 $settings_key,
678 $settings_key,
679 array(
680 'sanitize_callback' => array( $this, 'settings_sanitize' ),
681 'show_in_rest' => true,
682 )
683 );
684 }
685
686 /**
687 * Flattens $this->registered_settings into $setting[id] => $setting[type] format.
688 *
689 * @return array Default settings
690 */
691 public function get_registered_settings_types() {
692
693 $options = array();
694
695 // Populate some default values.
696 foreach ( $this->registered_settings as $tab => $settings ) {
697 foreach ( $settings as $option ) {
698 $options[ $option['id'] ] = $option['type'];
699 }
700 }
701
702 /**
703 * Filters the settings array.
704 *
705 * @param array $options Default settings.
706 */
707 return apply_filters( $this->prefix . '_get_settings_types', $options ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
708 }
709
710
711 /**
712 * Default settings.
713 *
714 * @return array Default settings
715 */
716 public function settings_defaults() {
717
718 $options = array();
719
720 // Populate some default values.
721 foreach ( $this->registered_settings as $tab => $settings ) {
722 foreach ( $settings as $option ) {
723 /**
724 * Skip settings that are not really settings.
725 *
726 * @param array $non_setting_types Array of types which are not settings.
727 */
728 $non_setting_types = apply_filters( $this->prefix . '_non_setting_types', array( 'header', 'descriptive_text' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
729
730 if ( in_array( $option['type'], $non_setting_types, true ) ) {
731 continue;
732 }
733
734 // Base default per type.
735 $options[ $option['id'] ] = ( 'checkbox' === $option['type'] ) ? 0 : '';
736
737 // Prefer the explicit 'default' key when provided.
738 if ( isset( $option['default'] ) ) {
739 $options[ $option['id'] ] = $option['default'];
740 } else {
741 // Back-compat for legacy configs that used 'options' to store default values for text-like fields.
742 if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $option['options'] ) ) {
743 $options[ $option['id'] ] = $option['options'];
744 }
745
746 // Back-compat: when checkbox used 'options' truthy to indicate checked by default.
747 if ( 'checkbox' === $option['type'] && ! empty( $option['options'] ) ) {
748 $options[ $option['id'] ] = 1;
749 }
750 }
751 }
752 }
753
754 $options = array_merge( $options, $this->upgraded_settings );
755
756 /**
757 * Filters the default settings array.
758 *
759 * @param array $options Default settings.
760 */
761 return apply_filters( $this->prefix . '_settings_defaults', $options ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
762 }
763
764
765 /**
766 * Get the default option for a specific key
767 *
768 * @param string $key Key of the option to fetch.
769 * @return mixed
770 */
771 public function get_default_option( $key = '' ) {
772
773 $default_settings = $this->settings_defaults();
774
775 if ( array_key_exists( $key, $default_settings ) ) {
776 return $default_settings[ $key ];
777 } else {
778 return false;
779 }
780 }
781
782
783 /**
784 * Reset settings.
785 *
786 * @return void
787 */
788 public function settings_reset() {
789 delete_option( $this->settings_key );
790 }
791
792 /**
793 * Get sanitization callback for given Settings key.
794 *
795 * @param string $key Settings key.
796 *
797 * @return mixed Callback function or false if callback isn't found.
798 */
799 public function get_sanitize_callback( $key = '' ) {
800 if ( empty( $key ) ) {
801 return false;
802 }
803
804 $settings_sanitize = new Settings_Sanitize(
805 array(
806 'settings_key' => $this->settings_key,
807 'prefix' => $this->prefix,
808 )
809 );
810
811 // Iterate over registered fields and see if we can find proper callback.
812 foreach ( $this->registered_settings as $section => $settings ) {
813 foreach ( $settings as $setting ) {
814 if ( $setting['id'] !== $key ) {
815 continue;
816 }
817
818 // Return the callback name.
819 $sanitize_callback = false;
820
821 if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) {
822 $sanitize_callback = $setting['sanitize_callback'];
823 return $sanitize_callback;
824 }
825
826 if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) {
827 // For repeater fields, create a closure to pass the field configuration.
828 if ( 'repeater' === $setting['type'] ) {
829 return function ( $value ) use ( $settings_sanitize, $setting ) {
830 return $settings_sanitize->sanitize_repeater_field( $value, $setting );
831 };
832 }
833 $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' );
834 return $sanitize_callback;
835 }
836
837 return $sanitize_callback;
838 }
839 }
840
841 return false;
842 }
843
844 /**
845 * Get the settings keys that are rendered locked (disabled or pro-gated).
846 *
847 * @return array Map of settings key => true for each locked setting.
848 */
849 public function get_locked_settings() {
850 $locked = array();
851
852 foreach ( $this->registered_settings as $settings ) {
853 foreach ( $settings as $setting ) {
854 if ( isset( $setting['id'] ) && ( ! empty( $setting['disabled'] ) || ! empty( $setting['pro'] ) ) ) {
855 $locked[ $setting['id'] ] = true;
856 }
857 }
858 }
859
860 return $locked;
861 }
862
863 /**
864 * Sanitize the form data being submitted.
865 *
866 * @param array $input Input unclean array.
867 * @return array Sanitized array
868 */
869 public function settings_sanitize( $input ) {
870 // This should be set if a form is submitted, so let's save it in the $referrer variable.
871 if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
872 return $input;
873 }
874
875 parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
876
877 // Check if we need to set to defaults.
878 $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
879
880 if ( $reset ) {
881 $this->settings_reset();
882 $settings = get_option( $this->settings_key );
883
884 add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['reset_message'], 'error' );
885
886 return $settings;
887 }
888
889 // Get the various settings we've registered.
890 $settings = get_option( $this->settings_key );
891 $settings = is_array( $settings ) ? $settings : array();
892 $settings_types = $this->get_registered_settings_types();
893 $locked = $this->get_locked_settings();
894
895 // Get the tab. This is also our settings' section.
896 $tab = $referrer['tab'] ?? $this->default_tab;
897
898 $input = $input ? $input : array();
899
900 /**
901 * Filter the settings for the tab. e.g. prefix_settings_general_sanitize.
902 *
903 * @param array $input Input unclean array
904 */
905 $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
906
907 // Create an output array by merging the existing settings with the ones submitted.
908 $output = array_merge( $settings, $input );
909
910 // Loop through each setting being saved and pass it through a sanitization filter.
911 foreach ( $settings_types as $key => $type ) {
912 /**
913 * Skip settings that are not really settings.
914 *
915 * @param array $non_setting_types Array of types which are not settings.
916 */
917 $non_setting_types = apply_filters( $this->prefix . '_non_setting_types', array( 'header', 'descriptive_text' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
918
919 if ( in_array( $type, $non_setting_types, true ) ) {
920 continue;
921 }
922
923 if ( array_key_exists( $key, $input ) ) {
924 $sanitize_callback = $this->get_sanitize_callback( $key );
925
926 // If callback is set, call it.
927 if ( $sanitize_callback ) {
928 if ( 'sensitive' === $type ) {
929 $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key );
930 } else {
931 $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
932 }
933 continue;
934 }
935 }
936
937 // Delete any key that is not present when we submit the input array.
938 if ( ! isset( $input[ $key ] ) ) {
939 // Disabled fields are never submitted, so a missing key must not delete them.
940 if ( ! isset( $locked[ $key ] ) ) {
941 unset( $output[ $key ] );
942 }
943 }
944
945 // Delete any settings that are no longer part of our registered settings.
946 if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) {
947 unset( $output[ $key ] );
948 }
949 }
950
951 add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' );
952
953 /**
954 * Filter the settings array before it is returned.
955 *
956 * @param array $output Settings array.
957 * @param array $input Input settings array.
958 */
959 return apply_filters( $this->prefix . '_settings_sanitize', $output, $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
960 }
961
962 /**
963 * Render the settings page.
964 */
965 public function plugin_settings() {
966 ?>
967 <div class="wrap">
968 <?php do_action( $this->prefix . '_settings_page_header_before' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound ?>
969 <h1><?php echo esc_html( $this->translation_strings['page_header'] ); ?></h1>
970 <?php do_action( $this->prefix . '_settings_page_header' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound ?>
971
972 <?php
973 // WordPress automatically calls settings_errors() on Settings pages.
974 // Only call it manually on custom menu pages to prevent duplicates.
975 $current_screen = get_current_screen();
976 if ( $current_screen && 0 !== strpos( $current_screen->base, 'settings_page_' ) ) {
977 settings_errors( $this->prefix . '-notices' );
978 }
979 ?>
980
981 <div id="poststuff">
982 <div id="post-body" class="metabox-holder columns-2 wz-settings-post-body">
983 <div id="post-body-content" class="wz-vertical-tabs">
984
985 <?php $this->show_navigation(); ?>
986 <?php $this->show_form(); ?>
987
988 </div><!-- /#post-body-content -->
989
990 <div id="postbox-container-1" class="postbox-container">
991
992 <div id="side-sortables" class="meta-box-sortables ui-sortable">
993 <?php
994 $sidebar_file = dirname( __DIR__ ) . '/sidebar.php';
995 if ( file_exists( $sidebar_file ) ) {
996 include_once $sidebar_file;
997 }
998 ?>
999 </div><!-- /#side-sortables -->
1000
1001 </div><!-- /#postbox-container-1 -->
1002 </div><!-- /#post-body -->
1003 <br class="clear" />
1004 </div><!-- /#poststuff -->
1005
1006 </div><!-- /.wrap -->
1007
1008 <?php
1009 }
1010
1011 /**
1012 * Show navigations as tab
1013 *
1014 * Shows all the settings section labels as tab
1015 */
1016 public function show_navigation() {
1017 $active_tab = isset( $_GET['tab'] ) && array_key_exists( sanitize_key( wp_unslash( $_GET['tab'] ) ), $this->settings_sections ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : $this->default_tab; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
1018
1019 $count = count( $this->settings_sections );
1020
1021 // Don't show the navigation if only one section exists.
1022 if ( 1 === $count ) {
1023 return;
1024 }
1025
1026 $html = '<ul class="nav-tab-wrapper">';
1027
1028 // Settings search box. Rendered via wp_kses() with an extended allowed list below as
1029 // wp_kses_post() strips input and button tags.
1030 $search_box = sprintf(
1031 '<li class="wz-settings-search-wrap">' .
1032 '<label class="screen-reader-text" for="%1$s">%2$s</label>' .
1033 '<span class="wz-settings-search-box">' .
1034 '<input type="search" id="%1$s" class="wz-settings-search" placeholder="%2$s" autocomplete="off" />' .
1035 '<button type="button" class="wz-settings-search-clear" aria-label="%3$s" hidden><span aria-hidden="true">&times;</span></button>' .
1036 '</span>' .
1037 '<span class="wz-settings-search-status screen-reader-text" role="status" aria-live="polite"></span>' .
1038 '</li>',
1039 esc_attr( "{$this->prefix}-settings-search" ),
1040 esc_attr( $this->translation_strings['search_placeholder'] ?? 'Search settings' ),
1041 esc_attr( $this->translation_strings['search_clear'] ?? 'Clear search' )
1042 );
1043
1044 foreach ( $this->settings_sections as $tab_id => $tab_name ) {
1045
1046 $active = $active_tab === $tab_id ? ' ' : '';
1047
1048 $html .= sprintf(
1049 '<li><a href="#%s" title="%s" class="nav-tab %s">%s</a></li>',
1050 esc_attr( $tab_id ),
1051 esc_attr( $tab_name ),
1052 sanitize_html_class( $active ),
1053 esc_html( $tab_name )
1054 );
1055
1056 }
1057
1058 $html .= '</ul>';
1059
1060 $allowed_html = wp_kses_allowed_html( 'post' );
1061 $allowed_html['input'] = array(
1062 'type' => true,
1063 'id' => true,
1064 'class' => true,
1065 'placeholder' => true,
1066 'autocomplete' => true,
1067 );
1068 $allowed_html['button'] = array(
1069 'type' => true,
1070 'class' => true,
1071 'aria-label' => true,
1072 'hidden' => true,
1073 );
1074 $allowed_html['span'] = array_merge(
1075 (array) ( $allowed_html['span'] ?? array() ),
1076 array(
1077 'class' => true,
1078 'role' => true,
1079 'aria-live' => true,
1080 'aria-hidden' => true,
1081 )
1082 );
1083
1084 $html = str_replace( '<ul class="nav-tab-wrapper">', '<ul class="nav-tab-wrapper">' . $search_box, $html );
1085
1086 echo wp_kses( $html, $allowed_html );
1087 }
1088
1089 /**
1090 * Show the section settings forms
1091 *
1092 * This public function displays every sections in a different form
1093 */
1094 public function show_form() {
1095 ?>
1096
1097 <form method="post" action="options.php" id="<?php echo esc_attr( "{$this->prefix}-settings-form" ); ?>">
1098
1099 <?php settings_fields( $this->settings_key ); ?>
1100
1101 <?php foreach ( $this->settings_sections as $tab_id => $tab_name ) : ?>
1102
1103 <div id="<?php echo esc_attr( $tab_id ); ?>">
1104 <h2 class="wz-section-title" tabindex="-1"><?php echo esc_html( $tab_name ); ?></h2>
1105 <table class="form-table">
1106 <?php
1107 do_settings_fields( $this->prefix . '_settings_' . $tab_id, $this->prefix . '_settings_' . $tab_id );
1108 ?>
1109 </table>
1110 <p>
1111 <?php
1112 // Default submit button.
1113 submit_button(
1114 $this->translation_strings['save_changes'],
1115 'primary',
1116 'submit',
1117 false
1118 );
1119
1120 echo '&nbsp;&nbsp;';
1121
1122 // Reset button.
1123 $confirm = esc_js( $this->translation_strings['reset_button_confirm'] );
1124 submit_button(
1125 $this->translation_strings['reset_settings'],
1126 'secondary',
1127 'settings_reset',
1128 false,
1129 array(
1130 'onclick' => "return confirm('{$confirm}');",
1131 )
1132 );
1133
1134 echo '&nbsp;&nbsp;';
1135
1136 /**
1137 * Action to add more buttons in each tab.
1138 *
1139 * @param string $tab_id Tab ID.
1140 * @param string $tab_name Tab name.
1141 * @param array $settings_sections Settings sections.
1142 */
1143 do_action( $this->prefix . '_settings_form_buttons', $tab_id, $tab_name, $this->settings_sections ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1144 ?>
1145 </p>
1146 <p class="wz-modified-legend"><span class="wz-modified-dot"></span><?php echo esc_html( $this->translation_strings['modified_legend'] ?? 'Setting modified from its default value' ); ?></p>
1147 </div><!-- /#tab_id-->
1148
1149 <?php endforeach; ?>
1150
1151 </form>
1152
1153 <?php
1154 }
1155
1156 /**
1157 * Add rating links to the admin dashboard
1158 *
1159 * @param string $footer_text The existing footer text.
1160 * @return string Updated Footer text
1161 */
1162 public function admin_footer_text( $footer_text ) {
1163
1164 if ( ! empty( $this->admin_footer_text ) && get_current_screen()->id === $this->settings_page ) {
1165
1166 $text = $this->admin_footer_text;
1167
1168 return str_replace( '</span>', '', $footer_text ) . ' | ' . $text . '</span>';
1169 } else {
1170 return $footer_text;
1171 }
1172 }
1173
1174 /**
1175 * Function to add the contextual help in the settings page.
1176 */
1177 public function settings_help() {
1178 $screen = get_current_screen();
1179
1180 if ( $screen->id !== $this->settings_page ) {
1181 return;
1182 }
1183
1184 $screen->set_help_sidebar( $this->help_sidebar );
1185
1186 foreach ( $this->help_tabs as $tab ) {
1187 $screen->add_help_tab( $tab );
1188 }
1189 }
1190
1191 /**
1192 * Parse field arguments with defaults.
1193 *
1194 * @param array $field Field arguments.
1195 * @param string $section Section name.
1196 *
1197 * @return array Parsed field arguments.
1198 */
1199 public static function parse_field_args( $field, $section = '' ) {
1200 $defaults = array(
1201 'id' => null,
1202 'name' => '',
1203 'desc' => '',
1204 'type' => 'text',
1205 'size' => null,
1206 'options' => '',
1207 'default' => '',
1208 'min' => 0,
1209 'max' => 999999,
1210 'step' => 1,
1211 'field_class' => '',
1212 'field_attributes' => array(),
1213 'placeholder' => '',
1214 'readonly' => false,
1215 'required' => false,
1216 'disabled' => false,
1217 'pro' => false,
1218 'section' => $section,
1219 );
1220
1221 $field = wp_parse_args( $field, $defaults );
1222
1223 // Add required indicator to field name if the field is required.
1224 if ( ! empty( $field['required'] ) && true === $field['required'] ) {
1225 $field['name'] = sprintf( '%s <span class="required" title="%s">*</span>', $field['name'], 'Required' );
1226 }
1227
1228 return $field;
1229 }
1230
1231 /**
1232 * Get the encryption key for API key encryption/decryption.
1233 *
1234 * @param string $prefix Optional prefix for fallback key.
1235 * @return string The encryption key.
1236 */
1237 public static function get_encryption_key( $prefix = '' ) {
1238 $fallback = $prefix ? str_replace( '-', '_', $prefix ) . '_encryption_fallback' : 'settings_api_encryption_fallback';
1239 return defined( 'AUTH_SALT' ) ? AUTH_SALT : ( defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : hash( 'sha256', __NAMESPACE__ . $fallback ) );
1240 }
1241
1242 /**
1243 * Encrypts an API key using either OpenSSL or Sodium, if available.
1244 *
1245 * @param string $key The API key to encrypt.
1246 * @param string $prefix Optional prefix for fallback key.
1247 * @return string The encrypted API key, or the plain text key if no secure method is available.
1248 */
1249 public static function encrypt_api_key( $key, $prefix = '' ) {
1250 if ( empty( $key ) ) {
1251 return '';
1252 }
1253
1254 // Use OpenSSL if available.
1255 if ( extension_loaded( 'openssl' ) ) {
1256 $iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
1257 $iv = openssl_random_pseudo_bytes( $iv_length );
1258 $encrypted = openssl_encrypt( $key, 'aes-256-cbc', self::get_encryption_key( $prefix ), 0, $iv );
1259
1260 // Store IV + ciphertext in hex format.
1261 return 'enc:' . bin2hex( $iv . $encrypted );
1262 }
1263
1264 // Use Sodium (libsodium) if OpenSSL is unavailable.
1265 if ( extension_loaded( 'sodium' ) ) {
1266 $sodium_key = substr( hash( 'sha256', self::get_encryption_key( $prefix ), true ), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES );
1267 $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
1268 $encrypted = sodium_crypto_secretbox( $key, $nonce, $sodium_key );
1269
1270 return 'enc:' . sodium_bin2hex( $nonce . $encrypted );
1271 }
1272
1273 return $key;
1274 }
1275
1276 /**
1277 * Decrypts an API key using either OpenSSL or Sodium, if available.
1278 *
1279 * @param string $encrypted_key The encrypted API key to decrypt.
1280 * @param string $prefix Optional prefix for fallback key.
1281 * @return string The decrypted API key, or the encrypted key if no secure method is available.
1282 */
1283 public static function decrypt_api_key( $encrypted_key, $prefix = '' ) {
1284 if ( empty( $encrypted_key ) ) {
1285 return '';
1286 }
1287
1288 // If the key doesn't start with 'enc:', it's not encrypted.
1289 if ( strpos( $encrypted_key, 'enc:' ) !== 0 ) {
1290 return $encrypted_key;
1291 }
1292
1293 // Remove the 'enc:' prefix.
1294 $encrypted_key = substr( $encrypted_key, 4 );
1295
1296 // Try OpenSSL decryption.
1297 if ( extension_loaded( 'openssl' ) ) {
1298 $data = hex2bin( $encrypted_key );
1299 if ( false === $data ) {
1300 return '';
1301 }
1302
1303 $iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
1304 $iv = mb_substr( $data, 0, $iv_length, '8bit' );
1305 $ciphertext = mb_substr( $data, $iv_length, null, '8bit' );
1306
1307 $decrypted = openssl_decrypt( $ciphertext, 'aes-256-cbc', self::get_encryption_key( $prefix ), 0, $iv );
1308 return false === $decrypted ? '' : $decrypted;
1309 }
1310
1311 // Try Sodium (libsodium) decryption.
1312 if ( extension_loaded( 'sodium' ) ) {
1313 $sodium_key = substr( hash( 'sha256', self::get_encryption_key( $prefix ), true ), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES );
1314 $decoded = sodium_hex2bin( $encrypted_key );
1315
1316 if ( ! $decoded ) {
1317 return '';
1318 }
1319
1320 $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
1321 $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
1322 $decrypted = sodium_crypto_secretbox_open( $ciphertext, $nonce, $sodium_key );
1323
1324 return false === $decrypted ? '' : $decrypted;
1325 }
1326
1327 return $encrypted_key;
1328 }
1329 }
1330