Blurbette Plugin: Options Metabox


The last class in the project blueprint is the Options metabox on the Blurbette admin edit panel. The aim is to present all possible contexts in which a Blurbette could appear, and offer checkboxes for each. As a bonus, if the Blurbette was copied from a post, indicate so with a link back to the post’s edit panel.

Here’s an example:
Screen Shot 2015-03-07 at 8.24.12 AM

Strategy

Other than widgets, WordPress outputs all content in one kind of post type or another. In its basic ‘unplugged’ install, possible post types include posts, pages, and attachments. Internally, there are others such as nav menu items and revisions, but those aren’t appropriate for Blurbette output. Then of course any number of custom post types can be defined by plugins and themes, which may or may not output user-generated content.

WordPress provides a get_post_types() function, which returns a master list of all defined post types and their settings. All the ‘front-facing’ ones have a public property set true — so that’s the first criteria to determine if a post type is appropriate for Blurbette output.

Another test is to determine whether a post type supports a TinyMCE ‘editor’. This may or may not indicate whether user-defined content can be output within a post type, since content can be artificially generated by other means, e.g. a simple textarea input. But I choose this criterion, assuming the plugin/theme developers have provided an editor if they intend formatted text to be output.

I can check an ‘editor’ is supported by by examining the global $_wp_post_type_features variable, an array keyed by post type names, each matching a list of supported features defined in the register_post_type() function.

There is one post type, attachment, that is public, but does not support an editor. I choose to add this to the list because a user might wish to include/exclude Blurbettes within captions and attachment detail descriptions (simple textareas) — for example, copyright statements. Recall the Shortcode chapter, where I added a special caption shortcode converter — in there I added an ‘attachment’ context attribute, which will omit a Blurbette if it’s not allowed for Media in this metabox.

For each allowed post type, I’ll add a separate post meta datum for a Blurbette; each key will be the same, and have as its value the post type name. Additionally, I’ll add a separate post meta datum determining whether widgets are allowed.

Also, I’ve made the decision that every Blurbette is available within the Blurbette post type. Every Blurbette can output any other Blurbette (but not itself — remember the ‘recursion buster’ method from the Shortcode chapter).

Writing The Code

First, a bit of business in the WPCX_Blurbette_Def class: constants which define the post metadata keys.

class WPCX_Blurbette_Def {
	const POST_TYPE = 'wpcx_blurbette';
	const TEXT_DOMAIN = 'wpcx_blurbette';
	const OPTION_METAKEY = 'wpcx_blurbette_options';
	const COPIED_TO_METAKEY = 'wpcx_blurbette_copied_to';
	const COPIED_FROM_METAKEY = 'wpcx_blurbette_copied_from';
	const ALLOWED_POSTTYPE_METAKEY = 'blurbette_allowed_post_type';
	const ALLOWED_WIDGET_METAKEY = 'blurbette_allowed_in_widget';

Now, on to the metabox. Thanks to the metabox abstract class, the task of defining this metabox is reduced to merely three methods: the output and the saving, plus an additional helper method. Presenting in chunks:

<?php
	class WPCX_Blurbette_Opts_Metabox extends WPCX_Abs_Metabox {
		
		function output_meta_box( $post ) {
			$allowed_post_types = ( array ) get_post_meta( $post->ID, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, false );
			$allowed_in_widget = get_post_meta( $post->ID, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY, true );
			$copied_from = get_post_meta( $post->ID, WPCX_Blurbette_Def::COPIED_FROM_METAKEY, true );

			$this->noncefield();

			?><p><?php _e( 'Allow this blurbette in:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></p>
			<ul>
				<li><label><input type="checkbox" name="wpcx_blurbette_widg" value="1" <?php
				checked( ! empty( $allowed_in_widget ), true );
			?> /> <?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label></li>
			<?php
			foreach( (array) self::eligible_post_types( false ) as $post_type => $label ): ?>
					<li><label><input type="checkbox" name="wpcx_blurbette_pt[<?php echo $post_type ?>]" value="1" <?php
						checked( in_array( $post_type, $allowed_post_types ), true );
					?> /> <?php
						echo esc_html( $label );
					?></label></li>
			<?php endforeach; ?>
			</ul>

The first three lines of the output_meta_box method fetch the post meta. $allowed_post_types will be an array, each element containing the name of a post type. $allowed_in_widget will be the value 1, or null. $copied_from will be the ID of a post, as defined in the last chapter.

Then, I present the Widget checkbox, followed by the post-type checkboxes. Here, I call a method I’ll define below, which returns an associative array of post type names => human-readable names. For reasons I’ll explain, this will be a static method. The single boolean argument determines whether the Blurbette post type should be included (true) or not (false). Here I’ve skipped the Blurbette post type because it’s always enabled.

Next chunk, through the end of the output method:

			<?php
			if ( ! empty( $copied_from ) ) :
				$editor_url = add_query_arg( 
					array( 
						'post' => $copied_from, 
						'action' => 'edit'
					 ), 
					admin_url( 'post.php' )
				 );
				$title = get_the_title( $copied_from );
				if ( empty( $title ) ) $title = __( '( Untitled )', WPCX_Blurbette_Def::TEXT_DOMAIN );
				echo sprintf( '<p>'. __( 'This Blurbette was copied from: ', WPCX_Blurbette_Def::TEXT_DOMAIN ) .
					'<a href="%s" title="%s">%s</a></p>', 
						$editor_url, 
						esc_attr( $title ), 
						esc_html( $title )
						), PHP_EOL;
			endif;
		}

This part simply outputs the name (and editor hyperlink) of the post which spawned the Blurbette, if applicable.

Next, the save_meta_box method:

		function save_meta_box( $postid ){
			if ( ! $this->okay_to_save( $postid ) ) return;

			$filtered_input = array( 
				'wpcx_blurbette_widg' => filter_var( 
					$_POST['wpcx_blurbette_widg'], 
					FILTER_VALIDATE_BOOLEAN, 
					FILTER_NULL_ON_FAILURE
				 ), 
				'wpcx_blurbette_pt' => filter_var_array( 
					( array ) $_POST['wpcx_blurbette_pt'], 
					FILTER_VALIDATE_BOOLEAN
				 )
			 );
			if ( 
				in_array( false, $filtered_input['wpcx_blurbette_pt'], true ) ||
				$filtered_input['wpcx_blurbette_widg'] === null
			 ) return; // suspicious because a value other than '1' was passed

			$filtered_input['wpcx_blurbette_pt'][WPCX_Blurbette_Def::POST_TYPE] = true;
			if ( $filtered_input['wpcx_blurbette_widg'] ) :
				update_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY, 1 );
			else:
				delete_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY );
			endif;
			
			delete_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY );
			foreach ( $filtered_input['wpcx_blurbette_pt'] as $post_type => $dummy ) :
				add_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, $post_type );
			endforeach;
		}

First, I use the helpful $this->okay_to_save method, provided by the metabox abstract.

Then, I filter the input (always important!) — every checkbox simply has the value “1″, so the PHP functions filter_var() and filter_var_array() can act in FILTER_VALIDATE_BOOLEAN mode. If either of those filters encounters a value that doesn’t evaluate to true — that’s something like 1, “1″, “true”, etc. — it changes the value to false. Since all my incoming data should be “1″, or nonexistent, I know that a value of false indicates mischief, so I bail.

After passing the checks and filters, I add the Blurbette post type to the list of input, and update the post metadata.

Next, the helper method — the static one called above, which returns all eligible post types a Blurbette can appear in. I’m making it static because I want to enable easy access to it, from other portions of code. This class will only be instantiated if WordPress is displaying an admin panel (is_admin()), and I can’t think of a good reason to prevent non-admin scripts from accessing it.

		public static function eligible_post_types( $include_blurbettes = true ) {
			$return = array();
			if ( $include_blurbettes ):
				$return[WPCX_Blurbette_Def::POST_TYPE] = __( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			$all_post_types = get_post_types( array( 
				'public' => true, 
				), 'objects' );
			if ( is_array( $all_post_types ) ) :
				foreach( $all_post_types as $post_type => $defs ) :
					if ( WPCX_Blurbette_Def::POST_TYPE == $post_type ) continue;
					if ( 'attachment' != $post_type && ! in_array( 'editor', array_keys( $GLOBALS['_wp_post_type_features'][$post_type] ) ) ) continue;
					$return[$post_type] = $defs->label;
				endforeach;
			endif;
			return $return;
		}
	} // end class WPCX_Blurbette_Opts_Metabox

I set the variable $all_post_types to the output of WordPress’ get_post_types() function, which can be filtered on only 'public' types, and configured to return data in 'objects' format.

Then, I loop through that variable, and check to see if the post type supports an 'editor' by looking for its presence in the global $_wp_post_type_features array (this is defined when the register_post_type() function executes) — making an exception for the ‘attachment’ post type.

Updating the Other Classes

Recall in the Admin Control Panel chapter, one of the settings was for newly-copied Blurbettes to be available “everywhere” (meaning all these boxes are checked), or “nowhere” (meaning none of them are). Now that I’ve defined the metadata, I can return to the WPCX_Blurbette_Copy_Metabox class and update it to properly add these metadata.

I’ll add this to the ajax_save_copy() method, just before the end where the return data is echoed:

			add_post_meta( $blurbette_result, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, WPCX_Blurbette_Def::POST_TYPE );

			if ( !empty( $this->registry->options['copied_everywhere'] ) && $this->registry->options['copied_everywhere'] == 'y' ) :
				add_post_meta( $blurbette_result, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY, 1 );
				foreach( (array) WPCX_Blurbette_Opts_Metabox::eligible_post_types( false ) as $post_type => $label ):
					add_post_meta( $blurbette_result, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, $post_type );
				endforeach;
			endif;

			echo json_encode( $return );
			die();
		}

I add the Blurbette’s native post type because it’s always enabled. Then, if allowed by the admin settings, I call the static helper method created earlier, and add post meta for each post type plus the widget.

Now, at long last, I can write the WPCX_Blurbette_Def::check_availability method; recall that it’s been merely a placeholder until now.

	public static function check_availability( $id, $context ) {
		if ( empty( $context ) ) return true;
		if ( 'widget' == $context ) :
			$is_available = get_post_meta( $id, self::ALLOWED_WIDGET_METAKEY, true );
		
		else:
			$allowed_post_types = (array) get_post_meta( $id, self::ALLOWED_POSTTYPE_METAKEY, false );
			$is_available = ( in_array( $context, $allowed_post_types ) );
		endif;
		
		return $is_available;
	}

As a refresher, this method returns true if the Blurbette with a given $id is allowed within a given $context. If $context is blank for some reason, simply return true. Now that the postmeta has been defined, it’s easy to check the Blurbette’s postmeta values for existence of the given $context.

Finally, return to the WPCX_Blurbette_Registry and instantiate this class if within the admin area:

function instantiate_the_rest() {
    if ( is_admin() ) :
        if ( ! empty( $this->options['use_shortcode'] ) ) :
            $this->register( 'WPCX_Blurbette_MCE_WithDialog', array(
                'registry'  => $this,
                'name'      => 'WPCXBlurbette',
                'row'       => 2,
                'js'        => plugin_dir_url( __FILE__ ) . 'mce/mce_blurbette.js'
            ) );
        endif;
        $this->register( 'WPCX_Blurbette_AdminPanel', array(
            'registry'=>$this
        ) );
        if ( ! empty( $this->options['use_copy_metabox'] ) ) :
            $this->register( 'WPCX_Blurbette_Copy_Metabox', array(
                'registry'      => $this,
                'title'         => 'Copy to Blurbette',
                'post_types'    => array( 'post', 'page' )
            ) );
        endif;
        $this->register( 'WPCX_Blurbette_Opts_Metabox', array(
            'registry'		=> $this,
            'title'			=> 'Blurbette Options',
            'post_types'	=> array( WPCX_Blurbette_Def::POST_TYPE )
        ) );
    endif;
}

Recapping the code

<?php
	class WPCX_Blurbette_Opts_Metabox extends WPCX_Abs_Metabox {
		
		function output_meta_box( $post ) {
			$allowed_post_types = ( array ) get_post_meta( $post->ID, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, false );
			$allowed_in_widget = get_post_meta( $post->ID, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY, true );
			$copied_from = get_post_meta( $post->ID, WPCX_Blurbette_Def::COPIED_FROM_METAKEY, true );

			$this->noncefield();

			?><p><?php _e( 'Allow this blurbette in:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></p>
			<ul>
				<li><label><input type="checkbox" name="wpcx_blurbette_widg" value="1" <?php
				checked( ! empty( $allowed_in_widget ), true );
			?> /> <?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label></li>
			<?php
			foreach( (array) self::eligible_post_types( false ) as $post_type => $label ): ?>
					<li><label><input type="checkbox" name="wpcx_blurbette_pt[<?php echo $post_type ?>]" value="1" <?php
						checked( in_array( $post_type, $allowed_post_types ), true );
					?> /> <?php
						echo esc_html( $label );
					?></label></li>
			<?php endforeach; ?>
			</ul>
			
			<?php
			if ( ! empty( $copied_from ) ) :
				$editor_url = add_query_arg( 
					array( 
						'post' => $copied_from, 
						'action' => 'edit'
					 ), 
					admin_url( 'post.php' )
				 );
				$title = get_the_title( $copied_from );
				if ( empty( $title ) ) $title = __( '( Untitled )', WPCX_Blurbette_Def::TEXT_DOMAIN );
				echo sprintf( '<p>'. __( 'This Blurbette was copied from: ', WPCX_Blurbette_Def::TEXT_DOMAIN ) .
					'<a href="%s" title="%s">%s</a></p>', 
						$editor_url, 
						esc_attr( $title ), 
						esc_html( $title )
						), PHP_EOL;
			endif;
		}
		
		function save_meta_box( $postid ){
			if ( ! $this->okay_to_save( $postid ) ) return;
			if ( empty( $_POST[$this->noncename] ) ) return;

			$filtered_input = array( 
				'wpcx_blurbette_widg' => filter_var( 
					$_POST['wpcx_blurbette_widg'], 
					FILTER_VALIDATE_BOOLEAN, 
					FILTER_NULL_ON_FAILURE
				 ), 
				'wpcx_blurbette_pt' => filter_var_array( 
					( array ) $_POST['wpcx_blurbette_pt'], 
					FILTER_VALIDATE_BOOLEAN
				 )
			 );
			if ( 
				in_array( false, $filtered_input['wpcx_blurbette_pt'], true ) ||
				$filtered_input['wpcx_blurbette_widg'] === null
			) return;

			$filtered_input['wpcx_blurbette_pt'][WPCX_Blurbette_Def::POST_TYPE] = true;
			if ( $filtered_input['wpcx_blurbette_widg'] ) :
				update_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY, 1 );
			else:
				delete_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_WIDGET_METAKEY );
			endif;
			
			delete_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY );
			foreach ( array_keys( $filtered_input['wpcx_blurbette_pt'] ) as $post_type ) :
				add_post_meta( $postid, WPCX_Blurbette_Def::ALLOWED_POSTTYPE_METAKEY, $post_type );
			endforeach;
		}
		
		public static function eligible_post_types( $include_blurbettes = true ) {
			$return = array();
			if ( $include_blurbettes ):
				$return[WPCX_Blurbette_Def::POST_TYPE] = __( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			$all_post_types = get_post_types( array( 
				'public' => true, 
				), 'objects' );
			if ( is_array( $all_post_types ) ) :
				foreach( $all_post_types as $post_type => $defs ) :
					if ( WPCX_Blurbette_Def::POST_TYPE == $post_type ) continue;
					if ( 'attachment' != $post_type && ! in_array( 'editor', array_keys( $GLOBALS['_wp_post_type_features'][$post_type] ) ) ) continue;
					$return[$post_type] = $defs->label;
				endforeach;
			endif;
			return $return;
		}
	} // end class WPCX_Blurbette_Opts_Metabox