settings_key = $settings_key; $this->prefix = $prefix; $defaults = array( 'translation_strings' => array(), 'props' => array(), 'settings_sections' => array(), 'registered_settings' => array(), 'upgraded_settings' => array(), ); $args = wp_parse_args( $args, $defaults ); $this->hooks(); $this->set_translation_strings( $args['translation_strings'] ); $this->set_props( $args['props'] ); $this->set_sections( $args['settings_sections'] ); $this->set_registered_settings( $args['registered_settings'] ); $this->set_upgraded_settings( $args['upgraded_settings'] ); } /** * Adds the functions to the appropriate WordPress hooks. */ public function hooks() { add_action( 'admin_menu', array( $this, 'admin_menu' ), 11 ); add_action( 'admin_init', array( $this, 'admin_init' ) ); add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) ); } /** * Filters the CSS classes for the body tag in the admin. * * @param string $classes Space-separated list of CSS classes. * @return string Space-separated list of CSS classes. */ public function admin_body_class( $classes ) { $current_screen = get_current_screen(); if ( in_array( $current_screen->id, $this->menu_pages, true ) ) { $classes .= " {$this->prefix}-dashboard-page"; } return $classes; } /** * Sets properties. * * @param array|string $args { * Array or string of arguments. Default is blank array. * * @type array $menus Array of admin menus. See add_custom_menu_page() for more info. * @type string $default_tab Default tab. * @type string $admin_footer_text Admin footer text. * @type string $help_sidebar Help sidebar. * @type array $help_tabs Help tabs. * } */ public function set_props( $args ) { $defaults = array( 'menus' => array(), 'default_tab' => 'general', 'admin_footer_text' => '', 'help_sidebar' => '', 'help_tabs' => array(), ); $args = wp_parse_args( $args, $defaults ); foreach ( $args as $name => $value ) { $this->$name = $value; } } /** * Sets translation strings. * * @param array $strings { * Array of translation strings. * * @type string $page_title Page title. * @type string $menu_title Menu title. * @type string $page_header Page header. * @type string $reset_message Reset message. * @type string $success_message Success message. * @type string $save_changes Save changes button label. * @type string $reset_settings Reset settings button label. * @type string $reset_button_confirm Reset button confirmation message. * @type string $checkbox_modified Checkbox modified label. * } * * @return void */ public function set_translation_strings( $strings ) { // Args prefixed with an underscore are reserved for internal use. $defaults = array( 'page_header' => '', 'reset_message' => 'Settings have been reset to their default values. Reload this page to view the updated settings.', 'success_message' => 'Settings updated.', 'save_changes' => 'Save Changes', 'reset_settings' => 'Reset all settings', 'reset_button_confirm' => 'Do you really want to reset all these settings to their default values?', 'checkbox_modified' => 'Modified from default setting', 'button_label' => 'Choose File', 'previous_saved' => 'Previously saved', 'repeater_new_item' => 'New Item', 'required_label' => 'Required', 'tom_select_no_results' => 'No results found for "%s"', ); $strings = wp_parse_args( $strings, $defaults ); $this->translation_strings = $strings; } /** * Set settings sections * * @param array $sections Setting sections array in the format of: id => Title. * @return object Class object. */ public function set_sections( $sections ) { $this->settings_sections = (array) $sections; return $this; } /** * Add a single section * * @param array $section New Section. * @return object Object of the class instance. */ public function add_section( $section ) { $this->settings_sections[] = $section; return $this; } /** * Set the settings fields for registered settings. * * @param array $registered_settings { * Array of settings in format id => attributes. * @type string $section Section title. * @type string $id Field ID. * @type string $name Field name. * @type string $desc Field description. * @type string $type Field type. * @type string $options Field default option(s). * @type string $max Field max. Applicable for numbers. * @type string $min Field min. Applicable for numbers. * @type string $step Field step. Applicable for numbers. * @type string $size Field size. Applicable for text and textarea. * @type string $field_class CSS class. * @type array $field_attributes HTML Attributes in the form of attribute => value. * @type string $placeholder Placeholder. Applicable for text and textarea. * @type string $sanitize_callback Sanitize callback. * } * } * } * @return object Object of the class instance. */ public function set_registered_settings( $registered_settings ) { $this->registered_settings = (array) $registered_settings; return $this; } /** * Set the settings fields for settings to upgrade. * * @param array $upgraded_settings Settings array. * @return object Object of the class instance. */ public function set_upgraded_settings( $upgraded_settings = array() ) { $this->upgraded_settings = (array) $upgraded_settings; return $this; } /** * Add a menu page to the WordPress admin area. * * @param array $menu Array of settings for the menu page. * * @return string|false The resulting page’s hook_suffix, or false if the user does not have the capability required. */ public function add_custom_menu_page( $menu ) { $defaults = array( // Modes: submenu, management, options, theme, plugins, users, dashboard, posts, media, links, pages, comments. 'type' => 'submenu', // Submenu default settings. 'parent_slug' => 'options-general.php', 'page_title' => '', 'menu_title' => '', 'capability' => $this->get_capability_for_menu(), 'menu_slug' => '', 'function' => array( $this, 'plugin_settings' ), // Menu default settings. 'icon_url' => 'dashicons-admin-generic', 'position' => null, ); $menu = wp_parse_args( $menu, $defaults ); $menu_page = false; switch ( $menu['type'] ) { case 'submenu': $menu_page = add_submenu_page( $menu['parent_slug'], $menu['page_title'], $menu['menu_title'], $menu['capability'], $menu['menu_slug'], $menu['function'], $menu['position'] ); break; case 'management': case 'options': case 'theme': case 'plugins': case 'users': case 'dashboard': case 'posts': case 'media': case 'links': case 'pages': case 'comments': $f = 'add_' . $menu['type'] . '_page'; if ( function_exists( $f ) ) { $menu_page = $f( $menu['page_title'], $menu['menu_title'], $menu['capability'], $menu['menu_slug'], $menu['function'], $menu['position'] ); } break; default: $menu_page = add_menu_page( $menu['page_title'], $menu['menu_title'], $menu['capability'], $menu['menu_slug'], $menu['function'], $menu['icon_url'], $menu['position'] ); break; } return $menu_page; } /** * Add admin menu. */ public function admin_menu() { foreach ( $this->menus as $menu ) { $menu_page = $this->add_custom_menu_page( $menu ); $this->menu_pages[ $menu['menu_slug'] ] = $menu_page; if ( isset( $menu['settings_page'] ) && $menu['settings_page'] ) { $this->settings_page = $menu_page; } } // Load the settings contextual help. add_action( 'load-' . $this->settings_page, array( $this, 'settings_help' ) ); } /** * Get the appropriate capability for the menu based on the user's roles and settings. * * @param array $roles Array of roles to check. * @param string $base_capability The default capability. * @param \WP_User $current_user The current user object. * @param array $role_capabilities Array of role capabilities. * @return string The capability to use for the menu. */ public static function get_capability_for_menu( $roles = array(), $base_capability = 'manage_options', $current_user = null, $role_capabilities = array() ) { if ( ! $current_user ) { $current_user = wp_get_current_user(); } if ( empty( $roles ) || in_array( 'administrator', $current_user->roles, true ) ) { return $base_capability; } if ( empty( $role_capabilities ) ) { $role_capabilities = array( 'editor' => 'edit_others_posts', 'author' => 'publish_posts', 'contributor' => 'edit_posts', 'subscriber' => 'read', ); } foreach ( $current_user->roles as $role ) { if ( in_array( $role, $roles, true ) && isset( $role_capabilities[ $role ] ) ) { return $role_capabilities[ $role ]; } } return $base_capability; } /** * Enqueue scripts and styles. * * @param string $hook The current admin page. */ public function admin_enqueue_scripts( $hook ) { $minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; // Settings API scripts. wp_register_script( 'wz-' . $this->prefix . '-admin', plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ), array( 'jquery', 'wp-color-picker', 'jquery-ui-tabs' ), self::VERSION, true ); wp_register_script( 'wz-' . $this->prefix . '-codemirror', plugins_url( 'js/apply-cm' . $minimize . '.js', __FILE__ ), array( 'jquery', 'underscore', 'code-editor' ), self::VERSION, true ); wp_register_script( 'wz-' . $this->prefix . '-media-selector', plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ), array( 'jquery', 'media-editor', 'media-views' ), self::VERSION, true ); wp_register_style( 'wz-' . $this->prefix . '-admin', plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ), array( 'wp-color-picker' ), self::VERSION ); // Tom Select scripts and styles. wp_register_style( 'wz-' . $this->prefix . '-tom-select', plugins_url( 'css/tom-select.min.css', __FILE__ ), array(), self::VERSION ); wp_register_script( 'wz-' . $this->prefix . '-tom-select', plugins_url( 'js/tom-select.complete.min.js', __FILE__ ), array( 'jquery' ), self::VERSION, true ); wp_register_script( 'wz-' . $this->prefix . '-tom-select-init', plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js', array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ), self::VERSION, true ); wp_localize_script( "wz-{$this->prefix}-admin", 'WZSettingsAdmin', array( 'prefix' => $this->prefix, 'settings_key' => $this->settings_key, ) ); if ( $hook === $this->settings_page ) { $args = array( 'strings' => array( 'no_results' => isset( $this->translation_strings['tom_select_no_results'] ) ? esc_html( $this->translation_strings['tom_select_no_results'] ) : 'No results found for "%s"', ), ); self::enqueue_scripts_styles( $this->prefix, $args ); } } /** * Enqueues all scripts, styles, settings, and templates necessary to use the Settings API. * * @param string $prefix Prefix which is used for creating the unique filters and actions. * @param array $args Array of arguments. */ public static function enqueue_scripts_styles( $prefix, $args = array() ) { wp_enqueue_media(); wp_enqueue_code_editor( array( 'type' => 'text/html', 'codemirror' => array( 'indentUnit' => 2, 'tabSize' => 2, ), ) ); wp_enqueue_script( "wz-{$prefix}-admin" ); wp_enqueue_script( "wz-{$prefix}-codemirror" ); wp_enqueue_script( "wz-{$prefix}-media-selector" ); // Enqueue Tom Select. wp_enqueue_style( "wz-{$prefix}-tom-select" ); wp_enqueue_script( "wz-{$prefix}-tom-select" ); $defaults = array( 'endpoint' => 'category', 'strings' => array( 'no_results' => 'No results found for "%s"', ), ); $args = wp_parse_args( $args, $defaults ); // Localize Tom Select settings. wp_localize_script( "wz-{$prefix}-tom-select-init", 'WZTomSelectSettings', $args ); wp_enqueue_script( "wz-{$prefix}-tom-select-init" ); wp_enqueue_style( 'wz-' . $prefix . '-admin' ); } /** * Initialize and registers the settings sections and fields to WordPress * * Usually this should be called at `admin_init` hook. * * This public function gets the initiated settings sections and fields. Then * registers them to WordPress and ready for use. */ public function admin_init() { $settings_key = $this->settings_key; if ( false === get_option( $settings_key ) ) { add_option( $settings_key, $this->settings_defaults() ); } $this->settings_form = new Settings_Form( array( 'settings_key' => $settings_key, 'prefix' => $this->prefix, 'translation_strings' => $this->translation_strings, ) ); foreach ( $this->registered_settings as $section => $settings ) { add_settings_section( "{$settings_key}_{$section}", // ID used to identify this section and with which to register options. '', // No title, we will handle this via a separate function. '__return_false', // No callback function needed. We'll process this separately. "{$settings_key}_{$section}" // Page on which these options will be added. ); foreach ( $settings as $setting ) { $args = self::parse_field_args( $setting, $section ); $id = $args['id']; $name = $args['name']; $type = isset( $args['type'] ) ? $args['type'] : 'text'; $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' ); add_settings_field( "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array. $name, // Label of the setting. $callback, // Function to handle the setting. "{$settings_key}_{$section}", // Page to display the setting. In our case it is the section as defined above. "{$settings_key}_{$section}", // Name of the section. $args ); } } // Register the settings into the options table. register_setting( $settings_key, $settings_key, array( 'sanitize_callback' => array( $this, 'settings_sanitize' ), 'show_in_rest' => true, ) ); } /** * Flattens $this->registered_settings into $setting[id] => $setting[type] format. * * @return array Default settings */ public function get_registered_settings_types() { $options = array(); // Populate some default values. foreach ( $this->registered_settings as $tab => $settings ) { foreach ( $settings as $option ) { $options[ $option['id'] ] = $option['type']; } } /** * Filters the settings array. * * @param array $options Default settings. */ return apply_filters( $this->prefix . '_get_settings_types', $options ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound } /** * Default settings. * * @return array Default settings */ public function settings_defaults() { $options = array(); // Populate some default values. foreach ( $this->registered_settings as $tab => $settings ) { foreach ( $settings as $option ) { /** * Skip settings that are not really settings. * * @param array $non_setting_types Array of types which are not settings. */ $non_setting_types = apply_filters( $this->prefix . '_non_setting_types', array( 'header', 'descriptive_text' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound if ( in_array( $option['type'], $non_setting_types, true ) ) { continue; } // Base default per type. $options[ $option['id'] ] = ( 'checkbox' === $option['type'] ) ? 0 : ''; // Prefer the explicit 'default' key when provided. if ( isset( $option['default'] ) ) { $options[ $option['id'] ] = $option['default']; } else { // Back-compat for legacy configs that used 'options' to store default values for text-like fields. if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $option['options'] ) ) { $options[ $option['id'] ] = $option['options']; } // Back-compat: when checkbox used 'options' truthy to indicate checked by default. if ( 'checkbox' === $option['type'] && ! empty( $option['options'] ) ) { $options[ $option['id'] ] = 1; } } } } $options = array_merge( $options, $this->upgraded_settings ); /** * Filters the default settings array. * * @param array $options Default settings. */ return apply_filters( $this->prefix . '_settings_defaults', $options ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound } /** * Get the default option for a specific key * * @param string $key Key of the option to fetch. * @return mixed */ public function get_default_option( $key = '' ) { $default_settings = $this->settings_defaults(); if ( array_key_exists( $key, $default_settings ) ) { return $default_settings[ $key ]; } else { return false; } } /** * Reset settings. * * @return void */ public function settings_reset() { delete_option( $this->settings_key ); } /** * Get sanitization callback for given Settings key. * * @param string $key Settings key. * * @return mixed Callback function or false if callback isn't found. */ public function get_sanitize_callback( $key = '' ) { if ( empty( $key ) ) { return false; } $settings_sanitize = new Settings_Sanitize( array( 'settings_key' => $this->settings_key, 'prefix' => $this->prefix, ) ); // Iterate over registered fields and see if we can find proper callback. foreach ( $this->registered_settings as $section => $settings ) { foreach ( $settings as $setting ) { if ( $setting['id'] !== $key ) { continue; } // Return the callback name. $sanitize_callback = false; if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) { $sanitize_callback = $setting['sanitize_callback']; return $sanitize_callback; } if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) { // For repeater fields, create a closure to pass the field configuration. if ( 'repeater' === $setting['type'] ) { return function ( $value ) use ( $settings_sanitize, $setting ) { return $settings_sanitize->sanitize_repeater_field( $value, $setting ); }; } $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ); return $sanitize_callback; } return $sanitize_callback; } } return false; } /** * Sanitize the form data being submitted. * * @param array $input Input unclean array. * @return array Sanitized array */ public function settings_sanitize( $input ) { // This should be set if a form is submitted, so let's save it in the $referrer variable. if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return $input; } parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Check if we need to set to defaults. $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $reset ) { $this->settings_reset(); $settings = get_option( $this->settings_key ); add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['reset_message'], 'error' ); return $settings; } // Get the various settings we've registered. $settings = get_option( $this->settings_key ); $settings = is_array( $settings ) ? $settings : array(); $settings_types = $this->get_registered_settings_types(); // Get the tab. This is also our settings' section. $tab = $referrer['tab'] ?? $this->default_tab; $input = $input ? $input : array(); /** * Filter the settings for the tab. e.g. prefix_settings_general_sanitize. * * @param array $input Input unclean array */ $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound // Create an output array by merging the existing settings with the ones submitted. $output = array_merge( $settings, $input ); // Loop through each setting being saved and pass it through a sanitization filter. foreach ( $settings_types as $key => $type ) { /** * Skip settings that are not really settings. * * @param array $non_setting_types Array of types which are not settings. */ $non_setting_types = apply_filters( $this->prefix . '_non_setting_types', array( 'header', 'descriptive_text' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound if ( in_array( $type, $non_setting_types, true ) ) { continue; } if ( array_key_exists( $key, $input ) ) { $sanitize_callback = $this->get_sanitize_callback( $key ); // If callback is set, call it. if ( $sanitize_callback ) { if ( 'sensitive' === $type ) { $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key ); } else { $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] ); } continue; } } // Delete any key that is not present when we submit the input array. if ( ! isset( $input[ $key ] ) ) { unset( $output[ $key ] ); } // Delete any settings that are no longer part of our registered settings. if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) { unset( $output[ $key ] ); } } add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' ); /** * Filter the settings array before it is returned. * * @param array $output Settings array. * @param array $input Input settings array. */ return apply_filters( $this->prefix . '_settings_sanitize', $output, $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound } /** * Render the settings page. */ public function plugin_settings() { ?>