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