Prototype for the new user menus
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / user / notification / UserNotificationAction.class.php
1 <?php
2
3 namespace wcf\data\user\notification;
4
5 use wcf\action\NotificationConfirmAction;
6 use wcf\data\AbstractDatabaseObjectAction;
7 use wcf\data\user\UserProfile;
8 use wcf\system\database\util\PreparedStatementConditionBuilder;
9 use wcf\system\exception\PermissionDeniedException;
10 use wcf\system\request\LinkHandler;
11 use wcf\system\user\notification\event\IUserNotificationEvent;
12 use wcf\system\user\notification\UserNotificationHandler;
13 use wcf\system\user\storage\UserStorageHandler;
14 use wcf\system\WCF;
15
16 /**
17 * Executes user notification-related actions.
18 *
19 * @author Marcel Werk
20 * @copyright 2001-2019 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @package WoltLabSuite\Core\Data\User\Notification
23 *
24 * @method UserNotificationEditor[] getObjects()
25 * @method UserNotificationEditor getSingleObject()
26 */
27 class UserNotificationAction extends AbstractDatabaseObjectAction
28 {
29 /**
30 * notification editor object
31 * @var UserNotificationEditor
32 */
33 public $notificationEditor;
34
35 /**
36 * @inheritDoc
37 * @return UserNotification
38 */
39 public function create()
40 {
41 /** @var UserNotification $notification */
42 $notification = parent::create();
43
44 $sql = "INSERT INTO wcf" . WCF_N . "_user_notification_to_user
45 (notificationID, userID)
46 VALUES (?, ?)";
47 $statement = WCF::getDB()->prepareStatement($sql);
48 $statement->execute([
49 $notification->notificationID,
50 $notification->userID,
51 ]);
52
53 return $notification;
54 }
55
56 /**
57 * Creates a simple notification without stacking support, applies to legacy notifications too.
58 *
59 * @return mixed[][]
60 */
61 public function createDefault()
62 {
63 $notifications = [];
64 foreach ($this->parameters['recipients'] as $recipient) {
65 $this->parameters['data']['userID'] = $recipient->userID;
66 $this->parameters['data']['mailNotified'] = (($recipient->mailNotificationType == 'none' || $recipient->mailNotificationType == 'instant') ? 1 : 0);
67 $notification = $this->create();
68
69 $notifications[$recipient->userID] = [
70 'isNew' => true,
71 'object' => $notification,
72 ];
73 }
74
75 // insert author
76 $sql = "INSERT INTO wcf" . WCF_N . "_user_notification_author
77 (notificationID, authorID, time)
78 VALUES (?, ?, ?)";
79 $statement = WCF::getDB()->prepareStatement($sql);
80
81 WCF::getDB()->beginTransaction();
82 foreach ($notifications as $notificationData) {
83 $statement->execute([
84 $notificationData['object']->notificationID,
85 $this->parameters['authorID'] ?: null,
86 TIME_NOW,
87 ]);
88 }
89 WCF::getDB()->commitTransaction();
90
91 return $notifications;
92 }
93
94 /**
95 * Creates a notification or adds another author to an existing one.
96 *
97 * @return mixed[][]
98 */
99 public function createStackable()
100 {
101 // get existing notifications
102 $notificationList = new UserNotificationList();
103 $notificationList->getConditionBuilder()->add("eventID = ?", [$this->parameters['data']['eventID']]);
104 $notificationList->getConditionBuilder()->add("eventHash = ?", [$this->parameters['data']['eventHash']]);
105 $notificationList->getConditionBuilder()->add("userID IN (?)", [\array_keys($this->parameters['recipients'])]);
106 $notificationList->getConditionBuilder()->add("confirmTime = ?", [0]);
107 $notificationList->readObjects();
108 $existingNotifications = [];
109 foreach ($notificationList as $notification) {
110 $existingNotifications[$notification->userID] = $notification;
111 }
112
113 $notifications = [];
114 foreach ($this->parameters['recipients'] as $recipient) {
115 $notification = ($existingNotifications[$recipient->userID] ?? null);
116 $isNew = ($notification === null);
117
118 if ($notification === null) {
119 $this->parameters['data']['userID'] = $recipient->userID;
120 $this->parameters['data']['mailNotified'] = (($recipient->mailNotificationType == 'none' || $recipient->mailNotificationType == 'instant') ? 1 : 0);
121 $notification = $this->create();
122 }
123
124 $notifications[$recipient->userID] = [
125 'isNew' => $isNew,
126 'object' => $notification,
127 ];
128 }
129
130 \uasort($notifications, static function ($a, $b) {
131 if ($a['object']->notificationID == $b['object']->notificationID) {
132 return 0;
133 } elseif ($a['object']->notificationID < $b['object']->notificationID) {
134 return -1;
135 }
136
137 return 1;
138 });
139
140 // insert author
141 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_user_notification_author
142 (notificationID, authorID, time)
143 VALUES (?, ?, ?)";
144 $authorStatement = WCF::getDB()->prepareStatement($sql);
145
146 // update trigger count
147 $sql = "UPDATE wcf" . WCF_N . "_user_notification
148 SET timesTriggered = timesTriggered + ?,
149 guestTimesTriggered = guestTimesTriggered + ?
150 WHERE notificationID = ?";
151 $triggerStatement = WCF::getDB()->prepareStatement($sql);
152
153 WCF::getDB()->beginTransaction();
154 $notificationIDs = [];
155 foreach ($notifications as $notificationData) {
156 $notificationIDs[] = $notificationData['object']->notificationID;
157
158 $authorStatement->execute([
159 $notificationData['object']->notificationID,
160 $this->parameters['authorID'] ?: null,
161 TIME_NOW,
162 ]);
163 $triggerStatement->execute([
164 1,
165 $this->parameters['authorID'] ? 0 : 1,
166 $notificationData['object']->notificationID,
167 ]);
168 }
169 WCF::getDB()->commitTransaction();
170
171 $notificationList = new UserNotificationList();
172 $notificationList->setObjectIDs($notificationIDs);
173 $notificationList->readObjects();
174 $updatedNotifications = $notificationList->getObjects();
175
176 $notifications = \array_map(static function ($notificationData) use ($updatedNotifications) {
177 $notificationData['object'] = $updatedNotifications[$notificationData['object']->notificationID];
178
179 return $notificationData;
180 }, $notifications);
181
182 return $notifications;
183 }
184
185 /**
186 * Validates the 'getOutstandingNotifications' action.
187 */
188 public function validateGetOutstandingNotifications()
189 {
190 // does nothing
191 }
192
193 /**
194 * Loads user notifications.
195 *
196 * @return mixed[]
197 */
198 public function getOutstandingNotifications()
199 {
200 $notifications = UserNotificationHandler::getInstance()->getMixedNotifications();
201 WCF::getTPL()->assign([
202 'notifications' => $notifications,
203 ]);
204
205 return [
206 'template' => WCF::getTPL()->fetch('notificationListUserPanel'),
207 'totalCount' => $notifications['notificationCount'],
208 ];
209 }
210
211 public function validateGetNotificationData(): void
212 {
213 }
214
215 public function getNotificationData(): array
216 {
217 $data = UserNotificationHandler::getInstance()->getMixedNotifications();
218 if ($data['count'] === 0) {
219 return [];
220 }
221
222 $notifications = [];
223 foreach ($data['notifications'] as $notificationData) {
224 $notificationID = $notificationData['notificationID'];
225
226 /** @var IUserNotificationEvent $event */
227 $event = $notificationData['event'];
228
229 if ($notificationData['authors'] === 1) {
230 $image = $event->getAuthor()->getAvatar()->getImageTag(48);
231 } else {
232 $image = '<span class="icon icon48 fa-users"></span>';
233 }
234
235
236 if ($event->isConfirmed()) {
237 $link = $event->getLink();
238 } else {
239 $link = LinkHandler::getInstance()->getControllerLink(
240 NotificationConfirmAction::class,
241 ['id' => $notificationID]
242 );
243 }
244
245 $usernames = array_map(static function (UserProfile $userProfile) {
246 return $userProfile->getFormattedUsername();
247 }, $event->getAuthors());
248
249 $notifications[] = [
250 'content' => $event->getMessage(),
251 'image' => $image,
252 'isUnread' => !$event->isConfirmed(),
253 'link' => $link,
254 'objectId' => $notificationID,
255 'time' => $notificationData['time'],
256 'usernames' => $usernames,
257 ];
258 }
259
260 return $notifications;
261 }
262
263 /**
264 * Validates parameters to mark a notification as confirmed.
265 */
266 public function validateMarkAsConfirmed()
267 {
268 $this->notificationEditor = $this->getSingleObject();
269 if ($this->notificationEditor->userID != WCF::getUser()->userID) {
270 throw new PermissionDeniedException();
271 }
272 }
273
274 /**
275 * Marks a notification as confirmed.
276 *
277 * @return array
278 */
279 public function markAsConfirmed()
280 {
281 UserNotificationHandler::getInstance()->markAsConfirmedByIDs([$this->notificationEditor->notificationID]);
282
283 return [
284 'markAsRead' => $this->notificationEditor->notificationID,
285 'totalCount' => UserNotificationHandler::getInstance()->getNotificationCount(true),
286 ];
287 }
288
289 /**
290 * Validates parameters to mark all notifications of current user as confirmed.
291 */
292 public function validateMarkAllAsConfirmed()
293 {
294 // does nothing
295 }
296
297 /**
298 * Marks all notifications of current user as confirmed.
299 *
300 * @return bool[]
301 */
302 public function markAllAsConfirmed()
303 {
304 // Step 1) Find the IDs of the unread notifications.
305 // This is done in a separate step, because this allows the UPDATE query to
306 // leverage fine-grained locking of exact rows based off the PRIMARY KEY.
307 // Simply updating all notifications belonging to a specific user will need
308 // to prevent concurrent threads from inserting new notifications for proper
309 // consistency, possibly leading to deadlocks.
310 $sql = "SELECT notificationID
311 FROM wcf" . WCF_N . "_user_notification
312 WHERE userID = ?
313 AND confirmTime = ?
314 AND time < ?";
315 $statement = WCF::getDB()->prepareStatement($sql);
316 $statement->execute([
317 WCF::getUser()->userID,
318 0,
319 TIME_NOW,
320 ]);
321 $notificationIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
322
323 if (!empty($notificationIDs)) {
324 // Step 2) Mark the notifications as read.
325 $condition = new PreparedStatementConditionBuilder();
326 $condition->add('notificationID IN (?)', [$notificationIDs]);
327
328 $sql = "UPDATE wcf" . WCF_N . "_user_notification
329 SET confirmTime = ?
330 {$condition}";
331 $statement = WCF::getDB()->prepareStatement($sql);
332 $statement->execute(\array_merge([TIME_NOW], $condition->getParameters()));
333
334 // Step 3) Delete notification_to_user assignments (mimic legacy notification system)
335
336 // This conditions technically is not required, because notificationIDs are unique.
337 // As this is not enforced at the database layer we play safe until this legacy table
338 // finally is removed.
339 $condition->add('userID = ?', [WCF::getUser()->userID]);
340
341 $sql = "DELETE FROM wcf" . WCF_N . "_user_notification_to_user
342 {$condition}";
343 $statement = WCF::getDB()->prepareStatement($sql);
344 $statement->execute($condition->getParameters());
345 }
346
347 // Step 4) Clear cached values.
348 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'userNotificationCount');
349
350 return [
351 'markAllAsRead' => true,
352 ];
353 }
354 }