Restrict individual category combinations

Okay, I’ve had a couple minutes free time, so I wrote up a small plugin. šŸ˜‰


The following goes into a new plugin file tf-restrict-categories/tf-restrict-categories.php:

The introduction

<?php
/**
 * Plugin Name: Restrict Categories
 * Description: Individually restrict category combinations.
 * License: MIT
 * License URI: http://opensource.org/licenses/MIT
 * Text Domain: tf-restrict-categories
 * Domain Path: /languages
 */


if (! class_exists('TFRestrictCategories')) :


/**
 * Main (and only) class.
 */
class TFRestrictCategories {

    /**
     * Plugin instance.
     *
     * @type    object
     */
    protected static $instance = null;


    /**
     * basename() of global $pagenow.
     *
     * @type    string
     */
    protected static $page_base;


    /**
     * Plugin textdomain.
     *
     * @type    string
     */
    protected $textdomain = 'tf-restrict-categories';


    /**
     * Plugin option name.
     *
     * @type    string
     */
    protected $option_name="tf_restrict_categories";


    /**
     * Plugin settings page name.
     *
     * @type    string
     */
    protected $settings_page_name="tf-restrict-categories";


    /**
     * Plugin settings page.
     *
     * @type    string
     */
    protected $settings_page;

The basics

    /**
     * Constructor. Registers activation routine.
     *
     * @hook    wp_loaded
     * @return  void
     */
    public function __construct() {
        register_activation_hook(__FILE__, array(__CLASS__, 'activation'));
    } // function __construct


    /**
     * Get plugin instance.
     *
     * @hook    wp_loaded
     * @return  object TFRestrictCategories
     */
    public static function get_instance() {
        if (null === self::$instance)
            self::$instance = new self;

        return self::$instance;
    } // function get_instance


    /**
     * Registers uninstall routine.
     *
     * @hook    activation
     * @return  void
     */
    public static function activation() {
        register_uninstall_hook(__FILE__, array(__CLASS__, 'uninstall'));
    } // function activation


    /**
     * Checks if the plugin has to be loaded.
     *
     * @return  boolean
     */
    public static function has_to_be_loaded() {
        global $pagenow;

        if (empty($pagenow))
            return false;

        self::$page_base = basename($pagenow, '.php');

        // Load plugin for all admin pages
        return is_admin();
    } // function has_to_be_loaded


    /**
     * Registers plugin actions and filters.
     *
     * @hook    wp_loaded
     * @return  void
     */
    public function init() {
        add_action('admin_menu', array($this, 'add_settings_page'));
        add_action('wp_insert_post', array($this, 'restrict_categories'));

        $pages = array(
            'edit',
            'post',
            'post-new',
        );
        if (in_array(self::$page_base, $pages))
            add_action('admin_print_scripts-posts_page_tf-restrict-categories', array($this, 'enqueue_scripts'));

        if ('plugins' === self::$page_base)
            add_filter('plugin_action_links_'.plugin_basename(__FILE__), array($this, 'add_settings_link'));

        if ('options' === self::$page_base)
            add_action('admin_init', array($this, 'register_setting'));
    } // function init


    /**
     * Wrapper for get_option().
     *
     * @param   string $key Option name.
     * @param   mixed $default Return value for missing key.
     * @return  mixed|$default Option value.
     */
    protected function get_option($key = null, $default = false) {
        static $option = null;
        if (null === $option) {
            $option = get_option($this->option_name, false);
            if (false === $option)
                $option = array(
                );
        }

        if (null === $key)
            return $option;

        if (! isset($option[$key]))
            return $default;

        return $option[$key];
    } // function get_option

This is where the action is

    /**
     * Adds custom settings page to posts settings.
     *
     * @hook    admin_menu
     * @return  void
     */
    public function add_settings_page() {
        $this->settings_page = add_posts_page('Restrict Categories', 'Restrict Categories', 'manage_categories', $this->settings_page_name, array($this, 'print_settings_page'));
    } // function add_settings_page


    /**
     * Prints settings page.
     *
     * @see     add_settings_page()
     * @return  void
     */
    public function print_settings_page() {
        $this->load_textdomain();
        ?>
        <div class="wrap">
            <h2>Restrict Categories</h2>
            <div class="tool-box">
                <form method="post" action="<?php echo admin_url('options.php'); ?>">
                    <?php
                    settings_fields($this->option_name);
                    $args = array(
                        'hide_empty' => 0,
                    );
                    if (count($categories = get_categories($args))) {
                        $option = $this->get_option($this->option_name, array());
                        ?>
                        <table id="tf-restrict-categories" class="widefat">
                            <thead>
                                <tr>
                                    <th></th>
                                    <?php
                                    foreach ($categories as $category) {
                                        ?>
                                        <th><?php echo $category->name; ?></th>
                                        <?php
                                    }
                                    ?>
                                </tr>
                            </thead>
                            <tbody>
                                <?php
                                $alternate = true;
                                foreach ($categories as $category) {
                                    $class = ($alternate) ? ' class="alternate"' : '';
                                    $alternate = ! $alternate;
                                    ?>
                                    <tr<?php echo $class; ?>>
                                        <td id=""><?php echo $category->name; ?></td>
                                        <?php
                                        foreach ($categories as $second_category) {
                                            ?>
                                            <td id="restrict-category-<?php echo $category->term_id; ?>-<?php echo $second_category->term_id; ?>">
                                                <?php
                                                if ($category->term_id !== $second_category->term_id) {
                                                    $checked = (
                                                        isset($option[$category->term_id]) && isset($option[$category->term_id][$second_category->term_id])
                                                        || isset($option[$second_category->term_id]) && isset($option[$second_category->term_id][$category->term_id])
                                                    );
                                                    $checked = ($checked) ? ' checked="checked"' : '';
                                                    $disabled = (isset($option[$second_category->term_id]) && isset($option[$second_category->term_id][$category->term_id]));
                                                    $disabled = ($disabled) ? ' disabled="disabled"' : '';
                                                    ?>
                                                    <input type="checkbox" id="<?php echo $category->term_id; ?>-<?php echo $second_category->term_id; ?>" name="<?php echo $this->option_name; ?>[<?php echo $category->term_id; ?>][<?php echo $second_category->term_id; ?>]" value="1"<?php echo $checked.$disabled; ?> />
                                                    <?php
                                                }
                                                ?>
                                            </td>
                                            <?php
                                        }
                                        ?>
                                    </tr>
                                    <?php
                                }
                                ?>
                            </tbody>
                        </table>
                        <div class="submit">
                            <input type="submit" class="button-primary" value="<?php _e('Save Changes'); ?>" />
                        </div>
                        <?php
                    } else
                        _e("No categories found.", 'tf-restrict-categories');
                    ?>
                </form>
            </div>
        </div>
        <?php
        $this->unload_textdomain();
    } // function print_settings_page


    /**
     * Restricts categories according to stored plugin settings.
     *
     * @hook    wp_insert_post
     * @param   int $id Post ID.
     * @return  void
     */
    public function restrict_categories($id) {
        $args = array(
            'fields' => 'ids',
            'orderby' => 'term_id'
        );
        if (
            count($option = $this->get_option($this->option_name, array()))
            && count($categories = wp_get_object_terms($id, 'category', $args))
        ) {
            foreach ($option as $master_key => $restrict)
                foreach($restrict as $restrict_key => $v)
                    if (in_array($master_key, $categories))
                        foreach($categories as $key => $category)
                            if ($category === $restrict_key)
                                unset($categories[$key]);

            wp_set_object_terms($id, $categories, 'category');
        }
    } // function restrict_categories


    /**
     * Adds a link to the settings to the plugin list.
     *
     * @hook    plugin_action_links_{$file}
     * @param   array $links Already existing links.
     * @return  array
     */
    public function add_settings_link($links) {
        $settings_link = array(
            '<a href="'.admin_url('edit.php?page=".$this->settings_page_name)."">'.__("Settings").'</a>'
        );

        return array_merge($settings_link, $links);
    } // function add_settings_link


    /**
     * Enqueues necessary script files.
     *
     * @hook    admin_print_scripts-posts_page_tf-restrict-categories
     * @return  void
     */
    public function enqueue_scripts() {
        $this->load_textdomain();
        wp_enqueue_script('tf-restrict-categories-js', plugin_dir_url(__FILE__).'js/tf-restrict-categories.js', array('jquery'), filemtime(plugin_dir_path(__FILE__).'js/tf-restrict-categories.js'), true);
        $this->unload_textdomain();
    } // function enqueue_scripts


    /**
     * Registers setting for custom options page.
     *
     * @hook    admin_init
     * @return  void
     */
    public function register_setting() {
        register_setting($this->option_name, $this->option_name, array($this, 'save_setting'));
    } // function register_setting


    /**
     * Prepares option values before they are saved.
     *
     * @param   array $data Original option values.
     * @return  array Sanitized option values.
     */
    public function save_setting($data) {
        $sanitized_data = $this->get_option();
        if (isset($data) && ! empty($data))
            $sanitized_data[$this->option_name] = $data;
        else
            unset($sanitized_data[$this->option_name]);

        return $sanitized_data;
    } // function save_setting

The end

    /**
     * Loads plugin textdomain.
     *
     * @return  boolean
     */
    protected function load_textdomain() {
        return load_plugin_textdomain($this->textdomain, false, plugin_basename(dirname(__FILE__)).'/languages');
    } // function load_textdomain


    /**
     * Remove translations from memory.
     *
     * @return  void
     */
    protected function unload_textdomain() {
        unset($GLOBALS['l10n'][$this->textdomain]);
    } // function unload_textdomain


    /**
     * Deletes plugin data on uninstall.
     *
     * @hook    uninstall
     * @return  void
     */
    public static function uninstall() {
        delete_option(self::get_instance()->option_name);
    } // function uninstall

} // class TFRestrictCategories


if (TFRestrictCategories::has_to_be_loaded())
    add_action('wp_loaded', array(TFRestrictCategories::get_instance(), 'init'));


endif; // if (! class_exists('TFRestrictCategories'))

The following goes into a new plugin file tf-restrict-categories/js/tf-restrict-categories.js:

The JavaScript

jQuery(function($) {

    $('#tf-restrict-categories [type="checkbox"]').click(function() {
        var $this = $(this);
        if (! $this.is(':disabled')) {
            var n = this.id.split('-');
            $('#tf-restrict-categories [type="checkbox"][name$="\\['+n[1]+'\\]\\['+n[0]+'\\]"]')
                .attr('disabled', $this.is(':checked'))
                .attr('checked', $this.is(':checked'));
        }
    });

});

Copy&Paste the code into the two files, upload to your plugins folder, activate the plugin and find the new settings page.

Happy restricting. šŸ™‚


Okay, okay, some words of explaining…

The plugin works as follows. On the settings page, we define some category combination. Let’s say, we want to have the rule If ‘Cat A’ is present, do NOT allow ‘Cat C’. This can be achieved by checking the checkbox in the row Cat A and column Cat C. This means: the row category is the master category, and the column category will be restricted.
Of course, you can have multiple combinations with Cat A (and multiple other categories).

When saving/updating a post (or, to be more precise: when having saved/updated a post), the categories are checked against the stored plugin settings – and adapted.

Anything else?

Leave a Comment