Writing a WordPress Plugin, Part 1

The Basics

So you want learn about writing a WordPress plugin, eh? We’re going to cover the basics on how to write a WordPress plugin. It will be very limited in scope, since this is a tutorial. You can find working resources at the end of the posts. This post assumes you’ve already installed WordPress.

Getting Started

The first things to understand about creating WordPress plugins is that they need a plugin file. This file handles declaring the plugin and listing information that you see on the Plugins page within the WP Admin. Let’s create one.

<?php

/**
 * Plugin Name: Editor Styles
 * Plugin URI: http://www.gnarlyweb.com/
 * Description: Add the Ability to Customize the Styles used within the Editor
 * Version: 1.0
 * Author: Kyle Keiper
 * Author URI: http://www.gnarlyweb.com/
 * License: gplv3
 */

if( ! defined('ABSPATH') )
    throw new Exception( "Script ". __FILE__ ." Accessed Directly." );

We’ll save this as editor-styles.php in the plugins directory, and WordPress should be able to pick up on it right away. Although the fields are rather obvious, let’s just go over what information needs to go in the file header.

  1. Plugin Name – A unique, human readable name for your plugin
  2. Plugin URI – The URI for the homepage of your new plugin
  3. Description – A quick description about what your plugin will do
  4. Version – The version of your plugin
  5. Author – 98% of the time will simply be your name
  6. Author URI – The Website URI of the Author
  7. License – A slug representing the license, eg ‘gplv3’, ‘mit’, etc

The last thing you’ll notice above is a security precaution; in order to prevent people from accessing the plugin outside of the WordPress system, we check to see if a WordPress-specific CONSTANT is defined. If it’s not, then halt execution by throwing an exception.

WordPress Plugin Body

I like to contain everything within an object to avoid naming collisions, so that’s how I’m going to code the body of this plugin.

/**
 * Class EditorStyles
 *
 * A Demonstration plugin that actually does something. Provides an admin screen for adding styles to the editor.
 */
class EditorStyles {

    /**
     * @var $_options
     *
     * Array of options for the plugin
     */
    protected $_options = array();

    /**
     * @method __construct
     *
     * Registers some hooks upon creation and sets the options
     */
    public function __construct()
    {
        // register hooks upon instantiation
        add_action("admin_init", array(&$this, "init"));
        add_action("admin_menu", array(&$this, "menus"));

        // set the _options var to the settings in the database
	$this->_options = get_option("_editor_styles", array());
    }

    /**
     * @method init
     *
     * Main body of the plugin. This function handles the logic within the plugin
     *
     */
    public function init()
    {
	if( "POST" === $_SERVER['REQUEST_METHOD'] )
		$this->processPost();

        // enqueue our plugin styles and scripts
        wp_enqueue_style( "editor-styles", plugin_dir_url(__FILE__) . 'resources/editor-styles.css' );
        wp_enqueue_script( "editor-styles", plugin_dir_url(__FILE__) . "resources/editor-styles.js", ["jquery"] );


        $this->addStyles();
    }

    /**
     * @method menus
     *
     * Add a page to the menu
     */
    public function menus()
    {
        // add an Editor Styles Page to the Menu
        add_menu_page(
            "Editor Style Options Title", // HTML title of the page
            "Editor Style Options", // Text for the Menu Item
            "manage_options", // capability needed to access this
            "editor-styles/pages/options.php" // file to use for the callback
        );
    }

    /**
     * @method processPost
     *
     * Handle updating the database if all is valid
     */
    protected function processPost()
    {
	    // if we're not updating the editor styles, don't worry about processing
	    if( !isset( $_POST['editor-styles-nonce'] ) )
		    return;

        // if the correct nonce field is included, but the value is wrong
        if ( isset( $_POST['editor-styles-nonce'] )
            && ! wp_verify_nonce( $_POST['editor-styles-nonce'], 'editor-styles-post' )
        )
        {
            // we don't want to proceed. Possible attack if we did.
            throw new Exception("Invalid Nonce Field!");
        }

        // filter out empty entries in the styles array
        $_POST['editor-styles']['styles'] = array_filter( $_POST['editor-styles']['styles'], function($idx){
            return !empty($idx);
        } );

        // update the database with the new options
        update_option("_editor_styles", $_POST['editor-styles']);

        // reload the options
        $this->_options = $_POST['editor-styles'];
    }

    /**
     * @method addStyles
     *
     * Method that handles adding the stylesheets to the TinyMCE Editor
     */
    protected function addStyles()
    {
        // add each stylesheet in the array to the editor
        foreach( $this->getOption("styles", array()) as $stylesheet )
            add_editor_style( $stylesheet );

        // add the theme's stylesheet, as long as that's not disabled
        if( $this->getOption("exclude_default", "0") == "0" )
                add_editor_style( get_stylesheet_uri() );
    }

    /**
     * @method getOption
     *
     * A simple helper function to get the options and set a default
     *
     * @param $idx mixed Name of the index to retrieve
     * @param $default mixed Default value to return should the index not exist
     * @return mixed
     */
    protected function getOption( $idx, $default )
    {
        return ( ! empty($this->_options[$idx]) ) ? $this->_options[$idx] : $default;
    }

}

// Instantiate the Plugin

new EditorStyles;

That’s quite a bit of information, so let’s break down just exactly what this is doing.

  1. Declare a class called EditorStyles.
  2. Add and hook some actions for admin_init and admin_menu.
  3. Handle POST requests and make sure that the request is valid
  4. Add styles to the editor.
  5. Add a helper method to get the _options indexes.

Creating the Form to Updating The Fields

<?php

if (!defined("ABSPATH"))
    throw new Exception("Script " . __FILE__ . " Accessed Directly!");

// get the current options, including their defaults
$options = get_option("_editor_styles", array(
    "styles" => array(),
    "exclude_default" => "0"
));

global $wp_styles;
global $wp_scripts;

// read the stylesheet directory for css files
$cssFiles = array();

// if we're using a childtheme, get parent dir stuff
if( strcmp(get_template_directory(), get_stylesheet_directory()) !== 0 )
{
    $ssDir = new RecursiveDirectoryIterator( get_template_directory() );
    $sIt = new RecursiveIteratorIterator( $ssDir );
// regex is "search for any path ending in .css, case insensitive
    $regex = new RegexIterator($sIt, '/^.+\.css$/i', RecursiveRegexIterator::GET_MATCH);

    foreach($regex as $filename => $fileObj)
    {
        $cssFiles[] = get_template_directory_uri() . substr($filename, strlen(get_template_directory() ));
    }
}


$ssDir = new RecursiveDirectoryIterator( get_stylesheet_directory() );
$sIt = new RecursiveIteratorIterator( $ssDir );
// regex is "search for any path ending in .css, case insensitive
$regex = new RegexIterator($sIt, '/^.+\.css$/i', RecursiveRegexIterator::GET_MATCH);

foreach($regex as $filename => $fileObj)
{
    $cssFiles[] = get_stylesheet_directory_uri() . substr($filename, strlen(get_stylesheet_directory() ));
}

?>
<div class="wrap">
    <h2>EditorStyles Options</h2>

    <form method="post" id='editor-styles-form'>
        <?php wp_nonce_field('editor-styles-post', 'editor-styles-nonce'); ?>

        <table class="form-table">
            <tbody>
            <tr>
                <th scope="row">
                    <label for="exclude-default">Exclude Theme Stylesheet?</label>
                </th>
                <td>
                    <input type="hidden" name="editor-styles[exclude_default]" value="0"/>
                    <input type="checkbox" name="editor-styles[exclude_default]" id="exclude-default" value="1"
                        <?php echo ($options['exclude_default'] != "1") ? "" : "checked=\"checked\""; ?>
                        />

                    <p class="description">
                        If for some reason, you don't want to include your theme's stylesheet, check this box.
                    </p>
                </td>
            </tr>
            <tr>
                <th scope="row">
                    <label>URIs for Custom Styles</label>
                </th>
                <td id="customStyleUris">
                    <?php foreach ($options['styles'] as $value): ?>
                        <input type="text" name="editor-styles[styles][]" class="regular-text"
                               value="<?php echo $value; ?>"
                               placeholder="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css"/>
                    <?php endforeach; ?>

                    <input type="text" name="editor-styles[styles][]" class="regular-text"
                           placeholder="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css"/>
                    <button class="button" id="addInput" type="button">Add Another Stylesheet</button>
                </td>
            </tr>
            <tr>
                <th scope="row">
                    <label>Here Are the CSS Files We Detected In Your Theme:</label>
                </th>
                <td>
                    <?php foreach($cssFiles as $file): ?>
                    <p><em><?php echo $file; ?></em></p>
                    <?php endforeach; ?>
                </td>
            </tr>
            </tbody>
        </table>

        <p class="submit">
            <input type="submit" class="button button-primary" value="Save Settings"/>
        </p>
    </form>
</div>

That’s quite a lot of code, but really it doesn’t do a ton of different things. Here’s a synopsis:

  1. Check if this is being accessed correctly.
  2. Get all the _options that we will need to auto-populate the form.
  3. Collect the URI paths of all CSS files in the active themes. (Read comments in above sample for more information)
  4. Render the nonce field to prevent CSRF attacks.
  5. Build a form with the fields that are needed.
  6. Give a helper list of all the Active Theme’s CSS files

To Be Continued in Part 2

Link to the Full Plugin Source Code

Leave a Reply

Your email address will not be published. Required fields are marked *

Thanks for your thoughts!

*