Blurbette Plugin: Admin Control Panel


In this chapter I’ll proceed a bit differently: I’ll make a few changes to WPCX_Blurbette_Def and WPCX_Blurbette_Registry first, then define the new class below.

The sole aim of this admin control panel is to update a list of options. WordPress provides a Settings API that provides output helpers and manages groups of individual settings; but for no particular reason, I prefer to assign them all to a single option as an array.

Here is how the control panel will look:
Control Panel Screenshot

Notice most of the options are checkbox on/off values. This means the form simply doesn’t submit them if unchecked, and upon processing they won’t show up in our options array. So a simple empty() or isset() serves to check their values. I’ll decide on some key names now, which I can use in building the form and processing the data:

  • use_shortcode
  • use_widget
  • use_copy_metabox
  • copied_everywhere (can have 3 values: ‘y’, ‘n’ or undefined)
  • clear_on_deactivate

Definition / Registry Updates

I’ll start by adding a new constant to WPCX_Blurbette_Def:

class WPCX_Blurbette_Def {
    const POST_TYPE = 'wpcx_blurbette';
    const TEXT_DOMAIN = 'wpcx_blurbette';
    const OPTION_METAKEY = 'wpcx_blurbette_options';

Then, a new options property in WPCX_Blurbette_Registry:

class WPCX_Blurbette_Registry {
	public $objects = array();
	public $options = array();

… and a simple addition to its __construct() method:

function __construct() {
    spl_autoload_register( array( $this, 'class_autoloader' ) );
    $this->options = get_option( WPCX_Blurbette_Def::OPTION_METAKEY );
    WPCX_Blurbette_Def::do_all_hooks();
    WPCX_Blurbette_Shortcode::do_all_hooks();
    add_action( 'widgets_init', array( $this, 'register_widget' ) );
    add_action( 'plugins_loaded', array( $this, 'instantiate_the_rest' ) );
}

Now, the Registry fetches and stores all the settings once, and all the classes can look to its options array for values.

While I’m editing WPCX_Blurbette_Registry, I’ll add a few conditionals. First, in the __construct() method:

function __construct() {
    spl_autoload_register( array( $this, 'class_autoloader' ) );
    $this->options = get_option( WPCX_Blurbette_Def::OPTION_METAKEY );
    WPCX_Blurbette_Def::do_all_hooks();
    if ( ! empty( $this->options['use_shortcode'] ) ) :
        WPCX_Blurbette_Shortcode::do_all_hooks();
    endif;
    if ( ! empty( $this->options['use_widget'] ) ) :
        add_action( 'widgets_init', array( $this, 'register_widget' ) );
    endif;
    add_action( 'plugins_loaded', array( $this, 'instantiate_the_rest' ) );
}

Now, the shortcode and widget methods are only hooked if the appropriate settings are enabled. This is a clear benefit to using a Registry coding pattern and autonomous classes — it’s easy enable or disable whole sections on a single conditional.

One more change — if shortcodes are disabled, then it doesn’t make sense to provide the TinyMCE button which outputs them:

	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;
		endif;
	}

Immediately after that, I’ll instantiate the new class, which I’ll build below. I’ll place it within the is_admin() conditional too:

	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
			) );

		endif;
	}

I’ve decided to pass a registry arg, pointing back to this WPCX_Blurbette_Registry instance, for ease of access.

The Admin Panel Class

This class is little more than a standard web form. Taking it in chunks:

<?php
	class WPCX_Blurbette_AdminPanel {
		
		private $registry;
		const CAPABILITY = 'manage_options';
		const NONCE_NAME = 'wpcx_blurbette_admin_nonce';
		const NONCE_VALUE = __FILE__;
		
		function __construct( $options ) {
			$this->registry = $options['registry'];
			add_action( 'admin_menu', array( $this, 'register_submenu' ) );
		}
		
		function register_submenu() {
			add_submenu_page( 
				'options-general.php', 
				__( 'Blurbette Settings', WPCX_Blurbette_Def::TEXT_DOMAIN ), 
				__( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN ), 
				self::CAPABILITY, 
				WPCX_Blurbette_Def::POST_TYPE . '_mgmt', 
				array( $this, 'control_panel' )
			 );
		}

As mentioned above, the $registry property points back to the main WPCX_Blurbette_Registry instance that registered this object. With this property defined, all the settings can be accessed herein as $this->registry->options.

Also, I’ve decided that only users with manage_options capability will have access to this control panel.

Then, I’ve defined a nonce name and value for use below. Notice I’ve set the nonce value to __FILE__; internally WordPress adds the logged-in user id, so this nonce value is completely unique to this class (file) and user.

The add_submenu_page() function (documented here) is purely standard. Next is its referenced callback method, which outputs the page — starting with the first half:

		function control_panel() {
			if ( isset( $_POST[self::NONCE_NAME] ) ) :
				$update_message = $this->save_posted_settings();
			endif;
			
			$settings = $this->registry->options;
			
			?><div class="wrap">

				<h2><?php echo $GLOBALS['title'] ?></h2>

				<?php if ( isset( $update_message ) ) : ?>
					<div id="setting-error-settings_updated" class="<?php echo ( stripos( $update_message, 'saved' ) === false )? 'error' : 'updated'; ?> settings-error"> 
					<p><strong><?php echo $update_message ?></strong></p></div>
				<?php endif; ?>
				
				<form name="wpcx_blurbette_optsform" method="post" action="<?php
					echo add_query_arg( array( 
						'page' => $GLOBALS['plugin_page']
					 ), admin_url( $GLOBALS['pagenow'] ) );
				
				?>">
				<?php wp_nonce_field( self::NONCE_VALUE, self::NONCE_NAME ); ?>

This first checks if there is $_POSTed data by looking for the presence of a defined field (I’ve chosen the nonce field), and if so calls save_posted_settings() (below), which returns a message. If that message is defined, then present a standardized <div> element containing it — HTML classname “saved” if successful, “error” if not.

Then, the form tag, which defines this page as its action. This page’s URL is constructed by adding a ‘page’ query arg (value defined in add_submenu_page(), above) to the global variable $GLOBALS['pagenow']. Followed by the the all-important wp_nonce_field().

The second half of the method is simply the HTML table that presents the information, styled to match other WordPress panels, and the closing tags:

			    <table class="form-table">
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_shortcode]" value="1" <?php
			        	checked( $settings['use_shortcode'], 1 );
			        ?> /> <?php _e( 'Use Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_widget]" value="1" <?php
			        	checked( $settings['use_widget'], 1 );
			        ?> /> <?php _e( 'Use Widget', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Copy', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_copy_metabox]" value="1" <?php
			        	checked( $settings['use_copy_metabox'], 1 );
			        ?> /> <?php _e( 'Enable &quot;Copy&quot; button in post panels', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        <blockquote><?php _e( 'New Blurbettes are available:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?>
			        <br />
			        <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="y" <?php
			        	checked( $settings['copied_everywhere'], 'y' );
			        ?> /> <?php _e( 'Everywhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        &nbsp;
			        <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="n" <?php
			        	checked( $settings['copied_everywhere'], 'n' );
			        ?> /> <?php _e( 'Nowhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </blockquote>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Deactivation', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[clear_on_deactivate]" value="1" <?php
			        	checked( $settings['clear_on_deactivate'] , 1 );
			        ?> /> <?php _e( 'Clear everything when deactivating this plugin', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
				</table>
				<?php submit_button(); ?>
				</form>
			</div>
			<?php
			
		}

Lastly, the save_posted_settings() method, followed by a filter callback:

		protected function save_posted_settings() {
			if ( ! wp_verify_nonce( $_POST[self::NONCE_NAME], self::NONCE_VALUE ) ) :
				return __( 'Sorry, there was an error on this form page.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			if ( ! current_user_can( self::CAPABILITY ) ) :
				return __( 'Unauthorized.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			$filtered_input = filter_var( 
				$_POST['blurbette_opt'], 
				FILTER_CALLBACK, 
				array( 'options' => array( $this, 'single_wordchar' ) )
			 );
			$settings_changed = false;
			foreach( $filtered_input as $key=>$value ) :
				if ( $value != $this->registry->options[$key] ) :
					$settings_changed = true;
					break;
				endif;
			endforeach;

			$success = update_option( WPCX_Blurbette_Def::OPTION_METAKEY, $filtered_input );
			if ( $success || ! $settings_changed ) :
				$this->registry->options = $filtered_input;
				return __( 'Settings saved.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			else:
				return __( 'Sorry, settings were not updated.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
		}

		protected function single_wordchar( $val ) {
			$filtered = preg_replace( '/[^\w]/', '', $val );
			return $filtered[0];
		}
	} // end class WPCX_Blurbette_AdminPanel

As with all form-processing routines, input must be checked and filtered. So first this method bails if wp_nonce_verify() fails, and if the user lacks proper capability.

Then, input is filtered. For variety, I’ve chosen the PHP filter_var() function with the callback single_wordchar(). Every input value should be either a 1 or a single letter, so the callback gets to be very strict, stripping out all non-word characters and returning only the first character.

I’ve also defined a variable $settings_changed for a specific reason: if update_option() fails for some reason, it returns false — however it also returns false if the updated option is unchanged (new value matches the database value). By checking if settings have changed, I can combine it with update_options()‘s returned value to determine success or failure.

Recapping the code

Here is the entire WPCX_Blurbette_AdminPanel class:

<?php
	class WPCX_Blurbette_AdminPanel {
		
		private $registry;
		const CAPABILITY = 'manage_options';
		const NONCE_NAME = 'wpcx_blurbette_admin_nonce';
		const NONCE_VALUE = __FILE__;
		
		function __construct( $options ) {
			$this->registry = $options['registry'];
			add_action( 'admin_menu', array( $this, 'register_submenu' ) );
		}
		
		function register_submenu() {
			add_submenu_page( 
				'options-general.php', 
				__( 'Blurbette Settings', WPCX_Blurbette_Def::TEXT_DOMAIN ), 
				__( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN ), 
				self::CAPABILITY, 
				WPCX_Blurbette_Def::POST_TYPE . '_mgmt', 
				array( $this, 'control_panel' )
			 );
		}
		
		function control_panel() {
			if ( isset( $_POST[self::NONCE_NAME] ) ) :
				$update_message = $this->save_posted_settings();
			endif;
			
			$settings = $this->registry->options;
			
			?><div class="wrap">

				<h2><?php echo $GLOBALS['title'] ?></h2>

				<?php if ( isset( $update_message ) ) : ?>
					<div id="setting-error-settings_updated" class="<?php echo ( stripos( $update_message, 'saved' ) === false )? 'error' : 'updated'; ?> settings-error"> 
					<p><strong><?php echo $update_message ?></strong></p></div>
				<?php endif; ?>
				
				<form name="wpcx_blurbette_optsform" method="post" action="<?php
					echo add_query_arg( array( 
						'page' => $GLOBALS['plugin_page']
					 ), admin_url( $GLOBALS['pagenow'] ) );
				
				?>">
				<?php wp_nonce_field( self::NONCE_VALUE, self::NONCE_NAME ); ?>
			    <table class="form-table">
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_shortcode]" value="1" <?php
			        	checked( $settings['use_shortcode'], 1 );
			        ?> /> <?php _e( 'Use Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_widget]" value="1" <?php
			        	checked( $settings['use_widget'], 1 );
			        ?> /> <?php _e( 'Use Widget', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Copy', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[use_copy_metabox]" value="1" <?php
			        	checked( $settings['use_copy_metabox'], 1 );
			        ?> /> <?php _e( 'Enable &quot;Copy&quot; button in post panels', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        <blockquote><?php _e( 'New Blurbettes are available:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?>
			        <br />
			        <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="y" <?php
			        	checked( $settings['copied_everywhere'], 'y' );
			        ?> /> <?php _e( 'Everywhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        &nbsp;
			        <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="n" <?php
			        	checked( $settings['copied_everywhere'], 'n' );
			        ?> /> <?php _e( 'Nowhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </blockquote>
			        </td>
			        </tr>
			        
			        <tr valign="top">
			        <th scope="row"><?php _e( 'Deactivation', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th>
			        <td><label><input type="checkbox" name="blurbette_opt[clear_on_deactivate]" value="1" <?php
			        	checked( $settings['clear_on_deactivate'] , 1 );
			        ?> /> <?php _e( 'Clear everything when deactivating this plugin', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label>
			        </td>
			        </tr>
			        
				</table>
				<?php submit_button(); ?>
				</form>
			</div>
			<?php
			
		}
		
		protected function save_posted_settings() {
			if ( ! wp_verify_nonce( $_POST[self::NONCE_NAME], self::NONCE_VALUE ) ) :
				return __( 'Sorry, there was an error on this form page.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			if ( ! current_user_can( self::CAPABILITY ) ) :
				return __( 'Unauthorized.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
			$filtered_input = filter_var( 
				$_POST['blurbette_opt'], 
				FILTER_CALLBACK, 
				array( 'options' => array( $this, 'single_wordchar' ) )
			 );
			$settings_changed = false;
			foreach( $filtered_input as $key=>$value ) :
				if ( $value != $this->registry->options[$key] ) :
					$settings_changed = true;
					break;
				endif;
			endforeach;

			$success = update_option( WPCX_Blurbette_Def::OPTION_METAKEY, $filtered_input );
			if ( $success || ! $settings_changed ) :
				$this->registry->options = $filtered_input;
				return __( 'Settings saved.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			else:
				return __( 'Sorry, settings were not updated.', WPCX_Blurbette_Def::TEXT_DOMAIN );
			endif;
		}
		
		protected function single_wordchar( $val ) {
			$filtered = preg_replace( '/[^\w]/', '', $val );
			return $filtered[0];
		}
	} // end class WPCX_Blurbette_AdminPanel