Add number new posts (post_status = pending) to administration menu [duplicate]

Normally everything is contained in $labels['menu_name'] argument on cpt registration is printed in the menu, so the right place where put the count span is there, but there are 2 problems:

  1. You have to dinamically count posts, so you can’t just write there the html
  2. Even if you insert the html in the $labels['menu_name'] option or registration, the span is stripped out, because before printing the menu the value of the param is wrapped in esc_attr

The second ‘problem’, is not a real problem, it’s the solution!

In fact, the esc_attr function fires a filter hook attribute_escape, that we can use to calculate the pending posts count and then output the right html unescaped.

We we must be very careful if we remove the sanitize functionality from esc_attr because this is a function used a lot times in WP and is a function related to secutirity.
So, we have to isolate the behaviour only to admin menu and only in specific case.

Moreover we can add filter again when it has done its work, in this way it’s removed only once.

The only problem now is how to understand which is the right menu item for which remove the filter and add the count.

The only chance we have, it’s insert a sort of placeholder that let us recognize the menu item. In other words, when you register your cpt you have to add an uncommon string to $labels['menu_name']:

$labels = array(
  /** This do the magic ;) ! */
  'menu_name' => _x('My CPT %%PENDING_COUNT%%', 'My CPT post type label menu_name', 'mytextdomain'),

  'name' => _x('My CPT','My CPT label name','mytextdomain'),
  'singular_name' => _x('My CPT','My CPT label singular_name','mytextdomain'),
  'all_items' => _x('All My CPT','My CPT label all_items','mytextdomain'),
  'add_new' => _x('Add New','My CPT label add_new','mytextdomain'),
  'add_new_item' => _x('Add New My CPT','My CPT label add_new_item','mytextdomain'),
  'edit_item' => _x('Edit My CPT','My CPT label edit_item','mytextdomain'),
  'new_item' => _x('New My CPT','My CPT label new_item','mytextdomain'),
  'view_item' => _x('View My CPT','My CPTlabel view_item','mytextdomain'),
  'search_items' => _x('Search My CPT','My CPT label search_items','mytextdomain'),
  'not_found' => _x('My CPT not found','My CPT label not_found','mytextdomain'),
  'not_found_in_trash' => _x('My CPT not found in trash','My CPT label not_found_in_trash','mytextdomain'),
  'parent_item_colon' => _x('Parent:','My CPT label parent_item_colon','mytextdomain')
);
$args = array(
  'labels' => $labels,
  ... other args here
)
register_post_type( 'my_cpt', $args );

Once the %%PENDING_COUNT%% is added, when the esc_attr is called for a string containing that text, we run our work:

add_action('auth_redirect', 'add_pending_count_filter'); // modify esc_attr on auth_redirect
add_action('admin_menu', 'esc_attr_restore'); // restore on admin_menu (very soon)

function add_pending_count_filter() {
  add_filter('attribute_escape', 'remove_esc_attr_and_count', 20, 2);
}

function esc_attr_restore() {
  remove_filter('attribute_escape', 'remove_esc_attr_and_count', 20, 2);
}

function remove_esc_attr_and_count( $safe_text="", $text="" ) {
  if ( substr_count($text, '%%PENDING_COUNT%%') ) {
    $text = trim( str_replace('%%PENDING_COUNT%%', '', $text) );
    // run only once!
    remove_filter('attribute_escape', 'remove_esc_attr_and_count', 20, 2);
    $safe_text = esc_attr($text);
    // remember to set the right cpt name below
    $count = (int)wp_count_posts( 'my_cpt',  'readable' )->pending;
    if ( $count > 0 ) {
      // we have pending, add the count
      $text = esc_attr($text) . '<span class="awaiting-mod count-' . $count . '"><span class="pending-count">' . $count . '</span></span>';
      return $text;
    } 
  }
  return $safe_text;
}

That’s all, and it works!

enter image description here

There is only a little issue: if a pending post is published via the quick edit the pending count is not updated, because quick edit use ajax, and to update the numebr you need to use some js.
Work on it, if you want.