PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.1
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.1
2.2.1 2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 All 44 releases
← All changes | app/Services/NotificationService.php +300 -49 1.35 → 2.2.1 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 +use FluentBoards\App\Models\Board;
5 6 use FluentBoards\App\Models\Meta;
6 7 use FluentBoards\App\Models\Notification;
7 8 use FluentBoards\App\Models\NotificationUser;
8 9 use FluentBoards\App\Models\Relation;
@@ -10,17 +11,28 @@
10 11 use FluentBoards\App\Models\User;
11 12
12 13 class NotificationService
13 14 {
14 - public function getAllNotifications($per_page, $page)
15 + public function getAllNotifications($per_page, $page, $action = 'all')
15 16 {
17 + $per_page = Sanitize::perPage($per_page);
18 + $page = max(1, (int) $page);
16 19 $user = wp_get_current_user();
17 20
18 - $notifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
21 + $query = $this->getAccessibleNotificationsQuery($user->ID)
19 22 ->whereHas('users', function($q) use ($user) {
20 23 $q->where('user_id', $user->ID);
21 - })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
24 + });
22 25
26 + // Add action filter if not 'all'
27 + if ($action !== 'all') {
28 + $query->where('action', $action);
29 + }
30 +
31 + $notifications = $query->with('activitist', 'task')
32 + ->orderBy('created_at', 'desc')
33 + ->paginate($per_page, ['*'], 'page', $page);
34 +
23 35 foreach ($notifications as $notification){
24 36 $notification->read = $notification->checkReadOrNot();
25 37 }
26 38
@@ -31,13 +43,15 @@
31 43 * @throws \Exception
32 44 */
33 45 public function getAllUnreadNotifications($per_page, $page)
34 46 {
47 + $per_page = Sanitize::perPage($per_page);
48 + $page = max(1, (int) $page);
35 49 $userId = get_current_user_id();
36 50 if (!$userId) {
37 - throw new \Exception('You are not allowed to do that', 403);
51 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
38 52 }
39 - $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
53 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
40 54 ->whereHas('users', function($q) use ($userId) {
41 55 $q->where('user_id', $userId)
42 56 ->whereNull('marked_read_at');
43 57 })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
@@ -65,8 +79,12 @@
65 79
66 80 $notification = NotificationUser::where('user_id', $user->ID)
67 81 ->where('notification_id', $notificationId)
68 82 ->first();
83 + if (!$notification) {
84 + throw new \Exception(esc_html__('Notification could not be found', 'fluent-boards'), 404);
85 + }
86 +
69 87 $notification->marked_read_at = current_time('mysql');
70 88 $notification->save();
71 89 return $notification;
72 90 }
@@ -74,9 +92,9 @@
74 92 public function newNotificationNumber()
75 93 {
76 94 $user = wp_get_current_user();
77 95
78 - $unread_notifications = Notification::query()->where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
96 + $unread_notifications = $this->getAccessibleNotificationsQuery($user->ID)
79 97 ->whereHas('users', function($q) use ($user) {
80 98 $q->where('user_id', $user->ID)
81 99 ->whereNull('marked_read_at');
82 100 });
@@ -83,8 +101,41 @@
83 101
84 102 return $unread_notifications->count();
85 103 }
86 104
105 + /**
106 + * Historical recipients must still have access to the source board and current task.
107 + * Keep legacy subtasks readable through their parent when their board_id is null.
108 + */
109 + private function getAccessibleNotificationsQuery($userId)
110 + {
111 + $query = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION);
112 + if (!$userId) {
113 + return $query->where('id', 0);
114 + }
115 +
116 + $boardIds = Board::select('id');
117 + if (!PermissionManager::isAdmin($userId)) {
118 + $boardIds->whereIn('id', Relation::select('object_id')
119 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
120 + ->where('foreign_id', $userId));
121 + }
122 +
123 + return $query->whereIn('object_id', $boardIds)
124 + ->where(function ($notifications) use ($boardIds) {
125 + $notifications->whereNull('task_id')
126 + ->orWhereHas('task', function ($tasks) use ($boardIds) {
127 + $tasks->where(function ($ownedTasks) use ($boardIds) {
128 + $ownedTasks->whereIn('board_id', $boardIds)
129 + ->orWhere(function ($legacySubtasks) use ($boardIds) {
130 + $legacySubtasks->whereNull('board_id')
131 + ->whereIn('parent_id', Task::select('id')->whereIn('board_id', $boardIds));
132 + });
133 + });
134 + });
135 + });
136 + }
137 +
87 138 public function isCurrentUserObservingTask($task)
88 139 {
89 140 $currentUserId = get_current_user_id();
90 141 $observers = $task->watchers()->get()->pluck('ID');
@@ -116,8 +167,91 @@
116 167
117 168 return $settings;
118 169 }
119 170
171 + /**
172 + * Return board notification preferences with defaults and global watch fallbacks applied.
173 + */
174 + public function getBoardNotificationSettingsWithDefaults($boardId, $userId)
175 + {
176 + $settings = $this->getDefaultBoardNotificationSettings($userId);
177 +
178 + $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
179 + if ($boardSettings && $boardSettings->preferences) {
180 + $preferences = maybe_unserialize($boardSettings->preferences);
181 + if (is_array($preferences)) {
182 + $settings = array_merge($settings, array_intersect_key($preferences, $settings));
183 + }
184 + }
185 +
186 + foreach ($settings as $key => $value) {
187 + $settings[$key] = $this->normalizePreferenceValue($value);
188 + }
189 +
190 + return $settings;
191 + }
192 +
193 + /**
194 + * Check if a board-scoped auto-watch preference is enabled for a user.
195 + */
196 + public function isBoardAutoWatchEnabled($boardId, $userId, $preferenceKey)
197 + {
198 + $settings = $this->getBoardNotificationSettingsWithDefaults($boardId, $userId);
199 +
200 + return array_key_exists($preferenceKey, $settings) && $this->normalizePreferenceValue($settings[$preferenceKey]);
201 + }
202 +
203 + /**
204 + * Get global watch preferences for fallback when a board has no saved watch keys.
205 + */
206 + private function getGlobalWatchNotificationSettings($userId)
207 + {
208 + $watchSettings = $this->getWatchNotificationDefaults();
209 +
210 + $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
211 + if (!$globalSettings || !$globalSettings->value) {
212 + return $watchSettings;
213 + }
214 +
215 + $preferences = maybe_unserialize($globalSettings->value);
216 + if (!is_array($preferences)) {
217 + return $watchSettings;
218 + }
219 +
220 + foreach (array_keys($watchSettings) as $key) {
221 + if (array_key_exists($key, $preferences)) {
222 + $watchSettings[$key] = $this->normalizePreferenceValue($preferences[$key]);
223 + }
224 + }
225 +
226 + return $watchSettings;
227 + }
228 +
229 + private function getDefaultBoardNotificationSettings($userId)
230 + {
231 + return array_merge(
232 + Constant::BOARD_NOTIFICATION_TYPES,
233 + $this->getGlobalWatchNotificationSettings($userId)
234 + );
235 + }
236 +
237 + private function getWatchNotificationDefaults()
238 + {
239 + return [
240 + Constant::GLOBAL_EMAIL_NOTIFICATION_CREATING_TASK => true,
241 + Constant::GLOBAL_EMAIL_NOTIFICATION_COMMENTING => true,
242 + Constant::GLOBAL_EMAIL_NOTIFICATION_ASSIGNING => true,
243 + ];
244 + }
245 +
246 + /**
247 + * Convert stored preference values into strict booleans.
248 + */
249 + private function normalizePreferenceValue($value)
250 + {
251 + return true === $value || 1 === $value || '1' === $value || 'true' === $value;
252 + }
253 +
120 254 public function updateBoardNotificationSettings($newSettings, $id)
121 255 {
122 256 $userId = get_current_user_id();
123 257 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
@@ -123,13 +257,23 @@
123 257 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
124 258 if(empty($boardSettings)){
125 259 return;
126 260 }
127 - foreach ($newSettings as $index => $setting)
128 - {
129 - $newSettings[$index] = $setting == 'true' ? true : false;
261 +
262 + $allowedSettings = array_merge(
263 + Constant::BOARD_NOTIFICATION_TYPES,
264 + $this->getWatchNotificationDefaults()
265 + );
266 + $filteredSettings = [];
267 + foreach ($newSettings as $index => $setting) {
268 + if (!array_key_exists($index, $allowedSettings)) {
269 + continue;
270 + }
271 +
272 + $filteredSettings[$index] = $this->normalizePreferenceValue($setting);
130 273 }
131 - $boardSettings->preferences = $newSettings;
274 +
275 + $boardSettings->preferences = $filteredSettings;
132 276 $boardSettings->save();
133 277
134 278 }
135 279
@@ -137,23 +281,48 @@
137 281 $task = Task::findOrFail($taskId);
138 282 $watchers = $task->watchers;
139 283 $currentUserId = get_current_user_id();
140 284
141 - $wathersToSendEmail = array();
285 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
286 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, $emailPurpose);
287 + $eligibleLookup = array_fill_keys($eligibleIds, true);
288 + $watchersToSendEmail = [];
142 289
143 - foreach ($watchers as $watcher){
144 - if($watcher->ID != $currentUserId)
145 - {
146 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
147 - {
148 - $wathersToSendEmail[] = $watcher->user_email;
149 - }
290 + foreach ($watchers as $watcher) {
291 + if (isset($eligibleLookup[$watcher->ID])) {
292 + $watchersToSendEmail[] = $watcher->user_email;
150 293 }
151 294 }
152 295
153 - return $wathersToSendEmail;
296 + return $watchersToSendEmail;
154 297 }
155 298
299 + /**
300 + * Get comment notification recipients as revocable user IDs for async delivery.
301 + *
302 + * @param int $taskId
303 + * @return array
304 + */
305 + public function getCommentRecipientUserIds($taskId)
306 + {
307 + $task = Task::findOrFail(absint($taskId));
308 + $currentUserId = get_current_user_id();
309 + $watchers = $task->watchers;
310 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
311 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, Constant::BOARD_EMAIL_COMMENT);
312 + $eligibleLookup = array_fill_keys($eligibleIds, true);
313 + $recipientUserIds = [];
314 +
315 + foreach ($watchers as $watcher) {
316 + $watcherId = absint($watcher->ID);
317 + if (isset($eligibleLookup[$watcherId])) {
318 + $recipientUserIds[] = $watcherId;
319 + }
320 + }
321 +
322 + return array_values(array_unique($recipientUserIds));
323 + }
324 +
156 325 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
157 326 {
158 327 if(
159 328 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -163,8 +332,38 @@
163 332 return false;
164 333 }
165 334 }
166 335
336 + /**
337 + * Resolve board email recipients in one query; missing preference keys default to enabled without writes.
338 + */
339 + public function getEmailEnabledUserIds($boardId, $userIds, $purpose)
340 + {
341 + $boardId = absint($boardId);
342 + $userIds = array_values(array_unique(array_filter(array_map('intval', (array) $userIds))));
343 + if (!$boardId || !$userIds) {
344 + return [];
345 + }
346 +
347 + // Match the first membership used by individual checks, including old duplicates.
348 + $relations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
349 + ->where('object_id', $boardId)
350 + ->whereIn('foreign_id', $userIds)
351 + ->orderBy('id', 'ASC')
352 + ->get(['foreign_id', 'preferences'])
353 + ->unique('foreign_id');
354 +
355 + $enabledUserIds = [];
356 + foreach ($relations as $relation) {
357 + $preferences = (array) maybe_unserialize($relation->preferences);
358 + if (!array_key_exists($purpose, $preferences) || $preferences[$purpose]) {
359 + $enabledUserIds[] = (int) $relation->foreign_id;
360 + }
361 + }
362 +
363 + return array_values(array_unique($enabledUserIds));
364 + }
365 +
167 366 public function checkIfEmailEnabled($boardId, $userId, $purpose)
168 367 {
169 368 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
170 369
@@ -181,54 +380,72 @@
181 380
182 381 return false;
183 382 }
184 383
185 - public function checkIfEmailEnabledGlobally($userId, $purpose)
384 + /**
385 + * Return the supplied user IDs that currently belong to a board.
386 + *
387 + * Mention recipients are board-scoped because comment content is private to
388 + * the board, even when a caller supplies a valid WordPress user ID.
389 + *
390 + * @param int $boardId
391 + * @param array $mentionedUserIds
392 + * @return array
393 + */
394 + public function resolveBoardMentionUserIds($boardId, $mentionedUserIds)
186 395 {
187 - $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
396 + $boardId = absint($boardId);
397 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
188 398
189 - if($globalSettings){
190 - $preferences = maybe_unserialize($globalSettings['value']);
191 - if(!array_key_exists($purpose, $preferences)){
192 - $preferences[$purpose] = true;
193 - $globalSettings->preferences = $preferences;
194 - $globalSettings->save();
195 - return true;
196 - }
197 - return $preferences[$purpose];
399 + if (!$boardId || !$mentionedUserIds) {
400 + return [];
198 401 }
199 402
200 - return true;
403 + $boardMemberIdLookup = [];
404 + $boardRelations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
405 + ->where('object_id', $boardId)
406 + ->whereIn('foreign_id', $mentionedUserIds)
407 + ->get(['foreign_id']);
408 +
409 + foreach ($boardRelations as $boardRelation) {
410 + $boardMemberIdLookup[absint($boardRelation->foreign_id)] = true;
411 + }
412 +
413 + return array_values(array_filter($mentionedUserIds, function ($mentionedUserId) use ($boardMemberIdLookup) {
414 + return isset($boardMemberIdLookup[$mentionedUserId]);
415 + }));
201 416 }
202 417
203 418 public function mentionInComment($comment, $mentionedUserIds)
204 419 {
205 - $uniqueIds = array_unique($mentionedUserIds);
206 420 $currentUserId = get_current_user_id();
207 - if (in_array($currentUserId, $uniqueIds))
208 - {
209 - $key = array_search($currentUserId, $uniqueIds);
210 - if ($key !== false) {
211 - unset($uniqueIds[$key]);
212 - }
213 - // Re-index the array (optional)
214 - $uniqueIds = array_values($uniqueIds);
421 + $uniqueIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
422 + $uniqueIds = array_values(array_filter($uniqueIds, function ($mentionedUserId) use ($currentUserId) {
423 + return $mentionedUserId !== $currentUserId;
424 + }));
425 +
426 + if (!$uniqueIds) {
427 + return;
215 428 }
216 - //sending emails to mentioned users
217 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
218 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
219 429
430 + // Queued delivery revalidates membership and preferences before sending.
431 + $this->sendMailAfterMention($comment->id, $uniqueIds);
432 +
220 433 //sending desktop notifications
221 - do_action('fluent_boards/mention_comment_notification', $comment, $mentionedUserIds);
434 + do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
222 435 }
223 436
224 - public function sendMailAfterMention($commentId, $usersToSendEmail)
437 + public function sendMailAfterMention($commentId, $recipientUserIds)
225 438 {
439 + if (empty($recipientUserIds)) {
440 + return;
441 + }
442 +
226 443 $current_user_id = get_current_user_id();
227 444
228 445 /* this will run in background as soon as possible */
229 446 /* sending Model or Model Instance won't work here */
230 - as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $usersToSendEmail, $current_user_id], 'fluent-boards');
447 + as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
231 448 }
232 449
233 450 public function getUnreadNotificationsOfTasks($task)
234 451 {
@@ -233,11 +450,11 @@
233 450 public function getUnreadNotificationsOfTasks($task)
234 451 {
235 452 $userId = get_current_user_id();
236 453 if (!$userId) {
237 - throw new \Exception('You are not allowed to do that', 403);
454 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
238 455 }
239 - $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
456 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
240 457 ->whereHas('users', function($q) use ($userId) {
241 458 $q->where('user_id', $userId)
242 459 ->whereNull('marked_read_at');
243 460 })->whereHas('task', function($q) use ($task) {
@@ -245,7 +462,41 @@
245 462 })
246 463 ->with('activitist')->orderBy('created_at', 'desc')->count();
247 464 return $unreadNotifications;
248 465 }
249 -}
250 466
467 + public function getUnreadNotificationCountsByTaskIds($taskIds)
468 + {
469 + $userId = get_current_user_id();
470 + if (!$userId) {
471 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
472 + }
251 473
474 + $taskIds = array_values(array_filter(array_map('intval', (array) $taskIds)));
475 + if (!$taskIds) {
476 + return [];
477 + }
478 +
479 + $rows = $this->getAccessibleNotificationsQuery($userId)
480 + // Board/list views can return many tasks at once, so unread counts are
481 + // grouped in one query instead of issuing a count query per task.
482 + ->selectRaw('task_id, COUNT(*) as unread_count')
483 + ->join(
484 + (new NotificationUser())->getTable(),
485 + 'fbs_notifications.id',
486 + '=',
487 + 'fbs_notification_users.notification_id'
488 + )
489 + ->where('fbs_notification_users.user_id', $userId)
490 + ->whereNull('fbs_notification_users.marked_read_at')
491 + ->whereIn('task_id', $taskIds)
492 + ->groupBy('task_id')
493 + ->get();
494 +
495 + $notificationCounts = [];
496 + foreach ($rows as $row) {
497 + $notificationCounts[(int) $row->task_id] = (int) $row->unread_count;
498 + }
499 +
500 + return $notificationCounts;
501 + }
502 +}