Completed
Push — trunk ( f02feb...6ee722 )
by Justin
07:11
created

CMB2::__construct()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0047

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 12
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 28
rs 8.5806
ccs 14
cts 15
cp 0.9333
crap 4.0047
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 extends CMB2_Base {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
16
17
	/**
18
	 * The object properties name.
19
	 * @var   string
20
	 * @since 2.2.3
21
	 */
22
	protected $properties_name = 'meta_box';
23
24
	/**
25
	 * Metabox Config array
26
	 * @var   array
27
	 * @since 0.9.0
28
	 */
29
	protected $meta_box = array();
30
31
	/**
32
	 * Type of object registered for metabox. (e.g., post, user, or comment)
33
	 * @var   string
34
	 * @since 1.0.0
35
	 */
36
	protected $mb_object_type = null;
37
38
	/**
39
	 * List of fields that are changed/updated on save
40
	 * @var   array
41
	 * @since 1.1.0
42
	 */
43
	protected $updated = array();
44
45
	/**
46
	 * Metabox Defaults
47
	 * @var   array
48
	 * @since 1.0.1
49
	 */
50
	protected $mb_defaults = array(
51
		'id'               => '',
52
		'title'            => '',
53
		'type'             => '',
54
		'object_types'     => array(), // Post type
55
		'context'          => 'normal',
56
		'priority'         => 'high',
57
		'show_names'       => true, // Show field names on the left
58
		'show_on_cb'       => null, // Callback to determine if metabox should display.
59
		'show_on'          => array(), // Post IDs or page templates to display this metabox. overrides 'show_on_cb'
60
		'cmb_styles'       => true, // Include CMB2 stylesheet
61
		'enqueue_js'       => true, // Include CMB2 JS
62
		'fields'           => array(),
63
		'hookup'           => true,
64
		'save_fields'      => true, // Will not save during hookup if false
65
		'closed'           => false, // Default to metabox being closed?
66
		'taxonomies'       => array(),
67
		'new_user_section' => 'add-new-user', // or 'add-existing-user'
68
		'new_term_section' => true,
69
	);
70
71
	/**
72
	 * Metabox field objects
73
	 * @var   array
74
	 * @since 2.0.3
75
	 */
76
	protected $fields = array();
77
78
	/**
79
	 * An array of hidden fields to output at the end of the form
80
	 * @var   array
81
	 * @since 2.0.0
82
	 */
83
	protected $hidden_fields = array();
84
85
	/**
86
	 * Array of key => value data for saving. Likely $_POST data.
87
	 * @var   string
88
	 * @since 2.0.0
89
	 */
90
	protected $generated_nonce = '';
91
92
	/**
93
	 * Whether there are fields to be shown in columns. Set in CMB2::add_field().
94
	 * @var   bool
95
	 * @since 2.2.2
96
	 */
97
	protected $has_columns = false;
98
99
	/**
100
	 * If taxonomy field is requesting to remove_default, we store the taxonomy here.
101
	 * @var   bool
102
	 * @since 2.2.3
103
	 */
104
	protected $tax_metaboxes_to_remove = array();
105
106
	/**
107
	 * Get started
108
	 * @since 0.4.0
109
	 * @param array   $config    Metabox config array
110
	 * @param integer $object_id Optional object id
111
	 */
112 44
	public function __construct( $config, $object_id = 0 ) {
113
114 44
		if ( empty( $config['id'] ) ) {
115 1
			wp_die( __( 'Metabox configuration is required to have an ID parameter', 'cmb2' ) );
116
		}
117
118 44
		$this->meta_box = wp_parse_args( $config, $this->mb_defaults );
119 44
		$this->meta_box['fields'] = array();
120
121 44
		$this->object_id( $object_id );
122 44
		$this->mb_object_type();
123 44
		$this->cmb_id = $config['id'];
124
125 44
		if ( ! empty( $config['fields'] ) && is_array( $config['fields'] ) ) {
126 41
			$this->add_fields( $config['fields'] );
127 41
		}
128
129 44
		CMB2_Boxes::add( $this );
130
131
		/**
132
		 * Hook during initiation of CMB2 object
133
		 *
134
		 * The dynamic portion of the hook name, $this->cmb_id, is this meta_box id.
135
		 *
136
		 * @param array $cmb This CMB2 object
137
		 */
138 44
		do_action( "cmb2_init_{$this->cmb_id}", $this );
139 44
	}
140
141
	/**
142
	 * Loops through and displays fields
143
	 * @since 1.0.0
144
	 * @param int    $object_id   Object ID
145
	 * @param string $object_type Type of object being saved. (e.g., post, user, or comment)
146
	 */
147 1
	public function show_form( $object_id = 0, $object_type = '' ) {
148 1
		$this->render_form_open( $object_id, $object_type );
149
150 1
		foreach ( $this->prop( 'fields' ) as $field_args ) {
151 1
			$this->render_field( $field_args );
152 1
		}
153
154 1
		$this->render_form_close( $object_id, $object_type );
155 1
	}
156
157
	/**
158
	 * Outputs the opening form markup and runs corresponding hooks:
159
	 * 'cmb2_before_form' and "cmb2_before_{$object_type}_form_{$this->cmb_id}"
160
	 * @since  2.2.0
161
	 * @param  integer $object_id   Object ID
162
	 * @param  string  $object_type Object type
163
	 * @return void
164
	 */
165 1
	public function render_form_open( $object_id = 0, $object_type = '' ) {
166 1
		$object_type = $this->object_type( $object_type );
167 1
		$object_id = $this->object_id( $object_id );
168
169 1
		echo "\n<!-- Begin CMB2 Fields -->\n";
170
171 1
		$this->nonce_field();
172
173
		/**
174
		 * Hook before form table begins
175
		 *
176
		 * @param array  $cmb_id      The current box ID
177
		 * @param int    $object_id   The ID of the current object
178
		 * @param string $object_type The type of object you are working with.
179
		 *	                           Usually `post` (this applies to all post-types).
180
		 *	                           Could also be `comment`, `user` or `options-page`.
181
		 * @param array  $cmb         This CMB2 object
182
		 */
183 1
		do_action( 'cmb2_before_form', $this->cmb_id, $object_id, $object_type, $this );
184
185
		/**
186
		 * Hook before form table begins
187
		 *
188
		 * The first dynamic portion of the hook name, $object_type, is the type of object
189
		 * you are working with. Usually `post` (this applies to all post-types).
190
		 * Could also be `comment`, `user` or `options-page`.
191
		 *
192
		 * The second dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
193
		 *
194
		 * @param array  $cmb_id      The current box ID
195
		 * @param int    $object_id   The ID of the current object
196
		 * @param array  $cmb         This CMB2 object
197
		 */
198 1
		do_action( "cmb2_before_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
199
200 1
		echo '<div class="', $this->box_classes(), '"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
201
202 1
	}
203
204
	/**
205
	 * Defines the classes for the CMB2 form/wrap.
206
	 *
207
	 * @since  2.0.0
208
	 * @return string Space concatenated list of classes
209
	 */
210 1
	public function box_classes() {
211
212 1
		$classes = array( 'cmb2-wrap', 'form-table' );
213
214
		// Use the callback to fetch classes.
215 1 View Code Duplication
		if ( $added_classes = $this->get_param_callback_result( 'classes_cb' ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
217 1
			$classes = array_merge( $classes, $added_classes );
218 1
		}
219
220 1 View Code Duplication
		if ( $added_classes = $this->prop( 'classes' ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
222 1
			$classes = array_merge( $classes, $added_classes );
223 1
		}
224
225
		/**
226
		 * Globally filter box wrap classes
227
		 *
228
		 * @since 2.2.2
229
		 *
230
		 * @param string $classes Array of classes for the cmb2-wrap.
231
		 * @param CMB2   $cmb     This CMB2 object.
232
		 */
233 1
		$classes = apply_filters( 'cmb2_wrap_classes', $classes, $this );
234
235
		// Clean up.
236 1
		$classes = array_map( 'strip_tags', array_filter( $classes ) );
237
238
		// Make a string.
239 1
		return implode( ' ', $classes );
240
	}
241
242
	/**
243
	 * Outputs the closing form markup and runs corresponding hooks:
244
	 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
245
	 * @since  2.2.0
246
	 * @param  integer $object_id   Object ID
247
	 * @param  string  $object_type Object type
248
	 * @return void
249
	 */
250 1
	public function render_form_close( $object_id = 0, $object_type = '' ) {
251 1
		$object_type = $this->object_type( $object_type );
252 1
		$object_id = $this->object_id( $object_id );
253
254 1
		echo '</div></div>';
255
256 1
		$this->render_hidden_fields();
257
258
		/**
259
		 * Hook after form form has been rendered
260
		 *
261
		 * @param array  $cmb_id      The current box ID
262
		 * @param int    $object_id   The ID of the current object
263
		 * @param string $object_type The type of object you are working with.
264
		 *	                           Usually `post` (this applies to all post-types).
265
		 *	                           Could also be `comment`, `user` or `options-page`.
266
		 * @param array  $cmb         This CMB2 object
267
		 */
268 1
		do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
269
270
		/**
271
		 * Hook after form form has been rendered
272
		 *
273
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
274
		 *
275
		 * The first dynamic portion of the hook name, $object_type, is the type of object
276
		 * you are working with. Usually `post` (this applies to all post-types).
277
		 * Could also be `comment`, `user` or `options-page`.
278
		 *
279
		 * @param int    $object_id   The ID of the current object
280
		 * @param array  $cmb         This CMB2 object
281
		 */
282 1
		do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
283
284 1
		echo "\n<!-- End CMB2 Fields -->\n";
285
286 1
	}
287
288
	/**
289
	 * Renders a field based on the field type
290
	 * @since  2.2.0
291
	 * @param  array $field_args A field configuration array.
292
	 * @return mixed CMB2_Field object if successful.
293
	 */
294 1
	public function render_field( $field_args ) {
295 1
		$field_args['context'] = $this->prop( 'context' );
296
297 1
		if ( 'group' == $field_args['type'] ) {
298
299
			if ( ! isset( $field_args['show_names'] ) ) {
300
				$field_args['show_names'] = $this->prop( 'show_names' );
301
			}
302
			$field = $this->render_group( $field_args );
303
304 1
		} elseif ( 'hidden' == $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
305
			// Save rendering for after the metabox
306
			$field = $this->add_hidden_field( $field_args );
307
308
		} else {
309
310 1
			$field_args['show_names'] = $this->prop( 'show_names' );
311
312
			// Render default fields
313 1
			$field = $this->get_field( $field_args )->render_field();
314
		}
315
316 1
		return $field;
317
	}
318
319
	/**
320
	 * Render a repeatable group.
321
	 * @param array $args Array of field arguments for a group field parent.
322
	 * @return CMB2_Field|null Group field object.
323
	 */
324 2
	public function render_group( $args ) {
325
326 2
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
327
			return;
328
		}
329
330 2
		$field_group = $this->get_field( $args );
331
332
		// If field is requesting to be conditionally shown
333 2
		if ( ! $field_group || ! $field_group->should_show() ) {
334
			return;
335
		}
336
337 2
		$desc            = $field_group->args( 'description' );
338 2
		$label           = $field_group->args( 'name' );
339 2
		$sortable        = $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
340 2
		$repeat_class    = $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
341 2
		$group_val       = (array) $field_group->value();
342 2
		$nrows           = count( $group_val );
343 2
		$remove_disabled = $nrows <= 1 ? 'disabled="disabled" ' : '';
344 2
		$field_group->index = 0;
345
346 2
		$field_group->peform_param_callback( 'before_group' );
347
348 2
		echo '<div class="cmb-row cmb-repeat-group-wrap ', $field_group->row_classes(), '" data-fieldtype="group"><div class="cmb-td"><div data-groupid="', $field_group->id(), '" id="', $field_group->id(), '_repeat" class="cmb-nested cmb-field-list cmb-repeatable-group', $sortable, $repeat_class, '" style="width:100%;">';
349
350 2
		if ( $desc || $label ) {
351 2
			$class = $desc ? ' cmb-group-description' : '';
352 2
			echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
353 2
				if ( $label ) {
354 2
					echo '<h2 class="cmb-group-name">', $label, '</h2>';
355 2
				}
356 2
				if ( $desc ) {
357 1
					echo '<p class="cmb2-metabox-description">', $desc, '</p>';
358 1
				}
359 2
			echo '</div></div>';
360 2
		}
361
362 2
		if ( ! empty( $group_val ) ) {
363
364
			foreach ( $group_val as $group_key => $field_id ) {
365
				$this->render_group_row( $field_group, $remove_disabled );
366
				$field_group->index++;
367
			}
368
		} else {
369 2
			$this->render_group_row( $field_group, $remove_disabled );
370
		}
371
372 2
		if ( $field_group->args( 'repeatable' ) ) {
373 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>';
374 1
		}
375
376 2
		echo '</div></div></div>';
377
378 2
		$field_group->peform_param_callback( 'after_group' );
379
380 2
		return $field_group;
381
	}
382
383
	/**
384
	 * Render a repeatable group row
385
	 * @since  1.0.2
386
	 * @param  CMB2_Field $field_group  CMB2_Field group field object
387
	 * @param  string  $remove_disabled Attribute string to disable the remove button
388
	 */
389 2
	public function render_group_row( $field_group, $remove_disabled ) {
390
391 2
		$field_group->peform_param_callback( 'before_group_row' );
392 2
		$closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
393
394
		echo '
395 2
		<div class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
396
397 2
			if ( $field_group->args( 'repeatable' ) ) {
398 1
				echo '<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="dashicons-before dashicons-no-alt cmb-remove-group-row"></button>';
399 1
			}
400
401
			echo '
402 2
			<div class="cmbhandle" title="' , __( 'Click to toggle', 'cmb2' ), '"><br></div>
403 2
			<h3 class="cmb-group-title cmbhandle-title"><span>', $field_group->replace_hash( $field_group->options( 'group_title' ) ), '</span></h3>
0 ignored issues
show
Documentation introduced by
$field_group->options('group_title') is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
404
405
			<div class="inside cmb-td cmb-nested cmb-field-list">';
406
				// Loop and render repeatable group fields
407 2
				foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
408 2
					if ( 'hidden' == $field_args['type'] ) {
409
410
						// Save rendering for after the metabox
411
						$this->add_hidden_field( $field_args, $field_group );
412
413
					} else {
414
415 2
						$field_args['show_names'] = $field_group->args( 'show_names' );
416 2
						$field_args['context']    = $field_group->args( 'context' );
417
418 2
						$field = $this->get_field( $field_args, $field_group )->render_field();
0 ignored issues
show
Unused Code introduced by
$field is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
419
					}
420 2
				}
421 2
				if ( $field_group->args( 'repeatable' ) ) {
422
					echo '
423
					<div class="cmb-row cmb-remove-field-row">
424
						<div class="cmb-remove-row">
425 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>
426
						</div>
427
					</div>
428
					';
429 1
				}
430
			echo '
431
			</div>
432
		</div>
433 2
		';
434
435 2
		$field_group->peform_param_callback( 'after_group_row' );
436 2
	}
437
438
	/**
439
	 * Add a hidden field to the list of hidden fields to be rendered later
440
	 * @since 2.0.0
441
	 * @param array  $field_args Array of field arguments to be passed to CMB2_Field
442
	 */
443
	public function add_hidden_field( $field_args, $field_group = null ) {
444
		if ( isset( $field_args['field_args'] ) ) {
445
			// For back-compatibility.
446
			$field = new CMB2_Field( $field_args );
447
		} else {
448
			$field = $this->get_new_field( $field_args, $field_group );
449
		}
450
451
		$type = new CMB2_Types( $field );
452
453
		if ( $field_group ) {
454
			$type->iterator = $field_group->index;
455
		}
456
457
		$this->hidden_fields[] = $type;
458
459
		return $field;
460
	}
461
462
	/**
463
	 * Loop through and output hidden fields
464
	 * @since  2.0.0
465
	 */
466 1
	public function render_hidden_fields() {
467 1
		if ( ! empty( $this->hidden_fields ) ) {
468
			foreach ( $this->hidden_fields as $hidden ) {
469
				$hidden->render();
470
			}
471
		}
472 1
	}
473
474
	/**
475
	 * Returns array of sanitized field values (without saving them)
476
	 * @since  2.0.3
477
	 * @param  array  $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
478
	 */
479 2
	public function get_sanitized_values( array $data_to_sanitize ) {
480 2
		$this->data_to_save = $data_to_sanitize;
481 2
		$stored_id          = $this->object_id();
482
483
		// We do this So CMB will sanitize our data for us, but not save it
484 2
		$this->object_id( '_' );
485
486
		// Ensure temp. data store is empty
487 2
		cmb2_options( 0 )->set();
488
489
		// Process/save fields
490 2
		$this->process_fields();
491
492
		// Get data from temp. data store
493 2
		$sanitized_values = cmb2_options( 0 )->get_options();
494
495
		// Empty out temp. data store again
496 2
		cmb2_options( 0 )->set();
497
498
		// Reset the object id
499 2
		$this->object_id( $stored_id );
500
501 2
		return $sanitized_values;
502
	}
503
504
	/**
505
	 * Loops through and saves field data
506
	 * @since  1.0.0
507
	 * @param  int    $object_id    Object ID
508
	 * @param  string $object_type  Type of object being saved. (e.g., post, user, or comment)
509
	 * @param  array  $data_to_save Array of key => value data for saving. Likely $_POST data.
510
	 */
511 1
	public function save_fields( $object_id = 0, $object_type = '', $data_to_save = array() ) {
0 ignored issues
show
Coding Style introduced by
save_fields uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
512
513
		// Fall-back to $_POST data
514 1
		$this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
515 1
		$object_id = $this->object_id( $object_id );
516 1
		$object_type = $this->object_type( $object_type );
517
518 1
		$this->process_fields();
519
520
		// If options page, save the updated options
521 1
		if ( 'options-page' == $object_type ) {
522 1
			cmb2_options( $object_id )->set();
523 1
		}
524
525 1
		$this->after_save();
526 1
	}
527
528
	/**
529
	 * Process and save form fields
530
	 * @since  2.0.0
531
	 */
532 3
	public function process_fields() {
533
534 3
		$this->pre_process();
535
536
		// Remove the show_on properties so saving works
537 3
		$this->prop( 'show_on', array() );
538
539
		// save field ids of those that are updated
540 3
		$this->updated = array();
541
542 3
		foreach ( $this->prop( 'fields' ) as $field_args ) {
543 3
			$this->process_field( $field_args );
544 3
		}
545 3
	}
546
547
	/**
548
	 * Process and save a field
549
	 * @since  2.0.0
550
	 * @param  array  $field_args Array of field arguments
551
	 */
552 3
	public function process_field( $field_args ) {
553
554 3
		switch ( $field_args['type'] ) {
555
556 3
			case 'group':
557 1
				if ( $this->save_group( $field_args ) ) {
558 1
					$this->updated[] = $field_args['id'];
559 1
				}
560
561 1
				break;
562
563 2
			case 'title':
564
				// Don't process title fields
565
				break;
566
567 2
			default:
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
568
569 2
				$field = $this->get_new_field( $field_args );
570
571 2
				if ( $field->save_field_from_data( $this->data_to_save ) ) {
572 2
					$this->updated[] = $field->id();
573 2
				}
574
575 2
				break;
576 3
		}
577
578 3
	}
579
580 3
	public function pre_process() {
581
		/**
582
		 * Fires before fields have been processed/saved.
583
		 *
584
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
585
		 *
586
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
587
		 * 	Usually `post` (this applies to all post-types).
588
		 *  	Could also be `comment`, `user` or `options-page`.
589
		 *
590
		 * @param array $cmb       This CMB2 object
591
		 * @param int   $object_id The ID of the current object
592
		 */
593 3
		do_action( "cmb2_{$this->object_type()}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
594 3
	}
595
596 1
	public function after_save() {
597 1
		$object_type = $this->object_type();
598 1
		$object_id   = $this->object_id();
599
600
		/**
601
		 * Fires after all fields have been saved.
602
		 *
603
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
604
		 * 	Usually `post` (this applies to all post-types).
605
		 *  	Could also be `comment`, `user` or `options-page`.
606
		 *
607
		 * @param int    $object_id   The ID of the current object
608
		 * @param array  $cmb_id      The current box ID
609
		 * @param string $updated     Array of field ids that were updated.
610
		 *                            Will only include field ids that had values change.
611
		 * @param array  $cmb         This CMB2 object
612
		 */
613 1
		do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
614
615
		/**
616
		 * Fires after all fields have been saved.
617
		 *
618
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
619
		 *
620
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
621
		 * 	Usually `post` (this applies to all post-types).
622
		 *  	Could also be `comment`, `user` or `options-page`.
623
		 *
624
		 * @param int    $object_id   The ID of the current object
625
		 * @param string $updated     Array of field ids that were updated.
626
		 *                            Will only include field ids that had values change.
627
		 * @param array  $cmb         This CMB2 object
628
		 */
629 1
		do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
630 1
	}
631
632
	/**
633
	 * Save a repeatable group
634
	 * @since  1.x.x
635
	 * @param  array  $args Field arguments array
636
	 * @return mixed        Return of CMB2_Field::update_data()
637
	 */
638 1
	public function save_group( $args ) {
639 1
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
640
			return;
641
		}
642
643 1
		return $this->save_group_field( $this->get_new_field( $args ) );
0 ignored issues
show
Documentation introduced by
$this->get_new_field($args) is of type object<CMB2_Field>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
644
	}
645
646
	/**
647
	 * Save a repeatable group
648
	 * @since  1.x.x
649
	 * @param  array $field_group CMB2_Field group field object
650
	 * @return mixed              Return of CMB2_Field::update_data()
651
	 */
652 1
	public function save_group_field( $field_group ) {
653 1
		$base_id = $field_group->id();
0 ignored issues
show
Bug introduced by
The method id cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
654
655 1
		if ( ! isset( $this->data_to_save[ $base_id ] ) ) {
656
			return;
657
		}
658
659 1
		$old        = $field_group->get_data();
0 ignored issues
show
Bug introduced by
The method get_data cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
660
		// Check if group field has sanitization_cb
661 1
		$group_vals = $field_group->sanitization_cb( $this->data_to_save[ $base_id ] );
0 ignored issues
show
Bug introduced by
The method sanitization_cb cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
662 1
		$saved      = array();
663
664 1
		$field_group->index = 0;
665 1
		$field_group->data_to_save = $this->data_to_save;
666
667 1
		foreach ( array_values( $field_group->fields() ) as $field_args ) {
0 ignored issues
show
Bug introduced by
The method fields cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
668
669 1
			$field  = $this->get_new_field( $field_args, $field_group );
0 ignored issues
show
Documentation introduced by
$field_group is of type array, but the function expects a object<CMB2_Field>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
670 1
			$sub_id = $field->id( true );
671
672 1
			foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
673
674
				// Get value
675 1
				$new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
676 1
					? $group_vals[ $field_group->index ][ $sub_id ]
677 1
					: false;
678
679
				// Sanitize
680 1
				$new_val = $field->sanitization_cb( $new_val );
681
682 1
				if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
683 1
					if ( $field->args( 'repeatable' ) ) {
684 1
						$_new_val = array();
685 1
						foreach ( $new_val as $group_index => $grouped_data ) {
686
							// Add the supporting data to the $saved array stack
687 1
							$saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
688
							// Reset var to the actual value
689 1
							$_new_val[ $group_index ] = $grouped_data['value'];
690 1
						}
691 1
						$new_val = $_new_val;
692 1
					} else {
693
						// Add the supporting data to the $saved array stack
694 1
						$saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
695
						// Reset var to the actual value
696 1
						$new_val = $new_val['value'];
697
					}
698 1
				}
699
700
				// Get old value
701 1
				$old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
702 1
					? $old[ $field_group->index ][ $sub_id ]
703 1
					: false;
704
705 1
				$is_updated = ( ! cmb2_utils()->isempty( $new_val ) && $new_val !== $old_val );
706 1
				$is_removed = ( cmb2_utils()->isempty( $new_val ) && ! cmb2_utils()->isempty( $old_val ) );
707
708
				// Compare values and add to `$updated` array
709 1
				if ( $is_updated || $is_removed ) {
710 1
					$this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
711 1
				}
712
713
				// Add to `$saved` array
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
714 1
				$saved[ $field_group->index ][ $sub_id ] = $new_val;
715
716 1
			}
717
718 1
			$saved[ $field_group->index ] = cmb2_utils()->filter_empty( $saved[ $field_group->index ] );
719 1
		}
720
721 1
		$saved = cmb2_utils()->filter_empty( $saved );
722
723 1
		return $field_group->update_data( $saved, true );
0 ignored issues
show
Bug introduced by
The method update_data cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
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 ) {
0 ignored issues
show
Coding Style introduced by
object_id uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
object_id uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
733 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...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $type can also be of type false. However, the property $mb_object_type is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 View Code Duplication
	public function current_object_type() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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.2
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $field; (string|array|CMB2_Field) is incompatible with the return type documented by CMB2::get_field of type CMB2_Field|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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 );
0 ignored issues
show
Unused Code introduced by
The call to CMB2::get_field_ids() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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 View Code Duplication
	protected function get_default_args( $field_args, $field_group = null ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
		if ( isset( $field['column'] ) && false !== $field['column'] ) {
1052
			$this->has_columns = true;
1053
1054
			$column = is_array( $field['column'] ) ? $field['column'] : array();
1055
1056
			$field['column'] = wp_parse_args( $column, array(
1057
				'name'     => isset( $field['name'] ) ? $field['name'] : '',
1058
				'position' => false,
1059
			) );
1060
		}
1061
1062 43
		if ( isset( $field['taxonomy'] ) && ! empty( $field['remove_default'] ) ) {
1063
			$this->tax_metaboxes_to_remove[ $field['taxonomy'] ] = $field['taxonomy'];
1064
		}
1065
1066 43
		$this->_add_field_to_array(
1067 43
			$field,
1068 43
			$this->meta_box['fields'],
1069
			$position
1070 43
		);
1071
1072 43
		return $field['id'];
1073
	}
1074
1075
	/**
1076
	 * Add a field to a group
1077
	 * @since  2.0.0
1078
	 * @param  string $parent_field_id The field id of the group field to add the field
1079
	 * @param  array  $field           Metabox field config array
1080
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1081
	 * @return mixed                   Array of parent/field ids or false
1082
	 */
1083 3
	public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
1084 3
		if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
1085
			return false;
1086
		}
1087
1088 3
		$parent_field = $this->meta_box['fields'][ $parent_field_id ];
1089
1090 3
		if ( 'group' !== $parent_field['type'] ) {
1091
			return false;
1092
		}
1093
1094 3
		if ( ! isset( $parent_field['fields'] ) ) {
1095 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
1096 3
		}
1097
1098 3
		$this->_add_field_to_array(
1099 3
			$field,
1100 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'],
1101
			$position
1102 3
		);
1103
1104 3
		return array( $parent_field_id, $field['id'] );
1105
	}
1106
1107
	/**
1108
	 * Add a field array to a fields array in desired position
1109
	 * @since 2.0.2
1110
	 * @param array   $field    Metabox field config array
1111
	 * @param array   &$fields  Array (passed by reference) to append the field (array) to
1112
	 * @param integer $position Optionally specify a position in the array to be inserted
1113
	 */
1114 43
	protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1115 43
		if ( $position ) {
1116 1
			cmb2_utils()->array_insert( $fields, array( $field['id'] => $field ), $position );
1117 1
		} else {
1118 43
			$fields[ $field['id'] ] = $field;
1119
		}
1120 43
	}
1121
1122
	/**
1123
	 * Remove a field from the metabox
1124
	 * @since 2.0.0
1125
	 * @param  string $field_id        The field id of the field to remove
1126
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1127
	 * @return bool                    True if field was removed
1128
	 */
1129 2
	public function remove_field( $field_id, $parent_field_id = '' ) {
1130 2
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1131
1132 2
		if ( ! $ids ) {
1133
			return false;
1134
		}
1135
1136 2
		list( $field_id, $sub_field_id ) = $ids;
1137
1138 2
		unset( $this->fields[ implode( '', $ids ) ] );
1139
1140 2
		if ( ! $sub_field_id ) {
1141 1
			unset( $this->meta_box['fields'][ $field_id ] );
1142 1
			return true;
1143
		}
1144
1145 1
		if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1146 1
			unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1147 1
		}
1148 1
		if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1149 1
			unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1150 1
		}
1151 1
		return true;
1152
	}
1153
1154
	/**
1155
	 * Update or add a property to a field
1156
	 * @since  2.0.0
1157
	 * @param  string $field_id        Field id
1158
	 * @param  string $property        Field property to set/update
1159
	 * @param  mixed  $value           Value to set the field property
1160
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1161
	 * @return mixed                   Field id. Strict compare to false, as success can return a falsey value (like 0)
1162
	 */
1163 4
	public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1164 4
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1165
1166 4
		if ( ! $ids ) {
1167 2
			return false;
1168
		}
1169
1170 2
		list( $field_id, $sub_field_id ) = $ids;
1171
1172 2
		if ( ! $sub_field_id ) {
1173 2
			$this->meta_box['fields'][ $field_id ][ $property ] = $value;
1174 2
			return $field_id;
1175
		}
1176
1177
		$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1178
		return $field_id;
1179
	}
1180
1181
	/**
1182
	 * Check if field ids match a field and return the index/field id
1183
	 * @since  2.0.2
1184
	 * @param  string  $field_id        Field id
1185
	 * @param  string  $parent_field_id (optional) Parent field id
1186
	 * @return mixed                    Array of field/parent ids, or false
1187
	 */
1188 19
	public function get_field_ids( $field_id, $parent_field_id = '' ) {
1189 19
		$sub_field_id = $parent_field_id ? $field_id : '';
1190 19
		$field_id     = $parent_field_id ? $parent_field_id : $field_id;
1191 19
		$fields       =& $this->meta_box['fields'];
1192
1193 19
		if ( ! array_key_exists( $field_id, $fields ) ) {
1194 2
			$field_id = $this->search_old_school_array( $field_id, $fields );
1195 2
		}
1196
1197 19
		if ( false === $field_id ) {
1198 2
			return false;
1199
		}
1200
1201 17
		if ( ! $sub_field_id ) {
1202 17
			return array( $field_id, $sub_field_id );
1203
		}
1204
1205 3
		if ( 'group' !== $fields[ $field_id ]['type'] ) {
1206
			return false;
1207
		}
1208
1209 3
		if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1210
			$sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1211
		}
1212
1213 3
		return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1214
	}
1215
1216
	/**
1217
	 * When using the old array filter, it is unlikely field array indexes will be the field id
1218
	 * @since  2.0.2
1219
	 * @param  string $field_id The field id
1220
	 * @param  array  $fields   Array of fields to search
1221
	 * @return mixed            Field index or false
1222
	 */
1223 2
	public function search_old_school_array( $field_id, $fields ) {
1224 2
		$ids = wp_list_pluck( $fields, 'id' );
1225 2
		$index = array_search( $field_id, $ids );
1226 2
		return false !== $index ? $index : false;
1227
	}
1228
1229
	/**
1230
	 * Handles metabox property callbacks, and passes this $cmb object as property.
1231
	 * @since  2.2.3
1232
	 * @param  callable $cb The callback method/function/closure
1233
	 * @return mixed        Return of the callback function.
1234
	 */
1235 1
	protected function do_callback( $cb ) {
1236 1
		return call_user_func( $cb, $this );
1237
	}
1238
1239
	/**
1240
	 * Generate a unique nonce field for each registered meta_box
1241
	 * @since  2.0.0
1242
	 * @return string unique nonce hidden input
1243
	 */
1244 1
	public function nonce_field() {
1245 1
		wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1246 1
	}
1247
1248
	/**
1249
	 * Generate a unique nonce for each registered meta_box
1250
	 * @since  2.0.0
1251
	 * @return string unique nonce string
1252
	 */
1253 1
	public function nonce() {
1254 1
		if ( $this->generated_nonce ) {
1255 1
			return $this->generated_nonce;
1256
		}
1257 1
		$this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1258 1
		return $this->generated_nonce;
1259
	}
1260
1261
	/**
1262
	 * Magic getter for our object.
1263
	 * @param string $field
1264
	 * @throws Exception Throws an exception if the field is invalid.
1265
	 * @return mixed
1266
	 */
1267 44
	public function __get( $field ) {
1268
		switch ( $field ) {
1269 44
			case 'updated':
1270 44
			case 'has_columns':
1271 44
			case 'tax_metaboxes_to_remove':
1272 1
				return $this->{$field};
1273 44
			default:
1274 44
				return parent::__get( $field );
1275 44
		}
1276
	}
1277
1278
}
1279