Completed
Pull Request — trunk (#588)
by Juliette
05:59
created

CMB2_Field   D

Complexity

Total Complexity 142

Size/Duplication

Total Lines 1072
Duplicated Lines 1.03 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 87.37%
Metric Value
wmc 142
lcom 1
cbo 4
dl 11
loc 1072
ccs 325
cts 372
cp 0.8737
rs 4.4192

34 Methods

Rating   Name   Duplication   Size   Complexity  
A __call() 0 4 2
A id() 0 4 2
A args() 0 14 4
A value() 0 3 1
A val_or_default() 0 3 2
A field_timezone_offset() 0 3 1
A format_timestamp() 0 3 1
A get_timestamp_from_value() 0 3 1
A peform_param_callback() 0 3 1
A replace_hash() 0 4 1
B __construct() 0 17 6
A _data() 0 7 3
C get_data() 0 63 9
C update_data() 0 64 8
A remove_data() 0 56 4
A data_args() 0 10 1
B sanitization_cb() 0 43 6
A save_field_from_data() 0 9 2
C save_field() 0 65 12
B maybe_callback() 0 18 6
A escaping_exception() 0 8 1
B repeatable_exception() 0 30 1
D escaped_value() 0 40 10
A field_timezone() 0 14 3
A get_timestamp_format() 0 12 4
A render_field() 0 7 2
B render_field_callback() 0 44 6
A label() 0 9 3
B row_classes() 0 48 6
A should_show() 11 11 2
B get_param_callback_result() 0 28 5
D options() 0 25 9
F _set_field_defaults() 0 88 16
A maybe_set_attributes() 0 3 1

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_Field 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_Field, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CMB2 field objects
4
 *
5
 * @since  1.1.0
6
 *
7
 * @category  WordPress_Plugin
8
 * @package   CMB2
9
 * @author    WebDevStudios
10
 * @license   GPL-2.0+
11
 * @link      http://webdevstudios.com
12
 *
13
 * @method string _id()
14
 * @method string type()
15
 * @method mixed fields()
16
 */
17
class CMB2_Field {
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
	 * Metabox object id
21
	 * @var   mixed
22
	 * @since 1.1.0
23
	 */
24
	public $object_id = null;
25
26
	/**
27
	 * Metabox object type
28
	 * @var   string
29
	 * @since 1.1.0
30
	 */
31
	public $object_type = '';
32
33
	/**
34
	 * Field arguments
35
	 * @var   mixed
36
	 * @since 1.1.0
37
	 */
38
	public $args = array();
39
40
	/**
41
	 * Field group object or false (if no group)
42
	 * @var   mixed
43
	 * @since 1.1.0
44
	 */
45
	public $group = false;
46
47
	/**
48
	 * Field meta value
49
	 * @var   mixed
50
	 * @since 1.1.0
51
	 */
52
	public $value = null;
53
54
	/**
55
	 * Field meta value
56
	 * @var   mixed
57
	 * @since 1.1.0
58
	 */
59
	public $escaped_value = null;
60
61
	/**
62
	 * Grouped Field's current numeric index during the save process
63
	 * @var   mixed
64
	 * @since 2.0.0
65
	 */
66
	public $index = 0;
67
68
	/**
69
	 * Array of field options
70
	 * @var   array
71
	 * @since 2.0.0
72
	 */
73
	protected $field_options = array();
74
75
	/**
76
	 * Array of field param callback results
77
	 * @var   array
78
	 * @since 2.0.0
79
	 */
80
	protected $callback_results = array();
81
82
	/**
83
	 * Array of key => value data for saving. Likely $_POST data.
84
	 * @var   array
85
	 * @since 2.0.0
86
	 */
87
	public $data_to_save = array();
88
89
	/**
90
	 * Constructs our field object
91
	 * @since 1.1.0
92
	 * @param array $args Field arguments
93
	 */
94 69
	public function __construct( $args ) {
95
96 69
		if ( ! empty( $args['group_field'] ) ) {
97 3
			$this->group       = $args['group_field'];
98 3
			$this->object_id   = $this->group->object_id;
99 3
			$this->object_type = $this->group->object_type;
100 3
		} else {
101 69
			$this->object_id   = isset( $args['object_id'] ) && '_' !== $args['object_id'] ? $args['object_id'] : 0;
102 69
			$this->object_type = isset( $args['object_type'] ) ? $args['object_type'] : 'post';
103
		}
104
105 69
		$this->args = $this->_set_field_defaults( $args['field_args'] );
106
107 69
		if ( $this->object_id ) {
108 63
			$this->value = $this->get_data();
109 63
		}
110 69
	}
111
112
	/**
113
	 * Non-existent methods fallback to checking for field arguments of the same name
114
	 * @since  1.1.0
115
	 * @param  string $name     Method name
116
	 * @param  array  $arguments Array of passed-in arguments
117
	 * @return mixed             Value of field argument
118
	 */
119 59
	public function __call( $name, $arguments ) {
120 59
		$key = isset( $arguments[0] ) ? $arguments[0] : false;
121 59
		return $this->args( $name, $key );
122
	}
123
124
	/**
125
	 * Retrieves the field id
126
	 * @since  1.1.0
127
	 * @param  boolean $raw Whether to retrieve pre-modidifed id
128
	 * @return string       Field id
129
	 */
130 67
	public function id( $raw = false ) {
131 67
		$id = $raw ? '_id' : 'id';
132 67
		return $this->args( $id );
133
	}
134
135
	/**
136
	 * Get a field argument
137
	 * @since  1.1.0
138
	 * @param  string $key  Argument to check
139
	 * @param  string $_key Sub argument to check
140
	 * @return mixed        Argument value or false if non-existent
141
	 */
142 70
	public function args( $key = '', $_key = '' ) {
143 70
		$arg = $this->_data( 'args', $key );
144
145 70
		if ( 'default' == $key ) {
146
147 13
			$arg = $this->get_param_callback_result( 'default', false );
148
149 70
		} elseif ( $_key ) {
150
151
			$arg = isset( $arg[ $_key ] ) ? $arg[ $_key ] : false;
152
		}
153
154 70
		return $arg;
155
	}
156
157
	/**
158
	 * Retrieve a portion of a field property
159
	 * @since  1.1.0
160
	 * @param  string  $var Field property to check
161
	 * @param  string  $key Field property array key to check
162
	 * @return mixed        Queried property value or false
163
	 */
164 70
	public function _data( $var, $key = '' ) {
165 70
		$vars = $this->$var;
166 70
		if ( $key ) {
167 70
			return isset( $vars[ $key ] ) ? $vars[ $key ] : false;
168
		}
169 60
		return $vars;
170
	}
171
172
	/**
173
	 * Get Field's value
174
	 * @since  1.1.0
175
	 * @param  string $key If value is an array, is used to get array key->value
176
	 * @return mixed       Field value or false if non-existent
177
	 */
178 44
	public function value( $key = '' ) {
179 44
		return $this->_data( 'value', $key );
180
	}
181
182
	/**
183
	 * Retrieves metadata/option data
184
	 * @since  1.0.1
185
	 * @param  string $field_id Meta key/Option array key
186
	 * @param  array  $args     Override arguments
187
	 * @return mixed            Meta/Option value
188
	 */
189 65
	public function get_data( $field_id = '', $args = array() ) {
190 65
		if ( $field_id ) {
191
			$args['field_id'] = $field_id;
192 65
		} else if ( $this->group ) {
193
			$args['field_id'] = $this->group->id();
194
		}
195
196 65
		$a = $this->data_args( $args );
197
198
		/**
199
		 * Filter whether to override getting of meta value.
200
		 * Returning a non 'cmb2_field_no_override_val' value
201
		 * will effectively short-circuit the value retrieval.
202
		 *
203
		 * @since 2.0.0
204
		 *
205
		 * @param mixed $value     The value get_metadata() should
206
		 *                         return - a single metadata value,
207
		 *                         or an array of values.
208
		 *
209
		 * @param int   $object_id Object ID.
210
		 *
211
		 * @param array $args {
212
		 *     An array of arguments for retrieving data
213
		 *
214
		 *     @type string $type     The current object type
215
		 *     @type int    $id       The current object ID
216
		 *     @type string $field_id The ID of the field being requested
217
		 *     @type bool   $repeat   Whether current field is repeatable
218
		 *     @type bool   $single   Whether current field is a single database row
219
		 * }
220
		 *
221
		 * @param CMB2_Field object $field This field object
222
		 */
223 65
		$data = apply_filters( 'cmb2_override_meta_value', 'cmb2_field_no_override_val', $this->object_id, $a, $this );
224
225
		/**
226
		 * Filter and parameters are documented for 'cmb2_override_meta_value' filter (above).
227
		 *
228
		 * The dynamic portion of the hook, $field_id, refers to the current
229
		 * field id paramater. Returning a non 'cmb2_field_no_override_val' value
230
		 * will effectively short-circuit the value retrieval.
231
		 *
232
		 * @since 2.0.0
233
		 */
234 65
		$data = apply_filters( "cmb2_override_{$a['field_id']}_meta_value", $data, $this->object_id, $a, $this );
235
236
		// If no override, get value normally
237 65
		if ( 'cmb2_field_no_override_val' === $data ) {
238 65
			$data = 'options-page' === $a['type']
239 65
				? cmb2_options( $a['id'] )->get( $a['field_id'] )
240 65
				: get_metadata( $a['type'], $a['id'], $a['field_id'], ( $a['single'] || $a['repeat'] ) );
241 65
		}
242
243 65
		if ( $this->group ) {
244
245
			$data = is_array( $data ) && isset( $data[ $this->group->index ][ $this->args( '_id' ) ] )
246
				? $data[ $this->group->index ][ $this->args( '_id' ) ]
247
				: false;
248
		}
249
250 65
		return $data;
251
	}
252
253
	/**
254
	 * Updates metadata/option data
255
	 * @since  1.0.1
256
	 * @param  mixed $new_value Value to update data with
257
	 * @param  bool  $single    Whether data is an array (add_metadata)
258
	 */
259 8
	public function update_data( $new_value, $single = true ) {
260 8
		$a = $this->data_args( array( 'single' => $single ) );
261
262 8
		$a['value'] = $a['repeat'] ? array_values( $new_value ) : $new_value;
263
264
		/**
265
		 * Filter whether to override saving of meta value.
266
		 * Returning a non-null value will effectively short-circuit the function.
267
		 *
268
		 * @since 2.0.0
269
		 *
270
		 * @param null|bool $check  Whether to allow updating metadata for the given type.
271
		 *
272
		 * @param array $args {
273
		 *     Array of data about current field including:
274
		 *
275
		 *     @type string $value    The value to set
276
		 *     @type string $type     The current object type
277
		 *     @type int    $id       The current object ID
278
		 *     @type string $field_id The ID of the field being updated
279
		 *     @type bool   $repeat   Whether current field is repeatable
280
		 *     @type bool   $single   Whether current field is a single database row
281
		 * }
282
		 *
283
		 * @param array $field_args All field arguments
284
		 *
285
		 * @param CMB2_Field object $field This field object
286
		 */
287 8
		$override = apply_filters( 'cmb2_override_meta_save', null, $a, $this->args(), $this );
288
289
		/**
290
		 * Filter and parameters are documented for 'cmb2_override_meta_save' filter (above).
291
		 *
292
		 * The dynamic portion of the hook, $a['field_id'], refers to the current
293
		 * field id paramater. Returning a non-null value
294
		 * will effectively short-circuit the function.
295
		 *
296
		 * @since 2.0.0
297
		 */
298 8
		$override = apply_filters( "cmb2_override_{$a['field_id']}_meta_save", $override, $a, $this->args(), $this );
299
300
		// If override, return that
301 8
		if ( null !== $override ) {
302 1
			return $override;
303
		}
304
305
		// Options page handling (or temp data store)
306 8
		if ( 'options-page' === $a['type'] || empty( $a['id'] ) ) {
307 3
			return cmb2_options( $a['id'] )->update( $a['field_id'], $a['value'], false, $a['single'] );
308
		}
309
310
		// Add metadata if not single
311 5
		if ( ! $a['single'] ) {
312 1
			return add_metadata( $a['type'], $a['id'], $a['field_id'], $a['value'], false );
313
		}
314
315
		// Delete meta if we have an empty array
316 4
		if ( is_array( $a['value'] ) && empty( $a['value'] ) ) {
317
			return delete_metadata( $a['type'], $a['id'], $a['field_id'], $this->value );
318
		}
319
320
		// Update metadata
321 4
		return update_metadata( $a['type'], $a['id'], $a['field_id'], $a['value'] );
322
	}
323
324
	/**
325
	 * Removes/updates metadata/option data
326
	 * @since  1.0.1
327
	 * @param  string $old Old value
328
	 */
329 1
	public function remove_data( $old = '' ) {
330 1
		$a = $this->data_args( array( 'old' => $old ) );
331
332
		/**
333
		 * Filter whether to override removing of meta value.
334
		 * Returning a non-null value will effectively short-circuit the function.
335
		 *
336
		 * @since 2.0.0
337
		 *
338
		 * @param null|bool $delete Whether to allow metadata deletion of the given type.
339
		 * @param array $args       Array of data about current field including:
340
		 *                              'type'     : Current object type
341
		 *                              'id'       : Current object ID
342
		 *                              'field_id' : Current Field ID
343
		 *                              'repeat'   : Whether current field is repeatable
344
		 *                              'single'   : Whether to save as a
345
		 *                              					single meta value
346
		 * @param array $field_args All field arguments
347
		 * @param CMB2_Field object $field This field object
348
		 */
349 1
		$override = apply_filters( 'cmb2_override_meta_remove', null, $a, $this->args(), $this );
350
351
		/**
352
		 * Filter whether to override removing of meta value.
353
		 *
354
		 * The dynamic portion of the hook, $a['field_id'], refers to the current
355
		 * field id paramater. Returning a non-null value
356
		 * will effectively short-circuit the function.
357
		 *
358
		 * @since 2.0.0
359
		 *
360
		 * @param null|bool $delete Whether to allow metadata deletion of the given type.
361
		 * @param array $args       Array of data about current field including:
362
		 *                              'type'     : Current object type
363
		 *                              'id'       : Current object ID
364
		 *                              'field_id' : Current Field ID
365
		 *                              'repeat'   : Whether current field is repeatable
366
		 *                              'single'   : Whether to save as a
367
		 *                              					single meta value
368
		 * @param array $field_args All field arguments
369
		 * @param CMB2_Field object $field This field object
370
		 */
371 1
		$override = apply_filters( "cmb2_override_{$a['field_id']}_meta_remove", $override, $a, $this->args(), $this );
372
373
		// If no override, remove as usual
374 1
		if ( null !== $override ) {
375
			return $override;
376
		}
377
		// Option page handling
378 1
		elseif ( 'options-page' === $a['type'] || empty( $a['id'] ) ) {
379
			return cmb2_options( $a['id'] )->remove( $a['field_id'] );
380
		}
381
382
		// Remove metadata
383 1
		return delete_metadata( $a['type'], $a['id'], $a['field_id'], $old );
384
	}
385
386
	/**
387
	 * Data variables for get/set data methods
388
	 * @since  1.1.0
389
	 * @param  array $args Override arguments
390
	 * @return array       Updated arguments
391
	 */
392 65
	public function data_args( $args = array() ) {
393 65
		$args = wp_parse_args( $args, array(
394 65
			'type'     => $this->object_type,
395 65
			'id'       => $this->object_id,
396 65
			'field_id' => $this->id( true ),
397 65
			'repeat'   => $this->args( 'repeatable' ),
398 65
			'single'   => ! $this->args( 'multiple' ),
399 65
		) );
400 65
		return $args;
401
	}
402
403
	/**
404
	 * Checks if field has a registered sanitization callback
405
	 * @since  1.0.1
406
	 * @param  mixed $meta_value Meta value
407
	 * @return mixed             Possibly sanitized meta value
408
	 */
409 8
	public function sanitization_cb( $meta_value ) {
410
411 8
		if ( $this->args( 'repeatable' ) && is_array( $meta_value ) ) {
412
			// Remove empties
413 1
			$meta_value = array_filter( $meta_value );
414 1
		}
415
416
		// Check if the field has a registered validation callback
417 8
		$cb = $this->maybe_callback( 'sanitization_cb' );
418 8
		if ( false === $cb ) {
419
			// If requesting NO validation, return meta value
420
			return $meta_value;
421 8
		} elseif ( $cb ) {
422
			// Ok, callback is good, let's run it.
423
			return call_user_func( $cb, $meta_value, $this->args(), $this );
424
		}
425
426 8
		$sanitizer = new CMB2_Sanitize( $this, $meta_value );
427
428
		/**
429
		 * Filter the value before it is saved.
430
		 *
431
		 * The dynamic portion of the hook name, $this->type(), refers to the field type.
432
		 *
433
		 * Passing a non-null value to the filter will short-circuit saving
434
		 * the field value, saving the passed value instead.
435
		 *
436
		 * @param bool|mixed $override_value Sanitization/Validation override value to return.
437
		 *                                   Default false to skip it.
438
		 * @param mixed      $value      The value to be saved to this field.
439
		 * @param int        $object_id  The ID of the object where the value will be saved
440
		 * @param array      $field_args The current field's arguments
441
		 * @param object     $sanitizer  This `CMB2_Sanitize` object
442
		 */
443 8
		$override_value = apply_filters( "cmb2_sanitize_{$this->type()}", null, $sanitizer->value, $this->object_id, $this->args(), $sanitizer );
444
445 8
		if ( null !== $override_value ) {
446 1
			return $override_value;
447
		}
448
449
		// Sanitization via 'CMB2_Sanitize'
450 7
		return $sanitizer->{$this->type()}();
451
	}
452
453
	/**
454
	 * Process $_POST data to save this field's value
455
	 * @since  2.0.3
456
	 * @param  array $data_to_save $_POST data to check
457
	 * @return bool                Result of save
458
	 */
459 1
	public function save_field_from_data( array $data_to_save ) {
460 1
		$this->data_to_save = $data_to_save;
461
462 1
		$meta_value = isset( $this->data_to_save[ $this->id( true ) ] )
463 1
			? $this->data_to_save[ $this->id( true ) ]
464 1
			: null;
465
466 1
		return $this->save_field( $meta_value );
467
	}
468
469
	/**
470
	 * Sanitize/store a value to this field
471
	 * @since  2.0.0
472
	 * @param  array $meta_value Desired value to sanitize/store
473
	 * @return bool              Result of save
474
	 */
475 7
	public function save_field( $meta_value ) {
476
477 7
		$new_value = $this->sanitization_cb( $meta_value );
478 7
		$old       = $this->get_data();
479 7
		$updated   = false;
480 7
		$action    = '';
481
482 7
		if ( $this->args( 'multiple' ) && ! $this->args( 'repeatable' ) && ! $this->group ) {
483
484 1
			$this->remove_data();
485 1
			$count = 0;
486
487 1
			if ( ! empty( $new_value ) ) {
488 1
				foreach ( $new_value as $add_new ) {
489 1
					if ( $this->update_data( $add_new, false ) ) {
490 1
						$count++;
491 1
					}
492 1
				}
493 1
			}
494
495 1
			$updated = $count ? $count : false;
496 1
			$action  = 'repeatable';
497
498 7
		} elseif ( ! cmb2_utils()->isempty( $new_value ) && $new_value !== $old ) {
499 6
			$updated = $this->update_data( $new_value );
500 6
			$action  = 'updated';
501 6
		} elseif ( cmb2_utils()->isempty( $new_value ) ) {
502
			$updated = $this->remove_data();
503
			$action  = 'removed';
504
		}
505
506 7
		if ( $updated ) {
507 7
			$this->value = $this->get_data();
508 7
		}
509
510 7
		$field_id = $this->id( true );
511
512
		/**
513
		 * Hooks after save field action.
514
		 *
515
		 * @since 2.2.0
516
		 *
517
		 * @param string            $field_id the current field id paramater.
518
		 * @param bool              $updated  Whether the metadata update action occurred.
519
		 * @param string            $action   Action performed. Could be "repeatable", "updated", or "removed".
520
		 * @param CMB2_Field object $field    This field object
521
		 */
522 7
		do_action( 'cmb2_save_field', $field_id, $updated, $action, $this );
523
524
		/**
525
		 * Hooks after save field action.
526
		 *
527
		 * The dynamic portion of the hook, $field_id, refers to the
528
		 * current field id paramater.
529
		 *
530
		 * @since 2.2.0
531
		 *
532
		 * @param bool              $updated Whether the metadata update action occurred.
533
		 * @param string            $action  Action performed. Could be "repeatable", "updated", or "removed".
534
		 * @param CMB2_Field object $field   This field object
535
		 */
536 6
		do_action( "cmb2_save_field_{$field_id}", $updated, $action, $this );
537
538 5
		return $updated;
539
	}
540
541
	/**
542
	 * Checks if field has a callback value
543
	 * @since  1.0.1
544
	 * @param  string $cb Callback string
545
	 * @return mixed      NULL, false for NO validation, or $cb string if it exists.
546
	 */
547 60
	public function maybe_callback( $cb ) {
548 60
		$field_args = $this->args();
549 60
		if ( ! isset( $field_args[ $cb ] ) ) {
550 53
			return;
551
		}
552
553
		// Check if metabox is requesting NO validation
554 17
		$cb = false !== $field_args[ $cb ] && 'false' !== $field_args[ $cb ] ? $field_args[ $cb ] : false;
555
556
		// If requesting NO validation, return false
557 17
		if ( ! $cb ) {
558 7
			return false;
559
		}
560
561 17
		if ( is_callable( $cb ) ) {
562 15
			return $cb;
563
		}
564 6
	}
565
566
	/**
567
	 * Determine if current type is excempt from escaping
568
	 * @since  1.1.0
569
	 * @return bool  True if exempt
570
	 */
571 44
	public function escaping_exception() {
572
		// These types cannot be escaped
573 44
		return in_array( $this->type(), array(
574 44
			'file_list',
575 44
			'multicheck',
576 44
			'text_datetime_timestamp_timezone',
577 44
		) );
578
	}
579
580
	/**
581
	 * Determine if current type cannot be repeatable
582
	 * @since  1.1.0
583
	 * @param  string $type Field type to check
584
	 * @return bool         True if type cannot be repeatable
585
	 */
586 3
	public function repeatable_exception( $type ) {
587
		// These types cannot be escaped
588
		$internal_fields = array(
589
			// Use file_list instead
590 3
			'file'                => 1,
591 3
			'radio'               => 1,
592 3
			'title'               => 1,
593
			// @todo Ajax load wp_editor: http://wordpress.stackexchange.com/questions/51776/how-to-load-wp-editor-through-ajax-jquery
594 3
			'wysiwyg'             => 1,
595 3
			'checkbox'            => 1,
596 3
			'radio_inline'        => 1,
597 3
			'taxonomy_radio'      => 1,
598 3
			'taxonomy_select'     => 1,
599 3
			'taxonomy_multicheck' => 1,
600 3
		);
601
602
		/**
603
		 * Filter field types that are non-repeatable.
604
		 *
605
		 * Note that this does *not* allow overriding the default non-repeatable types.
606
		 *
607
		 * @since 2.1.1
608
		 *
609
		 * @param array $fields Array of fields designated as non-repeatable. Note that the field names are *keys*,
610
		 *                      and not values. The value can be anything, because it is meaningless. Example:
611
		 *                      array( 'my_custom_field' => 1 )
612
		 */
613 3
		$all_fields = array_merge( apply_filters( 'cmb2_non_repeatable_fields', array() ), $internal_fields );
614 3
		return isset( $all_fields[ $type ] );
615
	}
616
617
	/**
618
	 * Escape the value before output. Defaults to 'esc_attr()'
619
	 * @since  1.0.1
620
	 * @param  callable $func       Escaping function (if not esc_attr())
621
	 * @param  mixed    $meta_value Meta value
622
	 * @return mixed                Final value
623
	 */
624 44
	public function escaped_value( $func = 'esc_attr', $meta_value = '' ) {
625
626 44
		if ( null !== $this->escaped_value ) {
627 22
			return $this->escaped_value;
628
		}
629
630 44
		$meta_value = $meta_value ? $meta_value : $this->value();
631
632
		// Check if the field has a registered escaping callback
633 44
		if ( $cb = $this->maybe_callback( 'escape_cb' ) ) {
634
			// Ok, callback is good, let's run it.
635
			return call_user_func( $cb, $meta_value, $this->args(), $this );
636
		}
637
638
		// Or custom escaping filter can be used
639 44
		$esc = apply_filters( "cmb2_types_esc_{$this->type()}", null, $meta_value, $this->args(), $this );
640 44
		if ( null !== $esc ) {
641
			return $esc;
642
		}
643
644 44
		if ( false === $cb || $this->escaping_exception() ) {
645
			// If requesting NO escaping, return meta value
646 5
			return $this->val_or_default( $meta_value );
647
		}
648
649
		// escaping function passed in?
650 39
		$func       = $func ? $func : 'esc_attr';
651 39
		$meta_value = $this->val_or_default( $meta_value );
652
653 39
		if ( is_array( $meta_value ) ) {
654
			foreach ( $meta_value as $key => $value ) {
655
				$meta_value[ $key ] = call_user_func( $func, $value );
656
			}
657
		} else {
658 39
			$meta_value = call_user_func( $func, $meta_value );
659
		}
660
661 39
		$this->escaped_value = $meta_value;
662 39
		return $this->escaped_value;
663
	}
664
665
	/**
666
	 * Return non-empty value or field default if value IS empty
667
	 * @since  2.0.0
668
	 * @param  mixed $meta_value Field value
669
	 * @return mixed             Field value, or default value
670
	 */
671 44
	public function val_or_default( $meta_value ) {
672 44
		return ! cmb2_utils()->isempty( $meta_value ) ? $meta_value : $this->get_param_callback_result( 'default', false );
673
	}
674
675
	/**
676
	 * Offset a time value based on timezone
677
	 * @since  1.0.0
678
	 * @return string Offset time string
679
	 */
680
	public function field_timezone_offset() {
681
		return cmb2_utils()->timezone_offset( $this->field_timezone() );
682
	}
683
684
	/**
685
	 * Return timezone string
686
	 * @since  1.0.0
687
	 * @return string Timezone string
688
	 */
689
	public function field_timezone() {
690
		$value = '';
691
692
		// Is timezone arg set?
693
		if ( $this->args( 'timezone' ) ) {
694
			$value = $this->args( 'timezone' );
695
		}
696
		// Is there another meta key with a timezone stored as its value we should use?
697
		else if ( $this->args( 'timezone_meta_key' ) ) {
698
			$value = $this->get_data( $this->args( 'timezone_meta_key' ) );
699
		}
700
701
		return $value;
702
	}
703
704
	/**
705
	 * Format the timestamp field value based on the field date/time format arg
706
	 * @since  2.0.0
707
	 * @param  int    $meta_value Timestamp
708
	 * @param  string $format     Either date_format or time_format
709
	 * @return string             Formatted date
710
	 */
711 5
	public function format_timestamp( $meta_value, $format = 'date_format' ) {
712 5
		return date( stripslashes( $this->args( $format ) ), $meta_value );
713
	}
714
715
	/**
716
	 * Return a formatted timestamp for a field
717
	 * @since  2.0.0
718
	 * @param  string $format     Either date_format or time_format
719
	 * @param  string $meta_value Optional meta value to check
720
	 * @return string             Formatted date
721
	 */
722 5
	public function get_timestamp_format( $format = 'date_format', $meta_value = 0 ) {
723 5
		$meta_value = $meta_value ? $meta_value : $this->escaped_value();
724 5
		$meta_value = cmb2_utils()->make_valid_time_stamp( $meta_value );
725
726 5
		if ( empty( $meta_value ) ) {
727
			return '';
728
		}
729
730 5
		return is_array( $meta_value )
731 5
			? array_map( array( $this, 'format_timestamp' ), $meta_value, $format )
732 5
			: $this->format_timestamp( $meta_value, $format );
733
	}
734
735
	/**
736
	 * Get timestamp from text date
737
	 * @since  2.2.0
738
	 * @param  string $value Date value
739
	 * @return mixed         Unix timestamp representing the date.
740
	 */
741
	public function get_timestamp_from_value( $value ) {
742
		return cmb2_utils()->get_timestamp_from_value( $value, $this->args( 'date_format' ) );
743
	}
744
745
	/**
746
	 * Get field render callback and Render the field row
747
	 * @since 1.0.0
748
	 */
749 9
	public function render_field() {
750
		// Check if the field has a registered render_field callback
751 9
		if ( $cb = $this->maybe_callback( 'render_row_cb' ) ) {
752
			// Ok, callback is good, let's run it.
753 9
			return call_user_func( $cb, $this->args(), $this );
754
		}
755
	}
756
757
	/**
758
	 * Default field render callback
759
	 * @since 2.1.1
760
	 */
761 8
	public function render_field_callback() {
762
763
		// If field is requesting to not be shown on the front-end
764 8
		if ( ! is_admin() && ! $this->args( 'on_front' ) ) {
765
			return;
766
		}
767
768
		// If field is requesting to be conditionally shown
769 8
		if ( ! $this->should_show() ) {
770
			return;
771
		}
772
773 8
		$this->peform_param_callback( 'before_row' );
774
775 8
		printf( "<div class=\"cmb-row %s\">\n", $this->row_classes() );
776
777 8
		if ( ! $this->args( 'show_names' ) ) {
778
			echo "\n\t<div class=\"cmb-td\">\n";
779
780
			$this->peform_param_callback( 'label_cb' );
781
782
		} else {
783
784 8
			if ( $this->get_param_callback_result( 'label_cb', false ) ) {
785 8
				echo '<div class="cmb-th">', $this->peform_param_callback( 'label_cb' ), '</div>';
786 8
			}
787
788 8
			echo "\n\t<div class=\"cmb-td\">\n";
789
		}
790
791 8
		$this->peform_param_callback( 'before' );
792
793 8
		$field_type = new CMB2_Types( $this );
794 8
		$field_type->render();
795
796 8
		$this->peform_param_callback( 'after' );
797
798 8
		echo "\n\t</div>\n</div>";
799
800 8
		$this->peform_param_callback( 'after_row' );
801
802
		// For chaining
803 8
		return $this;
804
	}
805
806
	/**
807
	 * The default label_cb callback (if not a title field)
808
	 *
809
	 * @since  2.1.1
810
	 * @return string Label html markup
811
	 */
812 8
	public function label() {
813 8
		if ( ! $this->args( 'name' ) ) {
814
			return '';
815
		}
816
817 8
		$style = ! $this->args( 'show_names' ) ? ' style="display:none;"' : '';
818
819 8
		return sprintf( "\n" . '<label%1$s for="%2$s">%3$s</label>' . "\n", $style, $this->id(), $this->args( 'name' ) );
820
	}
821
822
	/**
823
	 * Defines the classes for the current CMB2 field row
824
	 *
825
	 * @since  2.0.0
826
	 * @return string Space concatenated list of classes
827
	 */
828 11
	public function row_classes() {
829
830 11
		$classes = array();
831
832
		/**
833
		 * By default, 'text_url' and 'text' fields get table-like styling
834
		 *
835
		 * @since 2.0.0
836
		 *
837
		 * @param array $field_types The types of fields which should get the 'table-layout' class
838
		 */
839 11
		$repeat_table_rows_types = apply_filters( 'cmb2_repeat_table_row_types', array(
840 11
			'text_url', 'text',
841 11
		) );
842
843
		$conditional_classes = array(
844 11
			'cmb-type-' . str_replace( '_', '-', sanitize_html_class( $this->type() ) ) => true,
845 11
			'cmb2-id-' . str_replace( '_', '-', sanitize_html_class( $this->id() ) )    => true,
846 11
			'cmb-repeat'             => $this->args( 'repeatable' ),
847 11
			'cmb-repeat-group-field' => $this->group,
848 11
			'cmb-inline'             => $this->args( 'inline' ),
849 11
			'table-layout'           => in_array( $this->type(), $repeat_table_rows_types ),
850 11
		);
851
852 11
		foreach ( $conditional_classes as $class => $condition ) {
853 11
			if ( $condition ) {
854 11
				$classes[] = $class;
855 11
			}
856 11
		}
857
858 11
		if ( $added_classes = $this->get_param_callback_result( 'row_classes', false ) ) {
859 3
			$added_classes = is_array( $added_classes ) ? implode( ' ', $added_classes ) : (string) $added_classes;
860 3
		}
861
862 11
		if ( $added_classes ) {
863 3
			$classes[] = esc_attr( $added_classes );
864 3
		}
865
866
		/**
867
		 * Globally filter row classes
868
		 *
869
		 * @since 2.0.0
870
		 *
871
		 * @param string            $classes Space-separated list of row classes
872
		 * @param CMB2_Field object $field   This field object
873
		 */
874 11
		return apply_filters( 'cmb2_row_classes', implode( ' ', $classes ), $this );
875
	}
876
877
	/**
878
	 * Determine whether this field should show, based on the 'show_on_cb' callback.
879
	 *
880
	 * @since 2.0.9
881
	 *
882
	 * @return bool Whether the field should be shown.
883
	 */
884 9 View Code Duplication
	public function should_show() {
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...
885
		// Default to showing the field
886 9
		$show = true;
887
888
		// Use the callback to determine showing the field, if it exists
889 9
		if ( is_callable( $this->args( 'show_on_cb' ) ) ) {
890 1
			$show = call_user_func( $this->args( 'show_on_cb' ), $this );
891 1
		}
892
893 9
		return $show;
894
	}
895
896
	/**
897
	 * Displays the results of the param callbacks.
898
	 *
899
	 * @since 2.0.0
900
	 * @param string $param Field parameter
901
	 */
902 45
	public function peform_param_callback( $param ) {
903 45
		echo $this->get_param_callback_result( $param );
904 45
	}
905
906
	/**
907
	 * Store results of the param callbacks for continual access
908
	 * @since  2.0.0
909
	 * @param  string $param Field parameter
910
	 * @param  bool   $echo  Whether field should be 'echoed'
911
	 * @return mixed         Results of param/param callback
912
	 */
913 50
	public function get_param_callback_result( $param, $echo = true ) {
914
915
		// If we've already retrieved this param's value,
916 50
		if ( array_key_exists( $param, $this->callback_results ) ) {
917
			// send it back
918 17
			return $this->callback_results[ $param ];
919
		}
920
921 50
		if ( $cb = $this->maybe_callback( $param ) ) {
922 14
			if ( $echo ) {
923
				// Ok, callback is good, let's run it and store the result
924 4
				ob_start();
925 4
				echo call_user_func( $cb, $this->args(), $this );
926
				// grab the result from the output buffer and store it
927 4
				$this->callback_results[ $param ] = ob_get_contents();
928 4
				ob_end_clean();
929 4
			} else {
930 11
				$this->callback_results[ $param ] = call_user_func( $cb, $this->args(), $this );
931
			}
932
933 14
			return $this->callback_results[ $param ];
934
		}
935
936
		// Otherwise just get whatever is there
937 46
		$this->callback_results[ $param ] = isset( $this->args[ $param ] ) ? $this->args[ $param ] : false;
938
939 46
		return $this->callback_results[ $param ];
940
	}
941
942
	/**
943
	 * Replaces a hash key - {#} - with the repeatable index
944
	 * @since  1.2.0
945
	 * @param  string $value Value to update
946
	 * @return string        Updated value
947
	 */
948 2
	public function replace_hash( $value ) {
949
		// Replace hash with 1 based count
950 2
		return str_ireplace( '{#}', ( $this->index + 1 ), $value );
951
	}
952
953
	/**
954
	 * Retrieve options args. Calls options_cb if it exists.
955
	 * @since  2.0.0
956
	 * @param  string  $key Specific option to retrieve
957
	 * @return array        Array of options
958
	 */
959 23
	public function options( $key = '' ) {
960 23
		if ( ! empty( $this->field_options ) ) {
961 6
			if ( $key ) {
962 4
				return array_key_exists( $key, $this->field_options ) ? $this->field_options[ $key ] : false;
963
			}
964
965 2
			return $this->field_options;
966
		}
967
968 23
		$this->field_options = (array) $this->args['options'];
969
970 23
		if ( is_callable( $this->args['options_cb'] ) ) {
971 1
			$options = call_user_func( $this->args['options_cb'], $this );
972
973 1
			if ( $options && is_array( $options ) ) {
974 1
				$this->field_options += $options;
975 1
			}
976 1
		}
977
978 23
		if ( $key ) {
979 8
			return array_key_exists( $key, $this->field_options ) ? $this->field_options[ $key ] : false;
980
		}
981
982 17
		return $this->field_options;
983
	}
984
985
	/**
986
	 * Fills in empty field parameters with defaults
987
	 * @since 1.1.0
988
	 * @param array $args Metabox field config array
989
	 */
990 69
	public function _set_field_defaults( $args ) {
991
992
		// Set up blank or default values for empty ones
993 69
		$args = wp_parse_args( $args, apply_filters( 'cmb2_field_defaults', array(
994 69
			'type'              => '',
995 69
			'name'              => '',
996 69
			'desc'              => '',
997 69
			'before'            => '',
998 69
			'after'             => '',
999 69
			'options_cb'        => '',
1000 69
			'options'           => array(),
1001 69
			'attributes'        => array(),
1002 69
			'protocols'         => null,
1003 69
			'default'           => null,
1004 69
			'select_all_button' => true,
1005 69
			'multiple'          => false,
1006 69
			'repeatable'        => isset( $args['type'] ) && 'group' == $args['type'],
1007 69
			'inline'            => false,
1008 69
			'on_front'          => true,
1009 69
			'show_names'        => true,
1010 69
			'date_format'       => 'm\/d\/Y',
1011 69
			'time_format'       => 'h:i A',
1012 69
			'description'       => isset( $args['desc'] ) ? $args['desc'] : '',
1013 69
			'preview_size'      => 'file' == $args['type'] ? array( 350, 350 ) : array( 50, 50 ),
1014 69
			'render_row_cb'     => array( $this, 'render_field_callback' ),
1015 69
			'label_cb'          => 'title' != $args['type'] ? array( $this, 'label' ) : '',
1016 69
		), $args['id'], $args['type'], $this->object_type ) );
1017
1018
		// Allow for filtering of the arguments once merged with the defaults, but before further processing.
1019 69
		$args = apply_filters( 'cmb2_field_arguments_raw', $args, $args['id'], $args['type'], $this->object_type );
1020
1021
		// Allow a filter override of the default value
1022 69
		$args['default']    = apply_filters( 'cmb2_default_filter', $args['default'], $this );
1023 69
		$args['repeatable'] = $args['repeatable'] && ! $this->repeatable_exception( $args['type'] );
1024 69
		$args['inline']     = $args['inline'] || false !== stripos( $args['type'], '_inline' );
1025
1026
		// options param can be passed a callback as well
1027 69
		if ( is_callable( $args['options'] ) ) {
1028
			$args['options_cb'] = $args['options'];
1029
			$args['options'] = array();
1030
		}
1031
1032 69
		$args['options']    = 'group' == $args['type'] ? wp_parse_args( $args['options'], array(
1033 3
			'add_button'    => __( 'Add Group', 'cmb2' ),
1034 3
			'remove_button' => __( 'Remove Group', 'cmb2' ),
1035 69
		) ) : $args['options'];
1036
1037 69
		$args['_id']        = $args['id'];
1038 69
		$args['_name']      = $args['id'];
1039
1040 69
		if ( $this->group ) {
1041
1042 3
			$args['id']    = $this->group->args( 'id' ) . '_' . $this->group->index . '_' . $args['id'];
1043 3
			$args['_name'] = $this->group->args( 'id' ) . '[' . $this->group->index . '][' . $args['_name'] . ']';
1044 3
		}
1045
1046 69
		if ( 'wysiwyg' == $args['type'] ) {
1047 1
			$args['id'] = strtolower( str_ireplace( '-', '_', $args['id'] ) );
1048 1
			$args['options']['textarea_name'] = $args['_name'];
1049 1
		}
1050
1051 69
		$option_types = apply_filters( 'cmb2_all_or_nothing_types', array( 'select', 'radio', 'radio_inline', 'taxonomy_select', 'taxonomy_radio', 'taxonomy_radio_inline' ), $this );
1052
1053 69
		if ( in_array( $args['type'], $option_types, true ) ) {
1054
1055 9
			$args['show_option_none'] = isset( $args['show_option_none'] ) ? $args['show_option_none'] : null;
1056 9
			$args['show_option_none'] = true === $args['show_option_none'] ? __( 'None', 'cmb2' ) : $args['show_option_none'];
1057
1058 9
			if ( null === $args['show_option_none'] ) {
1059 9
				$off_by_default = in_array( $args['type'], array( 'select', 'radio', 'radio_inline' ), true );
1060 9
				$args['show_option_none'] = $off_by_default ? false : __( 'None', 'cmb2' );
1061 9
			}
1062
1063 9
		}
1064
1065 69
		$args['has_supporting_data'] = in_array(
1066 69
			$args['type'],
1067
			array(
1068
				// CMB2_Sanitize::_save_file_id_value()/CMB2_Sanitize::_get_group_file_value_array()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
1069 69
				'file',
1070
				// See CMB2_Sanitize::_save_utc_value()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
1071 69
				'text_datetime_timestamp_timezone',
1072 69
			),
1073
			true
1074 69
		);
1075
1076 69
		return apply_filters( 'cmb2_field_arguments', $args, $args['id'], $args['type'], $this->object_type );
1077
	}
1078
1079
	/**
1080
	 * Updates attributes array values unless they exist from the field config array
1081
	 * @since 1.1.0
1082
	 * @param array $attrs Array of attributes to update
1083
	 */
1084 44
	public function maybe_set_attributes( $attrs = array() ) {
1085 44
		return wp_parse_args( $this->args['attributes'], $attrs );
1086
	}
1087
1088
}
1089