Completed
Push — master ( a10c45...8e6d86 )
by Morris
13:50
created

Manager::clearCaches()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 4
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
	protected function clearCaches() {
149
		$this->cachedGroups = array();
150
		$this->cachedUserGroups = array();
151
	}
152
153
	/**
154
	 * @param string $gid
155
	 * @return \OC\Group\Group
156
	 */
157
	public function get($gid) {
158
		if (isset($this->cachedGroups[$gid])) {
159
			return $this->cachedGroups[$gid];
160
		}
161
		return $this->getGroupObject($gid);
162
	}
163
164
	/**
165
	 * @param string $gid
166
	 * @param string $displayName
167
	 * @return \OCP\IGroup
168
	 */
169
	protected function getGroupObject($gid, $displayName = null) {
170
		$backends = array();
171
		foreach ($this->backends as $backend) {
172
			if ($backend->implementsActions(\OC\Group\Backend::GROUP_DETAILS)) {
173
				$groupData = $backend->getGroupDetails($gid);
0 ignored issues
show
Bug introduced by
The method getGroupDetails() 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...
174
				if (is_array($groupData)) {
175
					// take the display name from the first backend that has a non-null one
176
					if (is_null($displayName) && isset($groupData['displayName'])) {
177
						$displayName = $groupData['displayName'];
178
					}
179
					$backends[] = $backend;
180
				}
181
			} else if ($backend->groupExists($gid)) {
182
				$backends[] = $backend;
183
			}
184
		}
185
		if (count($backends) === 0) {
186
			return null;
187
		}
188
		$this->cachedGroups[$gid] = new Group($gid, $backends, $this->userManager, $this, $displayName);
189
		return $this->cachedGroups[$gid];
190
	}
191
192
	/**
193
	 * @param string $gid
194
	 * @return bool
195
	 */
196
	public function groupExists($gid) {
197
		return $this->get($gid) instanceof IGroup;
198
	}
199
200
	/**
201
	 * @param string $gid
202
	 * @return \OC\Group\Group
203
	 */
204
	public function createGroup($gid) {
205
		if ($gid === '' || $gid === null) {
206
			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...
207
		} else if ($group = $this->get($gid)) {
208
			return $group;
209
		} else {
210
			$this->emit('\OC\Group', 'preCreate', array($gid));
211
			foreach ($this->backends as $backend) {
212
				if ($backend->implementsActions(\OC\Group\Backend::CREATE_GROUP)) {
213
					$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: 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...
214
					$group = $this->getGroupObject($gid);
215
					$this->emit('\OC\Group', 'postCreate', array($group));
216
					return $group;
217
				}
218
			}
219
			return null;
220
		}
221
	}
222
223
	/**
224
	 * @param string $search
225
	 * @param int $limit
226
	 * @param int $offset
227
	 * @return \OC\Group\Group[]
228
	 */
229
	public function search($search, $limit = null, $offset = null) {
230
		$groups = array();
231
		foreach ($this->backends as $backend) {
232
			$groupIds = $backend->getGroups($search, $limit, $offset);
233 View Code Duplication
			foreach ($groupIds as $groupId) {
234
				$aGroup = $this->get($groupId);
235
				if ($aGroup instanceof IGroup) {
236
					$groups[$groupId] = $aGroup;
237
				} else {
238
					$this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']);
239
				}
240
			}
241
			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...
242
				return array_values($groups);
243
			}
244
		}
245
		return array_values($groups);
246
	}
247
248
	/**
249
	 * @param IUser|null $user
250
	 * @return \OC\Group\Group[]
251
	 */
252
	public function getUserGroups(IUser $user= null) {
253
		if (!$user instanceof IUser) {
254
			return [];
255
		}
256
		return $this->getUserIdGroups($user->getUID());
257
	}
258
259
	/**
260
	 * @param string $uid the user id
261
	 * @return \OC\Group\Group[]
262
	 */
263
	public function getUserIdGroups($uid) {
264
		if (isset($this->cachedUserGroups[$uid])) {
265
			return $this->cachedUserGroups[$uid];
266
		}
267
		$groups = array();
268
		foreach ($this->backends as $backend) {
269
			$groupIds = $backend->getUserGroups($uid);
270
			if (is_array($groupIds)) {
271 View Code Duplication
				foreach ($groupIds as $groupId) {
272
					$aGroup = $this->get($groupId);
273
					if ($aGroup instanceof IGroup) {
274
						$groups[$groupId] = $aGroup;
275
					} else {
276
						$this->logger->debug('User "' . $uid . '" belongs to deleted group: "' . $groupId . '"', ['app' => 'core']);
277
					}
278
				}
279
			}
280
		}
281
		$this->cachedUserGroups[$uid] = $groups;
282
		return $this->cachedUserGroups[$uid];
283
	}
284
285
	/**
286
	 * Checks if a userId is in the admin group
287
	 * @param string $userId
288
	 * @return bool if admin
289
	 */
290
	public function isAdmin($userId) {
291
		foreach ($this->backends as $backend) {
292
			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...
293
				return true;
294
			}
295
		}
296
		return $this->isInGroup($userId, 'admin');
297
	}
298
299
	/**
300
	 * Checks if a userId is in a group
301
	 * @param string $userId
302
	 * @param string $group
303
	 * @return bool if in group
304
	 */
305
	public function isInGroup($userId, $group) {
306
		return array_key_exists($group, $this->getUserIdGroups($userId));
307
	}
308
309
	/**
310
	 * get a list of group ids for a user
311
	 * @param IUser $user
312
	 * @return array with group ids
313
	 */
314
	public function getUserGroupIds(IUser $user) {
315
		return array_map(function($value) {
316
			return (string) $value;
317
		}, array_keys($this->getUserGroups($user)));
318
	}
319
320
	/**
321
	 * get a list of all display names in a group
322
	 * @param string $gid
323
	 * @param string $search
324
	 * @param int $limit
325
	 * @param int $offset
326
	 * @return array an array of display names (value) and user ids (key)
327
	 */
328
	public function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) {
329
		$group = $this->get($gid);
330
		if(is_null($group)) {
331
			return array();
332
		}
333
334
		$search = trim($search);
335
		$groupUsers = array();
336
337
		if(!empty($search)) {
338
			// only user backends have the capability to do a complex search for users
339
			$searchOffset = 0;
340
			$searchLimit = $limit * 100;
341
			if($limit === -1) {
342
				$searchLimit = 500;
343
			}
344
345
			do {
346
				$filteredUsers = $this->userManager->searchDisplayName($search, $searchLimit, $searchOffset);
347
				foreach($filteredUsers as $filteredUser) {
348
					if($group->inGroup($filteredUser)) {
349
						$groupUsers[]= $filteredUser;
350
					}
351
				}
352
				$searchOffset += $searchLimit;
353
			} while(count($groupUsers) < $searchLimit+$offset && count($filteredUsers) >= $searchLimit);
354
355
			if($limit === -1) {
356
				$groupUsers = array_slice($groupUsers, $offset);
357
			} else {
358
				$groupUsers = array_slice($groupUsers, $offset, $limit);
359
			}
360
		} else {
361
			$groupUsers = $group->searchUsers('', $limit, $offset);
362
		}
363
364
		$matchingUsers = array();
365
		foreach($groupUsers as $groupUser) {
366
			$matchingUsers[$groupUser->getUID()] = $groupUser->getDisplayName();
367
		}
368
		return $matchingUsers;
369
	}
370
371
	/**
372
	 * @return \OC\SubAdmin
373
	 */
374
	public function getSubAdmin() {
375
		if (!$this->subAdmin) {
376
			$this->subAdmin = new \OC\SubAdmin(
377
				$this->userManager,
378
				$this,
379
				\OC::$server->getDatabaseConnection()
380
			);
381
		}
382
383
		return $this->subAdmin;
384
	}
385
}
386