* @copyright Copyright (c) 2016 * @link http://jeremyhixon.com * @version 1.0.0 */ class RationalOptionPages { /* ========================================================================== Vars ========================================================================== */ protected $attributes = array( 'input' => array( 'autocomplete' => false, 'autofocus' => false, 'disabled' => false, 'list' => false, 'max' => false, 'maxlength' => false, 'min' => false, 'pattern' => false, 'readonly' => false, 'required' => false, 'size' => false, 'step' => false, ), 'select' => array( 'multiple' => false, 'size' => 4, ), 'textarea' => array( 'cols' => 20, 'rows' => 2, 'wrap' => 'soft', ), ); protected $defaults = array( 'add_menu_page' => array( 'page_title' => 'Option Page', 'menu_title' => 'Option Page', 'capability' => 'manage_options', 'menu_slug' => 'option_page', 'callback' => false, 'icon_url' => false, 'position' => null, ), 'add_settings_field' => array( 'id' => 'settings_field', 'title' => 'Settings Field', 'callback' => false, 'page' => 'option_page', 'section' => 'settings_section', 'args' => false, ), 'add_settings_section' => array( 'id' => 'settings_section', 'title' => 'Settings Section', 'callback' => false, 'page' => 'option_page', ), 'add_submenu_page' => array( 'parent_slug' => 'option_page', 'page_title' => 'Sub Option Page', 'menu_title' => 'Sub Option Page', 'capability' => 'manage_options', 'menu_slug' => 'sub_option_page', 'callback' => false, ), ); protected $errors; protected $fields = array( 'checkbox' => array( 'checked' => false, 'value' => 'on', ), 'text' => array( 'class' => 'regular-text', 'placeholder' => '', 'value' => false, ), 'textarea' => array( 'class' => 'large-text', 'placeholder' => '', 'rows' => 10, 'value' => false, ), 'wp_editor' => array( 'wpautop' => true, 'media_buttons' => true, 'textarea_rows' => 'default', 'tabindex' => false, 'editor_css' => false, 'editor_class' => '', 'editor_height' => false, 'teeny' => false, 'dfw' => false, 'tinymce' => true, 'quicktags' => true, 'drag_drop_upload' => false, ), ); protected $media_script = false; protected $notices; protected $options; protected $pages = array(); protected $subpages = array(); protected $points; /* ========================================================================== Magic methods ========================================================================== */ /** * Catches unknown method calls * * @param string $method The method being requested * @param array $arguments Array of arguments passed to the method */ public function __call( $method, $arguments ) { $request = explode( '|', $method ); $source = $request[0]; $page_key = !empty( $request[1] ) ? $request[1] : false; $section_key = !empty( $request[2] ) ? $request[2] : false; $field_key = !empty( $request[3] ) ? $request[3] : false; switch ( $source ) { case 'add_menu_page': case 'add_submenu_page': $this->build_menu_page( $page_key ); break; case 'add_settings_section': $this->build_settings_section( $page_key, $section_key ); break; case 'add_settings_field': $this->build_settings_field( $page_key, $section_key, $field_key ); break; case 'register_setting': $input = $this->sanitize_setting( $page_key, $arguments[0] ); return $input; break; default: $this->submit_notice( $method ); } } /** * Class construct method. Configures class and hooks into WordPress. * * @param array $pages Array of option pages */ public function __construct( $pages = array() ) { foreach ( $pages as $page_key => $page_params ) { $this->pages[ $page_key ] = $this->validate_page( $page_key, $page_params ); } $this->pages = array_merge( $this->pages, $this->subpages ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); add_action( 'admin_head', array( $this, 'admin_head' ) ); add_action( 'admin_init', array( $this, 'admin_init' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); } /* ========================================================================== WordPress hooks ========================================================================== */ /** * Action: admin_enqueue_scripts * Conditionally queue's up jQuery and the media uploader script */ public function admin_enqueue_scripts() { if ( $this->media_script ) { wp_enqueue_script( 'jquery' ); wp_enqueue_media(); } } /** * Action: admin_head * Conditionally adds the script to manage media uploads */ public function admin_head() { if ( $this->media_script ) { ?> pages as $page_key => $page_params ) { // Finalize sanitize if ( empty( $page_params['custom'] ) && !is_array( $page_params['sanitize'] ) ) { $page_params['sanitize'] = array( $this, $page_params['sanitize'] ); } register_setting( $page_key, $page_key, $page_params['sanitize'] ); if ( !empty( $page_params['sections'] ) ) { foreach ( $page_params['sections'] as $section_key => $section_params ) { // Sort and trim the array for the function $sort_order = array_keys( $this->defaults['add_settings_section'] ); $params = $this->sort_array( $section_params, $sort_order ); $params = array_slice( $params, 0, count( $this->defaults['add_settings_section'] ) ); // Finalize callback if ( empty( $params['custom'] ) && !is_array( $params['callback'] ) ) { $params['callback'] = array( $this, $params['callback'] ); } call_user_func_array( 'add_settings_section', array_values( $params ) ); if ( !empty( $section_params['fields'] ) ) { foreach ( $section_params['fields'] as $field_key => $field_params ) { // Check for "media" type for adding script if ( !$this->media_script && $field_params['type'] === 'media' ) { $this->media_script = true; } // Sort and trim the array for the function $sort_order = array_keys( $this->defaults['add_settings_field'] ); $params = $this->sort_array( $field_params, $sort_order ); $params = array_slice( $params, 0, count( $this->defaults['add_settings_field'] ) ); // Add label wrapper on title if ( !in_array( $field_params['type'], array( 'radio' ) ) && ( empty( $field_params['no_label'] ) || $field_params['no_label'] === false ) ) { $params['title'] = ""; } // Finalize callback if ( empty( $params['custom'] ) && !is_array( $params['callback'] ) ) { $params['callback'] = array( $this, $params['callback'] ); } call_user_func_array( 'add_settings_field', array_values( $params ) ); } } } } } } /** * Action: admin_menu. Adding the option pages to the admin menu. */ public function admin_menu() { $all_pages = array_merge( $this->pages, $this->subpages ); foreach ( $all_pages as $page ) { // Sort and trim the array for the function $sort_order = array_keys( $this->defaults[ $page['function'] ] ); $params = $this->sort_array( $page, $sort_order ); $params = array_slice( $params, 0, count( $this->defaults[ $page['function'] ] ) ); // Finalize callback $params['callback'] = array( $this, $params['callback'] ); call_user_func_array( $page['function'], array_values( $params ) ); } } /** * Action: admin_notices. Spitting out notices when needed. */ public function admin_notices() { // notice-error, notice-warning, notice-success, or notice-info. if ( !empty( $this->errors ) ) { foreach ( $this->errors as $error ) { echo $error; } } if ( !empty( $this->notices ) ) { foreach ( $this->notices as $notice ) { echo $notice; } } // update point in array for future reference $this->points['admin_notices'] = true; } /* ========================================================================== Helpers ========================================================================== */ public function add_page( $page_key, $page_params ) { $this->pages[ $page_key ] = $this->validate_page( $page_key, $page_params ); } /** * Builds the menu page * * @param string $page_key The array key of the page needing built */ protected function build_menu_page( $page_key ) { $page = $this->pages[ $page_key ]; $this->options = get_option( $page_key, array() ); settings_errors(); ?>
'.__($field['text'],'text-domain').'
' : '' // text ); break; case 'radio': echo ''; break; case 'select': if (!empty($field['attributes']) && isset($field['attributes']['multiple']) && $field['attributes']['multiple']) { $field_tag_name = "{$page_key}[{$field['id']}][]"; $field_name = "{$field['id']}[]"; } else { $field_tag_name = "{$page_key}[{$field['id']}]"; $field_name = "{$field['id']}"; } printf( ''; break; case 'textarea': printf( '%s', !empty( $field['class'] ) ? "class='{$field['class']}'" : '', // class $field['id'], // id "{$page_key}[{$field['id']}]", // name !empty( $field['placeholder'] ) ? 'placeholder="'.__($field['placeholder'],'text-domain').'"' : '', // placeholder !empty( $field['rows'] ) ? "rows='{$field['rows']}'" : '', // rows !empty( $field['cols'] ) ? "cols='{$field['cols']}'" : '', // cols !empty( $field['wrap'] ) ? "wrap='{$field['wrap']}'" : '', // wrap $field['title_attr'], // title $field['value'], // value !empty( $field['text'] ) ? ''.__($field['text'],'text-domain').'
' : '' // text ); break; case 'wp_editor': $field['textarea_name'] = "{$page_key}[{$field['id']}]"; wp_editor( isset( $field['value'] ) ? $field['value'] : '', $field['id'], array( 'textarea_name' => $field['textarea_name'], ) ); echo !empty( $field['text'] ) ? ''.__($field['text'],'text-domain').'
' : ''; break; default: printf( '%s', !empty( $field['class'] ) ? "class='{$field['class']}'" : '', // class $field['id'], // id "{$page_key}[{$field['id']}]", // name !empty( $field['placeholder'] ) ? 'placeholder="'.__($field['placeholder'],'text-domain').'"' : '', // placeholder $field['title_attr'], // title $field['type'], // type $field['value'], // value !empty( $attributes ) ? implode( ' ', $attributes ) : '', // additional attributes !empty( $field['text'] ) ? ''.__($field['text'],'text-domain').'
' : '' // text ); } } /** * Builds the settings sections * * @param string $page_key The array key of the page * @param type $section_key The array key of the section */ protected function build_settings_section( $page_key, $section_key ) { $page = $this->pages[ $page_key ]; $section = $page['sections'][ $section_key ]; echo !empty( $section['text'] ) ? $section['text'] : ''; if ( !empty( $section['include'] ) ) { include $section['include']; } } /** * Determines if the option page has fields or not * * @param array $page The page array * * @return boolean True if fields are found, false otherwise */ protected function has_fields( $page ) { if ( !empty( $page['sections'] ) ) { foreach ( $page['sections'] as $section ) { if ( !empty( $section['fields'] ) ) { return true; } } } return false; } /** * Cleans up the option page submissions before submitting to the DB * * @param string $page_key The array key of the page * * @return array The sanitized post input */ protected function sanitize_setting( $page_key, $input ) { $page = $this->pages[ $page_key ]; if ( !empty( $page['sections'] ) ) { foreach ( $page['sections'] as $section ) { if ( !empty( $section['fields'] ) ) { foreach ( $section['fields'] as $field ) { if ( isset( $field['sanitize'] ) && !$field['sanitize'] ) { continue; } switch ( $field['type'] ) { case 'select': if ( !empty($input[$field['id']]) && !empty($input['attributes']['multiple']) ) { $input[ $field['id'] ] = $input[ $field['id'] ]; } break; case 'checkbox': if ( empty( $input[ $field['id'] ] ) ) { $input[ $field['id'] ] = false; } break; case 'wp_editor': $input[ $field['id'] ] = wp_kses_post( $input[ $field['id'] ] ); break; default: // Sanitize by default; skip if this field's 'sanitize' setting is false. $input[ $field['id'] ] = sanitize_text_field( $input[ $field['id'] ] ); } } } } } return $input; } /** * Converts human-readable strings into more machine-friendly formats * * @param string $text String to be formatted * @param string $separator The character that fills in spaces * * @return string Formatted text */ protected function slugify( $text, $separator = '_' ) { $text = preg_replace( '~[^\\pL\d]+~u', $separator, $text ); $text = trim( $text, $separator ); $text = iconv( 'utf-8', 'windows-1251//TRANSLIT', $text ); $text = strtolower( $text ); $text = preg_replace( '~[^-\w]+~', '', $text ); if ( empty( $text ) ) { return 'n-a'; } return $text; } /** * Sorts one array using a second as a guide * * @param array $array Array to be sorted * @param array $order_array Guide array * * @return array Sorted array */ protected function sort_array( $array, $order_array ) { $ordered = array(); foreach ( $order_array as $key ) { if ( array_key_exists( $key, $array ) ) { $ordered[ $key ] = $array[ $key ]; unset( $array[ $key ] ); } } return $ordered + $array; } /** * Conditionally outputs an error in WordPress admin * * @param string $error The error to be output */ public function submit_error( $error ) { $error = sprintf( '%s
' . htmlspecialchars( print_r( $error, true ) ) . '' : $error ); if ( empty( $this->points['admin_notices'] ) ) { $this->errors[] = $error; } else { echo $error; } } /** * Conditionally outputs a notice in WordPress admin * * @param string $notice The text to be output */ public function submit_notice( $notice ) { $notice = sprintf( '
%s
' . htmlspecialchars( print_r( $notice, true ) ) . '' : $notice ); if ( empty( $this->points['admin_notices'] ) ) { $this->notices[] = $notice; } else { echo $notice; } } /** * Validates the field data submitted to the class * * @param array $field Field array * @param string $page_key Array key of the associated page * @param string $section_key Array key of the associated section * @param string $field_key Array key of the field * @param string $page ID of the associated page * @param type $section ID of the associated section * * @return array The validated field array */ protected function validate_field( $field, $page_key, $section_key, $field_key, $page, $section ) { // Label if ( empty( $field['title'] ) ) { $this->submit_error( __('Field parameter "title" is required','text-domain') ); } // ID if ( empty( $field['id'] ) ) { $field['id'] = $this->slugify( $field['title'] ); } // Callback $field['callback'] = empty( $field['callback'] ) ? "add_settings_field|{$page_key}|{$section_key}|{$field_key}" : $field['callback']; // Page $field['page'] = $page; // Section $field['section'] = $section; // Type $field['type'] = empty( $field['type'] ) ? 'text' : $field['type']; // Title attribute $field['title_attr'] = empty( $field['title_attr'] ) ? __($field['title'],'text-domain') : $field['title_attr']; // Choices if ( empty( $field['choices'] ) && in_array( $field['type'], array( 'radio', 'select' ) ) ) { $this->submit_error( 'Field parameter "choices" is required for the "radio" and "select" type' ); } // Other attributes if ( !empty( $field['attributes'] ) ) { switch ( $field['type'] ) { case 'select': case 'textarea': $field['attributes'] = wp_parse_args( $field['attributes'], $this->attributes[ $field['type'] ] ); break; default: $field['attributes'] = wp_parse_args( $field['attributes'], $this->attributes['input'] ); } } // Making sure we haven't missed anything switch ( $field['type'] ) { case 'checkbox': $field = wp_parse_args( $field, $this->fields['checkbox'] ); break; case 'color': case 'radio': case 'range': break; case 'date': $field['value'] = date( 'Y-m-d', strtotime( $field['value'] ) ); $field = wp_parse_args( $field, $this->fields['text'] ); break; case 'datetime': case 'datetime-local': $field['value'] = date( 'Y-m-d\TH:i:s', strtotime( $field['value'] ) ); $field = wp_parse_args( $field, $this->fields['text'] ); break; case 'month': $field['value'] = date( 'Y-m', strtotime( $field['value'] ) ); $field = wp_parse_args( $field, $this->fields['text'] ); break; case 'textarea': $field = wp_parse_args( $field, $this->fields[ $field['type'] ] ); break; case 'time': $field['value'] = date( 'H:i:s', strtotime( $field['value'] ) ); $field = wp_parse_args( $field, $this->fields['text'] ); break; case 'week': $field['value'] = date( 'Y-\WW', strtotime( $field['value'] ) ); $field = wp_parse_args( $field, $this->fields['text'] ); break; case 'wp_editor': $field = wp_parse_args( $field, $this->fields['wp_editor'] ); break; default: $field = wp_parse_args( $field, $this->fields['text'] ); } return $field; } /** * Validates the information submitted to the class * * @param string $page_key Array key of the page * @param array $page Array of page parameters * @param string $parent_slug Menu slug of the parent page if there is one * * @return array Validated array of page parameters */ protected function validate_page( $page_key, $page_params, $parent_slug = false ) { // Page title if ( empty( $page_params['page_title'] ) ) { $this->submit_error( __('Page parameter "page_title" is required','text-domain') ); } // Menu title if ( empty( $page_params['menu_title'] ) ) { $page_params['menu_title'] = __($page_params['page_title'],'text-domain'); } // Menu slug if ( empty( $page_params['menu_slug'] ) ) { // Basing it off the page title cause it's likely to be more unique than the menu title $page_params['menu_slug'] = $this->slugify( $page_params['page_title'] ); } // Menu or submenu item? if ( empty( $page_params['parent_slug'] ) && !$parent_slug ) { $page_params['function'] = 'add_menu_page'; } else { $page_params['function'] = 'add_submenu_page'; $page_params['parent_slug'] = $parent_slug ? $parent_slug : $page_params['parent_slug']; } // Callback $page_params['callback'] = "{$page_params['function']}|{$page_key}"; // Sanitize $page_params['sanitize'] = empty( $page_params['sanitize'] ) ? "register_setting|{$page_key}" : $page_params['sanitize']; // Make sure we haven't missed anything $page_params = wp_parse_args( $page_params, $this->defaults[ $page_params['function'] ] ); // Subpages? if ( !empty( $page_params['subpages'] ) ) { foreach ( $page_params['subpages'] as $subpage_key => $subpage ) { $this->subpages[ $subpage_key ] = $this->validate_page( $subpage_key, $subpage, $page_params['menu_slug'] ); } unset( $page_params['subpages'] ); } // Sections? if ( !empty( $page_params['sections'] ) ) { foreach ( $page_params['sections'] as $section_key => $section_params ) { $page_params['sections'][ $section_key ] = $this->validate_section( $section_params, $page_key, $section_key, $page_params['menu_slug'] ); } } return $page_params; } /** * Validates the section data submitted to the class * * @param array $section Section array * @param string $page_key Array key of the associated page * @param string $section_key Array key of the associated page * @param string $page ID of the associated page * * @return array Validated section array */ protected function validate_section( $section, $page_key, $section_key, $page ) { // Title if ( empty( $section['title'] ) ) { $this->submit_error( __('Section parameter "title" is required','text-domain') ); } // ID if ( empty( $section['id'] ) ) { $section['id'] = $this->slugify( $section['title'] ); } // Callback $section['callback'] = empty( $section['callback'] ) ? "add_settings_section|{$page_key}|{$section_key}" : $section['callback']; // Page $section['page'] = $page; // Fields? if ( !empty( $section['fields'] ) ) { foreach ( $section['fields'] as $field_key => $field_params ) { $section['fields'][ $field_key ] = $this->validate_field( $field_params, $page_key, $section_key, $field_key, $page, $section['id'] ); } } return $section; } }