Completed
Push — trunk ( 9de100...661220 )
by Justin
05:36
created

CMB2   D

Complexity

Total Complexity 185

Size/Duplication

Total Lines 1296
Duplicated Lines 3.32 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 81.29%

Importance

Changes 16
Bugs 1 Features 8
Metric Value
c 16
b 1
f 8
dl 43
loc 1296
ccs 413
cts 508
cp 0.8129
rs 4.4102
wmc 185
lcom 1
cbo 6

42 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 28 4
A show_form() 0 9 2
B render_form_open() 0 38 1
B box_classes() 8 31 5
B render_form_close() 0 37 1
B render_field() 0 24 5
F render_group() 0 78 16
B render_group_row() 0 48 6
A add_hidden_field() 0 18 3
A render_hidden_fields() 0 7 3
B get_sanitized_values() 0 24 1
A save_fields() 0 16 3
A process_fields() 0 14 2
B process_field() 0 27 5
A pre_process() 0 15 1
B after_save() 0 35 1
A save_group() 0 7 3
C save_group_field() 0 73 15
D object_id() 0 39 17
C mb_object_type() 0 54 13
A is_options_page_mb() 0 3 3
A object_type() 0 14 3
A current_object_type() 18 18 4
A set_prop() 0 5 1
A prop() 0 7 3
C get_field() 0 25 7
B get_field_args() 0 17 5
A get_default_args() 17 17 2
A get_new_field() 0 3 1
B add_fields() 0 18 5
C add_field() 0 26 8
A define_field_column() 0 12 3
B add_group_field() 0 23 4
A _add_field_to_array() 0 7 2
B remove_field() 0 24 5
A update_field_property() 0 17 3
D get_field_ids() 0 27 9
A search_old_school_array() 0 5 2
A do_callback() 0 3 1
A nonce_field() 0 3 1
A nonce() 0 7 2
A __get() 0 10 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CMB2 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CMB2, and based on these observations, apply Extract Interface, too.

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
 * @property-read bool  $has_columns
15
 * @property-read array $tax_metaboxes_to_remove
16
 */
17
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...
18
19
	/**
20
	 * The object properties name.
21
	 * @var   string
22
	 * @since 2.2.3
23
	 */
24
	protected $properties_name = 'meta_box';
25
26
	/**
27
	 * Metabox Config array
28
	 * @var   array
29
	 * @since 0.9.0
30
	 */
31
	protected $meta_box = array();
32
33
	/**
34
	 * Type of object registered for metabox. (e.g., post, user, or comment)
35
	 * @var   string
36
	 * @since 1.0.0
37
	 */
38
	protected $mb_object_type = null;
39
40
	/**
41
	 * List of fields that are changed/updated on save
42
	 * @var   array
43
	 * @since 1.1.0
44
	 */
45
	protected $updated = array();
46
47
	/**
48
	 * Metabox Defaults
49
	 * @var   array
50
	 * @since 1.0.1
51
	 */
52
	protected $mb_defaults = array(
53
		'id'               => '',
54
		'title'            => '',
55
		'type'             => '',
56
		'object_types'     => array(), // Post type
57
		'context'          => 'normal',
58
		'priority'         => 'high',
59
		'show_names'       => true, // Show field names on the left
60
		'show_on_cb'       => null, // Callback to determine if metabox should display.
61
		'show_on'          => array(), // Post IDs or page templates to display this metabox. overrides 'show_on_cb'
62
		'cmb_styles'       => true, // Include CMB2 stylesheet
63
		'enqueue_js'       => true, // Include CMB2 JS
64
		'fields'           => array(),
65
		'hookup'           => true,
66
		'save_fields'      => true, // Will not save during hookup if false
67
		'closed'           => false, // Default to metabox being closed?
68
		'taxonomies'       => array(),
69
		'new_user_section' => 'add-new-user', // or 'add-existing-user'
70
		'new_term_section' => true,
71
	);
72
73
	/**
74
	 * Metabox field objects
75
	 * @var   array
76
	 * @since 2.0.3
77
	 */
78
	protected $fields = array();
79
80
	/**
81
	 * An array of hidden fields to output at the end of the form
82
	 * @var   array
83
	 * @since 2.0.0
84
	 */
85
	protected $hidden_fields = array();
86
87
	/**
88
	 * Array of key => value data for saving. Likely $_POST data.
89
	 * @var   string
90
	 * @since 2.0.0
91
	 */
92
	protected $generated_nonce = '';
93
94
	/**
95
	 * Whether there are fields to be shown in columns. Set in CMB2::add_field().
96
	 * @var   bool
97
	 * @since 2.2.2
98
	 */
99
	protected $has_columns = false;
100
101
	/**
102
	 * If taxonomy field is requesting to remove_default, we store the taxonomy here.
103
	 * @var   array
104
	 * @since 2.2.3
105
	 */
106
	protected $tax_metaboxes_to_remove = array();
107
108
	/**
109
	 * Get started
110
	 * @since 0.4.0
111
	 * @param array   $config    Metabox config array
112
	 * @param integer $object_id Optional object id
113
	 */
114 44
	public function __construct( $config, $object_id = 0 ) {
115
116 44
		if ( empty( $config['id'] ) ) {
117 1
			wp_die( esc_html__( 'Metabox configuration is required to have an ID parameter.', 'cmb2' ) );
118
		}
119
120 44
		$this->meta_box = wp_parse_args( $config, $this->mb_defaults );
121 44
		$this->meta_box['fields'] = array();
122
123 44
		$this->object_id( $object_id );
124 44
		$this->mb_object_type();
125 44
		$this->cmb_id = $config['id'];
126
127 44
		if ( ! empty( $config['fields'] ) && is_array( $config['fields'] ) ) {
128 41
			$this->add_fields( $config['fields'] );
129 41
		}
130
131 44
		CMB2_Boxes::add( $this );
132
133
		/**
134
		 * Hook during initiation of CMB2 object
135
		 *
136
		 * The dynamic portion of the hook name, $this->cmb_id, is this meta_box id.
137
		 *
138
		 * @param array $cmb This CMB2 object
139
		 */
140 44
		do_action( "cmb2_init_{$this->cmb_id}", $this );
141 44
	}
142
143
	/**
144
	 * Loops through and displays fields
145
	 * @since 1.0.0
146
	 * @param int    $object_id   Object ID
147
	 * @param string $object_type Type of object being saved. (e.g., post, user, or comment)
148
	 */
149 1
	public function show_form( $object_id = 0, $object_type = '' ) {
150 1
		$this->render_form_open( $object_id, $object_type );
151
152 1
		foreach ( $this->prop( 'fields' ) as $field_args ) {
153 1
			$this->render_field( $field_args );
154 1
		}
155
156 1
		$this->render_form_close( $object_id, $object_type );
157 1
	}
158
159
	/**
160
	 * Outputs the opening form markup and runs corresponding hooks:
161
	 * 'cmb2_before_form' and "cmb2_before_{$object_type}_form_{$this->cmb_id}"
162
	 * @since  2.2.0
163
	 * @param  integer $object_id   Object ID
164
	 * @param  string  $object_type Object type
165
	 * @return void
166
	 */
167 1
	public function render_form_open( $object_id = 0, $object_type = '' ) {
168 1
		$object_type = $this->object_type( $object_type );
169 1
		$object_id = $this->object_id( $object_id );
170
171 1
		echo "\n<!-- Begin CMB2 Fields -->\n";
172
173 1
		$this->nonce_field();
174
175
		/**
176
		 * Hook before form table begins
177
		 *
178
		 * @param array  $cmb_id      The current box ID
179
		 * @param int    $object_id   The ID of the current object
180
		 * @param string $object_type The type of object you are working with.
181
		 *	                           Usually `post` (this applies to all post-types).
182
		 *	                           Could also be `comment`, `user` or `options-page`.
183
		 * @param array  $cmb         This CMB2 object
184
		 */
185 1
		do_action( 'cmb2_before_form', $this->cmb_id, $object_id, $object_type, $this );
186
187
		/**
188
		 * Hook before form table begins
189
		 *
190
		 * The first dynamic portion of the hook name, $object_type, is the type of object
191
		 * you are working with. Usually `post` (this applies to all post-types).
192
		 * Could also be `comment`, `user` or `options-page`.
193
		 *
194
		 * The second dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
195
		 *
196
		 * @param array  $cmb_id      The current box ID
197
		 * @param int    $object_id   The ID of the current object
198
		 * @param array  $cmb         This CMB2 object
199
		 */
200 1
		do_action( "cmb2_before_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
201
202 1
		echo '<div class="', $this->box_classes(), '"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
203
204 1
	}
205
206
	/**
207
	 * Defines the classes for the CMB2 form/wrap.
208
	 *
209
	 * @since  2.0.0
210
	 * @return string Space concatenated list of classes
211
	 */
212 1
	public function box_classes() {
213
214 1
		$classes = array( 'cmb2-wrap', 'form-table' );
215
216
		// Use the callback to fetch classes.
217 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...
218 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
219 1
			$classes = array_merge( $classes, $added_classes );
220 1
		}
221
222 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...
223 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
224 1
			$classes = array_merge( $classes, $added_classes );
225 1
		}
226
227
		/**
228
		 * Globally filter box wrap classes
229
		 *
230
		 * @since 2.2.2
231
		 *
232
		 * @param string $classes Array of classes for the cmb2-wrap.
233
		 * @param CMB2   $cmb     This CMB2 object.
234
		 */
235 1
		$classes = apply_filters( 'cmb2_wrap_classes', $classes, $this );
236
237
		// Clean up.
238 1
		$classes = array_map( 'strip_tags', array_filter( $classes ) );
239
240
		// Make a string.
241 1
		return implode( ' ', $classes );
242
	}
243
244
	/**
245
	 * Outputs the closing form markup and runs corresponding hooks:
246
	 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
247
	 * @since  2.2.0
248
	 * @param  integer $object_id   Object ID
249
	 * @param  string  $object_type Object type
250
	 * @return void
251
	 */
252 1
	public function render_form_close( $object_id = 0, $object_type = '' ) {
253 1
		$object_type = $this->object_type( $object_type );
254 1
		$object_id = $this->object_id( $object_id );
255
256 1
		echo '</div></div>';
257
258 1
		$this->render_hidden_fields();
259
260
		/**
261
		 * Hook after form form has been rendered
262
		 *
263
		 * @param array  $cmb_id      The current box ID
264
		 * @param int    $object_id   The ID of the current object
265
		 * @param string $object_type The type of object you are working with.
266
		 *	                           Usually `post` (this applies to all post-types).
267
		 *	                           Could also be `comment`, `user` or `options-page`.
268
		 * @param array  $cmb         This CMB2 object
269
		 */
270 1
		do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
271
272
		/**
273
		 * Hook after form form has been rendered
274
		 *
275
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
276
		 *
277
		 * The first dynamic portion of the hook name, $object_type, is the type of object
278
		 * you are working with. Usually `post` (this applies to all post-types).
279
		 * Could also be `comment`, `user` or `options-page`.
280
		 *
281
		 * @param int    $object_id   The ID of the current object
282
		 * @param array  $cmb         This CMB2 object
283
		 */
284 1
		do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
285
286 1
		echo "\n<!-- End CMB2 Fields -->\n";
287
288 1
	}
289
290
	/**
291
	 * Renders a field based on the field type
292
	 * @since  2.2.0
293
	 * @param  array $field_args A field configuration array.
294
	 * @return mixed CMB2_Field object if successful.
295
	 */
296 1
	public function render_field( $field_args ) {
297 1
		$field_args['context'] = $this->prop( 'context' );
298
299 1
		if ( 'group' == $field_args['type'] ) {
300
301
			if ( ! isset( $field_args['show_names'] ) ) {
302
				$field_args['show_names'] = $this->prop( 'show_names' );
303
			}
304
			$field = $this->render_group( $field_args );
305
306 1
		} elseif ( 'hidden' == $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
307
			// Save rendering for after the metabox
308
			$field = $this->add_hidden_field( $field_args );
309
310
		} else {
311
312 1
			$field_args['show_names'] = $this->prop( 'show_names' );
313
314
			// Render default fields
315 1
			$field = $this->get_field( $field_args )->render_field();
316
		}
317
318 1
		return $field;
319
	}
320
321
	/**
322
	 * Render a repeatable group.
323
	 * @param array $args Array of field arguments for a group field parent.
324
	 * @return CMB2_Field|null Group field object.
325
	 */
326 2
	public function render_group( $args ) {
327
328 2
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
329
			return;
330
		}
331
332 2
		$field_group = $this->get_field( $args );
333
334
		// If field is requesting to be conditionally shown
335 2
		if ( ! $field_group || ! $field_group->should_show() ) {
336
			return;
337
		}
338
339 2
		$desc            = $field_group->args( 'description' );
340 2
		$label           = $field_group->args( 'name' );
341 2
		$sortable        = $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
342 2
		$repeat_class    = $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
343 2
		$group_val       = (array) $field_group->value();
344 2
		$nrows           = count( $group_val );
345 2
		$remove_disabled = $nrows <= 1 ? 'disabled="disabled" ' : '';
346 2
		$field_group->index = 0;
347
348
		$group_wrap_attributes = array(
349 2
			'class' => 'cmb-nested cmb-field-list cmb-repeatable-group' . $sortable . $repeat_class,
350 2
			'style' => 'width:100%;',
351 2
		);
352
353
		/**
354
		 * Allow for adding additional HTML attributes to a group wrapper.
355
		 *
356
		 * The attributes will be an array of key => value pairs for each attribute.
357
		 *
358
		 * @since 2.2.2
359
		 *
360
		 * @param string     $group_wrap_attributes Current attributes array.
361
		 *
362
		 * @param CMB2_Field $field_group           The group CMB2_Field object.
363
		 */
364 2
		$group_wrap_attributes = apply_filters( 'cmb2_group_wrap_attributes', $group_wrap_attributes, $field_group );
365
366 2
		$group_wrap_attributes = CMB2_Utils::concat_attrs( $group_wrap_attributes );
367
368 2
		$field_group->peform_param_callback( 'before_group' );
369
370 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" ', $group_wrap_attributes, '>';
371
372 2
		if ( $desc || $label ) {
373 2
			$class = $desc ? ' cmb-group-description' : '';
374 2
			echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
375 2
				if ( $label ) {
376 2
					echo '<h2 class="cmb-group-name">', $label, '</h2>';
377 2
				}
378 2
				if ( $desc ) {
379 1
					echo '<p class="cmb2-metabox-description">', $desc, '</p>';
380 1
				}
381 2
			echo '</div></div>';
382 2
		}
383
384 2
		if ( ! empty( $group_val ) ) {
385
386
			foreach ( $group_val as $group_key => $field_id ) {
387
				$this->render_group_row( $field_group, $remove_disabled );
388
				$field_group->index++;
389
			}
390
		} else {
391 2
			$this->render_group_row( $field_group, $remove_disabled );
392
		}
393
394 2
		if ( $field_group->args( 'repeatable' ) ) {
395 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>';
396 1
		}
397
398 2
		echo '</div></div></div>';
399
400 2
		$field_group->peform_param_callback( 'after_group' );
401
402 2
		return $field_group;
403
	}
404
405
	/**
406
	 * Render a repeatable group row
407
	 * @since  1.0.2
408
	 * @param  CMB2_Field $field_group  CMB2_Field group field object
409
	 * @param  string  $remove_disabled Attribute string to disable the remove button
410
	 */
411 2
	public function render_group_row( $field_group, $remove_disabled ) {
412
413 2
		$field_group->peform_param_callback( 'before_group_row' );
414 2
		$closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
415
416
		echo '
417 2
		<div class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
418
419 2
			if ( $field_group->args( 'repeatable' ) ) {
420 1
				echo '<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="dashicons-before dashicons-no-alt cmb-remove-group-row"></button>';
421 1
			}
422
423
			echo '
424 2
			<div class="cmbhandle" title="' , esc_attr__( 'Click to toggle', 'cmb2' ), '"><br></div>
425 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...
426
427
			<div class="inside cmb-td cmb-nested cmb-field-list">';
428
				// Loop and render repeatable group fields
429 2
				foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
430 2
					if ( 'hidden' == $field_args['type'] ) {
431
432
						// Save rendering for after the metabox
433
						$this->add_hidden_field( $field_args, $field_group );
434
435
					} else {
436
437 2
						$field_args['show_names'] = $field_group->args( 'show_names' );
438 2
						$field_args['context']    = $field_group->args( 'context' );
439
440 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...
441
					}
442 2
				}
443 2
				if ( $field_group->args( 'repeatable' ) ) {
444
					echo '
445
					<div class="cmb-row cmb-remove-field-row">
446
						<div class="cmb-remove-row">
447 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>
448
						</div>
449
					</div>
450
					';
451 1
				}
452
			echo '
453
			</div>
454
		</div>
455 2
		';
456
457 2
		$field_group->peform_param_callback( 'after_group_row' );
458 2
	}
459
460
	/**
461
	 * Add a hidden field to the list of hidden fields to be rendered later
462
	 * @since 2.0.0
463
	 * @param array  $field_args Array of field arguments to be passed to CMB2_Field
464
	 */
465
	public function add_hidden_field( $field_args, $field_group = null ) {
466
		if ( isset( $field_args['field_args'] ) ) {
467
			// For back-compatibility.
468
			$field = new CMB2_Field( $field_args );
469
		} else {
470
			$field = $this->get_new_field( $field_args, $field_group );
471
		}
472
473
		$type = new CMB2_Types( $field );
474
475
		if ( $field_group ) {
476
			$type->iterator = $field_group->index;
477
		}
478
479
		$this->hidden_fields[] = $type;
480
481
		return $field;
482
	}
483
484
	/**
485
	 * Loop through and output hidden fields
486
	 * @since  2.0.0
487
	 */
488 1
	public function render_hidden_fields() {
489 1
		if ( ! empty( $this->hidden_fields ) ) {
490
			foreach ( $this->hidden_fields as $hidden ) {
491
				$hidden->render();
492
			}
493
		}
494 1
	}
495
496
	/**
497
	 * Returns array of sanitized field values (without saving them)
498
	 * @since  2.0.3
499
	 * @param  array  $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
500
	 */
501 2
	public function get_sanitized_values( array $data_to_sanitize ) {
502 2
		$this->data_to_save = $data_to_sanitize;
503 2
		$stored_id          = $this->object_id();
504
505
		// We do this So CMB will sanitize our data for us, but not save it
506 2
		$this->object_id( '_' );
507
508
		// Ensure temp. data store is empty
509 2
		cmb2_options( 0 )->set();
510
511
		// Process/save fields
512 2
		$this->process_fields();
513
514
		// Get data from temp. data store
515 2
		$sanitized_values = cmb2_options( 0 )->get_options();
516
517
		// Empty out temp. data store again
518 2
		cmb2_options( 0 )->set();
519
520
		// Reset the object id
521 2
		$this->object_id( $stored_id );
522
523 2
		return $sanitized_values;
524
	}
525
526
	/**
527
	 * Loops through and saves field data
528
	 * @since  1.0.0
529
	 * @param  int    $object_id    Object ID
530
	 * @param  string $object_type  Type of object being saved. (e.g., post, user, or comment)
531
	 * @param  array  $data_to_save Array of key => value data for saving. Likely $_POST data.
532
	 */
533 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...
534
535
		// Fall-back to $_POST data
536 1
		$this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
537 1
		$object_id = $this->object_id( $object_id );
538 1
		$object_type = $this->object_type( $object_type );
539
540 1
		$this->process_fields();
541
542
		// If options page, save the updated options
543 1
		if ( 'options-page' == $object_type ) {
544 1
			cmb2_options( $object_id )->set();
545 1
		}
546
547 1
		$this->after_save();
548 1
	}
549
550
	/**
551
	 * Process and save form fields
552
	 * @since  2.0.0
553
	 */
554 3
	public function process_fields() {
555
556 3
		$this->pre_process();
557
558
		// Remove the show_on properties so saving works
559 3
		$this->prop( 'show_on', array() );
560
561
		// save field ids of those that are updated
562 3
		$this->updated = array();
563
564 3
		foreach ( $this->prop( 'fields' ) as $field_args ) {
565 3
			$this->process_field( $field_args );
566 3
		}
567 3
	}
568
569
	/**
570
	 * Process and save a field
571
	 * @since  2.0.0
572
	 * @param  array  $field_args Array of field arguments
573
	 */
574 3
	public function process_field( $field_args ) {
575
576 3
		switch ( $field_args['type'] ) {
577
578 3
			case 'group':
579 1
				if ( $this->save_group( $field_args ) ) {
580 1
					$this->updated[] = $field_args['id'];
581 1
				}
582
583 1
				break;
584
585 2
			case 'title':
586
				// Don't process title fields
587
				break;
588
589 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...
590
591 2
				$field = $this->get_new_field( $field_args );
592
593 2
				if ( $field->save_field_from_data( $this->data_to_save ) ) {
594 2
					$this->updated[] = $field->id();
595 2
				}
596
597 2
				break;
598 3
		}
599
600 3
	}
601
602 3
	public function pre_process() {
603
		/**
604
		 * Fires before fields have been processed/saved.
605
		 *
606
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
607
		 *
608
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
609
		 * 	Usually `post` (this applies to all post-types).
610
		 *  	Could also be `comment`, `user` or `options-page`.
611
		 *
612
		 * @param array $cmb       This CMB2 object
613
		 * @param int   $object_id The ID of the current object
614
		 */
615 3
		do_action( "cmb2_{$this->object_type()}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
616 3
	}
617
618 1
	public function after_save() {
619 1
		$object_type = $this->object_type();
620 1
		$object_id   = $this->object_id();
621
622
		/**
623
		 * Fires after all fields have been saved.
624
		 *
625
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
626
		 * 	Usually `post` (this applies to all post-types).
627
		 *  	Could also be `comment`, `user` or `options-page`.
628
		 *
629
		 * @param int    $object_id   The ID of the current object
630
		 * @param array  $cmb_id      The current box ID
631
		 * @param string $updated     Array of field ids that were updated.
632
		 *                            Will only include field ids that had values change.
633
		 * @param array  $cmb         This CMB2 object
634
		 */
635 1
		do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
636
637
		/**
638
		 * Fires after all fields have been saved.
639
		 *
640
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
641
		 *
642
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
643
		 * 	Usually `post` (this applies to all post-types).
644
		 *  	Could also be `comment`, `user` or `options-page`.
645
		 *
646
		 * @param int    $object_id   The ID of the current object
647
		 * @param string $updated     Array of field ids that were updated.
648
		 *                            Will only include field ids that had values change.
649
		 * @param array  $cmb         This CMB2 object
650
		 */
651 1
		do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
652 1
	}
653
654
	/**
655
	 * Save a repeatable group
656
	 * @since  1.x.x
657
	 * @param  array  $args Field arguments array
658
	 * @return mixed        Return of CMB2_Field::update_data()
659
	 */
660 1
	public function save_group( $args ) {
661 1
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
662
			return;
663
		}
664
665 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...
666
	}
667
668
	/**
669
	 * Save a repeatable group
670
	 * @since  1.x.x
671
	 * @param  array $field_group CMB2_Field group field object
672
	 * @return mixed              Return of CMB2_Field::update_data()
673
	 */
674 1
	public function save_group_field( $field_group ) {
675 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...
676
677 1
		if ( ! isset( $this->data_to_save[ $base_id ] ) ) {
678
			return;
679
		}
680
681 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...
682
		// Check if group field has sanitization_cb
683 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...
684 1
		$saved      = array();
685
686 1
		$field_group->index = 0;
687 1
		$field_group->data_to_save = $this->data_to_save;
688
689 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...
690
691 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...
692 1
			$sub_id = $field->id( true );
693
694 1
			foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
695
696
				// Get value
697 1
				$new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
698 1
					? $group_vals[ $field_group->index ][ $sub_id ]
699 1
					: false;
700
701
				// Sanitize
702 1
				$new_val = $field->sanitization_cb( $new_val );
703
704 1
				if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
705 1
					if ( $field->args( 'repeatable' ) ) {
706 1
						$_new_val = array();
707 1
						foreach ( $new_val as $group_index => $grouped_data ) {
708
							// Add the supporting data to the $saved array stack
709 1
							$saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
710
							// Reset var to the actual value
711 1
							$_new_val[ $group_index ] = $grouped_data['value'];
712 1
						}
713 1
						$new_val = $_new_val;
714 1
					} else {
715
						// Add the supporting data to the $saved array stack
716 1
						$saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
717
						// Reset var to the actual value
718 1
						$new_val = $new_val['value'];
719
					}
720 1
				}
721
722
				// Get old value
723 1
				$old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
724 1
					? $old[ $field_group->index ][ $sub_id ]
725 1
					: false;
726
727 1
				$is_updated = ( ! CMB2_Utils::isempty( $new_val ) && $new_val !== $old_val );
728 1
				$is_removed = ( CMB2_Utils::isempty( $new_val ) && ! CMB2_Utils::isempty( $old_val ) );
729
730
				// Compare values and add to `$updated` array
731 1
				if ( $is_updated || $is_removed ) {
732 1
					$this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
733 1
				}
734
735
				// 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...
736 1
				$saved[ $field_group->index ][ $sub_id ] = $new_val;
737
738 1
			}
739
740 1
			$saved[ $field_group->index ] = CMB2_Utils::filter_empty( $saved[ $field_group->index ] );
741 1
		}
742
743 1
		$saved = CMB2_Utils::filter_empty( $saved );
744
745 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...
746
	}
747
748
	/**
749
	 * Get object id from global space if no id is provided
750
	 * @since  1.0.0
751
	 * @param  integer $object_id Object ID
752
	 * @return integer $object_id Object ID
753
	 */
754 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...
755 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...
756
757 48
		if ( $object_id ) {
758 18
			$this->object_id = $object_id;
759 18
			return $this->object_id;
760
		}
761
762 45
		if ( $this->object_id ) {
763 13
			return $this->object_id;
764
		}
765
766
		// Try to get our object ID from the global space
767 42
		switch ( $this->object_type() ) {
768 42
			case 'user':
769
				$object_id = isset( $_REQUEST['user_id'] ) ? $_REQUEST['user_id'] : $object_id;
770
				$object_id = ! $object_id && 'user-new.php' != $pagenow && isset( $GLOBALS['user_ID'] ) ? $GLOBALS['user_ID'] : $object_id;
771
				break;
772
773 42
			case 'comment':
774
				$object_id = isset( $_REQUEST['c'] ) ? $_REQUEST['c'] : $object_id;
775
				$object_id = ! $object_id && isset( $GLOBALS['comments']->comment_ID ) ? $GLOBALS['comments']->comment_ID : $object_id;
776
				break;
777
778 42
			case 'term':
779
				$object_id = isset( $_REQUEST['tag_ID'] ) ? $_REQUEST['tag_ID'] : $object_id;
780
				break;
781
782 42
			default:
783 42
				$object_id = isset( $GLOBALS['post']->ID ) ? $GLOBALS['post']->ID : $object_id;
784 42
				$object_id = isset( $_REQUEST['post'] ) ? $_REQUEST['post'] : $object_id;
785 42
				break;
786 42
		}
787
788
		// reset to id or 0
789 42
		$this->object_id = $object_id ? $object_id : 0;
790
791 42
		return $this->object_id;
792
	}
793
794
	/**
795
	 * Sets the $object_type based on metabox settings
796
	 * @since  1.0.0
797
	 * @return string Object type
798
	 */
799 44
	public function mb_object_type() {
800 44
		if ( null !== $this->mb_object_type ) {
801 12
			return $this->mb_object_type;
802
		}
803
804 44
		if ( $this->is_options_page_mb() ) {
805 36
			$this->mb_object_type = 'options-page';
806 36
			return $this->mb_object_type;
807
		}
808
809 43
		$registered_types = $this->prop( 'object_types' );
810
811 43
		if ( ! $registered_types ) {
812 40
			$this->mb_object_type = 'post';
813 40
			return $this->mb_object_type;
814
		}
815
816 4
		$type = false;
817
818
		// check if 'object_types' is a string
819 4
		if ( is_string( $registered_types ) ) {
820
			$type = $registered_types;
821
		}
822
823
		// if it's an array of one, extract it
824 4
		elseif ( is_array( $registered_types ) && 1 === count( $registered_types ) ) {
825 4
			$last = end( $registered_types );
826 4
			if ( is_string( $last ) ) {
827 4
				$type = $last;
828 4
			}
829 4
		} elseif ( is_array( $registered_types ) ) {
830
			$page_type = $this->current_object_type();
831
832
			if ( in_array( $page_type, $registered_types, true ) ) {
833
				$type = $page_type;
834
			}
835
		}
836
837
		// Get our object type
838
		switch ( $type ) {
839
840 4
			case 'user':
841 4
			case 'comment':
842 4
			case 'term':
843 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...
844 1
				break;
845
846 3
			default:
847 3
				$this->mb_object_type = 'post';
848 3
				break;
849 3
		}
850
851 4
		return $this->mb_object_type;
852
	}
853
854
	/**
855
	 * Determines if metabox is for an options page
856
	 * @since  1.0.1
857
	 * @return boolean True/False
858
	 */
859 44
	public function is_options_page_mb() {
860 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'] ) );
861
	}
862
863
	/**
864
	 * Returns the object type
865
	 * @since  1.0.0
866
	 * @return string Object type
867
	 */
868 48
	public function object_type( $object_type = '' ) {
869 48
		if ( $object_type ) {
870 18
			$this->object_type = $object_type;
871 18
			return $this->object_type;
872
		}
873
874 45
		if ( $this->object_type ) {
875 45
			return $this->object_type;
876
		}
877
878
		$this->object_type = $this->current_object_type();
879
880
		return $this->object_type;
881
	}
882
883
	/**
884
	 * Get the object type for the current page, based on the $pagenow global.
885
	 * @since  2.2.2
886
	 * @return string  Page object type name.
887
	 */
888 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...
889
		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...
890
		$type = 'post';
891
892
		if ( in_array( $pagenow, array( 'user-edit.php', 'profile.php', 'user-new.php' ), true ) ) {
893
			$type = 'user';
894
		}
895
896
		if ( in_array( $pagenow, array( 'edit-comments.php', 'comment.php' ), true ) ) {
897
			$type = 'comment';
898
		}
899
900
		if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
901
			$type = 'term';
902
		}
903
904
		return $type;
905
	}
906
907
	/**
908
	 * Set metabox property.
909
	 * @since  2.2.2
910
	 * @param  string $property Metabox config property to retrieve
911
	 * @param  mixed  $value    Value to set if no value found
912
	 * @return mixed            Metabox config property value or false
913
	 */
914 1
	public function set_prop( $property, $value ) {
915 1
		$this->meta_box[ $property ] = $value;
916
917 1
		return $this->prop( $property );
918
	}
919
920
	/**
921
	 * Get metabox property and optionally set a fallback
922
	 * @since  2.0.0
923
	 * @param  string $property Metabox config property to retrieve
924
	 * @param  mixed  $fallback Fallback value to set if no value found
925
	 * @return mixed            Metabox config property value or false
926
	 */
927 44
	public function prop( $property, $fallback = null ) {
928 44
		if ( array_key_exists( $property, $this->meta_box ) ) {
929 44
			return $this->meta_box[ $property ];
930 1
		} elseif ( $fallback ) {
931 1
			return $this->meta_box[ $property ] = $fallback;
932
		}
933
	}
934
935
	/**
936
	 * Get a field object
937
	 * @since  2.0.3
938
	 * @param  string|array|CMB2_Field $field       Metabox field id or field config array or CMB2_Field object
939
	 * @param  CMB2_Field              $field_group (optional) CMB2_Field object (group parent)
940
	 * @return CMB2_Field|false CMB2_Field object (or false)
941
	 */
942 15
	public function get_field( $field, $field_group = null ) {
943 15
		if ( is_a( $field, 'CMB2_Field' ) ) {
944
			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...
945
		}
946
947 15
		$field_id = is_string( $field ) ? $field : $field['id'];
948
949 15
		$parent_field_id = ! empty( $field_group ) ? $field_group->id() : '';
950 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...
951
952 15
		if ( ! $ids ) {
953
			return false;
954
		}
955
956 15
		list( $field_id, $sub_field_id ) = $ids;
957
958 15
		$index = implode( '', $ids ) . ( $field_group ? $field_group->index : '' );
959 15
		if ( array_key_exists( $index, $this->fields ) ) {
960 4
			return $this->fields[ $index ];
961
		}
962
963 13
		$this->fields[ $index ] = new CMB2_Field( $this->get_field_args( $field_id, $field, $sub_field_id, $field_group ) );
964
965 13
		return $this->fields[ $index ];
966
	}
967
968
	/**
969
	 * Handles determining which type of arguments to pass to CMB2_Field
970
	 * @since  2.0.7
971
	 * @param  mixed  $field_id     Field (or group field) ID
972
	 * @param  mixed  $field_args   Array of field arguments
973
	 * @param  mixed  $sub_field_id Sub field ID (if field_group exists)
974
	 * @param  mixed  $field_group  If a sub-field, will be the parent group CMB2_Field object
975
	 * @return array                Array of CMB2_Field arguments
976
	 */
977 13
	public function get_field_args( $field_id, $field_args, $sub_field_id, $field_group ) {
978
979
		// Check if group is passed and if fields were added in the old-school fields array
980 13
		if ( $field_group && ( $sub_field_id || 0 === $sub_field_id ) ) {
981
982
			// Update the fields array w/ any modified properties inherited from the group field
983 2
			$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] = $field_args;
984
985 2
			return $this->get_default_args( $field_args, $field_group );
986
		}
987
988 13
		if ( is_array( $field_args ) ) {
989 2
			$this->meta_box['fields'][ $field_id ] = array_merge( $field_args, $this->meta_box['fields'][ $field_id ] );
990 2
		}
991
992 13
		return $this->get_default_args( $this->meta_box['fields'][ $field_id ] );
993
	}
994
995
	/**
996
	 * Get default field arguments specific to this CMB2 object.
997
	 * @since  2.2.0
998
	 * @param  array      $field_args  Metabox field config array.
999
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
1000
	 * @return array                   Array of field arguments.
1001
	 */
1002 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...
1003 17
		if ( $field_group ) {
1004
			$args = array(
1005 3
				'field_args'  => $field_args,
1006 3
				'group_field' => $field_group,
1007 3
			);
1008 3
		} else {
1009
			$args = array(
1010 17
				'field_args'  => $field_args,
1011 17
				'object_type' => $this->object_type(),
1012 17
				'object_id'   => $this->object_id(),
1013 17
				'cmb_id'      => $this->cmb_id,
1014 17
			);
1015
		}
1016
1017 17
		return $args;
1018
	}
1019
1020
	/**
1021
	 * Get a new field object specific to this CMB2 object.
1022
	 * @since  2.2.0
1023
	 * @param  array      $field_args  Metabox field config array.
1024
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
1025
	 * @return CMB2_Field CMB2_Field object
1026
	 */
1027 5
	protected function get_new_field( $field_args, $field_group = null ) {
1028 5
		return new CMB2_Field( $this->get_default_args( $field_args, $field_group ) );
1029
	}
1030
1031
	/**
1032
	 * When fields are added in the old-school way, intitate them as they should be
1033
	 * @since 2.1.0
1034
	 * @param array $fields          Array of fields to add
1035
	 * @param mixed $parent_field_id Parent field id or null
1036
	 */
1037 41
	protected function add_fields( $fields, $parent_field_id = null ) {
1038 41
		foreach ( $fields as $field ) {
1039
1040 41
			$sub_fields = false;
1041 41
			if ( array_key_exists( 'fields', $field ) ) {
1042
				$sub_fields = $field['fields'];
1043
				unset( $field['fields'] );
1044
			}
1045
1046
			$field_id = $parent_field_id
1047 41
				? $this->add_group_field( $parent_field_id, $field )
1048 41
				: $this->add_field( $field );
1049
1050 41
			if ( $sub_fields ) {
1051
				$this->add_fields( $sub_fields, $field_id );
1052
			}
1053 41
		}
1054 41
	}
1055
1056
	/**
1057
	 * Add a field to the metabox
1058
	 * @since  2.0.0
1059
	 * @param  array  $field           Metabox field config array
1060
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1061
	 * @return mixed                   Field id or false
1062
	 */
1063 43
	public function add_field( array $field, $position = 0 ) {
1064 43
		if ( ! is_array( $field ) || ! array_key_exists( 'id', $field ) ) {
1065
			return false;
1066
		}
1067
1068 43
		if ( 'oembed' === $field['type'] ) {
1069
			// Initiate oembed Ajax hooks
1070 1
			cmb2_ajax();
1071 1
		}
1072
1073 43
		if ( isset( $field['column'] ) && false !== $field['column'] ) {
1074
			$field = $this->define_field_column( $field );
1075
		}
1076
1077 43
		if ( isset( $field['taxonomy'] ) && ! empty( $field['remove_default'] ) ) {
1078
			$this->tax_metaboxes_to_remove[ $field['taxonomy'] ] = $field['taxonomy'];
1079
		}
1080
1081 43
		$this->_add_field_to_array(
1082 43
			$field,
1083 43
			$this->meta_box['fields'],
1084
			$position
1085 43
		);
1086
1087 43
		return $field['id'];
1088
	}
1089
1090
	/**
1091
	 * Defines a field's column if requesting to be show in admin columns.
1092
	 * @since  2.2.3
1093
	 * @param  array  $field Metabox field config array.
1094
	 * @return array         Modified metabox field config array.
1095
	 */
1096
	protected function define_field_column( array $field ) {
1097
		$this->has_columns = true;
1098
1099
		$column = is_array( $field['column'] ) ? $field['column'] : array();
1100
1101
		$field['column'] = wp_parse_args( $column, array(
1102
			'name'     => isset( $field['name'] ) ? $field['name'] : '',
1103
			'position' => false,
1104
		) );
1105
1106
		return $field;
1107
	}
1108
1109
	/**
1110
	 * Add a field to a group
1111
	 * @since  2.0.0
1112
	 * @param  string $parent_field_id The field id of the group field to add the field
1113
	 * @param  array  $field           Metabox field config array
1114
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1115
	 * @return mixed                   Array of parent/field ids or false
1116
	 */
1117 3
	public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
1118 3
		if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
1119
			return false;
1120
		}
1121
1122 3
		$parent_field = $this->meta_box['fields'][ $parent_field_id ];
1123
1124 3
		if ( 'group' !== $parent_field['type'] ) {
1125
			return false;
1126
		}
1127
1128 3
		if ( ! isset( $parent_field['fields'] ) ) {
1129 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
1130 3
		}
1131
1132 3
		$this->_add_field_to_array(
1133 3
			$field,
1134 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'],
1135
			$position
1136 3
		);
1137
1138 3
		return array( $parent_field_id, $field['id'] );
1139
	}
1140
1141
	/**
1142
	 * Add a field array to a fields array in desired position
1143
	 * @since 2.0.2
1144
	 * @param array   $field    Metabox field config array
1145
	 * @param array   &$fields  Array (passed by reference) to append the field (array) to
1146
	 * @param integer $position Optionally specify a position in the array to be inserted
1147
	 */
1148 43
	protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1149 43
		if ( $position ) {
1150 1
			CMB2_Utils::array_insert( $fields, array( $field['id'] => $field ), $position );
1151 1
		} else {
1152 43
			$fields[ $field['id'] ] = $field;
1153
		}
1154 43
	}
1155
1156
	/**
1157
	 * Remove a field from the metabox
1158
	 * @since 2.0.0
1159
	 * @param  string $field_id        The field id of the field to remove
1160
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1161
	 * @return bool                    True if field was removed
1162
	 */
1163 2
	public function remove_field( $field_id, $parent_field_id = '' ) {
1164 2
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1165
1166 2
		if ( ! $ids ) {
1167
			return false;
1168
		}
1169
1170 2
		list( $field_id, $sub_field_id ) = $ids;
1171
1172 2
		unset( $this->fields[ implode( '', $ids ) ] );
1173
1174 2
		if ( ! $sub_field_id ) {
1175 1
			unset( $this->meta_box['fields'][ $field_id ] );
1176 1
			return true;
1177
		}
1178
1179 1
		if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1180 1
			unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1181 1
		}
1182 1
		if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1183 1
			unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1184 1
		}
1185 1
		return true;
1186
	}
1187
1188
	/**
1189
	 * Update or add a property to a field
1190
	 * @since  2.0.0
1191
	 * @param  string $field_id        Field id
1192
	 * @param  string $property        Field property to set/update
1193
	 * @param  mixed  $value           Value to set the field property
1194
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1195
	 * @return mixed                   Field id. Strict compare to false, as success can return a falsey value (like 0)
1196
	 */
1197 4
	public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1198 4
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1199
1200 4
		if ( ! $ids ) {
1201 2
			return false;
1202
		}
1203
1204 2
		list( $field_id, $sub_field_id ) = $ids;
1205
1206 2
		if ( ! $sub_field_id ) {
1207 2
			$this->meta_box['fields'][ $field_id ][ $property ] = $value;
1208 2
			return $field_id;
1209
		}
1210
1211
		$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1212
		return $field_id;
1213
	}
1214
1215
	/**
1216
	 * Check if field ids match a field and return the index/field id
1217
	 * @since  2.0.2
1218
	 * @param  string  $field_id        Field id
1219
	 * @param  string  $parent_field_id (optional) Parent field id
1220
	 * @return mixed                    Array of field/parent ids, or false
1221
	 */
1222 19
	public function get_field_ids( $field_id, $parent_field_id = '' ) {
1223 19
		$sub_field_id = $parent_field_id ? $field_id : '';
1224 19
		$field_id     = $parent_field_id ? $parent_field_id : $field_id;
1225 19
		$fields       =& $this->meta_box['fields'];
1226
1227 19
		if ( ! array_key_exists( $field_id, $fields ) ) {
1228 2
			$field_id = $this->search_old_school_array( $field_id, $fields );
1229 2
		}
1230
1231 19
		if ( false === $field_id ) {
1232 2
			return false;
1233
		}
1234
1235 17
		if ( ! $sub_field_id ) {
1236 17
			return array( $field_id, $sub_field_id );
1237
		}
1238
1239 3
		if ( 'group' !== $fields[ $field_id ]['type'] ) {
1240
			return false;
1241
		}
1242
1243 3
		if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1244
			$sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1245
		}
1246
1247 3
		return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1248
	}
1249
1250
	/**
1251
	 * When using the old array filter, it is unlikely field array indexes will be the field id
1252
	 * @since  2.0.2
1253
	 * @param  string $field_id The field id
1254
	 * @param  array  $fields   Array of fields to search
1255
	 * @return mixed            Field index or false
1256
	 */
1257 2
	public function search_old_school_array( $field_id, $fields ) {
1258 2
		$ids = wp_list_pluck( $fields, 'id' );
1259 2
		$index = array_search( $field_id, $ids );
1260 2
		return false !== $index ? $index : false;
1261
	}
1262
1263
	/**
1264
	 * Handles metabox property callbacks, and passes this $cmb object as property.
1265
	 * @since  2.2.3
1266
	 * @param  callable $cb The callback method/function/closure
1267
	 * @return mixed        Return of the callback function.
1268
	 */
1269 1
	protected function do_callback( $cb ) {
1270 1
		return call_user_func( $cb, $this );
1271
	}
1272
1273
	/**
1274
	 * Generate a unique nonce field for each registered meta_box
1275
	 * @since  2.0.0
1276
	 * @return string unique nonce hidden input
1277
	 */
1278 1
	public function nonce_field() {
1279 1
		wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1280 1
	}
1281
1282
	/**
1283
	 * Generate a unique nonce for each registered meta_box
1284
	 * @since  2.0.0
1285
	 * @return string unique nonce string
1286
	 */
1287 1
	public function nonce() {
1288 1
		if ( $this->generated_nonce ) {
1289 1
			return $this->generated_nonce;
1290
		}
1291 1
		$this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1292 1
		return $this->generated_nonce;
1293
	}
1294
1295
	/**
1296
	 * Magic getter for our object.
1297
	 * @param string $field
1298
	 * @throws Exception Throws an exception if the field is invalid.
1299
	 * @return mixed
1300
	 */
1301 44
	public function __get( $field ) {
1302
		switch ( $field ) {
1303 44
			case 'updated':
1304 44
			case 'has_columns':
1305 44
			case 'tax_metaboxes_to_remove':
1306 1
				return $this->{$field};
1307 44
			default:
1308 44
				return parent::__get( $field );
1309 44
		}
1310
	}
1311
1312
}
1313