Completed
Push — milestone/2_0/react-ui ( 7118aa...a33d5e )
by
unknown
06:00
created

Container::get_nonce_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
crap 2
1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\App;
6
use Carbon_Fields\Helper\Helper;
7
use Carbon_Fields\Field\Field;
8
use Carbon_Fields\Field\Group_Field;
9
use Carbon_Fields\Datastore\Datastore_Interface;
10
use Carbon_Fields\Datastore\Datastore_Holder_Interface;
11
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
12
13
/**
14
 * Base container class.
15
 * Defines the key container methods and their default implementations.
16
 */
17
abstract class Container implements Datastore_Holder_Interface {
18
	/**
19
	 * Where to put a particular tab -- at the head or the tail. Tail by default
20
	 */
21
	const TABS_TAIL = 1;
22
	const TABS_HEAD = 2;
23
24
	/**
25
	 * Separator signifying field hierarchy relation
26
	 * Used when searching for fields in a specific complex field
27
	 */
28
	const HIERARCHY_FIELD_SEPARATOR = '/';
29
30
	/**
31
	 * Separator signifying complex_field->group relation
32
	 * Used when searching for fields in a specific complex field group
33
	 */
34
	const HIERARCHY_GROUP_SEPARATOR = ':';
35
36
	/**
37
	 * Stores if the container is active on the current page
38
	 *
39
	 * @see activate()
40
	 * @var bool
41
	 */
42
	protected $active = false;
43
44
	/**
45
	 * List of registered unique field names for this container instance
46
	 *
47
	 * @see verify_unique_field_name()
48
	 * @var array
49
	 */
50
	protected $registered_field_names = array();
51
52
	/**
53
	 * Tabs available
54
	 */
55
	protected $tabs = array();
56
57
	/**
58
	 * List of default container settings
59
	 *
60
	 * @see init()
61
	 * @var array
62
	 */
63
	public $settings = array();
64
65
	/**
66
	 * Title of the container
67
	 *
68
	 * @var string
69
	 */
70
	public $title = '';
71
72
	/**
73
	 * List of notification messages to be displayed on the front-end
74
	 *
75
	 * @var array
76
	 */
77
	protected $notifications = array();
78
79
	/**
80
	 * List of error messages to be displayed on the front-end
81
	 *
82
	 * @var array
83
	 */
84
	protected $errors = array();
85
86
	/**
87
	 * List of container fields
88
	 *
89
	 * @see add_fields()
90
	 * @var array
91
	 */
92
	protected $fields = array();
93
94
	/**
95
	 * Container datastores. Propagated to all container fields
96
	 *
97
	 * @see set_datastore()
98
	 * @see get_datastore()
99
	 * @var object
100
	 */
101
	protected $datastore;
102
103
	/**
104
	 * Flag whether the datastore is the default one or replaced with a custom one
105
	 *
106
	 * @see set_datastore()
107
	 * @see get_datastore()
108
	 * @var boolean
109
	 */
110
	protected $has_default_datastore = true;
111
112
	/**
113
	 * Array of custom CSS classes.
114
	 *
115
	 * @see add_class()
116
	 * @see get_classes()
117
	 * @var array<string>
118
	 */
119
	protected $classes = array();
120
121
	/**
122
	 * Normalizes a container type string to an expected format
123
	 *
124
	 * @param string $type
125
	 * @return string $normalized_type
126
	 **/
127
	protected static function normalize_container_type( $type ) {
128
		// backward compatibility: post_meta container used to be called custom_fields
129
		if ( $type === 'custom_fields' ) {
130
			$type = 'post_meta';
131
		}
132
133
		$normalized_type = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $type ) ) );
134
		return $normalized_type;
135
	}
136
137
	/**
138
	 * Resolves a string-based type to a fully qualified container class name
139
	 *
140
	 * @param string $type
141
	 * @return string $class_name
142
	 **/
143
	protected static function container_type_to_class( $type ) {
144
		$class = __NAMESPACE__ . '\\' . $type . '_Container';
145 View Code Duplication
		if ( ! class_exists( $class ) ) {
146
			Incorrect_Syntax_Exception::raise( 'Unknown container "' . $type . '".' );
147
			$class = __NAMESPACE__ . '\\Broken_Container';
148
		}
149
		return $class;
150
	}
151
152
	/**
153
	 * Create a new container of type $type and name $name.
154
	 *
155
	 * @param string $type
156
	 * @param string $name Human-readable name of the container
157
	 * @return object $container
158
	 **/
159 9
	public static function factory( $type, $name ) {
160 9
		$repository = App::resolve( 'container_repository' );
161 9
		$unique_id = $repository->get_unique_panel_id( $name );
162
		
163 9
		$normalized_type = static::normalize_container_type( $type );
164 9
		$class = static::container_type_to_class( $normalized_type );
165 7
		$container = new $class( $unique_id, $name, $normalized_type );
166 7
		$repository->register_container( $container );
167
168 7
		return $container;
169
	}
170
171
	/**
172
	 * An alias of factory().
173
	 *
174
	 * @see Container::factory()
175
	 **/
176
	public static function make( $type, $name ) {
177
		return static::factory( $type, $name );
178
	}
179
180
	/**
181
	 * Create a new container
182
	 *
183
	 * @param string $unique_id Unique id of the container
184
	 * @param string $title title of the container
185
	 * @param string $type Type of the container
186
	 **/
187 2
	public function __construct( $unique_id, $title, $type ) {
188 2
		App::verify_boot();
189
190 2
		if ( empty( $title ) ) {
191 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
192
		}
193
194 1
		$this->id = $unique_id;
195 1
		$this->title = $title;
196 1
		$this->type = $type;
197 1
	}
198
199
	/**
200
	 * Return whether the container is active
201
	 **/
202
	public function active() {
203
		return $this->active;
204
	}
205
206
	/**
207
	 * Activate the container and trigger an action
208
	 **/
209
	protected function activate() {
210
		$this->active = true;
211
		$this->boot();
212
		do_action( 'crb_container_activated', $this );
213
214
		$fields = $this->get_fields();
215
		foreach ( $fields as $field ) {
216
			$field->activate();
217
		}
218
	}
219
220
	/**
221
	 * Perform instance initialization
222
	 **/
223
	abstract public function init();
224
225
	/**
226
	 * Boot the container once it's attached.
227
	 **/
228
	protected function boot() {
229
		add_action( 'admin_footer', array( get_class(), 'admin_hook_styles' ), 5 );
230
	}
231
232
	/**
233
	 * Load the value for each field in the container.
234
	 * Could be used internally during container rendering
235
	 **/
236
	public function load() {
237
		foreach ( $this->fields as $field ) {
238
			$field->load();
239
		}
240
	}
241
242
	/**
243
	 * Called first as part of the container save procedure.
244
	 * Responsible for checking the request validity and
245
	 * calling the container-specific save() method
246
	 *
247
	 * @see save()
248
	 * @see is_valid_save()
249
	 **/
250
	public function _save() {
251
		$param = func_get_args();
252
		if ( call_user_func_array( array( $this, '_is_valid_save' ), $param ) ) {
253
			call_user_func_array( array( $this, 'save' ), $param );
254
		}
255
	}
256
257
	/**
258
	 * Load submitted data and save each field in the container
259
	 *
260
	 * @see is_valid_save()
261
	 **/
262
	public function save( $data = null ) {
263
		foreach ( $this->fields as $field ) {
264
			$field->set_value_from_input( stripslashes_deep( $_POST ) );
1 ignored issue
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
265
			$field->save();
266
		}
267
	}
268
269
	/**
270
	 * Checks whether the current save request is valid
271
	 *
272
	 * @return bool
273
	 **/
274
	final protected function _is_valid_save() {
275
		$param = func_get_args();
276
		$is_valid_save = call_user_func_array( array( $this, 'is_valid_save' ), $param );
277
		return apply_filters( 'carbon_fields_container_is_valid_save', $is_valid_save, $this );
278
	}
279
280
	/**
281
	 * Checks whether the current save request is valid
282
	 *
283
	 * @return bool
284
	 **/
285
	abstract protected function is_valid_save();
286
287
	/**
288
	 * Called first as part of the container attachment procedure.
289
	 * Responsible for checking it's OK to attach the container
290
	 * and if it is, calling the container-specific attach() method
291
	 *
292
	 * @see attach()
293
	 * @see is_valid_attach()
294
	 **/
295
	public function _attach() {
296
		$param = func_get_args();
297
		if ( $this->is_valid_attach() ) {
298
			call_user_func_array( array( $this, 'attach' ), $param );
299
300
			// Allow containers to activate but not load (useful in cases such as theme options)
301
			if ( $this->should_activate() ) {
302
				$this->activate();
303
			}
304
		}
305
	}
306
307
	/**
308
	 * Attach the container rendering and helping methods
309
	 * to concrete WordPress Action hooks
310
	 **/
311
	public function attach() {}
312
313
	/**
314
	 * Perform checks whether the container should be attached during the current request
315
	 *
316
	 * @return bool True if the container is allowed to be attached
317
	 **/
318
	final public function is_valid_attach() {
319
		$is_valid_attach = $this->is_valid_attach_for_request();
320
		return apply_filters( 'carbon_fields_container_is_valid_attach', $is_valid_attach, $this );
321
	}
322
323
	/**
324
	 * Check container attachment rules against current page request (in admin)
325
	 *
326
	 * @return bool
327
	 **/
328
	abstract protected function is_valid_attach_for_request();
329
330
	/**
331
	 * Check container attachment rules against object id
332
	 *
333
	 * @param int $object_id
334
	 * @return bool
335
	 **/
336
	abstract public function is_valid_attach_for_object( $object_id = null );
337
338
	/**
339
	 * Whether this container is currently viewed.
340
	 **/
341
	public function should_activate() {
342
		return $this->is_valid_attach();
343
	}
344
345
	/**
346
	 * Perform a check whether the current container has fields
347
	 *
348
	 * @return bool
349
	 **/
350
	public function has_fields() {
351
		return (bool) $this->fields;
352
	}
353
354
	/**
355
	 * Returns the private container array of fields.
356
	 * Use only if you are completely aware of what you are doing.
357
	 *
358
	 * @return array
359
	 **/
360
	public function get_fields() {
361
		return $this->fields;
362
	}
363
364
	/**
365
	 * Return root field from container with specified name
366
	 * 
367
	 * @example crb_complex
368
	 * 
369
	 * @param string $field_name
370
	 * @return Field
371
	 **/
372
	public function get_root_field_by_name( $field_name ) {
373
		$fields = $this->get_fields();
374
		foreach ( $fields as $field ) {
375
			if ( $field->get_base_name() === $field_name ) {
376
				return $field;
377
			}
378
		}
379
		return null;
380
	}
381
382
	/**
383
	 * Get a regex to match field name patterns used to fetch specific fields
384
	 * 
385
	 * @return string
386
	 */
387
	protected function get_field_pattern_regex() {
388
		// matches:
389
		// field_name
390
		// field_name[0]
391
		// field_name[0]:group_name
392
		// field_name:group_name
393
		$regex = '/
394
			\A
395
			(?P<field_name>[a-z0-9_]+)
396
			(?:\[(?P<group_index>\d+)\])?
397
			(?:' .  preg_quote( static::HIERARCHY_GROUP_SEPARATOR, '/' ). '(?P<group_name>[a-z0-9_]+))?
398
			\z
399
		/x';
400
		return $regex;
401
	}
402
403
	/**
404
	 * Return field from container with specified name
405
	 * 
406
	 * @example crb_complex/text_field
407
	 * @example crb_complex/complex_2
408
	 * @example crb_complex/complex_2:text_group/text_field
409
	 * 
410
	 * @param string $field_name Can specify a field inside a complex with a / (slash) separator
411
	 * @return Field
412
	 **/
413
	public function get_field_by_name( $field_name ) {
414
		$hierarchy = array_filter( explode( static::HIERARCHY_FIELD_SEPARATOR, $field_name ) );
415
		$field = null;
416
417
		$field_group = $this->get_fields();
418
		$hierarchy_left = $hierarchy;
419
		$field_pattern_regex = $this->get_field_pattern_regex();
420
		$hierarchy_index = array();
421
422
		while ( ! empty( $hierarchy_left ) ) {
423
			$segment = array_shift( $hierarchy_left );
424
			$segment_pieces = array();
425
			if ( ! preg_match( $field_pattern_regex, $segment, $segment_pieces ) ) {
426
				Incorrect_Syntax_Exception::raise( 'Invalid field name pattern used: ' . $field_name );
427
			}
428
			
429
			$segment_field_name = $segment_pieces['field_name'];
430
			$segment_group_index = isset( $segment_pieces['group_index'] ) ? $segment_pieces['group_index'] : 0;
431
			$segment_group_name = isset( $segment_pieces['group_name'] ) ? $segment_pieces['group_name'] : Group_Field::DEFAULT_GROUP_NAME;
432
433
			foreach ( $field_group as $f ) {
434
				if ( $f->get_base_name() === $segment_field_name ) {
435
					if ( empty( $hierarchy_left ) ) {
436
						$field = clone $f;
437
						$field->set_hierarchy_index( $hierarchy_index );
438
					} else {
439
						if ( is_a( $f, 'Carbon_Fields\\Field\\Complex_Field' ) ) {
440
							$group = $f->get_group_by_name( $segment_group_name );
441
							if ( ! $group ) {
442
								Incorrect_Syntax_Exception::raise( 'Unknown group name specified when fetching a value inside a complex field: "' . $segment_group_name . '".' );
443
							}
444
							$field_group = $group->get_fields();
445
							$hierarchy_index[] = $segment_group_index;
446
						} else {
447
							Incorrect_Syntax_Exception::raise( 'Attempted to look for a nested field inside a non-complex field.' );
448
						}
449
					}
450
					break;
451
				}
452
			}
453
		}
454
455
		return $field;
456
	}
457
458
	/**
459
	 * Perform checks whether there is a field registered with the name $name.
460
	 * If not, the field name is recorded.
461
	 *
462
	 * @param string $name
463
	 **/
464 View Code Duplication
	public function verify_unique_field_name( $name ) {
465
		if ( in_array( $name, $this->registered_field_names ) ) {
466
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
467
		}
468
469
		$this->registered_field_names[] = $name;
470
	}
471
472
	/**
473
	 * Remove field name $name from the list of unique field names
474
	 *
475
	 * @param string $name
476
	 **/
477
	public function drop_unique_field_name( $name ) {
478
		$index = array_search( $name, $this->registered_field_names );
479
480
		if ( $index !== false ) {
481
			unset( $this->registered_field_names[ $index ] );
482
		}
483
	}
484
485
	/**
486
	 * Return whether the datastore instance is the default one or has been overriden
487
	 *
488
	 * @return boolean
489
	 **/
490 6
	public function has_default_datastore() {
491 6
		return $this->has_default_datastore;
492
	}
493
494
	/**
495
	 * Set datastore instance
496
	 *
497
	 * @param Datastore_Interface $datastore
498
	 * @return object $this
499
	 **/
500 6 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
501 6
		if ( $set_as_default && ! $this->has_default_datastore() ) {
502 1
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
503
		}
504 6
		$this->datastore = $datastore;
505 6
		$this->has_default_datastore = $set_as_default;
506
507 6
		foreach ( $this->fields as $field ) {
508
			$field->set_datastore( $this->get_datastore(), true );
509 6
		}
510 6
		return $this;
511
	}
512
513
	/**
514
	 * Get the DataStore instance
515
	 *
516
	 * @return Datastore_Interface $datastore
517
	 **/
518 6
	public function get_datastore() {
519 6
		return $this->datastore;
520
	}
521
522
	/**
523
	 * Return WordPress nonce name used to identify the current container instance
524
	 *
525
	 * @return string
526
	 **/
527
	public function get_nonce_name() {
528
		return 'carbon_panel_' . $this->id . '_nonce';
529
	}
530
531
	/**
532
	 * Return WordPress nonce field
533
	 *
534
	 * @return string
535
	 **/
536
	public function get_nonce_field() {
537
		return wp_nonce_field( $this->get_nonce_name(), $this->get_nonce_name(), /*referer?*/ false, /*echo?*/ false );
538
	}
539
540
	/**
541
	 * Check if the nonce is present in the request and that it is verified
542
	 *
543
	 * @return bool
544
	 **/
545
	protected function verified_nonce_in_request() {
546
		$nonce_name = $this->get_nonce_name();
547
		$nonce_value = isset( $_REQUEST[ $nonce_name ] ) ? $_REQUEST[ $nonce_name ] : '';
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
548
		return wp_verify_nonce( $nonce_value, $nonce_name );
549
	}
550
551
	/**
552
	 * Internal function that creates the tab and associates it with particular field set
553
	 *
554
	 * @param string $tab_name
555
	 * @param array $fields
556
	 * @param int $queue_end
557
	 * @return object $this
558
	 */
559
	private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
560
		if ( isset( $this->tabs[ $tab_name ] ) ) {
561
			Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
562
		}
563
564
		if ( $queue_end === static::TABS_TAIL ) {
565
			$this->tabs[ $tab_name ] = array();
566
		} else if ( $queue_end === static::TABS_HEAD ) {
567
			$this->tabs = array_merge(
568
				array( $tab_name => array() ),
569
				$this->tabs
570
			);
571
		}
572
573
		foreach ( $fields as $field ) {
574
			$field_name = $field->get_name();
575
			$this->tabs[ $tab_name ][ $field_name ] = $field;
576
		}
577
578
		$this->settings['tabs'] = $this->get_tabs_json();
579
	}
580
581
	/**
582
	 * Whether the container is tabbed or not
583
	 *
584
	 * @return bool
585
	 */
586
	public function is_tabbed() {
587
		return (bool) $this->tabs;
588
	}
589
590
	/**
591
	 * Retrieve all fields that are not defined under a specific tab
592
	 *
593
	 * @return array
594
	 */
595
	protected function get_untabbed_fields() {
596
		$tabbed_fields_names = array();
597
		foreach ( $this->tabs as $tab_fields ) {
598
			$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
599
		}
600
601
		$all_fields_names = array();
602
		foreach ( $this->fields as $field ) {
603
			$all_fields_names[] = $field->get_name();
604
		}
605
606
		$fields_not_in_tabs = array_diff( $all_fields_names, $tabbed_fields_names );
607
608
		$untabbed_fields = array();
609
		foreach ( $this->fields as $field ) {
610
			if ( in_array( $field->get_name(), $fields_not_in_tabs ) ) {
611
				$untabbed_fields[] = $field;
612
			}
613
		}
614
615
		return $untabbed_fields;
616
	}
617
618
	/**
619
	 * Retrieve all tabs.
620
	 * Create a default tab if there are any untabbed fields.
621
	 *
622
	 * @return array
623
	 */
624
	protected function get_tabs() {
625
		$untabbed_fields = $this->get_untabbed_fields();
626
627
		if ( ! empty( $untabbed_fields ) ) {
628
			$this->create_tab( __( 'General', \Carbon_Fields\TEXT_DOMAIN ), $untabbed_fields, static::TABS_HEAD );
629
		}
630
631
		return $this->tabs;
632
	}
633
634
	/**
635
	 * Build the tabs JSON
636
	 *
637
	 * @return array
638
	 */
639
	protected function get_tabs_json() {
640
		$tabs_json = array();
641
		$tabs = $this->get_tabs();
642
643
		foreach ( $tabs as $tab_name => $fields ) {
644
			foreach ( $fields as $field_name => $field ) {
645
				$tabs_json[ $tab_name ][] = $field_name;
646
			}
647
		}
648
649
		return $tabs_json;
650
	}
651
652
	/**
653
	 * Get custom CSS classes.
654
	 *
655
	 * @return array<string>
656
	 */
657
	public function get_classes() {
658
		return $this->classes;
659
	}
660
661
	/**
662
	 * Set CSS classes that the container should use.
663
	 *
664
	 * @param string|array $classes
665
	 * @return object $this
666
	 */
667
	public function set_classes( $classes ) {
668
		$this->classes = Helper::sanitize_classes( $classes );
0 ignored issues
show
Bug introduced by
It seems like $classes defined by parameter $classes on line 667 can also be of type array; however, Carbon_Fields\Helper\Helper::sanitize_classes() does only seem to accept string|array<integer,obj..._Fields\Helper\strnig>>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
669
		return $this;
670
	}
671
672
	/**
673
	 * Returns an array that holds the container data, suitable for JSON representation.
674
	 *
675
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
676
	 * @return array
677
	 */
678
	public function to_json( $load ) {
679
		$container_data = array(
680
			'id' => $this->id,
681
			'type' => $this->type,
682
			'title' => $this->title,
683
			'classes' => $this->get_classes(),
684
			'settings' => $this->settings,
685
			'fields' => array(),
686
		);
687
688
		$fields = $this->get_fields();
689
		foreach ( $fields as $field ) {
690
			$field_data = $field->to_json( $load );
691
			$container_data['fields'][] = $field_data;
692
		}
693
694
		return $container_data;
695
	}
696
697
	/**
698
	 * Enqueue admin styles
699
	 */
700
	public static function admin_hook_styles() {
701
		wp_enqueue_style( 'carbon-main', \Carbon_Fields\URL . '/assets/bundle.css', array(), \Carbon_Fields\VERSION );
702
	}
703
704
	/**
705
	 * COMMON USAGE METHODS
706
	 */
707
708
	/**
709
	 * Append array of fields to the current fields set. All items of the array
710
	 * must be instances of Field and their names should be unique for all
711
	 * Carbon containers.
712
	 * If a field does not have DataStore already, the container datastore is
713
	 * assigned to them instead.
714
	 *
715
	 * @param array $fields
716
	 * @return object $this
717
	 **/
718
	public function add_fields( $fields ) {
719
		foreach ( $fields as $field ) {
720
			if ( ! is_a( $field, 'Carbon_Fields\\Field\\Field' ) ) {
721
				Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
722
			}
723
724
			$this->verify_unique_field_name( $field->get_name() );
725
726
			$field->set_context( $this->type );
727
			if ( ! $field->get_datastore() ) {
728
				$field->set_datastore( $this->get_datastore(), $this->has_default_datastore() );
729
			}
730
		}
731
732
		$this->fields = array_merge( $this->fields, $fields );
733
734
		return $this;
735
	}
736
737
	/**
738
	 * Configuration function for adding tab with fields
739
	 *
740
	 * @param string $tab_name
741
	 * @param array $fields
742
	 * @return object $this
743
	 */
744
	public function add_tab( $tab_name, $fields ) {
745
		$this->add_fields( $fields );
746
		$this->create_tab( $tab_name, $fields );
747
		return $this;
748
	}
749
}
750