WordPress.org

Make WordPress Core

Ticket #18179: meta-box.5.php

File meta-box.5.php, 11.9 KB (added by ericlewis, 14 months ago)

Move meta field types into classes, which include their own rendering, sanitization, authorization, and script enqueueing methods.

Line 
1<?php
2/*
3Plugin Name: Custom Metaboxes for Core
4*/
5
6class WP_Meta_Box {
7        /**
8         * The screen where the meta box will be shown.
9         */
10        public $screen;
11
12        /**
13         * A unique slug to allow multiple meta boxes of the same class
14         * to exist on the same screen.
15         */
16        public $slug;
17
18        /**
19         * A unique identifier for the meta box.
20         *
21         * A string composed from the meta box class, screen, and slug.
22         */
23        public $unique_id;
24
25        /**
26         * Arguments passed to the meta box constructor.
27         */
28        public $args;
29
30        /**
31         * A registry that tracks each meta box instance.
32         */
33        public static $registry;
34
35        /**
36         * A multi-dimensional array of registered meta fields, and their settings.
37         */
38        public $meta_fields;
39
40
41        /**
42         * Constructor.
43         *
44         * Any subclasses MUST call parent::_construct( $screen, $args ).
45         *
46         * @param string $screen - The ID (string) for the screen where the meta box will be shown.
47         * @param array  $args - An array of supplied arguments. Optional.
48         *
49         * $args recognized by default:
50         *    title    - The title of the meta box.
51         *    context  - The context within the page where the box should show ('normal', 'advanced').
52         *    priority - The priority within the context where the boxes should show ('high', 'low').
53         *    slug     - A unique string to identify the meta box.
54         */
55        public function __construct( $screen, $args = array() ) {
56                // Set variables
57                $this->screen = $screen;
58                $this->slug   = empty( $args['slug'] ) ? '' : $args['slug'];
59
60                // Remove slug from args
61                unset( $args['slug'] );
62
63                // Set default args
64                $defaults = array(
65                        'title'         => __( 'Untitled' , 'wordcampbase'),
66                        'context'       => 'side',
67                        'priority'      => 'default',
68                );
69                $this->args = wp_parse_args( $args, $defaults );
70
71
72                add_action( 'save_post', array( $this, '_save' ), 10, 99 ); // Effectively infinite args.
73
74                // Add the meta box to the registry.
75                // Generates a slug if one doesn't already exist.
76                WP_Meta_Box::registry()->add( $this );
77
78                // Construct the meta box's unique ID.
79                $this->unique_id = get_class( $this ) . "_{$this->screen}_{$this->slug}";
80
81                // Bind hooks
82                add_action( 'admin_init',            array( $this, '_register_meta_box' ) );
83                add_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
84        }
85
86        /**
87         * Fetches the meta box registry.
88         */
89        public static function registry() {
90                if ( ! isset( WP_Meta_Box::$registry ) )
91                        WP_Meta_Box::$registry = new WP_Meta_Box_Registry();
92                return WP_Meta_Box::$registry;
93        }
94
95        /**
96         * Fires when the meta box is registered. Override in a subclass.
97         */
98        protected function register() {}
99
100        /**
101         * Remove the meta box.
102         */
103        public function remove() {
104                // Make sure we've removed any data from the registry.
105                WP_Meta_Box::registry()->remove( get_class( $this ), $this->screen, $this->slug );
106
107                // Unbind actions.
108                remove_action( 'admin_init',            array( $this, '_register_meta_box' ) );
109                remove_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
110        }
111       
112        /**
113         * Register a post meta field.
114         *
115         * For use in a subclass.
116         */
117        protected function register_meta_field( $meta_key, $args = array() ) {
118
119                $defaults = array( 
120                        'type_class' => 'WP_Meta_Box_Field_Text',
121                        'title_label' => $meta_key,
122                        'data' => array(), // not yet implemented. Intended to preload form fields that require data (radios, selects, etc.)
123                );
124
125                $args = wp_parse_args( $args, $defaults );
126
127                extract( $args );
128
129                // Store meta field details in an array for later.
130                $this->meta_fields[] = $meta_field = array(
131                        'class' => $meta_field_type_class = new $type_class(),
132                        'meta_key' => $meta_key,
133                        'sanitize_callback' => array( $meta_field_type_class, 'sanitize' ),
134                        'auth_callback' => array( $meta_field_type_class, 'authorize' ),
135                        'render_callback' => array( $meta_field_type_class, 'render_field' ),
136                        'title_label' => $title_label,
137                        'data' => array(), // not yet implemented. Intended to preload form fields that require data (radios, selects, etc.)
138                );
139                add_action( 'admin_enqueue_scripts', array( $meta_field_type_class, 'enqueue_scripts' ) );
140
141                // Register meta with WordPress. Sets up sanitization and authorization callbacks.
142                register_meta( 'post', $meta_key, $meta_field['sanitize_callback'], 
143                        $meta_field['auth_callback'] );
144        }
145
146        /**
147         * Render the meta box.
148         */
149        public function render_meta_box() {
150                foreach ( $this->meta_fields as $meta_field ) {
151                        call_user_func( $meta_field['render_callback'], $meta_field );
152                }
153        }
154
155        /**
156         * Enqueue meta box scripts. Override in a subclass.
157         */
158        protected function enqueue_scripts() {
159                foreach ( $this->meta_fields as $meta_field ) {
160                        call_user_func( array( $meta_field['class'], 'enqueue_scripts' ) );
161                }
162        }
163
164        /**
165         * Save the meta fields' data.
166         */
167        public function save() {
168                global $post;
169                foreach ( $this->meta_fields as $meta_field ) {
170                        // Capabilities check.
171                        if ( current_user_can( 'edit_post_meta', $post->ID, $meta_field['meta_key'] ) ) {
172                                // Check if the $_REQUEST key for the meta field is set.
173                                if ( isset( $_REQUEST['meta-field-' . $meta_field['meta_key']] ) ) {
174                                        $meta_value = $_REQUEST['meta-field-' . $meta_field['meta_key']];
175                                        update_post_meta( $post->ID, $meta_field['meta_key'], $meta_value );
176                                }
177                        }
178                }
179        }
180
181        /**
182         * Determine whether the meta box will be saved. Override in a subclass if necessary.
183         *
184         * Return true to save, false to cancel.
185         * @param int    The post ID.
186         * @param $post  The post object.
187         */
188        protected function maybe_save( $post_id, $post ) {
189                // Bail if we're autosaving
190                if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
191                        return;
192
193                // @TODO Add revision check
194
195                // Cap check
196                if ( ! current_user_can( 'edit_post', $post_id ) )
197                        die;
198
199                return true;
200        }
201
202        /* =====================================================================
203         * INTERNAL FUNCTIONS
204         * ===================================================================== */
205
206        /**
207         * Internal function. Registers the meta box.
208         */
209        public final function _register_meta_box() {
210                $id = "{$this->unique_id}-meta-box";
211
212                add_meta_box( $id, $this->args['title'], array( $this, '_render' ),
213                        $this->screen, $this->args['context'], $this->args['priority'],
214                        $this->args );
215
216                $this->register();
217        }
218
219        /**
220         * Internal function. Ensures scripts are only loaded when necessary.
221         */
222        public final function _enqueue_scripts() {
223                $current_screen = get_current_screen();
224
225                if ( isset( $current_screen ) && $this->screen == $current_screen->id )
226                        $this->enqueue_scripts();
227        }
228
229        /**
230         * Internal function, initiates the rendering process.
231         */
232        public final function _render() {
233                wp_nonce_field( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}", false );
234               
235                $this->render_meta_box();
236        }
237
238        /**
239         * Internal function, initiates the saving process.
240         */
241        public final function _save() {
242                $current_screen = get_current_screen();
243                if ( ! isset( $current_screen ) || $this->screen != $current_screen->id 
244                        || ! isset( $_REQUEST["_wpnonce_{$this->unique_id}"] ) )
245                        return;
246                // Nonce check (sorry, you don't have a choice about this one).
247                check_admin_referer( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}" );
248                $args = func_get_args();
249                // Check if we're autosaving/capabilities check.
250                if ( call_user_func_array( array( $this, 'maybe_save' ), $args ) ) {
251                        // Save the meta fields' data.
252                        call_user_func_array( array( $this, 'save' ), $args );
253                }
254        }
255}
256
257
258
259/**
260 * Meta Box Registry
261 *
262 * Implemented as singleton
263 *
264 */
265class WP_Meta_Box_Registry {
266
267        /**
268         * WP_Meta_Box_Registry singleton implementation
269         *
270         * @var WP_Meta_Box_Registry
271         */
272        private static $instance;
273
274        private $instances = array();
275
276        /**
277         * WP_Meta_Box_Registry singleton implementation
278         *
279         * @return WP_Meta_Box_Registry
280         */
281        public static function instance() {
282                if (null === self::$instance)
283                        self::$instance = new self();
284                return self::$instance;
285        }
286
287        /**
288         * Search registry for meta box instances
289         *
290         * @return array result
291         */
292        public function search($class_or_object, $screen = false, $slug = false) {
293                $instances = array();
294
295                foreach($this->instances as $instance) {
296
297                        if ( ! $instance instanceof $class_or_object )
298                                continue;
299
300                        if ( $screen !== false && $instance->screen !== $screen )
301                                continue;
302
303                        if ( $slug !== false && $instance->slug !== $slug )
304                                continue;
305
306                        $instances[] = $instance;
307                }
308
309                return $instances;
310        }
311
312        /**
313         * Adds a meta box instance.
314         *
315         * If $instance->slug is defined, will use $slug.
316         * If a meta box with the same slug exists, it will be overwritten.
317         *
318         * @param WP_Meta_Box $instance
319         */
320        public function add( WP_Meta_Box $instance ) {
321                static $counter = 0;
322
323                $hash = spl_object_hash( $instance );
324
325                if ( !empty( $instance->slug ) ) {
326                        // If slug is specified, remove existing instance
327                        $instances = $this->find( $instance, $instance->screen, $instance->slug );
328
329                        if ($instances) 
330                                $this->remove($instances[0]);
331
332                } else {
333                        // If no slug is specified, get the numerical index.
334                        // alternatively get the number of this instances' class by screen and count plus one.
335                        $instance->slug = ++$counter;
336                }
337
338                $this->instances[ $hash ] = $instance;
339        }
340
341        // in case this is still needed.
342        public function get( WP_Meta_Box $instance ) {
343                $hash = spl_object_hash( $instance );
344
345                if ( isset( $this->instances[ $hash ] ) )
346                        return $instance;
347
348                return false;
349        }
350
351        /**
352         * Remove instance from registry
353         *
354         * @param WP_Meta_Box $instance
355         */
356        public function remove( WP_Meta_Box $instance ) {
357                $hash = spl_object_hash( $instance );
358
359                if ( isset( $this->instances[ $hash ] ) )
360                        unset( $this->instances[ $hash ] );
361        }
362}
363
364/**
365 * Base class for all other WP_Meta_Box_Fields to inherit from and override.
366 *
367 */
368class WP_Meta_Box_Field {
369       
370        static function render_field( $meta_field ) {
371
372        }
373
374        /**
375         * Sanitization callback for generic meta fields.
376         *
377         * Override in a subclass.
378         */
379        public function sanitize( $meta_value, $meta_key ) {
380                return $meta_value;
381        }
382
383        /**
384         * Default authorization callback.
385         *
386         * Override in a subclass.
387         */
388        public function authorize( $allowed, $meta_key, $post_ID, $user_id, $cap, $caps ) {
389                return true;
390        }
391
392        /**
393         * Enqueue scripts for particular field type.
394         *
395         * Override in a subclass.
396         */
397        public function enqueue_scripts() {}
398}
399
400/**
401 * Meta field class for the WordPress WYSIWYG Editor
402 */
403class WP_Meta_Box_Field_WP_Editor extends WP_Meta_Box_Field {
404
405        /**
406         * Render callback for generic meta fields.
407         */
408        static function render_field( $meta_field ) {
409                global $post;
410                extract( $meta_field );
411                $meta_value = get_post_meta( $post->ID, $meta_key, true );
412                echo "<h4>$title_label</h4>";
413                wp_editor( $meta_value, 'meta-field-' . $meta_key );
414        }
415
416        /**
417         * Sanitization callback for text field.
418         */
419        public function sanitize( $meta_value, $meta_key ) {
420               
421                $meta_value = wp_kses( $meta_value, wp_kses_allowed_html( 'post' ) );
422                return $meta_value;
423        }
424}
425
426/**
427 * Meta field class for a basic textbox
428 */
429class WP_Meta_Box_Field_Text extends WP_Meta_Box_Field {
430
431        /**
432         * Render callback for generic meta fields.
433         */
434        static function render_field( $meta_field ) {
435                global $post;
436                extract( $meta_field );
437                $meta_value = get_post_meta( $post->ID, $meta_key, true );
438                echo "<h4>$title_label</h4>";
439                echo '<input type="text" id="' . $meta_key . '" name="meta-field-' . $meta_key . '" value="' . esc_attr( $meta_value ) .'">';
440        }
441
442        /**
443         * Sanitization callback for text field.
444         */
445        public function sanitize( $meta_value, $meta_key ) {
446                $meta_value = sanitize_text_field( $meta_value );
447                return $meta_value;
448        }
449}
450
451
452
453
454
455
456
457/**
458 * A usage example
459 */
460class WP_Basic_Meta_Box extends WP_Meta_Box {
461        function __construct( $screen, $args = array() ) {
462                // Call the WP_Meta_Box constructor.
463                parent::__construct( $screen, $args );
464
465                $this->register_meta_field(
466                        '_basic_meta_box_test_field', 
467                        array( 
468                                'type_class' => 'WP_Meta_box_Field_Text',
469                                'title_label' => 'Some Title here' 
470                        )
471                );
472                $this->register_meta_field(
473                        '_basic_meta_box_wp_editor_field', 
474                        array( 
475                                'type_class' => 'WP_Meta_box_Field_WP_Editor',
476                                'title_label' => 'WP Editor!' 
477                        )
478                );
479        }
480}
481
482new WP_Basic_Meta_Box( 'post', array( 'title' => 'Basic Meta Box' ) );