Completed
Push — milestone/2.0 ( 9939d7...e0608a )
by
unknown
03:03
created

Post_Meta_Container::is_valid_save()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 1
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
ccs 0
cts 6
cp 0
crap 20
1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\Datastore\Datastore;
6
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
7
8
/**
9
 * Field container designed to extend WordPress custom fields functionality,
10
 * providing easier user interface to add, edit and delete text, media files,
11
 * location information and more.
12
 */
13
class Post_Meta_Container extends Container {
14
	/**
15
	 * ID of the post the container is working with
16
	 *
17
	 * @see init()
18
	 * @var int
19
	 */
20
	protected $post_id;
21
22
	/**
23
	 * List of default container settings
24
	 *
25
	 * @see init()
26
	 * @var array
27
	 */
28
	public $settings = array(
29
		'post_type' => array( 'post' ),
30
		'panel_context' => 'normal',
31
		'panel_priority' => 'high',
32
		'show_on' => array(
33
			'category' => null,
34
			'template_names' => array(),
35
			'not_in_template_names' => array(),
36
			'post_formats' => array(),
37
			'level_limit' => null,
38
			'tax_term_id' => null,
39
			'page_id' => null,
40
			'parent_page_id' => null,
41
			'post_path' => null,
42
		),
43
	);
44
45
	/**
46
	 * Create a new container
47
	 *
48
	 * @param string $unique_id Unique id of the container
49
	 * @param string $title title of the container
50
	 * @param string $type Type of the container
51
	 **/
52
	public function __construct( $unique_id, $title, $type ) {
53
		parent::__construct( $unique_id, $title, $type );
54
55
		if ( ! $this->get_datastore() ) {
56
			$this->set_datastore( Datastore::make( 'post_meta' ), $this->has_default_datastore() );
57
		}
58
	}
59
60
	/**
61
	 * Create DataStore instance, set post ID to operate with (if such exists).
62
	 * Bind attach() and save() to the appropriate WordPress actions.
63
	 **/
64
	public function init() {
65
		$request_post_id = isset( $_GET['post'] ) ? intval( $_GET['post'] ) : 0;
1 ignored issue
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
66
		if ( $request_post_id > 0 ) {
67
			$this->set_post_id( $request_post_id );
68
		}
69
70
		// force post_type to be array
71
		if ( ! is_array( $this->settings['post_type'] ) ) {
72
			$this->settings['post_type'] = array( $this->settings['post_type'] );
73
		}
74
75
		add_action( 'admin_init', array( $this, '_attach' ) );
76
		add_action( 'save_post', array( $this, '_save' ) );
77
78
		// support for attachments
79
		add_action( 'add_attachment', array( $this, '_save' ) );
80
		add_action( 'edit_attachment', array( $this, '_save' ) );
81
	}
82
83
	/**
84
	 * Perform checks whether the current save() request is valid
85
	 * Possible errors are triggering save() for autosave requests
86
	 * or performing post save outside of the post edit page (like Quick Edit)
87
	 *
88
	 * @param int $post_id ID of the post against which save() is ran
89
	 * @return bool
90
	 **/
91
	public function is_valid_save( $post_id = 0 ) {
92
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
93
			return false;
94
		}
95
96
		if ( ! $this->verified_nonce_in_request() ) {
97
			return false;
98
		}
99
100
		return $this->is_valid_attach_for_object( $post_id );
101
	}
102
103
	/**
104
	 * Perform save operation after successful is_valid_save() check.
105
	 * The call is propagated to all fields in the container.
106
	 *
107
	 * @param int $post_id ID of the post against which save() is ran
108
	 **/
109 View Code Duplication
	public function save( $post_id = null ) {
110
		// Unhook action to garantee single save
111
		remove_action( 'save_post', array( $this, '_save' ) );
112
113
		$this->set_post_id( $post_id );
114
115
		foreach ( $this->fields as $field ) {
116
			$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...
117
			$field->save();
118
		}
119
120
		do_action( 'carbon_after_save_custom_fields', $post_id );
121
		do_action( 'carbon_after_save_post_meta', $post_id );
122
	}
123
124
	/**
125
	 * Perform checks whether the container should be attached during the current request
126
	 *
127
	 * @return bool True if the container is allowed to be attached
128
	 **/
129
	public function is_valid_attach_for_request() {
130
		global $pagenow;
131
132
		if ( $pagenow !== 'post.php' && $pagenow !== 'post-new.php' ) {
133
			return false;
134
		}
135
136
		// Post types check
137
		if ( ! empty( $this->settings['post_type'] ) ) {
138
			$post_type = '';
139
			$request_post_type = isset( $_GET['post_type'] ) ? $_GET['post_type'] : '';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
140
141
			if ( $this->post_id ) {
142
				$post_type = get_post_type( $this->post_id );
143
			} elseif ( ! empty( $request_post_type ) ) {
144
				$post_type = $request_post_type;
145
			} elseif ( $pagenow === 'post-new.php' ) {
146
				$post_type = 'post';
147
			}
148
149
			if ( ! $post_type || ! in_array( $post_type, $this->settings['post_type'] ) ) {
150
				return false;
151
			}
152
		}
153
154
		// Check show on conditions
155
		foreach ( $this->settings['show_on'] as $condition => $value ) {
156
			if ( is_null( $value ) || $condition !== 'page_id' ) {
157
				continue;
158
			}
159
160
			$post = get_post( $this->post_id );
161
			if ( ! $this->is_condition_fullfilled( $condition, $value, $post ) ) {
162
				return false;
163
			}
164
		}
165
166
		return true;
167
	}
168
169
	/**
170
	 * Check container attachment rules against object id
171
	 *
172
	 * @param int $object_id
173
	 * @return bool
174
	 **/
175
	public function is_valid_attach_for_object( $object_id = null ) {
176
		$post = get_post( intval( $object_id ) );
177
178
		if ( ! $post ) {
179
			return false;
180
		}
181
182
		// Check post type
183
		if ( ! in_array( $post->post_type, $this->settings['post_type'] ) ) {
184
			return false;
185
		}
186
187
		// Check show on conditions
188
		foreach ( $this->settings['show_on'] as $condition => $value ) {
189
			if ( is_null( $value ) ) {
190
				continue;
191
			}
192
193
			if ( ! $this->is_condition_fullfilled( $condition, $value, $post ) ) {
194
				return false;
195
			}
196
		}
197
198
		return true;
199
	}
200
201
	/**
202
	 * Add meta box for each of the container post types
203
	 **/
204
	public function attach() {
205
		foreach ( $this->settings['post_type'] as $post_type ) {
206
			add_meta_box(
207
				$this->id,
208
				$this->title,
209
				array( $this, 'render' ),
210
				$post_type,
211
				$this->settings['panel_context'],
212
				$this->settings['panel_priority']
213
			);
214
		}
215
216
		foreach ( $this->settings['post_type'] as $post_type ) {
217
			add_filter( "postbox_classes_{$post_type}_{$this->id}", array( $this, 'add_postbox_classes' ) );
218
		}
219
	}
220
221
	/**
222
	 * Classes to add to the post meta box
223
	 */
224
	public function add_postbox_classes( $classes ) {
225
		$classes[] = 'carbon-box';
226
		return $classes;
227
	}
228
229
	/**
230
	 * Output the container markup
231
	 **/
232
	public function render() {
233
		include \Carbon_Fields\DIR . '/templates/Container/post_meta.php';
234
	}
235
236
	/**
237
	 * Set the post ID the container will operate with.
238
	 *
239
	 * @param int $post_id
240
	 **/
241
	public function set_post_id( $post_id ) {
242
		$this->post_id = $post_id;
243
		$this->get_datastore()->set_id( $post_id );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Carbon_Fields\Datastore\Datastore_Interface as the method set_id() does only exist in the following implementations of said interface: Carbon_Fields\Datastore\Comment_Meta_Datastore, Carbon_Fields\Datastore\Meta_Datastore, Carbon_Fields\Datastore\Nav_Menu_Item_Datastore, Carbon_Fields\Datastore\Post_Meta_Datastore, Carbon_Fields\Datastore\Term_Meta_Datastore, Carbon_Fields\Datastore\User_Meta_Datastore.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
244
	}
245
246
	/**
247
	 * CONDITION TOOLS
248
	 */
249
	
250
	/**
251
	 * Check a condition against all supported conditions
252
	 * 
253
	 * @param string $condition
254
	 * @param mixed $value
255
	 * @param WP_Post $post
256
	 * @return bool
257
	 */
258
	protected function is_condition_fullfilled( $condition, $value, $post ) {
259
		switch ( $condition ) {
260
			// show_on_post_format
261
			case 'post_formats':
262
				if ( ! $this->condition_is_post_of_format( $post, $value ) ) {
263
					return false;
264
				}
265
				break;
266
267
			// show_on_taxonomy_term or show_on_category
268
			case 'category':
269
				$this->show_on_category( $value );
270
271
				/* fall-through intended */
272
			case 'tax_term_id':
273
				$has_term = has_term(
274
					intval( $this->settings['show_on']['tax_term_id'] ),
275
					$this->settings['show_on']['tax_slug'],
276
					$post->ID
277
				);
278
				if ( ! $has_term ) {
279
					return false;
280
				}
281
				break;
282
283
			// show_on_level
284
			case 'level_limit':
285
				if ( ! $this->condition_is_post_on_level( $post, $value ) ) {
286
					return false;
287
				}
288
				break;
289
290
			// show_on_page
291
			case 'page_id':
292
				if ( $post->ID !== intval( $value ) ) {
293
					return false;
294
				}
295
				break;
296
297
			// show_on_page_children
298
			case 'parent_page_id':
299
				if ( $post->post_parent !== $value ) {
300
					return false;
301
				}
302
				break;
303
304
			// show_on_template
305
			case 'template_names':
306
				if ( ! empty( $value ) && ! $this->condition_is_post_using_template( $post, $value ) ) {
307
					return false;
308
				}
309
				break;
310
311
			// hide_on_template
312
			case 'not_in_template_names':
313
				if ( ! empty( $value ) && $this->condition_is_post_using_template( $post, $value ) ) {
314
					return false;
315
				}
316
				break;
317
		}
318
		return true;
319
	}
320
	
321
	/**
322
	 * Check if a post is of a given post format
323
	 * 
324
	 * @param WP_Post $post
325
	 * @param string $format
326
	 * @return bool
327
	 */
328
	protected function condition_is_post_of_format( $post, $format ) {
329
		if ( empty( $format ) || $post->post_type !== 'post' ) {
330
			return true; // this doesn't make sense - returning true for a post that does not support formats (kept for backwards compatibility)
331
		}
332
333
		$current_format = get_post_format( $post->ID );
334
		if ( ! in_array( $current_format, $format ) ) {
335
			return false;
336
		}
337
338
		return true;
339
	}
340
	
341
	/**
342
	 * Check if a post is on a specific level in it's hierarchy
343
	 * 
344
	 * @param WP_Post $post
345
	 * @param int $level
346
	 * @return bool
347
	 */
348
	protected function condition_is_post_on_level( $post, $level ) {
349
		$level = intval( $level );
350
		$post_level = count( get_post_ancestors( $post->ID ) ) + 1;
351
		return ( $post_level === $level );
352
	}
353
	
354
	/**
355
	 * Check if a post uses one of a given array of templates
356
	 * 
357
	 * @param WP_Post $post
358
	 * @param string|array<string> $templates
359
	 * @return bool
360
	 */
361
	protected function condition_is_post_using_template( $post, $templates ) {
362
		$templates = is_array( $templates ) ? $templates : array( $templates );
363
		$current_template = get_post_meta( $post->ID, '_wp_page_template', true );
364
		return in_array( $current_template, $templates );
365
	}
366
367
	/**
368
	 * COMMON USAGE METHODS
369
	 */
370
371
	/**
372
	 * Show the container only on particular page referenced by it's path.
373
	 *
374
	 * @param int|string $page page ID or page path
375
	 * @return object $this
376
	 **/
377 4
	public function show_on_page( $page ) {
378 4
		$page_id = absint( $page );
379
380 4
		if ( $page_id && $page_id == $page ) {
381 2
			$page_obj = get_post( $page_id );
382 2
		} else {
383 2
			$page_obj = get_page_by_path( $page );
384
		}
385
386 4
		$this->show_on_post_type( 'page' );
387
388 4
		if ( $page_obj ) {
389 4
			$this->settings['show_on']['page_id'] = $page_obj->ID;
390 4
		} else {
391
			$this->settings['show_on']['page_id'] = -1;
392
		}
393
394 4
		return $this;
395
	}
396
397
	/**
398
	 * Show the container only on pages whose parent is referenced by $parent_page_path.
399
	 *
400
	 * @param string $parent_page_path
401
	 * @return object $this
402
	 **/
403 1
	public function show_on_page_children( $parent_page_path ) {
404 1
		$page = get_page_by_path( $parent_page_path );
405
406 1
		$this->show_on_post_type( 'page' );
407
408 1
		if ( $page ) {
409 1
			$this->settings['show_on']['parent_page_id'] = $page->ID;
410 1
		} else {
411
			$this->settings['show_on']['parent_page_id'] = -1;
412
		}
413
414 1
		return $this;
415
	}
416
417
	/**
418
	 * Show the container only on posts from the specified category.
419
	 *
420
	 * @see show_on_taxonomy_term()
421
	 *
422
	 * @param string $category_slug
423
	 * @return object $this
424
	 **/
425
	public function show_on_category( $category_slug ) {
426
		$this->settings['show_on']['category'] = $category_slug;
427
428
		return $this->show_on_taxonomy_term( $category_slug, 'category' );
429
	}
430
431
	/**
432
	 * Show the container only on pages whose template has filename $template_path.
433
	 *
434
	 * @param string|array $template_path
435
	 * @return object $this
436
	 **/
437 2
	public function show_on_template( $template_path ) {
438
		// Backwards compatibility where only pages support templates
439 2
		if ( version_compare( get_bloginfo( 'version' ), '4.7', '<' ) ) {
440 2
			$this->show_on_post_type( 'page' );
441 2
		}
442
443 2
		if ( is_array( $template_path ) ) {
444 1
			foreach ( $template_path as $path ) {
445 1
				$this->show_on_template( $path );
446 1
			}
447
448 1
			return $this;
449
		}
450
451 2
		$this->settings['show_on']['template_names'][] = $template_path;
452
453 2
		return $this;
454
	}
455
456
	/**
457
	 * Hide the container from pages whose template has filename $template_path.
458
	 *
459
	 * @param string|array $template_path
460
	 * @return object $this
461
	 **/
462
	public function hide_on_template( $template_path ) {
463
		if ( is_array( $template_path ) ) {
464
			foreach ( $template_path as $path ) {
465
				$this->hide_on_template( $path );
466
			}
467
			return $this;
468
		}
469
470
		$this->settings['show_on']['not_in_template_names'][] = $template_path;
471
472
		return $this;
473
	}
474
475
	/**
476
	 * Show the container only on hierarchical posts of level $level.
477
	 * Levels start from 1 (top level post)
478
	 *
479
	 * @param int $level
480
	 * @return object $this
481
	 **/
482
	public function show_on_level( $level ) {
483
		if ( $level < 0 ) {
484
			Incorrect_Syntax_Exception::raise( 'Invalid level limitation (' . $level . ')' );
485
		}
486
487
		$this->settings['show_on']['level_limit'] = $level;
488
489
		return $this;
490
	}
491
492
	/**
493
	 * Show the container only on posts which have term $term_slug from the $taxonomy_slug taxonomy.
494
	 *
495
	 * @param string $taxonomy_slug
496
	 * @param string $term_slug
497
	 * @return object $this
498
	 **/
499
	public function show_on_taxonomy_term( $term_slug, $taxonomy_slug ) {
500
		$term = get_term_by( 'slug', $term_slug, $taxonomy_slug );
501
502
		$this->settings['show_on']['tax_slug'] = $taxonomy_slug;
503
		$this->settings['show_on']['tax_term'] = $term_slug;
504
		$this->settings['show_on']['tax_term_id'] = $term ? $term->term_id : null;
505
506
		return $this;
507
	}
508
509
	/**
510
	 * Show the container only on posts from the specified format.
511
	 * Learn more about {@link http://codex.wordpress.org/Post_Formats Post Formats (Codex)}
512
	 *
513
	 * @param string|array $post_format Name of the format as listed on Codex
514
	 * @return object $this
515
	 **/
516
	public function show_on_post_format( $post_format ) {
517
		if ( is_array( $post_format ) ) {
518
			foreach ( $post_format as $format ) {
519
				$this->show_on_post_format( $format );
520
			}
521
			return $this;
522
		}
523
524
		if ( $post_format === 'standard' ) {
525
			$post_format = 0;
526
		}
527
528
		$this->settings['show_on']['post_formats'][] = strtolower( $post_format );
529
530
		return $this;
531
	}
532
533
	/**
534
	 * Show the container only on posts from the specified type(s).
535
	 *
536
	 * @param string|array $post_types
537
	 * @return object $this
538
	 **/
539
	public function show_on_post_type( $post_types ) {
540
		$post_types = (array) $post_types;
541
542
		$this->settings['post_type'] = $post_types;
543
544
		return $this;
545
	}
546
547
	/**
548
	 * Sets the meta box container context
549
	 *
550
	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box
551
	 * @param string $context ('normal', 'advanced' or 'side')
552
	 */
553
	public function set_context( $context ) {
554
		$this->settings['panel_context'] = $context;
555
556
		return $this;
557
	}
558
559
	/**
560
	 * Sets the meta box container priority
561
	 *
562
	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box
563
	 * @param string $priority ('high', 'core', 'default' or 'low')
564
	 */
565
	public function set_priority( $priority ) {
566
		$this->settings['panel_priority'] = $priority;
567
568
		return $this;
569
	}
570
}
571