WordPress.org

Make WordPress Core

Ticket #18179: meta-box.3.php

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

Custom sanitization callback support added, with a default for generic form field types. Basic text and wp_editor meta field types added to begin with. Authorization callbacks supported, with a default of automatic authorization (if the user already has edit_published_post cap). Form rendering has defaults that can be overridden in a subclass.

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' => 'text',
121                        'render_callback' => array( $this, 'render_field' ),
122                        'sanitize_callback' => array( $this, 'sanitize' ),
123                        'auth_callback' => array( $this, 'authorize' ),
124                        'title_label' => $meta_key,
125                        'data' => array(), // not yet used. to preload form fields that expect data (radios, selects, etc.)
126                );
127
128                $args = wp_parse_args( $args, $defaults );
129
130                extract( $args );
131
132                // Register meta with WordPress. Sets up sanitization and authorization callbacks.
133                register_meta( 'post', $meta_key, $sanitize_callback, $auth_callback );
134
135                // Store meta field details in an array for later.
136                $this->meta_fields[] = array( 
137                        'type' => $type, 
138                        'meta_key' => $meta_key, 
139                        'render_callback' => $render_callback,
140                        'title_label' => $title_label, 
141                        'data' => array(),
142                );
143        }
144
145        /**
146         * Render the meta box.
147         */
148        public function render_meta_box() {
149                foreach ( $this->meta_fields as $meta_field ) {
150                        call_user_func( $meta_field['render_callback'], $meta_field );
151                }
152        }
153
154        /**
155         * Render callback for generic meta fields.
156         */
157        protected function render_field( $meta_field ) {
158                global $post;
159               
160                extract( $meta_field );
161                $meta_value = get_post_meta( $post->ID, $meta_key, true );
162                echo "<h4>$title_label</h4>";
163                switch ( $type ) {
164                        case 'text':
165                                echo '<input type="text" id="' . $meta_key . '" name="meta-field-' . $meta_key . '" value="' . esc_attr( $meta_value ) .'">';
166                        break;
167                        case 'wp_editor':
168                                wp_editor( $meta_value, 'meta-field-' . $meta_key );
169                        break;
170                }
171        }
172
173        /**
174         * Enqueue meta box scripts. Override in a subclass.
175         */
176        protected function enqueue_scripts() {
177                $unique_meta_types = array_unique( wp_list_pluck( $this->meta_fields, 'type' ) );
178                foreach ( $unique_meta_types as $meta_type ) {
179                        switch ( $meta_type ) {
180                                // Specific script enqueues for the displayed meta fields.
181                        }
182                }
183        }
184
185        /**
186         * Sanitization callback for generic meta fields.
187         */
188        public function sanitize( $meta_value, $meta_key ) {
189                foreach ( $this->meta_fields as $meta_field ) {
190                        if ( $meta_field['meta_key'] == $meta_key ) {
191                                switch ( $meta_field['type'] ) {
192                                        case 'text' :
193                                                $meta_value = sanitize_text_field( $meta_value );
194                                        break;
195                                        case 'wp_editor' :
196                                                $meta_value = wp_kses_post( $meta_value );
197                                        break;
198                                }
199                        }
200                }
201                return $meta_value;
202        }
203
204        /**
205         * Default authorization callback.
206         */
207        public function authorize( $allowed, $meta_key, $post_ID, $user_id, $cap, $caps ) {
208                return true;
209        }
210
211        /**
212         * Save the meta fields' data.
213         */
214        public function save() {
215                global $post;
216                foreach ( $this->meta_fields as $meta_field ) {
217                        // Capabilities check.
218                        if ( current_user_can( 'edit_post_meta', $post->ID, $meta_field['meta_key'] ) ) {
219                                // Check if the $_REQUEST array has the specific key set.
220
221                                if ( isset( $_REQUEST['meta-field-' . $meta_field['meta_key']] ) ) {
222                                        $meta_value = $_REQUEST['meta-field-' . $meta_field['meta_key']];
223                                        update_post_meta( $post->ID, $meta_field['meta_key'], $meta_value );
224                                }
225                        }
226                }
227        }
228
229        /**
230         * Determine whether the meta box will be saved. Override in a subclass if necessary.
231         *
232         * Return true to save, false to cancel.
233         * @param int    The post ID.
234         * @param $post  The post object.
235         */
236        protected function maybe_save( $post_id, $post ) {
237                // Bail if we're autosaving
238                if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
239                        return;
240
241                // @TODO Add revision check
242
243                // Cap check
244                if ( ! current_user_can( 'edit_post', $post_id ) )
245                        die;
246
247                return true;
248        }
249
250        /* =====================================================================
251         * INTERNAL FUNCTIONS
252         * ===================================================================== */
253
254        /**
255         * Internal function. Registers the meta box.
256         */
257        public final function _register_meta_box() {
258                $id = "{$this->unique_id}-meta-box";
259
260                add_meta_box( $id, $this->args['title'], array( $this, '_render' ),
261                        $this->screen, $this->args['context'], $this->args['priority'],
262                        $this->args );
263
264                $this->register();
265        }
266
267        /**
268         * Internal function. Ensures scripts are only loaded when necessary.
269         */
270        public final function _enqueue_scripts() {
271                $current_screen = get_current_screen();
272
273                if ( isset( $current_screen ) && $this->screen == $current_screen->id )
274                        $this->enqueue_scripts();
275        }
276
277        /**
278         * Internal function, initiates the rendering process.
279         */
280        public final function _render() { // todo should this be public? can subclasses access public parent class functions?
281                wp_nonce_field( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}", false );
282               
283                $this->render_meta_box();
284        }
285
286        /**
287         * Internal function, initiates the saving process.
288         */
289        public final function _save() {
290                // Nonce check (sorry, you don't have a choice about this one).
291                check_admin_referer( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}" );
292
293                $args = func_get_args();
294                // Check if we're autosaving/capabilities check.
295                if ( call_user_func_array( array( $this, 'maybe_save' ), $args ) ) {
296                        // Save the meta fields' data.
297                        call_user_func_array( array( $this, 'save' ), $args );
298                }
299        }
300}
301
302
303
304/**
305 * Meta Box Registry
306 *
307 * Implemented as singleton
308 *
309 */
310class WP_Meta_Box_Registry {
311
312        /**
313         * WP_Meta_Box_Registry singleton implementation
314         *
315         * @var WP_Meta_Box_Registry
316         */
317        private static $instance;
318
319        private $instances = array();
320
321        /**
322         * WP_Meta_Box_Registry singleton implementation
323         *
324         * @return WP_Meta_Box_Registry
325         */
326        public static function instance() {
327                if (null === self::$instance)
328                        self::$instance = new self();
329                return self::$instance;
330        }
331
332        /**
333         * Search registry for meta box instances
334         *
335         * @return array result
336         */
337        public function search($class_or_object, $screen = false, $slug = false) {
338                $instances = array();
339
340                foreach($this->instances as $instance) {
341
342                        if ( ! $instance instanceof $class_or_object )
343                                continue;
344
345                        if ( $screen !== false && $instance->screen !== $screen )
346                                continue;
347
348                        if ( $slug !== false && $instance->slug !== $slug )
349                                continue;
350
351                        $instances[] = $instance;
352                }
353
354                return $instances;
355        }
356
357        /**
358         * Adds a meta box instance.
359         *
360         * If $instance->slug is defined, will use $slug.
361         * If a meta box with the same slug exists, it will be overwritten.
362         *
363         * @param WP_Meta_Box $instance
364         */
365        public function add( WP_Meta_Box $instance ) {
366                static $counter = 0;
367
368                $hash = spl_object_hash( $instance );
369
370                if ( !empty( $instance->slug ) ) {
371                        // If slug is specified, remove existing instance
372                        $instances = $this->find( $instance, $instance->screen, $instance->slug );
373
374                        if ($instances) 
375                                $this->remove($instances[0]);
376
377                } else {
378                        // If no slug is specified, get the numerical index.
379                        // alternatively get the number of this instances' class by screen and count plus one.
380                        $instance->slug = ++$counter;
381                }
382
383                $this->instances[ $hash ] = $instance;
384        }
385
386        // in case this is still needed.
387        public function get( WP_Meta_Box $instance ) {
388                $hash = spl_object_hash( $instance );
389
390                if ( isset( $this->instances[ $hash ] ) )
391                        return $instance;
392
393                return false;
394        }
395
396        /**
397         * Remove instance from registry
398         *
399         * @param WP_Meta_Box $instance
400         */
401        public function remove( WP_Meta_Box $instance ) {
402                $hash = spl_object_hash( $instance );
403
404                if ( isset( $this->instances[ $hash ] ) )
405                        unset( $this->instances[ $hash ] );
406        }
407}
408
409class WP_Basic_Meta_Box extends WP_Meta_Box {
410        function __construct( $screen, $args = array() ) {
411                // Call the WP_Meta_Box constructor.
412                parent::__construct( $screen, $args );
413
414                // Save the meta box contents when 'save_post' is triggered.
415                $this->register_meta_field(
416                        '_basic_meta_box_test_field', 
417                        array( 
418                                'type' => 'text',
419                                'title_label' => 'Some Title here' 
420                        )
421                );
422                $this->register_meta_field(
423                        '_basic_meta_box_wp_editor_field', 
424                        array( 
425                                'type' => 'wp_editor',
426                                'title_label' => 'WP Editor!' 
427                        )
428                );
429        }
430}
431
432new WP_Basic_Meta_Box( 'post', array( 'title' => 'Basic Meta Box' ) );