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 +215 -22 1.95 → 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 }
@@ -129,8 +133,91 @@
129 133
130 134 return $settings;
131 135 }
132 136
137 + /**
138 + * Return board notification preferences with defaults and global watch fallbacks applied.
139 + */
140 + public function getBoardNotificationSettingsWithDefaults($boardId, $userId)
141 + {
142 + $settings = $this->getDefaultBoardNotificationSettings($userId);
143 +
144 + $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
145 + if ($boardSettings && $boardSettings->preferences) {
146 + $preferences = maybe_unserialize($boardSettings->preferences);
147 + if (is_array($preferences)) {
148 + $settings = array_merge($settings, array_intersect_key($preferences, $settings));
149 + }
150 + }
151 +
152 + foreach ($settings as $key => $value) {
153 + $settings[$key] = $this->normalizePreferenceValue($value);
154 + }
155 +
156 + return $settings;
157 + }
158 +
159 + /**
160 + * Check if a board-scoped auto-watch preference is enabled for a user.
161 + */
162 + public function isBoardAutoWatchEnabled($boardId, $userId, $preferenceKey)
163 + {
164 + $settings = $this->getBoardNotificationSettingsWithDefaults($boardId, $userId);
165 +
166 + return array_key_exists($preferenceKey, $settings) && $this->normalizePreferenceValue($settings[$preferenceKey]);
167 + }
168 +
169 + /**
170 + * Get global watch preferences for fallback when a board has no saved watch keys.
171 + */
172 + private function getGlobalWatchNotificationSettings($userId)
173 + {
174 + $watchSettings = $this->getWatchNotificationDefaults();
175 +
176 + $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
177 + if (!$globalSettings || !$globalSettings->value) {
178 + return $watchSettings;
179 + }
180 +
181 + $preferences = maybe_unserialize($globalSettings->value);
182 + if (!is_array($preferences)) {
183 + return $watchSettings;
184 + }
185 +
186 + foreach (array_keys($watchSettings) as $key) {
187 + if (array_key_exists($key, $preferences)) {
188 + $watchSettings[$key] = $this->normalizePreferenceValue($preferences[$key]);
189 + }
190 + }
191 +
192 + return $watchSettings;
193 + }
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 +
212 + /**
213 + * Convert stored preference values into strict booleans.
214 + */
215 + private function normalizePreferenceValue($value)
216 + {
217 + return true === $value || 1 === $value || '1' === $value || 'true' === $value;
218 + }
219 +
133 220 public function updateBoardNotificationSettings($newSettings, $id)
134 221 {
135 222 $userId = get_current_user_id();
136 223 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
@@ -136,13 +223,23 @@
136 223 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
137 224 if(empty($boardSettings)){
138 225 return;
139 226 }
140 - foreach ($newSettings as $index => $setting)
141 - {
142 - $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);
143 239 }
144 - $boardSettings->preferences = $newSettings;
240 +
241 + $boardSettings->preferences = $filteredSettings;
145 242 $boardSettings->save();
146 243
147 244 }
148 245
@@ -150,23 +247,48 @@
150 247 $task = Task::findOrFail($taskId);
151 248 $watchers = $task->watchers;
152 249 $currentUserId = get_current_user_id();
153 250
154 - $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 = [];
155 255
156 - foreach ($watchers as $watcher){
157 - if($watcher->ID != $currentUserId)
158 - {
159 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
160 - {
161 - $wathersToSendEmail[] = $watcher->user_email;
162 - }
256 + foreach ($watchers as $watcher) {
257 + if (isset($eligibleLookup[$watcher->ID])) {
258 + $watchersToSendEmail[] = $watcher->user_email;
163 259 }
164 260 }
165 261
166 - return $wathersToSendEmail;
262 + return $watchersToSendEmail;
167 263 }
168 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 +
169 291 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
170 292 {
171 293 if(
172 294 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -176,8 +298,38 @@
176 298 return false;
177 299 }
178 300 }
179 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 +
180 332 public function checkIfEmailEnabled($boardId, $userId, $purpose)
181 333 {
182 334 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
183 335
@@ -194,31 +346,72 @@
194 346
195 347 return false;
196 348 }
197 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 +
198 384 public function mentionInComment($comment, $mentionedUserIds)
199 385 {
200 - $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 + }));
201 391
202 - $uniqueIds = array_filter($uniqueIds, function($value) {
203 - return (int)$value !== get_current_user_id();
204 - });
392 + if (!$uniqueIds) {
393 + return;
394 + }
205 395
206 - //sending emails to mentioned users
207 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
208 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
396 + // Queued delivery revalidates membership and preferences before sending.
397 + $this->sendMailAfterMention($comment->id, $uniqueIds);
209 398
210 399 //sending desktop notifications
211 400 do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
212 401 }
213 402
214 - public function sendMailAfterMention($commentId, $usersToSendEmail)
403 + public function sendMailAfterMention($commentId, $recipientUserIds)
215 404 {
405 + if (empty($recipientUserIds)) {
406 + return;
407 + }
408 +
216 409 $current_user_id = get_current_user_id();
217 410
218 411 /* this will run in background as soon as possible */
219 412 /* sending Model or Model Instance won't work here */
220 - 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');
221 414 }
222 415
223 416 public function getUnreadNotificationsOfTasks($task)
224 417 {