PluginProbe
WebberZone Top 10 — Popular Posts / 4.3.2
WebberZone Top 10 — Popular Posts v4.3.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 1.6.1 All 117 releases
top-10 / includes / admin / settings / class-settings-api.php

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

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