Completed
Pull Request — stable9 (#1770)
by Joas
65:47 queued 53:45
created

Manager::getUserObject()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 13
c 1
b 0
f 0
nc 9
nop 3
dl 0
loc 21
rs 8.7624
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Michael U <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Robin McCorkell <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Vincent Chan <[email protected]>
15
 * @author Volkan Gezer <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\User;
34
35
use OC\Hooks\PublicEmitter;
36
use OCP\IUserBackend;
37
use OCP\IUserManager;
38
use OCP\IConfig;
39
40
/**
41
 * Class Manager
42
 *
43
 * Hooks available in scope \OC\User:
44
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
45
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
46
 * - preDelete(\OC\User\User $user)
47
 * - postDelete(\OC\User\User $user)
48
 * - preCreateUser(string $uid, string $password)
49
 * - postCreateUser(\OC\User\User $user, string $password)
50
 * - change(\OC\User\User $user)
51
 *
52
 * @package OC\User
53
 */
54
class Manager extends PublicEmitter implements IUserManager {
55
	/**
56
	 * @var \OCP\UserInterface[] $backends
57
	 */
58
	private $backends = array();
59
60
	/**
61
	 * @var \OC\User\User[] $cachedUsers
62
	 */
63
	private $cachedUsers = array();
64
65
	/**
66
	 * @var \OCP\IConfig $config
67
	 */
68
	private $config;
69
70
	/**
71
	 * @param \OCP\IConfig $config
72
	 */
73
	public function __construct(IConfig $config = null) {
74
		$this->config = $config;
75
		$cachedUsers = &$this->cachedUsers;
76
		$this->listen('\OC\User', 'postDelete', function ($user) use (&$cachedUsers) {
77
			/** @var \OC\User\User $user */
78
			unset($cachedUsers[$user->getUID()]);
79
		});
80
		$this->listen('\OC\User', 'postLogin', function ($user) {
81
			/** @var \OC\User\User $user */
82
			$user->updateLastLoginTimestamp();
83
		});
84
		$this->listen('\OC\User', 'postRememberedLogin', function ($user) {
85
			/** @var \OC\User\User $user */
86
			$user->updateLastLoginTimestamp();
87
		});
88
	}
89
90
	/**
91
	 * Get the active backends
92
	 * @return \OCP\UserInterface[]
93
	 */
94
	public function getBackends() {
95
		return $this->backends;
96
	}
97
98
	/**
99
	 * register a user backend
100
	 *
101
	 * @param \OCP\UserInterface $backend
102
	 */
103
	public function registerBackend($backend) {
104
		$this->backends[] = $backend;
105
	}
106
107
	/**
108
	 * remove a user backend
109
	 *
110
	 * @param \OCP\UserInterface $backend
111
	 */
112
	public function removeBackend($backend) {
113
		$this->cachedUsers = array();
114
		if (($i = array_search($backend, $this->backends)) !== false) {
115
			unset($this->backends[$i]);
116
		}
117
	}
118
119
	/**
120
	 * remove all user backends
121
	 */
122
	public function clearBackends() {
123
		$this->cachedUsers = array();
124
		$this->backends = array();
125
	}
126
127
	/**
128
	 * get a user by user id
129
	 *
130
	 * @param string $uid
131
	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
132
	 */
133
	public function get($uid) {
134
		if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
135
			return $this->cachedUsers[$uid];
136
		}
137
		foreach ($this->backends as $backend) {
138
			if ($backend->userExists($uid)) {
139
				return $this->getUserObject($uid, $backend);
140
			}
141
		}
142
		return null;
143
	}
144
145
	/**
146
	 * get or construct the user object
147
	 *
148
	 * @param string $uid
149
	 * @param \OCP\UserInterface $backend
150
	 * @param bool $cacheUser If false the newly created user object will not be cached
151
	 * @return \OC\User\User
152
	 */
153
	protected function getUserObject($uid, $backend, $cacheUser = true) {
154
		if (isset($this->cachedUsers[$uid])) {
155
			return $this->cachedUsers[$uid];
156
		}
157
158
		if (method_exists($backend, 'loginName2UserName')) {
159
			$loginName = $backend->loginName2UserName($uid);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method loginName2UserName() does only exist in the following implementations of said interface: OCA\user_ldap\USER_LDAP, OCA\user_ldap\User_Proxy, OC_User_Database.

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...
160
			if ($loginName !== false) {
161
				$uid = $loginName;
162
			}
163
			if (isset($this->cachedUsers[$uid])) {
164
				return $this->cachedUsers[$uid];
165
			}
166
		}
167
168
		$user = new User($uid, $backend, $this, $this->config);
169
		if ($cacheUser) {
170
			$this->cachedUsers[$uid] = $user;
171
		}
172
		return $user;
173
	}
174
175
	/**
176
	 * check if a user exists
177
	 *
178
	 * @param string $uid
179
	 * @return bool
180
	 */
181
	public function userExists($uid) {
182
		$user = $this->get($uid);
183
		return ($user !== null);
184
	}
185
186
	/**
187
	 * Check if the password is valid for the user
188
	 *
189
	 * @param string $loginName
190
	 * @param string $password
191
	 * @return mixed the User object on success, false otherwise
192
	 */
193
	public function checkPassword($loginName, $password) {
194
		$loginName = str_replace("\0", '', $loginName);
195
		$password = str_replace("\0", '', $password);
196
		
197
		foreach ($this->backends as $backend) {
198
			if ($backend->implementsActions(\OC_User_Backend::CHECK_PASSWORD)) {
199
				$uid = $backend->checkPassword($loginName, $password);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method checkPassword() does only exist in the following implementations of said interface: OCA\user_ldap\USER_LDAP, OCA\user_ldap\User_Proxy, OC_User_Database.

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
				if ($uid !== false) {
201
					return $this->getUserObject($uid, $backend);
202
				}
203
			}
204
		}
205
206
		\OC::$server->getLogger()->warning('Login failed: \''. $loginName .'\' (Remote IP: \''. \OC::$server->getRequest()->getRemoteAddress(). '\')', ['app' => 'core']);
207
		return false;
208
	}
209
210
	/**
211
	 * search by user id
212
	 *
213
	 * @param string $pattern
214
	 * @param int $limit
215
	 * @param int $offset
216
	 * @return \OC\User\User[]
217
	 */
218 View Code Duplication
	public function search($pattern, $limit = null, $offset = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
219
		$users = array();
220
		foreach ($this->backends as $backend) {
221
			$backendUsers = $backend->getUsers($pattern, $limit, $offset);
222
			if (is_array($backendUsers)) {
223
				foreach ($backendUsers as $uid) {
224
					$users[$uid] = $this->getUserObject($uid, $backend);
225
				}
226
			}
227
		}
228
229
		uasort($users, function ($a, $b) {
230
			/**
231
			 * @var \OC\User\User $a
232
			 * @var \OC\User\User $b
233
			 */
234
			return strcmp($a->getUID(), $b->getUID());
235
		});
236
		return $users;
237
	}
238
239
	/**
240
	 * search by displayName
241
	 *
242
	 * @param string $pattern
243
	 * @param int $limit
244
	 * @param int $offset
245
	 * @return \OC\User\User[]
246
	 */
247 View Code Duplication
	public function searchDisplayName($pattern, $limit = null, $offset = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
248
		$users = array();
249
		foreach ($this->backends as $backend) {
250
			$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
251
			if (is_array($backendUsers)) {
252
				foreach ($backendUsers as $uid => $displayName) {
253
					$users[] = $this->getUserObject($uid, $backend);
254
				}
255
			}
256
		}
257
258
		usort($users, function ($a, $b) {
259
			/**
260
			 * @var \OC\User\User $a
261
			 * @var \OC\User\User $b
262
			 */
263
			return strcmp($a->getDisplayName(), $b->getDisplayName());
264
		});
265
		return $users;
266
	}
267
268
	/**
269
	 * @param string $uid
270
	 * @param string $password
271
	 * @throws \Exception
272
	 * @return bool|\OC\User\User the created user or false
273
	 */
274
	public function createUser($uid, $password) {
275
		$l = \OC::$server->getL10N('lib');
276
		// Check the name for bad characters
277
		// Allowed are: "a-z", "A-Z", "0-9" and "_.@-'"
278
		if (preg_match('/[^a-zA-Z0-9 _\.@\-\']/', $uid)) {
279
			throw new \Exception($l->t('Only the following characters are allowed in a username:'
280
				. ' "a-z", "A-Z", "0-9", and "_.@-\'"'));
281
		}
282
		// No empty username
283
		if (trim($uid) == '') {
284
			throw new \Exception($l->t('A valid username must be provided'));
285
		}
286
		// No whitespace at the beginning or at the end
287
		if (strlen(trim($uid, "\t\n\r\0\x0B\xe2\x80\x8b")) !== strlen(trim($uid))) {
288
			throw new \Exception($l->t('Username contains whitespace at the beginning or at the end'));
289
		}
290
		// No empty password
291
		if (trim($password) == '') {
292
			throw new \Exception($l->t('A valid password must be provided'));
293
		}
294
295
		// Check if user already exists
296
		if ($this->userExists($uid)) {
297
			throw new \Exception($l->t('The username is already being used'));
298
		}
299
300
		$this->emit('\OC\User', 'preCreateUser', array($uid, $password));
301
		foreach ($this->backends as $backend) {
302
			if ($backend->implementsActions(\OC_User_Backend::CREATE_USER)) {
303
				$backend->createUser($uid, $password);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method createUser() does only exist in the following implementations of said interface: OC_User_Database.

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...
304
				$user = $this->getUserObject($uid, $backend);
305
				$this->emit('\OC\User', 'postCreateUser', array($user, $password));
306
				return $user;
307
			}
308
		}
309
		return false;
310
	}
311
312
	/**
313
	 * returns how many users per backend exist (if supported by backend)
314
	 *
315
	 * @return array an array of backend class as key and count number as value
316
	 */
317
	public function countUsers() {
318
		$userCountStatistics = array();
319
		foreach ($this->backends as $backend) {
320
			if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS)) {
321
				$backendUsers = $backend->countUsers();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method countUsers() does only exist in the following implementations of said interface: OCA\user_ldap\USER_LDAP, OCA\user_ldap\User_Proxy, OC_User_Database.

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...
322
				if($backendUsers !== false) {
323
					if($backend instanceof IUserBackend) {
324
						$name = $backend->getBackendName();
325
					} else {
326
						$name = get_class($backend);
327
					}
328
					if(isset($userCountStatistics[$name])) {
329
						$userCountStatistics[$name] += $backendUsers;
330
					} else {
331
						$userCountStatistics[$name] = $backendUsers;
332
					}
333
				}
334
			}
335
		}
336
		return $userCountStatistics;
337
	}
338
339
	/**
340
	 * The callback is executed for each user on each backend.
341
	 * If the callback returns false no further users will be retrieved.
342
	 *
343
	 * @param \Closure $callback
344
	 * @param string $search
345
	 * @since 9.0.0
346
	 */
347
	public function callForAllUsers(\Closure $callback, $search = '') {
348
		foreach($this->getBackends() as $backend) {
349
			$limit = 500;
350
			$offset = 0;
351
			do {
352
				$users = $backend->getUsers($search, $limit, $offset);
353
				foreach ($users as $uid) {
354
					if (!$backend->userExists($uid)) {
355
						continue;
356
					}
357
					$user = $this->getUserObject($uid, $backend, false);
358
					$return = $callback($user);
359
					if ($return === false) {
360
						break;
361
					}
362
				}
363
				$offset += $limit;
364
			} while (count($users) >= $limit);
365
		}
366
	}
367
}
368