Profile   F
last analyzed

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
ccs 148
cts 148
cp 1
rs 2.5423
c 0
b 0
f 0
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
 * @license 0BSD
6
 */
7
namespace cs\User;
8
use
9
	cs\Config,
10
	cs\Event,
11
	cs\Language,
12
	cs\Session,
13
	cs\User,
14
	h;
15
16
/**
17
 * Trait that contains all methods for `cs\User` for working with user's profile
18
 *
19
 * @property \cs\Cache\Prefix $cache
20
 * @property int              $id
21
 *
22
 * @method \cs\DB\_Abstract db()
23
 * @method \cs\DB\_Abstract db_prime()
24
 * @method false|int[]      get_groups(false|int $user)
25
 */
26
trait Profile {
27
	/**
28
	 * Copy of columns list of users table for internal needs without Cache usage
29
	 * @var array
30
	 */
31
	protected $users_columns = [];
32
	/**
33
	 * Local cache of users data
34
	 * @var array
35
	 */
36
	protected $data = [];
37 69
	protected function initialize_data () {
38 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...
39 69
			'columns',
40 69
			function () {
41 6
				return $this->db()->columns('[prefix]users');
42 69
			}
43
		);
44 69
	}
45
	/**
46
	 * Get data item of specified user
47
	 *
48
	 * @param string|string[] $item
49
	 * @param false|int       $user If not specified - current user assumed
50
	 *
51
	 * @return false|int|mixed[]|string|Properties If <i>$item</i> is integer - cs\User\Properties object will be returned
52
	 */
53 51
	public function get ($item, $user = false) {
54 51
		if (is_scalar($item) && ctype_digit((string)$item)) {
55 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

55
			return new Properties(/** @scrutinizer ignore-type */ $item);
Loading history...
56
		}
57 51
		return $this->get_internal($item, $user);
58
	}
59
	/**
60
	 * Get data item of specified user
61
	 *
62
	 * @param string|string[] $item
63
	 * @param false|int       $user If not specified - current user assumed
64
	 *
65
	 * @return false|int|string|mixed[]
66
	 */
67 51
	protected function get_internal ($item, $user = false) {
68 51
		$user = (int)$user ?: $this->id;
69 51
		if (isset($this->data[$user])) {
70 48
			$data = $this->data[$user];
71
		} else {
72 51
			$data = $this->cache->get(
73 51
				$user,
74 51
				function () use ($user) {
75 48
					return $this->db()->qf(
76
						"SELECT *
0 ignored issues
show
Bug introduced by
'SELECT * FROM `[p....$user.' LIMIT 1' 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

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

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

309
			$email_hash = md5(/** @scrutinizer ignore-type */ $this->get('email', $user));
Loading history...
310 6
			$avatar     = "https://www.gravatar.com/avatar/$email_hash?d=mm&s=$size&d=".urlencode($guest_avatar);
311
		}
312 12
		if (!$avatar) {
313 12
			$avatar = $guest_avatar;
314
		}
315 12
		return h::prepare_url($avatar, true);
0 ignored issues
show
Bug introduced by
It seems like $avatar can also be of type cs\User\Properties; however, parameter $url of nazarpc\BananaHTML::prepare_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

315
		return h::prepare_url(/** @scrutinizer ignore-type */ $avatar, true);
Loading history...
316
	}
317
	/**
318
	 * Get user name or login or email, depending on existing information
319
	 *
320
	 * @param false|int $user If not specified - current user assumed
321
	 *
322
	 * @return string
323
	 */
324 12
	public function username ($user = false) {
325 12
		$user = (int)$user ?: $this->id;
326 12
		if ($user == User::GUEST_ID) {
327 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...
328
		}
329 9
		$username = $this->get('username', $user);
330 9
		if (!$username) {
331 9
			$username = $this->get('login', $user);
332
		}
333 9
		return $username;
334
	}
335
	/**
336
	 * Returns array of users columns, available for getting of data
337
	 *
338
	 * @return array
339
	 */
340 3
	public function get_users_columns () {
341 3
		return $this->users_columns;
342
	}
343
}
344