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