Completed
Push — milestone/2.0 ( 8a1186...26a446 )
by
unknown
04:33
created

User_Meta_Container::attach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
rs 9.4285
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
class User_Meta_Container extends Container {
9
	protected $user_id;
10
11
	public $settings = array(
12
		'show_on' => array(
13
			'role' => array(),
14
		),
15
		'show_for' => array(
16
			'relation' => 'AND',
17
			'edit_users',
18
		),
19
	);
20
21
	/**
22
	 * Create a new container
23
	 *
24
	 * @param string $unique_id Unique id of the container
25
	 * @param string $title title of the container
26
	 * @param string $type Type of the container
27
	 **/
28 View Code Duplication
	public function __construct( $unique_id, $title, $type ) {
29
		parent::__construct( $unique_id, $title, $type );
30
31
		if ( ! $this->get_datastore() ) {
32
			$this->set_datastore( Datastore::make( 'user_meta' ), $this->has_default_datastore() );
33
		}
34
	}
35
36
	/**
37
	 * Bind attach() and save() to the appropriate WordPress actions.
38
	 **/
39
	public function init() {
40
		add_action( 'admin_init', array( $this, '_attach' ) );
41
		add_action( 'profile_update', array( $this, '_save' ), 10, 1 );
42
		add_action( 'user_register', array( $this, '_save' ), 10, 1 );
43
	}
44
45
	/**
46
	 * Checks whether the current request is valid
47
	 *
48
	 * @return bool
49
	 **/
50
	public function is_valid_save( $user_id = 0 ) {
51
		if ( ! $this->verified_nonce_in_request() ) {
52
			return false;
53
		}
54
55
		if ( ! $this->is_valid_attach_for_request() ) {
56
			return false;
57
		}
58
59
		return $this->is_valid_attach_for_object( $user_id );
60
	}
61
62
	/**
63
	 * Perform save operation after successful is_valid_save() check.
64
	 * The call is propagated to all fields in the container.
65
	 *
66
	 * @param int $user_id ID of the user against which save() is ran
67
	 **/
68 View Code Duplication
	public function save( $user_id ) {
69
		// Unhook action to garantee single save
70
		remove_action( 'profile_update', array( $this, '_save' ) );
71
72
		$this->set_user_id( $user_id );
73
74
		foreach ( $this->fields as $field ) {
75
			$field->set_value_from_input();
76
			$field->save();
77
		}
78
79
		do_action( 'carbon_after_save_user_meta', $user_id );
80
	}
81
82
	/**
83
	 * Perform checks whether the container should be attached during the current request
84
	 *
85
	 * @return bool True if the container is allowed to be attached
86
	 **/
87
	public function is_valid_attach_for_request() {
88
		if ( ! $this->is_profile_page() || ! $this->is_valid_show_for() ) {
89
			return false;
90
		}
91
92
		return true;
93
	}
94
95
	/**
96
	 * Check container attachment rules against object id
97
	 *
98
	 * @return bool
99
	 **/
100
	public function is_valid_attach_for_object( $object_id = null ) {
101
		$valid = true;
102
		$user_id = $object_id;
103
		$user = get_userdata( $user_id );
104
105
		if ( ! $user  ) {
106
			return false;
107
		}
108
109
		if ( empty( $user->roles ) ) {
110
			return;
111
		}
112
113
		// Check user role
114
		if ( ! empty( $this->settings['show_on']['role'] ) ) {
115
			$allowed_roles = (array) $this->settings['show_on']['role'];
116
117
			// array_shift removed the returned role from the $user_profile->roles
118
			// $roles_to_shift prevents changing of the $user_profile->roles variable
119
			$roles_to_shift = $user->roles;
120
			$profile_role = array_shift( $roles_to_shift );
121
			if ( ! in_array( $profile_role, $allowed_roles ) ) {
122
				$valid = false;
123
			}
124
		}
125
126
		return $valid;
127
	}
128
129
	/**
130
	 * Add the container to the user
131
	 **/
132
	public function attach() {
133
		add_action( 'show_user_profile', array( $this, 'render' ), 10, 1 );
134
		add_action( 'edit_user_profile', array( $this, 'render' ), 10, 1 );
135
		add_action( 'user_new_form', array( $this, 'render' ), 10, 1 );
136
	}
137
138
	/**
139
	 * Whether we're on the user profile page
140
	 **/
141
	public function is_profile_page() {
142
		global $pagenow;
143
144
		return $pagenow === 'profile.php' || $pagenow === 'user-new.php' || $pagenow === 'user-edit.php';
145
	}
146
147
	/**
148
	 * Perform checks whether the container should be seen for the currently logged in user
149
	 *
150
	 * @return bool True if the current user is allowed to see the container
151
	 **/
152
	public function is_valid_show_for() {
153
		$show_for = $this->settings['show_for'];
154
155
		$relation = $show_for['relation'];
156
		unset( $show_for['relation'] );
157
158
		$validated_capabilities_count = 0;
159
		foreach ( $show_for as $capability ) {
160
			if ( current_user_can( $capability ) ) {
161
				$validated_capabilities_count++;
162
			}
163
		}
164
165
		/**
166
		 * When the relation is AND all capabilities must be evaluated to true
167
		 * When the relation is OR at least 1 must be evaluated to true
168
		 */
169
		$min_valid_capabilities_count = $relation === 'AND' ? count( $show_for ) : 1;
170
171
		return apply_filters( 'carbon_container_user_meta_is_valid_show_for', $validated_capabilities_count >= $min_valid_capabilities_count, $this );
172
	}
173
174
	/**
175
	 * Output the container markup
176
	 **/
177
	public function render( $user_profile = null ) {
178
		$profile_role = '';
179
180
		if ( is_object( $user_profile ) ) {
181
			$this->set_user_id( $user_profile->ID );
182
183
			// array_shift removed the returned role from the $user_profile->roles
184
			// $roles_to_shift prevents changing of the $user_profile->roles variable
185
			$roles_to_shift = $user_profile->roles;
186
			$profile_role = array_shift( $roles_to_shift );
187
		}
188
189
		include \Carbon_Fields\DIR . '/templates/Container/user_meta.php';
190
	}
191
192
	/**
193
	 * Set the user ID the container will operate with.
194
	 *
195
	 * @param int $user_id
196
	 **/
197
	public function set_user_id( $user_id ) {
198
		$this->user_id = $user_id;
199
		$this->get_datastore()->set_id( $user_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...
200
	}
201
202
	/**
203
	 * Validate and parse the show_for logic rules.
204
	 *
205
	 * @param array $show_for
206
	 * @return array
207
	 */
208
	protected function parse_show_for( $show_for ) {
209
		if ( ! is_array( $show_for ) ) {
210
			Incorrect_Syntax_Exception::raise( 'Show for argument should be an array.' );
211
		}
212
213
		$allowed_relations = array( 'AND', 'OR' );
214
215
		$parsed_show_for = array(
216
			'relation' => 'AND',
217
		);
218
219
		foreach ( $show_for as $key => $rule ) {
220
			// Check if we have a relation key
221 View Code Duplication
			if ( $key === 'relation' ) {
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...
222
				$relation = strtoupper( $rule );
223
224
				if ( ! in_array( $relation, $allowed_relations ) ) {
225
					Incorrect_Syntax_Exception::raise( 'Invalid relation type ' . $rule . '. ' .
226
					'The rule should be one of the following: "' . implode( '", "', $allowed_relations ) . '"' );
227
				}
228
229
				$parsed_show_for['relation'] = $relation;
230
				continue;
231
			}
232
233
			// Check if the rule is valid
234
			if ( ! is_string( $rule ) || empty( $rule ) ) {
235
				Incorrect_Syntax_Exception::raise( 'Invalid show_for logic rule format. ' .
236
				'The rule should be a string, containing an user capability/role.' );
237
			}
238
239
			$parsed_show_for[] = $rule;
240
		}
241
242
		return $parsed_show_for;
243
	}
244
245
	/**
246
	 * COMMON USAGE METHODS
247
	 */
248
249
	/**
250
	 * Show the container only on users who have the $role role.
251
	 *
252
	 * @param string $role
253
	 * @return object $this
254
	 **/
255
	public function show_on_user_role( $role ) {
256
		$this->settings['show_on']['role'] = (array) $role;
257
258
		return $this;
259
	}
260
261
	/**
262
	 * Show the container only for users who have either capabilities or roles setup
263
	 *
264
	 * @param array $show_for
265
	 * @return object $this
266
	 **/
267
	public function show_for( $show_for ) {
268
		$this->settings['show_for'] = $this->parse_show_for( $show_for );
269
270
		return $this;
271
	}
272
}
273