Completed
Push — master ( 40aac8...cfd797 )
by Jörn Friedrich
10:18
created

Manager::search()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 3
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Jörn Friedrich Dreyer <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Michael U <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Tom Needham <[email protected]>
12
 * @author Victor Dubiniuk <[email protected]>
13
 * @author Vincent Chan <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 * @author Volkan Gezer <[email protected]>
16
 *
17
 * @copyright Copyright (c) 2018, ownCloud GmbH
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\User;
35
36
use OC\Cache\CappedMemoryCache;
37
use OC\Hooks\PublicEmitter;
38
use OCP\AppFramework\Db\DoesNotExistException;
39
use OCP\Events\EventEmitterTrait;
40
use OCP\ILogger;
41
use OCP\IUser;
42
use OCP\IUserManager;
43
use OCP\IConfig;
44
use OCP\User\IProvidesExtendedSearchBackend;
45
use OCP\User\IProvidesEMailBackend;
46
use OCP\User\IProvidesQuotaBackend;
47
use OCP\UserInterface;
48
use Symfony\Component\EventDispatcher\GenericEvent;
49
50
/**
51
 * Class Manager
52
 *
53
 * Hooks available in scope \OC\User:
54
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
55
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
56
 * - preDelete(\OC\User\User $user)
57
 * - postDelete(\OC\User\User $user)
58
 * - preCreateUser(string $uid, string $password)
59
 * - postCreateUser(\OC\User\User $user, string $password)
60
 * - change(\OC\User\User $user)
61
 *
62
 * @package OC\User
63
 */
64
class Manager extends PublicEmitter implements IUserManager {
65
	use EventEmitterTrait;
66
	/** @var UserInterface[] $backends */
67
	private $backends = [];
68
69
	/** @var CappedMemoryCache $cachedUsers */
70
	private $cachedUsers;
71
72
	/** @var IConfig $config */
73
	private $config;
74
75
	/** @var ILogger $logger */
76
	private $logger;
77
78
	/** @var AccountMapper */
79
	private $accountMapper;
80
81
	/** @var SyncService */
82
	private $syncService;
83
84
	/**
85
	 * @param IConfig $config
86
	 * @param ILogger $logger
87
	 * @param AccountMapper $accountMapper
88
	 * @param SyncService $syncService
89
	 */
90
	public function __construct(IConfig $config, ILogger $logger, AccountMapper $accountMapper, SyncService $syncService) {
91
		$this->config = $config;
92
		$this->logger = $logger;
93
		$this->accountMapper = $accountMapper;
94
		$this->cachedUsers = new CappedMemoryCache();
95
		$this->syncService = $syncService;
96
		$cachedUsers = &$this->cachedUsers;
97
		$this->listen('\OC\User', 'postDelete', function ($user) use (&$cachedUsers) {
98
			/** @var \OC\User\User $user */
99
			unset($cachedUsers[$user->getUID()]);
100
		});
101
	}
102
103
	/**
104
	 * only used for unit testing
105
	 *
106
	 * @param AccountMapper $mapper
107
	 * @param array $backends
108
	 * @param SyncService $syncService
109
	 * @return array
110
	 */
111
	public function reset(AccountMapper $mapper, $backends, $syncService) {
112
		$return = [$this->accountMapper, $this->backends, $this->syncService];
113
		$this->accountMapper = $mapper;
114
		$this->backends = $backends;
115
		$this->syncService = $syncService;
116
		$this->cachedUsers->clear();
117
118
		return $return;
119
	}
120
121
	/**
122
	 * Get the active backends
123
	 * @return \OCP\UserInterface[]
124
	 */
125
	public function getBackends() {
126
		return array_values($this->backends);
127
	}
128
129
	/**
130
	 * register a user backend
131
	 *
132
	 * @param \OCP\UserInterface $backend
133
	 */
134
	public function registerBackend($backend) {
135
		$this->backends[get_class($backend)] = $backend;
136
	}
137
138
	/**
139
	 * remove a user backend
140
	 *
141
	 * @param \OCP\UserInterface $backend
142
	 */
143
	public function removeBackend($backend) {
144
		$this->cachedUsers->clear();
145
		unset($this->backends[get_class($backend)]);
146
	}
147
148
	/**
149
	 * remove all user backends
150
	 */
151
	public function clearBackends() {
152
		$this->cachedUsers->clear();
153
		$this->backends = [];
154
	}
155
156
	/**
157
	 * get a user by user id
158
	 *
159
	 * @param string $uid
160
	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
161
	 */
162
	public function get($uid) {
163
		if (is_null($uid) || !is_string($uid)) {
164
			return null;
165
		}
166
		if ($this->cachedUsers->hasKey($uid)) { //check the cache first to prevent having to loop over the backends
167
			return $this->cachedUsers->get($uid);
168
		}
169
		try {
170
			$account = $this->accountMapper->getByUid($uid);
171
			if (is_null($account)) {
172
				$this->cachedUsers->set($uid, null);
173
				return null;
174
			}
175
			return $this->getUserObject($account);
176
		} catch (DoesNotExistException $ex) {
177
			return null;
178
		}
179
	}
180
181
	/**
182
	 * get or construct the user object
183
	 *
184
	 * @param Account $account
185
	 * @param bool $cacheUser If false the newly created user object will not be cached
186
	 * @return \OC\User\User
187
	 */
188
	protected function getUserObject(Account $account, $cacheUser = true) {
189
		if ($this->cachedUsers->hasKey($account->getUserId())) {
190
			return $this->cachedUsers->get($account->getUserId());
191
		}
192
193
		$user = new User($account, $this->accountMapper, $this, $this->config, null, \OC::$server->getEventDispatcher() );
0 ignored issues
show
Documentation introduced by
\OC::$server->getEventDispatcher() is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

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...
194
		if ($cacheUser) {
195
			$this->cachedUsers->set($account->getUserId(), $user);
196
		}
197
		return $user;
198
	}
199
200
	/**
201
	 * check if a user exists
202
	 *
203
	 * @param string $uid
204
	 * @return bool
205
	 */
206
	public function userExists($uid) {
207
		$user = $this->get($uid);
208
		return ($user !== null);
209
	}
210
211
	/**
212
	 * Check if the password is valid for the user
213
	 *
214
	 * @param string $loginName
215
	 * @param string $password
216
	 * @return mixed the User object on success, false otherwise
217
	 */
218
	public function checkPassword($loginName, $password) {
219
		$loginName = str_replace("\0", '', $loginName);
220
		$password = str_replace("\0", '', $password);
221
222
		if (empty($this->backends)) {
223
			$this->registerBackend(new Database());
224
		}
225
226
		foreach ($this->backends as $backend) {
227
			if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
228
				$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\Testing\AlternativeHomeUserBackend, 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...
229
				if ($uid !== false) {
230
					$account = $this->syncService->createOrSyncAccount($uid, $backend);
231
					return $this->getUserObject($account);
232
				}
233
			}
234
		}
235
236
		$this->logger->warning('Login failed: \''. $loginName .'\' (Remote IP: \''. \OC::$server->getRequest()->getRemoteAddress(). '\')', ['app' => 'core']);
237
		return false;
238
	}
239
240
	/**
241
	 * search by user id
242
	 *
243
	 * @param string $pattern
244
	 * @param int $limit
245
	 * @param int $offset
246
	 * @return \OC\User\User[]
247
	 */
248 View Code Duplication
	public function search($pattern, $limit = null, $offset = null) {
249
		$accounts = $this->accountMapper->search('user_id', $pattern, $limit, $offset);
250
		$users = [];
251
		foreach ($accounts as $account) {
252
			$user = $this->getUserObject($account);
253
			$users[$user->getUID()] = $user;
254
		}
255
256
		return $users;
257
	}
258
259
	/**
260
	 * find a user account by checking user_id, display name and email fields
261
	 *
262
	 * @param string $pattern
263
	 * @param int $limit
264
	 * @param int $offset
265
	 * @return \OC\User\User[]
266
	 */
267 View Code Duplication
	public function find($pattern, $limit = null, $offset = null) {
268
		$accounts = $this->accountMapper->find($pattern, $limit, $offset);
269
		$users = [];
270
		foreach ($accounts as $account) {
271
			$user = $this->getUserObject($account);
272
			$users[$user->getUID()] = $user;
273
		}
274
		return $users;
275
	}
276
277
	/**
278
	 * search by displayName
279
	 *
280
	 * @param string $pattern
281
	 * @param int $limit
282
	 * @param int $offset
283
	 * @return \OC\User\User[]
284
	 */
285
	public function searchDisplayName($pattern, $limit = null, $offset = null) {
286
		$accounts = $this->accountMapper->search('display_name', $pattern, $limit, $offset);
287
		return array_map(function(Account $account) {
288
			return $this->getUserObject($account);
289
		}, $accounts);
290
	}
291
292
	/**
293
	 * @param string $uid
294
	 * @param string $password
295
	 * @throws \Exception
296
	 * @return bool|IUser the created user or false
297
	 */
298
	public function createUser($uid, $password) {
299
		return $this->emittingCall(function () use (&$uid, &$password) {
300
			$l = \OC::$server->getL10N('lib');
301
302
			// Check the name for bad characters
303
			// Allowed are: "a-z", "A-Z", "0-9" and "_.@-'"
304
			if (preg_match('/[^a-zA-Z0-9 _\.@\-\']/', $uid)) {
305
				throw new \Exception($l->t('Only the following characters are allowed in a username:'
306
					. ' "a-z", "A-Z", "0-9", and "_.@-\'"'));
307
			}
308
			// No empty username
309
			if (trim($uid) == '') {
310
				throw new \Exception($l->t('A valid username must be provided'));
311
			}
312
			// No whitespace at the beginning or at the end
313
			if (strlen(trim($uid, "\t\n\r\0\x0B\xe2\x80\x8b")) !== strlen(trim($uid))) {
314
				throw new \Exception($l->t('Username contains whitespace at the beginning or at the end'));
315
			}
316
317
			// Username must be at least 3 characters long
318
			if(strlen($uid) < 3) {
319
				throw new \Exception($l->t('The username must be at least 3 characters long'));
320
			}
321
322
			// No empty password
323
			if (trim($password) == '') {
324
				throw new \Exception($l->t('A valid password must be provided'));
325
			}
326
327
			// Check if user already exists
328
			if ($this->userExists($uid)) {
329
				throw new \Exception($l->t('The username is already being used'));
330
			}
331
332
			$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
333
			\OC::$server->getEventDispatcher()->dispatch(
334
				'OCP\User::validatePassword',
335
				new GenericEvent(null, ['uid' => $uid, 'password' => $password])
336
			);
337
338
			if (empty($this->backends)) {
339
				$this->registerBackend(new Database());
340
			}
341
342
			foreach ($this->backends as $backend) {
343
				if ($backend->implementsActions(Backend::CREATE_USER)) {
344
					$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: OCA\Testing\AlternativeHomeUserBackend, 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...
345
					$user = $this->createUserFromBackend($uid, $password, $backend);
346
					return $user === null ? false : $user;
347
				}
348
			}
349
350
351
			return false;
352
		}, ['before' => ['uid' => $uid], 'after' => ['uid' => $uid, 'password' => $password]], 'user', 'create');
353
	}
354
355
	/**
356
	 * @param string $uid
357
	 * @param UserInterface $backend
358
	 * @return IUser | null
359
	 * @deprecated core is responsible for creating accounts, see user_ldap how it is done
360
	 */
361
	public function createUserFromBackend($uid, $password, $backend) {
362
		return $this->emittingCall(function () use (&$uid, &$password, &$backend) {
363
			$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
364
			try {
365
				$account = $this->syncService->createOrSyncAccount($uid, $backend);
366
			} catch (\InvalidArgumentException $e) {
367
				return null; // because that's what this method should do
368
			}
369
			$user = $this->getUserObject($account);
370
			$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
371
			return $user;
372
		}, ['before' => ['uid' => $uid]], 'user', 'create');
373
	}
374
375
	/**
376
	 * returns how many users per backend exist (if supported by backend)
377
	 *
378
	 * @param boolean $hasLoggedIn when true only users that have a lastLogin
379
	 *                entry in the preferences table will be affected
380
	 * @return array|int an array of backend class as key and count number as value
381
	 *                if $hasLoggedIn is true only an int is returned
382
	 */
383
	public function countUsers($hasLoggedIn = false) {
384
		if ($hasLoggedIn) {
385
			return $this->accountMapper->getUserCount($hasLoggedIn);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->accountMap...serCount($hasLoggedIn); (integer) is incompatible with the return type declared by the interface OCP\IUserManager::countUsers of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
386
		}
387
		return $this->accountMapper->getUserCountPerBackend($hasLoggedIn);
388
	}
389
390
	/**
391
	 * The callback is executed for each user on each backend.
392
	 * If the callback returns false no further users will be retrieved.
393
	 *
394
	 * @param \Closure $callback
395
	 * @param string $search
396
	 * @param boolean $onlySeen when true only users that have a lastLogin entry
397
	 *                in the preferences table will be affected
398
	 * @since 9.0.0
399
	 */
400
	public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
401
		$this->accountMapper->callForAllUsers(function (Account $account) use ($callback) {
402
			$user = $this->getUserObject($account);
403
			return $callback($user);
404
		}, $search, $onlySeen);
405
	}
406
407
	/**
408
	 * returns how many users have logged in once
409
	 *
410
	 * @return int
411
	 * @since 10.0
412
	 */
413
	public function countSeenUsers() {
414
		return $this->accountMapper->getUserCount(true);
415
	}
416
417
	/**
418
	 * @param \Closure $callback
419
	 * @since 10.0
420
	 */
421
	public function callForSeenUsers (\Closure $callback) {
422
		$this->callForAllUsers($callback, '', true);
423
	}
424
425
	/**
426
	 * @param string $email
427
	 * @return IUser[]
428
	 * @since 9.1.0
429
	 */
430
	public function getByEmail($email) {
431
		if ($email === null || trim($email) === '') {
432
			throw new \InvalidArgumentException('$email cannot be empty');
433
		}
434
		$accounts = $this->accountMapper->getByEmail($email);
435
		return array_map(function(Account $account) {
436
			return $this->getUserObject($account);
437
		}, $accounts);
438
	}
439
440
	public function getBackend($backendClass) {
441
		if (isset($this->backends[$backendClass])) {
442
			return $this->backends[$backendClass];
443
		}
444
		return null;
445
	}
446
447
}
448