* @link https://github.com/jrfnl/Debug-Bar-Shortcodes * @since 1.0 * @since 2.0 Class renamed - was: Debug_Bar_Shortcodes_Info * * @copyright 2013-2016 Juliette Reinders Folmer * @license http://creativecommons.org/licenses/GPL/2.0/ GNU General Public License, version 2 or higher */ // Avoid direct calls to this file. if ( ! function_exists( 'add_action' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * The classes in this file extend the functionality provided by the parent plugin "Debug Bar". */ if ( ! class_exists( 'Debug_Bar_Shortcodes_Render' ) ) : /** * Debug Bar Shortcodes - Debug Bar Panel Renderer. */ class Debug_Bar_Shortcodes_Render { /** * Plugin name for use in localization, class names etc. * * @var string $name */ public static $name = 'debug-bar-shortcodes'; /** * The amount of shortcodes before the table header will be doubled at the bottom of the table. * * @var int */ public $double_min = 8; /** * Render the actual panel. */ public function display() { $shortcodes = $GLOBALS['shortcode_tags']; $count = count( $shortcodes ); $double = ( ( $count >= $this->double_min ) ? true : false ); // Whether to repeat the row labels at the bottom of the table. echo '

', esc_html__( 'Total Registered Shortcodes:', 'debug-bar-shortcodes' ), '', absint( $count ), '

'; $output = ''; if ( is_array( $shortcodes ) && ! empty( $shortcodes ) ) { uksort( $shortcodes, 'strnatcasecmp' ); $is_singular = ( is_main_query() && is_singular() ); $header_row = $this->render_table_header( $is_singular ); $output .= ' ' . $header_row . ' ' . ( ( true === $double ) ? '' . $header_row . '' : '' ) . ' '; $i = 1; foreach ( $shortcodes as $shortcode => $callback ) { $sc_info = new Debug_Bar_Shortcode_Info( $shortcode ); $info = $sc_info->get_info_object(); $has_details = $sc_info->has_details(); $class = ( ( $i % 2 ) ? '' : ' class="even"' ); $output .= ' '; if ( true === $is_singular ) { $in_use = $this->has_shortcode( $shortcode ); $output .= ' '; unset( $in_use ); } $output .= ' '; if ( true === $has_details ) { $class = ( ( $i % 2 ) ? ' class="' . esc_attr( self::$name . '-details' ) . '"' : ' class="even ' . esc_attr( self::$name . '-details' ) . '"' ); $output .= ' '; } $i++; } unset( $shortcode, $callback, $sc_info, $info, $has_details, $class, $i ); $output .= '
' . $i . ' [' . esc_html( $shortcode ) . '] ' . $this->render_action_links( $shortcode, $has_details, $info ) . ' ' . $this->determine_callback_type( $callback ) . '' . $this->render_image_based_on_bool( array( 'true' => esc_html__( 'Shortcode is used', 'debug-bar-shortcodes' ), 'false' => esc_html__( 'Shortcode not used', 'debug-bar-shortcodes' ) ), $in_use, true ) . ' ' . ( ( true === $in_use ) ? $this->find_shortcode_usage( $shortcode ) : ' ' ) . '
  ' . $this->render_details_table( $shortcode, $info ) . '
'; } else { $output = '

' . esc_html__( 'No shortcodes found.', 'debug-bar-shortcodes' ) . '

'; } echo $output; // WPCS: xss ok. } /** * Generate the table header/footer row html. * * @param bool $is_singular Whether we are viewing a singular page/post/post type. * * @return string */ private function render_table_header( $is_singular ) { $output = ' # ' . esc_html__( 'Shortcode', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Rendered by', 'debug-bar-shortcodes' ) . ''; if ( true === $is_singular ) { $output .= ' ' . esc_html__( 'In use?', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Usage', 'debug-bar-shortcodes' ) . ''; } $output .= ''; return $output; } /** * Generate the action links for a shortcode. * * @param string $shortcode Current shortcode. * @param bool $has_details Whether or not the $info is equal to the defaults. * @param \Debug_Bar_Shortcode_Info_Defaults $info Shortcode info. * * @return string */ private function render_action_links( $shortcode, $has_details, $info ) { $links = array(); if ( true === $has_details ) { $links[] = '' . esc_html__( 'View details', 'debug-bar-shortcodes' ) . ''; } else { $links[] = '' . esc_html__( 'Retrieve details', 'debug-bar-shortcodes' ) . ''; } $links[] = '' . esc_html__( 'Find uses', 'debug-bar-shortcodes' ) . ''; if ( true === $has_details && '' !== $info->info_url ) { $links[] = $this->render_view_online_link( $info->info_url ); } return '
' . implode( ' | ', $links ) . '
'; } /** * Generate 'View online' link. * * @internal Separated from render_action_links() to also be able to use it as supplemental for ajax retrieve. * * @param string $url The URL to link to. * * @return string */ private function render_view_online_link( $url ) { return '' . esc_html__( 'View online', 'debug-bar-shortcodes' ) . ''; } /** * Function to retrieve a displayable string representing the callback. * * @internal Similar to callback determination in the Debug Bar Actions and Filters plugin, * keep them in line with each other. * * @param mixed $callback A callback. * * @return string */ private function determine_callback_type( $callback ) { if ( ( ! is_string( $callback ) && ! is_object( $callback ) ) && ( ! is_array( $callback ) || ( is_array( $callback ) && ( ! is_string( $callback[0] ) && ! is_object( $callback[0] ) ) ) ) ) { // Type 1 - not a callback. return ''; } elseif ( self::is_closure( $callback ) ) { // Type 2 - closure. return '[closure]'; } elseif ( ( is_array( $callback ) || is_object( $callback ) ) && self::is_closure( $callback[0] ) ) { // Type 3 - closure within an array/object. return '[closure]'; } elseif ( is_string( $callback ) && false === strpos( $callback, '::' ) ) { // Type 4 - simple string function (includes lambda's). return sanitize_text_field( $callback ) . '()'; } elseif ( is_string( $callback ) && false !== strpos( $callback, '::' ) ) { // Type 5 - static class method calls - string. return '[class] ' . str_replace( '::', ' :: ', sanitize_text_field( $callback ) ) . '()'; } elseif ( is_array( $callback ) && ( is_string( $callback[0] ) && is_string( $callback[1] ) ) ) { // Type 6 - static class method calls - array. return '[class] ' . sanitize_text_field( $callback[0] ) . ' :: ' . sanitize_text_field( $callback[1] ) . '()'; } elseif ( is_array( $callback ) && ( is_object( $callback[0] ) && is_string( $callback[1] ) ) ) { // Type 7 - object method calls. return '[object] ' . get_class( $callback[0] ) . ' -> ' . sanitize_text_field( $callback[1] ) . '()'; } else { // Type 8 - undetermined. return '
' . var_export( $callback, true ) . '
'; } } /** * Whether the current (singular) post contains the specified shortcode. * * Freely based on WP native implementation: * Source http://core.trac.wordpress.org/browser/trunk/src/wp-includes/shortcodes.php#L144 * Last compared against source: 2015-12-14. * * @global object $post Current post object. * * @static array $matches Regex matches for the post in the form [id] -> [matches]. * * @param string $shortcode The shortcode to check for. * * @return bool */ private function has_shortcode( $shortcode ) { static $matches; /* Have we got post content ? */ if ( ! is_object( $GLOBALS['post'] ) || ! isset( $GLOBALS['post']->post_content ) || '' === $GLOBALS['post']->post_content ) { return false; } $content = $GLOBALS['post']->post_content; // Current post. /* Use WP native function if available (WP 3.6+). */ if ( function_exists( 'has_shortcode' ) ) { return has_shortcode( $content, $shortcode ); } /* Otherwise use adjusted copy of the native function (WP < 3.6). */ $post_id = $GLOBALS['post']->ID; // Cache retrieved shortcode matches in a static for efficiency. if ( ! isset( $matches ) || ( is_array( $matches ) && ! isset( $matches[ $post_id ] ) ) ) { preg_match_all( '/' . get_shortcode_regex() . '/s', $content, $matches[ $post_id ], PREG_SET_ORDER ); } if ( empty( $matches[ $post_id ] ) ) { return false; } foreach ( $matches[ $post_id ] as $found ) { if ( $shortcode === $found[2] ) { return true; } elseif ( ! empty( $shortcode[5] ) && has_shortcode( $shortcode[5], $shortcode ) ) { return true; } } return false; } /** * Find the uses of a shortcode within the current post. * * @param string $shortcode The requested shortcode. * @param string|null $content (optional) Content to search through for the shortcode. * Defaults to the content of the current post/page/etc. * * @return string */ private function find_shortcode_usage( $shortcode, $content = null ) { $result = __( 'Not found', 'debug-bar-shortcodes' ); if ( ! isset( $content ) && ( ! isset( $GLOBALS['post'] ) || ! is_object( $GLOBALS['post'] ) || ! isset( $GLOBALS['post']->post_content ) ) ) { return $result; } if ( ! isset( $content ) ) { $content = $GLOBALS['post']->post_content; } $shortcode = preg_quote( $shortcode ); $regex = '`(?:^|[^\[])(\[' . $shortcode . '[^\]]*\])(?:.*?(\[/' . $shortcode . '\])(?:[^\]]|$))?`s'; $count = preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ); if ( is_int( $count ) && $count > 0 ) { // Only one result, keep it simple. if ( 1 === $count ) { $result = '' . esc_html( $matches[0][1] ); if ( isset( $matches[0][2] ) && '' !== $matches[0][2] ) { $result .= '…' . esc_html( $matches[0][2] ); } $result .= ''; } // More results, let's make it a neat list. else { $result = '
    '; foreach ( $matches as $match ) { $result .= '
  1. ' . esc_html( $match[1] ); if ( isset( $match[2] ) && '' !== $match[2] ) { $result .= '…' . esc_html( $match[2] ); } $result .= '
  2. '; } unset( $match ); $result .= '
'; } } return $result; } /** * Retrieve a html image tag based on a value. * * @param array $alt Array with only three allowed keys: * ['true'] => Alt value for true image. * ['false'] => Alt value for false image. * ['null'] => Alt value for null image (status unknown). * @param bool|null $bool The value to base the output on, either boolean or null. * @param bool $show_false Whether to show an image if false or to return an empty string. * @param bool $show_null Whether to show an image if null or to return an empty string. * * @return string */ private function render_image_based_on_bool( $alt = array(), $bool = null, $show_false = false, $show_null = false ) { static $images; if ( ! isset( $images ) ) { $images = array( 'true' => plugins_url( 'images/badge-circle-check-16.png', __FILE__ ), 'false' => plugins_url( 'images/badge-circle-cross-16.png', __FILE__ ), 'null' => plugins_url( 'images/help.png', __FILE__ ), ); } $img = ( ( isset( $bool ) ) ? ( ( true === $bool ) ? $images['true'] : $images['false'] ) : $images['null'] ); $alt_value = ''; if ( isset( $bool ) ) { if ( true === $bool && isset( $alt['true'] ) ) { $alt_value = $alt['true']; } elseif ( false === $bool && isset( $alt['false'] ) ) { $alt_value = $alt['false']; } } elseif ( isset( $alt['null'] ) ) { $alt_value = $alt['null']; } $title_tag = ''; $alt_tag = ''; if ( '' !== $alt_value ) { $title_tag = ' title="' . esc_attr( $alt_value ) . '"'; $alt_tag = ' alt="' . esc_attr( $alt_value ) . '"'; } $return = ''; if ( ( null === $bool && true === $show_null ) || ( true === $bool || ( false === $bool && true === $show_false ) ) ) { $return = ''; } return $return; } /** * Generate the html for a shortcode detailed info table. * * @param string $shortcode Current shortcode. * @param \Debug_Bar_Shortcode_Info_Defaults $info Shortcode info. * * @return string */ private function render_details_table( $shortcode, $info ) { $rows = array(); if ( '' !== $info->name ) { $rows['name'] = ' ' . esc_html__( 'Name', 'debug-bar-shortcodes' ) . ' ' . esc_html( $info->name ) . ' '; } if ( '' !== $info->description ) { $rows['description'] = ' ' . esc_html__( 'Description', 'debug-bar-shortcodes' ) . ' ' . $info->description . ' '; } $rows['syntax'] = $this->render_details_syntax_row( $shortcode, $info ); if ( '' !== $info->info_url ) { $rows['info_url'] = ' ' . esc_html__( 'Info Url', 'debug-bar-shortcodes' ) . ' ' . esc_html( $info->info_url ) . ' '; } if ( ! empty( $info->parameters['required'] ) ) { $rows['rp'] = $this->render_details_parameter_row( $info, 'required', __( 'Required parameters', 'debug-bar-shortcodes' ) ); } if ( ! empty( $info->parameters['optional'] ) ) { $rows['op'] = $this->render_details_parameter_row( $info, 'optional', __( 'Optional parameters', 'debug-bar-shortcodes' ) ); } /* Ignore the result if syntax is the only info row (as it's always there). */ if ( 1 >= count( $rows ) && isset( $rows['syntax'] ) ) { $output = ''; } else { $output = '

' . esc_html__( 'Shortcode details', 'debug-bar-shortcodes' ) . '

' . implode( $rows ) . '
'; } return $output; } /** * Generate the html for a shortcode detailed info table syntax row. * * @param string $shortcode Current shortcode. * @param \Debug_Bar_Shortcode_Info_Defaults $info Shortcode info. * * @return string */ private function render_details_syntax_row( $shortcode, $info ) { $row = ' ' . esc_html__( 'Syntax', 'debug-bar-shortcodes' ) . ' '; if ( isset( $info->self_closing ) ) { $param = ( ( ! empty( $info->parameters['required'] ) || ! empty( $info->parameters['optional'] ) ) ? ' [parameters] ' : '' ); if ( true === $info->self_closing ) { $row .= '[' . esc_html( $shortcode ) . $param . ' /]'; } else { $row .= '[' . esc_html( $shortcode ) . $param . '] … [/' . esc_html( $shortcode ) . ']'; } } else { $row .= '' . esc_html__( 'Unknown', 'debug-bar-shortcodes' ) . ''; } $row .= ' '; return $row; } /** * Generate the html for a shortcode detailed info table parameter row. * * @param \Debug_Bar_Shortcode_Info_Defaults $info Shortcode info. * @param string $type Parameter type: 'required' or 'optional'. * @param string $label Parameter label. * * @return string */ private function render_details_parameter_row( $info, $type, $label ) { $row = ' ' . esc_html( $label ) . ''; $first = true; foreach ( $info->parameters[ $type ] as $pm => $explain ) { if ( true !== $first ) { $row .= ' '; } else { $first = false; } $row .= ' ' . esc_html( $pm ) . ' ' . esc_html( $explain ) . ' '; } return $row; } /* ************** METHODS TO HANDLE AJAX REQUESTS ************** */ /** * Try and retrieve more information about the shortcode from the actual php code. * * @param string $shortcode Validated shortcode. * @param string $action The AJAX action which led to this function being called. * * @return void */ public function ajax_retrieve_details( $shortcode, $action ) { $sc_info = new Debug_Bar_Shortcode_Info( $shortcode, true ); if ( false === $sc_info->has_details() ) { $response = array( 'id' => 0, 'data' => '', 'action' => $action, ); $this->send_ajax_response( $response ); exit; } $info = $sc_info->get_info_object(); $response = array( 'id' => 1, 'data' => $this->render_details_table( $shortcode, $info ), 'action' => $action, 'tr_class' => self::$name . '-details', ); if ( isset( $info->info_url ) && '' !== $info->info_url ) { $response['supplemental'] = $this->render_view_online_link( $info->info_url ); } $this->send_ajax_response( $response ); exit; } /** * Find out if a shortcode is used anywhere. * * Liberally nicked from TR All Shortcodes plugin & adjusted based on WP posts-list-table code. * Source: https://wordpress.org/plugins/tr-all-shortcodes/ * Source: http://core.trac.wordpress.org/browser/trunk/src/wp-admin/includes/class-wp-posts-list-table.php#L473 * * @param string $shortcode Validated shortcode. * @param string $action The AJAX action which led to this function being called. * * @return void */ public function ajax_find_shortcode_uses( $shortcode, $action ) { // '_' is a wildcard in mysql, so escape it. $query = $GLOBALS['wpdb']->prepare( 'select * from `' . $GLOBALS['wpdb']->posts . '` where `post_status` <> "inherit" and `post_type` <> "attachment" and `post_content` like %s order by `post_type` ASC, `post_date` DESC;', '%[' . str_replace( '_', '\_', $shortcode ) . '%' ); $posts = $GLOBALS['wpdb']->get_results( $query ); /* Do we have posts ? */ if ( 0 === $GLOBALS['wpdb']->num_rows ) { $response = array( 'id' => 0, 'data' => '', 'action' => $action, ); $this->send_ajax_response( $response ); exit; } /* Ok, we've found some posts using the shortcode. */ $output = '

' . __( 'Shortcode found in the following posts/pages/etc:', 'debug-bar-shortcodes' ) . '

'; foreach ( $posts as $i => $post ) { $edit_link = get_edit_post_link( $post->ID ); $title = _draft_or_post_title( $post->ID ); $post_type_object = get_post_type_object( $post->post_type ); $can_edit_post = current_user_can( 'edit_post', $post->ID ); $post_status_string = $this->get_post_status_string( $post->post_status ); $actions = array(); if ( $can_edit_post && 'trash' !== $post->post_status ) { /* translators: no need to translate, WP standard translation will be used. */ $actions['edit'] = ''; /* translators: no need to translate, WP standard translation will be used. */ $actions['edit'] .= __( 'Edit' ) . ''; } if ( $post_type_object->public ) { if ( in_array( $post->post_status, array( 'pending', 'draft', 'future' ), true ) ) { if ( $can_edit_post ) { /* translators: no need to translate, WP standard translation will be used. */ $actions['view'] = ''; /* translators: no need to translate, WP standard translation will be used. */ $actions['view'] .= __( 'Preview' ) . ''; } } elseif ( 'trash' !== $post->post_status ) { /* translators: no need to translate, WP standard translation will be used. */ $actions['view'] = ''; /* translators: no need to translate, WP standard translation will be used. */ $actions['view'] .= __( 'View' ) . ''; } } $output .= ' '; } unset( $i, $post ); $output .= '
# ' . esc_html__( 'Title', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Post Type', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Status', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Author', 'debug-bar-shortcodes' ) . ' ' . esc_html__( 'Shortcode usage(s)', 'debug-bar-shortcodes' ) . '
' . ( $i + 1 ) . ' ' . $title . ''; if ( ! empty( $actions ) ) { $output .= '
' . implode( ' | ', $actions ) . '
'; } $output .= '
' . esc_html( $post_type_object->labels->singular_name ) . ' ' . esc_html( $post_status_string ) . ' ' . esc_html( get_the_author_meta( 'display_name', $post->post_author ) ) . ' ' . $this->find_shortcode_usage( $shortcode, $post->post_content ) . '
'; $response = array( 'id' => 1, 'data' => $output, 'action' => $action, 'tr_class' => self::$name . '-uses', ); $this->send_ajax_response( $response ); exit; } /** * Translate a post status keyword to a human readable localized string. * * @param string $post_status The post status to translate. * * @return string */ private function get_post_status_string( $post_status ) { switch ( $post_status ) { case 'publish': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Published' ); break; case 'future': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Scheduled' ); break; case 'private': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Private' ); break; case 'pending': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Pending Review' ); break; case 'draft': case 'auto-draft': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Draft' ); break; case 'trash': /* translators: no need to translate, WP standard translation will be used. */ $post_status_string = __( 'Trash' ); break; default: $post_status_string = __( 'Unknown', 'debug-bar-shortcodes' ); break; } return $post_status_string; } /** * Send ajax response. * * @param array $response Part response in the format: * [id] => 0 = no result, 1 = result. * [data] => html string (can be empty if no result). * [supplemental] => (optional) supplemental info to pass. * [tr_class] => (optional) class for the wrapping row. * * @return void */ public function send_ajax_response( $response ) { $tr_class = ''; if ( isset( $response['tr_class'] ) && '' !== $response['tr_class'] ) { $tr_class = ' class="' . esc_attr( $response['tr_class'] ) . '"'; } $data = ''; if ( '' !== $response['data'] ) { $data = '   ' . $response['data'] . ' '; } $supplemental = array(); // Only accounts for the expected new view online link, everything else will be buggered. if ( isset( $response['supplemental'] ) && '' !== $response['supplemental'] ) { $supplemental['url_link'] = ' | ' . $response['supplemental']; } /* Send the response. */ $ajax_response = new WP_Ajax_Response(); $ajax_response->add( array( 'what' => self::$name, 'action' => $response['action'], 'id' => $response['id'], 'data' => $data, 'supplemental' => $supplemental, ) ); $ajax_response->send(); exit; } /* ************** HELPER METHODS ************** */ /** * Check if a callback is a closure. * * @param mixed $arg Function name. * * @return bool */ public static function is_closure( $arg ) { if ( version_compare( PHP_VERSION, '5.3', '<' ) ) { return false; } include_once plugin_dir_path( __FILE__ ) . 'php53/php5.3-closure-test.php'; return debug_bar_shortcodes_is_closure( $arg ); } } // End of class Debug_Bar_Shortcodes_Render. endif; // End of if class_exists wrapper.