Stop reporting the pseudo options category as missing phrase
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / reaction / ReactionHandler.class.php
CommitLineData
2e926c5e 1<?php
a9229942 2
2e926c5e 3namespace wcf\system\reaction;
a9229942 4
2e926c5e 5use wcf\data\like\ILikeObjectTypeProvider;
a9229942 6use wcf\data\like\Like;
be505648 7use wcf\data\like\LikeList;
2e926c5e
JR
8use wcf\data\like\object\ILikeObject;
9use wcf\data\like\object\LikeObject;
e8ee27a1 10use wcf\data\like\object\LikeObjectAction;
be505648 11use wcf\data\like\object\LikeObjectList;
2e926c5e
JR
12use wcf\data\object\type\ObjectType;
13use wcf\data\object\type\ObjectTypeCache;
42ce3082 14use wcf\data\reaction\object\IReactionObject;
e8ee27a1 15use wcf\data\reaction\ReactionAction;
2e926c5e
JR
16use wcf\data\reaction\type\ReactionType;
17use wcf\data\reaction\type\ReactionTypeCache;
18use wcf\data\user\User;
19use wcf\data\user\UserEditor;
20use wcf\system\cache\runtime\UserRuntimeCache;
21use wcf\system\database\exception\DatabaseQueryException;
22use wcf\system\database\util\PreparedStatementConditionBuilder;
23use wcf\system\event\EventHandler;
24use wcf\system\exception\ImplementationException;
a9229942 25use wcf\system\SingletonFactory;
98e163c9 26use wcf\system\user\activity\event\UserActivityEventHandler;
2e926c5e 27use wcf\system\user\activity\point\UserActivityPointHandler;
be505648 28use wcf\system\user\notification\UserNotificationHandler;
2e926c5e
JR
29use wcf\system\WCF;
30use wcf\util\JSON;
a87abbb2 31use wcf\util\StringUtil;
2e926c5e
JR
32
33/**
34 * Handles the reactions of objects.
35 *
a9229942
TD
36 * @author Joshua Ruesweg
37 * @copyright 2001-2019 WoltLab GmbH
38 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
a9229942 39 * @since 5.2
2e926c5e 40 */
a9229942
TD
41class ReactionHandler extends SingletonFactory
42{
43 /**
44 * loaded like objects
45 * @var LikeObject[][]
46 */
47 protected $likeObjectCache = [];
48
49 /**
50 * cached object types
51 * @var ObjectType[]
52 */
53 protected $cache;
54
55 /**
56 * Cache for likeable objects sorted by objectType.
57 * @var ILikeObject[][]
58 */
59 private $likeableObjectsCache = [];
60
61 /**
62 * Creates a new ReactionHandler instance.
63 */
64 protected function init()
65 {
66 $this->cache = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.like.likeableObject');
67 }
68
69 /**
70 * Returns the JSON encoded JavaScript variable for the template.
71 *
72 * @return string
73 */
74 public function getReactionsJSVariable()
75 {
76 $reactions = ReactionTypeCache::getInstance()->getReactionTypes();
77
78 $returnValues = [];
79
80 foreach ($reactions as $reaction) {
81 $returnValues[$reaction->reactionTypeID] = [
82 'title' => $reaction->getTitle(),
83 'renderedIcon' => $reaction->renderIcon(),
84 'iconPath' => $reaction->getIconPath(),
85 'showOrder' => $reaction->showOrder,
86 'reactionTypeID' => $reaction->reactionTypeID,
87 'isAssignable' => $reaction->isAssignable,
88 ];
89 }
90
91 return JSON::encode($returnValues);
92 }
93
94 /**
95 * Returns all enabled reaction types.
96 *
97 * @return ReactionType[]
98 */
99 public function getReactionTypes()
100 {
101 return ReactionTypeCache::getInstance()->getReactionTypes();
102 }
103
104 /**
105 * Returns a reaction type by id.
106 *
107 * @param int $reactionID
108 * @return ReactionType|null
109 */
110 public function getReactionTypeByID($reactionID)
111 {
112 return ReactionTypeCache::getInstance()->getReactionTypeByID($reactionID);
113 }
114
115 /**
116 * Builds the data attributes for the object container.
117 *
118 * @param string $objectTypeName
119 * @param int $objectID
120 * @return string
121 */
122 public function getDataAttributes($objectTypeName, $objectID)
123 {
124 $object = $this->getLikeableObject($objectTypeName, $objectID);
125
126 $dataAttributes = [
127 'object-id' => $object->getObjectID(),
128 'object-type' => $objectTypeName,
129 'user-id' => $object->getUserID(),
130 ];
131
132 EventHandler::getInstance()->fireAction($this, 'getDataAttributes', $dataAttributes);
133
134 $returnDataAttributes = '';
135
136 foreach ($dataAttributes as $key => $value) {
137 if (!\preg_match('/^[a-z0-9-]+$/', $key)) {
138 throw new \RuntimeException("Invalid key '" . $key . "' for data attribute.");
139 }
140
141 if (!empty($returnDataAttributes)) {
142 $returnDataAttributes .= ' ';
143 }
144
145 $returnDataAttributes .= 'data-' . $key . '="' . StringUtil::encodeHTML($value) . '"';
146 }
147
148 return $returnDataAttributes;
149 }
150
151 /**
152 * Cache likeable objects.
153 *
154 * @param string $objectTypeName
155 * @param int[] $objectIDs
156 */
157 public function cacheLikeableObjects($objectTypeName, array $objectIDs)
158 {
159 $objectType = $this->getObjectType($objectTypeName);
160 if ($objectType === null) {
161 throw new \InvalidArgumentException(
162 "ObjectName '{$objectTypeName}' is unknown for definition 'com.woltlab.wcf.like.likeableObject'."
163 );
164 }
165
166 /** @var ILikeObjectTypeProvider $objectTypeProcessor */
167 $objectTypeProcessor = $objectType->getProcessor();
168
169 $objects = $objectTypeProcessor->getObjectsByIDs($objectIDs);
170
171 if (!isset($this->likeableObjectsCache[$objectTypeName])) {
172 $this->likeableObjectsCache[$objectTypeName] = [];
173 }
174
175 foreach ($objects as $object) {
176 $this->likeableObjectsCache[$objectTypeName][$object->getObjectID()] = $object;
177 }
178 }
179
180 /**
181 * Get an likeable object from the internal cache.
182 *
183 * @param string $objectTypeName
184 * @param int $objectID
185 * @return ILikeObject
186 */
187 public function getLikeableObject($objectTypeName, $objectID)
188 {
189 if (!isset($this->likeableObjectsCache[$objectTypeName][$objectID])) {
190 $this->cacheLikeableObjects($objectTypeName, [$objectID]);
191 }
192
193 if (!isset($this->likeableObjectsCache[$objectTypeName][$objectID])) {
194 throw new \InvalidArgumentException(
195 "Object with the object id '{$objectID}' for object type '{$objectTypeName}' is unknown."
196 );
197 }
198
199 if (!($this->likeableObjectsCache[$objectTypeName][$objectID] instanceof ILikeObject)) {
200 throw new ImplementationException(
201 \get_class($this->likeableObjectsCache[$objectTypeName][$objectID]),
202 ILikeObject::class
203 );
204 }
205
206 return $this->likeableObjectsCache[$objectTypeName][$objectID];
207 }
208
209 /**
210 * Returns an object type from cache.
211 *
212 * @param string $objectName
213 * @return ObjectType|null
214 */
215 public function getObjectType($objectName)
216 {
813c41ce 217 return $this->cache[$objectName] ?? null;
a9229942
TD
218 }
219
220 /**
221 * Returns a like object.
222 *
223 * @param ObjectType $objectType
224 * @param int $objectID
225 * @return LikeObject|null
226 */
227 public function getLikeObject(ObjectType $objectType, $objectID)
228 {
229 if (!isset($this->likeObjectCache[$objectType->objectTypeID][$objectID])) {
230 $this->loadLikeObjects($objectType, [$objectID]);
231 }
232
233 return $this->likeObjectCache[$objectType->objectTypeID][$objectID] ?? null;
234 }
235
236 /**
237 * Returns the like objects of a specific object type.
238 *
239 * @param ObjectType $objectType
240 * @return LikeObject[]
241 */
242 public function getLikeObjects(ObjectType $objectType)
243 {
244 if (isset($this->likeObjectCache[$objectType->objectTypeID])) {
245 return $this->likeObjectCache[$objectType->objectTypeID];
246 }
247
248 return [];
249 }
250
251 /**
252 * Loads the like data for a set of objects and returns the number of loaded
253 * like objects
254 *
255 * @param ObjectType $objectType
256 * @param array $objectIDs
257 * @return int
258 */
259 public function loadLikeObjects(ObjectType $objectType, array $objectIDs)
260 {
261 if (empty($objectIDs)) {
262 return 0;
263 }
264
265 $this->cacheLikeableObjects($objectType->objectType, $objectIDs);
266
267 $i = 0;
268
269 $conditions = new PreparedStatementConditionBuilder();
270 $conditions->add("like_object.objectTypeID = ?", [$objectType->objectTypeID]);
271 $conditions->add("like_object.objectID IN (?)", [$objectIDs]);
272 $parameters = $conditions->getParameters();
273
274 if (WCF::getUser()->userID) {
275 $sql = "SELECT like_object.*,
276 COALESCE(like_table.reactionTypeID, 0) AS reactionTypeID,
277 COALESCE(like_table.likeValue, 0) AS liked
278 FROM wcf" . WCF_N . "_like_object like_object
279 LEFT JOIN wcf" . WCF_N . "_like like_table
c240c98a
MS
280 ON like_table.objectTypeID = like_object.objectTypeID
281 AND like_table.objectID = like_object.objectID
282 AND like_table.userID = ?
a9229942
TD
283 " . $conditions;
284
285 \array_unshift($parameters, WCF::getUser()->userID);
286 } else {
287 $sql = "SELECT like_object.*, 0 AS liked
288 FROM wcf" . WCF_N . "_like_object like_object
289 " . $conditions;
290 }
291
292 $statement = WCF::getDB()->prepareStatement($sql);
293 $statement->execute($parameters);
294 while ($row = $statement->fetchArray()) {
295 $this->likeObjectCache[$objectType->objectTypeID][$row['objectID']] = new LikeObject(null, $row);
296 $i++;
297 }
298
299 return $i;
300 }
301
302 /**
303 * Add a reaction to an object.
304 *
305 * @param ILikeObject $likeable
306 * @param User $user
307 * @param int $reactionTypeID
308 * @param int $time
309 * @return array
310 * @throws DatabaseQueryException
311 */
312 public function react(ILikeObject $likeable, User $user, $reactionTypeID, $time = TIME_NOW)
313 {
314 // verify if object is already liked by user
315 $like = Like::getLike($likeable->getObjectType()->objectTypeID, $likeable->getObjectID(), $user->userID);
316
317 // get like object
318 $likeObject = LikeObject::getLikeObject($likeable->getObjectType()->objectTypeID, $likeable->getObjectID());
319
320 // if vote is identically just revert the vote
321 if ($like->likeID && ($like->reactionTypeID == $reactionTypeID)) {
322 return $this->revertReact($like, $likeable, $likeObject, $user);
323 }
324
325 $reaction = ReactionTypeCache::getInstance()->getReactionTypeByID($reactionTypeID);
326
327 try {
328 WCF::getDB()->beginTransaction();
329
330 $likeObjectData = $this->updateLikeObject($likeable, $likeObject, $like, $reaction);
331
332 // update owner's like counter
333 $this->updateUsersLikeCounter($likeable, $likeObject, $like, $reaction);
334
335 if (!$like->likeID) {
336 // save like
337 $returnValues = (new ReactionAction([], 'create', [
338 'data' => [
339 'objectID' => $likeable->getObjectID(),
340 'objectTypeID' => $likeable->getObjectType()->objectTypeID,
341 'objectUserID' => $likeable->getUserID() ?: null,
342 'userID' => $user->userID,
343 'time' => $time,
344 'likeValue' => 1,
345 'reactionTypeID' => $reactionTypeID,
346 ],
347 ]))->executeAction();
348
349 $like = $returnValues['returnValues'];
350
351 if ($likeable->getUserID()) {
352 UserActivityPointHandler::getInstance()->fireEvent(
353 'com.woltlab.wcf.like.activityPointEvent.receivedLikes',
354 $like->likeID,
355 $likeable->getUserID()
356 );
357 }
358 } else {
359 (new ReactionAction([$like], 'update', [
360 'data' => [
361 'time' => $time,
362 'likeValue' => 1,
363 'reactionTypeID' => $reactionTypeID,
364 ],
365 ]))->executeAction();
366
35b86518 367 if ($like->reactionTypeID == $reactionTypeID) {
81e95594
MW
368 if ($likeable->getUserID()) {
369 UserActivityPointHandler::getInstance()->removeEvents(
370 'com.woltlab.wcf.like.activityPointEvent.receivedLikes',
371 [$likeable->getUserID() => 1]
372 );
373 }
a9229942
TD
374 }
375 }
376
377 // This interface should help to determine whether the plugin has been adapted to the API 5.2.
378 // If a LikeableObject does not implement this interface, no notification will be sent, because
379 // we assume, that the plugin has not been adapted to the new API.
380 if ($likeable instanceof IReactionObject) {
381 $likeable->sendNotification($like);
382 }
383
384 // update object's like counter
385 $likeable->updateLikeCounter($likeObjectData['cumulativeLikes']);
386
387 // update recent activity
388 if (UserActivityEventHandler::getInstance()->getObjectTypeID($likeable->getObjectType()->objectType . '.recentActivityEvent')) {
389 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
390 'com.woltlab.wcf.user.recentActivityEvent',
391 $likeable->getObjectType()->objectType . '.recentActivityEvent'
392 );
393
394 if ($objectType->supportsReactions) {
395 if ($like->likeID) {
396 UserActivityEventHandler::getInstance()->removeEvent(
397 $likeable->getObjectType()->objectType . '.recentActivityEvent',
398 $likeable->getObjectID(),
399 $user->userID
400 );
401 }
402
403 UserActivityEventHandler::getInstance()->fireEvent(
404 $likeable->getObjectType()->objectType . '.recentActivityEvent',
405 $likeable->getObjectID(),
406 $likeable->getLanguageID(),
407 $user->userID,
408 TIME_NOW,
690741e5
C
409 [
410 'reactionTypeID' => $reaction->reactionTypeID,
411 /* @deprecated 6.1 use `reactionTypeID` */
412 'reactionType' => $reaction,
413 ]
a9229942
TD
414 );
415 }
416 }
417
418 WCF::getDB()->commitTransaction();
419
420 return [
421 'cachedReactions' => $likeObjectData['cachedReactions'],
422 'reactionTypeID' => $reactionTypeID,
423 'like' => $like,
424 'likeObject' => $likeObjectData['likeObject'],
425 'cumulativeLikes' => $likeObjectData['cumulativeLikes'],
426 ];
427 } catch (DatabaseQueryException $e) {
428 WCF::getDB()->rollBackTransaction();
429
430 throw $e;
431 }
432
433 /** @noinspection PhpUnreachableStatementInspection */
434 throw new \LogicException('Unreachable');
435 }
436
437 /**
438 * Creates or updates a LikeObject for an likable object.
439 *
440 * @param ILikeObject $likeable
441 * @param LikeObject $likeObject
442 * @param Like $like
443 * @param ReactionType $reactionType
444 * @return array
445 */
446 private function updateLikeObject(
447 ILikeObject $likeable,
448 LikeObject $likeObject,
449 Like $like,
450 ReactionType $reactionType
451 ) {
452 // update existing object
453 if ($likeObject->likeObjectID) {
454 $cumulativeLikes = $likeObject->cumulativeLikes;
455
456 if ($likeObject->cachedReactions !== null) {
457 $cachedReactions = @\unserialize($likeObject->cachedReactions);
458 } else {
459 $cachedReactions = [];
460 }
461
462 if (!\is_array($cachedReactions)) {
463 $cachedReactions = [];
464 }
465
466 if ($like->likeID) {
467 $cumulativeLikes--;
468
469 if (isset($cachedReactions[$like->getReactionType()->reactionTypeID])) {
470 if (--$cachedReactions[$like->getReactionType()->reactionTypeID] == 0) {
471 unset($cachedReactions[$like->getReactionType()->reactionTypeID]);
472 }
473 }
474 }
475
476 $cumulativeLikes++;
477
478 if (isset($cachedReactions[$reactionType->reactionTypeID])) {
479 $cachedReactions[$reactionType->reactionTypeID]++;
480 } else {
481 $cachedReactions[$reactionType->reactionTypeID] = 1;
482 }
483
484 $cachedReactions = self::cleanUpCachedReactions($cachedReactions);
485
486 // build update date
487 $updateData = [
488 'likes' => $cumulativeLikes,
489 'dislikes' => 0,
490 'cumulativeLikes' => $cumulativeLikes,
491 'cachedReactions' => \serialize($cachedReactions),
492 ];
493
494 // update data
495 (new LikeObjectAction([$likeObject], 'update', ['data' => $updateData]))->executeAction();
496 } else {
497 $cumulativeLikes = 1;
498 $cachedReactions = [
499 $reactionType->reactionTypeID => 1,
500 ];
501
502 // create cache
503 $likeObjectActionReturnValues = (new LikeObjectAction([], 'create', [
504 'data' => [
505 'objectTypeID' => $likeable->getObjectType()->objectTypeID,
506 'objectID' => $likeable->getObjectID(),
507 'objectUserID' => $likeable->getUserID() ?: null,
508 'likes' => $cumulativeLikes,
509 'dislikes' => 0,
510 'cumulativeLikes' => $cumulativeLikes,
511 'cachedReactions' => \serialize($cachedReactions),
512 ],
513 ]))->executeAction();
514 $likeObject = $likeObjectActionReturnValues['returnValues'];
515 }
516
517 return [
518 'cumulativeLikes' => $cumulativeLikes,
519 'cachedReactions' => $cachedReactions,
520 'likeObject' => $likeObject,
521 ];
522 }
523
524 /**
525 * Updates the like counter for a user.
526 *
527 * @param ILikeObject $likeable
528 * @param LikeObject $likeObject
529 * @param Like $like
530 * @param ReactionType $reactionType
531 */
532 private function updateUsersLikeCounter(
533 ILikeObject $likeable,
534 LikeObject $likeObject,
535 Like $like,
536 ?ReactionType $reactionType = null
537 ) {
538 if ($likeable->getUserID()) {
539 $likesReceived = 0;
540 if ($like->likeID) {
541 $likesReceived--;
542 }
543
544 if ($reactionType !== null) {
545 $likesReceived++;
546 }
547
548 if ($likesReceived !== 0) {
549 $userEditor = new UserEditor(UserRuntimeCache::getInstance()->getObject($likeable->getUserID()));
550 $userEditor->updateCounters(['likesReceived' => $likesReceived]);
551 }
552 }
553 }
554
555 /**
556 * Reverts a reaction for an object.
557 *
558 * @param Like $like
559 * @param ILikeObject $likeable
560 * @param LikeObject $likeObject
561 * @param User $user
562 * @return array
563 */
564 public function revertReact(Like $like, ILikeObject $likeable, LikeObject $likeObject, User $user)
565 {
566 if (!$like->likeID) {
567 throw new \InvalidArgumentException('The given parameter $like is invalid.');
568 }
569
570 try {
571 WCF::getDB()->beginTransaction();
572
573 $likeObjectData = $this->revertLikeObject($likeObject, $like);
574
575 // update owner's like counter
576 $this->updateUsersLikeCounter($likeable, $likeObject, $like, null);
577
578 (new ReactionAction([$like], 'delete'))->executeAction();
579
580 if ($likeable->getUserID()) {
581 UserActivityPointHandler::getInstance()->removeEvents(
582 'com.woltlab.wcf.like.activityPointEvent.receivedLikes',
583 [$likeable->getUserID() => 1]
584 );
585 }
586
587 // update object's like counter
588 $likeable->updateLikeCounter($likeObjectData['cumulativeLikes']);
589
590 // delete recent activity
591 if (UserActivityEventHandler::getInstance()->getObjectTypeID($likeable->getObjectType()->objectType . '.recentActivityEvent')) {
592 UserActivityEventHandler::getInstance()->removeEvent(
593 $likeable->getObjectType()->objectType . '.recentActivityEvent',
594 $likeable->getObjectID(),
595 $user->userID
596 );
597 }
598
599 WCF::getDB()->commitTransaction();
600
601 return [
602 'cachedReactions' => $likeObjectData['cachedReactions'],
603 'reactionTypeID' => null,
604 'likeObject' => $likeObjectData['likeObject'],
605 'cumulativeLikes' => $likeObjectData['cumulativeLikes'],
606 ];
607 } catch (DatabaseQueryException $e) {
608 WCF::getDB()->rollBackTransaction();
609 }
610
611 return [
612 'cachedReactions' => [],
613 'reactionTypeID' => null,
614 'likeObject' => [],
615 'cumulativeLikes' => null,
616 ];
617 }
618
619 /**
620 * Creates or updates a LikeObject for an likable object.
621 *
622 * @param LikeObject $likeObject
623 * @param Like $like
624 * @return array
625 */
626 private function revertLikeObject(LikeObject $likeObject, Like $like)
627 {
628 if (!$likeObject->likeObjectID) {
629 throw new \InvalidArgumentException('The given parameter $likeObject is invalid.');
630 }
631
632 // update existing object
633 $cumulativeLikes = $likeObject->cumulativeLikes;
634 $cachedReactions = @\unserialize($likeObject->cachedReactions);
635 if (!\is_array($cachedReactions)) {
636 $cachedReactions = [];
637 }
638
639 if ($like->likeID) {
640 $cumulativeLikes--;
641
642 if (isset($cachedReactions[$like->getReactionType()->reactionTypeID])) {
643 if (--$cachedReactions[$like->getReactionType()->reactionTypeID] == 0) {
644 unset($cachedReactions[$like->getReactionType()->reactionTypeID]);
645 }
646 }
647
648 $cachedReactions = self::cleanUpCachedReactions($cachedReactions);
649
650 // build update date
651 $updateData = [
652 'likes' => $cumulativeLikes,
653 'dislikes' => 0,
654 'cumulativeLikes' => $cumulativeLikes,
655 'cachedReactions' => \serialize($cachedReactions),
656 ];
657
658 // update data
659 (new LikeObjectAction([$likeObject], 'update', ['data' => $updateData]))->executeAction();
660 }
661
662 return [
663 'cumulativeLikes' => $cumulativeLikes,
664 'cachedReactions' => $cachedReactions,
665 'likeObject' => $likeObject,
666 ];
667 }
668
669 /**
670 * Removes all reactions for given objects.
671 *
672 * @param string $objectType
673 * @param int[] $objectIDs
674 * @param string[] $notificationObjectTypes
675 */
676 public function removeReactions($objectType, array $objectIDs, array $notificationObjectTypes = [])
677 {
678 $objectTypeObj = $this->getObjectType($objectType);
679
680 if ($objectTypeObj === null) {
681 throw new \InvalidArgumentException('Given objectType is invalid.');
682 }
683
684 // get like objects
685 $likeObjectList = new LikeObjectList();
686 $likeObjectList->getConditionBuilder()->add('like_object.objectTypeID = ?', [$objectTypeObj->objectTypeID]);
687 $likeObjectList->getConditionBuilder()->add('like_object.objectID IN (?)', [$objectIDs]);
688 $likeObjectList->readObjects();
689 $likeObjects = $likeObjectList->getObjects();
690 $likeObjectIDs = $likeObjectList->getObjectIDs();
691
692 // reduce count of received users
693 $users = [];
694 foreach ($likeObjects as $likeObject) {
6eab4137 695 if ($likeObject->likes && $likeObject->objectUserID) {
a9229942
TD
696 if (!isset($users[$likeObject->objectUserID])) {
697 $users[$likeObject->objectUserID] = 0;
698 }
699
700 $users[$likeObject->objectUserID] -= \count($likeObject->getReactions());
701 }
702 }
703
704 foreach ($users as $userID => $reactionData) {
705 $userEditor = new UserEditor(new User(null, ['userID' => $userID]));
706 $userEditor->updateCounters([
6eab4137 707 'likesReceived' => $reactionData,
a9229942
TD
708 ]);
709 }
710
711 // get like ids
712 $likeList = new LikeList();
713 $likeList->getConditionBuilder()->add('like_table.objectTypeID = ?', [$objectTypeObj->objectTypeID]);
714 $likeList->getConditionBuilder()->add('like_table.objectID IN (?)', [$objectIDs]);
715 $likeList->readObjects();
716
717 if (\count($likeList)) {
718 $activityPoints = $likeData = [];
719 foreach ($likeList as $like) {
720 $likeData[$like->likeID] = $like->userID;
721
6eab4137 722 if ($like->objectUserID) {
723 if (!isset($activityPoints[$like->objectUserID])) {
724 $activityPoints[$like->objectUserID] = 0;
725 }
726 $activityPoints[$like->objectUserID]++;
a9229942 727 }
a9229942
TD
728 }
729
730 // delete like notifications
731 if (!empty($notificationObjectTypes)) {
732 foreach ($notificationObjectTypes as $notificationObjectType) {
733 UserNotificationHandler::getInstance()
734 ->removeNotifications($notificationObjectType, $likeList->getObjectIDs());
735 }
736 } elseif (UserNotificationHandler::getInstance()->getObjectTypeID($objectType . '.notification')) {
737 UserNotificationHandler::getInstance()
738 ->removeNotifications($objectType . '.notification', $likeList->getObjectIDs());
739 }
740
741 // revoke activity points
742 UserActivityPointHandler::getInstance()
743 ->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $activityPoints);
744
745 // delete likes
746 (new ReactionAction(\array_keys($likeData), 'delete'))->executeAction();
747 }
748
749 // delete like objects
750 if (!empty($likeObjectIDs)) {
751 (new LikeObjectAction($likeObjectIDs, 'delete'))->executeAction();
752 }
753
754 // delete activity events
755 if (UserActivityEventHandler::getInstance()->getObjectTypeID($objectTypeObj->objectType . '.recentActivityEvent')) {
756 UserActivityEventHandler::getInstance()
757 ->removeEvents($objectTypeObj->objectType . '.recentActivityEvent', $objectIDs);
758 }
759 }
760
761 /**
762 * Returns current like object status.
763 *
764 * @param LikeObject $likeObject
765 * @param User $user
766 * @return array
767 */
768 protected function loadLikeStatus(LikeObject $likeObject, User $user)
769 {
770 $sql = "SELECT like_object.likes, like_object.dislikes, like_object.cumulativeLikes,
771 COALESCE(like_table.reactionTypeID, 0) AS reactionTypeID,
772 COALESCE(like_table.likeValue, 0) AS liked
773 FROM wcf" . WCF_N . "_like_object like_object
774 LEFT JOIN wcf" . WCF_N . "_like like_table
c240c98a
MS
775 ON like_table.objectTypeID = ?
776 AND like_table.objectID = like_object.objectID
777 AND like_table.userID = ?
a9229942
TD
778 WHERE like_object.likeObjectID = ?";
779 $statement = WCF::getDB()->prepareStatement($sql);
780 $statement->execute([
781 $likeObject->objectTypeID,
782 $user->userID,
783 $likeObject->likeObjectID,
784 ]);
785
786 return $statement->fetchArray();
787 }
788
789 /**
790 * Returns the first available reaction type.
791 *
792 * @return ReactionType|null
793 */
794 public function getFirstReactionType()
795 {
796 static $firstReactionType;
797
798 if ($firstReactionType === null) {
799 $reactionTypes = ReactionTypeCache::getInstance()->getReactionTypes();
800 ReactionType::sort($reactionTypes, 'showOrder');
801
802 $firstReactionType = \reset($reactionTypes);
803 }
804
805 return $firstReactionType;
806 }
807
808 /**
809 * Returns the first available reaction type's id.
810 *
811 * @return int|null
812 */
813 public function getFirstReactionTypeID()
814 {
815 $firstReactionType = $this->getFirstReactionType();
816
817 return $firstReactionType ? $firstReactionType->reactionTypeID : null;
818 }
819
820 /**
821 * Removes deleted reactions from the reaction counter for the like object table.
822 *
823 * @param array $cachedReactions
824 * @return array
825 */
826 private function cleanUpCachedReactions(array $cachedReactions)
827 {
828 foreach ($cachedReactions as $reactionTypeID => $count) {
829 if (self::getReactionTypeByID($reactionTypeID) === null) {
830 unset($cachedReactions[$reactionTypeID]);
831 }
832 }
833
834 return $cachedReactions;
835 }
836
837 /**
838 * @param string|null $cachedReactions
839 * @return array|null
840 * @since 5.2
841 */
842 public function getTopReaction($cachedReactions)
843 {
844 if ($cachedReactions) {
845 $cachedReactions = @\unserialize($cachedReactions);
846
847 if (\is_array($cachedReactions)) {
848 $cachedReactions = self::cleanUpCachedReactions($cachedReactions);
849
850 if (!empty($cachedReactions)) {
851 $allReactions = \array_sum($cachedReactions);
852
853 \arsort($cachedReactions, \SORT_NUMERIC);
854
855 $count = \current($cachedReactions);
856
857 return [
858 'count' => $count,
859 'other' => $allReactions - $count,
860 'reaction' => ReactionTypeCache::getInstance()->getReactionTypeByID(\key($cachedReactions)),
861 ];
862 }
863 }
864 }
5227ebc7
MS
865
866 return null;
a9229942
TD
867 }
868
869 /**
870 * Renders an inline list of reaction counts.
871 *
872 * @param int[] $reactionCounts format: `[reactionID => count]`
873 * @return string
874 * @since 5.3
875 */
876 public function renderInlineList(array $reactionCounts)
877 {
878 $reactionsOuput = [];
879 foreach ($reactionCounts as $reactionTypeID => $count) {
880 $reactionsOuput[] = WCF::getLanguage()->getDynamicVariable('wcf.reactions.reactionTypeCount', [
881 'count' => $count,
882 'reaction' => $this->getReactionTypeByID($reactionTypeID),
883 ]);
884 }
885
886 return \implode(', ', $reactionsOuput);
887 }
2e926c5e 888}