Completed
Push — trunk ( df6be1...704260 )
by Justin
05:04
created

includes/CMB2.php (2 issues)

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 View Code Duplication
	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="cmb2-wrap form-table"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
208
209 1
	}
210
211
	/**
212
	 * Outputs the closing form markup and runs corresponding hooks:
213
	 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
214
	 * @since  2.2.0
215
	 * @param  integer $object_id   Object ID
216
	 * @param  string  $object_type Object type
217
	 * @return void
218
	 */
219 1 View Code Duplication
	public function render_form_close( $object_id = 0, $object_type = '' ) {
220 1
		$object_type = $this->object_type( $object_type );
221 1
		$object_id = $this->object_id( $object_id );
222
223 1
		echo '</div></div>';
224
225 1
		$this->render_hidden_fields();
226
227
		/**
228
		 * Hook after form form has been rendered
229
		 *
230
		 * @param array  $cmb_id      The current box ID
231
		 * @param int    $object_id   The ID of the current object
232
		 * @param string $object_type The type of object you are working with.
233
		 *	                           Usually `post` (this applies to all post-types).
234
		 *	                           Could also be `comment`, `user` or `options-page`.
235
		 * @param array  $cmb         This CMB2 object
236
		 */
237 1
		do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
238
239
		/**
240
		 * Hook after form form has been rendered
241
		 *
242
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
243
		 *
244
		 * The first dynamic portion of the hook name, $object_type, is the type of object
245
		 * you are working with. Usually `post` (this applies to all post-types).
246
		 * Could also be `comment`, `user` or `options-page`.
247
		 *
248
		 * @param int    $object_id   The ID of the current object
249
		 * @param array  $cmb         This CMB2 object
250
		 */
251 1
		do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
252
253 1
		echo "\n<!-- End CMB2 Fields -->\n";
254
255 1
	}
256
257
	/**
258
	 * Renders a field based on the field type
259
	 * @since  2.2.0
260
	 * @param  array $field_args A field configuration array.
261
	 * @return mixed CMB2_Field object if successful.
262
	 */
263 1
	public function render_field( $field_args ) {
264 1
		$field_args['context'] = $this->prop( 'context' );
265
266 1
		if ( 'group' == $field_args['type'] ) {
267
268
			if ( ! isset( $field_args['show_names'] ) ) {
269
				$field_args['show_names'] = $this->prop( 'show_names' );
270
			}
271
			$field = $this->render_group( $field_args );
272
273 1
		} elseif ( 'hidden' == $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
274
			// Save rendering for after the metabox
275
			$field = $this->add_hidden_field( $field_args );
276
277
		} else {
278
279 1
			$field_args['show_names'] = $this->prop( 'show_names' );
280
281
			// Render default fields
282 1
			$field = $this->get_field( $field_args )->render_field();
283
		}
284
285 1
		return $field;
286
	}
287
288
	/**
289
	 * Render a repeatable group.
290
	 * @param array $args Array of field arguments for a group field parent.
291
	 * @return CMB2_Field|null Group field object.
292
	 */
293 2
	public function render_group( $args ) {
294
295 2 View Code Duplication
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
296
			return;
297
		}
298
299 2
		$field_group = $this->get_field( $args );
300
301
		// If field is requesting to be conditionally shown
302 2
		if ( ! $field_group || ! $field_group->should_show() ) {
303
			return;
304
		}
305
306 2
		$desc            = $field_group->args( 'description' );
307 2
		$label           = $field_group->args( 'name' );
308 2
		$sortable        = $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
309 2
		$repeat_class    = $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
310 2
		$group_val       = (array) $field_group->value();
311 2
		$nrows           = count( $group_val );
312 2
		$remove_disabled = $nrows <= 1 ? 'disabled="disabled" ' : '';
313 2
		$field_group->index = 0;
314
315 2
		$field_group->peform_param_callback( 'before_group' );
316
317 2
		echo '<div class="cmb-row cmb-repeat-group-wrap ', $field_group->row_classes(), '"><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%;">';
318
319 2
		if ( $desc || $label ) {
320 2
			$class = $desc ? ' cmb-group-description' : '';
321 2
			echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
322 2
				if ( $label ) {
323 2
					echo '<h2 class="cmb-group-name">', $label, '</h2>';
324 2
				}
325 2
				if ( $desc ) {
326 1
					echo '<p class="cmb2-metabox-description">', $desc, '</p>';
327 1
				}
328 2
			echo '</div></div>';
329 2
		}
330
331 2
		if ( ! empty( $group_val ) ) {
332
333
			foreach ( $group_val as $group_key => $field_id ) {
334
				$this->render_group_row( $field_group, $remove_disabled );
335
				$field_group->index++;
336
			}
337
		} else {
338 2
			$this->render_group_row( $field_group, $remove_disabled );
339
		}
340
341 2
		if ( $field_group->args( 'repeatable' ) ) {
342 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>';
343 1
		}
344
345 2
		echo '</div></div></div>';
346
347 2
		$field_group->peform_param_callback( 'after_group' );
348
349 2
		return $field_group;
350
	}
351
352
	/**
353
	 * Render a repeatable group row
354
	 * @since  1.0.2
355
	 * @param  CMB2_Field $field_group  CMB2_Field group field object
356
	 * @param  string  $remove_disabled Attribute string to disable the remove button
357
	 */
358 2
	public function render_group_row( $field_group, $remove_disabled ) {
359
360 2
		$field_group->peform_param_callback( 'before_group_row' );
361 2
		$closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
362
363
		echo '
364 2
		<div class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
365
366 2
			if ( $field_group->args( 'repeatable' ) ) {
367 1
				echo '<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="dashicons-before dashicons-no-alt cmb-remove-group-row"></button>';
368 1
			}
369
370
			echo '
371 2
			<div class="cmbhandle" title="' , __( 'Click to toggle', 'cmb2' ), '"><br></div>
372 2
			<h3 class="cmb-group-title cmbhandle-title"><span>', $field_group->replace_hash( $field_group->options( 'group_title' ) ), '</span></h3>
373
374
			<div class="inside cmb-td cmb-nested cmb-field-list">';
375
				// Loop and render repeatable group fields
376 2
				foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
377 2
					if ( 'hidden' == $field_args['type'] ) {
378
379
						// Save rendering for after the metabox
380
						$this->add_hidden_field( $field_args, $field_group );
381
382
					} else {
383
384 2
						$field_args['show_names'] = $field_group->args( 'show_names' );
385 2
						$field_args['context']    = $field_group->args( 'context' );
386
387 2
						$field = $this->get_field( $field_args, $field_group )->render_field();
388
					}
389 2
				}
390 2
				if ( $field_group->args( 'repeatable' ) ) {
391
					echo '
392
					<div class="cmb-row cmb-remove-field-row">
393
						<div class="cmb-remove-row">
394 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>
395
						</div>
396
					</div>
397
					';
398 1
				}
399
			echo '
400
			</div>
401
		</div>
402 2
		';
403
404 2
		$field_group->peform_param_callback( 'after_group_row' );
405 2
	}
406
407
	/**
408
	 * Add a hidden field to the list of hidden fields to be rendered later
409
	 * @since 2.0.0
410
	 * @param array  $field_args Array of field arguments to be passed to CMB2_Field
411
	 */
412
	public function add_hidden_field( $field_args, $field_group = null ) {
413
		if ( isset( $field_args['field_args'] ) ) {
414
			// For back-compatibility.
415
			$field = new CMB2_Field( $field_args );
416
		} else {
417
			$field = $this->get_new_field( $field_args, $field_group );
418
		}
419
420
		$this->hidden_fields[] = new CMB2_Types( $field );
421
422
		return $field;
423
	}
424
425
	/**
426
	 * Loop through and output hidden fields
427
	 * @since  2.0.0
428
	 */
429 1
	public function render_hidden_fields() {
430 1
		if ( ! empty( $this->hidden_fields ) ) {
431
			foreach ( $this->hidden_fields as $hidden ) {
432
				$hidden->render();
433
			}
434
		}
435 1
	}
436
437
	/**
438
	 * Returns array of sanitized field values (without saving them)
439
	 * @since  2.0.3
440
	 * @param  array  $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
441
	 */
442 2
	public function get_sanitized_values( array $data_to_sanitize ) {
443 2
		$this->data_to_save = $data_to_sanitize;
444 2
		$stored_id          = $this->object_id();
445
446
		// We do this So CMB will sanitize our data for us, but not save it
447 2
		$this->object_id( '_' );
448
449
		// Ensure temp. data store is empty
450 2
		cmb2_options( 0 )->set();
451
452
		// Process/save fields
453 2
		$this->process_fields();
454
455
		// Get data from temp. data store
456 2
		$sanitized_values = cmb2_options( 0 )->get_options();
457
458
		// Empty out temp. data store again
459 2
		cmb2_options( 0 )->set();
460
461
		// Reset the object id
462 2
		$this->object_id( $stored_id );
463
464 2
		return $sanitized_values;
465
	}
466
467
	/**
468
	 * Loops through and saves field data
469
	 * @since  1.0.0
470
	 * @param  int    $object_id    Object ID
471
	 * @param  string $object_type  Type of object being saved. (e.g., post, user, or comment)
472
	 * @param  array  $data_to_save Array of key => value data for saving. Likely $_POST data.
473
	 */
474 1
	public function save_fields( $object_id = 0, $object_type = '', $data_to_save = array() ) {
475
476
		// Fall-back to $_POST data
477 1
		$this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
478 1
		$object_id = $this->object_id( $object_id );
479 1
		$object_type = $this->object_type( $object_type );
480
481 1
		$this->process_fields();
482
483
		// If options page, save the updated options
484 1
		if ( 'options-page' == $object_type ) {
485 1
			cmb2_options( $object_id )->set();
486 1
		}
487
488
		/**
489
		 * Fires after all fields have been saved.
490
		 *
491
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
492
		 * 	Usually `post` (this applies to all post-types).
493
		 *  	Could also be `comment`, `user` or `options-page`.
494
		 *
495
		 * @param int    $object_id   The ID of the current object
496
		 * @param array  $cmb_id      The current box ID
497
		 * @param string $updated     Array of field ids that were updated.
498
		 *                            Will only include field ids that had values change.
499
		 * @param array  $cmb         This CMB2 object
500
		 */
501 1
		do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
502
503
		/**
504
		 * Fires after all fields have been saved.
505
		 *
506
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
507
		 *
508
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
509
		 * 	Usually `post` (this applies to all post-types).
510
		 *  	Could also be `comment`, `user` or `options-page`.
511
		 *
512
		 * @param int    $object_id   The ID of the current object
513
		 * @param string $updated     Array of field ids that were updated.
514
		 *                            Will only include field ids that had values change.
515
		 * @param array  $cmb         This CMB2 object
516
		 */
517 1
		do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
518
519 1
	}
520
521
	/**
522
	 * Process and save form fields
523
	 * @since  2.0.0
524
	 */
525 3
	public function process_fields() {
526
527
		/**
528
		 * Fires before fields have been processed/saved.
529
		 *
530
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
531
		 *
532
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
533
		 * 	Usually `post` (this applies to all post-types).
534
		 *  	Could also be `comment`, `user` or `options-page`.
535
		 *
536
		 * @param array $cmb       This CMB2 object
537
		 * @param int   $object_id The ID of the current object
538
		 */
539 3
		do_action( "cmb2_{$this->object_type()}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
540
541
		// Remove the show_on properties so saving works
542 3
		$this->prop( 'show_on', array() );
543
544
		// save field ids of those that are updated
545 3
		$this->updated = array();
546
547 3
		foreach ( $this->prop( 'fields' ) as $field_args ) {
548 3
			$this->process_field( $field_args );
549 3
		}
550 3
	}
551
552
	/**
553
	 * Process and save a field
554
	 * @since  2.0.0
555
	 * @param  array  $field_args Array of field arguments
556
	 */
557 3
	public function process_field( $field_args ) {
558
559 3
		switch ( $field_args['type'] ) {
560
561 3
			case 'group':
562 1
				$this->save_group( $field_args );
563 1
				break;
564
565 2
			case 'title':
566
				// Don't process title fields
567
				break;
568
569 2
			default:
570
571 2
				$field = $this->get_new_field( $field_args );
572
573 2
				if ( $field->save_field_from_data( $this->data_to_save ) ) {
574 2
					$this->updated[] = $field->id();
575 2
				}
576
577 2
				break;
578 3
		}
579
580 3
	}
581
582
	/**
583
	 * Save a repeatable group
584
	 */
585 1
	public function save_group( $args ) {
586
587 1 View Code Duplication
		if ( ! isset( $args['id'], $args['fields'], $this->data_to_save[ $args['id'] ] ) || ! is_array( $args['fields'] ) ) {
588
			return;
589
		}
590
591 1
		$field_group        = $this->get_new_field( $args );
592 1
		$base_id            = $field_group->id();
593 1
		$old                = $field_group->get_data();
594
		// Check if group field has sanitization_cb
595 1
		$group_vals         = $field_group->sanitization_cb( $this->data_to_save[ $base_id ] );
596 1
		$saved              = array();
597 1
		$field_group->index = 0;
598 1
		$field_group->data_to_save = $this->data_to_save;
599
600 1
		foreach ( array_values( $field_group->fields() ) as $field_args ) {
601
602 1
			$field  = $this->get_new_field( $field_args, $field_group );
603 1
			$sub_id = $field->id( true );
604
605 1
			foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
606
607
				// Get value
608 1
				$new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
609 1
					? $group_vals[ $field_group->index ][ $sub_id ]
610 1
					: false;
611
612
				// Sanitize
613 1
				$new_val = $field->sanitization_cb( $new_val );
614
615 1
				if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
616 1
					if ( $field->args( 'repeatable' ) ) {
617 1
						$_new_val = array();
618 1
						foreach ( $new_val as $group_index => $grouped_data ) {
619
							// Add the supporting data to the $saved array stack
620 1
							$saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
621
							// Reset var to the actual value
622 1
							$_new_val[ $group_index ] = $grouped_data['value'];
623 1
						}
624 1
						$new_val = $_new_val;
625 1
					} else {
626
						// Add the supporting data to the $saved array stack
627 1
						$saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
628
						// Reset var to the actual value
629 1
						$new_val = $new_val['value'];
630
					}
631 1
				}
632
633
				// Get old value
634 1
				$old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
635 1
					? $old[ $field_group->index ][ $sub_id ]
636 1
					: false;
637
638 1
				$is_updated = ( ! empty( $new_val ) && $new_val != $old_val );
639 1
				$is_removed = ( empty( $new_val ) && ! empty( $old_val ) );
640
				// Compare values and add to `$updated` array
641 1
				if ( $is_updated || $is_removed ) {
642 1
					$this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
643 1
				}
644
645
				// Add to `$saved` array
646 1
				$saved[ $field_group->index ][ $sub_id ] = $new_val;
647
648 1
			}
649 1
			$saved[ $field_group->index ] = array_filter( $saved[ $field_group->index ] );
650 1
		}
651 1
		$saved = array_filter( $saved );
652
653 1
		$field_group->update_data( $saved, true );
654 1
	}
655
656
	/**
657
	 * Get object id from global space if no id is provided
658
	 * @since  1.0.0
659
	 * @param  integer $object_id Object ID
660
	 * @return integer $object_id Object ID
661
	 */
662 48
	public function object_id( $object_id = 0 ) {
663 48
		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...
664
665 48
		if ( $object_id ) {
666 18
			$this->object_id = $object_id;
667 18
			return $this->object_id;
668
		}
669
670 45
		if ( $this->object_id ) {
671 13
			return $this->object_id;
672
		}
673
674
		// Try to get our object ID from the global space
675 42
		switch ( $this->object_type() ) {
676 42
			case 'user':
677
				$object_id = isset( $_REQUEST['user_id'] ) ? $_REQUEST['user_id'] : $object_id;
678
				$object_id = ! $object_id && 'user-new.php' != $pagenow && isset( $GLOBALS['user_ID'] ) ? $GLOBALS['user_ID'] : $object_id;
679
				break;
680
681 42
			case 'comment':
682
				$object_id = isset( $_REQUEST['c'] ) ? $_REQUEST['c'] : $object_id;
683
				$object_id = ! $object_id && isset( $GLOBALS['comments']->comment_ID ) ? $GLOBALS['comments']->comment_ID : $object_id;
684
				break;
685
686 42
			case 'term':
687
				$object_id = isset( $_REQUEST['tag_ID'] ) ? $_REQUEST['tag_ID'] : $object_id;
688
				break;
689
690 42
			default:
691 42
				$object_id = isset( $GLOBALS['post']->ID ) ? $GLOBALS['post']->ID : $object_id;
692 42
				$object_id = isset( $_REQUEST['post'] ) ? $_REQUEST['post'] : $object_id;
693 42
				break;
694 42
		}
695
696
		// reset to id or 0
697 42
		$this->object_id = $object_id ? $object_id : 0;
698
699 42
		return $this->object_id;
700
	}
701
702
	/**
703
	 * Sets the $object_type based on metabox settings
704
	 * @since  1.0.0
705
	 * @return string Object type
706
	 */
707 44
	public function mb_object_type() {
708
709 44
		if ( null !== $this->mb_object_type ) {
710 12
			return $this->mb_object_type;
711
		}
712
713 44
		if ( $this->is_options_page_mb() ) {
714 36
			$this->mb_object_type = 'options-page';
715 36
			return $this->mb_object_type;
716
		}
717
718 43
		if ( ! $this->prop( 'object_types' ) ) {
719 40
			$this->mb_object_type = 'post';
720 40
			return $this->mb_object_type;
721
		}
722
723 4
		$type = false;
724
		// check if 'object_types' is a string
725 4
		if ( is_string( $this->prop( 'object_types' ) ) ) {
726
			$type = $this->prop( 'object_types' );
727
		}
728
		// if it's an array of one, extract it
729 4
		elseif ( is_array( $this->prop( 'object_types' ) ) && 1 === count( $this->prop( 'object_types' ) ) ) {
730 4
			$cpts = $this->prop( 'object_types' );
731 4
			$type = is_string( end( $cpts ) )
732 4
				? end( $cpts )
733 4
				: false;
734 4
		}
735
736 4
		if ( ! $type ) {
737
			$this->mb_object_type = 'post';
738
			return $this->mb_object_type;
739
		}
740
741
		// Get our object type
742
		switch ( $type ) {
743
744 4
			case 'user':
745 4
			case 'comment':
746 4
			case 'term':
747 1
				$this->mb_object_type = $type;
748 1
				break;
749
750 3
			default:
751 3
				$this->mb_object_type = 'post';
752 3
				break;
753 3
		}
754
755 4
		return $this->mb_object_type;
756
	}
757
758
	/**
759
	 * Determines if metabox is for an options page
760
	 * @since  1.0.1
761
	 * @return boolean True/False
762
	 */
763 44
	public function is_options_page_mb() {
764 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'] ) );
765
	}
766
767
	/**
768
	 * Returns the object type
769
	 * @since  1.0.0
770
	 * @return string Object type
771
	 */
772 48
	public function object_type( $object_type = '' ) {
773 48
		if ( $object_type ) {
774 18
			$this->object_type = $object_type;
775 18
			return $this->object_type;
776
		}
777
778 45
		if ( $this->object_type ) {
779 45
			return $this->object_type;
780
		}
781
782
		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...
783
784
		if ( in_array( $pagenow, array( 'user-edit.php', 'profile.php', 'user-new.php' ), true ) ) {
785
			$this->object_type = 'user';
786
787
		} elseif ( in_array( $pagenow, array( 'edit-comments.php', 'comment.php' ), true ) ) {
788
			$this->object_type = 'comment';
789
790
		} elseif ( 'edit-tags.php' == $pagenow ) {
791
			$this->object_type = 'term';
792
793
		} else {
794
			$this->object_type = 'post';
795
		}
796
797
		return $this->object_type;
798
	}
799
800
	/**
801
	 * Get metabox property and optionally set a fallback
802
	 * @since  2.0.0
803
	 * @param  string $property Metabox config property to retrieve
804
	 * @param  mixed  $fallback Fallback value to set if no value found
805
	 * @return mixed            Metabox config property value or false
806
	 */
807 44
	public function prop( $property, $fallback = null ) {
808 44
		if ( array_key_exists( $property, $this->meta_box ) ) {
809 44
			return $this->meta_box[ $property ];
810 1
		} elseif ( $fallback ) {
811 1
			return $this->meta_box[ $property ] = $fallback;
812
		}
813
	}
814
815
	/**
816
	 * Get a field object
817
	 * @since  2.0.3
818
	 * @param  string|array|CMB2_Field $field       Metabox field id or field config array or CMB2_Field object
819
	 * @param  CMB2_Field              $field_group (optional) CMB2_Field object (group parent)
820
	 * @return CMB2_Field|false CMB2_Field object (or false)
821
	 */
822 15
	public function get_field( $field, $field_group = null ) {
823 15
		if ( is_a( $field, 'CMB2_Field' ) ) {
824
			return $field;
825
		}
826
827 15
		$field_id = is_string( $field ) ? $field : $field['id'];
828
829 15
		$parent_field_id = ! empty( $field_group ) ? $field_group->id() : '';
830 15
		$ids = $this->get_field_ids( $field_id, $parent_field_id, true );
831
832 15
		if ( ! $ids ) {
833
			return false;
834
		}
835
836 15
		list( $field_id, $sub_field_id ) = $ids;
837
838 15
		$index = implode( '', $ids ) . ( $field_group ? $field_group->index : '' );
839 15
		if ( array_key_exists( $index, $this->fields ) ) {
840 3
			return $this->fields[ $index ];
841
		}
842
843 13
		$this->fields[ $index ] = new CMB2_Field( $this->get_field_args( $field_id, $field, $sub_field_id, $field_group ) );
844
845 13
		return $this->fields[ $index ];
846
	}
847
848
	/**
849
	 * Handles determining which type of arguments to pass to CMB2_Field
850
	 * @since  2.0.7
851
	 * @param  mixed  $field_id     Field (or group field) ID
852
	 * @param  mixed  $field_args   Array of field arguments
853
	 * @param  mixed  $sub_field_id Sub field ID (if field_group exists)
854
	 * @param  mixed  $field_group  If a sub-field, will be the parent group CMB2_Field object
855
	 * @return array                Array of CMB2_Field arguments
856
	 */
857 13
	public function get_field_args( $field_id, $field_args, $sub_field_id, $field_group ) {
858
859
		// Check if group is passed and if fields were added in the old-school fields array
860 13
		if ( $field_group && ( $sub_field_id || 0 === $sub_field_id ) ) {
861
862
			// Update the fields array w/ any modified properties inherited from the group field
863 2
			$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] = $field_args;
864
865 2
			return $this->get_default_args( $field_args, $field_group );
866
		}
867
868 13
		if ( is_array( $field_args ) ) {
869 2
			$this->meta_box['fields'][ $field_id ] = array_merge( $field_args, $this->meta_box['fields'][ $field_id ] );
870 2
		}
871
872 13
		return $this->get_default_args( $this->meta_box['fields'][ $field_id ] );
873
	}
874
875
	/**
876
	 * Get default field arguments specific to this CMB2 object.
877
	 * @since  2.2.0
878
	 * @param  array      $field_args  Metabox field config array.
879
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
880
	 * @return array                   Array of field arguments.
881
	 */
882 17
	protected function get_default_args( $field_args, $field_group = null ) {
883 17
		if ( $field_group ) {
884
			$args = array(
885 3
				'field_args'  => $field_args,
886 3
				'group_field' => $field_group,
887 3
			);
888 3
		} else {
889
			$args = array(
890 17
				'field_args'  => $field_args,
891 17
				'object_type' => $this->object_type(),
892 17
				'object_id'   => $this->object_id(),
893 17
				'cmb_id'      => $this->cmb_id,
894 17
			);
895
		}
896
897 17
		return $args;
898
	}
899
900
	/**
901
	 * Get a new field object specific to this CMB2 object.
902
	 * @since  2.2.0
903
	 * @param  array      $field_args  Metabox field config array.
904
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
905
	 * @return CMB2_Field CMB2_Field object
906
	 */
907 5
	protected function get_new_field( $field_args, $field_group = null ) {
908 5
		return new CMB2_Field( $this->get_default_args( $field_args, $field_group ) );
909
	}
910
911
	/**
912
	 * When fields are added in the old-school way, intitate them as they should be
913
	 * @since 2.1.0
914
	 * @param array $fields          Array of fields to add
915
	 * @param mixed $parent_field_id Parent field id or null
916
	 */
917 41
	protected function add_fields( $fields, $parent_field_id = null ) {
918 41
		foreach ( $fields as $field ) {
919
920 41
			$sub_fields = false;
921 41
			if ( array_key_exists( 'fields', $field ) ) {
922
				$sub_fields = $field['fields'];
923
				unset( $field['fields'] );
924
			}
925
926
			$field_id = $parent_field_id
927 41
				? $this->add_group_field( $parent_field_id, $field )
928 41
				: $this->add_field( $field );
929
930 41
			if ( $sub_fields ) {
931
				$this->add_fields( $sub_fields, $field_id );
932
			}
933 41
		}
934 41
	}
935
936
	/**
937
	 * Add a field to the metabox
938
	 * @since  2.0.0
939
	 * @param  array  $field           Metabox field config array
940
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
941
	 * @return mixed                   Field id or false
942
	 */
943 43
	public function add_field( array $field, $position = 0 ) {
944 43
		if ( ! is_array( $field ) || ! array_key_exists( 'id', $field ) ) {
945
			return false;
946
		}
947
948 43
		if ( 'oembed' === $field['type'] ) {
949
			// Initiate oembed Ajax hooks
950 1
			cmb2_ajax();
951 1
		}
952
953 43
		$this->_add_field_to_array(
954 43
			$field,
955 43
			$this->meta_box['fields'],
956
			$position
957 43
		);
958
959 43
		return $field['id'];
960
	}
961
962
	/**
963
	 * Add a field to a group
964
	 * @since  2.0.0
965
	 * @param  string $parent_field_id The field id of the group field to add the field
966
	 * @param  array  $field           Metabox field config array
967
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
968
	 * @return mixed                   Array of parent/field ids or false
969
	 */
970 3
	public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
971 3
		if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
972
			return false;
973
		}
974
975 3
		$parent_field = $this->meta_box['fields'][ $parent_field_id ];
976
977 3
		if ( 'group' !== $parent_field['type'] ) {
978
			return false;
979
		}
980
981 3
		if ( ! isset( $parent_field['fields'] ) ) {
982 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
983 3
		}
984
985 3
		$this->_add_field_to_array(
986 3
			$field,
987 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'],
988
			$position
989 3
		);
990
991 3
		return array( $parent_field_id, $field['id'] );
992
	}
993
994
	/**
995
	 * Add a field array to a fields array in desired position
996
	 * @since 2.0.2
997
	 * @param array   $field    Metabox field config array
998
	 * @param array   &$fields  Array (passed by reference) to append the field (array) to
999
	 * @param integer $position Optionally specify a position in the array to be inserted
1000
	 */
1001 43
	protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1002 43
		if ( $position ) {
1003 1
			cmb2_utils()->array_insert( $fields, array( $field['id'] => $field ), $position );
1004 1
		} else {
1005 43
			$fields[ $field['id'] ] = $field;
1006
		}
1007 43
	}
1008
1009
	/**
1010
	 * Remove a field from the metabox
1011
	 * @since 2.0.0
1012
	 * @param  string $field_id        The field id of the field to remove
1013
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1014
	 * @return bool                    True if field was removed
1015
	 */
1016 2
	public function remove_field( $field_id, $parent_field_id = '' ) {
1017 2
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1018
1019 2
		if ( ! $ids ) {
1020
			return false;
1021
		}
1022
1023 2
		list( $field_id, $sub_field_id ) = $ids;
1024
1025 2
		unset( $this->fields[ implode( '', $ids ) ] );
1026
1027 2
		if ( ! $sub_field_id ) {
1028 1
			unset( $this->meta_box['fields'][ $field_id ] );
1029 1
			return true;
1030
		}
1031
1032 1
		if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1033 1
			unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1034 1
		}
1035 1
		if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1036 1
			unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1037 1
		}
1038 1
		return true;
1039
	}
1040
1041
	/**
1042
	 * Update or add a property to a field
1043
	 * @since  2.0.0
1044
	 * @param  string $field_id        Field id
1045
	 * @param  string $property        Field property to set/update
1046
	 * @param  mixed  $value           Value to set the field property
1047
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1048
	 * @return mixed                   Field id. Strict compare to false, as success can return a falsey value (like 0)
1049
	 */
1050 4
	public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1051 4
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1052
1053 4
		if ( ! $ids ) {
1054 2
			return false;
1055
		}
1056
1057 2
		list( $field_id, $sub_field_id ) = $ids;
1058
1059 2
		if ( ! $sub_field_id ) {
1060 2
			$this->meta_box['fields'][ $field_id ][ $property ] = $value;
1061 2
			return $field_id;
1062
		}
1063
1064
		$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1065
		return $field_id;
1066
	}
1067
1068
	/**
1069
	 * Check if field ids match a field and return the index/field id
1070
	 * @since  2.0.2
1071
	 * @param  string  $field_id        Field id
1072
	 * @param  string  $parent_field_id (optional) Parent field id
1073
	 * @return mixed                    Array of field/parent ids, or false
1074
	 */
1075 19
	public function get_field_ids( $field_id, $parent_field_id = '' ) {
1076 19
		$sub_field_id = $parent_field_id ? $field_id : '';
1077 19
		$field_id     = $parent_field_id ? $parent_field_id : $field_id;
1078 19
		$fields       =& $this->meta_box['fields'];
1079
1080 19
		if ( ! array_key_exists( $field_id, $fields ) ) {
1081 2
			$field_id = $this->search_old_school_array( $field_id, $fields );
1082 2
		}
1083
1084 19
		if ( false === $field_id ) {
1085 2
			return false;
1086
		}
1087
1088 17
		if ( ! $sub_field_id ) {
1089 17
			return array( $field_id, $sub_field_id );
1090
		}
1091
1092 3
		if ( 'group' !== $fields[ $field_id ]['type'] ) {
1093
			return false;
1094
		}
1095
1096 3
		if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1097
			$sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1098
		}
1099
1100 3
		return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1101
	}
1102
1103
	/**
1104
	 * When using the old array filter, it is unlikely field array indexes will be the field id
1105
	 * @since  2.0.2
1106
	 * @param  string $field_id The field id
1107
	 * @param  array  $fields   Array of fields to search
1108
	 * @return mixed            Field index or false
1109
	 */
1110 2
	public function search_old_school_array( $field_id, $fields ) {
1111 2
		$ids = wp_list_pluck( $fields, 'id' );
1112 2
		$index = array_search( $field_id, $ids );
1113 2
		return false !== $index ? $index : false;
1114
	}
1115
1116
	/**
1117
	 * Determine whether this cmb object should show, based on the 'show_on_cb' callback.
1118
	 *
1119
	 * @since 2.0.9
1120
	 *
1121
	 * @return bool Whether this cmb should be shown.
1122
	 */
1123 View Code Duplication
	public function should_show() {
1124
		// Default to showing this cmb
1125
		$show = true;
1126
1127
		// Use the callback to determine showing the cmb, if it exists
1128
		if ( is_callable( $this->prop( 'show_on_cb' ) ) ) {
1129
			$show = (bool) call_user_func( $this->prop( 'show_on_cb' ), $this );
1130
		}
1131
1132
		return $show;
1133
	}
1134
1135
	/**
1136
	 * Generate a unique nonce field for each registered meta_box
1137
	 * @since  2.0.0
1138
	 * @return string unique nonce hidden input
1139
	 */
1140 1
	public function nonce_field() {
1141 1
		wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1142 1
	}
1143
1144
	/**
1145
	 * Generate a unique nonce for each registered meta_box
1146
	 * @since  2.0.0
1147
	 * @return string unique nonce string
1148
	 */
1149 1
	public function nonce() {
1150 1
		if ( $this->generated_nonce ) {
1151 1
			return $this->generated_nonce;
1152
		}
1153 1
		$this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1154 1
		return $this->generated_nonce;
1155
	}
1156
1157
	/**
1158
	 * Magic getter for our object.
1159
	 * @param string $field
1160
	 * @throws Exception Throws an exception if the field is invalid.
1161
	 * @return mixed
1162
	 */
1163 44
	public function __get( $field ) {
1164
		switch ( $field ) {
1165 44
			case 'cmb_id':
1166 44
			case 'meta_box':
1167 44
			case 'updated':
1168 44
				return $this->{$field};
1169 3
			case 'object_id':
1170 1
				return $this->object_id();
1171 2
			default:
1172 2
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
1173 2
		}
1174
	}
1175
1176
}
1177