Completed
Push — master ( fde08a...208e38 )
by Blizzz
17:49
created

Manager::getUserGroupNames()   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 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Jörn Friedrich Dreyer <[email protected]>
10
 * @author Knut Ahlers <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author macjohnny <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Robin Appelman <[email protected]>
15
 * @author Robin McCorkell <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Roman Kreisel <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 * @author Vinicius Cubas Brand <[email protected]>
21
 * @author voxsim "Simon Vocella"
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OC\Group;
40
41
use OC\Hooks\PublicEmitter;
42
use OCP\GroupInterface;
43
use OCP\IGroup;
44
use OCP\IGroupManager;
45
use OCP\ILogger;
46
use OCP\IUser;
47
48
/**
49
 * Class Manager
50
 *
51
 * Hooks available in scope \OC\Group:
52
 * - preAddUser(\OC\Group\Group $group, \OC\User\User $user)
53
 * - postAddUser(\OC\Group\Group $group, \OC\User\User $user)
54
 * - preRemoveUser(\OC\Group\Group $group, \OC\User\User $user)
55
 * - postRemoveUser(\OC\Group\Group $group, \OC\User\User $user)
56
 * - preDelete(\OC\Group\Group $group)
57
 * - postDelete(\OC\Group\Group $group)
58
 * - preCreate(string $groupId)
59
 * - postCreate(\OC\Group\Group $group)
60
 *
61
 * @package OC\Group
62
 */
63
class Manager extends PublicEmitter implements IGroupManager {
64
	/**
65
	 * @var GroupInterface[] $backends
66
	 */
67
	private $backends = array();
68
69
	/**
70
	 * @var \OC\User\Manager $userManager
71
	 */
72
	private $userManager;
73
74
	/**
75
	 * @var \OC\Group\Group[]
76
	 */
77
	private $cachedGroups = array();
78
79
	/**
80
	 * @var \OC\Group\Group[]
81
	 */
82
	private $cachedUserGroups = array();
83
84
	/** @var \OC\SubAdmin */
85
	private $subAdmin = null;
86
87
	/** @var ILogger */
88
	private $logger;
89
90
	/**
91
	 * @param \OC\User\Manager $userManager
92
	 * @param ILogger $logger
93
	 */
94
	public function __construct(\OC\User\Manager $userManager, ILogger $logger) {
95
		$this->userManager = $userManager;
96
		$this->logger = $logger;
97
		$cachedGroups = & $this->cachedGroups;
98
		$cachedUserGroups = & $this->cachedUserGroups;
99
		$this->listen('\OC\Group', 'postDelete', function ($group) use (&$cachedGroups, &$cachedUserGroups) {
100
			/**
101
			 * @var \OC\Group\Group $group
102
			 */
103
			unset($cachedGroups[$group->getGID()]);
104
			$cachedUserGroups = array();
105
		});
106
		$this->listen('\OC\Group', 'postAddUser', function ($group) use (&$cachedUserGroups) {
0 ignored issues
show
Unused Code introduced by
The parameter $group is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
107
			/**
108
			 * @var \OC\Group\Group $group
109
			 */
110
			$cachedUserGroups = array();
111
		});
112
		$this->listen('\OC\Group', 'postRemoveUser', function ($group) use (&$cachedUserGroups) {
0 ignored issues
show
Unused Code introduced by
The parameter $group is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
113
			/**
114
			 * @var \OC\Group\Group $group
115
			 */
116
			$cachedUserGroups = array();
117
		});
118
	}
119
120
	/**
121
	 * Checks whether a given backend is used
122
	 *
123
	 * @param string $backendClass Full classname including complete namespace
124
	 * @return bool
125
	 */
126
	public function isBackendUsed($backendClass) {
127
		$backendClass = strtolower(ltrim($backendClass, '\\'));
128
129
		foreach ($this->backends as $backend) {
130
			if (strtolower(get_class($backend)) === $backendClass) {
131
				return true;
132
			}
133
		}
134
135
		return false;
136
	}
137
138
	/**
139
	 * @param \OCP\GroupInterface $backend
140
	 */
141
	public function addBackend($backend) {
142
		$this->backends[] = $backend;
143
		$this->clearCaches();
144
	}
145
146
	public function clearBackends() {
147
		$this->backends = array();
148
		$this->clearCaches();
149
	}
150
151
	/**
152
	 * Get the active backends
153
	 * @return \OCP\GroupInterface[]
154
	 */
155
	public function getBackends() {
156
		return $this->backends;
157
	}
158
159
160
	protected function clearCaches() {
161
		$this->cachedGroups = array();
162
		$this->cachedUserGroups = array();
163
	}
164
165
	/**
166
	 * @param string $gid
167
	 * @return \OC\Group\Group
168
	 */
169
	public function get($gid) {
170
		if (isset($this->cachedGroups[$gid])) {
171
			return $this->cachedGroups[$gid];
172
		}
173
		return $this->getGroupObject($gid);
174
	}
175
176
	/**
177
	 * @param string $gid
178
	 * @param string $displayName
179
	 * @return \OCP\IGroup
180
	 */
181
	protected function getGroupObject($gid, $displayName = null) {
182
		$backends = array();
183
		foreach ($this->backends as $backend) {
184
			if ($backend->implementsActions(\OC\Group\Backend::GROUP_DETAILS)) {
185
				$groupData = $backend->getGroupDetails($gid);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\GroupInterface as the method getGroupDetails() does only exist in the following implementations of said interface: OCA\User_LDAP\Group_LDAP, OCA\User_LDAP\Group_Proxy.

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...
186
				if (is_array($groupData)) {
187
					// take the display name from the first backend that has a non-null one
188
					if (is_null($displayName) && isset($groupData['displayName'])) {
189
						$displayName = $groupData['displayName'];
190
					}
191
					$backends[] = $backend;
192
				}
193
			} else if ($backend->groupExists($gid)) {
194
				$backends[] = $backend;
195
			}
196
		}
197
		if (count($backends) === 0) {
198
			return null;
199
		}
200
		$this->cachedGroups[$gid] = new Group($gid, $backends, $this->userManager, $this, $displayName);
201
		return $this->cachedGroups[$gid];
202
	}
203
204
	/**
205
	 * @param string $gid
206
	 * @return bool
207
	 */
208
	public function groupExists($gid) {
209
		return $this->get($gid) instanceof IGroup;
210
	}
211
212
	/**
213
	 * @param string $gid
214
	 * @return \OC\Group\Group
215
	 */
216
	public function createGroup($gid) {
217
		if ($gid === '' || $gid === null) {
218
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface OCP\IGroupManager::createGroup of type OCP\IGroup.

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...
219
		} else if ($group = $this->get($gid)) {
220
			return $group;
221
		} else {
222
			$this->emit('\OC\Group', 'preCreate', array($gid));
223
			foreach ($this->backends as $backend) {
224
				if ($backend->implementsActions(\OC\Group\Backend::CREATE_GROUP)) {
225
					$backend->createGroup($gid);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\GroupInterface as the method createGroup() does only exist in the following implementations of said interface: OCA\User_LDAP\Group_LDAP, OCA\User_LDAP\Group_Proxy, OC\Group\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...
226
					$group = $this->getGroupObject($gid);
227
					$this->emit('\OC\Group', 'postCreate', array($group));
228
					return $group;
229
				}
230
			}
231
			return null;
232
		}
233
	}
234
235
	/**
236
	 * @param string $search
237
	 * @param int $limit
238
	 * @param int $offset
239
	 * @return \OC\Group\Group[]
240
	 */
241
	public function search($search, $limit = null, $offset = null) {
242
		$groups = array();
243
		foreach ($this->backends as $backend) {
244
			$groupIds = $backend->getGroups($search, $limit, $offset);
245 View Code Duplication
			foreach ($groupIds as $groupId) {
246
				$aGroup = $this->get($groupId);
247
				if ($aGroup instanceof IGroup) {
248
					$groups[$groupId] = $aGroup;
249
				} else {
250
					$this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']);
251
				}
252
			}
253
			if (!is_null($limit) and $limit <= 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
254
				return array_values($groups);
255
			}
256
		}
257
		return array_values($groups);
258
	}
259
260
	/**
261
	 * @param IUser|null $user
262
	 * @return \OC\Group\Group[]
263
	 */
264
	public function getUserGroups(IUser $user= null) {
265
		if (!$user instanceof IUser) {
266
			return [];
267
		}
268
		return $this->getUserIdGroups($user->getUID());
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getUserIdGroups($user->getUID()); of type OC\Group\Group|array adds the type OC\Group\Group to the return on line 268 which is incompatible with the return type declared by the interface OCP\IGroupManager::getUserGroups of type OCP\IGroup[].
Loading history...
269
	}
270
271
	/**
272
	 * @param string $uid the user id
273
	 * @return \OC\Group\Group[]
274
	 */
275
	public function getUserIdGroups($uid) {
276
		if (isset($this->cachedUserGroups[$uid])) {
277
			return $this->cachedUserGroups[$uid];
278
		}
279
		$groups = array();
280
		foreach ($this->backends as $backend) {
281
			$groupIds = $backend->getUserGroups($uid);
282
			if (is_array($groupIds)) {
283 View Code Duplication
				foreach ($groupIds as $groupId) {
284
					$aGroup = $this->get($groupId);
285
					if ($aGroup instanceof IGroup) {
286
						$groups[$groupId] = $aGroup;
287
					} else {
288
						$this->logger->debug('User "' . $uid . '" belongs to deleted group: "' . $groupId . '"', ['app' => 'core']);
289
					}
290
				}
291
			}
292
		}
293
		$this->cachedUserGroups[$uid] = $groups;
294
		return $this->cachedUserGroups[$uid];
295
	}
296
297
	/**
298
	 * Checks if a userId is in the admin group
299
	 * @param string $userId
300
	 * @return bool if admin
301
	 */
302
	public function isAdmin($userId) {
303
		foreach ($this->backends as $backend) {
304
			if ($backend->implementsActions(\OC\Group\Backend::IS_ADMIN) && $backend->isAdmin($userId)) {
0 ignored issues
show
Bug introduced by
The method isAdmin() does not seem to exist on object<OCP\GroupInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
305
				return true;
306
			}
307
		}
308
		return $this->isInGroup($userId, 'admin');
309
	}
310
311
	/**
312
	 * Checks if a userId is in a group
313
	 * @param string $userId
314
	 * @param string $group
315
	 * @return bool if in group
316
	 */
317
	public function isInGroup($userId, $group) {
318
		return array_key_exists($group, $this->getUserIdGroups($userId));
319
	}
320
321
	/**
322
	 * get a list of group ids for a user
323
	 * @param IUser $user
324
	 * @return array with group ids
325
	 */
326
	public function getUserGroupIds(IUser $user) {
327
		return array_map(function($value) {
328
			return (string) $value;
329
		}, array_keys($this->getUserGroups($user)));
330
	}
331
332
	/**
333
	 * get an array of groupid and displayName for a user
334
	 * @param IUser $user
335
	 * @return array ['displayName' => displayname]
336
	 */
337
	public function getUserGroupNames(IUser $user) {
338
		return array_map(function($group) {
339
			return array('displayName' => $group->getDisplayName());
340
		}, $this->getUserGroups($user));
341
	}
342
343
	/**
344
	 * get a list of all display names in a group
345
	 * @param string $gid
346
	 * @param string $search
347
	 * @param int $limit
348
	 * @param int $offset
349
	 * @return array an array of display names (value) and user ids (key)
350
	 */
351
	public function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) {
352
		$group = $this->get($gid);
353
		if(is_null($group)) {
354
			return array();
355
		}
356
357
		$search = trim($search);
358
		$groupUsers = array();
359
360
		if(!empty($search)) {
361
			// only user backends have the capability to do a complex search for users
362
			$searchOffset = 0;
363
			$searchLimit = $limit * 100;
364
			if($limit === -1) {
365
				$searchLimit = 500;
366
			}
367
368
			do {
369
				$filteredUsers = $this->userManager->searchDisplayName($search, $searchLimit, $searchOffset);
370
				foreach($filteredUsers as $filteredUser) {
371
					if($group->inGroup($filteredUser)) {
372
						$groupUsers[]= $filteredUser;
373
					}
374
				}
375
				$searchOffset += $searchLimit;
376
			} while(count($groupUsers) < $searchLimit+$offset && count($filteredUsers) >= $searchLimit);
377
378
			if($limit === -1) {
379
				$groupUsers = array_slice($groupUsers, $offset);
380
			} else {
381
				$groupUsers = array_slice($groupUsers, $offset, $limit);
382
			}
383
		} else {
384
			$groupUsers = $group->searchUsers('', $limit, $offset);
385
		}
386
387
		$matchingUsers = array();
388
		foreach($groupUsers as $groupUser) {
389
			$matchingUsers[$groupUser->getUID()] = $groupUser->getDisplayName();
390
		}
391
		return $matchingUsers;
392
	}
393
394
	/**
395
	 * @return \OC\SubAdmin
396
	 */
397
	public function getSubAdmin() {
398
		if (!$this->subAdmin) {
399
			$this->subAdmin = new \OC\SubAdmin(
400
				$this->userManager,
401
				$this,
402
				\OC::$server->getDatabaseConnection()
403
			);
404
		}
405
406
		return $this->subAdmin;
407
	}
408
}
409