Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

Group   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 289
Duplicated Lines 7.61 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 22
loc 289
rs 6.96
c 0
b 0
f 0
wmc 53
lcom 1
cbo 7

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getGID() 0 3 1
A getDisplayName() 0 6 2
A getUsers() 0 20 4
A inGroup() 0 12 4
B addUser() 0 23 7
B removeUser() 0 27 10
A searchUsers() 11 11 4
A count() 0 14 4
A searchDisplayName() 11 11 4
B delete() 0 23 7
A getVerifiedUsers() 0 13 4
A getBackend() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Group often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Group, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Morris Jobke <[email protected]>
7
 * @author Robin Appelman <[email protected]>
8
 * @author Robin McCorkell <[email protected]>
9
 * @author Roeland Jago Douma <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Vincent Petry <[email protected]>
12
 *
13
 * @copyright Copyright (c) 2018, ownCloud GmbH
14
 * @license AGPL-3.0
15
 *
16
 * This code is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License, version 3,
18
 * as published by the Free Software Foundation.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License, version 3,
26
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
27
 *
28
 */
29
30
namespace OC\Group;
31
32
use OCP\IGroup;
33
use OCP\IUser;
34
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
35
use Symfony\Component\EventDispatcher\GenericEvent;
36
37
class Group implements IGroup {
38
	/**
39
	 * @var string $id
40
	 */
41
	private $gid;
42
43
	/**
44
	 * @var \OC\User\User[] $users
45
	 */
46
	private $users = [];
47
48
	/**
49
	 * @var bool $usersLoaded
50
	 */
51
	private $usersLoaded;
52
53
	/**
54
	 * @var \OC\Group\Backend[]|\OC\Group\Database[] $backend
55
	 */
56
	private $backends;
57
58
	/**
59
	 * @var \OC\Hooks\PublicEmitter $emitter
60
	 */
61
	private $emitter;
62
63
	/**
64
	 * @var \OC\User\Manager $userManager
65
	 */
66
	private $userManager;
67
68
	/** @var EventDispatcherInterface */
69
	private $eventDispatcher;
70
71
	/**
72
	 * @param string $gid
73
	 * @param \OC\Group\Backend[] $backends
74
	 * @param \OC\User\Manager $userManager
75
	 * @param \OC\Hooks\PublicEmitter $emitter
76
	 * @param string $displayName
77
	 */
78
	public function __construct($gid, $backends, $userManager, EventDispatcherInterface $eventDispatcher, $emitter = null, $displayName = null) {
79
		$this->gid = $gid;
80
		$this->backends = $backends;
81
		$this->userManager = $userManager;
82
		$this->eventDispatcher = $eventDispatcher;
83
		$this->emitter = $emitter;
84
		$this->displayName = $displayName;
0 ignored issues
show
Bug introduced by
The property displayName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
85
	}
86
87
	public function getGID() {
88
		return $this->gid;
89
	}
90
91
	public function getDisplayName() {
92
		if ($this->displayName === null) {
93
			return $this->gid;
94
		}
95
		return $this->displayName;
96
	}
97
98
	/**
99
	 * get all users in the group
100
	 *
101
	 * @return \OC\User\User[]
102
	 */
103
	public function getUsers() {
104
		if ($this->usersLoaded) {
105
			return $this->users;
106
		}
107
108
		$userIds = [];
109
		foreach ($this->backends as $backend) {
110
			$diff = \array_diff(
111
				$backend->usersInGroup($this->gid),
112
				$userIds
113
			);
114
			if ($diff) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $diff of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
115
				$userIds = \array_merge($userIds, $diff);
116
			}
117
		}
118
119
		$this->users = $this->getVerifiedUsers($userIds);
120
		$this->usersLoaded = true;
121
		return $this->users;
122
	}
123
124
	/**
125
	 * check if a user is in the group
126
	 *
127
	 * @param IUser $user
128
	 * @return bool
129
	 */
130
	public function inGroup($user) {
131
		if (isset($this->users[$user->getUID()])) {
132
			return true;
133
		}
134
		foreach ($this->backends as $backend) {
135
			if ($backend->inGroup($user->getUID(), $this->gid)) {
136
				$this->users[$user->getUID()] = $user;
137
				return true;
138
			}
139
		}
140
		return false;
141
	}
142
143
	/**
144
	 * add a user to the group
145
	 *
146
	 * @param \OC\User\User $user
147
	 */
148
	public function addUser($user) {
149
		if ($this->inGroup($user)) {
150
			return;
151
		}
152
153
		if ($this->emitter) {
154
			$this->emitter->emit('\OC\Group', 'preAddUser', [$this, $user]);
155
		}
156
		$this->eventDispatcher->dispatch('group.preAddUser', new GenericEvent($this, ['user' => $user]));
157
		foreach ($this->backends as $backend) {
158
			if ($backend->implementsActions(\OC\Group\Backend::ADD_TO_GROUP)) {
159
				$backend->addToGroup($user->getUID(), $this->gid);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class OC\Group\Backend as the method addToGroup() does only exist in the following sub-classes of OC\Group\Backend: OC\Group\Database, OC_Group_Database. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
160
				if ($this->users) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->users of type OC\User\User[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
161
					$this->users[$user->getUID()] = $user;
162
				}
163
				if ($this->emitter) {
164
					$this->emitter->emit('\OC\Group', 'postAddUser', [$this, $user]);
165
				}
166
				$this->eventDispatcher->dispatch('group.postAddUser', new GenericEvent($this, ['user' => $user]));
167
				return;
168
			}
169
		}
170
	}
171
172
	/**
173
	 * remove a user from the group
174
	 *
175
	 * @param \OC\User\User $user
176
	 */
177
	public function removeUser($user) {
178
		$result = false;
179
		if ($this->emitter) {
180
			$this->emitter->emit('\OC\Group', 'preRemoveUser', [$this, $user]);
181
		}
182
		$this->eventDispatcher->dispatch('group.preRemoveUser', new GenericEvent($this, ['user' => $user]));
183
		foreach ($this->backends as $backend) {
184
			if ($backend->implementsActions(\OC\Group\Backend::REMOVE_FROM_GOUP) and $backend->inGroup($user->getUID(), $this->gid)) {
185
				$backend->removeFromGroup($user->getUID(), $this->gid);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class OC\Group\Backend as the method removeFromGroup() does only exist in the following sub-classes of OC\Group\Backend: OC\Group\Database, OC_Group_Database. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
186
				$result = true;
187
			}
188
		}
189
		if ($result) {
190
			if ($this->emitter) {
191
				$this->emitter->emit('\OC\Group', 'postRemoveUser', [$this, $user]);
192
			}
193
			$this->eventDispatcher->dispatch('group.postRemoveUser', new GenericEvent($this, ['user' => $user]));
194
			if ($this->users) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->users of type OC\User\User[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
195
				foreach ($this->users as $index => $groupUser) {
196
					if ($groupUser->getUID() === $user->getUID()) {
197
						unset($this->users[$index]);
198
						return;
199
					}
200
				}
201
			}
202
		}
203
	}
204
205
	/**
206
	 * search for users in the group by userid
207
	 *
208
	 * @param string $search
209
	 * @param int $limit
210
	 * @param int $offset
211
	 * @return \OC\User\User[]
212
	 */
213 View Code Duplication
	public function searchUsers($search, $limit = null, $offset = null) {
214
		$users = [];
215
		foreach ($this->backends as $backend) {
216
			$userIds = $backend->usersInGroup($this->gid, $search, $limit, $offset);
217
			$users += $this->getVerifiedUsers($userIds);
218
			if ($limit !== null and $limit <= 0) {
219
				return \array_values($users);
220
			}
221
		}
222
		return \array_values($users);
223
	}
224
225
	/**
226
	 * returns the number of users matching the search string
227
	 *
228
	 * @param string $search
229
	 * @return int|bool
230
	 */
231
	public function count($search = '') {
232
		$users = false;
233
		foreach ($this->backends as $backend) {
234
			if ($backend->implementsActions(\OC\Group\Backend::COUNT_USERS)) {
235
				if ($users === false) {
236
					//we could directly add to a bool variable, but this would
237
					//be ugly
238
					$users = 0;
239
				}
240
				$users += $backend->countUsersInGroup($this->gid, $search);
0 ignored issues
show
Bug introduced by
The method countUsersInGroup() does not exist on OC\Group\Backend. Did you maybe mean inGroup()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
241
			}
242
		}
243
		return $users;
0 ignored issues
show
Bug Compatibility introduced by
The expression return $users; of type integer|double|false is incompatible with the return type declared by the interface OCP\IGroup::count of type integer|boolean as it can also be of type double which is not included in this return type.
Loading history...
244
	}
245
246
	/**
247
	 * search for users in the group by displayname
248
	 *
249
	 * @param string $search
250
	 * @param int $limit
251
	 * @param int $offset
252
	 * @return \OC\User\User[]
253
	 */
254 View Code Duplication
	public function searchDisplayName($search, $limit = null, $offset = null) {
255
		$users = [];
256
		foreach ($this->backends as $backend) {
257
			$userIds = $backend->usersInGroup($this->gid, $search, $limit, $offset);
258
			$users = $this->getVerifiedUsers($userIds);
259
			if ($limit !== null and $limit <= 0) {
260
				return \array_values($users);
261
			}
262
		}
263
		return \array_values($users);
264
	}
265
266
	/**
267
	 * delete the group
268
	 *
269
	 * @return bool
270
	 */
271
	public function delete() {
272
		// Prevent users from deleting group admin
273
		if ($this->getGID() === 'admin') {
274
			return false;
275
		}
276
277
		$result = false;
278
		if ($this->emitter) {
279
			$this->emitter->emit('\OC\Group', 'preDelete', [$this]);
280
		}
281
		$this->eventDispatcher->dispatch('group.preDelete', new GenericEvent($this));
282
		foreach ($this->backends as $backend) {
283
			if ($backend->implementsActions(\OC\Group\Backend::DELETE_GROUP)) {
284
				$result = true;
285
				$backend->deleteGroup($this->gid);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class OC\Group\Backend as the method deleteGroup() does only exist in the following sub-classes of OC\Group\Backend: OC\Group\Database, OC_Group_Database. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
286
			}
287
		}
288
		if ($result and $this->emitter) {
289
			$this->emitter->emit('\OC\Group', 'postDelete', [$this]);
290
		}
291
		$this->eventDispatcher->dispatch('group.postDelete', new GenericEvent($this));
292
		return $result;
293
	}
294
295
	/**
296
	 * returns all the Users from an array that really exists
297
	 * @param string[] $userIds an array containing user IDs
298
	 * @return \OC\User\User[] an Array with the userId as Key and \OC\User\User as value
299
	 */
300
	private function getVerifiedUsers($userIds) {
301
		if (!\is_array($userIds)) {
302
			return [];
303
		}
304
		$users = [];
305
		foreach ($userIds as $userId) {
306
			$user = $this->userManager->get($userId);
307
			if ($user !== null) {
308
				$users[$userId] = $user;
309
			}
310
		}
311
		return $users;
312
	}
313
314
	/**
315
	 * Returns the backend for this group
316
	 *
317
	 * @return \OC\Group\Backend
318
	 * @since 10.0.0
319
	 */
320
	public function getBackend() {
321
		// multiple backends can exist for the same group name,
322
		// but in practice there is only a single one, so return that one
323
		return $this->backends[0];
324
	}
325
}
326