Completed
Pull Request — development (#194)
by
unknown
14:14
created

User_Meta_Container::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\Datastore\Meta_Datastore;
6
use Carbon_Fields\Datastore\User_Meta_Datastore;
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 user meta container
23
	 *
24
	 * @param string $title Unique title of the container
25
	 **/
26
	public function __construct( $title ) {
27
		parent::__construct( $title );
28
29
		if ( ! $this->get_datastore() ) {
30
			$this->set_datastore( new User_Meta_Datastore(), $this->has_default_datastore() );
0 ignored issues
show
Documentation introduced by
$this->has_default_datastore() is of type object<Carbon_Fields\Dat...re\Datastore_Interface>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
31
		}
32
	}
33
34
	/**
35
	 * Bind attach() and save() to the appropriate WordPress actions.
36
	 **/
37
	public function init() {
38
		add_action( 'admin_init', array( $this, '_attach' ) );
39
		add_action( 'profile_update', array( $this, '_save' ), 10, 1 );
40
		add_action( 'user_register', array( $this, '_save' ), 10, 1 );
41
	}
42
43
	/**
44
	 * Perform save operation after successful is_valid_save() check.
45
	 * The call is propagated to all fields in the container.
46
	 *
47
	 * @param int $user_id ID of the user against which save() is ran
48
	 **/
49
	public function save( $user_id ) {
50
		// Unhook action to garantee single save
51
		remove_action( 'profile_update', array( $this, '_save' ) );
52
53
		$this->set_user_id( $user_id );
54
55
		foreach ( $this->fields as $field ) {
56
			$field->set_value_from_input();
57
			$field->save();
58
		}
59
60
		do_action( 'carbon_after_save_user_meta', $user_id );
61
	}
62
63
	/**
64
	 * Checks whether the current request is valid
65
	 *
66
	 * @return bool
67
	 **/
68
	public function is_valid_save( $user_id = 0 ) {
69 View Code Duplication
		if ( ! isset( $_REQUEST[ $this->get_nonce_name() ] ) || ! wp_verify_nonce( $_REQUEST[ $this->get_nonce_name() ], $this->get_nonce_name() ) ) { // Input var okay.
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...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
70
			return false;
71
		} else if ( ! $this->is_valid_attach() ) {
72
			return false;
73
		}
74
75
		return $this->is_valid_save_conditions( $user_id );
76
	}
77
78
	/**
79
	 * Perform checks whether the current save() request is valid
80
	 *
81
	 * @param int $user_id ID of the user against which save() is ran
82
	 * @return bool
83
	 **/
84
	public function is_valid_save_conditions( $user_id ) {
85
		$valid = true;
86
		$user = get_userdata( $user_id );
87
88
		if ( empty( $user->roles ) ) {
89
			return;
90
		}
91
92
		// Check user role
93
		if ( ! empty( $this->settings['show_on']['role'] ) ) {
94
			$allowed_roles = (array) $this->settings['show_on']['role'];
95
96
			// array_shift removed the returned role from the $user_profile->roles
97
			// $roles_to_shift prevents changing of the $user_profile->roles variable
98
			$roles_to_shift = $user->roles;
99
			$profile_role = array_shift( $roles_to_shift );
100
			if ( ! in_array( $profile_role, $allowed_roles ) ) {
101
				$valid = false;
102
			}
103
		}
104
105
		return $valid;
106
	}
107
108
	/**
109
	 * Show the container only on users who have the $role role.
110
	 *
111
	 * @param string $role
112
	 * @return object $this
113
	 **/
114
	public function show_on_user_role( $role ) {
115
		$this->settings['show_on']['role'] = (array) $role;
116
117
		return $this;
118
	}
119
120
	/**
121
	 * Show the container only for users who have either capabilities or roles setup
122
	 *
123
	 * @param array $show_for
124
	 * @return object $this
125
	 **/
126
	public function show_for( $show_for ) {
127
		$this->settings['show_for'] = $this->parse_show_for( $show_for );
128
129
		return $this;
130
	}
131
132
	/**
133
	 * Validate and parse the show_for logic rules.
134
	 *
135
	 * @param array $rules
0 ignored issues
show
Bug introduced by
There is no parameter named $rules. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
136
	 * @return array
137
	 */
138
	protected function parse_show_for( $show_for ) {
139
		if ( ! is_array( $show_for ) ) {
140
			Incorrect_Syntax_Exception::raise( 'Show for argument should be an array.' );
141
		}
142
143
		$allowed_relations = array( 'AND', 'OR' );
144
145
		$parsed_show_for = array(
146
			'relation' => 'AND',
147
		);
148
149
		foreach ( $show_for as $key => $rule ) {
150
			// Check if we have a relation key
151 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...
152
				$relation = strtoupper( $rule );
153
154
				if ( ! in_array( $relation, $allowed_relations ) ) {
155
					Incorrect_Syntax_Exception::raise( 'Invalid relation type ' . $rule . '. ' .
156
					'The rule should be one of the following: "' . implode( '", "', $allowed_relations ) . '"' );
157
				}
158
159
				$parsed_show_for['relation'] = $relation;
160
				continue;
161
			}
162
163
			// Check if the rule is valid
164
			if ( ! is_string( $rule ) || empty( $rule ) ) {
165
				Incorrect_Syntax_Exception::raise( 'Invalid show_for logic rule format. ' .
166
				'The rule should be a string, containing an user capability/role.' );
167
			}
168
169
			$parsed_show_for[] = $rule;
170
		}
171
172
		return $parsed_show_for;
173
	}
174
175
	/**
176
	 * Add the container to the user
177
	 **/
178
	public function attach() {
179
		add_action( 'show_user_profile', array( $this, 'render' ), 10, 1 );
180
		add_action( 'edit_user_profile', array( $this, 'render' ), 10, 1 );
181
		add_action( 'user_new_form', array( $this, 'render' ), 10, 1 );
182
	}
183
184
	/**
185
	 * Whether we're on the user profile page
186
	 **/
187
	public function is_profile_page() {
188
		global $pagenow;
189
190
		return $pagenow === 'profile.php' || $pagenow === 'user-new.php' || $pagenow === 'user-edit.php';
191
	}
192
193
	/**
194
	 * Perform checks whether the container should be attached during the current request
195
	 *
196
	 * @return bool True if the container is allowed to be attached
197
	 **/
198
	public function is_valid_attach() {
199
		if ( ! $this->is_profile_page() || ! $this->is_valid_show_for() ) {
200
			return false;
201
		}
202
203
		return true;
204
	}
205
206
	/**
207
	 * Perform checks whether the container should be seen for the currently logged in user
208
	 *
209
	 * @return bool True if the current user is allowed to see the container
210
	 **/
211
	public function is_valid_show_for() {
212
		$show_for = $this->settings['show_for'];
213
214
		$relation = $show_for['relation'];
215
		unset( $show_for['relation'] );
216
217
		$validated_capabilities_count = 0;
1 ignored issue
show
Comprehensibility Naming introduced by
The variable name $validated_capabilities_count exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
218
		foreach ( $show_for as $capability ) {
219
			if ( current_user_can( $capability ) ) {
220
				$validated_capabilities_count++;
221
			}
222
		}
223
224
		/**
225
		 * When the relation is AND all capabilities must be evaluated to true
226
		 * When the relation is OR at least 1 must be evaluated to true
227
		 */
228
		$min_valid_capabilities_count = $relation === 'AND' ? count( $show_for ) : 1;
1 ignored issue
show
Comprehensibility Naming introduced by
The variable name $min_valid_capabilities_count exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
229
230
		return apply_filters( 'carbon_container_user_meta_is_valid_show_for', $validated_capabilities_count >= $min_valid_capabilities_count, $this );
231
	}
232
233
	/**
234
	 * Output the container markup
235
	 **/
236
	public function render( $user_profile = null ) {
237
		$profile_role = '';
238
239
		if ( is_object( $user_profile ) ) {
240
			$this->set_user_id( $user_profile->ID );
241
242
			// array_shift removed the returned role from the $user_profile->roles
243
			// $roles_to_shift prevents changing of the $user_profile->roles variable
244
			$roles_to_shift = $user_profile->roles;
245
			$profile_role = array_shift( $roles_to_shift );
246
		}
247
248
		include \Carbon_Fields\DIR . '/templates/Container/user_meta.php';
249
	}
250
251
	/**
252
	 * Set the user ID the container will operate with.
253
	 *
254
	 * @param int $user_id
255
	 **/
256
	public function set_user_id( $user_id ) {
257
		$this->user_id = $user_id;
258
		$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_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...
259
	}
260
}
261