Merge remote-tracking branch 'origin/next' into reactions
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / like / LikeHandler.class.php
1 <?php
2 declare(strict_types=1);
3 namespace wcf\system\like;
4 use wcf\data\like\object\ILikeObject;
5 use wcf\data\like\object\LikeObject;
6 use wcf\data\like\object\LikeObjectEditor;
7 use wcf\data\like\object\LikeObjectList;
8 use wcf\data\like\Like;
9 use wcf\data\like\LikeEditor;
10 use wcf\data\like\LikeList;
11 use wcf\data\object\type\ObjectType;
12 use wcf\data\object\type\ObjectTypeCache;
13 use wcf\data\user\User;
14 use wcf\data\user\UserEditor;
15 use wcf\system\database\util\PreparedStatementConditionBuilder;
16 use wcf\system\database\DatabaseException;
17 use wcf\system\user\activity\event\UserActivityEventHandler;
18 use wcf\system\user\activity\point\UserActivityPointHandler;
19 use wcf\system\user\notification\UserNotificationHandler;
20 use wcf\system\SingletonFactory;
21 use wcf\system\WCF;
22
23 /**
24 * Handles the likes of liked objects.
25 *
26 * Usage (retrieve all likes for a list of objects):
27 * // get type object
28 * $objectType = LikeHandler::getInstance()->getObjectType('com.woltlab.wcf.foo.bar');
29 * // load like data
30 * LikeHandler::getInstance()->loadLikeObjects($objectType, $objectIDs);
31 * // get like data
32 * $likeObjects = LikeHandler::getInstance()->getLikeObjects($objectType);
33 *
34 * @author Marcel Werk
35 * @copyright 2001-2018 WoltLab GmbH
36 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
37 * @package WoltLabSuite\Core\System\Like
38 * @deprecated The LikeHandler is deprecated since 3.2 in favor of the \wcf\system\reaction\ReactionHandler
39 */
40 class LikeHandler 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
53 /**
54 * Creates a new LikeHandler instance.
55 */
56 protected function init() {
57 // load cache
58 $this->cache = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.like.likeableObject');
59 }
60
61 /**
62 * Returns an object type from cache.
63 *
64 * @param string $objectName
65 * @return ObjectType
66 */
67 public function getObjectType($objectName) {
68 if (isset($this->cache[$objectName])) {
69 return $this->cache[$objectName];
70 }
71
72 return null;
73 }
74
75 /**
76 * Returns a like object.
77 *
78 * @param ObjectType $objectType
79 * @param integer $objectID
80 * @return LikeObject|null
81 */
82 public function getLikeObject(ObjectType $objectType, $objectID) {
83 if (isset($this->likeObjectCache[$objectType->objectTypeID][$objectID])) {
84 return $this->likeObjectCache[$objectType->objectTypeID][$objectID];
85 }
86
87 return null;
88 }
89
90 /**
91 * Returns the like objects of a specific object type.
92 *
93 * @param ObjectType $objectType
94 * @return LikeObject[]
95 */
96 public function getLikeObjects(ObjectType $objectType) {
97 if (isset($this->likeObjectCache[$objectType->objectTypeID])) {
98 return $this->likeObjectCache[$objectType->objectTypeID];
99 }
100
101 return [];
102 }
103
104 /**
105 * Loads the like data for a set of objects and returns the number of loaded
106 * like objects
107 *
108 * @param ObjectType $objectType
109 * @param array $objectIDs
110 * @return integer
111 */
112 public function loadLikeObjects(ObjectType $objectType, array $objectIDs) {
113 if (empty($objectIDs)) {
114 return 0;
115 }
116
117 $i = 0;
118
119 $conditions = new PreparedStatementConditionBuilder();
120 $conditions->add("like_object.objectTypeID = ?", [$objectType->objectTypeID]);
121 $conditions->add("like_object.objectID IN (?)", [$objectIDs]);
122 $parameters = $conditions->getParameters();
123
124 if (WCF::getUser()->userID) {
125 $sql = "SELECT like_object.*,
126 CASE WHEN like_table.userID IS NOT NULL THEN like_table.likeValue ELSE 0 END AS liked
127 FROM wcf".WCF_N."_like_object like_object
128 LEFT JOIN wcf".WCF_N."_like like_table
129 ON (like_table.objectTypeID = like_object.objectTypeID
130 AND like_table.objectID = like_object.objectID
131 AND like_table.userID = ?)
132 ".$conditions;
133
134 array_unshift($parameters, WCF::getUser()->userID);
135 }
136 else {
137 $sql = "SELECT like_object.*, 0 AS liked
138 FROM wcf".WCF_N."_like_object like_object
139 ".$conditions;
140 }
141
142 $statement = WCF::getDB()->prepareStatement($sql);
143 $statement->execute($parameters);
144 while ($row = $statement->fetchArray()) {
145 $this->likeObjectCache[$objectType->objectTypeID][$row['objectID']] = new LikeObject(null, $row);
146 $i++;
147 }
148
149 return $i;
150 }
151
152 /**
153 * Saves the like of an object.
154 *
155 * @param ILikeObject $likeable
156 * @param User $user
157 * @param integer $likeValue
158 * @param integer $time
159 * @return array
160 */
161 public function like(ILikeObject $likeable, User $user, $likeValue, $time = TIME_NOW) {
162 // verify if object is already liked by user
163 $like = Like::getLike($likeable->getObjectType()->objectTypeID, $likeable->getObjectID(), $user->userID);
164
165 // get like object
166 $likeObject = LikeObject::getLikeObject($likeable->getObjectType()->objectTypeID, $likeable->getObjectID());
167
168 // if vote is identically just revert the vote
169 if ($like->likeID && ($like->likeValue == $likeValue)) {
170 return $this->revertLike($like, $likeable, $likeObject, $user);
171 }
172
173 // like data
174 /** @noinspection PhpUnusedLocalVariableInspection */
175 $cumulativeLikes = 0;
176 /** @noinspection PhpUnusedLocalVariableInspection */
177 $newValue = $oldValue = null;
178 $users = [];
179
180 try {
181 WCF::getDB()->beginTransaction();
182
183 // update existing object
184 if ($likeObject->likeObjectID) {
185 $likes = $likeObject->likes;
186 $dislikes = $likeObject->dislikes;
187 $cumulativeLikes = $likeObject->cumulativeLikes;
188
189 // previous (dis-)like already exists
190 if ($like->likeID) {
191 $oldValue = $like->likeValue;
192
193 // revert like and replace it with a dislike
194 if ($like->likeValue == Like::LIKE) {
195 $likes--;
196 $dislikes++;
197 $cumulativeLikes -= 2;
198 $newValue = Like::DISLIKE;
199 }
200 else {
201 // revert dislike and replace it with a like
202 $likes++;
203 $dislikes--;
204 $cumulativeLikes += 2;
205 $newValue = Like::LIKE;
206 }
207 }
208 else {
209 if ($likeValue == Like::LIKE) {
210 $likes++;
211 $cumulativeLikes++;
212 $newValue = Like::LIKE;
213 }
214 else {
215 $dislikes++;
216 $cumulativeLikes--;
217 $newValue = Like::DISLIKE;
218 }
219 }
220
221 // build update date
222 $updateData = [
223 'likes' => $likes,
224 'dislikes' => $dislikes,
225 'cumulativeLikes' => $cumulativeLikes
226 ];
227
228 if ($likeValue == 1) {
229 $users = unserialize($likeObject->cachedUsers);
230 if (count($users) < 3) {
231 $users[$user->userID] = ['userID' => $user->userID, 'username' => $user->username];
232 $updateData['cachedUsers'] = serialize($users);
233 }
234 }
235 else if ($likeValue == -1) {
236 $users = unserialize($likeObject->cachedUsers);
237 if (isset($users[$user->userID])) {
238 unset($users[$user->userID]);
239 $updateData['cachedUsers'] = serialize($users);
240 }
241 }
242
243 // update data
244 $likeObjectEditor = new LikeObjectEditor($likeObject);
245 $likeObjectEditor->update($updateData);
246 }
247 else {
248 $cumulativeLikes = $likeValue;
249 $newValue = $likeValue;
250 $users = [];
251 if ($likeValue == 1) $users = [$user->userID => ['userID' => $user->userID, 'username' => $user->username]];
252
253 // create cache
254 $likeObject = LikeObjectEditor::create([
255 'objectTypeID' => $likeable->getObjectType()->objectTypeID,
256 'objectID' => $likeable->getObjectID(),
257 'objectUserID' => $likeable->getUserID() ?: null,
258 'likes' => ($likeValue == Like::LIKE) ? 1 : 0,
259 'dislikes' => ($likeValue == Like::DISLIKE) ? 1 : 0,
260 'cumulativeLikes' => $cumulativeLikes,
261 'cachedUsers' => serialize($users)
262 ]);
263 }
264
265 // update owner's like counter
266 if ($likeable->getUserID()) {
267 if ($like->likeID) {
268 $userEditor = new UserEditor(new User($likeable->getUserID()));
269 $userEditor->updateCounters([
270 'likesReceived' => $like->likeValue == Like::LIKE ? -1 : 1
271 ]);
272 }
273 else if ($likeValue == Like::LIKE) {
274 $userEditor = new UserEditor(new User($likeable->getUserID()));
275 $userEditor->updateCounters([
276 'likesReceived' => 1
277 ]);
278 }
279 }
280
281 if (!$like->likeID) {
282 // save like
283 $like = LikeEditor::create([
284 'objectID' => $likeable->getObjectID(),
285 'objectTypeID' => $likeable->getObjectType()->objectTypeID,
286 'objectUserID' => $likeable->getUserID() ?: null,
287 'userID' => $user->userID,
288 'time' => $time,
289 'likeValue' => $likeValue
290 ]);
291
292 if ($likeValue == Like::LIKE && $likeable->getUserID()) {
293 UserActivityPointHandler::getInstance()->fireEvent('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $like->likeID, $likeable->getUserID());
294 $likeable->sendNotification($like);
295 }
296 }
297 else {
298 $likeEditor = new LikeEditor($like);
299 $likeEditor->update([
300 'time' => $time,
301 'likeValue' => $likeValue
302 ]);
303
304 if ($likeable->getUserID()) {
305 if ($likeValue == Like::DISLIKE) {
306 UserActivityPointHandler::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', [$likeable->getUserID() => 1]);
307 }
308 else {
309 UserActivityPointHandler::getInstance()->fireEvent('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $like->likeID, $likeable->getUserID());
310 $likeable->sendNotification($like);
311 }
312 }
313 }
314
315 // update object's like counter
316 $likeable->updateLikeCounter($cumulativeLikes);
317
318 WCF::getDB()->commitTransaction();
319 }
320 catch (DatabaseException $e) {
321 WCF::getDB()->rollBackTransaction();
322 }
323
324 return [
325 'data' => $this->loadLikeStatus($likeObject, $user),
326 'like' => $like,
327 'newValue' => $newValue,
328 'oldValue' => $oldValue,
329 'users' => $users
330 ];
331 }
332
333 /**
334 * Reverts the like of an object.
335 *
336 * @param Like $like
337 * @param ILikeObject $likeable
338 * @param LikeObject $likeObject
339 * @param User $user
340 * @return array
341 */
342 public function revertLike(Like $like, ILikeObject $likeable, LikeObject $likeObject, User $user) {
343 $usersArray = [];
344
345 try {
346 WCF::getDB()->beginTransaction();
347
348 // delete like
349 $editor = new LikeEditor($like);
350 $editor->delete();
351
352 // update like object cache
353 $likes = $likeObject->likes;
354 $dislikes = $likeObject->dislikes;
355 $cumulativeLikes = $likeObject->cumulativeLikes;
356
357 if ($like->likeValue == Like::LIKE) {
358 $likes--;
359 $cumulativeLikes--;
360 }
361 else {
362 $dislikes--;
363 $cumulativeLikes++;
364 }
365
366 // build update data
367 $updateData = [
368 'likes' => $likes,
369 'dislikes' => $dislikes,
370 'cumulativeLikes' => $cumulativeLikes
371 ];
372
373 $users = $likeObject->getUsers();
374 foreach ($users as $user2) {
375 $usersArray[$user2->userID] = ['userID' => $user2->userID, 'username' => $user2->username];
376 }
377
378 if (isset($usersArray[$user->userID])) {
379 unset($usersArray[$user->userID]);
380 $updateData['cachedUsers'] = serialize($usersArray);
381 }
382
383 $likeObjectEditor = new LikeObjectEditor($likeObject);
384 if (!$updateData['likes'] && !$updateData['dislikes']) {
385 // remove object instead
386 $likeObjectEditor->delete();
387 }
388 else {
389 // update data
390 $likeObjectEditor->update($updateData);
391 }
392
393 // update owner's like counter and activity points
394 if ($likeable->getUserID()) {
395 if ($like->likeValue == Like::LIKE) {
396 $userEditor = new UserEditor(new User($likeable->getUserID()));
397 $userEditor->updateCounters([
398 'likesReceived' => -1
399 ]);
400
401 UserActivityPointHandler::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', [$likeable->getUserID() => 1]);
402 }
403 }
404
405 // update object's like counter
406 $likeable->updateLikeCounter($cumulativeLikes);
407
408 WCF::getDB()->commitTransaction();
409 }
410 catch (DatabaseException $e) {
411 WCF::getDB()->rollBackTransaction();
412 }
413
414 return [
415 'data' => $this->loadLikeStatus($likeObject, $user),
416 'newValue' => null,
417 'oldValue' => $like->likeValue,
418 'users' => $usersArray
419 ];
420 }
421
422 /**
423 * Removes all likes for given objects.
424 *
425 * @param string $objectType
426 * @param integer[] $objectIDs
427 * @param string[] $notificationObjectTypes
428 */
429 public function removeLikes($objectType, array $objectIDs, array $notificationObjectTypes = []) {
430 $objectTypeObj = $this->getObjectType($objectType);
431
432 // get like objects
433 $likeObjectList = new LikeObjectList();
434 $likeObjectList->getConditionBuilder()->add('like_object.objectTypeID = ?', [$objectTypeObj->objectTypeID]);
435 $likeObjectList->getConditionBuilder()->add('like_object.objectID IN (?)', [$objectIDs]);
436 $likeObjectList->readObjects();
437 $likeObjects = $likeObjectList->getObjects();
438 $likeObjectIDs = $likeObjectList->getObjectIDs();
439
440 // reduce count of received users
441 $users = [];
442 foreach ($likeObjects as $likeObject) {
443 if ($likeObject->likes) {
444 if (!isset($users[$likeObject->objectUserID])) $users[$likeObject->objectUserID] = 0;
445 $users[$likeObject->objectUserID] += $likeObject->likes;
446 }
447 }
448 foreach ($users as $userID => $receivedLikes) {
449 $userEditor = new UserEditor(new User(null, ['userID' => $userID]));
450 $userEditor->updateCounters([
451 'likesReceived' => $receivedLikes * -1
452 ]);
453 }
454
455 // get like ids
456 $likeList = new LikeList();
457 $likeList->getConditionBuilder()->add('like_table.objectTypeID = ?', [$objectTypeObj->objectTypeID]);
458 $likeList->getConditionBuilder()->add('like_table.objectID IN (?)', [$objectIDs]);
459 $likeList->readObjects();
460
461 if (count($likeList)) {
462 $likeData = [];
463 foreach ($likeList as $like) {
464 $likeData[$like->likeID] = $like->userID;
465 }
466
467 // delete like notifications
468 if (!empty($notificationObjectTypes)) {
469 foreach ($notificationObjectTypes as $notificationObjectType) {
470 UserNotificationHandler::getInstance()->removeNotifications($notificationObjectType, $likeList->getObjectIDs());
471 }
472 }
473 else if (UserNotificationHandler::getInstance()->getObjectTypeID($objectType.'.notification')) {
474 UserNotificationHandler::getInstance()->removeNotifications($objectType.'.notification', $likeList->getObjectIDs());
475 }
476
477 // revoke activity points
478 UserActivityPointHandler::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $likeData);
479
480 // delete likes
481 LikeEditor::deleteAll(array_keys($likeData));
482 }
483
484 // delete like objects
485 if (!empty($likeObjectIDs)) {
486 LikeObjectEditor::deleteAll($likeObjectIDs);
487 }
488
489 // delete activity events
490 if (UserActivityEventHandler::getInstance()->getObjectTypeID($objectTypeObj->objectType.'.recentActivityEvent')) {
491 UserActivityEventHandler::getInstance()->removeEvents($objectTypeObj->objectType.'.recentActivityEvent', $objectIDs);
492 }
493 }
494
495 /**
496 * Returns current like object status.
497 *
498 * @param LikeObject $likeObject
499 * @param User $user
500 * @return array
501 */
502 protected function loadLikeStatus(LikeObject $likeObject, User $user) {
503 $sql = "SELECT like_object.likes, like_object.dislikes, like_object.cumulativeLikes,
504 CASE WHEN like_table.likeValue IS NOT NULL THEN like_table.likeValue ELSE 0 END AS liked
505 FROM wcf".WCF_N."_like_object like_object
506 LEFT JOIN wcf".WCF_N."_like like_table
507 ON (like_table.objectTypeID = ?
508 AND like_table.objectID = like_object.objectID
509 AND like_table.userID = ?)
510 WHERE like_object.likeObjectID = ?";
511 $statement = WCF::getDB()->prepareStatement($sql);
512 $statement->execute([
513 $likeObject->objectTypeID,
514 $user->userID,
515 $likeObject->likeObjectID
516 ]);
517
518 return $statement->fetchArray();
519 }
520 }