Completed
Push — trunk ( 131d47...e1f52d )
by Justin
05:45
created

includes/CMB2.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * CMB2 - The core metabox object
4
 *
5
 * @category  WordPress_Plugin
6
 * @package   CMB2
7
 * @author    WebDevStudios
8
 * @license   GPL-2.0+
9
 * @link      http://webdevstudios.com
10
 *
11
 * @property-read string $cmb_id
12
 * @property-read array $meta_box
13
 * @property-read array $updated
14
 */
15
class CMB2 {
16
17
	/**
18
	 * Current CMB2 instance ID
19
	 * @var   string
20
	 * @since 2.0.0
21
	 */
22
	protected $cmb_id = '';
23
24
	/**
25
	 * Metabox Config array
26
	 * @var   array
27
	 * @since 0.9.0
28
	 */
29
	protected $meta_box = array();
30
31
	/**
32
	 * Object ID for metabox meta retrieving/saving
33
	 * @var   mixed
34
	 * @since 1.0.0
35
	 */
36
	protected $object_id = 0;
37
38
	/**
39
	 * Type of object being saved. (e.g., post, user, or comment)
40
	 * @var   string
41
	 * @since 1.0.0
42
	 */
43
	protected $object_type = 'post';
44
45
	/**
46
	 * Type of object registered for metabox. (e.g., post, user, or comment)
47
	 * @var   string
48
	 * @since 1.0.0
49
	 */
50
	protected $mb_object_type = null;
51
52
	/**
53
	 * List of fields that are changed/updated on save
54
	 * @var   array
55
	 * @since 1.1.0
56
	 */
57
	protected $updated = array();
58
59
	/**
60
	 * Metabox Defaults
61
	 * @var   array
62
	 * @since 1.0.1
63
	 */
64
	protected $mb_defaults = array(
65
		'id'               => '',
66
		'title'            => '',
67
		'type'             => '',
68
		'object_types'     => array(), // Post type
69
		'context'          => 'normal',
70
		'priority'         => 'high',
71
		'show_names'       => true, // Show field names on the left
72
		'show_on_cb'       => null, // Callback to determine if metabox should display.
73
		'show_on'          => array(), // Post IDs or page templates to display this metabox. overrides 'show_on_cb'
74
		'cmb_styles'       => true, // Include CMB2 stylesheet
75
		'enqueue_js'       => true, // Include CMB2 JS
76
		'fields'           => array(),
77
		'hookup'           => true,
78
		'save_fields'      => true, // Will not save during hookup if false
79
		'closed'           => false, // Default to metabox being closed?
80
		'taxonomies'       => array(),
81
		'new_user_section' => 'add-new-user', // or 'add-existing-user'
82
		'new_term_section' => true,
83
	);
84
85
	/**
86
	 * Metabox field objects
87
	 * @var   array
88
	 * @since 2.0.3
89
	 */
90
	protected $fields = array();
91
92
	/**
93
	 * An array of hidden fields to output at the end of the form
94
	 * @var   array
95
	 * @since 2.0.0
96
	 */
97
	protected $hidden_fields = array();
98
99
	/**
100
	 * Array of key => value data for saving. Likely $_POST data.
101
	 * @var   array
102
	 * @since 2.0.0
103
	 */
104
	public $data_to_save = array();
105
106
	/**
107
	 * Array of key => value data for saving. Likely $_POST data.
108
	 * @var   string
109
	 * @since 2.0.0
110
	 */
111
	protected $generated_nonce = '';
112
113
	/**
114
	 * Get started
115
	 * @since 0.4.0
116
	 * @param array   $meta_box  Metabox config array
117
	 * @param integer $object_id Optional object id
118
	 */
119 44
	public function __construct( $meta_box, $object_id = 0 ) {
120
121 44
		if ( empty( $meta_box['id'] ) ) {
122 1
			wp_die( __( 'Metabox configuration is required to have an ID parameter', 'cmb2' ) );
123
		}
124
125 44
		$this->meta_box = wp_parse_args( $meta_box, $this->mb_defaults );
126 44
		$this->meta_box['fields'] = array();
127
128 44
		$this->object_id( $object_id );
129 44
		$this->mb_object_type();
130 44
		$this->cmb_id = $meta_box['id'];
131
132 44
		if ( ! empty( $meta_box['fields'] ) && is_array( $meta_box['fields'] ) ) {
133 41
			$this->add_fields( $meta_box['fields'] );
134 41
		}
135
136 44
		CMB2_Boxes::add( $this );
137
138
		/**
139
		 * Hook during initiation of CMB2 object
140
		 *
141
		 * The dynamic portion of the hook name, $this->cmb_id, is this meta_box id.
142
		 *
143
		 * @param array $cmb This CMB2 object
144
		 */
145 44
		do_action( "cmb2_init_{$this->cmb_id}", $this );
146 44
	}
147
148
	/**
149
	 * Loops through and displays fields
150
	 * @since 1.0.0
151
	 * @param int    $object_id   Object ID
152
	 * @param string $object_type Type of object being saved. (e.g., post, user, or comment)
153
	 */
154 1
	public function show_form( $object_id = 0, $object_type = '' ) {
155 1
		$this->render_form_open( $object_id, $object_type );
156
157 1
		foreach ( $this->prop( 'fields' ) as $field_args ) {
158 1
			$this->render_field( $field_args );
159 1
		}
160
161 1
		$this->render_form_close( $object_id, $object_type );
162 1
	}
163
164
	/**
165
	 * Outputs the opening form markup and runs corresponding hooks:
166
	 * 'cmb2_before_form' and "cmb2_before_{$object_type}_form_{$this->cmb_id}"
167
	 * @since  2.2.0
168
	 * @param  integer $object_id   Object ID
169
	 * @param  string  $object_type Object type
170
	 * @return void
171
	 */
172 1
	public function render_form_open( $object_id = 0, $object_type = '' ) {
173 1
		$object_type = $this->object_type( $object_type );
174 1
		$object_id = $this->object_id( $object_id );
175
176 1
		$this->nonce_field();
177
178 1
		echo "\n<!-- Begin CMB2 Fields -->\n";
179
180
		/**
181
		 * Hook before form table begins
182
		 *
183
		 * @param array  $cmb_id      The current box ID
184
		 * @param int    $object_id   The ID of the current object
185
		 * @param string $object_type The type of object you are working with.
186
		 *	                           Usually `post` (this applies to all post-types).
187
		 *	                           Could also be `comment`, `user` or `options-page`.
188
		 * @param array  $cmb         This CMB2 object
189
		 */
190 1
		do_action( 'cmb2_before_form', $this->cmb_id, $object_id, $object_type, $this );
191
192
		/**
193
		 * Hook before form table begins
194
		 *
195
		 * The first dynamic portion of the hook name, $object_type, is the type of object
196
		 * you are working with. Usually `post` (this applies to all post-types).
197
		 * Could also be `comment`, `user` or `options-page`.
198
		 *
199
		 * The second dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
200
		 *
201
		 * @param array  $cmb_id      The current box ID
202
		 * @param int    $object_id   The ID of the current object
203
		 * @param array  $cmb         This CMB2 object
204
		 */
205 1
		do_action( "cmb2_before_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
206
207 1
		echo '<div class="', $this->box_classes(), '"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
208
209 1
	}
210
211
	/**
212
	 * Defines the classes for the CMB2 form/wrap.
213
	 *
214
	 * @since  2.0.0
215
	 * @return string Space concatenated list of classes
216
	 */
217 1
	public function box_classes() {
218
219 1
		$classes = array( 'cmb2-wrap', 'form-table' );
220
221 1
		$cb = $this->prop( 'classes_cb' );
222
223
		// Use the callback to fetch classes.
224 1
		if ( is_callable( $cb ) && ( $added_classes = call_user_func( $cb, $this ) ) ) {
225 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
226 1
			$classes = array_merge( $classes, $added_classes );
227 1
		}
228
229 1
		if ( $added_classes = $this->prop( 'classes' ) ) {
230 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
231 1
			$classes = array_merge( $classes, $added_classes );
232 1
		}
233
234
		/**
235
		 * Globally filter box wrap classes
236
		 *
237
		 * @since 2.2.2
238
		 *
239
		 * @param string $classes Array of classes for the cmb2-wrap.
240
		 * @param CMB2   $cmb     This CMB2 object.
241
		 */
242 1
		$classes = apply_filters( 'cmb2_wrap_classes', $classes, $this );
243
244
		// Clean up.
245 1
		$classes = array_map( 'strip_tags', array_filter( $classes ) );
246
247
		// Make a string.
248 1
		return implode( ' ', $classes );
249
	}
250
251
	/**
252
	 * Outputs the closing form markup and runs corresponding hooks:
253
	 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
254
	 * @since  2.2.0
255
	 * @param  integer $object_id   Object ID
256
	 * @param  string  $object_type Object type
257
	 * @return void
258
	 */
259 1
	public function render_form_close( $object_id = 0, $object_type = '' ) {
260 1
		$object_type = $this->object_type( $object_type );
261 1
		$object_id = $this->object_id( $object_id );
262
263 1
		echo '</div></div>';
264
265 1
		$this->render_hidden_fields();
266
267
		/**
268
		 * Hook after form form has been rendered
269
		 *
270
		 * @param array  $cmb_id      The current box ID
271
		 * @param int    $object_id   The ID of the current object
272
		 * @param string $object_type The type of object you are working with.
273
		 *	                           Usually `post` (this applies to all post-types).
274
		 *	                           Could also be `comment`, `user` or `options-page`.
275
		 * @param array  $cmb         This CMB2 object
276
		 */
277 1
		do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
278
279
		/**
280
		 * Hook after form form has been rendered
281
		 *
282
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
283
		 *
284
		 * The first dynamic portion of the hook name, $object_type, is the type of object
285
		 * you are working with. Usually `post` (this applies to all post-types).
286
		 * Could also be `comment`, `user` or `options-page`.
287
		 *
288
		 * @param int    $object_id   The ID of the current object
289
		 * @param array  $cmb         This CMB2 object
290
		 */
291 1
		do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
292
293 1
		echo "\n<!-- End CMB2 Fields -->\n";
294
295 1
	}
296
297
	/**
298
	 * Renders a field based on the field type
299
	 * @since  2.2.0
300
	 * @param  array $field_args A field configuration array.
301
	 * @return mixed CMB2_Field object if successful.
302
	 */
303 1
	public function render_field( $field_args ) {
304 1
		$field_args['context'] = $this->prop( 'context' );
305
306 1
		if ( 'group' == $field_args['type'] ) {
307
308
			if ( ! isset( $field_args['show_names'] ) ) {
309
				$field_args['show_names'] = $this->prop( 'show_names' );
310
			}
311
			$field = $this->render_group( $field_args );
312
313 1
		} elseif ( 'hidden' == $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
314
			// Save rendering for after the metabox
315
			$field = $this->add_hidden_field( $field_args );
316
317
		} else {
318
319 1
			$field_args['show_names'] = $this->prop( 'show_names' );
320
321
			// Render default fields
322 1
			$field = $this->get_field( $field_args )->render_field();
323
		}
324
325 1
		return $field;
326
	}
327
328
	/**
329
	 * Render a repeatable group.
330
	 * @param array $args Array of field arguments for a group field parent.
331
	 * @return CMB2_Field|null Group field object.
332
	 */
333 2
	public function render_group( $args ) {
334
335 2
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
336
			return;
337
		}
338
339 2
		$field_group = $this->get_field( $args );
340
341
		// If field is requesting to be conditionally shown
342 2
		if ( ! $field_group || ! $field_group->should_show() ) {
343
			return;
344
		}
345
346 2
		$desc            = $field_group->args( 'description' );
347 2
		$label           = $field_group->args( 'name' );
348 2
		$sortable        = $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
349 2
		$repeat_class    = $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
350 2
		$group_val       = (array) $field_group->value();
351 2
		$nrows           = count( $group_val );
352 2
		$remove_disabled = $nrows <= 1 ? 'disabled="disabled" ' : '';
353 2
		$field_group->index = 0;
354
355 2
		$field_group->peform_param_callback( 'before_group' );
356
357 2
		echo '<div class="cmb-row cmb-repeat-group-wrap ', $field_group->row_classes(), '" data-fieldtype="group"><div class="cmb-td"><div id="', $field_group->id(), '_repeat" class="cmb-nested cmb-field-list cmb-repeatable-group', $sortable, $repeat_class, '" style="width:100%;">';
358
359 2
		if ( $desc || $label ) {
360 2
			$class = $desc ? ' cmb-group-description' : '';
361 2
			echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
362 2
				if ( $label ) {
363 2
					echo '<h2 class="cmb-group-name">', $label, '</h2>';
364 2
				}
365 2
				if ( $desc ) {
366 1
					echo '<p class="cmb2-metabox-description">', $desc, '</p>';
367 1
				}
368 2
			echo '</div></div>';
369 2
		}
370
371 2
		if ( ! empty( $group_val ) ) {
372
373
			foreach ( $group_val as $group_key => $field_id ) {
374
				$this->render_group_row( $field_group, $remove_disabled );
375
				$field_group->index++;
376
			}
377
		} else {
378 2
			$this->render_group_row( $field_group, $remove_disabled );
379
		}
380
381 2
		if ( $field_group->args( 'repeatable' ) ) {
382 1
			echo '<div class="cmb-row"><div class="cmb-td"><p class="cmb-add-row"><button type="button" data-selector="', $field_group->id(), '_repeat" data-grouptitle="', $field_group->options( 'group_title' ), '" class="cmb-add-group-row button">', $field_group->options( 'add_button' ), '</button></p></div></div>';
383 1
		}
384
385 2
		echo '</div></div></div>';
386
387 2
		$field_group->peform_param_callback( 'after_group' );
388
389 2
		return $field_group;
390
	}
391
392
	/**
393
	 * Render a repeatable group row
394
	 * @since  1.0.2
395
	 * @param  CMB2_Field $field_group  CMB2_Field group field object
396
	 * @param  string  $remove_disabled Attribute string to disable the remove button
397
	 */
398 2
	public function render_group_row( $field_group, $remove_disabled ) {
399
400 2
		$field_group->peform_param_callback( 'before_group_row' );
401 2
		$closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
402
403
		echo '
404 2
		<div class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
405
406 2
			if ( $field_group->args( 'repeatable' ) ) {
407 1
				echo '<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="dashicons-before dashicons-no-alt cmb-remove-group-row"></button>';
408 1
			}
409
410
			echo '
411 2
			<div class="cmbhandle" title="' , __( 'Click to toggle', 'cmb2' ), '"><br></div>
412 2
			<h3 class="cmb-group-title cmbhandle-title"><span>', $field_group->replace_hash( $field_group->options( 'group_title' ) ), '</span></h3>
413
414
			<div class="inside cmb-td cmb-nested cmb-field-list">';
415
				// Loop and render repeatable group fields
416 2
				foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
417 2
					if ( 'hidden' == $field_args['type'] ) {
418
419
						// Save rendering for after the metabox
420
						$this->add_hidden_field( $field_args, $field_group );
421
422
					} else {
423
424 2
						$field_args['show_names'] = $field_group->args( 'show_names' );
425 2
						$field_args['context']    = $field_group->args( 'context' );
426
427 2
						$field = $this->get_field( $field_args, $field_group )->render_field();
428
					}
429 2
				}
430 2
				if ( $field_group->args( 'repeatable' ) ) {
431
					echo '
432
					<div class="cmb-row cmb-remove-field-row">
433
						<div class="cmb-remove-row">
434 1
							<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="button cmb-remove-group-row alignright">', $field_group->options( 'remove_button' ), '</button>
435
						</div>
436
					</div>
437
					';
438 1
				}
439
			echo '
440
			</div>
441
		</div>
442 2
		';
443
444 2
		$field_group->peform_param_callback( 'after_group_row' );
445 2
	}
446
447
	/**
448
	 * Add a hidden field to the list of hidden fields to be rendered later
449
	 * @since 2.0.0
450
	 * @param array  $field_args Array of field arguments to be passed to CMB2_Field
451
	 */
452
	public function add_hidden_field( $field_args, $field_group = null ) {
453
		if ( isset( $field_args['field_args'] ) ) {
454
			// For back-compatibility.
455
			$field = new CMB2_Field( $field_args );
456
		} else {
457
			$field = $this->get_new_field( $field_args, $field_group );
458
		}
459
460
		$this->hidden_fields[] = new CMB2_Types( $field );
461
462
		return $field;
463
	}
464
465
	/**
466
	 * Loop through and output hidden fields
467
	 * @since  2.0.0
468
	 */
469 1
	public function render_hidden_fields() {
470 1
		if ( ! empty( $this->hidden_fields ) ) {
471
			foreach ( $this->hidden_fields as $hidden ) {
472
				$hidden->render();
473
			}
474
		}
475 1
	}
476
477
	/**
478
	 * Returns array of sanitized field values (without saving them)
479
	 * @since  2.0.3
480
	 * @param  array  $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
481
	 */
482 2
	public function get_sanitized_values( array $data_to_sanitize ) {
483 2
		$this->data_to_save = $data_to_sanitize;
484 2
		$stored_id          = $this->object_id();
485
486
		// We do this So CMB will sanitize our data for us, but not save it
487 2
		$this->object_id( '_' );
488
489
		// Ensure temp. data store is empty
490 2
		cmb2_options( 0 )->set();
491
492
		// Process/save fields
493 2
		$this->process_fields();
494
495
		// Get data from temp. data store
496 2
		$sanitized_values = cmb2_options( 0 )->get_options();
497
498
		// Empty out temp. data store again
499 2
		cmb2_options( 0 )->set();
500
501
		// Reset the object id
502 2
		$this->object_id( $stored_id );
503
504 2
		return $sanitized_values;
505
	}
506
507
	/**
508
	 * Loops through and saves field data
509
	 * @since  1.0.0
510
	 * @param  int    $object_id    Object ID
511
	 * @param  string $object_type  Type of object being saved. (e.g., post, user, or comment)
512
	 * @param  array  $data_to_save Array of key => value data for saving. Likely $_POST data.
513
	 */
514 1
	public function save_fields( $object_id = 0, $object_type = '', $data_to_save = array() ) {
515
516
		// Fall-back to $_POST data
517 1
		$this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
518 1
		$object_id = $this->object_id( $object_id );
519 1
		$object_type = $this->object_type( $object_type );
520
521 1
		$this->process_fields();
522
523
		// If options page, save the updated options
524 1
		if ( 'options-page' == $object_type ) {
525 1
			cmb2_options( $object_id )->set();
526 1
		}
527
528 1
		$this->after_save();
529 1
	}
530
531
	/**
532
	 * Process and save form fields
533
	 * @since  2.0.0
534
	 */
535 3
	public function process_fields() {
536
537 3
		$this->pre_process();
538
539
		// Remove the show_on properties so saving works
540 3
		$this->prop( 'show_on', array() );
541
542
		// save field ids of those that are updated
543 3
		$this->updated = array();
544
545 3
		foreach ( $this->prop( 'fields' ) as $field_args ) {
546 3
			$this->process_field( $field_args );
547 3
		}
548 3
	}
549
550
	/**
551
	 * Process and save a field
552
	 * @since  2.0.0
553
	 * @param  array  $field_args Array of field arguments
554
	 */
555 3
	public function process_field( $field_args ) {
556
557 3
		switch ( $field_args['type'] ) {
558
559 3
			case 'group':
560 1
				if ( $this->save_group( $field_args ) ) {
561 1
					$this->updated[] = $field_args['id'];
562 1
				}
563
564 1
				break;
565
566 2
			case 'title':
567
				// Don't process title fields
568
				break;
569
570 2
			default:
571
572 2
				$field = $this->get_new_field( $field_args );
573
574 2
				if ( $field->save_field_from_data( $this->data_to_save ) ) {
575 2
					$this->updated[] = $field->id();
576 2
				}
577
578 2
				break;
579 3
		}
580
581 3
	}
582
583 3
	public function pre_process() {
584
		/**
585
		 * Fires before fields have been processed/saved.
586
		 *
587
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
588
		 *
589
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
590
		 * 	Usually `post` (this applies to all post-types).
591
		 *  	Could also be `comment`, `user` or `options-page`.
592
		 *
593
		 * @param array $cmb       This CMB2 object
594
		 * @param int   $object_id The ID of the current object
595
		 */
596 3
		do_action( "cmb2_{$this->object_type()}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
597 3
	}
598
599 1
	public function after_save() {
600 1
		$object_type = $this->object_type();
601 1
		$object_id   = $this->object_id();
602
603
		/**
604
		 * Fires after all fields have been saved.
605
		 *
606
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
607
		 * 	Usually `post` (this applies to all post-types).
608
		 *  	Could also be `comment`, `user` or `options-page`.
609
		 *
610
		 * @param int    $object_id   The ID of the current object
611
		 * @param array  $cmb_id      The current box ID
612
		 * @param string $updated     Array of field ids that were updated.
613
		 *                            Will only include field ids that had values change.
614
		 * @param array  $cmb         This CMB2 object
615
		 */
616 1
		do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
617
618
		/**
619
		 * Fires after all fields have been saved.
620
		 *
621
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
622
		 *
623
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
624
		 * 	Usually `post` (this applies to all post-types).
625
		 *  	Could also be `comment`, `user` or `options-page`.
626
		 *
627
		 * @param int    $object_id   The ID of the current object
628
		 * @param string $updated     Array of field ids that were updated.
629
		 *                            Will only include field ids that had values change.
630
		 * @param array  $cmb         This CMB2 object
631
		 */
632 1
		do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
633 1
	}
634
635
	/**
636
	 * Save a repeatable group
637
	 * @since  1.x.x
638
	 * @param  array  $args Field arguments array
639
	 * @return mixed        Return of CMB2_Field::update_data()
640
	 */
641 1
	public function save_group( $args ) {
642 1
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
643
			return;
644
		}
645
646 1
		return $this->save_group_field( $this->get_new_field( $args ) );
647
	}
648
649
	/**
650
	 * Save a repeatable group
651
	 * @since  1.x.x
652
	 * @param  array $field_group CMB2_Field group field object
653
	 * @return mixed              Return of CMB2_Field::update_data()
654
	 */
655 1
	public function save_group_field( $field_group ) {
656 1
		$base_id = $field_group->id();
657
658 1
		if ( ! isset( $this->data_to_save[ $base_id ] ) ) {
659
			return;
660
		}
661
662 1
		$old        = $field_group->get_data();
663
		// Check if group field has sanitization_cb
664 1
		$group_vals = $field_group->sanitization_cb( $this->data_to_save[ $base_id ] );
665 1
		$saved      = array();
666
667 1
		$field_group->index = 0;
668 1
		$field_group->data_to_save = $this->data_to_save;
669
670 1
		foreach ( array_values( $field_group->fields() ) as $field_args ) {
671
672 1
			$field  = $this->get_new_field( $field_args, $field_group );
673 1
			$sub_id = $field->id( true );
674
675 1
			foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
676
677
				// Get value
678 1
				$new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
679 1
					? $group_vals[ $field_group->index ][ $sub_id ]
680 1
					: false;
681
682
				// Sanitize
683 1
				$new_val = $field->sanitization_cb( $new_val );
684
685 1
				if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
686 1
					if ( $field->args( 'repeatable' ) ) {
687 1
						$_new_val = array();
688 1
						foreach ( $new_val as $group_index => $grouped_data ) {
689
							// Add the supporting data to the $saved array stack
690 1
							$saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
691
							// Reset var to the actual value
692 1
							$_new_val[ $group_index ] = $grouped_data['value'];
693 1
						}
694 1
						$new_val = $_new_val;
695 1
					} else {
696
						// Add the supporting data to the $saved array stack
697 1
						$saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
698
						// Reset var to the actual value
699 1
						$new_val = $new_val['value'];
700
					}
701 1
				}
702
703
				// Get old value
704 1
				$old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
705 1
					? $old[ $field_group->index ][ $sub_id ]
706 1
					: false;
707
708 1
				$is_updated = ( ! empty( $new_val ) && $new_val != $old_val );
709 1
				$is_removed = ( empty( $new_val ) && ! empty( $old_val ) );
710
				// Compare values and add to `$updated` array
711 1
				if ( $is_updated || $is_removed ) {
712 1
					$this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
713 1
				}
714
715
				// Add to `$saved` array
716 1
				$saved[ $field_group->index ][ $sub_id ] = $new_val;
717
718 1
			}
719 1
			$saved[ $field_group->index ] = array_filter( $saved[ $field_group->index ] );
720 1
		}
721 1
		$saved = array_filter( $saved );
722
723 1
		return $field_group->update_data( $saved, true );
724
	}
725
726
	/**
727
	 * Get object id from global space if no id is provided
728
	 * @since  1.0.0
729
	 * @param  integer $object_id Object ID
730
	 * @return integer $object_id Object ID
731
	 */
732 48
	public function object_id( $object_id = 0 ) {
733 48
		global $pagenow;
734
735 48
		if ( $object_id ) {
736 18
			$this->object_id = $object_id;
737 18
			return $this->object_id;
738
		}
739
740 45
		if ( $this->object_id ) {
741 13
			return $this->object_id;
742
		}
743
744
		// Try to get our object ID from the global space
745 42
		switch ( $this->object_type() ) {
746 42
			case 'user':
747
				$object_id = isset( $_REQUEST['user_id'] ) ? $_REQUEST['user_id'] : $object_id;
748
				$object_id = ! $object_id && 'user-new.php' != $pagenow && isset( $GLOBALS['user_ID'] ) ? $GLOBALS['user_ID'] : $object_id;
749
				break;
750
751 42
			case 'comment':
752
				$object_id = isset( $_REQUEST['c'] ) ? $_REQUEST['c'] : $object_id;
753
				$object_id = ! $object_id && isset( $GLOBALS['comments']->comment_ID ) ? $GLOBALS['comments']->comment_ID : $object_id;
754
				break;
755
756 42
			case 'term':
757
				$object_id = isset( $_REQUEST['tag_ID'] ) ? $_REQUEST['tag_ID'] : $object_id;
758
				break;
759
760 42
			default:
761 42
				$object_id = isset( $GLOBALS['post']->ID ) ? $GLOBALS['post']->ID : $object_id;
762 42
				$object_id = isset( $_REQUEST['post'] ) ? $_REQUEST['post'] : $object_id;
763 42
				break;
764 42
		}
765
766
		// reset to id or 0
767 42
		$this->object_id = $object_id ? $object_id : 0;
768
769 42
		return $this->object_id;
770
	}
771
772
	/**
773
	 * Sets the $object_type based on metabox settings
774
	 * @since  1.0.0
775
	 * @return string Object type
776
	 */
777 44
	public function mb_object_type() {
778 44
		if ( null !== $this->mb_object_type ) {
779 12
			return $this->mb_object_type;
780
		}
781
782 44
		if ( $this->is_options_page_mb() ) {
783 36
			$this->mb_object_type = 'options-page';
784 36
			return $this->mb_object_type;
785
		}
786
787 43
		$registered_types = $this->prop( 'object_types' );
788
789 43
		if ( ! $registered_types ) {
790 40
			$this->mb_object_type = 'post';
791 40
			return $this->mb_object_type;
792
		}
793
794 4
		$type = false;
795
796
		// check if 'object_types' is a string
797 4
		if ( is_string( $registered_types ) ) {
798
			$type = $registered_types;
799
		}
800
801
		// if it's an array of one, extract it
802 4
		elseif ( is_array( $registered_types ) && 1 === count( $registered_types ) ) {
803 4
			$last = end( $registered_types );
804 4
			if ( is_string( $last ) ) {
805 4
				$type = $last;
806 4
			}
807 4
		} elseif ( is_array( $registered_types ) ) {
808
			$page_type = $this->current_object_type();
809
810
			if ( in_array( $page_type, $registered_types, true ) ) {
811
				$type = $page_type;
812
			}
813
		}
814
815
		// Get our object type
816
		switch ( $type ) {
817
818 4
			case 'user':
819 4
			case 'comment':
820 4
			case 'term':
821 1
				$this->mb_object_type = $type;
822 1
				break;
823
824 3
			default:
825 3
				$this->mb_object_type = 'post';
826 3
				break;
827 3
		}
828
829 4
		return $this->mb_object_type;
830
	}
831
832
	/**
833
	 * Determines if metabox is for an options page
834
	 * @since  1.0.1
835
	 * @return boolean True/False
836
	 */
837 44
	public function is_options_page_mb() {
838 44
		return ( isset( $this->meta_box['show_on']['key'] ) && 'options-page' === $this->meta_box['show_on']['key'] || array_key_exists( 'options-page', $this->meta_box['show_on'] ) );
839
	}
840
841
	/**
842
	 * Returns the object type
843
	 * @since  1.0.0
844
	 * @return string Object type
845
	 */
846 48
	public function object_type( $object_type = '' ) {
847 48
		if ( $object_type ) {
848 18
			$this->object_type = $object_type;
849 18
			return $this->object_type;
850 1
		}
851
852 45
		if ( $this->object_type ) {
853 45
			return $this->object_type;
854
		}
855
856
		$this->object_type = $this->current_object_type();
857
858
		return $this->object_type;
859
	}
860
861
	/**
862
	 * Get the object type for the current page, based on the $pagenow global.
863
	 * @since  2.2.2
864
	 * @return string  Page object type name.
865
	 */
866
	public function current_object_type() {
867
		global $pagenow;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
868
		$type = 'post';
869
870
		if ( in_array( $pagenow, array( 'user-edit.php', 'profile.php', 'user-new.php' ), true ) ) {
871
			$type = 'user';
872
		}
873
874
		if ( in_array( $pagenow, array( 'edit-comments.php', 'comment.php' ), true ) ) {
875
			$type = 'comment';
876
		}
877
878
		if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
879
			$type = 'term';
880
		}
881
882
		return $type;
883
	}
884
885
	/**
886
	 * Set metabox property.
887
	 * @since  2.2.0
888
	 * @param  string $property Metabox config property to retrieve
889
	 * @param  mixed  $value    Value to set if no value found
890
	 * @return mixed            Metabox config property value or false
891
	 */
892 1
	public function set_prop( $property, $value ) {
893 1
		$this->meta_box[ $property ] = $value;
894
895 1
		return $this->prop( $property );
896
	}
897
898
	/**
899
	 * Get metabox property and optionally set a fallback
900
	 * @since  2.0.0
901
	 * @param  string $property Metabox config property to retrieve
902
	 * @param  mixed  $fallback Fallback value to set if no value found
903
	 * @return mixed            Metabox config property value or false
904
	 */
905 44
	public function prop( $property, $fallback = null ) {
906 44
		if ( array_key_exists( $property, $this->meta_box ) ) {
907 44
			return $this->meta_box[ $property ];
908 1
		} elseif ( $fallback ) {
909 1
			return $this->meta_box[ $property ] = $fallback;
910
		}
911
	}
912
913
	/**
914
	 * Get a field object
915
	 * @since  2.0.3
916
	 * @param  string|array|CMB2_Field $field       Metabox field id or field config array or CMB2_Field object
917
	 * @param  CMB2_Field              $field_group (optional) CMB2_Field object (group parent)
918
	 * @return CMB2_Field|false CMB2_Field object (or false)
919
	 */
920 15
	public function get_field( $field, $field_group = null ) {
921 15
		if ( is_a( $field, 'CMB2_Field' ) ) {
922
			return $field;
923
		}
924
925 15
		$field_id = is_string( $field ) ? $field : $field['id'];
926
927 15
		$parent_field_id = ! empty( $field_group ) ? $field_group->id() : '';
928 15
		$ids = $this->get_field_ids( $field_id, $parent_field_id, true );
929
930 15
		if ( ! $ids ) {
931
			return false;
932
		}
933
934 15
		list( $field_id, $sub_field_id ) = $ids;
935
936 15
		$index = implode( '', $ids ) . ( $field_group ? $field_group->index : '' );
937 15
		if ( array_key_exists( $index, $this->fields ) ) {
938 3
			return $this->fields[ $index ];
939
		}
940
941 13
		$this->fields[ $index ] = new CMB2_Field( $this->get_field_args( $field_id, $field, $sub_field_id, $field_group ) );
942
943 13
		return $this->fields[ $index ];
944
	}
945
946
	/**
947
	 * Handles determining which type of arguments to pass to CMB2_Field
948
	 * @since  2.0.7
949
	 * @param  mixed  $field_id     Field (or group field) ID
950
	 * @param  mixed  $field_args   Array of field arguments
951
	 * @param  mixed  $sub_field_id Sub field ID (if field_group exists)
952
	 * @param  mixed  $field_group  If a sub-field, will be the parent group CMB2_Field object
953
	 * @return array                Array of CMB2_Field arguments
954
	 */
955 13
	public function get_field_args( $field_id, $field_args, $sub_field_id, $field_group ) {
956
957
		// Check if group is passed and if fields were added in the old-school fields array
958 13
		if ( $field_group && ( $sub_field_id || 0 === $sub_field_id ) ) {
959
960
			// Update the fields array w/ any modified properties inherited from the group field
961 2
			$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] = $field_args;
962
963 2
			return $this->get_default_args( $field_args, $field_group );
964
		}
965
966 13
		if ( is_array( $field_args ) ) {
967 2
			$this->meta_box['fields'][ $field_id ] = array_merge( $field_args, $this->meta_box['fields'][ $field_id ] );
968 2
		}
969
970 13
		return $this->get_default_args( $this->meta_box['fields'][ $field_id ] );
971
	}
972
973
	/**
974
	 * Get default field arguments specific to this CMB2 object.
975
	 * @since  2.2.0
976
	 * @param  array      $field_args  Metabox field config array.
977
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
978
	 * @return array                   Array of field arguments.
979
	 */
980 17
	protected function get_default_args( $field_args, $field_group = null ) {
981 17
		if ( $field_group ) {
982
			$args = array(
983 3
				'field_args'  => $field_args,
984 3
				'group_field' => $field_group,
985 3
			);
986 3
		} else {
987
			$args = array(
988 17
				'field_args'  => $field_args,
989 17
				'object_type' => $this->object_type(),
990 17
				'object_id'   => $this->object_id(),
991 17
				'cmb_id'      => $this->cmb_id,
992 17
			);
993
		}
994
995 17
		return $args;
996
	}
997
998
	/**
999
	 * Get a new field object specific to this CMB2 object.
1000
	 * @since  2.2.0
1001
	 * @param  array      $field_args  Metabox field config array.
1002
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
1003
	 * @return CMB2_Field CMB2_Field object
1004
	 */
1005 5
	protected function get_new_field( $field_args, $field_group = null ) {
1006 5
		return new CMB2_Field( $this->get_default_args( $field_args, $field_group ) );
1007
	}
1008
1009
	/**
1010
	 * When fields are added in the old-school way, intitate them as they should be
1011
	 * @since 2.1.0
1012
	 * @param array $fields          Array of fields to add
1013
	 * @param mixed $parent_field_id Parent field id or null
1014
	 */
1015 41
	protected function add_fields( $fields, $parent_field_id = null ) {
1016 41
		foreach ( $fields as $field ) {
1017
1018 41
			$sub_fields = false;
1019 41
			if ( array_key_exists( 'fields', $field ) ) {
1020
				$sub_fields = $field['fields'];
1021
				unset( $field['fields'] );
1022
			}
1023
1024
			$field_id = $parent_field_id
1025 41
				? $this->add_group_field( $parent_field_id, $field )
1026 41
				: $this->add_field( $field );
1027
1028 41
			if ( $sub_fields ) {
1029
				$this->add_fields( $sub_fields, $field_id );
1030
			}
1031 41
		}
1032 41
	}
1033
1034
	/**
1035
	 * Add a field to the metabox
1036
	 * @since  2.0.0
1037
	 * @param  array  $field           Metabox field config array
1038
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1039
	 * @return mixed                   Field id or false
1040
	 */
1041 43
	public function add_field( array $field, $position = 0 ) {
1042 43
		if ( ! is_array( $field ) || ! array_key_exists( 'id', $field ) ) {
1043
			return false;
1044
		}
1045
1046 43
		if ( 'oembed' === $field['type'] ) {
1047
			// Initiate oembed Ajax hooks
1048 1
			cmb2_ajax();
1049 1
		}
1050
1051 43
		$this->_add_field_to_array(
1052 43
			$field,
1053 43
			$this->meta_box['fields'],
1054
			$position
1055 43
		);
1056
1057 43
		return $field['id'];
1058
	}
1059
1060
	/**
1061
	 * Add a field to a group
1062
	 * @since  2.0.0
1063
	 * @param  string $parent_field_id The field id of the group field to add the field
1064
	 * @param  array  $field           Metabox field config array
1065
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1066
	 * @return mixed                   Array of parent/field ids or false
1067
	 */
1068 3
	public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
1069 3
		if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
1070
			return false;
1071
		}
1072
1073 3
		$parent_field = $this->meta_box['fields'][ $parent_field_id ];
1074
1075 3
		if ( 'group' !== $parent_field['type'] ) {
1076
			return false;
1077
		}
1078
1079 3
		if ( ! isset( $parent_field['fields'] ) ) {
1080 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
1081 3
		}
1082
1083 3
		$this->_add_field_to_array(
1084 3
			$field,
1085 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'],
1086
			$position
1087 3
		);
1088
1089 3
		return array( $parent_field_id, $field['id'] );
1090
	}
1091
1092
	/**
1093
	 * Add a field array to a fields array in desired position
1094
	 * @since 2.0.2
1095
	 * @param array   $field    Metabox field config array
1096
	 * @param array   &$fields  Array (passed by reference) to append the field (array) to
1097
	 * @param integer $position Optionally specify a position in the array to be inserted
1098
	 */
1099 43
	protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1100 43
		if ( $position ) {
1101 1
			cmb2_utils()->array_insert( $fields, array( $field['id'] => $field ), $position );
1102 1
		} else {
1103 43
			$fields[ $field['id'] ] = $field;
1104
		}
1105 43
	}
1106
1107
	/**
1108
	 * Remove a field from the metabox
1109
	 * @since 2.0.0
1110
	 * @param  string $field_id        The field id of the field to remove
1111
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1112
	 * @return bool                    True if field was removed
1113
	 */
1114 2
	public function remove_field( $field_id, $parent_field_id = '' ) {
1115 2
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1116
1117 2
		if ( ! $ids ) {
1118
			return false;
1119
		}
1120
1121 2
		list( $field_id, $sub_field_id ) = $ids;
1122
1123 2
		unset( $this->fields[ implode( '', $ids ) ] );
1124
1125 2
		if ( ! $sub_field_id ) {
1126 1
			unset( $this->meta_box['fields'][ $field_id ] );
1127 1
			return true;
1128
		}
1129
1130 1
		if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1131 1
			unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1132 1
		}
1133 1
		if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1134 1
			unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1135 1
		}
1136 1
		return true;
1137
	}
1138
1139
	/**
1140
	 * Update or add a property to a field
1141
	 * @since  2.0.0
1142
	 * @param  string $field_id        Field id
1143
	 * @param  string $property        Field property to set/update
1144
	 * @param  mixed  $value           Value to set the field property
1145
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1146
	 * @return mixed                   Field id. Strict compare to false, as success can return a falsey value (like 0)
1147
	 */
1148 4
	public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1149 4
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1150
1151 4
		if ( ! $ids ) {
1152 2
			return false;
1153
		}
1154
1155 2
		list( $field_id, $sub_field_id ) = $ids;
1156
1157 2
		if ( ! $sub_field_id ) {
1158 2
			$this->meta_box['fields'][ $field_id ][ $property ] = $value;
1159 2
			return $field_id;
1160
		}
1161
1162
		$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1163
		return $field_id;
1164
	}
1165
1166
	/**
1167
	 * Check if field ids match a field and return the index/field id
1168
	 * @since  2.0.2
1169
	 * @param  string  $field_id        Field id
1170
	 * @param  string  $parent_field_id (optional) Parent field id
1171
	 * @return mixed                    Array of field/parent ids, or false
1172
	 */
1173 19
	public function get_field_ids( $field_id, $parent_field_id = '' ) {
1174 19
		$sub_field_id = $parent_field_id ? $field_id : '';
1175 19
		$field_id     = $parent_field_id ? $parent_field_id : $field_id;
1176 19
		$fields       =& $this->meta_box['fields'];
1177
1178 19
		if ( ! array_key_exists( $field_id, $fields ) ) {
1179 2
			$field_id = $this->search_old_school_array( $field_id, $fields );
1180 2
		}
1181
1182 19
		if ( false === $field_id ) {
1183 2
			return false;
1184
		}
1185
1186 17
		if ( ! $sub_field_id ) {
1187 17
			return array( $field_id, $sub_field_id );
1188
		}
1189
1190 3
		if ( 'group' !== $fields[ $field_id ]['type'] ) {
1191
			return false;
1192
		}
1193
1194 3
		if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1195
			$sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1196
		}
1197
1198 3
		return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1199
	}
1200
1201
	/**
1202
	 * When using the old array filter, it is unlikely field array indexes will be the field id
1203
	 * @since  2.0.2
1204
	 * @param  string $field_id The field id
1205
	 * @param  array  $fields   Array of fields to search
1206
	 * @return mixed            Field index or false
1207
	 */
1208 2
	public function search_old_school_array( $field_id, $fields ) {
1209 2
		$ids = wp_list_pluck( $fields, 'id' );
1210 2
		$index = array_search( $field_id, $ids );
1211 2
		return false !== $index ? $index : false;
1212
	}
1213
1214
	/**
1215
	 * Determine whether this cmb object should show, based on the 'show_on_cb' callback.
1216
	 *
1217
	 * @since 2.0.9
1218
	 *
1219
	 * @return bool Whether this cmb should be shown.
1220
	 */
1221 View Code Duplication
	public function should_show() {
1222
		// Default to showing this cmb
1223
		$show = true;
1224
1225
		// Use the callback to determine showing the cmb, if it exists
1226
		if ( is_callable( $this->prop( 'show_on_cb' ) ) ) {
1227
			$show = (bool) call_user_func( $this->prop( 'show_on_cb' ), $this );
1228
		}
1229
1230
		return $show;
1231
	}
1232
1233
	/**
1234
	 * Generate a unique nonce field for each registered meta_box
1235
	 * @since  2.0.0
1236
	 * @return string unique nonce hidden input
1237
	 */
1238 1
	public function nonce_field() {
1239 1
		wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1240 1
	}
1241
1242
	/**
1243
	 * Generate a unique nonce for each registered meta_box
1244
	 * @since  2.0.0
1245
	 * @return string unique nonce string
1246
	 */
1247 1
	public function nonce() {
1248 1
		if ( $this->generated_nonce ) {
1249 1
			return $this->generated_nonce;
1250
		}
1251 1
		$this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1252 1
		return $this->generated_nonce;
1253
	}
1254
1255
	/**
1256
	 * Magic getter for our object.
1257
	 * @param string $field
1258
	 * @throws Exception Throws an exception if the field is invalid.
1259
	 * @return mixed
1260
	 */
1261 44
	public function __get( $field ) {
1262
		switch ( $field ) {
1263 44
			case 'cmb_id':
1264 44
			case 'meta_box':
1265 44
			case 'updated':
1266 44
				return $this->{$field};
1267 3
			case 'object_id':
1268 1
				return $this->object_id();
1269 2
			default:
1270 2
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
1271 2
		}
1272
	}
1273
1274
}
1275