<?php
/*
Plugin Name: Custom Metaboxes for Core
*/

class WP_Meta_Box {
	/**
	 * The screen where the meta box will be shown.
	 */
	public $screen;

	/**
	 * A unique slug to allow multiple meta boxes of the same class
	 * to exist on the same screen.
	 */
	public $slug;

	/**
	 * A unique identifier for the meta box.
	 *
	 * A string composed from the meta box class, screen, and slug.
	 */
	public $unique_id;

	/**
	 * Arguments passed to the meta box constructor.
	 */
	public $args;

	/**
	 * A registry that tracks each meta box instance.
	 */
	public static $registry;

	/**
	 * A multi-dimensional array of registered meta fields, and their settings.
	 */
	public $meta_fields;


	/**
	 * Constructor.
	 *
	 * Any subclasses MUST call parent::_construct( $screen, $args ).
	 *
	 * @param string $screen - The ID (string) for the screen where the meta box will be shown.
	 * @param array  $args - An array of supplied arguments. Optional.
	 *
	 * $args recognized by default:
	 *    title    - The title of the meta box.
	 *    context  - The context within the page where the box should show ('normal', 'advanced').
	 *    priority - The priority within the context where the boxes should show ('high', 'low').
	 *    slug     - A unique string to identify the meta box.
	 */
	public function __construct( $screen, $args = array() ) {
		// Set variables
		$this->screen = $screen;
		$this->slug   = empty( $args['slug'] ) ? '' : $args['slug'];

		// Remove slug from args
		unset( $args['slug'] );

		// Set default args
		$defaults = array(
			'title'         => __( 'Untitled' , 'wordcampbase'),
			'context'       => 'side',
			'priority'      => 'default',
		);
		$this->args = wp_parse_args( $args, $defaults );


		add_action( 'save_post', array( $this, '_save' ), 10, 99 ); // Effectively infinite args.

		// Add the meta box to the registry.
		// Generates a slug if one doesn't already exist.
		WP_Meta_Box::registry()->add( $this );

		// Construct the meta box's unique ID.
		$this->unique_id = get_class( $this ) . "_{$this->screen}_{$this->slug}";

		// Bind hooks
		add_action( 'admin_init',            array( $this, '_register_meta_box' ) );
		add_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
	}

	/**
	 * Fetches the meta box registry.
	 */
	public static function registry() {
		if ( ! isset( WP_Meta_Box::$registry ) )
			WP_Meta_Box::$registry = new WP_Meta_Box_Registry();
		return WP_Meta_Box::$registry;
	}

	/**
	 * Fires when the meta box is registered. Override in a subclass.
	 */
	protected function register() {}

	/**
	 * Remove the meta box.
	 */
	public function remove() {
		// Make sure we've removed any data from the registry.
		WP_Meta_Box::registry()->remove( get_class( $this ), $this->screen, $this->slug );

		// Unbind actions.
		remove_action( 'admin_init',            array( $this, '_register_meta_box' ) );
		remove_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
	}
	
	/**
	 * Register a post meta field.
	 * 
	 * For use in a subclass.
	 */
	protected function register_meta_field( $meta_key, $args = array() ) {

		$defaults = array( 
			'type_class' => 'WP_Meta_Box_Field_Text',
			'title_label' => $meta_key,
			'data' => array(), // not yet implemented. Intended to preload form fields that require data (radios, selects, etc.)
		);

		$args = wp_parse_args( $args, $defaults );

		extract( $args );

		// Store meta field details in an array for later.
		$this->meta_fields[] = $meta_field = array(
			'class' => $meta_field_type_class = new $type_class(),
			'meta_key' => $meta_key,
			'sanitize_callback' => array( $meta_field_type_class, 'sanitize' ),
			'auth_callback' => array( $meta_field_type_class, 'authorize' ),
			'render_callback' => array( $meta_field_type_class, 'render_field' ),
			'title_label' => $title_label,
			'data' => array(), // not yet implemented. Intended to preload form fields that require data (radios, selects, etc.)
		);
		add_action( 'admin_enqueue_scripts', array( $meta_field_type_class, 'enqueue_scripts' ) );

		// Register meta with WordPress. Sets up sanitization and authorization callbacks.
		register_meta( 'post', $meta_key, $meta_field['sanitize_callback'], 
			$meta_field['auth_callback'] );
	}

	/**
	 * Render the meta box.
	 */
	public function render_meta_box() {
		foreach ( $this->meta_fields as $meta_field ) {
			call_user_func( $meta_field['render_callback'], $meta_field );
		}
	}

	/**
	 * Enqueue meta box scripts. Override in a subclass.
	 */
	protected function enqueue_scripts() {
		foreach ( $this->meta_fields as $meta_field ) {
			call_user_func( array( $meta_field['class'], 'enqueue_scripts' ) );
		}
	}

	/**
	 * Save the meta fields' data.
	 */
	public function save() {
		global $post;
		foreach ( $this->meta_fields as $meta_field ) {
			// Capabilities check. 
			if ( current_user_can( 'edit_post_meta', $post->ID, $meta_field['meta_key'] ) ) {
				// Check if the $_REQUEST key for the meta field is set.
				if ( isset( $_REQUEST['meta-field-' . $meta_field['meta_key']] ) ) {
					$meta_value = $_REQUEST['meta-field-' . $meta_field['meta_key']];
					update_post_meta( $post->ID, $meta_field['meta_key'], $meta_value );
				}
			}
		}
	}

	/**
	 * Determine whether the meta box will be saved. Override in a subclass if necessary.
	 *
	 * Return true to save, false to cancel.
	 * @param int    The post ID.
	 * @param $post  The post object.
	 */
	protected function maybe_save( $post_id, $post ) {
		// Bail if we're autosaving
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
			return;

		// @TODO Add revision check

		// Cap check
		if ( ! current_user_can( 'edit_post', $post_id ) )
			die;

		return true;
	}

	/* =====================================================================
	 * INTERNAL FUNCTIONS
	 * ===================================================================== */

	/**
	 * Internal function. Registers the meta box.
	 */
	public final function _register_meta_box() {
		$id = "{$this->unique_id}-meta-box";

		add_meta_box( $id, $this->args['title'], array( $this, '_render' ),
			$this->screen, $this->args['context'], $this->args['priority'],
			$this->args );

		$this->register();
	}

	/**
	 * Internal function. Ensures scripts are only loaded when necessary.
	 */
	public final function _enqueue_scripts() {
		$current_screen = get_current_screen();

		if ( isset( $current_screen ) && $this->screen == $current_screen->id )
			$this->enqueue_scripts();
	}

	/**
	 * Internal function, initiates the rendering process.
	 */
	public final function _render() {
		wp_nonce_field( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}", false );
		
		$this->render_meta_box();
	}

	/**
	 * Internal function, initiates the saving process.
	 */
	public final function _save() {
		$current_screen = get_current_screen();
		if ( ! isset( $current_screen ) || $this->screen != $current_screen->id 
			|| ! isset( $_REQUEST["_wpnonce_{$this->unique_id}"] ) )
			return;
		// Nonce check (sorry, you don't have a choice about this one).
		check_admin_referer( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}" );
		$args = func_get_args();
		// Check if we're autosaving/capabilities check.
		if ( call_user_func_array( array( $this, 'maybe_save' ), $args ) ) {
			// Save the meta fields' data.
			call_user_func_array( array( $this, 'save' ), $args );
		}
	}
}



/**
 * Meta Box Registry
 *
 * Implemented as singleton
 *
 */
class WP_Meta_Box_Registry {

	/**
	 * WP_Meta_Box_Registry singleton implementation
	 *
	 * @var WP_Meta_Box_Registry
	 */
	private static $instance;

	private $instances = array();

	/**
	 * WP_Meta_Box_Registry singleton implementation
	 *
	 * @return WP_Meta_Box_Registry
	 */
	public static function instance() {
		if (null === self::$instance)
			self::$instance = new self();
		return self::$instance;
	}

	/**
	 * Search registry for meta box instances
	 *
	 * @return array result
	 */
	public function search($class_or_object, $screen = false, $slug = false) {
		$instances = array();

		foreach($this->instances as $instance) {

			if ( ! $instance instanceof $class_or_object )
				continue;

			if ( $screen !== false && $instance->screen !== $screen )
				continue;

			if ( $slug !== false && $instance->slug !== $slug )
				continue;

			$instances[] = $instance;
		}

		return $instances;
	}

	/**
	 * Adds a meta box instance.
	 *
	 * If $instance->slug is defined, will use $slug.
	 * If a meta box with the same slug exists, it will be overwritten.
	 *
	 * @param WP_Meta_Box $instance
	 */
	public function add( WP_Meta_Box $instance ) {
		static $counter = 0;

		$hash = spl_object_hash( $instance );

		if ( !empty( $instance->slug ) ) {
			// If slug is specified, remove existing instance
			$instances = $this->find( $instance, $instance->screen, $instance->slug );

			if ($instances) 
				$this->remove($instances[0]);

		} else {
			// If no slug is specified, get the numerical index.
			// alternatively get the number of this instances' class by screen and count plus one.
			$instance->slug = ++$counter;
		}

		$this->instances[ $hash ] = $instance;
	}

	// in case this is still needed.
	public function get( WP_Meta_Box $instance ) {
		$hash = spl_object_hash( $instance );

		if ( isset( $this->instances[ $hash ] ) )
			return $instance;

		return false;
	}

	/**
	 * Remove instance from registry
	 *
	 * @param WP_Meta_Box $instance
	 */
	public function remove( WP_Meta_Box $instance ) {
		$hash = spl_object_hash( $instance );

		if ( isset( $this->instances[ $hash ] ) )
			unset( $this->instances[ $hash ] );
	}
}

/**
 * Base class for all other WP_Meta_Box_Fields to inherit from and override.
 * 
 */
class WP_Meta_Box_Field {
	
	static function render_field( $meta_field ) {

	}

	/**
	 * Sanitization callback for generic meta fields.
	 * 
	 * Override in a subclass.
	 */
	public function sanitize( $meta_value, $meta_key ) {
		return $meta_value;
	}

	/**
	 * Default authorization callback. 
	 * 
	 * Override in a subclass.
	 */
	public function authorize( $allowed, $meta_key, $post_ID, $user_id, $cap, $caps ) {
		return true;
	}

	/**
	 * Enqueue scripts for particular field type.
	 * 
	 * Override in a subclass.
	 */
	public function enqueue_scripts() {}
}

/**
 * Meta field class for the WordPress WYSIWYG Editor 
 */
class WP_Meta_Box_Field_WP_Editor extends WP_Meta_Box_Field {

	/**
	 * Render callback for generic meta fields.
	 */
	static function render_field( $meta_field ) {
		global $post;
		extract( $meta_field );
		$meta_value = get_post_meta( $post->ID, $meta_key, true );
		echo "<h4>$title_label</h4>";
		wp_editor( $meta_value, 'meta-field-' . $meta_key );
	}

	/**
	 * Sanitization callback for text field.
	 */
	public function sanitize( $meta_value, $meta_key ) {
		
		$meta_value = wp_kses( $meta_value, wp_kses_allowed_html( 'post' ) );
		return $meta_value;
	}
}

/**
 * Meta field class for a basic textbox
 */
class WP_Meta_Box_Field_Text extends WP_Meta_Box_Field {

	/**
	 * Render callback for generic meta fields.
	 */
	static function render_field( $meta_field ) {
		global $post;
		extract( $meta_field );
		$meta_value = get_post_meta( $post->ID, $meta_key, true );
		echo "<h4>$title_label</h4>";
		echo '<input type="text" id="' . $meta_key . '" name="meta-field-' . $meta_key . '" value="' . esc_attr( $meta_value ) .'">';
	}

	/**
	 * Sanitization callback for text field.
	 */
	public function sanitize( $meta_value, $meta_key ) {
		$meta_value = sanitize_text_field( $meta_value );
		return $meta_value;
	}
}







/**
 * A usage example
 */
class WP_Basic_Meta_Box extends WP_Meta_Box {
	function __construct( $screen, $args = array() ) {
		// Call the WP_Meta_Box constructor.
		parent::__construct( $screen, $args );

		$this->register_meta_field(
			'_basic_meta_box_test_field', 
			array( 
				'type_class' => 'WP_Meta_box_Field_Text',
				'title_label' => 'Some Title here' 
			)
		);
		$this->register_meta_field(
			'_basic_meta_box_wp_editor_field', 
			array( 
				'type_class' => 'WP_Meta_box_Field_WP_Editor',
				'title_label' => 'WP Editor!' 
			)
		);
	}
}

new WP_Basic_Meta_Box( 'post', array( 'title' => 'Basic Meta Box' ) );