html->pointer() notification is based on current user... if ( ! did_action( 'set_current_user' ) ) { add_action( 'set_current_user', array( __CLASS__, 'instance' ) ); return null; } if ( null === $Inst ) { $Inst = new CustomSidebars(); } return $Inst; } /** * Private, since it is a singleton. * We directly initialize sidebar options when class is created. */ private function __construct() { add_action( 'admin_init', array( $this, 'admin_init' ) ); // Extensions use this hook to initialize themselfs. do_action( 'cs_init' ); } /** * Admin init * * @since 3.0.5 */ public function admin_init() { $plugin_title = 'Custom Sidebars'; /** * ID of the WP-Pointer used to introduce the plugin upon activation * * ========== Pointer ========== * Internal ID: wpmudcs1 [WPMUDev CustomSidebars 1] * Point at: #menu-appearance (Appearance menu item) * Title: Custom Sidebars * Description: Create and edit custom sidebars in your widget screen! * ------------------------------------------------------------------------- */ $user_id = get_current_user_id(); $dismissed_wp_pointers = get_user_meta( $user_id, 'dismissed_wp_pointers', true ); $dismissed_wp_pointers = explode( ',', $dismissed_wp_pointers ); if ( in_array( 'wpmudcs1', $dismissed_wp_pointers ) || wp_is_mobile() ) { lib3()->ui->add( 'core', 'widgets.php' ); } else { lib3()->ui->add( 'core' ); lib3()->html->pointer( 'wpmudcs1', // Internal Pointer-ID '#menu-appearance', // Point at $plugin_title, sprintf( __( 'Now you can create and edit custom sidebars in your ' . 'Widgets screen!', 'custom-sidebars' ), admin_url( 'widgets.php' ) ) // Body ); } // Find out if the page is loaded in accessibility mode. $flag = isset( $_GET['widgets-access'] ) ? $_GET['widgets-access'] : get_user_setting( 'widgets_access' ); self::$accessibility_mode = ( 'on' == $flag ); // We don't support accessibility mode. Display a note to the user. if ( true === self::$accessibility_mode ) { $nonce = wp_create_nonce( 'widgets-access' ); lib3()->ui->admin_message( sprintf( __( 'Accessibility mode is not supported by the %1$s plugin.
Click here to disable accessibility mode and use the %1$s plugin!', 'custom-sidebars' ), $plugin_title, admin_url( 'widgets.php?widgets-access=off&_wpnonce='.urlencode( $nonce ) ) ), 'err', 'widgets' ); } else { // Load javascripts/css files lib3()->ui->add( 'select', 'widgets.php' ); lib3()->ui->add( CSB_JS_URL . 'cs.min.js', 'widgets.php' ); lib3()->ui->add( CSB_CSS_URL . 'cs.css', 'widgets.php' ); lib3()->ui->add( CSB_CSS_URL . 'cs.css', 'edit.php' ); // AJAX actions add_action( 'wp_ajax_cs-ajax', array( $this, 'ajax_handler' ) ); // Display a message after import. if ( ! empty( $_GET['cs-msg'] ) ) { $msg = base64_decode( $_GET['cs-msg'] ); // Prevent XSS attacks... $kses_args = array( 'br' => array(), 'b' => array(), 'strong' => array(), 'i' => array(), 'em' => array(), ); $msg = wp_kses( $msg, $kses_args ); if ( ! empty( $msg ) ) { lib3()->ui->admin_message( $msg ); } } } /** * add links on plugin page. */ add_filter( 'plugin_action_links_' . plugin_basename( CSB_PLUGIN ), array( $this, 'add_action_links' ), 10, 4 ); add_action( 'admin_footer', array( $this, 'print_templates' ) ); } // ========================================================================= // == DATA ACCESS // ========================================================================= /** * * ==1== PLUGIN OPTIONS * Option-Key: cs_modifiable * * { * // Sidebars that can be replaced: * 'modifiable': [ * 'sidebar_1', * 'sidebar_2' * ], * * // Default replacements: * 'post_type_single': [ // Former "defaults" * 'post_type1': , * 'post_type2': * ], * 'post_type_archive': [ // Former "post_type_pages" * 'post_type1': , * 'post_type2': * ], * 'category_single': [ // Former "category_posts" * 'category_id1': , * 'category_id2': * ], * 'category_archive': [ // Former "category_pages" * 'category_id1': , * 'category_id2': * ], * 'blog': , * 'tags': , * 'authors': , * 'search': , * 'date': * } * * ==2== REPLACEMENT-DEF * Meta-Key: _cs_replacements * Option-Key: cs_modifiable * * { * 'sidebar_1': 'custom_sb_id1', * 'sidebar_2': 'custom_sb_id2' * } * * ==3== SIDEBAR DEFINITION * Option-Key: cs_sidebars * * Array of these objects * { * id: '', // sidebar-id * name: '', * description: '', * before_title: '', * after_title: '', * before_widget: '', * after_widget: '' * } * * ==4== WIDGET LIST * Option-Key: sidebars_widgets * * { * 'sidebar_id': [ * 'widget_id1', * 'widget_id2' * ], * 'sidebar_2': [ * ], * 'sidebar_3': [ * 'widget_id1', * 'widget_id3' * ], * } */ /** * If the specified variable is an array it will be returned. Otherwise * an empty array is returned. * * @since 2.0 * @param mixed $val1 Value that maybe is an array. * @param mixed $val2 Optional, Second value that maybe is an array. * @return array */ static public function get_array( $val1, $val2 = array() ) { if ( is_array( $val1 ) ) { return $val1; } elseif ( is_array( $val2 ) ) { return $val2; } else { return array(); } } /** * Returns a list with sidebars that were marked as "modifiable". * Also contains information on the default replacements of these sidebars. * * Option-Key: 'cs_modifiable' (1) */ static public function get_options( $key = null ) { static $Options = null; $need_update = false; if ( null === $Options ) { $Options = get_option( 'cs_modifiable', array() ); if ( ! is_array( $Options ) ) { $Options = array(); } // List of modifiable sidebars. if ( ! isset( $Options['modifiable'] ) || ! is_array( $Options['modifiable'] ) ) { // By default we make ALL theme sidebars replaceable: $all = self::get_sidebars( 'theme' ); $Options['modifiable'] = array_keys( $all ); $need_update = true; } /** * In version 2.0 four config values have been renamed and are * migrated in the following block: */ /** * set defaults */ $keys = array( 'authors', 'blog', 'category_archive', 'category_pages', 'category_posts', 'category_single', 'date', 'defaults', 'post_type_archive', 'post_type_pages', 'post_type_single', 'search', 'tags', ); foreach ( $keys as $k ) { if ( isset( $Options[ $k ] ) ) { continue; } $Options[ $k ] = null; } // Single/Archive pages - new names $Options['post_type_single'] = self::get_array( $Options['post_type_single'], // new name $Options['defaults'] // old name ); $Options['post_type_archive'] = self::get_array( $Options['post_type_archive'], // new name $Options['post_type_pages'] // old name ); $Options['category_single'] = self::get_array( $Options['category_single'], // new name $Options['category_posts'] // old name ); $Options['category_archive'] = self::get_array( $Options['category_archive'], // new name $Options['category_pages'] // old name ); // Remove old item names from the array. if ( isset( $Options['defaults'] ) ) { unset( $Options['defaults'] ); $need_update = true; } if ( isset( $Options['post_type_pages'] ) ) { unset( $Options['post_type_pages'] ); $need_update = true; } if ( isset( $Options['category_posts'] ) ) { unset( $Options['category_posts'] ); $need_update = true; } if ( isset( $Options['category_pages'] ) ) { unset( $Options['category_pages'] ); $need_update = true; } // Special archive pages $keys = array( 'blog', 'tags', 'authors', 'search', 'date' ); foreach ( $keys as $temporary_key ) { if ( isset( $Options[ $temporary_key ] ) ) { $Options[ $temporary_key ] = self::get_array( $Options[ $temporary_key ] ); } else { $Options[ $temporary_key ] = array(); } } $Options = self::validate_options( $Options ); if ( $need_update ) { self::set_options( $Options ); } } if ( ! empty( $key ) ) { return isset( $Options[ $key ] )? $Options[ $key ] : null; } else { return $Options; } } /** * Saves the sidebar options to DB. * * Option-Key: 'cs_modifiable' (1) * @since 2.0 * @param array $value The options array. */ static public function set_options( $value ) { // Permission check. if ( ! current_user_can( self::$cap_required ) ) { return; } update_option( 'cs_modifiable', $value ); } /** * Removes invalid settings from the options array. * * @since 1.0.4 * @param array $data This array will be validated and returned. * @return array */ static public function validate_options( $data = null ) { $data = (is_object( $data ) ? (array) $data : $data ); if ( ! is_array( $data ) ) { return array(); } $valid = array_keys( self::get_sidebars( 'theme' ) ); $current = array(); if ( isset( $data['modifiable'] ) ) { $current = self::get_array( $data['modifiable'] ); } // Get all the sidebars that are modifiable AND exist. $modifiable = array_intersect( $valid, $current ); $data['modifiable'] = $modifiable; return $data; } /** * Returns a list with all custom sidebars that were created by the user. * Array of custom sidebars * * Option-Key: 'cs_sidebars' (3) */ static public function get_custom_sidebars() { $sidebars = get_option( 'cs_sidebars', array() ); if ( ! is_array( $sidebars ) ) { $sidebars = array(); } // Remove invalid items. foreach ( $sidebars as $key => $data ) { if ( ! is_array( $data ) ) { unset( $sidebars[ $key ] ); } } return $sidebars; } /** * Saves the custom sidebars to DB. * * Option-Key: 'cs_sidebars' (3) * @since 2.0 */ static public function set_custom_sidebars( $value ) { // Permission check. if ( ! current_user_can( self::$cap_required ) ) { return; } update_option( 'cs_sidebars', $value ); } /** * Returns a list of all registered sidebars including a list of their * widgets (this is stored inside a WordPress core option). * * Option-Key: 'sidebars_widgets' (4) * @since 2.0 */ static public function get_sidebar_widgets() { return get_option( 'sidebars_widgets', array() ); } /** * Update the WordPress core settings for sidebar widgets: * 1. Add empty widget information for new sidebars. * 2. Remove widget information for sidebars that no longer exist. * * Option-Key: 'sidebars_widgets' (4) */ static public function refresh_sidebar_widgets() { // Contains an array of all sidebars and widgets inside each sidebar. $widgetized_sidebars = self::get_sidebar_widgets(); $cs_sidebars = self::get_custom_sidebars(); $delete_widgetized_sidebars = array(); foreach ( $widgetized_sidebars as $id => $bar ) { if ( substr( $id, 0, 3 ) == self::$sidebar_prefix ) { $found = false; foreach ( $cs_sidebars as $csbar ) { if ( $csbar['id'] == $id ) { $found = true; } } if ( ! $found ) { $delete_widgetized_sidebars[] = $id; } } } $all_ids = array_keys( $widgetized_sidebars ); foreach ( $cs_sidebars as $cs ) { $sb_id = $cs['id']; if ( ! in_array( $sb_id, $all_ids ) ) { $widgetized_sidebars[ $sb_id ] = array(); } } foreach ( $delete_widgetized_sidebars as $id ) { unset( $widgetized_sidebars[ $id ] ); } update_option( 'sidebars_widgets', $widgetized_sidebars ); } /** * Returns the custom sidebar metadata of a single post. * * Meta-Key: '_cs_replacements' (2) * @since 2.0 */ static public function get_post_meta( $post_id ) { $data = get_post_meta( $post_id, '_cs_replacements', true ); if ( ! is_array( $data ) ) { $data = array(); } return $data; } /** * Saves custom sidebar metadata to a single post. * * Meta-Key: '_cs_replacements' (2) * @since 2.0 * @param int $post_id * @param array $data When array is empty the meta data will be deleted. */ static public function set_post_meta( $post_id, $data ) { if ( ! empty( $data ) ) { update_post_meta( $post_id, '_cs_replacements', $data ); } else { delete_post_meta( $post_id, '_cs_replacements' ); } } /** * Returns a list of all sidebars available. * Depending on the parameter this will be either all sidebars or only * sidebars defined by the current theme. * * @param string $type [all|cust|theme] What kind of sidebars to return. */ static public function get_sidebars( $type = 'theme' ) { global $wp_registered_sidebars; $allsidebars = CustomSidebars::sort_sidebars_by_name( $wp_registered_sidebars ); $result = array(); // Remove inactive sidebars. foreach ( $allsidebars as $sb_id => $sidebar ) { if ( false !== strpos( $sidebar['class'], 'inactive-sidebar' ) ) { unset( $allsidebars[ $sb_id ] ); } } ksort( $allsidebars ); if ( 'all' == $type ) { $result = $allsidebars; } elseif ( 'cust' == $type ) { foreach ( $allsidebars as $key => $sb ) { // Only keep custom sidebars in the results. if ( substr( $key, 0, 3 ) == self::$sidebar_prefix ) { $result[ $key ] = $sb; } } } elseif ( 'theme' == $type ) { foreach ( $allsidebars as $key => $sb ) { // Remove custom sidebars from results. if ( substr( $key, 0, 3 ) != self::$sidebar_prefix ) { $result[ $key ] = $sb; } } } return $result; } /** * Returns the sidebar with the specified ID. * Sidebar can be both a custom sidebar or theme sidebar. * * @param string $id Sidebar-ID. * @param string $type [all|cust|theme] What kind of sidebars to check. */ static public function get_sidebar( $id, $type = 'all' ) { if ( empty( $id ) ) { return false; } // Get all sidebars $sidebars = self::get_sidebars( $type ); if ( isset( $sidebars[ $id ] ) ) { return $sidebars[ $id ]; } else { return false; } } /** * Get sidebar replacement information for a single post. */ static public function get_replacements( $postid ) { $replacements = self::get_post_meta( $postid ); if ( ! is_array( $replacements ) ) { $replacements = array(); } else { $replacements = $replacements; } return $replacements; } /** * Returns true, when the specified post type supports custom sidebars. * * @since 2.0 * @param object|string $posttype The posttype to validate. Either the * posttype name or the full posttype object. * @return bool */ static public function supported_post_type( $posttype ) { $Ignored_types = null; $Response = array(); if ( null === $Ignored_types ) { $Ignored_types = get_post_types( array( 'public' => false ), 'names' ); $Ignored_types[] = 'attachment'; } if ( is_object( $posttype ) ) { $posttype = $posttype->name; } if ( ! isset( $Response[ $posttype ] ) ) { $response = ! in_array( $posttype, $Ignored_types ); /** * Filters the support-flag. The flag defines if the posttype supports * custom sidebars or not. * * @since 2.0 * * @param bool $response Flag if the posttype is supported. * @param string $posttype Name of the posttype that is checked. */ $response = apply_filters( 'cs_support_posttype', $response, $posttype ); $Response[ $posttype ] = $response; } return $Response[ $posttype ]; } /** * Returns a list of all post types that support custom sidebars. * * @uses self::supported_post_type() * @param string $type [names|objects] Defines details of return data. * @return array List of posttype names or objects, depending on the param. */ static public function get_post_types( $type = 'names' ) { $Valid = array(); if ( 'objects' != $type ) { $type = 'names'; } if ( ! isset( $Valid[ $type ] ) ) { $all = get_post_types( array(), $type ); $Valid[ $type ] = array(); foreach ( $all as $post_type ) { if ( self::supported_post_type( $post_type ) ) { $Valid[ $type ][] = $post_type; } } } return $Valid[ $type ]; } /** * Returns an array of all categories. * * @since 2.0 * @return array List of categories, including empty ones. */ static public function get_all_categories() { $args = array( 'hide_empty' => 0, 'taxonomy' => 'category', ); return get_categories( $args ); } /** * Returns a sorted list of all category terms of the current post. * This information is used to find sidebar replacements. * * @uses self::cmp_cat_level() */ static public function get_sorted_categories( $post_id = null ) { static $Sorted = array(); // Return categories of current post when no post_id is specified. $post_id = empty( $post_id ) ? get_the_ID() : $post_id; if ( ! isset( $Sorted[ $post_id ] ) ) { $Sorted[ $post_id ] = get_the_category( $post_id ); usort( $Sorted[ $post_id ], array( __CLASS__, 'cmp_cat_level' ) ); } return $Sorted[ $post_id ]; } /** * Helper function used to sort categories. * * @uses self::get_category_level() */ static public function cmp_cat_level( $cat1, $cat2 ) { $l1 = self::get_category_level( $cat1->cat_ID ); $l2 = self::get_category_level( $cat2->cat_ID ); if ( $l1 == $l2 ) { return strcasecmp( $cat1->name, $cat1->name ); } else { return $l1 < $l2 ? 1 : -1; } } /** * Helper function used to sort categories. */ static public function get_category_level( $catid ) { if ( ! $catid ) { return 0; } $cat = get_category( $catid ); return 1 + self::get_category_level( $cat->category_parent ); } // ========================================================================= // == AJAX FUNCTIONS // ========================================================================= /** * Output JSON data and die() * * @since 1.0.0 */ static protected function json_response( $obj ) { // Flush any output that was made prior to this function call while ( 0 < ob_get_level() ) { ob_end_clean(); } header( 'Content-Type: application/json' ); echo json_encode( (object) $obj ); die(); } /** * Output HTML data and die() * * @since 2.0 */ static protected function plain_response( $data ) { // Flush any output that was made prior to this function call while ( 0 < ob_get_level() ) { ob_end_clean(); } header( 'Content-Type: text/plain' ); echo '' . $data; die(); } /** * Sets the response object to ERR state with the specified message/reason. * * @since 2.0 * @param object $req Initial response object. * @param string $message Error message or reason; already translated. * @return object Updated response object. */ static protected function req_err( $req, $message ) { $req->status = 'ERR'; $req->message = $message; return $req; } /** * All Ajax request are handled by this function. * It analyzes the post-data and calls the required functions to execute * the requested action. * * -------------------------------- * * IMPORTANT! ANY SERVER RESPONSE MUST BE MADE VIA ONE OF THESE FUNCTIONS! * Using direct `echo` or include an html file will not work. * * self::json_response( $obj ) * self::plain_response( $text ) * * -------------------------------- * * @since 1.0.0 */ public function ajax_handler() { // Permission check. if ( ! current_user_can( self::$cap_required ) ) { return; } // Try to disable debug output for ajax handlers of this plugin. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { defined( 'WP_DEBUG_DISPLAY' ) || define( 'WP_DEBUG_DISPLAY', false ); defined( 'WP_DEBUG_LOG' ) || define( 'WP_DEBUG_LOG', true ); } // Catch any unexpected output via output buffering. ob_start(); $action = isset( $_POST['do'] )? $_POST['do']:null; $get_action = isset( $_GET['do'] )? $_GET['do']:null; /** * Notify all extensions about the ajax call. * * @since 2.0 * @param string $action The specified ajax action. */ do_action( 'cs_ajax_request', $action ); /** * Notify all extensions about the GET ajax call. * * @since 2.0.9.7 * @param string $action The specified ajax action. */ do_action( 'cs_ajax_request_get', $get_action ); } /** * This function will sort an array by key 'name'. * * @since 2.1.1.2 * * @param $a Mixed - first value to compare. * @param $b Mixed - secound value to compare. * @return integer value of comparation. */ public static function sort_sidebars_cmp_function( $a, $b ) { if ( ! isset( $a['name'] ) || ! isset( $b['name'] ) ) { return 0; } if ( function_exists( 'mb_strtolower' ) ) { $a_name = mb_strtolower( $a['name'] ); $b_name = mb_strtolower( $b['name'] ); } else { $a_name = strtolower( $a['name'] ); $b_name = strtolower( $b['name'] ); } if ( $a_name == $b_name ) { return 0; } return ($a_name < $b_name ) ? -1 : 1; } /** * Returns sidebars sorted by name. * * @since 2.1.1.2 * * @param array $available Array of sidebars. * @return array Sorted array of sidebars. */ public static function sort_sidebars_by_name( $available ) { if ( empty( $available ) ) { return $available; } foreach ( $available as $key => $data ) { $available[ $key ]['cs-key'] = $key; } usort( $available, array( __CLASS__, 'sort_sidebars_cmp_function' ) ); $sorted = array(); foreach ( $available as $data ) { $sorted[ $data['cs-key'] ] = $data; } return $sorted; } /** * Add "support" and (configure) "widgets" on plugin list page * * @since 2.1.1.8 * */ public function add_action_links( $actions, $plugin_file, $plugin_data, $context ) { if ( current_user_can( 'edit_theme_options' ) ) { $actions['widgets'] = sprintf( '%s', esc_url( admin_url( 'widgets.php' ) ), __( 'Widgets', 'custom-sidebars' ) ); } $url = 'https://wordpress.org/support/plugin/custom-sidebars'; $actions['support'] = sprintf( '%s', esc_url( $url ), __( 'Support', 'custom-sidebars' ) ); return $actions; } /** * Print JavaScript template. * * @since 3.0.1 */ public function print_templates() { wp_enqueue_script( 'wp-util' ); ?>