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