Passed
Pull Request — master (#1516)
by Daniel
03:43
created

ShareService::sendInvitation()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 7
c 2
b 0
f 0
dl 0
loc 15
rs 10
ccs 0
cts 0
cp 0
cc 4
nc 3
nop 1
crap 20
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
namespace OCA\Polls\Service;
25
26
use Psr\Log\LoggerInterface;
27
use OCP\AppFramework\Db\DoesNotExistException;
28
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
29
use OCA\Polls\Exceptions\NotAuthorizedException;
30
use OCA\Polls\Exceptions\InvalidShareTypeException;
31
use OCA\Polls\Exceptions\ShareAlreadyExistsException;
32
use OCA\Polls\Exceptions\NotFoundException;
33
34
use OCP\Security\ISecureRandom;
35
use OCP\IGroupManager;
36
use OCA\Polls\Db\ShareMapper;
37
use OCA\Polls\Db\Share;
38
use OCA\Polls\Model\Acl;
39
use OCA\Polls\Model\UserGroupClass;
40
41
class ShareService {
42
43
	/** @var LoggerInterface */
44
	private $logger;
45
46
	/** @var string */
47
	private $appName;
48
49
	/** @var string|null */
50
	private $userId;
51
52
	/** @var IGroupManager */
53
	private $groupManager;
54
55
	/** @var SystemService */
56
	private $systemService;
57
58
	/** @var ShareMapper */
59
	private $shareMapper;
60
61
	/** @var Share */
62
	private $share;
63
64
	/** @var MailService */
65
	private $mailService;
66
67
	/** @var Acl */
68
	private $acl;
69
70
	/** @var NotificationService */
71
	private $notificationService;
72
73
	public function __construct(
74
		string $AppName,
75
		LoggerInterface $logger,
76
		?string $UserId,
77
		IGroupManager $groupManager,
78
		SystemService $systemService,
79
		ShareMapper $shareMapper,
80
		Share $share,
81
		MailService $mailService,
82
		Acl $acl,
83
		NotificationService $notificationService
84
	) {
85
		$this->appName = $AppName;
86
		$this->logger = $logger;
87
		$this->userId = $UserId;
88
		$this->groupManager = $groupManager;
89
		$this->systemService = $systemService;
90
		$this->shareMapper = $shareMapper;
91
		$this->share = $share;
92
		$this->mailService = $mailService;
93
		$this->acl = $acl;
94
		$this->notificationService = $notificationService;
95
	}
96
97
	/**
98
	 * Read all shares of a poll based on the poll id and return list as array
99
	 *
100
	 * @return Share[]
101
	 *
102
	 * @psalm-return array<array-key, Share>
103
	 */
104
	public function list(int $pollId): array {
105
		try {
106
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
107
			$shares = $this->shareMapper->findByPoll($pollId);
108
		} catch (NotAuthorizedException $e) {
109
			return [];
110
		} catch (DoesNotExistException $e) {
111
			return [];
112
		}
113
114
		return $shares;
115
	}
116
117
	/**
118
	 * Validate share
119
	 */
120
	private function validate():void {
121
		switch ($this->share->getType()) {
122
			case Share::TYPE_PUBLIC:
123
				// public shares are alway valid
124
				break;
125
			case Share::TYPE_USER:
126
				if ($this->share->getUserId() !== $this->userId) {
127
					// share is not valid for user
128
					throw new NotAuthorizedException;
129
				}
130
				break;
131
			case Share::TYPE_GROUP:
132
				if (!\OC::$server->getUserSession()->isLoggedIn()) {
133
					throw new NotAuthorizedException;
134
				}
135
136
				if (!$this->groupManager->isInGroup($this->share->getUserId(), $this->userId)) {
137
					throw new NotAuthorizedException;
138
				}
139
140
				break;
141
			case Share::TYPE_EMAIL:
142
				break;
143
			case Share::TYPE_EXTERNAL:
144
				break;
145
			default:
146
				$this->logger->alert(json_encode('invalid share type ' . $this->share->getType()));
147
				throw new NotAuthorizedException;
148
		}
149
	}
150
151
	/**
152
	 * Get share by token
153
	 */
154
	public function get(string $token, bool $validate = false) {
155
		try {
156
			$this->share = $this->shareMapper->findByToken($token);
157
			if ($validate) {
158
				$this->validate();
159
			}
160
		} catch (DoesNotExistException $e) {
161
			throw new NotFoundException('Token ' . $token . ' does not exist');
162
		}
163
		// Allow users entering the poll with a public share access
164
165
		if ($this->share->getType() === Share::TYPE_PUBLIC && \OC::$server->getUserSession()->isLoggedIn()) {
166
			try {
167
				// Test if the user has already access.
168
				$this->acl->setPollId($this->share->getPollId())->request(Acl::PERMISSION_VIEW);
169
			} catch (NotAuthorizedException $e) {
170
				// If he is not authorized until now, create a new personal share for this user.
171
				// Return the created share
172
				return $this->create(
173
					$this->share->getPollId(),
174
					UserGroupClass::getUserGroupChild(Share::TYPE_USER, \OC::$server->getUserSession()->getUser()->getUID()),
175
					true
176
				);
177
			}
178
		}
179
		return $this->share;
180
	}
181
182
	/**
183
	 * Get share by token
184
	 *
185
	 * @return Share
186
	 */
187
	public function setInvitationSent(string $token): Share {
188
		$share = $this->shareMapper->findByToken($token);
189
		$share->setInvitationSent(time());
190
		return $this->shareMapper->update($share);
191
	}
192
193
	/**
194
	 * crate share - MUST BE PRIVATE!
195
	 *
196
	 * @return Share
197
	 */
198
	private function create(int $pollId, UserGroupClass $userGroup, bool $preventInvitation = false): Share {
199
		$preventInvitation = $userGroup->getType() === UserGroupClass::TYPE_PUBLIC ?: $preventInvitation;
200
		$token = \OC::$server->getSecureRandom()->generate(
201
			16,
202
			ISecureRandom::CHAR_DIGITS .
203
			ISecureRandom::CHAR_LOWER .
204
			ISecureRandom::CHAR_UPPER
205
		);
206
207
		$this->share = new Share();
208
		$this->share->setToken($token);
209
		$this->share->setPollId($pollId);
210
211
		// Convert user type contact to share type email
212
		if ($userGroup->getType() === UserGroupClass::TYPE_CONTACT) {
213
			$this->share->setType(Share::TYPE_EMAIL);
214
			$this->share->setUserId($userGroup->getEmailAddress());
215
		} else {
216
			$this->share->setType($userGroup->getType());
217
			$this->share->setUserId($userGroup->getType() === UserGroupClass::TYPE_PUBLIC ? $token : $userGroup->getPublicId());
218
		}
219
220
		$this->share->setInvitationSent($preventInvitation ? time() : 0);
221
		$this->share->setDisplayName($userGroup->getDisplayName());
222
		$this->share->setEmailAddress($userGroup->getEmailAddress());
223
224
		return $this->shareMapper->insert($this->share);
225
	}
226
227
	/**
228
	 * Add share
229
	 *
230
	 * @return Share
231
	 */
232
	public function add(int $pollId, string $type, string $userId = ''): Share {
233
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
234
235
		if ($type !== UserGroupClass::TYPE_PUBLIC) {
236
			try {
237
				$this->shareMapper->findByPollAndUser($pollId, $userId);
238
				throw new ShareAlreadyExistsException;
239
			} catch (MultipleObjectsReturnedException $e) {
240
				throw new ShareAlreadyExistsException;
241
			} catch (DoesNotExistException $e) {
242
				// continue
243
			}
244
		}
245
246
		$userGroup = UserGroupClass::getUserGroupChild($type, $userId);
247
		return $this->create($pollId, $userGroup);
248
	}
249
250
	/**
251
	 * Set emailAddress to personal share
252
	 * or update an email share with the username
253
	 *
254
	 * @return Share
255
	 */
256
	public function setEmailAddress(string $token, string $emailAddress, bool $emptyIsValid = false): Share {
257
		try {
258
			$this->share = $this->shareMapper->findByToken($token);
259
		} catch (DoesNotExistException $e) {
260
			throw new NotFoundException('Token ' . $token . ' does not exist');
261
		}
262
263
		if ($this->share->getType() === Share::TYPE_EXTERNAL) {
264
			$this->systemService->validateEmailAddress($emailAddress, $emptyIsValid);
265
			$this->share->setEmailAddress($emailAddress);
266
			// TODO: Send confirmation
267
			return $this->shareMapper->update($this->share);
268
		} else {
269
			throw new InvalidShareTypeException('Email address can only be set in external shares.');
270
		}
271
	}
272
273
	/**
274
	 * Delete emailAddress of personal share
275
	 *
276
	 * @return Share
277
	 */
278
	public function deleteEmailAddress(string $token): Share {
279
		try {
280
			$this->share = $this->shareMapper->findByToken($token);
281
		} catch (DoesNotExistException $e) {
282
			throw new NotFoundException('Token ' . $token . ' does not exist');
283
		}
284
285
		if ($this->share->getType() === Share::TYPE_EXTERNAL) {
286
			$this->share->setEmailAddress('');
287
			return $this->shareMapper->update($this->share);
288
		} else {
289
			throw new InvalidShareTypeException('Email address can only be set in external shares.');
290
		}
291
	}
292
293
	/**
294
	 * Create a personal share from a public share
295
	 * or update an email share with the username
296
	 *
297
	 * @return Share
298
	 */
299
	public function personal(string $token, string $userName, string $emailAddress = ''): Share {
300
		try {
301
			$this->share = $this->shareMapper->findByToken($token);
302
		} catch (DoesNotExistException $e) {
303
			throw new NotFoundException('Token ' . $token . ' does not exist');
304
		}
305
306
		$this->systemService->validatePublicUsername($userName, $token);
307
		$this->systemService->validateEmailAddress($emailAddress, true);
308
309
		if ($this->share->getType() === Share::TYPE_PUBLIC) {
310
			// Create new external share for user, who entered the poll via public link,
311
			// prevent invtation sending, when no email address is given
312
			$this->create(
313
				$this->share->getPollId(),
314
				UserGroupClass::getUserGroupChild(Share::TYPE_EXTERNAL, $userName, $userName, $emailAddress),
315
				!$emailAddress
316
			);
317
		} elseif ($this->share->getType() === Share::TYPE_EMAIL
318
				|| $this->share->getType() === Share::TYPE_CONTACT) {
319
			// Convert email and contact shares to external share, if user registers
320
			$this->share->setType(Share::TYPE_EXTERNAL);
321
			$this->share->setUserId($userName);
322
			$this->share->setDisplayName($userName);
323
324
			// prepare for resending inviataion to new email address
325
			if ($emailAddress !== $this->share->getEmailAddress()) {
326
				$this->share->setInvitationSent(0);
327
			}
328
			$this->share->setEmailAddress($emailAddress);
329
			$this->shareMapper->update($this->share);
330
		} else {
331
			throw new NotAuthorizedException;
332
		}
333
334
		// send invitaitoin mail, if invitationSent has no timestamp
335
		if (!$this->share->getInvitationSent()) {
336
			$this->mailService->resendInvitation($this->share->getToken());
337
		}
338
339
		return $this->share;
340
	}
341
342
	/**
343
	 * Delete share
344
	 */
345
	public function delete(string $token): string {
346
		try {
347
			$this->share = $this->shareMapper->findByToken($token);
348
			$this->acl->setPollId($this->share->getPollId())->request(Acl::PERMISSION_EDIT);
349
			$this->shareMapper->delete($this->share);
350
		} catch (DoesNotExistException $e) {
351
			// silently catch
352
		}
353
		return $token;
354
	}
355
356
	/**
357
	 * Sent invitation mails for a share
358
	 * Additionally send notification via notifications
359
	 */
360
	public function sendInvitation(string $token): array {
361
		$share = $this->get($token);
362
		if ($share->getType() === Share::TYPE_USER) {
363
			$this->notificationService->sendInvitation($share->getPollId(), $share->getUserId());
364
365
			// TODO: skip this atm, to send invitations as mail too, if user is a site user
366
			// $sentResult = ['sentMails' => [new User($share->getuserId())]];
367
			// $this->shareService->setInvitationSent($token);
368
		} elseif ($share->getType() === Share::TYPE_GROUP) {
369
			foreach ($share->getMembers() as $member) {
370
				$this->notificationService->sendInvitation($share->getPollId(), $member->getId());
371
			}
372
		}
373
374
		return $this->mailService->sendInvitation($token);
375
	}
376
}
377