Passed
Push — master ( 98fd66...bf4acd )
by Joas
18:11 queued 12s
created

StatusService::addDefaultMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2020, Georg Ehrke
7
 *
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 *
11
 * @license GNU AGPL version 3 or any later version
12
 *
13
 * This program is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License as
15
 * published by the Free Software Foundation, either version 3 of the
16
 * License, or (at your option) any later version.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
 *
26
 */
27
namespace OCA\UserStatus\Service;
28
29
use OCA\UserStatus\Db\UserStatus;
30
use OCA\UserStatus\Db\UserStatusMapper;
31
use OCA\UserStatus\Exception\InvalidClearAtException;
32
use OCA\UserStatus\Exception\InvalidMessageIdException;
33
use OCA\UserStatus\Exception\InvalidStatusIconException;
34
use OCA\UserStatus\Exception\InvalidStatusTypeException;
35
use OCA\UserStatus\Exception\StatusMessageTooLongException;
36
use OCP\AppFramework\Db\DoesNotExistException;
37
use OCP\AppFramework\Utility\ITimeFactory;
38
use OCP\DB\Exception;
39
use OCP\IConfig;
40
use OCP\IUser;
41
use OCP\UserStatus\IUserStatus;
42
43
/**
44
 * Class StatusService
45
 *
46
 * @package OCA\UserStatus\Service
47
 */
48
class StatusService {
49
50
	/** @var UserStatusMapper */
51
	private $mapper;
52
53
	/** @var ITimeFactory */
54
	private $timeFactory;
55
56
	/** @var PredefinedStatusService */
57
	private $predefinedStatusService;
58
59
	/** @var EmojiService */
60
	private $emojiService;
61
62
	/** @var bool */
63
	private $shareeEnumeration;
64
65
	/** @var bool */
66
	private $shareeEnumerationInGroupOnly;
67
68
	/** @var bool */
69
	private $shareeEnumerationPhone;
70
71
	/**
72
	 * List of priorities ordered by their priority
73
	 */
74
	public const PRIORITY_ORDERED_STATUSES = [
75
		IUserStatus::ONLINE,
76
		IUserStatus::AWAY,
77
		IUserStatus::DND,
78
		IUserStatus::INVISIBLE,
79
		IUserStatus::OFFLINE,
80
	];
81
82
	/**
83
	 * List of statuses that persist the clear-up
84
	 * or UserLiveStatusEvents
85
	 */
86
	public const PERSISTENT_STATUSES = [
87
		IUserStatus::AWAY,
88
		IUserStatus::DND,
89
		IUserStatus::INVISIBLE,
90
	];
91
92
	/** @var int */
93
	public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
94
95
	/** @var int */
96
	public const MAXIMUM_MESSAGE_LENGTH = 80;
97
98
	/**
99
	 * StatusService constructor.
100
	 *
101
	 * @param UserStatusMapper $mapper
102
	 * @param ITimeFactory $timeFactory
103
	 * @param PredefinedStatusService $defaultStatusService
104
	 * @param EmojiService $emojiService
105
	 * @param IConfig $config
106
	 */
107
	public function __construct(UserStatusMapper $mapper,
108
								ITimeFactory $timeFactory,
109
								PredefinedStatusService $defaultStatusService,
110
								EmojiService $emojiService,
111
								IConfig $config) {
112
		$this->mapper = $mapper;
113
		$this->timeFactory = $timeFactory;
114
		$this->predefinedStatusService = $defaultStatusService;
115
		$this->emojiService = $emojiService;
116
		$this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
117
		$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
118
		$this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
119
	}
120
121
	/**
122
	 * @param int|null $limit
123
	 * @param int|null $offset
124
	 * @return UserStatus[]
125
	 */
126
	public function findAll(?int $limit = null, ?int $offset = null): array {
127
		// Return empty array if user enumeration is disabled or limited to groups
128
		// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
129
		//       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
130
		if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
131
			return [];
132
		}
133
134
		return array_map(function ($status) {
135
			return $this->processStatus($status);
136
		}, $this->mapper->findAll($limit, $offset));
137
	}
138
139
	/**
140
	 * @param int|null $limit
141
	 * @param int|null $offset
142
	 * @return array
143
	 */
144
	public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
145
		// Return empty array if user enumeration is disabled or limited to groups
146
		// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
147
		//       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
148
		if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
149
			return [];
150
		}
151
152
		return array_map(function ($status) {
153
			return $this->processStatus($status);
154
		}, $this->mapper->findAllRecent($limit, $offset));
155
	}
156
157
	/**
158
	 * @param string $userId
159
	 * @return UserStatus
160
	 * @throws DoesNotExistException
161
	 */
162
	public function findByUserId(string $userId):UserStatus {
163
		return $this->processStatus($this->mapper->findByUserId($userId));
164
	}
165
166
	/**
167
	 * @param array $userIds
168
	 * @return UserStatus[]
169
	 */
170
	public function findByUserIds(array $userIds):array {
171
		return array_map(function ($status) {
172
			return $this->processStatus($status);
173
		}, $this->mapper->findByUserIds($userIds));
174
	}
175
176
	/**
177
	 * @param string $userId
178
	 * @param string $status
179
	 * @param int|null $statusTimestamp
180
	 * @param bool $isUserDefined
181
	 * @return UserStatus
182
	 * @throws InvalidStatusTypeException
183
	 */
184
	public function setStatus(string $userId,
185
							  string $status,
186
							  ?int $statusTimestamp,
187
							  bool $isUserDefined): UserStatus {
188
		try {
189
			$userStatus = $this->mapper->findByUserId($userId);
190
		} catch (DoesNotExistException $ex) {
191
			$userStatus = new UserStatus();
192
			$userStatus->setUserId($userId);
193
		}
194
195
		// Check if status-type is valid
196
		if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
197
			throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
198
		}
199
		if ($statusTimestamp === null) {
200
			$statusTimestamp = $this->timeFactory->getTime();
201
		}
202
203
		$userStatus->setStatus($status);
204
		$userStatus->setStatusTimestamp($statusTimestamp);
205
		$userStatus->setIsUserDefined($isUserDefined);
206
		$userStatus->setIsBackup(false);
207
208
		if ($userStatus->getId() === null) {
0 ignored issues
show
introduced by
The condition $userStatus->getId() === null is always false.
Loading history...
209
			return $this->mapper->insert($userStatus);
210
		}
211
212
		return $this->mapper->update($userStatus);
213
	}
214
215
	/**
216
	 * @param string $userId
217
	 * @param string $messageId
218
	 * @param int|null $clearAt
219
	 * @return UserStatus
220
	 * @throws InvalidMessageIdException
221
	 * @throws InvalidClearAtException
222
	 */
223
	public function setPredefinedMessage(string $userId,
224
										 string $messageId,
225
										 ?int $clearAt): UserStatus {
226
		try {
227
			$userStatus = $this->mapper->findByUserId($userId);
228
		} catch (DoesNotExistException $ex) {
229
			$userStatus = new UserStatus();
230
			$userStatus->setUserId($userId);
231
			$userStatus->setStatus(IUserStatus::OFFLINE);
232
			$userStatus->setStatusTimestamp(0);
233
			$userStatus->setIsUserDefined(false);
234
			$userStatus->setIsBackup(false);
235
		}
236
237
		if (!$this->predefinedStatusService->isValidId($messageId)) {
238
			throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
239
		}
240
241
		// Check that clearAt is in the future
242
		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
243
			throw new InvalidClearAtException('ClearAt is in the past');
244
		}
245
246
		$userStatus->setMessageId($messageId);
247
		$userStatus->setCustomIcon(null);
248
		$userStatus->setCustomMessage(null);
249
		$userStatus->setClearAt($clearAt);
250
251
		if ($userStatus->getId() === null) {
0 ignored issues
show
introduced by
The condition $userStatus->getId() === null is always false.
Loading history...
252
			return $this->mapper->insert($userStatus);
253
		}
254
255
		return $this->mapper->update($userStatus);
256
	}
257
258
	/**
259
	 * @param string $userId
260
	 * @param string $status
261
	 * @param string $messageId
262
	 * @param bool $createBackup
263
	 * @throws InvalidStatusTypeException
264
	 * @throws InvalidMessageIdException
265
	 */
266
	public function setUserStatus(string $userId,
267
										 string $status,
268
										 string $messageId,
269
										 bool $createBackup): void {
270
		// Check if status-type is valid
271
		if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
272
			throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
273
		}
274
275
		if (!$this->predefinedStatusService->isValidId($messageId)) {
276
			throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
277
		}
278
279
		if ($createBackup) {
280
			if ($this->backupCurrentStatus($userId) === false) {
0 ignored issues
show
introduced by
The condition $this->backupCurrentStatus($userId) === false is always false.
Loading history...
281
				return; // Already a status set automatically => abort.
282
			}
283
284
			// If we just created the backup
285
			$userStatus = new UserStatus();
286
			$userStatus->setUserId($userId);
287
		} else {
288
			try {
289
				$userStatus = $this->mapper->findByUserId($userId);
290
			} catch (DoesNotExistException $ex) {
291
				$userStatus = new UserStatus();
292
				$userStatus->setUserId($userId);
293
			}
294
		}
295
296
		$userStatus->setStatus($status);
297
		$userStatus->setStatusTimestamp($this->timeFactory->getTime());
298
		$userStatus->setIsUserDefined(false);
299
		$userStatus->setIsBackup(false);
300
		$userStatus->setMessageId($messageId);
301
		$userStatus->setCustomIcon(null);
302
		$userStatus->setCustomMessage(null);
303
		$userStatus->setClearAt(null);
304
305
		if ($userStatus->getId() !== null) {
0 ignored issues
show
introduced by
The condition $userStatus->getId() !== null is always true.
Loading history...
306
			$this->mapper->update($userStatus);
307
			return;
308
		}
309
		$this->mapper->insert($userStatus);
310
	}
311
312
	/**
313
	 * @param string $userId
314
	 * @param string|null $statusIcon
315
	 * @param string $message
316
	 * @param int|null $clearAt
317
	 * @return UserStatus
318
	 * @throws InvalidClearAtException
319
	 * @throws InvalidStatusIconException
320
	 * @throws StatusMessageTooLongException
321
	 */
322
	public function setCustomMessage(string $userId,
323
									 ?string $statusIcon,
324
									 string $message,
325
									 ?int $clearAt): UserStatus {
326
		try {
327
			$userStatus = $this->mapper->findByUserId($userId);
328
		} catch (DoesNotExistException $ex) {
329
			$userStatus = new UserStatus();
330
			$userStatus->setUserId($userId);
331
			$userStatus->setStatus(IUserStatus::OFFLINE);
332
			$userStatus->setStatusTimestamp(0);
333
			$userStatus->setIsUserDefined(false);
334
		}
335
336
		// Check if statusIcon contains only one character
337
		if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) {
338
			throw new InvalidStatusIconException('Status-Icon is longer than one character');
339
		}
340
		// Check for maximum length of custom message
341
		if (\mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) {
342
			throw new StatusMessageTooLongException('Message is longer than supported length of ' . self::MAXIMUM_MESSAGE_LENGTH . ' characters');
343
		}
344
		// Check that clearAt is in the future
345
		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
346
			throw new InvalidClearAtException('ClearAt is in the past');
347
		}
348
349
		$userStatus->setMessageId(null);
350
		$userStatus->setCustomIcon($statusIcon);
351
		$userStatus->setCustomMessage($message);
352
		$userStatus->setClearAt($clearAt);
353
354
		if ($userStatus->getId() === null) {
0 ignored issues
show
introduced by
The condition $userStatus->getId() === null is always false.
Loading history...
355
			return $this->mapper->insert($userStatus);
356
		}
357
358
		return $this->mapper->update($userStatus);
359
	}
360
361
	/**
362
	 * @param string $userId
363
	 * @return bool
364
	 */
365
	public function clearStatus(string $userId): bool {
366
		try {
367
			$userStatus = $this->mapper->findByUserId($userId);
368
		} catch (DoesNotExistException $ex) {
369
			// if there is no status to remove, just return
370
			return false;
371
		}
372
373
		$userStatus->setStatus(IUserStatus::OFFLINE);
374
		$userStatus->setStatusTimestamp(0);
375
		$userStatus->setIsUserDefined(false);
376
377
		$this->mapper->update($userStatus);
378
		return true;
379
	}
380
381
	/**
382
	 * @param string $userId
383
	 * @return bool
384
	 */
385
	public function clearMessage(string $userId): bool {
386
		try {
387
			$userStatus = $this->mapper->findByUserId($userId);
388
		} catch (DoesNotExistException $ex) {
389
			// if there is no status to remove, just return
390
			return false;
391
		}
392
393
		$userStatus->setMessageId(null);
394
		$userStatus->setCustomMessage(null);
395
		$userStatus->setCustomIcon(null);
396
		$userStatus->setClearAt(null);
397
398
		$this->mapper->update($userStatus);
399
		return true;
400
	}
401
402
	/**
403
	 * @param string $userId
404
	 * @return bool
405
	 */
406
	public function removeUserStatus(string $userId): bool {
407
		try {
408
			$userStatus = $this->mapper->findByUserId($userId, false);
409
		} catch (DoesNotExistException $ex) {
410
			// if there is no status to remove, just return
411
			return false;
412
		}
413
414
		$this->mapper->delete($userStatus);
415
		return true;
416
	}
417
418
	public function removeBackupUserStatus(string $userId): bool {
419
		try {
420
			$userStatus = $this->mapper->findByUserId($userId, true);
421
		} catch (DoesNotExistException $ex) {
422
			// if there is no status to remove, just return
423
			return false;
424
		}
425
426
		$this->mapper->delete($userStatus);
427
		return true;
428
	}
429
430
	/**
431
	 * Processes a status to check if custom message is still
432
	 * up to date and provides translated default status if needed
433
	 *
434
	 * @param UserStatus $status
435
	 * @return UserStatus
436
	 */
437
	private function processStatus(UserStatus $status): UserStatus {
438
		$clearAt = $status->getClearAt();
439
440
		if ($status->getStatusTimestamp() < $this->timeFactory->getTime() - self::INVALIDATE_STATUS_THRESHOLD
441
			&& (!$status->getIsUserDefined() || $status->getStatus() === IUserStatus::ONLINE)) {
442
			$this->cleanStatus($status);
443
		}
444
		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
445
			$this->cleanStatusMessage($status);
446
		}
447
		if ($status->getMessageId() !== null) {
0 ignored issues
show
introduced by
The condition $status->getMessageId() !== null is always true.
Loading history...
448
			$this->addDefaultMessage($status);
449
		}
450
451
		return $status;
452
	}
453
454
	/**
455
	 * @param UserStatus $status
456
	 */
457
	private function cleanStatus(UserStatus $status): void {
458
		if ($status->getStatus() === IUserStatus::OFFLINE && !$status->getIsUserDefined()) {
459
			return;
460
		}
461
462
		$status->setStatus(IUserStatus::OFFLINE);
463
		$status->setStatusTimestamp($this->timeFactory->getTime());
464
		$status->setIsUserDefined(false);
465
466
		$this->mapper->update($status);
467
	}
468
469
	/**
470
	 * @param UserStatus $status
471
	 */
472
	private function cleanStatusMessage(UserStatus $status): void {
473
		$status->setMessageId(null);
474
		$status->setCustomIcon(null);
475
		$status->setCustomMessage(null);
476
		$status->setClearAt(null);
477
478
		$this->mapper->update($status);
479
	}
480
481
	/**
482
	 * @param UserStatus $status
483
	 */
484
	private function addDefaultMessage(UserStatus $status): void {
485
		// If the message is predefined, insert the translated message and icon
486
		$predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId());
487
		if ($predefinedMessage !== null) {
488
			$status->setCustomMessage($predefinedMessage['message']);
489
			$status->setCustomIcon($predefinedMessage['icon']);
490
		}
491
	}
492
493
	/**
494
	 * @return bool false if there is already a backup. In this case abort the procedure.
495
	 */
496
	public function backupCurrentStatus(string $userId): bool {
497
		try {
498
			$this->mapper->createBackupStatus($userId);
499
			return true;
500
		} catch (Exception $ex) {
501
			if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
0 ignored issues
show
introduced by
The condition $ex->getReason() === OCP...UE_CONSTRAINT_VIOLATION is always false.
Loading history...
Bug introduced by
Are you sure the usage of $ex->getReason() targeting OCP\DB\Exception::getReason() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
502
				return false;
503
			}
504
			throw $ex;
505
		}
506
	}
507
508
	public function revertUserStatus(string $userId, string $messageId, string $status): void {
509
		try {
510
			/** @var UserStatus $userStatus */
511
			$backupUserStatus = $this->mapper->findByUserId($userId, true);
512
		} catch (DoesNotExistException $ex) {
513
			// No user status to revert, do nothing
514
			return;
515
		}
516
517
		$deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status);
518
		if (!$deleted) {
519
			// Another status is set automatically or no status, do nothing
520
			return;
521
		}
522
523
		$backupUserStatus->setIsBackup(false);
524
		// Remove the underscore prefix added when creating the backup
525
		$backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
526
		$this->mapper->update($backupUserStatus);
527
	}
528
529
	public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
530
		// Get all user statuses and the backups
531
		$findById = $userIds;
532
		foreach ($userIds as $userId) {
533
			$findById[] = '_' . $userId;
534
		}
535
		$userStatuses = $this->mapper->findByUserIds($findById);
536
537
		$backups = $restoreIds = $statuesToDelete = [];
538
		foreach ($userStatuses as $userStatus) {
539
			if (!$userStatus->getIsBackup()
540
				&& $userStatus->getMessageId() === $messageId
541
				&& $userStatus->getStatus() === $status) {
542
				$statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
543
			} else if ($userStatus->getIsBackup()) {
544
				$backups[$userStatus->getUserId()] = $userStatus->getId();
545
			}
546
		}
547
548
		// For users with both (normal and backup) delete the status when matching
549
		foreach ($statuesToDelete as $userId => $statusId) {
550
			$backupUserId = '_' . $userId;
551
			if (isset($backups[$backupUserId])) {
552
				$restoreIds[] = $backups[$backupUserId];
553
			}
554
		}
555
556
		$this->mapper->deleteByIds(array_values($statuesToDelete));
557
558
		// For users that matched restore the previous status
559
		$this->mapper->restoreBackupStatuses($restoreIds);
560
	}
561
}
562