Passed
Pull Request — master (#1339)
by René
05:25 queued 01:34
created

Acl::getAllowEdit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 0
dl 0
loc 2
ccs 0
cts 2
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Vinzenz Rosenkranz <[email protected]>
4
 *
5
 * @author René Gieling <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 *  This program is free software: you can redistribute it and/or modify
10
 *  it under the terms of the GNU Affero General Public License as
11
 *  published by the Free Software Foundation, either version 3 of the
12
 *  License, or (at your option) any later version.
13
 *
14
 *  This program is distributed in the hope that it will be useful,
15
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 *  GNU Affero General Public License for more details.
18
 *
19
 *  You should have received a copy of the GNU Affero General Public License
20
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
25
namespace OCA\Polls\Model;
26
27
use JsonSerializable;
28
use OCP\AppFramework\Db\DoesNotExistException;
29
use OCA\Polls\Exceptions\NotAuthorizedException;
30
use OCA\Polls\Exceptions\VoteLimitExceededException;
31
32
use OCP\IUserManager;
33
use OCP\IGroupManager;
34
use OCA\Polls\Db\Poll;
35
use OCA\Polls\Db\Share;
36
use OCA\Polls\Db\PollMapper;
37
use OCA\Polls\Db\VoteMapper;
38
use OCA\Polls\Db\ShareMapper;
39
40
/**
41
 * Class Acl
42
 *
43
 * @package OCA\Polls\Model\Acl
44
 */
45
class Acl implements JsonSerializable {
46
	public const PERMISSION_VIEW ='view';
47
	public const PERMISSION_EDIT ='edit';
48
	public const PERMISSION_DELETE ='delete';
49
	public const PERMISSION_COMMENT ='comment';
50
	public const PERMISSION_SUBSCRIBE ='subscribe';
51
	public const PERMISSION_VOTE ='vote';
52
	public const PERMISSION_SEE_RESULTS ='seeResults';
53
	public const PERMISSION_SEE_USERNAMES ='seeUserNames';
54
	public const PERMISSION_TAKE_OVER ='takeOver';
55
56
	/** @var IUserManager */
57
	private $userManager;
58
59
	/** @var IGroupManager */
60
	private $groupManager;
61
62
	/** @var PollMapper */
63
	private $pollMapper;
64
65
	/** @var VoteMapper */
66
	private $voteMapper;
67
68
	/** @var ShareMapper */
69
	private $shareMapper;
70
71
	/** @var Poll */
72
	private $poll;
73
74
	/** @var Share */
75
	private $share;
76
77
	public function __construct(
78
		IUserManager $userManager,
79
		IGroupManager $groupManager,
80
		PollMapper $pollMapper,
81
		VoteMapper $voteMapper,
82
		ShareMapper $shareMapper
83
	) {
84
		$this->userManager = $userManager;
85
		$this->groupManager = $groupManager;
86
		$this->pollMapper = $pollMapper;
87
		$this->voteMapper = $voteMapper;
88
		$this->shareMapper = $shareMapper;
89
		$this->poll = new Poll;
90
		$this->share = new Share;
91
	}
92
93
94
	/**
95
	 * load share via token and than call setShare
96
	 */
97
	public function setToken(string $token = ''): Acl {
98
		try {
99
			return $this->setShare($this->shareMapper->findByToken($token));
100
		} catch (DoesNotExistException $e) {
101
			throw new NotAuthorizedException('Error loading share ' . $token);
102
		}
103
	}
104
105
	/**
106
	 * setShare - sets and validates the share
107
	 * read access is
108
	 */
109
	public function setShare(Share $share): Acl {
110
		$this->share = $share;
111
		$this->validateShareAccess();
112
		$this->setPollId($share->getPollId());
113
		$this->request(Self::PERMISSION_VIEW);
114
		return $this;
115
	}
116
117
	public function getToken(): string {
118
		return strval($this->share->getToken());
119
	}
120
121
	public function setPollId(?int $pollId = 0): Acl {
122
		try {
123
			return $this->setPoll($this->pollMapper->find($pollId));
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $id of OCA\Polls\Db\PollMapper::find() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

123
			return $this->setPoll($this->pollMapper->find(/** @scrutinizer ignore-type */ $pollId));
Loading history...
124
		} catch (DoesNotExistException $e) {
125
			throw new NotAuthorizedException('Error loading poll ' . $pollId);
126
		}
127
	}
128
129
	public function setPoll(Poll $poll): Acl {
130
		$this->poll = $poll;
131
		return $this;
132
	}
133
134
	public function getPollId(): int {
135
		return $this->poll->getId();
136
	}
137
138
	public function getUserId() {
139
		if ($this->getLoggedIn()) {
140
			return \OC::$server->getUserSession()->getUser()->getUID();
141
		} else {
142
			return $this->share->getUserId();
143
		}
144
	}
145
146
	public function validateUserId(string $userId): void {
147
		if ($this->getUserId() !== $userId) {
148
			throw new NotAuthorizedException;
149
		}
150
	}
151
152
	private function getDisplayName(): string {
153
		if ($this->getLoggedIn()) {
154
			return $this->userManager->get($this->getUserId())->getDisplayName();
155
		} else {
156
			return $this->share->getDisplayName();
157
		}
158
	}
159
160
	public function isAllowed(string $permission): bool {
161
		switch ($permission) {
162
			case Self::PERMISSION_VIEW:
163
				if ($this->getIsOwner() || $this->hasAdminAccess()) {
164
					// always grant access, if user has edit rights
165
					return true;
166
				} elseif ($this->poll->getDeleted()) {
167
					// always deny access, if poll is deleted
168
					return false;
169
				} elseif ($this->poll->getAccess() === Poll::ACCESS_PUBLIC) {
170
					// grant access if poll poll is public
171
					return true;
172
				} elseif ($this->getUserIsInvolved()) {
173
					// grant access if user is involved in poll in any way
174
					return true;
175
				} elseif ($this->getToken()) {
176
					// user has token
177
					return true;
178
				}
179
				break;
180
181
			case Self::PERMISSION_EDIT:
182
				return $this->getIsOwner() || $this->hasAdminAccess();
183
			case Self::PERMISSION_DELETE:
184
				return $this->getIsOwner() || $this->hasAdminAccess() || $this->getIsAdmin();
185
			case Self::PERMISSION_COMMENT:
186
				return true;
187
			case Self::PERMISSION_SUBSCRIBE:
188
				return $this->hasEmail();
189
			case Self::PERMISSION_VOTE:
190
				return !$this->poll->getExpired() && $this->share->getType() !== Share::TYPE_PUBLIC;
191
			case Self::PERMISSION_SEE_RESULTS:
192
				if ($this->getIsOwner()) {
193
					return true;
194
				} elseif ($this->poll->getShowResults() === Poll::SHOW_RESULTS_ALWAYS) {
195
					return true;
196
				} elseif ($this->poll->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->poll->getExpired()) {
197
					return true;
198
				}
199
				break;
200
			case Self::PERMISSION_SEE_USERNAMES:
201
				return $this->getIsOwner() || !$this->poll->getAnonymous();
202
			case Self::PERMISSION_TAKE_OVER:
203
				return $this->getIsAdmin();
204
			default:
205
				break;
206
		}
207
		return false;
208
	}
209
210
	public function request(string $permission): void {
211
		if (!$this->isAllowed($permission)) {
212
			throw new NotAuthorizedException('denied permission ' . $permission);
213
		}
214
	}
215
216
	public function getAllowYesVote(): bool {
217
		return !($this->poll->getVoteLimit() && $this->getYesVotes() >= $this->poll->getVoteLimit());
218
	}
219
220
	public function getOptionLimit(): int {
221
		return $this->poll->getOptionLimit();
222
	}
223
224
	private function getYesVotes(): int {
225
		return $this->voteMapper->countYesVotes($this->getUserId(), $this->getPollId());
226
	}
227
228
	public function requestYesVotes(): void {
229
		if (!$this->getAllowYesVote()) {
230
			throw new VoteLimitExceededException;
231
		}
232
	}
233
234
	public function jsonSerialize(): array {
235
		return	[
236
			'allowComment'      => $this->isAllowed(Self::PERMISSION_COMMENT),
237
			'allowEdit'         => $this->isAllowed(Self::PERMISSION_EDIT),
238
			'allowSeeResults'   => $this->isAllowed(Self::PERMISSION_SEE_RESULTS),
239
			'allowSeeUsernames' => $this->isAllowed(Self::PERMISSION_SEE_USERNAMES),
240
			'allowSubscribe'    => $this->isAllowed(Self::PERMISSION_SUBSCRIBE),
241
			'allowView'         => $this->isAllowed(Self::PERMISSION_VIEW),
242
			'allowVote'         => $this->isAllowed(Self::PERMISSION_VOTE),
243
			'displayName'       => $this->getDisplayName(),
244
			'isOwner'           => $this->getIsOwner(),
245
			'loggedIn'			=> $this->getLoggedIn(),
246
			'pollId'            => $this->getPollId(),
247
			'token'             => $this->getToken(),
248
			'userHasVoted'		=> $this->getUserHasVoted(),
249
			'userId'            => $this->getUserId(),
250
			'userIsInvolved'	=> $this->getUserIsInvolved(),
251
		];
252
	}
253
254
	/**
255
	 * getLoggedIn - Is user logged in to nextcloud?
256
	 */
257
	private function getLoggedIn(): bool {
258
		return \OC::$server->getUserSession()->isLoggedIn();
259
	}
260
261
	/**
262
	 * getIsOwner - Is user owner of the poll?
263
	 */
264
	private function getIsOwner(): bool {
265
		return ($this->getLoggedIn() && $this->poll->getOwner() === $this->getUserId());
266
	}
267
268
	/**
269
	 * getIsAdmin - Is the user admin
270
	 * Returns true, if user is in admin group
271
	 */
272
	private function getIsAdmin(): bool {
273
		return ($this->getLoggedIn() && $this->groupManager->isAdmin($this->getUserId()));
274
	}
275
276
	/**
277
	 * hasAdminAccess - Has user administrative rights?
278
	 * Returns true, if user is in admin group and poll has allowed admins to manage the poll
279
	 */
280
	private function hasAdminAccess(): bool {
281
		return ($this->getIsAdmin() && $this->poll->getAdminAccess());
282
	}
283
284
	/**
285
	 * getUserIsInvolved - Is user involved?
286
	 * Returns true, if the current user is involved in the share via share or if he is a participant.
287
	 */
288
	private function getUserIsInvolved(): bool {
289
		return (
290
			   $this->getIsOwner()
291
			|| $this->getUserHasVoted()
292
			|| $this->getGroupShare()
293
			|| $this->getPersonalShare());
294
	}
295
296
	/**
297
	 * getUserHasVoted - Is user a participant?
298
	 * Returns true, if the current user is already a particitipant of the current poll.
299
	 */
300
	private function getUserHasVoted(): bool {
301
		return count(
302
			$this->voteMapper->findParticipantsVotes($this->getPollId(), $this->getUserId())
303
		) > 0;
304
	}
305
306
	/**
307
	 * getGroupShare - Is the poll shared via group share?
308
	 * Returns true, if the current poll contains a group share with a group,
309
	 * where the current user is member of. This only affects logged users.
310
	 */
311
	private function getGroupShare(): int {
312
		if (!$this->getLoggedIn()) {
313
			return 0;
314
		}
315
		return count(
316
			array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
317
				if ($item->getType() === Share::TYPE_GROUP && $this->groupManager->isInGroup($this->getUserId(), $item->getUserId())) {
318
					return true;
319
				}
320
			})
321
		);
322
	}
323
324
	/**
325
	 * getPersonalShare - Is the poll shared via user share?
326
	 * Returns >0, if the current poll contains a user share for the current user.
327
	 * This only affects logged users.
328
	 */
329
	private function getPersonalShare(): int {
330
		if (!$this->getLoggedIn()) {
331
			return 0;
332
		}
333
		return count(
334
			array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
335
				if (in_array($item->getType(), [
336
					Share::TYPE_USER,
337
					Share::TYPE_EXTERNAL,
338
					Share::TYPE_EMAIL,
339
					Share::TYPE_CONTACT
340
				])
341
					&& $item->getUserId() === $this->getUserId()
342
				) {
343
					return true;
344
				}
345
			})
346
		);
347
	}
348
349
	private function validateShareAccess(): void {
350
		if ($this->getLoggedIn()) {
351
			if (!$this->getValidAuthenticatedShare()) {
352
				throw new NotAuthorizedException('Share type "' . $this->share->getType() . '"only valid for external users');
353
			};
354
		} else {
355
			if (!$this->getValidPublicShare()) {
356
				throw new NotAuthorizedException('Share type "' . $this->share->getType() . '"only valid for internal users');
357
			};
358
		}
359
	}
360
361
	private function getValidPublicShare(): bool {
362
		return in_array($this->share->getType(), [
363
			Share::TYPE_PUBLIC,
364
			Share::TYPE_EMAIL,
365
			Share::TYPE_CONTACT,
366
			Share::TYPE_EXTERNAL
367
		]);
368
	}
369
370
	private function getValidAuthenticatedShare(): bool {
371
		return in_array($this->share->getType(), [
372
			Share::TYPE_PUBLIC,
373
			Share::TYPE_USER,
374
			Share::TYPE_GROUP
375
		]);
376
	}
377
378
	private function hasEmail(): bool {
379
		if ($this->share->getToken()) {
380
			return strlen($this->share->getEmailAddress()) > 0;
381
		} else {
382
			return $this->getLoggedIn();
383
		}
384
	}
385
}
386