PluginProbe
AutomatorWP – No-Code Workflow Automation, Integration & Webhooks Plugin, now with AI / 6.0.2
AutomatorWP – No-Code Workflow Automation, Integration & Webhooks Plugin, now with AI v6.0.2
6.0.2 6.0.1 6.0.0 5.8.6 5.8.5 5.8.4 5.8.3 5.8.2 5.8.1 5.8.0 5.7.9.2 5.7.9.1 5.7.8 5.7.9 5.7.6 5.7.7 5.7.5 5.7.4 5.7.3 5.7.2 5.7.1 trunk 5.6.0 5.6.1 5.6.2 All 33 releases
automatorwp / libraries / cmb2 / includes / CMB2.php

CMB2.php in AutomatorWP – No-Code Workflow Automation, Integration & Webhooks Plugin, now with AI 6.0.2, at libraries/cmb2/includes/CMB2.php

1,827 lines 54.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CMB2 - The core metabox object
4 *
5 * @category WordPress_Plugin
6 * @package CMB2
7 * @author CMB2 team
8 * @license GPL-2.0+
9 * @link https://cmb2.io
10 *
11 * @property-read string $cmb_id
12 * @property-read array $meta_box
13 * @property-read array $updated
14 * @property-read bool $has_columns
15 * @property-read array $tax_metaboxes_to_remove
16 */
17
18 /**
19 * The main CMB2 object for storing box data/properties.
20 */
21 class CMB2 extends CMB2_Base {
22
23 /**
24 * Supported CMB2 object types
25 *
26 * @var array
27 * @since 2.11.0
28 */
29 protected $core_object_types = array( 'post', 'user', 'comment', 'term', 'options-page' );
30
31 /**
32 * The object properties name.
33 *
34 * @var string
35 * @since 2.2.3
36 */
37 protected $properties_name = 'meta_box';
38
39 /**
40 * Metabox Config array
41 *
42 * @var array
43 * @since 0.9.0
44 */
45 protected $meta_box = array();
46
47 /**
48 * Type of object registered for metabox. (e.g., post, user, or comment)
49 *
50 * @var string
51 * @since 1.0.0
52 */
53 protected $mb_object_type = null;
54
55 /**
56 * List of fields that are changed/updated on save
57 *
58 * @var array
59 * @since 1.1.0
60 */
61 protected $updated = array();
62
63 /**
64 * Metabox Defaults
65 *
66 * @var array
67 * @since 1.0.1
68 */
69 protected $mb_defaults = array(
70 'id' => '',
71 'title' => '',
72 // Post type slug, or 'user', 'term', 'comment', or 'options-page'.
73 'object_types' => array(),
74
75 /**
76 * The context within the screen where the boxes should display. Available contexts vary
77 * from screen to screen. Post edit screen contexts include 'normal', 'side', and 'advanced'.
78 *
79 * For placement in locations outside of a metabox, other options include:
80 * 'form_top', 'before_permalink', 'after_title', 'after_editor'
81 *
82 * Comments screen contexts include 'normal' and 'side'. Default is 'normal'.
83 */
84 'context' => 'normal',
85 'priority' => 'high', // Or 10 for options pages.
86 'show_names' => true, // Show field names on the left.
87 'show_on_cb' => null, // Callback to determine if metabox should display.
88 'show_on' => array(), // Post IDs or page templates to display this metabox. overrides 'show_on_cb'.
89 'cmb_styles' => true, // Include CMB2 stylesheet.
90 'enqueue_js' => true, // Include CMB2 JS.
91 'fields' => array(),
92
93 /**
94 * Handles hooking CMB2 forms/metaboxes into the post/attachement/user/options-page screens
95 * and handles hooking in and saving those fields.
96 */
97 'hookup' => true,
98 'save_fields' => true, // Will not save during hookup if false.
99 'closed' => false, // Default metabox to being closed.
100 'taxonomies' => array(),
101 'new_user_section' => 'add-new-user', // or 'add-existing-user'.
102 'new_term_section' => true,
103 'show_in_rest' => false,
104 'classes' => null, // Optionally add classes to the CMB2 wrapper.
105 'classes_cb' => '', // Optionally add classes to the CMB2 wrapper (via a callback).
106
107 /*
108 * The following parameter is for post alternate-context metaboxes only.
109 *
110 * To output the fields 'naked' (without a postbox wrapper/style), then
111 * add a `'remove_box_wrap' => true` to your metabox registration array.
112 */
113 'remove_box_wrap' => false,
114
115 /*
116 * The following parameter is any additional arguments passed as $callback_args
117 * to add_meta_box, if/when applicable.
118 *
119 * CMB2 does not use these arguments in the add_meta_box callback, however, these args
120 * are parsed for certain special properties, like determining Gutenberg/block-editor
121 * compatibility.
122 *
123 * Examples:
124 *
125 * - Make sure default editor is used as metabox is not compatible with block editor
126 * [ '__block_editor_compatible_meta_box' => false/true ]
127 *
128 * - Or declare this box exists for backwards compatibility
129 * [ '__back_compat_meta_box' => false ]
130 *
131 * More: https://wordpress.org/gutenberg/handbook/extensibility/meta-box/
132 */
133 'mb_callback_args' => null,
134
135 /*
136 * The following parameters are for options-page metaboxes,
137 * and several are passed along to add_menu_page()/add_submenu_page()
138 */
139
140 // 'menu_title' => null, // Falls back to 'title' (above). Do not define here so we can set a fallback.
141 'message_cb' => '', // Optionally define the options-save message (via a callback).
142 'option_key' => '', // The actual option key and admin menu page slug.
143 'parent_slug' => '', // Used as first param in add_submenu_page().
144 'capability' => 'manage_options', // Cap required to view options-page.
145 'icon_url' => '', // Menu icon. Only applicable if 'parent_slug' is left empty.
146 'position' => null, // Menu position. Only applicable if 'parent_slug' is left empty.
147
148 'admin_menu_hook' => 'admin_menu', // Alternately 'network_admin_menu' to add network-level options page.
149 'display_cb' => false, // Override the options-page form output (CMB2_Hookup::options_page_output()).
150 'save_button' => '', // The text for the options-page save button. Defaults to 'Save'.
151 'disable_settings_errors' => false, // On settings pages (not options-general.php sub-pages), allows disabling.
152 'tab_group' => '', // Tab-group identifier, enables options page tab navigation.
153 // 'tab_title' => null, // Falls back to 'title' (above). Do not define here so we can set a fallback.
154 // 'autoload' => true, // Defaults to true, the options-page option will be autloaded.
155 );
156
157 /**
158 * Metabox field objects
159 *
160 * @var array
161 * @since 2.0.3
162 */
163 protected $fields = array();
164
165 /**
166 * An array of hidden fields to output at the end of the form
167 *
168 * @var array
169 * @since 2.0.0
170 */
171 protected $hidden_fields = array();
172
173 /**
174 * Array of key => value data for saving. Likely $_POST data.
175 *
176 * @var string
177 * @since 2.0.0
178 */
179 protected $generated_nonce = '';
180
181 /**
182 * Whether there are fields to be shown in columns. Set in CMB2::add_field().
183 *
184 * @var bool
185 * @since 2.2.2
186 */
187 protected $has_columns = false;
188
189 /**
190 * If taxonomy field is requesting to remove_default, we store the taxonomy here.
191 *
192 * @var array
193 * @since 2.2.3
194 */
195 protected $tax_metaboxes_to_remove = array();
196
197 /**
198 * Get started
199 *
200 * @since 0.4.0
201 * @param array $config Metabox config array.
202 * @param integer $object_id Optional object id.
203 */
204 public function __construct( $config, $object_id = 0 ) {
205
206 if ( empty( $config['id'] ) ) {
207 wp_die( esc_html__( 'Metabox configuration is required to have an ID parameter.', 'cmb2' ) );
208 }
209
210 $this->cmb_id = $config['id'];
211 $this->meta_box = wp_parse_args( $config, $this->mb_defaults );
212 $this->meta_box['fields'] = array();
213
214 // Ensures object_types is an array.
215 $this->set_prop( 'object_types', $this->box_types() );
216 $this->object_id( $object_id );
217
218 if ( $this->is_options_page_mb() ) {
219
220 // Check initial priority.
221 if ( empty( $config['priority'] ) ) {
222
223 // If not explicitly defined, Reset the priority to 10
224 // Fixes https://github.com/CMB2/CMB2/issues/1410.
225 $this->meta_box['priority'] = 10;
226 }
227
228 $this->init_options_mb();
229 }
230
231 $this->mb_object_type();
232
233 if ( ! empty( $config['fields'] ) && is_array( $config['fields'] ) ) {
234 $this->add_fields( $config['fields'] );
235 }
236
237 CMB2_Boxes::add( $this );
238
239 /**
240 * Hook during initiation of CMB2 object
241 *
242 * The dynamic portion of the hook name, $this->cmb_id, is this meta_box id.
243 *
244 * @param array $cmb This CMB2 object
245 */
246 do_action( "cmb2_init_{$this->cmb_id}", $this );
247
248 // Hook in the hookup... how meta.
249 add_action( "cmb2_init_hookup_{$this->cmb_id}", array( 'CMB2_Hookup', 'maybe_init_and_hookup' ) );
250
251 // Hook in the rest api functionality.
252 add_action( "cmb2_init_hookup_{$this->cmb_id}", array( 'CMB2_REST', 'maybe_init_and_hookup' ) );
253 }
254
255 /**
256 * Loops through and displays fields
257 *
258 * @since 1.0.0
259 * @param int $object_id Object ID.
260 * @param string $object_type Type of object being saved. (e.g., post, user, or comment).
261 *
262 * @return CMB2
263 */
264 public function show_form( $object_id = 0, $object_type = '' ) {
265 $this->render_form_open( $object_id, $object_type );
266
267 foreach ( $this->prop( 'fields' ) as $field_args ) {
268 $this->render_field( $field_args );
269 }
270
271 return $this->render_form_close( $object_id, $object_type );
272 }
273
274 /**
275 * Outputs the opening form markup and runs corresponding hooks:
276 * 'cmb2_before_form' and "cmb2_before_{$object_type}_form_{$this->cmb_id}"
277 *
278 * @since 2.2.0
279 * @param integer $object_id Object ID.
280 * @param string $object_type Object type.
281 *
282 * @return CMB2
283 */
284 public function render_form_open( $object_id = 0, $object_type = '' ) {
285 $object_type = $this->object_type( $object_type );
286 $object_id = $this->object_id( $object_id );
287
288 echo "\n<!-- Begin CMB2 Fields -->\n";
289
290 $this->nonce_field();
291
292 /**
293 * Hook before form table begins
294 *
295 * @param array $cmb_id The current box ID.
296 * @param int $object_id The ID of the current object.
297 * @param string $object_type The type of object you are working with.
298 * Usually `post` (this applies to all post-types).
299 * Could also be `comment`, `user` or `options-page`.
300 * @param array $cmb This CMB2 object.
301 */
302 do_action( 'cmb2_before_form', $this->cmb_id, $object_id, $object_type, $this );
303
304 /**
305 * Hook before form table begins
306 *
307 * The first dynamic portion of the hook name, $object_type, is the type of object
308 * you are working with. Usually `post` (this applies to all post-types).
309 * Could also be `comment`, `user` or `options-page`.
310 *
311 * The second dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
312 *
313 * @param array $cmb_id The current box ID
314 * @param int $object_id The ID of the current object
315 * @param array $cmb This CMB2 object
316 */
317 do_action( "cmb2_before_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
318
319 echo '<div class="', esc_attr( $this->box_classes() ), '"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
320
321 return $this;
322 }
323
324 /**
325 * Defines the classes for the CMB2 form/wrap.
326 *
327 * @since 2.0.0
328 * @return string Space concatenated list of classes
329 */
330 public function box_classes() {
331
332 $classes = array( 'cmb2-wrap', 'form-table' );
333
334 // Use the callback to fetch classes.
335 if ( $added_classes = $this->get_param_callback_result( 'classes_cb' ) ) {
336 $added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
337 $classes = array_merge( $classes, $added_classes );
338 }
339
340 if ( $added_classes = $this->prop( 'classes' ) ) {
341 $added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
342 $classes = array_merge( $classes, $added_classes );
343 }
344
345 /**
346 * Add our context classes for non-standard metaboxes.
347 *
348 * @since 2.2.4
349 */
350 if ( $this->is_alternate_context_box() ) {
351 $context = array();
352
353 // Include custom class if requesting no title.
354 if ( ! $this->prop( 'title' ) && ! $this->prop( 'remove_box_wrap' ) ) {
355 $context[] = 'cmb2-context-wrap-no-title';
356 }
357
358 // Include a generic context wrapper.
359 $context[] = 'cmb2-context-wrap';
360
361 // Include a context-type based context wrapper.
362 $context[] = 'cmb2-context-wrap-' . $this->prop( 'context' );
363
364 // Include an ID based context wrapper as well.
365 $context[] = 'cmb2-context-wrap-' . $this->prop( 'id' );
366
367 // And merge all the classes back into the array.
368 $classes = array_merge( $classes, $context );
369 }
370
371 /**
372 * Globally filter box wrap classes
373 *
374 * @since 2.2.2
375 *
376 * @param string $classes Array of classes for the cmb2-wrap.
377 * @param CMB2 $cmb This CMB2 object.
378 */
379 $classes = apply_filters( 'cmb2_wrap_classes', $classes, $this );
380
381 $split = array();
382 foreach ( array_filter( $classes ) as $class ) {
383 foreach ( explode( ' ', $class ) as $_class ) {
384 // Clean up & sanitize.
385 $split[] = sanitize_html_class( wp_strip_all_tags( $_class ) );
386 }
387 }
388 $classes = $split;
389
390 // Remove any duplicates.
391 $classes = array_unique( $classes );
392
393 // Make it a string.
394 return implode( ' ', $classes );
395 }
396
397 /**
398 * Outputs the closing form markup and runs corresponding hooks:
399 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
400 *
401 * @since 2.2.0
402 * @param integer $object_id Object ID.
403 * @param string $object_type Object type.
404 *
405 * @return CMB2
406 */
407 public function render_form_close( $object_id = 0, $object_type = '' ) {
408 $object_type = $this->object_type( $object_type );
409 $object_id = $this->object_id( $object_id );
410
411 echo '</div></div>';
412
413 $this->render_hidden_fields();
414
415 /**
416 * Hook after form form has been rendered
417 *
418 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
419 *
420 * The first dynamic portion of the hook name, $object_type, is the type of object
421 * you are working with. Usually `post` (this applies to all post-types).
422 * Could also be `comment`, `user` or `options-page`.
423 *
424 * @param int $object_id The ID of the current object
425 * @param array $cmb This CMB2 object
426 */
427 do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
428
429 /**
430 * Hook after form form has been rendered
431 *
432 * @param array $cmb_id The current box ID.
433 * @param int $object_id The ID of the current object.
434 * @param string $object_type The type of object you are working with.
435 * Usually `post` (this applies to all post-types).
436 * Could also be `comment`, `user` or `options-page`.
437 * @param array $cmb This CMB2 object.
438 */
439 do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
440
441 echo "\n<!-- End CMB2 Fields -->\n";
442
443 return $this;
444 }
445
446 /**
447 * Renders a field based on the field type
448 *
449 * @since 2.2.0
450 * @param array $field_args A field configuration array.
451 * @return mixed CMB2_Field object if successful.
452 */
453 public function render_field( $field_args ) {
454 $field_args['context'] = $this->prop( 'context' );
455
456 if ( 'group' === $field_args['type'] ) {
457
458 if ( ! isset( $field_args['show_names'] ) ) {
459 $field_args['show_names'] = $this->prop( 'show_names' );
460 }
461 $field = $this->render_group( $field_args );
462
463 } elseif ( 'hidden' === $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
464 // Save rendering for after the metabox.
465 $field = $this->add_hidden_field( $field_args );
466
467 } else {
468
469 $field_args['show_names'] = $this->prop( 'show_names' );
470
471 // Render default fields.
472 $field = $this->get_field( $field_args )->render_field();
473 }
474
475 return $field;
476 }
477
478 /**
479 * Render a group of fields.
480 *
481 * @param array|CMB2_Field $args Array of field arguments for a group field parent or the group parent field.
482 * @return CMB2_Field|null Group field object.
483 */
484 public function render_group( $args ) {
485 $field_group = false;
486
487 if ( $args instanceof CMB2_Field ) {
488 $field_group = 'group' === $args->type() ? $args : false;
489 } elseif ( isset( $args['id'], $args['fields'] ) && is_array( $args['fields'] ) ) {
490 $field_group = $this->get_field( $args );
491 }
492
493 if ( ! $field_group ) {
494 return;
495 }
496
497 $field_group->render_context = 'edit';
498 $field_group->peform_param_callback( 'render_row_cb' );
499
500 return $field_group;
501 }
502
503 /**
504 * The default callback to render a group of fields.
505 *
506 * @since 2.2.6
507 *
508 * @param array $field_args Array of field arguments for the group field parent.
509 * @param CMB2_Field $field_group The CMB2_Field group object.
510 *
511 * @return CMB2_Field|null Group field object.
512 */
513 public function render_group_callback( $field_args, $field_group ) {
514
515 // If field is requesting to be conditionally shown.
516 if ( ! $field_group || ! $field_group->should_show() ) {
517 return;
518 }
519
520 $field_group->index = 0;
521
522 $field_group->peform_param_callback( 'before_group' );
523
524 $desc = $field_group->args( 'description' );
525 $label = $field_group->args( 'name' );
526 $group_val = (array) $field_group->value();
527
528 echo '<div class="cmb-row cmb-repeat-group-wrap ', esc_attr( $field_group->row_classes() ), '" data-fieldtype="group"><div class="cmb-td"><div data-groupid="', esc_attr( $field_group->id() ), '" id="', esc_attr( $field_group->id() ), '_repeat" ', $this->group_wrap_attributes( $field_group ), '>';
529
530 if ( $desc || $label ) {
531 $class = $desc ? ' cmb-group-description' : '';
532 echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
533 if ( $label ) {
534 echo '<h2 class="cmb-group-name">', $label, '</h2>';
535 }
536 if ( $desc ) {
537 echo '<p class="cmb2-metabox-description">', $desc, '</p>';
538 }
539 echo '</div></div>';
540 }
541
542 if ( ! empty( $group_val ) ) {
543 $group_val_count = count( $group_val );
544 for ( $i = 0; $i < $group_val_count; $i++ ) {
545 $this->render_group_row( $field_group );
546 $field_group->index++;
547 }
548 } else {
549 $this->render_group_row( $field_group );
550 }
551
552 if ( $field_group->args( 'repeatable' ) ) {
553 echo '<div class="cmb-row"><div class="cmb-td"><p class="cmb-add-row"><button type="button" data-selector="', esc_attr( $field_group->id() ), '_repeat" data-grouptitle="', esc_attr( $field_group->options( 'group_title' ) ), '" class="cmb-add-group-row button-secondary">', $field_group->options( 'add_button' ), '</button></p></div></div>';
554 }
555
556 echo '</div></div></div>';
557
558 $field_group->peform_param_callback( 'after_group' );
559
560 return $field_group;
561 }
562
563 /**
564 * Get the group wrap attributes, which are passed through a filter.
565 *
566 * @since 2.2.3
567 * @param CMB2_Field $field_group The group CMB2_Field object.
568 * @return string The attributes string.
569 */
570 public function group_wrap_attributes( $field_group ) {
571 $classes = 'cmb-nested cmb-field-list cmb-repeatable-group';
572 $classes .= $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
573 $classes .= $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
574
575 $group_wrap_attributes = array(
576 'class' => $classes,
577 'style' => 'width:100%;',
578 );
579
580 /**
581 * Allow for adding additional HTML attributes to a group wrapper.
582 *
583 * The attributes will be an array of key => value pairs for each attribute.
584 *
585 * @since 2.2.2
586 *
587 * @param string $group_wrap_attributes Current attributes array.
588 * @param CMB2_Field $field_group The group CMB2_Field object.
589 */
590 $group_wrap_attributes = apply_filters( 'cmb2_group_wrap_attributes', $group_wrap_attributes, $field_group );
591
592 $atts = array();
593 foreach ( $group_wrap_attributes as $att => $att_value ) {
594 if ( ! CMB2_Utils::is_data_attribute( $att ) ) {
595 $att_value = htmlspecialchars( $att_value, ENT_COMPAT );
596 }
597
598 $atts[ sanitize_html_class( $att ) ] = sanitize_text_field( $att_value );
599 }
600
601 return CMB2_Utils::concat_attrs( $atts );
602 }
603
604 /**
605 * Render a repeatable group row
606 *
607 * @since 1.0.2
608 * @param CMB2_Field $field_group CMB2_Field group field object.
609 *
610 * @return CMB2
611 */
612 public function render_group_row( $field_group ) {
613
614 $field_group->peform_param_callback( 'before_group_row' );
615 $closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
616 $confirm_deletion = $field_group->options( 'remove_confirm' );
617 $confirm_deletion = ! empty( $confirm_deletion ) ? $confirm_deletion : '';
618
619 echo '
620 <div id="cmb-group-', $field_group->id(), '-', $field_group->index, '" class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
621
622 if ( $field_group->args( 'repeatable' ) ) {
623 echo '<button type="button" data-selector="', $field_group->id(), '_repeat" data-confirm="', esc_attr( $confirm_deletion ), '" class="dashicons-before dashicons-no-alt cmb-remove-group-row" title="', esc_attr( $field_group->options( 'remove_button' ) ), '"></button>';
624 }
625
626 echo '
627 <div class="cmbhandle" title="', esc_attr__( 'Click to toggle', 'cmb2' ), '"><br></div>
628 <h3 class="cmb-group-title cmbhandle-title"><span>', $field_group->replace_hash( $field_group->options( 'group_title' ) ), '</span></h3>
629
630 <div class="inside cmb-td cmb-nested cmb-field-list">';
631 // Loop and render repeatable group fields.
632 foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
633 if ( 'hidden' === $field_args['type'] ) {
634
635 // Save rendering for after the metabox.
636 $this->add_hidden_field( $field_args, $field_group );
637
638 } else {
639
640 $field_args['show_names'] = $field_group->args( 'show_names' );
641 $field_args['context'] = $field_group->args( 'context' );
642
643 $this->get_field( $field_args, $field_group )->render_field();
644 }
645 }
646
647 if ( $field_group->args( 'repeatable' ) ) {
648 echo '
649 <div class="cmb-row cmb-remove-field-row">
650 <div class="cmb-remove-row">
651 <button type="button" data-selector="', $field_group->id(), '_repeat" data-confirm="', esc_attr( $confirm_deletion ), '" class="cmb-remove-group-row cmb-remove-group-row-button alignright button-secondary">', $field_group->options( 'remove_button' ), '</button>
652 </div>
653 </div>
654 ';
655 }
656 echo '
657 </div>
658 </div>
659 ';
660
661 $field_group->peform_param_callback( 'after_group_row' );
662
663 return $this;
664 }
665
666 /**
667 * Add a hidden field to the list of hidden fields to be rendered later.
668 *
669 * @since 2.0.0
670 *
671 * @param array $field_args Array of field arguments to be passed to CMB2_Field.
672 * @param CMB2_Field|null $field_group CMB2_Field group field object.
673 * @return CMB2_Field
674 */
675 public function add_hidden_field( $field_args, $field_group = null ) {
676 if ( isset( $field_args['field_args'] ) ) {
677 // For back-compatibility.
678 $field = new CMB2_Field( $field_args );
679 } else {
680 $field = $this->get_new_field( $field_args, $field_group );
681 }
682
683 $types = new CMB2_Types( $field );
684
685 if ( $field_group ) {
686 $types->iterator = $field_group->index;
687 }
688
689 $this->hidden_fields[] = $types;
690
691 return $field;
692 }
693
694 /**
695 * Loop through and output hidden fields
696 *
697 * @since 2.0.0
698 *
699 * @return CMB2
700 */
701 public function render_hidden_fields() {
702 if ( ! empty( $this->hidden_fields ) ) {
703 foreach ( $this->hidden_fields as $hidden ) {
704 $hidden->render();
705 }
706 }
707
708 return $this;
709 }
710
711 /**
712 * Returns array of sanitized field values (without saving them)
713 *
714 * @since 2.0.3
715 * @param array $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
716 * @return mixed
717 */
718 public function get_sanitized_values( array $data_to_sanitize ) {
719 $this->data_to_save = $data_to_sanitize;
720 $stored_id = $this->object_id();
721
722 // We do this So CMB will sanitize our data for us, but not save it.
723 $this->object_id( '_' );
724
725 // Ensure temp. data store is empty.
726 cmb2_options( 0 )->set();
727
728 // We want to get any taxonomy values back.
729 add_filter( "cmb2_return_taxonomy_values_{$this->cmb_id}", '__return_true' );
730
731 // Process/save fields.
732 $this->process_fields();
733
734 // Put things back the way they were.
735 remove_filter( "cmb2_return_taxonomy_values_{$this->cmb_id}", '__return_true' );
736
737 // Get data from temp. data store.
738 $sanitized_values = cmb2_options( 0 )->get_options();
739
740 // Empty out temp. data store again.
741 cmb2_options( 0 )->set();
742
743 // Reset the object id.
744 $this->object_id( $stored_id );
745
746 return $sanitized_values;
747 }
748
749 /**
750 * Loops through and saves field data
751 *
752 * @since 1.0.0
753 * @param int $object_id Object ID.
754 * @param string $object_type Type of object being saved. (e.g., post, user, or comment).
755 * @param array $data_to_save Array of key => value data for saving. Likely $_POST data.
756 *
757 * @return CMB2
758 */
759 public function save_fields( $object_id = 0, $object_type = '', $data_to_save = array() ) {
760
761 // Fall-back to $_POST data.
762 $this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
763 $object_id = $this->object_id( $object_id );
764 $object_type = $this->object_type( $object_type );
765
766 $this->process_fields();
767
768 // If options page, save the updated options.
769 if ( 'options-page' === $object_type ) {
770 cmb2_options( $object_id )->set();
771 }
772
773 return $this->after_save();
774 }
775
776 /**
777 * Process and save form fields
778 *
779 * @since 2.0.0
780 *
781 * @return CMB2
782 */
783 public function process_fields() {
784
785 $this->pre_process();
786
787 // Remove the show_on properties so saving works.
788 $this->prop( 'show_on', array() );
789
790 // save field ids of those that are updated.
791 $this->updated = array();
792
793 foreach ( $this->prop( 'fields' ) as $field_args ) {
794 $this->process_field( $field_args );
795 }
796
797 return $this;
798 }
799
800 /**
801 * Process and save a field
802 *
803 * @since 2.0.0
804 * @param array $field_args Array of field arguments.
805 *
806 * @return CMB2
807 */
808 public function process_field( $field_args ) {
809
810 switch ( $field_args['type'] ) {
811
812 case 'group':
813 if ( $this->save_group( $field_args ) ) {
814 $this->updated[] = $field_args['id'];
815 }
816
817 break;
818
819 case 'title':
820 // Don't process title fields.
821 break;
822
823 default:
824 $field = $this->get_new_field( $field_args );
825
826 if ( $field->save_field_from_data( $this->data_to_save ) ) {
827 $this->updated[] = $field->id();
828 }
829
830 break;
831 }
832
833 return $this;
834 }
835
836 /**
837 * Fires the "cmb2_{$object_type}_process_fields_{$cmb_id}" action hook.
838 *
839 * @since 2.2.2
840 *
841 * @return CMB2
842 */
843 public function pre_process() {
844 $object_type = $this->object_type();
845
846 /**
847 * Fires before fields have been processed/saved.
848 *
849 * The dynamic portion of the hook name, $object_type, refers to the
850 * metabox/form's object type
851 * Usually `post` (this applies to all post-types).
852 * Could also be `comment`, `user` or `options-page`.
853 *
854 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
855 *
856 * @param array $cmb This CMB2 object
857 * @param int $object_id The ID of the current object
858 */
859 do_action( "cmb2_{$object_type}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
860
861 return $this;
862 }
863
864 /**
865 * Fires the "cmb2_save_{$object_type}_fields" and
866 * "cmb2_save_{$object_type}_fields_{$cmb_id}" action hooks.
867 *
868 * @since 2.x.x
869 *
870 * @return CMB2
871 */
872 public function after_save() {
873 $object_type = $this->object_type();
874 $object_id = $this->object_id();
875
876 /**
877 * Fires after all fields have been saved.
878 *
879 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
880 * Usually `post` (this applies to all post-types).
881 * Could also be `comment`, `user` or `options-page`.
882 *
883 * @param int $object_id The ID of the current object
884 * @param array $cmb_id The current box ID
885 * @param string $updated Array of field ids that were updated.
886 * Will only include field ids that had values change.
887 * @param array $cmb This CMB2 object
888 */
889 do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
890
891 /**
892 * Fires after all fields have been saved.
893 *
894 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
895 *
896 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
897 * Usually `post` (this applies to all post-types).
898 * Could also be `comment`, `user` or `options-page`.
899 *
900 * @param int $object_id The ID of the current object
901 * @param string $updated Array of field ids that were updated.
902 * Will only include field ids that had values change.
903 * @param array $cmb This CMB2 object
904 */
905 do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
906
907 return $this;
908 }
909
910 /**
911 * Save a repeatable group
912 *
913 * @since 1.x.x
914 * @param array $args Field arguments array.
915 * @return mixed Return of CMB2_Field::update_data().
916 */
917 public function save_group( $args ) {
918 if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
919 return;
920 }
921
922 return $this->save_group_field( $this->get_new_field( $args ) );
923 }
924
925 /**
926 * Save a repeatable group
927 *
928 * @since 1.x.x
929 * @param CMB2_Field $field_group CMB2_Field group field object.
930 * @return mixed Return of CMB2_Field::update_data().
931 */
932 public function save_group_field( $field_group ) {
933 $base_id = $field_group->id();
934
935 if ( ! isset( $this->data_to_save[ $base_id ] ) ) {
936 return;
937 }
938
939 $old = $field_group->get_data();
940 // Check if group field has sanitization_cb.
941 $group_vals = $field_group->sanitization_cb( $this->data_to_save[ $base_id ] );
942 $saved = array();
943
944 $field_group->index = 0;
945 $field_group->data_to_save = $this->data_to_save;
946
947 foreach ( array_values( $field_group->fields() ) as $field_args ) {
948 if ( 'title' === $field_args['type'] ) {
949 // Don't process title fields.
950 continue;
951 }
952
953 $field = $this->get_new_field( $field_args, $field_group );
954 $sub_id = $field->id( true );
955 if ( empty( $saved[ $field_group->index ] ) ) {
956 $saved[ $field_group->index ] = array();
957 }
958
959 foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
960
961 // Get value.
962 $new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
963 ? $group_vals[ $field_group->index ][ $sub_id ]
964 : false;
965
966 // Sanitize.
967 $new_val = $field->sanitization_cb( $new_val );
968
969 if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
970 if ( $field->args( 'repeatable' ) ) {
971 $_new_val = array();
972 foreach ( $new_val as $group_index => $grouped_data ) {
973 // Add the supporting data to the $saved array stack.
974 $saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
975 // Reset var to the actual value.
976 $_new_val[ $group_index ] = $grouped_data['value'];
977 }
978 $new_val = $_new_val;
979 } else {
980 // Add the supporting data to the $saved array stack.
981 $saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
982 // Reset var to the actual value.
983 $new_val = $new_val['value'];
984 }
985 }
986
987 // Get old value.
988 $old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
989 ? $old[ $field_group->index ][ $sub_id ]
990 : false;
991
992 $is_updated = ( ! CMB2_Utils::isempty( $new_val ) && $new_val !== $old_val );
993 $is_removed = ( CMB2_Utils::isempty( $new_val ) && ! CMB2_Utils::isempty( $old_val ) );
994
995 // Compare values and add to `$updated` array.
996 if ( $is_updated || $is_removed ) {
997 $this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
998 }
999
1000 // Add to `$saved` array.
1001 $saved[ $field_group->index ][ $sub_id ] = $new_val;
1002
1003 }// End foreach.
1004
1005 $saved[ $field_group->index ] = CMB2_Utils::filter_empty( $saved[ $field_group->index ] );
1006 }// End foreach.
1007
1008 $saved = CMB2_Utils::filter_empty( $saved );
1009
1010 return $field_group->update_data( $saved, true );
1011 }
1012
1013 /**
1014 * Get object id from global space if no id is provided
1015 *
1016 * @since 1.0.0
1017 * @param integer|string $object_id Object ID.
1018 * @return integer|string $object_id Object ID.
1019 */
1020 public function object_id( $object_id = 0 ) {
1021 global $pagenow;
1022
1023 if ( $object_id ) {
1024 $this->object_id = $object_id;
1025 return $this->object_id;
1026 }
1027
1028 if ( $this->object_id ) {
1029 return $this->object_id;
1030 }
1031
1032 // Try to get our object ID from the global space.
1033 switch ( $this->object_type() ) {
1034 case 'user':
1035 $object_id = isset( $_REQUEST['user_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_id'] ) ) : $object_id;
1036 $object_id = ! $object_id && 'user-new.php' !== $pagenow && isset( $GLOBALS['user_ID'] ) ? $GLOBALS['user_ID'] : $object_id;
1037 break;
1038
1039 case 'comment':
1040 $object_id = isset( $_REQUEST['c'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['c'] ) ) : $object_id;
1041 $object_id = ! $object_id && isset( $GLOBALS['comments']->comment_ID ) ? $GLOBALS['comments']->comment_ID : $object_id;
1042 break;
1043
1044 case 'term':
1045 $object_id = isset( $_REQUEST['tag_ID'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['tag_ID'] ) ) : $object_id;
1046 break;
1047
1048 case 'options-page':
1049 $key = $this->doing_options_page();
1050 if ( ! empty( $key ) ) {
1051 $object_id = $key;
1052 }
1053 break;
1054
1055 default:
1056 $object_id = isset( $GLOBALS['post']->ID ) ? $GLOBALS['post']->ID : $object_id;
1057 $object_id = isset( $_REQUEST['post'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post'] ) ) : $object_id;
1058 break;
1059 }
1060
1061 /**
1062 * Filter the object id.
1063 *
1064 * @since 2.11.0
1065 *
1066 * @param integer|string $object_id Object ID.
1067 * @param CMB2 $cmb This CMB2 object.
1068 */
1069 $object_id = apply_filters( 'cmb2_set_object_id', $object_id, $this );
1070
1071 // reset to id or 0.
1072 $this->object_id = ! empty( $object_id ) ? $object_id : 0;
1073
1074 return $this->object_id;
1075 }
1076
1077 /**
1078 * Sets the $object_type based on metabox settings
1079 *
1080 * @since 1.0.0
1081 * @return string Object type.
1082 */
1083 public function mb_object_type() {
1084 if ( null !== $this->mb_object_type ) {
1085 return $this->mb_object_type;
1086 }
1087
1088 $found_type = '';
1089
1090 if ( $this->is_options_page_mb() ) {
1091 $found_type = 'options-page';
1092 } else {
1093 $registered_types = $this->box_types();
1094
1095 // if it's an array of one, extract it.
1096 if ( 1 === count( $registered_types ) ) {
1097 $last = end( $registered_types );
1098 if ( is_string( $last ) ) {
1099 $found_type = $last;
1100 }
1101 } else {
1102 $current_object_type = $this->current_object_type();
1103 if ( in_array( $current_object_type, $registered_types, true ) ) {
1104 $found_type = $current_object_type;
1105 }
1106 }
1107 }
1108
1109 // Get our object type.
1110 $mb_object_type = $this->is_supported_core_object_type( $found_type )
1111 ? $found_type
1112 : 'post';
1113
1114 /**
1115 * Filter the metabox object type.
1116 *
1117 * @since 2.11.0
1118 *
1119 * @param string $mb_object_type The metabox object type.
1120 * @param string $found_type The found object type.
1121 * @param CMB2 $cmb This CMB2 object.
1122 */
1123 $this->mb_object_type = apply_filters( 'cmb2_set_box_object_type', $mb_object_type, $found_type, $this );
1124
1125 return $this->mb_object_type;
1126 }
1127
1128 /**
1129 * Gets the box 'object_types' array based on box settings.
1130 *
1131 * @since 2.2.3
1132 * @param array $fallback Fallback value.
1133 *
1134 * @return array Object types.
1135 */
1136 public function box_types( $fallback = array() ) {
1137 return CMB2_Utils::ensure_array( $this->prop( 'object_types' ), $fallback );
1138 }
1139
1140 /**
1141 * Check if given object_type(s) matches any of the registered object types or
1142 * taxonomies for this box.
1143 *
1144 * @since 2.7.0
1145 * @param string|array $object_types The object type(s) to check.
1146 * @param array $fallback Fallback object_types value.
1147 *
1148 * @return bool Whether given object type(s) are registered to this box.
1149 */
1150 public function is_box_type( $object_types = array(), $fallback = array() ) {
1151 $object_types = (array) $object_types;
1152 $box_types = $this->box_types( $fallback );
1153
1154 if ( in_array( 'term', $box_types, true ) ) {
1155 $taxonomies = CMB2_Utils::ensure_array( $this->prop( 'taxonomies' ) );
1156 $box_types = array_merge( $box_types, $taxonomies );
1157 }
1158
1159 $found = array_intersect( $object_types, $box_types );
1160
1161 return ! empty( $found );
1162 }
1163
1164 /**
1165 * Initates the object types and option key for an options page metabox.
1166 *
1167 * @since 2.2.5
1168 *
1169 * @return void
1170 */
1171 public function init_options_mb() {
1172 $keys = $this->options_page_keys();
1173 $types = $this->box_types();
1174
1175 if ( empty( $keys ) ) {
1176 $keys = '';
1177 $types = $this->deinit_options_mb( $types );
1178 } else {
1179
1180 // Make sure 'options-page' is one of the object types.
1181 $types[] = 'options-page';
1182 }
1183
1184 // Set/Reset the option_key property.
1185 $this->set_prop( 'option_key', $keys );
1186
1187 // Reset the object types.
1188 $this->set_prop( 'object_types', array_unique( $types ) );
1189 }
1190
1191 /**
1192 * If object-page initiation failed, remove traces options page setup.
1193 *
1194 * @since 2.2.5
1195 *
1196 * @param array $types Array of types.
1197 * @return array
1198 */
1199 protected function deinit_options_mb( $types ) {
1200 if ( isset( $this->meta_box['show_on']['key'] ) && 'options-page' === $this->meta_box['show_on']['key'] ) {
1201 unset( $this->meta_box['show_on']['key'] );
1202 }
1203
1204 if ( array_key_exists( 'options-page', $this->meta_box['show_on'] ) ) {
1205 unset( $this->meta_box['show_on']['options-page'] );
1206 }
1207
1208 $index = array_search( 'options-page', $types );
1209
1210 if ( false !== $index ) {
1211 unset( $types[ $index ] );
1212 }
1213
1214 return $types;
1215 }
1216
1217 /**
1218 * Determines if metabox is for an options page
1219 *
1220 * @since 1.0.1
1221 * @return boolean True/False.
1222 */
1223 public function is_options_page_mb() {
1224 return (
1225 // 'show_on' values checked for back-compatibility.
1226 $this->is_old_school_options_page_mb()
1227 || in_array( 'options-page', $this->box_types() )
1228 );
1229 }
1230
1231 /**
1232 * Determines if metabox uses old-schoold options page config.
1233 *
1234 * @since 2.2.5
1235 * @return boolean True/False.
1236 */
1237 public function is_old_school_options_page_mb() {
1238 return (
1239 // 'show_on' values checked for back-compatibility.
1240 isset( $this->meta_box['show_on']['key'] ) && 'options-page' === $this->meta_box['show_on']['key']
1241 || array_key_exists( 'options-page', $this->meta_box['show_on'] )
1242 );
1243 }
1244
1245 /**
1246 * Determine if we are on an options page (or saving the options page).
1247 *
1248 * @since 2.2.5
1249 *
1250 * @return bool
1251 */
1252 public function doing_options_page() {
1253 $found_key = false;
1254 $keys = $this->options_page_keys();
1255
1256 if ( empty( $keys ) ) {
1257 return $found_key;
1258 }
1259
1260 if ( ! empty( $_GET['page'] ) && in_array( $_GET['page'], $keys ) ) {
1261 $found_key = $_GET['page'];
1262 }
1263
1264 if ( ! empty( $_POST['action'] ) && in_array( $_POST['action'], $keys ) ) {
1265 $found_key = $_POST['action'];
1266 }
1267
1268 return $found_key ? $found_key : false;
1269 }
1270
1271 /**
1272 * Get the options page key.
1273 *
1274 * @since 2.2.5
1275 * @return string|array
1276 */
1277 public function options_page_keys() {
1278 $key = '';
1279 if ( ! $this->is_options_page_mb() ) {
1280 return $key;
1281 }
1282
1283 $values = null;
1284 if ( ! empty( $this->meta_box['show_on']['value'] ) ) {
1285 $values = $this->meta_box['show_on']['value'];
1286 } elseif ( ! empty( $this->meta_box['show_on']['options-page'] ) ) {
1287 $values = $this->meta_box['show_on']['options-page'];
1288 } elseif ( $this->prop( 'option_key' ) ) {
1289 $values = $this->prop( 'option_key' );
1290 }
1291
1292 if ( $values ) {
1293 $key = $values;
1294 }
1295
1296 if ( ! is_array( $key ) ) {
1297 $key = array( $key );
1298 }
1299
1300 return $key;
1301 }
1302
1303 /**
1304 * Returns the object type
1305 *
1306 * @since 1.0.0
1307 * @param string $object_type Type of object being saved. (e.g., post, user, or comment). Optional.
1308 * @return string Object type.
1309 */
1310 public function object_type( $object_type = '' ) {
1311 if ( $object_type ) {
1312 $this->object_type = $object_type;
1313 return $this->object_type;
1314 }
1315
1316 if ( $this->object_type ) {
1317 return $this->object_type;
1318 }
1319
1320 $this->object_type = $this->current_object_type();
1321
1322 return $this->object_type;
1323 }
1324
1325 /**
1326 * Get the object type for the current page, based on the $pagenow global.
1327 *
1328 * @since 2.2.2
1329 * @return string Page object type name.
1330 */
1331 public function current_object_type() {
1332 global $pagenow;
1333 $type = 'post';
1334
1335 if ( in_array( $pagenow, array( 'user-edit.php', 'profile.php', 'user-new.php' ), true ) ) {
1336 $type = 'user';
1337 }
1338
1339 if ( in_array( $pagenow, array( 'edit-comments.php', 'comment.php' ), true ) ) {
1340 $type = 'comment';
1341 }
1342
1343 if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
1344 $type = 'term';
1345 }
1346
1347 if ( defined( 'DOING_AJAX' ) && isset( $_POST['action'] ) && 'add-tag' === $_POST['action'] ) {
1348 $type = 'term';
1349 }
1350
1351 if (
1352 in_array( $pagenow, array( 'admin.php', 'admin-post.php' ), true )
1353 && $this->doing_options_page()
1354 ) {
1355 $type = 'options-page';
1356 }
1357
1358 return $type;
1359 }
1360
1361 /**
1362 * Set metabox property.
1363 *
1364 * @since 2.2.2
1365 * @param string $property Metabox config property to retrieve.
1366 * @param mixed $value Value to set if no value found.
1367 * @return mixed Metabox config property value or false.
1368 */
1369 public function set_prop( $property, $value ) {
1370 $this->meta_box[ $property ] = $value;
1371
1372 return $this->prop( $property );
1373 }
1374
1375 /**
1376 * Get metabox property and optionally set a fallback
1377 *
1378 * @since 2.0.0
1379 * @param string $property Metabox config property to retrieve.
1380 * @param mixed $fallback Fallback value to set if no value found.
1381 * @return mixed Metabox config property value or false.
1382 */
1383 public function prop( $property, $fallback = null ) {
1384 if ( array_key_exists( $property, $this->meta_box ) ) {
1385 return $this->meta_box[ $property ];
1386 } elseif ( $fallback ) {
1387 return $this->meta_box[ $property ] = $fallback;
1388 }
1389 }
1390
1391 /**
1392 * Get a field object
1393 *
1394 * @since 2.0.3
1395 * @param string|array|CMB2_Field $field Metabox field id or field config array or CMB2_Field object.
1396 * @param CMB2_Field|null $field_group (optional) CMB2_Field object (group parent).
1397 * @param bool $reset_cached (optional) Reset the internal cache for this field object.
1398 * Use sparingly.
1399 *
1400 * @return CMB2_Field|false CMB2_Field object (or false).
1401 */
1402 public function get_field( $field, $field_group = null, $reset_cached = false ) {
1403 if ( $field instanceof CMB2_Field ) {
1404 return $field;
1405 }
1406
1407 $field_id = is_string( $field ) ? $field : $field['id'];
1408
1409 $parent_field_id = ! empty( $field_group ) ? $field_group->id() : '';
1410 $ids = $this->get_field_ids( $field_id, $parent_field_id );
1411
1412 if ( ! $ids ) {
1413 return false;
1414 }
1415
1416 list( $field_id, $sub_field_id ) = $ids;
1417
1418 $index = $field_id . ( $sub_field_id ? '|' . $sub_field_id : '' ) . ( $field_group ? '|' . $field_group->index : '' );
1419
1420 if ( array_key_exists( $index, $this->fields ) && ! $reset_cached ) {
1421 return $this->fields[ $index ];
1422 }
1423
1424 $this->fields[ $index ] = new CMB2_Field( $this->get_field_args( $field_id, $field, $sub_field_id, $field_group ) );
1425
1426 return $this->fields[ $index ];
1427 }
1428
1429 /**
1430 * Handles determining which type of arguments to pass to CMB2_Field
1431 *
1432 * @since 2.0.7
1433 * @param mixed $field_id Field (or group field) ID.
1434 * @param mixed $field_args Array of field arguments.
1435 * @param mixed $sub_field_id Sub field ID (if field_group exists).
1436 * @param CMB2_Field|null $field_group If a sub-field, will be the parent group CMB2_Field object.
1437 * @return array Array of CMB2_Field arguments.
1438 */
1439 public function get_field_args( $field_id, $field_args, $sub_field_id, $field_group ) {
1440
1441 // Check if group is passed and if fields were added in the old-school fields array.
1442 if ( $field_group && ( $sub_field_id || 0 === $sub_field_id ) ) {
1443
1444 // Update the fields array w/ any modified properties inherited from the group field.
1445 $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] = $field_args;
1446
1447 return $this->get_default_args( $field_args, $field_group );
1448 }
1449
1450 if ( is_array( $field_args ) ) {
1451 $this->meta_box['fields'][ $field_id ] = array_merge( $field_args, $this->meta_box['fields'][ $field_id ] );
1452 }
1453
1454 return $this->get_default_args( $this->meta_box['fields'][ $field_id ] );
1455 }
1456
1457 /**
1458 * Get default field arguments specific to this CMB2 object.
1459 *
1460 * @since 2.2.0
1461 * @param array $field_args Metabox field config array.
1462 * @param CMB2_Field $field_group (optional) CMB2_Field object (group parent).
1463 * @return array Array of field arguments.
1464 */
1465 protected function get_default_args( $field_args, $field_group = null ) {
1466 if ( $field_group ) {
1467 $args = array(
1468 'field_args' => $field_args,
1469 'group_field' => $field_group,
1470 );
1471 } else {
1472 $args = array(
1473 'field_args' => $field_args,
1474 'object_type' => $this->object_type(),
1475 'object_id' => $this->object_id(),
1476 'cmb_id' => $this->cmb_id,
1477 );
1478 }
1479
1480 return $args;
1481 }
1482
1483 /**
1484 * When fields are added in the old-school way, intitate them as they should be
1485 *
1486 * @since 2.1.0
1487 * @param array $fields Array of fields to add.
1488 * @param mixed $parent_field_id Parent field id or null.
1489 *
1490 * @return CMB2
1491 */
1492 protected function add_fields( $fields, $parent_field_id = null ) {
1493 foreach ( $fields as $field ) {
1494
1495 $sub_fields = false;
1496 if ( array_key_exists( 'fields', $field ) ) {
1497 $sub_fields = $field['fields'];
1498 unset( $field['fields'] );
1499 }
1500
1501 $field_id = $parent_field_id
1502 ? $this->add_group_field( $parent_field_id, $field )
1503 : $this->add_field( $field );
1504
1505 if ( $sub_fields ) {
1506 $this->add_fields( $sub_fields, $field_id );
1507 }
1508 }
1509
1510 return $this;
1511 }
1512
1513 /**
1514 * Add a field to the metabox
1515 *
1516 * @since 2.0.0
1517 * @param array $field Metabox field config array.
1518 * @param int $position (optional) Position of metabox. 1 for first, etc.
1519 * @return string|false Field id or false.
1520 */
1521 public function add_field( array $field, $position = 0 ) {
1522 if ( ! array_key_exists( 'id', $field ) ) {
1523 return false;
1524 }
1525
1526 $this->_add_field_to_array(
1527 $field,
1528 $this->meta_box['fields'],
1529 $position
1530 );
1531
1532 return $field['id'];
1533 }
1534
1535 /**
1536 * Add a field to a group
1537 *
1538 * @since 2.0.0
1539 * @param string $parent_field_id The field id of the group field to add the field.
1540 * @param array $field Metabox field config array.
1541 * @param int $position (optional) Position of metabox. 1 for first, etc.
1542 * @return mixed Array of parent/field ids or false.
1543 */
1544 public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
1545 if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
1546 return false;
1547 }
1548
1549 $parent_field = $this->meta_box['fields'][ $parent_field_id ];
1550
1551 if ( 'group' !== $parent_field['type'] ) {
1552 return false;
1553 }
1554
1555 if ( ! isset( $parent_field['fields'] ) ) {
1556 $this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
1557 }
1558
1559 $this->_add_field_to_array(
1560 $field,
1561 $this->meta_box['fields'][ $parent_field_id ]['fields'],
1562 $position
1563 );
1564
1565 return array( $parent_field_id, $field['id'] );
1566 }
1567
1568 /**
1569 * Perform some field-type-specific initiation actions.
1570 *
1571 * @since 2.7.0
1572 * @param array $field Metabox field config array.
1573 * @return void
1574 */
1575 protected function field_actions( $field ) {
1576 $field = CMB2_Hookup_Field::init( $field, $this );
1577
1578 if ( isset( $field['column'] ) && false !== $field['column'] ) {
1579 $field = $this->define_field_column( $field );
1580 }
1581
1582 if ( isset( $field['taxonomy'] ) && ! empty( $field['remove_default'] ) ) {
1583 $this->tax_metaboxes_to_remove[ $field['taxonomy'] ] = $field['taxonomy'];
1584 }
1585
1586 return $field;
1587 }
1588
1589 /**
1590 * Defines a field's column if requesting to be show in admin columns.
1591 *
1592 * @since 2.2.3
1593 * @param array $field Metabox field config array.
1594 * @return array Modified metabox field config array.
1595 */
1596 protected function define_field_column( array $field ) {
1597 $this->has_columns = true;
1598
1599 $column = is_array( $field['column'] ) ? $field['column'] : array();
1600
1601 $field['column'] = wp_parse_args( $column, array(
1602 'name' => isset( $field['name'] ) ? $field['name'] : '',
1603 'position' => false,
1604 ) );
1605
1606 return $field;
1607 }
1608
1609 /**
1610 * Add a field array to a fields array in desired position
1611 *
1612 * @since 2.0.2
1613 * @param array $field Metabox field config array.
1614 * @param array $fields Array (passed by reference) to append the field (array) to.
1615 * @param integer $position Optionally specify a position in the array to be inserted.
1616 */
1617 protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1618 $field = $this->field_actions( $field );
1619
1620 if ( $position ) {
1621 CMB2_Utils::array_insert( $fields, array( $field['id'] => $field ), $position );
1622 } else {
1623 $fields[ $field['id'] ] = $field;
1624 }
1625 }
1626
1627 /**
1628 * Remove a field from the metabox
1629 *
1630 * @since 2.0.0
1631 * @param string $field_id The field id of the field to remove.
1632 * @param string $parent_field_id (optional) The field id of the group field to remove field from.
1633 * @return bool True if field was removed.
1634 */
1635 public function remove_field( $field_id, $parent_field_id = '' ) {
1636 $ids = $this->get_field_ids( $field_id, $parent_field_id );
1637
1638 if ( ! $ids ) {
1639 return false;
1640 }
1641
1642 list( $field_id, $sub_field_id ) = $ids;
1643
1644 unset( $this->fields[ implode( '', $ids ) ] );
1645
1646 if ( ! $sub_field_id ) {
1647 unset( $this->meta_box['fields'][ $field_id ] );
1648 return true;
1649 }
1650
1651 if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1652 unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1653 }
1654 if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1655 unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1656 }
1657
1658 return true;
1659 }
1660
1661 /**
1662 * Update or add a property to a field
1663 *
1664 * @since 2.0.0
1665 * @param string $field_id Field id.
1666 * @param string $property Field property to set/update.
1667 * @param mixed $value Value to set the field property.
1668 * @param string $parent_field_id (optional) The field id of the group field to remove field from.
1669 * @return mixed Field id. Strict compare to false, as success can return a falsey value (like 0).
1670 */
1671 public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1672 $ids = $this->get_field_ids( $field_id, $parent_field_id );
1673
1674 if ( ! $ids ) {
1675 return false;
1676 }
1677
1678 list( $field_id, $sub_field_id ) = $ids;
1679
1680 if ( ! $sub_field_id ) {
1681 $this->meta_box['fields'][ $field_id ][ $property ] = $value;
1682 return $field_id;
1683 }
1684
1685 $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1686 return $field_id;
1687 }
1688
1689 /**
1690 * Check if field ids match a field and return the index/field id
1691 *
1692 * @since 2.0.2
1693 * @param string $field_id Field id.
1694 * @param string $parent_field_id (optional) Parent field id.
1695 * @return mixed Array of field/parent ids, or false.
1696 */
1697 public function get_field_ids( $field_id, $parent_field_id = '' ) {
1698 $sub_field_id = $parent_field_id ? $field_id : '';
1699 $field_id = $parent_field_id ? $parent_field_id : $field_id;
1700 $fields =& $this->meta_box['fields'];
1701
1702 if ( ! array_key_exists( $field_id, $fields ) ) {
1703 $field_id = $this->search_old_school_array( $field_id, $fields );
1704 }
1705
1706 if ( false === $field_id ) {
1707 return false;
1708 }
1709
1710 if ( ! $sub_field_id ) {
1711 return array( $field_id, $sub_field_id );
1712 }
1713
1714 if ( 'group' !== $fields[ $field_id ]['type'] ) {
1715 return false;
1716 }
1717
1718 if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1719 $sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1720 }
1721
1722 return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1723 }
1724
1725 /**
1726 * When using the old array filter, it is unlikely field array indexes will be the field id.
1727 *
1728 * @since 2.0.2
1729 * @param string $field_id The field id.
1730 * @param array $fields Array of fields to search.
1731 * @return mixed Field index or false.
1732 */
1733 public function search_old_school_array( $field_id, $fields ) {
1734 $ids = wp_list_pluck( $fields, 'id' );
1735 $index = array_search( $field_id, $ids );
1736 return false !== $index ? $index : false;
1737 }
1738
1739 /**
1740 * Handles metabox property callbacks, and passes this $cmb object as property.
1741 *
1742 * @since 2.2.3
1743 * @param callable $cb The callback method/function/closure.
1744 * @param mixed $additional_params Any additoinal parameters which should be passed to the callback.
1745 * @return mixed Return of the callback function.
1746 */
1747 public function do_callback( $cb, $additional_params = null ) {
1748 return call_user_func( $cb, $this, $additional_params );
1749 }
1750
1751 /**
1752 * Generate a unique nonce field for each registered meta_box
1753 *
1754 * @since 2.0.0
1755 * @return void
1756 */
1757 public function nonce_field() {
1758 wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1759 }
1760
1761 /**
1762 * Generate a unique nonce for each registered meta_box
1763 *
1764 * @since 2.0.0
1765 * @return string unique nonce string.
1766 */
1767 public function nonce() {
1768 if ( ! $this->generated_nonce ) {
1769 $this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1770 }
1771
1772 return $this->generated_nonce;
1773 }
1774
1775 /**
1776 * Checks if field-saving updated any fields.
1777 *
1778 * @since 2.2.5
1779 *
1780 * @return bool
1781 */
1782 public function was_updated() {
1783 return ! empty( $this->updated );
1784 }
1785
1786 /**
1787 * Whether this box is an "alternate context" box. This means the box has a 'context' property defined as:
1788 * 'form_top', 'before_permalink', 'after_title', or 'after_editor'.
1789 *
1790 * @since 2.2.4
1791 * @return bool
1792 */
1793 public function is_alternate_context_box() {
1794 return $this->prop( 'context' ) && in_array( $this->prop( 'context' ), array( 'form_top', 'before_permalink', 'after_title', 'after_editor' ), true );
1795 }
1796
1797 /**
1798 * Whether given object type is one of the core supported object types.
1799 *
1800 * @since 2.11.0
1801 * @return bool
1802 */
1803 public function is_supported_core_object_type( $object_type ) {
1804 return in_array( $object_type, $this->core_object_types, true );
1805 }
1806
1807 /**
1808 * Magic getter for our object.
1809 *
1810 * @param string $property Object property.
1811 * @throws Exception Throws an exception if the field is invalid.
1812 * @return mixed
1813 */
1814 public function __get( $property ) {
1815 switch ( $property ) {
1816 case 'updated':
1817 case 'has_columns':
1818 case 'tax_metaboxes_to_remove':
1819 case 'core_object_types':
1820 return $this->{$property};
1821 default:
1822 return parent::__get( $property );
1823 }
1824 }
1825
1826 }
1827