* @link https://github.com/iworks/wordpress-options-class * @license GPL-3.0-or-later * * Copyright 2011-2026 Marcin Pietrzak (marcin@iworks.pl) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 3, as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ defined( 'ABSPATH' ) || exit; // Exit if accessed directly if ( class_exists( 'iworks_options' ) ) { return; } class iworks_options { /** * Plugin version. * * @since 1.0.0 * @var string */ private string $version = '3.1.1'; /** * Core options array. * * @since 1.0.0 * @var array */ private array $options; /** * Option function name. * * @since 1.0.0 * @var string */ private string $option_function_name = ''; /** * Option group name. * * @since 1.0.0 * @var string */ private string $option_group = 'index'; /** * Option prefix. * * @since 1.0.0 * @var string */ private string $option_prefix = ''; /** * Registered page hooks. * * @since 1.0.0 * @var array */ private array $pagehooks = array(); /** * Enqueued scripts. * * @since 1.0.0 * @var array */ private array $scripts_enqueued = array(); /** * Control class mode. * * @since 2.6.5 * @var string */ private string $mode = 'plugin'; /** * Admin notices array. * * @since 1.0.0 * @var array */ public array $notices = array(); /** * Plugin identifier. * * @since 2.7.3 * @var string */ private string $plugin = '-not-set-'; /** * Files to enqueue. * * @since 2.8.4 * @var array */ private array $files = array(); /** * Logger instance. * * @since 3.1.0 * @var iworks_options_logger */ private iworks_options_logger $logger; /** * Constructor. * * Sets up the options class with default values and registers hooks. * * @since 1.0.0 */ public function __construct() { // Setup. $this->files = $this->get_files(); // Register hooks. add_action( 'admin_enqueue_scripts', array( $this, 'register_styles' ), 0 ); add_action( 'admin_head', array( $this, 'admin_head' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); add_filter( 'screen_layout_columns', array( $this, 'screen_layout_columns' ), 10, 2 ); // Include logger class. require_once plugin_dir_path( __FILE__ ) . 'includes/class-iworks-options-logger.php'; $this->logger = new iworks_options_logger( $this ); } /** * Initialize the options class. * * @since 1.0.0 */ public function init() { $this->get_option_array(); } /** * Set option class mode. * * @since 2.6.5 * * @param string $mode Working mode, possible values "plugin", "theme". */ public function set_mode( $mode ) { if ( preg_match( '/^(plugin|theme)$/', $mode ) ) { $this->mode = $mode; } } /** * Get option class mode. * * @since 3.1.0 * * @return string Working mode, possible values "plugin", "theme". */ public function get_mode() { return $this->mode; } /** * Get option group configuration. * * @since 2.6.7 * * @param string|null $option_group Name of config group. * @return array Group configuration array. */ public function get_group( $option_group = null ) { if ( null === $option_group ) { $option_group = $this->option_group; } return $this->get_option_array( $option_group ); } /** * Add admin menu. * * @since 1.0.0 */ public function admin_menu() { $data = $this->get_option_array(); if ( ! isset( $this->options ) ) { return; } $pages = array(); $pages['index'] = $data; if ( isset( $data['pages'] ) ) { $pages = $data['pages'] + $pages; } foreach ( $pages as $key => $data ) { /** * Parse and sanitize admin menu arguments. * * @since 1.0.0 * * @param array $data { * Array of menu page arguments. * * @type string $menu The menu type. Default 'top_level'. * @type string $capability The capability required for this menu. Default 'manage_options'. * @type int $position The position in the menu order. Default 10. * @type string $icon_url The URL to the icon to be used for this menu. Default null. * @type string $parent The parent menu slug. Default null for top-level menu. * @type string $page_title The text to be displayed in the title tags of the page. * Default 'No Page Title'. * } */ $data = wp_parse_args( $data, array( 'menu' => 'top_level', 'capability' => 'manage_options', 'position' => 10, 'icon_url' => null, 'parent' => null, 'page_title' => esc_html__( 'No Page Title', 'reading-position-indicator' ), ) ); /** * Check callback */ $callback = array( $this, 'show_page' ); if ( isset( $data['show_page_callback'] ) && is_callable( $data['show_page_callback'] ) ) { $callback = $data['show_page_callback']; } if ( isset( $data['set_callback_to_null'] ) && $data['set_callback_to_null'] ) { $callback = null; } /** * Add menu or submenu */ switch ( $data['menu'] ) { case 'comments': case 'dashboard': case 'links': case 'management': case 'media': case 'options': case 'pages': case 'plugins': case 'posts': case 'posts': case 'theme': case 'users': $function = sprintf( 'add_%s_page', $data['menu'] ); $this->pagehooks[ $key ] = $function( $data['page_title'], isset( $data['menu_title'] ) ? $data['menu_title'] : $data['page_title'], apply_filters( 'iworks_options_capability', 'manage_options', 'settings' ), $this->get_option_name( $key ), apply_filters( 'iworks_options_callback', $callback, $data, $this->options ), isset( $data['position'] ) ? floatval( $data['position'] ) : null ); add_action( 'load-' . $this->pagehooks[ $key ], array( $this, 'load_page' ) ); break; case 'top_level': $this->pagehooks[ $key ] = add_menu_page( $data['page_title'], isset( $data['menu_title'] ) ? $data['menu_title'] : $data['page_title'], apply_filters( 'iworks_options_capability', 'manage_options', 'settings' ), $this->get_option_name( $key ), apply_filters( 'iworks_options_callback', $callback, $data, $this->options ), apply_filters( 'iworks_options_icon_url', $data['icon_url'], $data ), isset( $data['position'] ) ? floatval( $data['position'] ) : null ); add_action( 'load-' . $this->pagehooks[ $key ], array( $this, 'load_page' ) ); break; default: if ( ! empty( $data['parent'] ) ) { $this->pagehooks[ $key ] = add_submenu_page( $data['parent'], $data['page_title'], isset( $data['menu_title'] ) ? $data['menu_title'] : $data['page_title'], apply_filters( 'iworks_options_capability', 'manage_options', 'settings' ), isset( $data['menu_slug'] ) ? $data['menu_slug'] : $this->get_option_name( $key ), apply_filters( 'iworks_options_callback', $callback, $data, $this->options ), isset( $data['position'] ) ? floatval( $data['position'] ) : null ); add_action( 'load-' . $this->pagehooks[ $key ], array( $this, 'load_page' ) ); } break; } } } /** * Get the version of the options class. * * @since 1.0.0 * * @return string The version of the options class. */ public function get_version() { return $this->version; } /** * Set the option function name. * * @since 1.0.0 * * @param string $option_function_name The option function name. */ public function set_option_function_name( $option_function_name ) { $this->option_function_name = $option_function_name; } /** * Get the option function name. * * @since 3.1.0 * * @return string The option function name. */ public function get_option_function_name() { return $this->option_function_name; } /** * Set the option prefix. * * @since 1.0.0 * * @param string $option_prefix The option prefix. */ public function set_option_prefix( $option_prefix ) { $this->option_prefix = $option_prefix; } /** * Get the option array. * * @since 1.0.0 * * @param string $option_group The option group. * * @return array The option array. */ private function get_option_array( $option_group = null ) { if ( null === $option_group ) { $option_group = $this->option_group; } $options = array(); if ( is_callable( $this->option_function_name ) ) { $options = apply_filters( $this->option_function_name, call_user_func( $this->option_function_name ) ); } if ( array_key_exists( $option_group, $options ) && ! empty( $options[ $option_group ] ) ) { $this->options[ $option_group ] = $options[ $option_group ]; return apply_filters( $this->option_function_name, $this->options[ $option_group ] ); } return apply_filters( $this->option_function_name, array() ); } /** * Build the options. * * @since 1.0.0 * * @param string $option_group The option group. * @param bool $echo Whether to echo the options. * @param int $term_id The term ID. * * @return void */ public function build_options( $option_group = 'index', $echo = true, $term_id = false ) { $this->option_group = $option_group; $options = $this->get_option_array(); /** * add some defaults */ $options['show_submit_button'] = true; $options['add_table'] = true; if ( ! array_key_exists( 'type', $options ) ) { $options['type'] = 'option'; } /** * add defaults for taxonomies */ if ( 'taxonomy' == $options['type'] ) { $options['show_submit_button'] = false; $options['add_table'] = false; } /** * check options exists? */ if ( ! is_array( $options['options'] ) ) { echo '
'; esc_html_e( 'An error occurred while getting the configuration.', 'reading-position-indicator' ); echo '
%s
%s
', wp_kses_post( $args['value'] ) ); } /** * Money input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The money input. */ private function money( $name, $value = '', $args = array() ) { if ( empty( $value ) || ! is_array( $value ) ) { $value = array(); } $value = wp_parse_args( $value, array( 'integer' => 0, 'fractional' => 0, 'currency' => false, ) ); $args = wp_parse_args( $args, array( 'kind' => 'complex', 'currency' => false, 'currency_default' => false, ) ); $content = ''; /** * Integer */ $n = sprintf( '%s[integer]', $name ); $content .= $this->input( $n, $value['integer'], array( 'min' => 0 ), 'number' ); if ( 'complex' === $args['kind'] ) { /** * fractional */ $n = sprintf( '%s[fractional]', $name ); $content .= $this->input( $n, $value['fractional'], array( 'min' => 0, 'max' => 99, ), 'number' ); } if ( is_array( $args['currency'] ) && ! empty( $args['currency'] ) ) { $n = sprintf( '%s[currency]', $name ); $atts = array( 'default' => $args['currency_default'], 'options' => $args['currency'], ); $content .= $this->select( $n, $value['currency'], $atts ); } return $content; } /** * Location input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The location input. */ private function location( $name, $value = '', $args = array() ) { if ( empty( $value ) || ! is_array( $value ) ) { $value = array(); } $defaults = array( 'country' => '', 'city' => '', 'street' => '', 'zip' => '', ); $i18n = array( 'country' => esc_html__( 'Country', 'reading-position-indicator' ), 'city' => esc_html__( 'City', 'reading-position-indicator' ), 'street' => esc_html__( 'Street', 'reading-position-indicator' ), 'zip' => esc_html__( 'ZIP code', 'reading-position-indicator' ), ); $value = wp_parse_args( $value, $defaults ); /** * Content */ $content = ''; foreach ( array_keys( $defaults ) as $key ) { $content .= sprintf( ''; } return $content; } /** * Enqueue scripts and styles. * * @since 2.6.4 * * @return void */ public function admin_head() { if ( false === $this->check_hooks_to_load_asses() ) { return; } $files = $this->get_files(); foreach ( $files as $data ) { if ( $data['style'] ) { wp_enqueue_style( $data['handle'] ); } else { wp_enqueue_script( $data['handle'] ); } } } /** * Convert color to rgb * * @since 2.4.1 * * @param string $hex Hex value of color * @return array RGB array. */ public function hex2rgb( $hex ) { $hex = str_replace( '#', '', $hex ); if ( strlen( $hex ) == 3 ) { $r = hexdec( substr( $hex, 0, 1 ) . substr( $hex, 0, 1 ) ); $g = hexdec( substr( $hex, 1, 1 ) . substr( $hex, 1, 1 ) ); $b = hexdec( substr( $hex, 2, 1 ) . substr( $hex, 2, 1 ) ); } else { $r = hexdec( substr( $hex, 0, 2 ) ); $g = hexdec( substr( $hex, 2, 2 ) ); $b = hexdec( substr( $hex, 4, 2 ) ); } $rgb = array( $r, $g, $b ); return $rgb; // returns an array with the rgb values } /** * Register styles and scripts. * * @since 2.6.4 * * @return void */ public function register_styles() { if ( false === $this->check_hooks_to_load_asses() ) { return; } $files = $this->get_files(); foreach ( $files as $data ) { $file = sprintf( 'assets/%s/%s', $data['style'] ? 'styles' : 'scripts', $data['file'] ); if ( 'theme' == $this->mode ) { $url = str_replace( get_template_directory(), '', __DIR__ ); $file = get_template_directory_uri() . $url . '/' . $file; } else { $file = plugins_url( $file, __FILE__ ); } $version = isset( $data['version'] ) ? $data['version'] : $this->version; $deps = isset( $data['deps'] ) ? $data['deps'] : array(); $in_footer = isset( $data['in_footer'] ) ? $data['in_footer'] : true; if ( $data['style'] ) { wp_register_style( $data['handle'], $file, $deps, $version ); } else { wp_register_script( $data['handle'], $file, $deps, $version, $in_footer ); if ( isset( $data['wp_localize_script'] ) ) { wp_localize_script( $data['handle'], $data['handle'], $data['wp_localize_script'] ); } } } } /** * Get files. * * @since 2.6.4 * * @return array The files. */ public function get_files() { $f = array( /** * iworks_options core files */ array( 'handle' => __CLASS__, 'file' => 'jquery-ui.min.css', ), array( 'handle' => __CLASS__, 'file' => 'common.js', 'deps' => array( 'jquery', 'switch_button', 'jquery-ui-tabs' ), 'wp_localize_script' => array( 'buttons' => array( 'select_media' => esc_html__( 'Select Image', 'reading-position-indicator' ), ), ), ), /** * switch checkbox */ array( 'handle' => 'switch_button', 'file' => 'jquery.switch_button.css', 'version' => '1.0', ), array( 'handle' => 'switch_button', 'file' => 'jquery.switch_button.js', 'version' => '1.0', 'deps' => array( 'jquery', 'jquery-effects-core', 'jquery-ui-widget' ), 'wp_localize_script' => $this->get_switch_button_data(), ), /** * select2 */ array( 'handle' => 'select2', 'file' => 'select2.min.css', 'version' => '4.0.13', ), array( 'handle' => 'select2', 'file' => 'select2.min.js', 'version' => '4.0.13', 'deps' => array( 'jquery' ), ), /** * options */ array( 'handle' => 'iworks-options', 'file' => 'options-admin.css', 'version' => $this->version, ), ); $files = array(); foreach ( $f as $data ) { $data['style'] = preg_match( '/css$/', $data['file'] ); $files[] = $data; } return $files; } /** * Get switch button data. * * @since 2.6.4 * * @return array The switch button data. */ public function get_switch_button_data() { $data = array( 'labels' => array( 'off_label' => esc_html__( 'OFF', 'reading-position-indicator' ), 'on_label' => esc_html__( 'ON', 'reading-position-indicator' ), ), ); return $data; } /** * Get option page * * @since 2.6.0 */ public function get_pagehook() { return $this->option_prefix . $this->option_group; } /** * Flush rewrite roles when it is configured * * @since 2.6.7 */ public function flush_rewrite_rules() { flush_rewrite_rules(); } /** * check to register or load assets * * check to register or load assets to avoid loading when it is not needed * * @since 2.8.0 */ private function check_hooks_to_load_asses() { if ( ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); if ( ! is_object( $screen ) ) { return false; } return in_array( $screen->id, $this->pagehooks ); } /** * Set plugin value * * @since 2.7.3 * * @param string $plugin Plugin file. */ public function set_plugin( $plugin ) { $this->plugin = $plugin; } /** * get nonce value * * @since 2.8.6 */ private function get_nonce_value() { $nonce_names = array( $this->get_nonce_name(), '_wpnonce' ); foreach ( $nonce_names as $nonce_name ) { if ( isset( $_REQUEST[ $nonce_name ] ) ) { return sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_value ] ) ); } } return new WP_Error( 'security', esc_html__( 'Failed Security Check', 'reading-position-indicator' ) ); } /** * get nonce name * * @since 2.8.6 */ private function get_nonce_name() { return apply_filters( 'iworks_options_nonce_name', 'iworks_options' ); } /** * get allowed tags * * @since 2.9.5 */ private function get_allowed_tags() { $tags = array( 'input' => array( 'accept' => true, 'alt' => true, 'aria-*' => true, 'autocomplete' => true, 'autofocus' => true, 'checked' => true, 'class' => true, 'data-*' => true, 'dirname' => true, 'disabled' => true, 'form' => true, 'formaction' => true, 'formenctype' => true, 'formmethod' => true, 'formnovalidate' => true, 'formtarget' => true, 'height' => true, 'id' => true, 'list' => true, 'max' => true, 'maxlength' => true, 'min' => true, 'minlength' => true, 'multiple' => true, 'name' => true, 'pattern' => true, 'placeholder' => true, 'popovertarget' => true, 'popovertargetaction' => true, 'readonly' => true, 'rel' => true, 'required' => true, 'size' => true, 'src' => true, 'step' => true, 'type' => true, 'value' => true, 'width' => true, ), 'button' => array( 'accept' => true, 'alt' => true, 'aria-*' => true, 'autocomplete' => true, 'autofocus' => true, 'checked' => true, 'class' => true, 'data-*' => true, 'dirname' => true, 'disabled' => true, 'form' => true, 'formaction' => true, 'formenctype' => true, 'formmethod' => true, 'formnovalidate' => true, 'formtarget' => true, 'height' => true, 'id' => true, 'list' => true, 'max' => true, 'maxlength' => true, 'min' => true, 'minlength' => true, 'multiple' => true, 'name' => true, 'pattern' => true, 'placeholder' => true, 'popovertarget' => true, 'popovertargetaction' => true, 'readonly' => true, 'rel' => true, 'required' => true, 'size' => true, 'src' => true, 'step' => true, 'type' => true, 'value' => true, 'width' => true, ), 'optgroup' => array( 'label' => true, 'class' => true, 'data-*' => true, 'aria-*' => true, 'id' => true, ), 'script' => array( 'aria-*' => true, 'async' => true, 'charset' => true, 'class' => true, 'crossorigin' => true, 'data-*' => true, 'defer' => true, 'disabled ' => true, 'id' => true, 'integrity' => true, 'language' => true, 'name' => true, 'nomodule' => true, 'src' => true, 'type' => true, ), 'style' => array( 'aria-*' => true, 'class' => true, 'data-*' => true, 'disabled ' => true, 'id' => true, 'media' => true, 'name' => true, 'scoped' => true, 'type' => true, ), 'select' => array( 'autocomplete' => true, 'autofocus' => true, 'disabled ' => true, 'form' => true, 'multiple' => true, 'name' => true, 'class' => true, 'data-*' => true, 'aria-*' => true, 'id' => true, 'required' => true, 'size' => true, ), 'option' => array( 'label' => true, 'disabled ' => true, 'value' => true, 'selected' => true, 'class' => true, 'data-*' => true, 'aria-*' => true, 'id' => true, ), 'textarea' => array( 'autocomplete' => true, 'autofocus' => true, 'cols' => true, 'dirname' => true, 'disabled' => true, 'form' => true, 'maxlength' => true, 'minlength' => true, 'name' => true, 'placeholder' => true, 'readonly' => true, 'required' => true, 'rows' => true, 'wrap' => true, 'class' => true, 'data-*' => true, 'aria-*' => true, 'id' => true, ), 'noscript' => array( 'class' => true, 'data-*' => true, 'aria-*' => true, 'id' => true, ), 'iframe' => array( 'allow' => true, 'allowfullscreen' => true, 'allowtransparency' => true, 'aria-*' => true, 'class' => true, 'data-*' => true, 'id' => true, 'height' => true, 'name' => true, 'sandbox' => true, 'scrolling' => true, 'src' => true, 'srcdoc' => true, 'style' => true, 'title' => true, 'width' => true, ), ); return apply_filters( 'iworks/options/wp_kses_allowed_html', wp_parse_args( $tags, wp_kses_allowed_html( 'post' ) ) ); } /** * Get the array of registered page hooks * * Retrieves all registered admin page hooks that have been added through this class. * * @since 3.0.3 * * @return array Associative array of page hooks where keys are page slugs * and values are the corresponding WordPress hook suffixes. */ public function get_pagehooks() { return $this->pagehooks; } /** * HTML INPUT ELEMENTS */ /** * Button input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The button input. */ private function button( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Checkbox HTML element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The checkbox. */ private function checkbox( $name, $value = '', $args = array() ) { if ( ! empty( $value ) ) { $args['checked'] = 'checked'; } return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Input type="color" element. * * @since 3.0.9 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The number input. */ private function color( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Date input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The date input. */ private function date( $name, $value = '', $args = array() ) { if ( ! isset( $args['class'] ) ) { $args['class'] = array(); } $args['class'][] = 'datepicker'; return $this->input( $name, $value, $args ); } /** * Input type="email" element. * * @since 3.0.9 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The number input. */ private function email( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Hidden input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The hidden input. */ private function hidden( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Input type="month" element. * * @since 3.0.9 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The number input. */ private function month( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Number input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The number input. */ private function number( $name, $value = '', $args = array() ) { return $this->input( $name, $value, $args, __FUNCTION__ ); } /** * Radio input element. * * @since 2.6.4 * * @param string $name The name. * @param mixed $value The value. * @param array $args The arguments. * * @return string The radio input. */ private function radio( $name, $value = '', $args = array() ) { $radio = ''; $options = $args['options']; unset( $args['options'] ); /** * default value */ if ( isset( $args['default'] ) && '' == $value ) { $value = $args['default']; } $i = 0; foreach ( $options as $option_value => $input ) { $id = sprintf( '%s%d', $name, $i++ ); $radio .= sprintf( '