tools/meta-box-generator
WordPress MetaBox Generator
WordPress MetaBox Generator is a tool that allows WordPress users to easily create custom meta boxes.
class-wp-skills-metabox-my-custom-metabox.php
<?php
/**
* Meta box: My Custom MetaBox
*
* Generated by wp-skills.com
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WP_Skills_MetaBox_My_Custom_MetaBox {
/**
* Both halves of the nonce come from here, so the field that is written and
* the key that is verified can never drift apart.
*/
const NONCE_ACTION = 'my_custom_metabox_meta_box';
const NONCE_NAME = 'my_custom_metabox_meta_box_nonce';
private $screens = array(
'post',
'page',
);
private $meta_fields = array(
array(
'label' => 'My Custom Field',
'id' => 'my_custom_field',
'type' => 'text',
'default' => '',
)
);
public function __construct() {
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post', array( $this, 'save_fields' ), 10, 2 );
add_action( 'init', array( $this, 'register_meta_keys' ) );
}
public function add_meta_boxes() {
foreach ( $this->screens as $screen ) {
add_meta_box(
'my_custom_metabox',
__( 'My Custom MetaBox', 'text-domain' ),
array( $this, 'render' ),
$screen,
'normal',
'default'
);
}
}
public function render( $post ) {
wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
printf( '<p>%s</p>', esc_html__( 'Lorem ipsum dolor sit amet.', 'text-domain' ) );
echo '<table class="form-table"><tbody>';
foreach ( $this->meta_fields as $meta_field ) {
$this->render_row( $post, $meta_field );
}
echo '</tbody></table>';
}
private function render_row( $post, $meta_field ) {
$meta_value = get_post_meta( $post->ID, $meta_field['id'], true );
// An unset key reads back as '', a saved unchecked box reads back as
// '0'. Testing for '' rather than empty() is what tells those apart, so
// the default cannot re-check a box the user just cleared.
if ( '' === $meta_value && isset( $meta_field['default'] ) ) {
$meta_value = $meta_field['default'];
}
// A radio group has no single input to point at, so it gets a plain
// label rather than one referencing an id nothing carries.
$label_for = 'radio' === $meta_field['type']
? ''
: ' for="' . esc_attr( $meta_field['id'] ) . '"';
printf(
'<tr><th scope="row"><label%1$s>%2$s</label></th><td>%3$s</td></tr>',
$label_for,
esc_html( $meta_field['label'] ),
$this->render_field( $meta_field, $meta_value )
);
}
private function render_field( $meta_field, $meta_value ) {
switch ( $meta_field['type'] ) {
default:
return sprintf(
'<input class="regular-text" id="%1$s" name="%1$s" type="%2$s" value="%3$s">',
esc_attr( $meta_field['id'] ),
esc_attr( $meta_field['type'] ),
esc_attr( $meta_value )
);
}
}
public function save_fields( $post_id, $post ) {
// save_post fires for every post type, so without this a box attached
// to 'post' would write its meta onto pages and every custom type too.
if ( ! in_array( $post->post_type, $this->screens, true ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! isset( $_POST[ self::NONCE_NAME ] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) {
return;
}
// The nonce proves the request came from the edit screen. It does not
// prove this user is allowed to edit this post.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
foreach ( $this->meta_fields as $meta_field ) {
$key = $meta_field['id'];
// An unchecked box posts nothing at all, so its absence is the
// value. Skipping it would leave the previous '1' in place forever.
if ( 'checkbox' === $meta_field['type'] ) {
update_post_meta( $post_id, $key, isset( $_POST[ $key ] ) ? '1' : '0' );
continue;
}
if ( ! isset( $_POST[ $key ] ) ) {
continue;
}
// wp_unslash first: WordPress adds slashes to everything in $_POST,
// so without it an apostrophe gains a backslash on every save.
$value = $this->sanitize_field( $meta_field, wp_unslash( $_POST[ $key ] ) );
update_post_meta( $post_id, $key, $value );
}
}
/**
* WordPress stores whatever it is handed, so this is the only point where
* the value is constrained. Running sanitize_text_field over everything
* would strip exactly the markup a WYSIWYG field exists to keep, which is
* why each type is handled on its own terms.
*/
private function sanitize_field( $meta_field, $value ) {
switch ( $meta_field['type'] ) {
case 'email':
return sanitize_email( $value );
case 'url':
return sanitize_url( $value );
case 'number':
return is_numeric( $value ) ? $value + 0 : '';
case 'color':
// Returns null for anything that is not a hex colour.
return (string) sanitize_hex_color( $value );
case 'textarea':
return sanitize_textarea_field( $value );
case 'wysiwyg':
return wp_kses_post( $value );
case 'categories':
case 'users':
return absint( $value );
case 'media':
return 'url' === $meta_field['returnvalue']
? sanitize_url( $value )
: absint( $value );
case 'radio':
case 'select':
// Nothing stops a crafted request from posting a value that was
// never offered, so the option list doubles as the allow-list.
$allowed = array_map( 'strval', array_keys( $this->field_options( $meta_field ) ) );
return in_array( (string) $value, $allowed, true ) ? $value : '';
default:
return sanitize_text_field( $value );
}
}
/**
* Registers each field's key with WordPress.
*
* 'show_in_rest' needs the post type to support 'custom-fields', which
* register_post_type() does not add by default. Without that support the
* keys register cleanly and simply never appear in a REST response, which
* is the version of this that is hard to debug.
*/
public function register_meta_keys() {
foreach ( $this->screens as $screen ) {
foreach ( $this->meta_fields as $meta_field ) {
register_post_meta(
$screen,
$meta_field['id'],
array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => array( $this, 'sanitize_meta_value' ),
'auth_callback' => array( $this, 'can_edit_this_post' ),
)
);
}
}
}
/**
* The sanitizer WordPress calls for any writer of these keys.
*
* register_meta() hands the callback the value and the key but not the
* field, so the field is looked up here before the type-specific rules in
* sanitize_field() can be applied.
*/
public function sanitize_meta_value( $value, $meta_key ) {
foreach ( $this->meta_fields as $meta_field ) {
if ( $meta_field['id'] === $meta_key ) {
return $this->sanitize_field( $meta_field, $value );
}
}
return sanitize_text_field( $value );
}
/**
* Who may write these keys over the REST API.
*
* Matches the capability the save routine already checks, so a value that
* cannot be edited on the post screen cannot be edited through the API
* either.
*/
public function can_edit_this_post( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
}
}
new WP_Skills_MetaBox_My_Custom_MetaBox();