Completed
Push — master ( 0256f6...5411d6 )
by Morris
22:12 queued 06:46
created

Manager::getBackends()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
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 Lukas Reschke <[email protected]>
11
 * @author macjohnny <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Robin McCorkell <[email protected]>
15
 * @author Roeland Jago Douma <[email protected]>
16
 * @author Roman Kreisel <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author voxsim <Simon Vocella>
19
 *
20
 * @license AGPL-3.0
21
 *
22
 * This code is free software: you can redistribute it and/or modify
23
 * it under the terms of the GNU Affero General Public License, version 3,
24
 * as published by the Free Software Foundation.
25
 *
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
 * GNU Affero General Public License for more details.
30
 *
31
 * You should have received a copy of the GNU Affero General Public License, version 3,
32
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33
 *
34
 */
35
36
namespace OC\Group;
37
38
use OC\Hooks\PublicEmitter;
39
use OCP\GroupInterface;
40
use OCP\IGroup;
41
use OCP\IGroupManager;
42
use OCP\ILogger;
43
use OCP\IUser;
44
45
/**
46
 * Class Manager
47
 *
48
 * Hooks available in scope \OC\Group:
49
 * - preAddUser(\OC\Group\Group $group, \OC\User\User $user)
50
 * - postAddUser(\OC\Group\Group $group, \OC\User\User $user)
51
 * - preRemoveUser(\OC\Group\Group $group, \OC\User\User $user)
52
 * - postRemoveUser(\OC\Group\Group $group, \OC\User\User $user)
53
 * - preDelete(\OC\Group\Group $group)
54
 * - postDelete(\OC\Group\Group $group)
55
 * - preCreate(string $groupId)
56
 * - postCreate(\OC\Group\Group $group)
57
 *
58
 * @package OC\Group
59
 */
60
class Manager extends PublicEmitter implements IGroupManager {
61
	/**
62
	 * @var GroupInterface[] $backends
63
	 */
64
	private $backends = array();
65
66
	/**
67
	 * @var \OC\User\Manager $userManager
68
	 */
69
	private $userManager;
70
71
	/**
72
	 * @var \OC\Group\Group[]
73
	 */
74
	private $cachedGroups = array();
75
76
	/**
77
	 * @var \OC\Group\Group[]
78
	 */
79
	private $cachedUserGroups = array();
80
81
	/** @var \OC\SubAdmin */
82
	private $subAdmin = null;
83
84
	/** @var ILogger */
85
	private $logger;
86
87
	/**
88
	 * @param \OC\User\Manager $userManager
89
	 * @param ILogger $logger
90
	 */
91
	public function __construct(\OC\User\Manager $userManager, ILogger $logger) {
92
		$this->userManager = $userManager;
93
		$this->logger = $logger;
94
		$cachedGroups = & $this->cachedGroups;
95
		$cachedUserGroups = & $this->cachedUserGroups;
96
		$this->listen('\OC\Group', 'postDelete', function ($group) use (&$cachedGroups, &$cachedUserGroups) {
97
			/**
98
			 * @var \OC\Group\Group $group
99
			 */
100
			unset($cachedGroups[$group->getGID()]);
101
			$cachedUserGroups = array();
102
		});
103
		$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...
104
			/**
105
			 * @var \OC\Group\Group $group
106
			 */
107
			$cachedUserGroups = array();
108
		});
109
		$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...
110
			/**
111
			 * @var \OC\Group\Group $group
112
			 */
113
			$cachedUserGroups = array();
114
		});
115
	}
116
117
	/**
118
	 * Checks whether a given backend is used
119
	 *
120
	 * @param string $backendClass Full classname including complete namespace
121
	 * @return bool
122
	 */
123
	public function isBackendUsed($backendClass) {
124
		$backendClass = strtolower(ltrim($backendClass, '\\'));
125
126
		foreach ($this->backends as $backend) {
127
			if (strtolower(get_class($backend)) === $backendClass) {
128
				return true;
129
			}
130
		}
131
132
		return false;
133
	}
134
135
	/**
136
	 * @param \OCP\GroupInterface $backend
137
	 */
138
	public function addBackend($backend) {
139
		$this->backends[] = $backend;
140
		$this->clearCaches();
141
	}
142
143
	public function clearBackends() {
144
		$this->backends = array();
145
		$this->clearCaches();
146
	}
147
148
	/**
149
	 * Get the active backends
150
	 * @return \OCP\GroupInterface[]
151
	 */
152
	public function getBackends() {
153
		return $this->backends;
154
	}
155
156
157
	protected function clearCaches() {
158
		$this->cachedGroups = array();
159
		$this->cachedUserGroups = array();
160
	}
161
162
	/**
163
	 * @param string $gid
164
	 * @return \OC\Group\Group
165
	 */
166
	public function get($gid) {
167
		if (isset($this->cachedGroups[$gid])) {
168
			return $this->cachedGroups[$gid];
169
		}
170
		return $this->getGroupObject($gid);
171
	}
172
173
	/**
174
	 * @param string $gid
175
	 * @param string $displayName
176
	 * @return \OCP\IGroup
177
	 */
178
	protected function getGroupObject($gid, $displayName = null) {
179
		$backends = array();
180
		foreach ($this->backends as $backend) {
181
			if ($backend->implementsActions(\OC\Group\Backend::GROUP_DETAILS)) {
182
				$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...
183
				if (is_array($groupData)) {
184
					// take the display name from the first backend that has a non-null one
185
					if (is_null($displayName) && isset($groupData['displayName'])) {
186
						$displayName = $groupData['displayName'];
187
					}
188
					$backends[] = $backend;
189
				}
190
			} else if ($backend->groupExists($gid)) {
191
				$backends[] = $backend;
192
			}
193
		}
194
		if (count($backends) === 0) {
195
			return null;
196
		}
197
		$this->cachedGroups[$gid] = new Group($gid, $backends, $this->userManager, $this, $displayName);
198
		return $this->cachedGroups[$gid];
199
	}
200
201
	/**
202
	 * @param string $gid
203
	 * @return bool
204
	 */
205
	public function groupExists($gid) {
206
		return $this->get($gid) instanceof IGroup;
207
	}
208
209
	/**
210
	 * @param string $gid
211
	 * @return \OC\Group\Group
212
	 */
213
	public function createGroup($gid) {
214
		if ($gid === '' || $gid === null) {
215
			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...
216
		} else if ($group = $this->get($gid)) {
217
			return $group;
218
		} else {
219
			$this->emit('\OC\Group', 'preCreate', array($gid));
220
			foreach ($this->backends as $backend) {
221
				if ($backend->implementsActions(\OC\Group\Backend::CREATE_GROUP)) {
222
					$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...
223
					$group = $this->getGroupObject($gid);
224
					$this->emit('\OC\Group', 'postCreate', array($group));
225
					return $group;
226
				}
227
			}
228
			return null;
229
		}
230
	}
231
232
	/**
233
	 * @param string $search
234
	 * @param int $limit
235
	 * @param int $offset
236
	 * @return \OC\Group\Group[]
237
	 */
238
	public function search($search, $limit = null, $offset = null) {
239
		$groups = array();
240
		foreach ($this->backends as $backend) {
241
			$groupIds = $backend->getGroups($search, $limit, $offset);
242 View Code Duplication
			foreach ($groupIds as $groupId) {
243
				$aGroup = $this->get($groupId);
244
				if ($aGroup instanceof IGroup) {
245
					$groups[$groupId] = $aGroup;
246
				} else {
247
					$this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']);
248
				}
249
			}
250
			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...
251
				return array_values($groups);
252
			}
253
		}
254
		return array_values($groups);
255
	}
256
257
	/**
258
	 * @param IUser|null $user
259
	 * @return \OC\Group\Group[]
260
	 */
261
	public function getUserGroups(IUser $user= null) {
262
		if (!$user instanceof IUser) {
263
			return [];
264
		}
265
		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 265 which is incompatible with the return type declared by the interface OCP\IGroupManager::getUserGroups of type OCP\IGroup[].
Loading history...
266
	}
267
268
	/**
269
	 * @param string $uid the user id
270
	 * @return \OC\Group\Group[]
271
	 */
272
	public function getUserIdGroups($uid) {
273
		if (isset($this->cachedUserGroups[$uid])) {
274
			return $this->cachedUserGroups[$uid];
275
		}
276
		$groups = array();
277
		foreach ($this->backends as $backend) {
278
			$groupIds = $backend->getUserGroups($uid);
279
			if (is_array($groupIds)) {
280 View Code Duplication
				foreach ($groupIds as $groupId) {
281
					$aGroup = $this->get($groupId);
282
					if ($aGroup instanceof IGroup) {
283
						$groups[$groupId] = $aGroup;
284
					} else {
285
						$this->logger->debug('User "' . $uid . '" belongs to deleted group: "' . $groupId . '"', ['app' => 'core']);
286
					}
287
				}
288
			}
289
		}
290
		$this->cachedUserGroups[$uid] = $groups;
291
		return $this->cachedUserGroups[$uid];
292
	}
293
294
	/**
295
	 * Checks if a userId is in the admin group
296
	 * @param string $userId
297
	 * @return bool if admin
298
	 */
299
	public function isAdmin($userId) {
300
		foreach ($this->backends as $backend) {
301
			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...
302
				return true;
303
			}
304
		}
305
		return $this->isInGroup($userId, 'admin');
306
	}
307
308
	/**
309
	 * Checks if a userId is in a group
310
	 * @param string $userId
311
	 * @param string $group
312
	 * @return bool if in group
313
	 */
314
	public function isInGroup($userId, $group) {
315
		return array_key_exists($group, $this->getUserIdGroups($userId));
316
	}
317
318
	/**
319
	 * get a list of group ids for a user
320
	 * @param IUser $user
321
	 * @return array with group ids
322
	 */
323
	public function getUserGroupIds(IUser $user) {
324
		return array_map(function($value) {
325
			return (string) $value;
326
		}, array_keys($this->getUserGroups($user)));
327
	}
328
329
	/**
330
	 * get a list of all display names in a group
331
	 * @param string $gid
332
	 * @param string $search
333
	 * @param int $limit
334
	 * @param int $offset
335
	 * @return array an array of display names (value) and user ids (key)
336
	 */
337
	public function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) {
338
		$group = $this->get($gid);
339
		if(is_null($group)) {
340
			return array();
341
		}
342
343
		$search = trim($search);
344
		$groupUsers = array();
345
346
		if(!empty($search)) {
347
			// only user backends have the capability to do a complex search for users
348
			$searchOffset = 0;
349
			$searchLimit = $limit * 100;
350
			if($limit === -1) {
351
				$searchLimit = 500;
352
			}
353
354
			do {
355
				$filteredUsers = $this->userManager->searchDisplayName($search, $searchLimit, $searchOffset);
356
				foreach($filteredUsers as $filteredUser) {
357
					if($group->inGroup($filteredUser)) {
358
						$groupUsers[]= $filteredUser;
359
					}
360
				}
361
				$searchOffset += $searchLimit;
362
			} while(count($groupUsers) < $searchLimit+$offset && count($filteredUsers) >= $searchLimit);
363
364
			if($limit === -1) {
365
				$groupUsers = array_slice($groupUsers, $offset);
366
			} else {
367
				$groupUsers = array_slice($groupUsers, $offset, $limit);
368
			}
369
		} else {
370
			$groupUsers = $group->searchUsers('', $limit, $offset);
371
		}
372
373
		$matchingUsers = array();
374
		foreach($groupUsers as $groupUser) {
375
			$matchingUsers[$groupUser->getUID()] = $groupUser->getDisplayName();
376
		}
377
		return $matchingUsers;
378
	}
379
380
	/**
381
	 * @return \OC\SubAdmin
382
	 */
383
	public function getSubAdmin() {
384
		if (!$this->subAdmin) {
385
			$this->subAdmin = new \OC\SubAdmin(
386
				$this->userManager,
387
				$this,
388
				\OC::$server->getDatabaseConnection()
389
			);
390
		}
391
392
		return $this->subAdmin;
393
	}
394
}
395