PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
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 1.40 All 43 releases
← All changes | app/Services/NotificationService.php +152 -32 2.0.0 → 2.2.0 View file →
@@ -12,8 +12,10 @@
12 12 class NotificationService
13 13 {
14 14 public function getAllNotifications($per_page, $page, $action = 'all')
15 15 {
16 + $per_page = Sanitize::perPage($per_page);
17 + $page = max(1, (int) $page);
16 18 $user = wp_get_current_user();
17 19
18 20 $query = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
19 21 ->whereHas('users', function($q) use ($user) {
@@ -40,8 +42,10 @@
40 42 * @throws \Exception
41 43 */
42 44 public function getAllUnreadNotifications($per_page, $page)
43 45 {
46 + $per_page = Sanitize::perPage($per_page);
47 + $page = max(1, (int) $page);
44 48 $userId = get_current_user_id();
45 49 if (!$userId) {
46 50 throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
47 51 }
@@ -134,18 +138,15 @@
134 138 * Return board notification preferences with defaults and global watch fallbacks applied.
135 139 */
136 140 public function getBoardNotificationSettingsWithDefaults($boardId, $userId)
137 141 {
138 - $settings = array_merge(
139 - Constant::BOARD_NOTIFICATION_TYPES,
140 - $this->getGlobalWatchNotificationSettings($userId)
141 - );
142 + $settings = $this->getDefaultBoardNotificationSettings($userId);
142 143
143 144 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
144 145 if ($boardSettings && $boardSettings->preferences) {
145 146 $preferences = maybe_unserialize($boardSettings->preferences);
146 147 if (is_array($preferences)) {
147 - $settings = array_merge($settings, $preferences);
148 + $settings = array_merge($settings, array_intersect_key($preferences, $settings));
148 149 }
149 150 }
150 151
151 152 foreach ($settings as $key => $value) {
@@ -169,13 +170,9 @@
169 170 * Get global watch preferences for fallback when a board has no saved watch keys.
170 171 */
171 172 private function getGlobalWatchNotificationSettings($userId)
172 173 {
173 - $watchSettings = [
174 - Constant::GLOBAL_EMAIL_NOTIFICATION_CREATING_TASK => true,
175 - Constant::GLOBAL_EMAIL_NOTIFICATION_COMMENTING => true,
176 - Constant::GLOBAL_EMAIL_NOTIFICATION_ASSIGNING => true,
177 - ];
174 + $watchSettings = $this->getWatchNotificationDefaults();
178 175
179 176 $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
180 177 if (!$globalSettings || !$globalSettings->value) {
181 178 return $watchSettings;
@@ -194,8 +191,25 @@
194 191
195 192 return $watchSettings;
196 193 }
197 194
195 + private function getDefaultBoardNotificationSettings($userId)
196 + {
197 + return array_merge(
198 + Constant::BOARD_NOTIFICATION_TYPES,
199 + $this->getGlobalWatchNotificationSettings($userId)
200 + );
201 + }
202 +
203 + private function getWatchNotificationDefaults()
204 + {
205 + return [
206 + Constant::GLOBAL_EMAIL_NOTIFICATION_CREATING_TASK => true,
207 + Constant::GLOBAL_EMAIL_NOTIFICATION_COMMENTING => true,
208 + Constant::GLOBAL_EMAIL_NOTIFICATION_ASSIGNING => true,
209 + ];
210 + }
211 +
198 212 /**
199 213 * Convert stored preference values into strict booleans.
200 214 */
201 215 private function normalizePreferenceValue($value)
@@ -209,13 +223,23 @@
209 223 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
210 224 if(empty($boardSettings)){
211 225 return;
212 226 }
213 - foreach ($newSettings as $index => $setting)
214 - {
215 - $newSettings[$index] = $setting == 'true' ? true : false;
227 +
228 + $allowedSettings = array_merge(
229 + Constant::BOARD_NOTIFICATION_TYPES,
230 + $this->getWatchNotificationDefaults()
231 + );
232 + $filteredSettings = [];
233 + foreach ($newSettings as $index => $setting) {
234 + if (!array_key_exists($index, $allowedSettings)) {
235 + continue;
236 + }
237 +
238 + $filteredSettings[$index] = $this->normalizePreferenceValue($setting);
216 239 }
217 - $boardSettings->preferences = $newSettings;
240 +
241 + $boardSettings->preferences = $filteredSettings;
218 242 $boardSettings->save();
219 243
220 244 }
221 245
@@ -223,23 +247,48 @@
223 247 $task = Task::findOrFail($taskId);
224 248 $watchers = $task->watchers;
225 249 $currentUserId = get_current_user_id();
226 250
227 - $wathersToSendEmail = array();
251 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
252 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, $emailPurpose);
253 + $eligibleLookup = array_fill_keys($eligibleIds, true);
254 + $watchersToSendEmail = [];
228 255
229 - foreach ($watchers as $watcher){
230 - if($watcher->ID != $currentUserId)
231 - {
232 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
233 - {
234 - $wathersToSendEmail[] = $watcher->user_email;
235 - }
256 + foreach ($watchers as $watcher) {
257 + if (isset($eligibleLookup[$watcher->ID])) {
258 + $watchersToSendEmail[] = $watcher->user_email;
236 259 }
237 260 }
238 261
239 - return $wathersToSendEmail;
262 + return $watchersToSendEmail;
240 263 }
241 264
265 + /**
266 + * Get comment notification recipients as revocable user IDs for async delivery.
267 + *
268 + * @param int $taskId
269 + * @return array
270 + */
271 + public function getCommentRecipientUserIds($taskId)
272 + {
273 + $task = Task::findOrFail(absint($taskId));
274 + $currentUserId = get_current_user_id();
275 + $watchers = $task->watchers;
276 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
277 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, Constant::BOARD_EMAIL_COMMENT);
278 + $eligibleLookup = array_fill_keys($eligibleIds, true);
279 + $recipientUserIds = [];
280 +
281 + foreach ($watchers as $watcher) {
282 + $watcherId = absint($watcher->ID);
283 + if (isset($eligibleLookup[$watcherId])) {
284 + $recipientUserIds[] = $watcherId;
285 + }
286 + }
287 +
288 + return array_values(array_unique($recipientUserIds));
289 + }
290 +
242 291 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
243 292 {
244 293 if(
245 294 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -249,8 +298,38 @@
249 298 return false;
250 299 }
251 300 }
252 301
302 + /**
303 + * Resolve board email recipients in one query; missing preference keys default to enabled without writes.
304 + */
305 + public function getEmailEnabledUserIds($boardId, $userIds, $purpose)
306 + {
307 + $boardId = absint($boardId);
308 + $userIds = array_values(array_unique(array_filter(array_map('intval', (array) $userIds))));
309 + if (!$boardId || !$userIds) {
310 + return [];
311 + }
312 +
313 + // Match the first membership used by individual checks, including old duplicates.
314 + $relations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
315 + ->where('object_id', $boardId)
316 + ->whereIn('foreign_id', $userIds)
317 + ->orderBy('id', 'ASC')
318 + ->get(['foreign_id', 'preferences'])
319 + ->unique('foreign_id');
320 +
321 + $enabledUserIds = [];
322 + foreach ($relations as $relation) {
323 + $preferences = (array) maybe_unserialize($relation->preferences);
324 + if (!array_key_exists($purpose, $preferences) || $preferences[$purpose]) {
325 + $enabledUserIds[] = (int) $relation->foreign_id;
326 + }
327 + }
328 +
329 + return array_values(array_unique($enabledUserIds));
330 + }
331 +
253 332 public function checkIfEmailEnabled($boardId, $userId, $purpose)
254 333 {
255 334 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
256 335
@@ -267,31 +346,72 @@
267 346
268 347 return false;
269 348 }
270 349
350 + /**
351 + * Return the supplied user IDs that currently belong to a board.
352 + *
353 + * Mention recipients are board-scoped because comment content is private to
354 + * the board, even when a caller supplies a valid WordPress user ID.
355 + *
356 + * @param int $boardId
357 + * @param array $mentionedUserIds
358 + * @return array
359 + */
360 + public function resolveBoardMentionUserIds($boardId, $mentionedUserIds)
361 + {
362 + $boardId = absint($boardId);
363 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
364 +
365 + if (!$boardId || !$mentionedUserIds) {
366 + return [];
367 + }
368 +
369 + $boardMemberIdLookup = [];
370 + $boardRelations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
371 + ->where('object_id', $boardId)
372 + ->whereIn('foreign_id', $mentionedUserIds)
373 + ->get(['foreign_id']);
374 +
375 + foreach ($boardRelations as $boardRelation) {
376 + $boardMemberIdLookup[absint($boardRelation->foreign_id)] = true;
377 + }
378 +
379 + return array_values(array_filter($mentionedUserIds, function ($mentionedUserId) use ($boardMemberIdLookup) {
380 + return isset($boardMemberIdLookup[$mentionedUserId]);
381 + }));
382 + }
383 +
271 384 public function mentionInComment($comment, $mentionedUserIds)
272 385 {
273 - $uniqueIds = array_unique($mentionedUserIds);
386 + $currentUserId = get_current_user_id();
387 + $uniqueIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
388 + $uniqueIds = array_values(array_filter($uniqueIds, function ($mentionedUserId) use ($currentUserId) {
389 + return $mentionedUserId !== $currentUserId;
390 + }));
274 391
275 - $uniqueIds = array_filter($uniqueIds, function($value) {
276 - return (int)$value !== get_current_user_id();
277 - });
392 + if (!$uniqueIds) {
393 + return;
394 + }
278 395
279 - //sending emails to mentioned users
280 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
281 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
396 + // Queued delivery revalidates membership and preferences before sending.
397 + $this->sendMailAfterMention($comment->id, $uniqueIds);
282 398
283 399 //sending desktop notifications
284 400 do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
285 401 }
286 402
287 - public function sendMailAfterMention($commentId, $usersToSendEmail)
403 + public function sendMailAfterMention($commentId, $recipientUserIds)
288 404 {
405 + if (empty($recipientUserIds)) {
406 + return;
407 + }
408 +
289 409 $current_user_id = get_current_user_id();
290 410
291 411 /* this will run in background as soon as possible */
292 412 /* sending Model or Model Instance won't work here */
293 - as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $usersToSendEmail, $current_user_id], 'fluent-boards');
413 + as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
294 414 }
295 415
296 416 public function getUnreadNotificationsOfTasks($task)
297 417 {