3 namespace wcf\data\user\notification
;
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
;
17 * Executes user notification-related actions.
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
24 * @method UserNotificationEditor[] getObjects()
25 * @method UserNotificationEditor getSingleObject()
27 class UserNotificationAction
extends AbstractDatabaseObjectAction
30 * notification editor object
31 * @var UserNotificationEditor
33 public $notificationEditor;
37 * @return UserNotification
39 public function create()
41 /** @var UserNotification $notification */
42 $notification = parent
::create();
44 $sql = "INSERT INTO wcf" . WCF_N
. "_user_notification_to_user
45 (notificationID, userID)
47 $statement = WCF
::getDB()->prepareStatement($sql);
49 $notification->notificationID
,
50 $notification->userID
,
57 * Creates a simple notification without stacking support, applies to legacy notifications too.
61 public function createDefault()
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();
69 $notifications[$recipient->userID
] = [
71 'object' => $notification,
76 $sql = "INSERT INTO wcf" . WCF_N
. "_user_notification_author
77 (notificationID, authorID, time)
79 $statement = WCF
::getDB()->prepareStatement($sql);
81 WCF
::getDB()->beginTransaction();
82 foreach ($notifications as $notificationData) {
84 $notificationData['object']->notificationID
,
85 $this->parameters
['authorID'] ?
: null,
89 WCF
::getDB()->commitTransaction();
91 return $notifications;
95 * Creates a notification or adds another author to an existing one.
99 public function createStackable()
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;
114 foreach ($this->parameters
['recipients'] as $recipient) {
115 $notification = ($existingNotifications[$recipient->userID
] ??
null);
116 $isNew = ($notification === null);
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();
124 $notifications[$recipient->userID
] = [
126 'object' => $notification,
130 \
uasort($notifications, static function ($a, $b) {
131 if ($a['object']->notificationID
== $b['object']->notificationID
) {
133 } elseif ($a['object']->notificationID
< $b['object']->notificationID
) {
141 $sql = "INSERT IGNORE INTO wcf" . WCF_N
. "_user_notification_author
142 (notificationID, authorID, time)
144 $authorStatement = WCF
::getDB()->prepareStatement($sql);
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);
153 WCF
::getDB()->beginTransaction();
154 $notificationIDs = [];
155 foreach ($notifications as $notificationData) {
156 $notificationIDs[] = $notificationData['object']->notificationID
;
158 $authorStatement->execute([
159 $notificationData['object']->notificationID
,
160 $this->parameters
['authorID'] ?
: null,
163 $triggerStatement->execute([
165 $this->parameters
['authorID'] ?
0 : 1,
166 $notificationData['object']->notificationID
,
169 WCF
::getDB()->commitTransaction();
171 $notificationList = new UserNotificationList();
172 $notificationList->setObjectIDs($notificationIDs);
173 $notificationList->readObjects();
174 $updatedNotifications = $notificationList->getObjects();
176 $notifications = \array_map
(static function ($notificationData) use ($updatedNotifications) {
177 $notificationData['object'] = $updatedNotifications[$notificationData['object']->notificationID
];
179 return $notificationData;
182 return $notifications;
186 * Validates the 'getOutstandingNotifications' action.
188 public function validateGetOutstandingNotifications()
194 * Loads user notifications.
198 public function getOutstandingNotifications()
200 $notifications = UserNotificationHandler
::getInstance()->getMixedNotifications();
201 WCF
::getTPL()->assign([
202 'notifications' => $notifications,
206 'template' => WCF
::getTPL()->fetch('notificationListUserPanel'),
207 'totalCount' => $notifications['notificationCount'],
211 public function validateGetNotificationData(): void
215 public function getNotificationData(): array
217 $data = UserNotificationHandler
::getInstance()->getMixedNotifications();
218 if ($data['count'] === 0) {
223 foreach ($data['notifications'] as $notificationData) {
224 $notificationID = $notificationData['notificationID'];
226 /** @var IUserNotificationEvent $event */
227 $event = $notificationData['event'];
229 if ($notificationData['authors'] === 1) {
230 $image = $event->getAuthor()->getAvatar()->getImageTag(48);
232 $image = '<span class="icon icon48 fa-users"></span>';
236 if ($event->isConfirmed()) {
237 $link = $event->getLink();
239 $link = LinkHandler
::getInstance()->getControllerLink(
240 NotificationConfirmAction
::class,
241 ['id' => $notificationID]
245 $usernames = array_map(static function (UserProfile
$userProfile) {
246 return $userProfile->getFormattedUsername();
247 }, $event->getAuthors());
250 'content' => $event->getMessage(),
252 'isUnread' => !$event->isConfirmed(),
254 'objectId' => $notificationID,
255 'time' => $notificationData['time'],
256 'usernames' => $usernames,
260 return $notifications;
264 * Validates parameters to mark a notification as confirmed.
266 public function validateMarkAsConfirmed()
268 $this->notificationEditor
= $this->getSingleObject();
269 if ($this->notificationEditor
->userID
!= WCF
::getUser()->userID
) {
270 throw new PermissionDeniedException();
275 * Marks a notification as confirmed.
279 public function markAsConfirmed()
281 UserNotificationHandler
::getInstance()->markAsConfirmedByIDs([$this->notificationEditor
->notificationID
]);
284 'markAsRead' => $this->notificationEditor
->notificationID
,
285 'totalCount' => UserNotificationHandler
::getInstance()->getNotificationCount(true),
290 * Validates parameters to mark all notifications of current user as confirmed.
292 public function validateMarkAllAsConfirmed()
298 * Marks all notifications of current user as confirmed.
302 public function markAllAsConfirmed()
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
315 $statement = WCF
::getDB()->prepareStatement($sql);
316 $statement->execute([
317 WCF
::getUser()->userID
,
321 $notificationIDs = $statement->fetchAll(\PDO
::FETCH_COLUMN
);
323 if (!empty($notificationIDs)) {
324 // Step 2) Mark the notifications as read.
325 $condition = new PreparedStatementConditionBuilder();
326 $condition->add('notificationID IN (?)', [$notificationIDs]);
328 $sql = "UPDATE wcf" . WCF_N
. "_user_notification
331 $statement = WCF
::getDB()->prepareStatement($sql);
332 $statement->execute(\array_merge
([TIME_NOW
], $condition->getParameters()));
334 // Step 3) Delete notification_to_user assignments (mimic legacy notification system)
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
]);
341 $sql = "DELETE FROM wcf" . WCF_N
. "_user_notification_to_user
343 $statement = WCF
::getDB()->prepareStatement($sql);
344 $statement->execute($condition->getParameters());
347 // Step 4) Clear cached values.
348 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'userNotificationCount');
351 'markAllAsRead' => true,