Passed
Push — master ( a4754c...e0c6ec )
by Nazar
05:32
created

Profile   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 316
rs 2.5423
c 0
b 0
f 0
ccs 146
cts 146
cp 1
wmc 72

11 Methods

Rating   Name   Duplication   Size   Complexity  
C set_internal_allowed() 0 30 13
C get_internal() 0 38 11
B get_id() 0 20 5
A username() 0 10 4
A get_users_columns() 0 2 1
D set() 0 30 9
A initialize_data() 0 5 1
B avatar() 0 13 6
A set_internal_correct_login() 0 12 4
C set_internal() 0 47 15
A get() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like Profile 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.

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

1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2017, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\User;
9
use
10
	cs\Config,
11
	cs\Event,
12
	cs\Language,
13
	cs\Session,
14
	cs\User,
15
	h;
16
17
/**
18
 * Trait that contains all methods for `cs\User` for working with user's profile
19
 *
20
 * @property \cs\Cache\Prefix $cache
21
 * @property int              $id
22
 *
23
 * @method \cs\DB\_Abstract db()
24
 * @method \cs\DB\_Abstract db_prime()
25
 * @method false|int[]      get_groups(false|int $user)
26
 */
27
trait Profile {
28
	/**
29
	 * Copy of columns list of users table for internal needs without Cache usage
30
	 * @var array
31
	 */
32
	protected $users_columns = [];
33
	/**
34
	 * Local cache of users data
35
	 * @var array
36
	 */
37
	protected $data = [];
38 69
	protected function initialize_data () {
39 69
		$this->users_columns = $this->cache->get(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->get('colum...ion(...) { /* ... */ }) can also be of type false. However, the property $users_columns is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
40 69
			'columns',
41
			function () {
42 6
				return $this->db()->columns('[prefix]users');
43 69
			}
44
		);
45 69
	}
46
	/**
47
	 * Get data item of specified user
48
	 *
49
	 * @param string|string[] $item
50
	 * @param false|int       $user If not specified - current user assumed
51
	 *
52
	 * @return false|int|mixed[]|string|Properties If <i>$item</i> is integer - cs\User\Properties object will be returned
53
	 */
54 51
	public function get ($item, $user = false) {
55 51
		if (is_scalar($item) && ctype_digit((string)$item)) {
56 3
			return new Properties($item);
0 ignored issues
show
Bug introduced by
$item of type string is incompatible with the type integer expected by parameter $user of cs\User\Properties::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

56
			return new Properties(/** @scrutinizer ignore-type */ $item);
Loading history...
57
		}
58 51
		return $this->get_internal($item, $user);
59
	}
60
	/**
61
	 * Get data item of specified user
62
	 *
63
	 * @param string|string[] $item
64
	 * @param false|int       $user If not specified - current user assumed
65
	 *
66
	 * @return false|int|string|mixed[]
67
	 */
68 51
	protected function get_internal ($item, $user = false) {
69 51
		$user = (int)$user ?: $this->id;
70 51
		if (isset($this->data[$user])) {
71 48
			$data = $this->data[$user];
72
		} else {
73 51
			$data = $this->cache->get(
74 51
				$user,
75
				function () use ($user) {
76 48
					return $this->db()->qf(
77
						"SELECT *
0 ignored issues
show
Bug introduced by
EncapsedNode of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qf(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

77
						/** @scrutinizer ignore-type */ "SELECT *
Loading history...
78
						FROM `[prefix]users`
79 48
						WHERE `id` = $user
80
						LIMIT 1"
81 48
					) ?: false;
82 51
				}
83
			);
84 51
			if (!$data) {
85 9
				return false;
86 51
			} elseif ($this->memory_cache || $user == User::GUEST_ID) {
0 ignored issues
show
Bug Best Practice introduced by
The property memory_cache does not exist on cs\User\Profile. Did you maybe forget to declare it?
Loading history...
87 51
				$this->data[$user] = $data;
88
			}
89
		}
90
		/**
91
		 * If get an array of values
92
		 */
93 51
		if (is_array($item)) {
94 48
			$result = [];
95
			/**
96
			 * Trying to get value from the local cache, or make up an array of missing values
97
			 */
98 48
			foreach ($item as $i) {
99 48
				if (in_array($i, $this->users_columns)) {
100 48
					$result[$i] = $data[$i];
101
				}
102
			}
103 48
			return $result;
104
		}
105 48
		return in_array($item, $this->users_columns) ? $data[$item] : false;
106
	}
107
	/**
108
	 * Set data item of specified user
109
	 *
110
	 * @param array|string    $item Item-value array may be specified for setting several items at once
111
	 * @param int|null|string $value
112
	 * @param false|int       $user If not specified - current user assumed
113
	 *
114
	 * @return bool
115
	 */
116 42
	public function set ($item, $value = null, $user = false) {
117 42
		$user     = (int)$user ?: $this->id;
118 42
		$data_set = [];
119 42
		$data     = is_array($item) ? $item : [$item => $value];
120 42
		$result   = true;
121 42
		foreach (xap($data) as $i => $v) {
122 42
			$result = $result && $this->set_internal($i, $v, $user, $data_set);
123
		}
124 42
		if (!$result) {
125 6
			return false;
126
		}
127 42
		if (!$data_set) {
0 ignored issues
show
introduced by
The condition ! $data_set can never be false.
Loading history...
128 3
			return true;
129
		}
130 42
		$this->set_internal_correct_login($data_set, $user);
131 42
		$update = [];
132 42
		foreach (array_keys($data_set) as $column) {
133 42
			$update[] = "`$column` = ?";
134
		}
135 42
		$update = implode(', ', $update);
136 42
		$result = $this->db_prime()->q(
137
			"UPDATE `[prefix]users`
138 42
			SET $update
139 42
			WHERE `id` = '$user'",
140 42
			$data_set
141
		);
142 42
		if ($result) {
143 42
			unset($this->data[$user], $this->cache->$user);
144
		}
145 42
		return (bool)$result;
146
	}
147
	/**
148
	 * Set data item of specified user
149
	 *
150
	 * @param string     $item Item-value array may be specified for setting several items at once
151
	 * @param int|string $value
152
	 * @param int        $user If not specified - current user assumed
153
	 * @param array      $data_set
154
	 *
155
	 * @return bool
156
	 */
157 42
	protected function set_internal ($item, $value, $user, &$data_set) {
158 42
		if (!$this->set_internal_allowed($user, $item, $value)) {
159 6
			return false;
160
		}
161 42
		$old_value = $this->get($item, $user);
162 42
		if ($value == $old_value) {
163 6
			return true;
164
		}
165 42
		if ($item == 'language') {
166 9
			$value = $value && Language::instance()->get('clanguage', $value) == $value ? $value : '';
167 42
		} elseif ($item == 'timezone') {
168 6
			$value = in_array($value, get_timezones_list(), true) ? $value : '';
169 42
		} elseif ($item == 'avatar') {
170
			if (
171 6
				strpos($value, 'http://') !== 0 &&
172 6
				strpos($value, 'https://') !== 0
173
			) {
174 3
				$value = '';
175
			}
176 6
			$Event = Event::instance();
177 6
			$Event->fire(
178 6
				'System/upload_files/del_tag',
179
				[
180 6
					'url' => $old_value,
181 6
					'tag' => "users/$user/avatar"
182
				]
183
			);
184 6
			$Event->fire(
185 6
				'System/upload_files/add_tag',
186
				[
187 6
					'url' => $value,
188 6
					'tag' => "users/$user/avatar"
189
				]
190
			);
191
		}
192
		/**
193
		 * @var string $item
194
		 */
195 42
		$data_set[$item] = $value;
196 42
		if (in_array($item, ['login', 'email'], true)) {
197 6
			$old_value               = $this->get($item.'_hash', $user);
198 6
			$data_set[$item.'_hash'] = hash('sha224', $value);
199 6
			unset($this->cache->$old_value);
200 42
		} elseif ($item == 'password_hash' || ($item == 'status' && $value != User::STATUS_ACTIVE)) {
201 42
			Session::instance()->del_all($user);
202
		}
203 42
		return true;
204
	}
205
	/**
206
	 * Check whether setting specified item to specified value for specified user is allowed
207
	 *
208
	 * @param int    $user
209
	 * @param string $item
210
	 * @param string $value
211
	 *
212
	 * @return bool
213
	 */
214 42
	protected function set_internal_allowed ($user, $item, $value) {
215
		if (
216 42
			$user == User::GUEST_ID ||
217 42
			$item == 'id' ||
218 42
			!in_array($item, $this->users_columns, true)
219
		) {
220 6
			return false;
221
		}
222 42
		if (in_array($item, ['login', 'email'], true)) {
223 6
			$value = mb_strtolower($value);
224
			if (
225 6
				$item == 'email' &&
226 6
				!filter_var($value, FILTER_VALIDATE_EMAIL)
227
			) {
228 3
				return false;
229
			}
230
			if (
231 6
				$item == 'login' &&
232 6
				filter_var($value, FILTER_VALIDATE_EMAIL) &&
233 6
				$value != $this->get('email', $user)
234
			) {
235 3
				return false;
236
			}
237 6
			if ($value == $this->get($item, $user)) {
238 6
				return true;
239
			}
240 6
			$existing_user = $this->get_id(hash('sha224', $value)) ?: $user;
241 6
			return $value && $existing_user == $user;
242
		}
243 42
		return true;
244
	}
245
	/**
246
	 * A bit tricky here
247
	 *
248
	 * User is allowed to change login to own email, but not to any other email. However, when user changes email, it might happen that login will remain to
249
	 * be the same as previous email, so we need to change login to new email as well.
250
	 *
251
	 * @param array $data_set
252
	 * @param int   $user
253
	 */
254 42
	protected function set_internal_correct_login (&$data_set, $user) {
255
		/**
256
		 * @var array $old_data
257
		 */
258 42
		$old_data      = $this->get(['login', 'email'], $user);
259 42
		$current_login = isset($data_set['login']) ? $data_set['login'] : $old_data['login'];
260
		if (
261 42
			isset($data_set['email']) &&
262 42
			$current_login == $old_data['email']
263
		) {
264 3
			$data_set['login']      = $data_set['email'];
265 3
			$data_set['login_hash'] = $data_set['email_hash'];
266
		}
267 42
	}
268
	/**
269
	 * Get user id by login or email hash (sha224) (hash from lowercase string)
270
	 *
271
	 * @param  string $login_hash Login or email hash
272
	 *
273
	 * @return false|int User id if found and not guest, otherwise - boolean <i>false</i>
274
	 */
275 42
	public function get_id ($login_hash) {
276 42
		if (!preg_match('/^[0-9a-z]{56}$/', $login_hash)) {
277 6
			return false;
278
		}
279 42
		$id = $this->cache->get(
280 42
			$login_hash,
281 42
			function () use ($login_hash) {
282 42
				return (int)$this->db()->qfs(
283 42
					"SELECT `id`
0 ignored issues
show
Bug introduced by
'SELECT `id` FROM `...h` = '%s' LIMIT 1' of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
					/** @scrutinizer ignore-type */ "SELECT `id`
Loading history...
284
					FROM `[prefix]users`
285
					WHERE
286
						`login_hash`	= '%s' OR
287
						`email_hash`	= '%s'
288
					LIMIT 1",
289 42
					$login_hash,
290 42
					$login_hash
291 42
				) ?: false;
292 42
			}
293
		);
294 42
		return $id && $id != User::GUEST_ID ? $id : false;
295
	}
296
	/**
297
	 * Get user avatar, if no one present - uses Gravatar
298
	 *
299
	 * @param int|null  $size Avatar size, if not specified or resizing is not possible - original image is used
300
	 * @param false|int $user If not specified - current user assumed
301
	 *
302
	 * @return string
303
	 */
304 12
	public function avatar ($size = null, $user = false) {
305 12
		$user         = (int)$user ?: $this->id;
306 12
		$avatar       = $this->get('avatar', $user);
307 12
		$Config       = Config::instance();
308 12
		$guest_avatar = $Config->core_url().'/assets/img/guest.svg';
309 12
		if (!$avatar && $this->id != User::GUEST_ID && $Config->core['gravatar_support']) {
310 6
			$email_hash = md5($this->get('email', $user));
0 ignored issues
show
Bug introduced by
$this->get('email', $user) of type cs\User\Properties|false is incompatible with the type string expected by parameter $str of md5(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
			$email_hash = md5(/** @scrutinizer ignore-type */ $this->get('email', $user));
Loading history...
311 6
			$avatar     = "https://www.gravatar.com/avatar/$email_hash?d=mm&s=$size&d=".urlencode($guest_avatar);
312
		}
313 12
		if (!$avatar) {
314 12
			$avatar = $guest_avatar;
315
		}
316 12
		return h::prepare_url($avatar, true);
317
	}
318
	/**
319
	 * Get user name or login or email, depending on existing information
320
	 *
321
	 * @param false|int $user If not specified - current user assumed
322
	 *
323
	 * @return string
324
	 */
325 12
	public function username ($user = false) {
326 12
		$user = (int)$user ?: $this->id;
327 12
		if ($user == User::GUEST_ID) {
328 3
			return Language::instance()->system_profile_guest;
0 ignored issues
show
Bug Best Practice introduced by
The property system_profile_guest does not exist on cs\Language. Since you implemented __get, consider adding a @property annotation.
Loading history...
329
		}
330 9
		$username = $this->get('username', $user);
331 9
		if (!$username) {
0 ignored issues
show
introduced by
The condition ! $username can never be false.
Loading history...
332 9
			$username = $this->get('login', $user);
333
		}
334 9
		return $username;
335
	}
336
	/**
337
	 * Returns array of users columns, available for getting of data
338
	 *
339
	 * @return array
340
	 */
341 3
	public function get_users_columns () {
342 3
		return $this->users_columns;
343
	}
344
}
345