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 +157 -24 2.0.1 → 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;
@@ -12,11 +13,13 @@
12 13 class NotificationService
13 14 {
14 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 - $query = 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 24 });
22 25
@@ -40,13 +43,15 @@
40 43 * @throws \Exception
41 44 */
42 45 public function getAllUnreadNotifications($per_page, $page)
43 46 {
47 + $per_page = Sanitize::perPage($per_page);
48 + $page = max(1, (int) $page);
44 49 $userId = get_current_user_id();
45 50 if (!$userId) {
46 51 throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
47 52 }
48 - $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
53 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
49 54 ->whereHas('users', function($q) use ($userId) {
50 55 $q->where('user_id', $userId)
51 56 ->whereNull('marked_read_at');
52 57 })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
@@ -87,9 +92,9 @@
87 92 public function newNotificationNumber()
88 93 {
89 94 $user = wp_get_current_user();
90 95
91 - $unread_notifications = Notification::query()->where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
96 + $unread_notifications = $this->getAccessibleNotificationsQuery($user->ID)
92 97 ->whereHas('users', function($q) use ($user) {
93 98 $q->where('user_id', $user->ID)
94 99 ->whereNull('marked_read_at');
95 100 });
@@ -96,8 +101,41 @@
96 101
97 102 return $unread_notifications->count();
98 103 }
99 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 +
100 138 public function isCurrentUserObservingTask($task)
101 139 {
102 140 $currentUserId = get_current_user_id();
103 141 $observers = $task->watchers()->get()->pluck('ID');
@@ -243,23 +281,48 @@
243 281 $task = Task::findOrFail($taskId);
244 282 $watchers = $task->watchers;
245 283 $currentUserId = get_current_user_id();
246 284
247 - $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 = [];
248 289
249 - foreach ($watchers as $watcher){
250 - if($watcher->ID != $currentUserId)
251 - {
252 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
253 - {
254 - $wathersToSendEmail[] = $watcher->user_email;
255 - }
290 + foreach ($watchers as $watcher) {
291 + if (isset($eligibleLookup[$watcher->ID])) {
292 + $watchersToSendEmail[] = $watcher->user_email;
256 293 }
257 294 }
258 295
259 - return $wathersToSendEmail;
296 + return $watchersToSendEmail;
260 297 }
261 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 +
262 325 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
263 326 {
264 327 if(
265 328 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -269,8 +332,38 @@
269 332 return false;
270 333 }
271 334 }
272 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 +
273 366 public function checkIfEmailEnabled($boardId, $userId, $purpose)
274 367 {
275 368 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
276 369
@@ -287,31 +380,72 @@
287 380
288 381 return false;
289 382 }
290 383
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)
395 + {
396 + $boardId = absint($boardId);
397 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
398 +
399 + if (!$boardId || !$mentionedUserIds) {
400 + return [];
401 + }
402 +
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 + }));
416 + }
417 +
291 418 public function mentionInComment($comment, $mentionedUserIds)
292 419 {
293 - $uniqueIds = array_unique($mentionedUserIds);
420 + $currentUserId = get_current_user_id();
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 + }));
294 425
295 - $uniqueIds = array_filter($uniqueIds, function($value) {
296 - return (int)$value !== get_current_user_id();
297 - });
426 + if (!$uniqueIds) {
427 + return;
428 + }
298 429
299 - //sending emails to mentioned users
300 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
301 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
430 + // Queued delivery revalidates membership and preferences before sending.
431 + $this->sendMailAfterMention($comment->id, $uniqueIds);
302 432
303 433 //sending desktop notifications
304 434 do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
305 435 }
306 436
307 - public function sendMailAfterMention($commentId, $usersToSendEmail)
437 + public function sendMailAfterMention($commentId, $recipientUserIds)
308 438 {
439 + if (empty($recipientUserIds)) {
440 + return;
441 + }
442 +
309 443 $current_user_id = get_current_user_id();
310 444
311 445 /* this will run in background as soon as possible */
312 446 /* sending Model or Model Instance won't work here */
313 - 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');
314 448 }
315 449
316 450 public function getUnreadNotificationsOfTasks($task)
317 451 {
@@ -318,9 +452,9 @@
318 452 $userId = get_current_user_id();
319 453 if (!$userId) {
320 454 throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
321 455 }
322 - $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
456 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
323 457 ->whereHas('users', function($q) use ($userId) {
324 458 $q->where('user_id', $userId)
325 459 ->whereNull('marked_read_at');
326 460 })->whereHas('task', function($q) use ($task) {
@@ -341,9 +475,9 @@
341 475 if (!$taskIds) {
342 476 return [];
343 477 }
344 478
345 - $rows = Notification::query()
479 + $rows = $this->getAccessibleNotificationsQuery($userId)
346 480 // Board/list views can return many tasks at once, so unread counts are
347 481 // grouped in one query instead of issuing a count query per task.
348 482 ->selectRaw('task_id, COUNT(*) as unread_count')
349 483 ->join(
@@ -351,9 +485,8 @@
351 485 'fbs_notifications.id',
352 486 '=',
353 487 'fbs_notification_users.notification_id'
354 488 )
355 - ->where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
356 489 ->where('fbs_notification_users.user_id', $userId)
357 490 ->whereNull('fbs_notification_users.marked_read_at')
358 491 ->whereIn('task_id', $taskIds)
359 492 ->groupBy('task_id')