| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentBoards\App\Services; |
| 4 | 4 | |
| 5 | +use Exception; | |
| 5 | 6 | use FluentBoards\App\Models\Activity; |
| 6 | 7 | use FluentBoards\App\Models\Meta; |
| 7 | 8 | use FluentBoards\App\Models\Notification; |
| 8 | 9 | use FluentBoards\App\Models\Task; |
| @@ -13,8 +14,214 @@ | ||
| 13 | 14 | |
| 14 | 15 | |
| 15 | 16 | class UserService |
| 16 | 17 | { |
| 18 | + /** Save only the current member's public display name. */ | |
| 19 | + public function updateDisplayName($userId, $displayName) | |
| 20 | + { | |
| 21 | + $userId = absint($userId); | |
| 22 | + $this->assertCanUpdateProfile($userId); | |
| 23 | + $displayName = is_string($displayName) ? trim(sanitize_text_field($displayName)) : ''; | |
| 24 | + if ($displayName === '') { | |
| 25 | + throw new Exception(__('Display name is required', 'fluent-boards'), 400); | |
| 26 | + } | |
| 27 | + // WordPress treats "0" as empty and replaces it with the login name. | |
| 28 | + if ($displayName === '0') { | |
| 29 | + throw new Exception(__('Display name cannot be "0"', 'fluent-boards'), 400); | |
| 30 | + } | |
| 31 | + | |
| 32 | + if (mb_strlen($displayName, 'UTF-8') > Constant::PROFILE_DISPLAY_NAME_MAX_LENGTH) { | |
| 33 | + throw new Exception(__('Display name must be 250 characters or fewer', 'fluent-boards'), 400); | |
| 34 | + } | |
| 35 | + | |
| 36 | + $expectedName = null; | |
| 37 | + // Validate the final, unslashed storage value after WordPress and plugin filters. | |
| 38 | + $validateStorage = function ($data, $update, $targetUserId) use ($userId, &$expectedName) { | |
| 39 | + if (!$update || (int) $targetUserId !== $userId) { | |
| 40 | + return $data; | |
| 41 | + } | |
| 42 | + $expectedName = $data['display_name'] ?? null; | |
| 43 | + if (!is_string($expectedName) || $expectedName === '') { | |
| 44 | + throw new Exception(__('Display name is required', 'fluent-boards'), 400); | |
| 45 | + } | |
| 46 | + if (mb_strlen($expectedName, 'UTF-8') > Constant::PROFILE_DISPLAY_NAME_MAX_LENGTH) { | |
| 47 | + throw new Exception(__('Display name must be 250 characters or fewer', 'fluent-boards'), 400); | |
| 48 | + } | |
| 49 | + return $data; | |
| 50 | + }; | |
| 51 | + add_filter('wp_pre_insert_user_data', $validateStorage, PHP_INT_MAX, 3); | |
| 52 | + try { | |
| 53 | + $result = wp_update_user(['ID' => $userId, 'display_name' => wp_slash($displayName)]); | |
| 54 | + } finally { | |
| 55 | + remove_filter('wp_pre_insert_user_data', $validateStorage, PHP_INT_MAX); | |
| 56 | + } | |
| 57 | + if (is_wp_error($result)) { | |
| 58 | + throw new Exception($result->get_error_message(), 400); | |
| 59 | + } | |
| 60 | + | |
| 61 | + // WordPress can return a user ID after a failed database update. | |
| 62 | + clean_user_cache($userId); | |
| 63 | + $user = get_userdata($userId); | |
| 64 | + if (!$user) { | |
| 65 | + throw new Exception(__('Could not load the updated profile', 'fluent-boards'), 400); | |
| 66 | + } | |
| 67 | + if ($expectedName === null || $user->display_name !== $expectedName) { | |
| 68 | + throw new Exception(__('Could not save the display name. Please try again.', 'fluent-boards'), 400); | |
| 69 | + } | |
| 70 | + $displayName = $user->display_name; | |
| 71 | + do_action('fluent_boards/profile_name_updated', $userId, $displayName); | |
| 72 | + return ['display_name' => $displayName]; | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** Validate and store a custom avatar without requiring media-library access. */ | |
| 76 | + public function updateProfilePhoto($userId, $file) | |
| 77 | + { | |
| 78 | + $userId = absint($userId); | |
| 79 | + $this->assertCanUpdateProfile($userId); | |
| 80 | + if (!is_array($file) || !isset($file['error'], $file['size'], $file['tmp_name'], $file['name']) | |
| 81 | + || $file['error'] !== UPLOAD_ERR_OK || !is_string($file['tmp_name']) || !is_string($file['name']) | |
| 82 | + || !is_uploaded_file($file['tmp_name'])) { | |
| 83 | + throw new Exception(__('No photo uploaded or upload error', 'fluent-boards'), 400); | |
| 84 | + } | |
| 85 | + if (filesize($file['tmp_name']) <= 0 || filesize($file['tmp_name']) > Constant::PROFILE_PHOTO_MAX_BYTES) { | |
| 86 | + throw new Exception(__('Photo must be 2 MB or smaller', 'fluent-boards'), 400); | |
| 87 | + } | |
| 88 | + | |
| 89 | + $mimes = ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp']; | |
| 90 | + // Inspect the file contents, not the browser MIME or filename alone. | |
| 91 | + if (!in_array(wp_get_image_mime($file['tmp_name']), $mimes, true)) { | |
| 92 | + throw new Exception(__('Choose a JPG, PNG, GIF or WebP image', 'fluent-boards'), 400); | |
| 93 | + } | |
| 94 | + $dimensions = wp_getimagesize($file['tmp_name']); | |
| 95 | + if (!$dimensions || $dimensions[0] < 1 || $dimensions[1] < 1 | |
| 96 | + || max($dimensions[0], $dimensions[1]) > Constant::PROFILE_PHOTO_MAX_DIMENSION | |
| 97 | + || $dimensions[0] * $dimensions[1] > Constant::PROFILE_PHOTO_MAX_PIXELS) { | |
| 98 | + throw new Exception(__('Photo dimensions must not exceed 4096 pixels per side or 12 megapixels', 'fluent-boards')); | |
| 99 | + } | |
| 100 | + if (!function_exists('wp_handle_upload')) { | |
| 101 | + require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 102 | + } | |
| 103 | + $uploaded = wp_handle_upload($file, ['test_form' => false, 'mimes' => $mimes]); | |
| 104 | + if (isset($uploaded['error'])) { | |
| 105 | + throw new Exception($uploaded['error'], 400); | |
| 106 | + } | |
| 107 | + | |
| 108 | + $uploaded = $this->createProfileAvatar($uploaded); | |
| 109 | + $this->replaceProfilePhoto($userId, $uploaded); | |
| 110 | + $photoUrl = esc_url_raw($uploaded['url']); | |
| 111 | + do_action('fluent_boards/profile_photo_updated', $userId, $photoUrl); | |
| 112 | + return ['photo' => fluent_boards_user_avatar(wp_get_current_user()->user_email)]; | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** Re-encode a bounded avatar and discard the source before persisting profile changes. */ | |
| 116 | + private function createProfileAvatar(array $uploaded) | |
| 117 | + { | |
| 118 | + $directory = dirname($uploaded['file']); | |
| 119 | + $filename = wp_unique_filename($directory, pathinfo($uploaded['file'], PATHINFO_FILENAME) . '-avatar.png'); | |
| 120 | + $avatarPath = $directory . '/' . $filename; | |
| 121 | + | |
| 122 | + try { | |
| 123 | + $editor = wp_get_image_editor($uploaded['file']); | |
| 124 | + if (is_wp_error($editor)) { | |
| 125 | + throw new Exception($editor->get_error_message()); | |
| 126 | + } | |
| 127 | + $size = $editor->get_size(); | |
| 128 | + if (max($size['width'], $size['height']) > Constant::PROFILE_AVATAR_SIZE) { | |
| 129 | + $resized = $editor->resize(Constant::PROFILE_AVATAR_SIZE, Constant::PROFILE_AVATAR_SIZE, false); | |
| 130 | + if (is_wp_error($resized)) { | |
| 131 | + throw new Exception($resized->get_error_message()); | |
| 132 | + } | |
| 133 | + } | |
| 134 | + $avatar = $editor->save($avatarPath, 'image/png'); | |
| 135 | + if (is_wp_error($avatar)) { | |
| 136 | + throw new Exception($avatar->get_error_message()); | |
| 137 | + } | |
| 138 | + | |
| 139 | + return [ | |
| 140 | + 'file' => $avatar['path'], | |
| 141 | + 'url' => trailingslashit(dirname($uploaded['url'])) . wp_basename($avatar['path']), | |
| 142 | + 'type' => $avatar['mime-type'], | |
| 143 | + ]; | |
| 144 | + } catch (Exception $e) { | |
| 145 | + wp_delete_file($avatarPath); | |
| 146 | + throw $e; | |
| 147 | + } finally { | |
| 148 | + wp_delete_file($uploaded['file']); | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** Serialize replacement across requests sharing the same WordPress user table. */ | |
| 153 | + private function replaceProfilePhoto($userId, array $uploaded) | |
| 154 | + { | |
| 155 | + global $wpdb; | |
| 156 | + | |
| 157 | + $lockName = Constant::USER_PROFILE_PHOTO_UPLOAD . ':' . md5(DB_NAME . ':' . $wpdb->users . ':' . $userId); | |
| 158 | + if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lockName)) !== 1) { | |
| 159 | + wp_delete_file($uploaded['file']); | |
| 160 | + throw new Exception(__('The profile photo is being updated. Please try again.', 'fluent-boards')); | |
| 161 | + } | |
| 162 | + | |
| 163 | + try { | |
| 164 | + // A waiting request may have cached metadata from before the previous upload. | |
| 165 | + wp_cache_delete($userId, 'user_meta'); | |
| 166 | + $this->persistProfilePhoto($userId, $uploaded); | |
| 167 | + } finally { | |
| 168 | + $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lockName)); | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** Track uploads we own; legacy or externally supplied avatar URLs never authorize deletion. */ | |
| 173 | + private function persistProfilePhoto($userId, array $uploaded) | |
| 174 | + { | |
| 175 | + $previousUrl = get_user_meta($userId, Constant::USER_PROFILE_PHOTO, true); | |
| 176 | + $previousUpload = get_user_meta($userId, Constant::USER_PROFILE_PHOTO_UPLOAD, true); | |
| 177 | + $ownedUpload = [ | |
| 178 | + 'file' => $uploaded['file'], | |
| 179 | + 'url' => esc_url_raw($uploaded['url']), | |
| 180 | + 'blog_id' => get_current_blog_id(), | |
| 181 | + ]; | |
| 182 | + | |
| 183 | + if (!update_user_meta($userId, Constant::USER_PROFILE_PHOTO_UPLOAD, $ownedUpload)) { | |
| 184 | + wp_delete_file($uploaded['file']); | |
| 185 | + throw new Exception(__('The profile photo could not be saved. Please try again.', 'fluent-boards')); | |
| 186 | + } | |
| 187 | + if (!update_user_meta($userId, Constant::USER_PROFILE_PHOTO, $ownedUpload['url'])) { | |
| 188 | + if ($previousUpload) { | |
| 189 | + update_user_meta($userId, Constant::USER_PROFILE_PHOTO_UPLOAD, $previousUpload); | |
| 190 | + } else { | |
| 191 | + delete_user_meta($userId, Constant::USER_PROFILE_PHOTO_UPLOAD); | |
| 192 | + } | |
| 193 | + wp_delete_file($uploaded['file']); | |
| 194 | + throw new Exception(__('The profile photo could not be saved. Please try again.', 'fluent-boards')); | |
| 195 | + } | |
| 196 | + | |
| 197 | + // Only a matching, locally tracked upload can be retired after both writes succeed. | |
| 198 | + if (!is_array($previousUpload) | |
| 199 | + || ($previousUpload['url'] ?? null) !== $previousUrl | |
| 200 | + || ($previousUpload['blog_id'] ?? null) !== get_current_blog_id() | |
| 201 | + || !is_string($previousUpload['file'] ?? null) | |
| 202 | + || $previousUpload['file'] === $ownedUpload['file']) { | |
| 203 | + return; | |
| 204 | + } | |
| 205 | + | |
| 206 | + $uploads = wp_get_upload_dir(); | |
| 207 | + $previousFile = realpath($previousUpload['file']); | |
| 208 | + $uploadsDirectory = realpath($uploads['basedir']); | |
| 209 | + if (!$uploads['error'] && $previousFile && $uploadsDirectory | |
| 210 | + && strpos(wp_normalize_path($previousFile), trailingslashit(wp_normalize_path($uploadsDirectory))) === 0 | |
| 211 | + && is_file($previousFile)) { | |
| 212 | + wp_delete_file($previousFile); | |
| 213 | + } | |
| 214 | + } | |
| 215 | + | |
| 216 | + /** Profile editing requires Fluent Boards membership and remains self-service for administrators. */ | |
| 217 | + private function assertCanUpdateProfile($userId) | |
| 218 | + { | |
| 219 | + if (!$userId || $userId !== get_current_user_id() || !PermissionManager::isFluentBoardsUser($userId)) { | |
| 220 | + throw new Exception(__('You can only update your own profile', 'fluent-boards'), 403); | |
| 221 | + } | |
| 222 | + } | |
| 223 | + | |
| 17 | 224 | public function allFluentBoardsUsers($boardId = null) |
| 18 | 225 | { |
| 19 | 226 | // Get the global admins first |
| 20 | 227 | $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN) |
| @@ -108,20 +315,46 @@ | ||
| 108 | 315 | |
| 109 | 316 | return $boardUsers; |
| 110 | 317 | } |
| 111 | 318 | |
| 319 | + /** | |
| 320 | + * Return associated users and roles only from boards shared with the requester. | |
| 321 | + */ | |
| 112 | 322 | public function memberAssociatedTaskUsers($userId) |
| 113 | 323 | { |
| 114 | 324 | $user = User::find($userId); |
| 115 | - $boards = $user->whichBoards->pluck('id'); | |
| 116 | - $boardUsers = Relation::whereIn('object_id', $boards) | |
| 325 | + $memberBoardIds = array_values(array_unique(array_filter(array_map( | |
| 326 | + 'intval', | |
| 327 | + $user->whichBoards->pluck('id')->toArray() | |
| 328 | + )))); | |
| 329 | + $requesterBoardIds = array_values(array_unique(array_filter(array_map( | |
| 330 | + 'intval', | |
| 331 | + PermissionManager::getBoardIdsForUser(get_current_user_id()) | |
| 332 | + )))); | |
| 333 | + $sharedBoardIds = array_values(array_intersect($memberBoardIds, $requesterBoardIds)); | |
| 334 | + | |
| 335 | + if (!$sharedBoardIds) { | |
| 336 | + return [ | |
| 337 | + 'uniqueUsers' => [], | |
| 338 | + 'userWiseBoardDesignation' => [], | |
| 339 | + ]; | |
| 340 | + } | |
| 341 | + | |
| 342 | + $boardUsers = Relation::whereIn('object_id', $sharedBoardIds) | |
| 117 | 343 | ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER) |
| 118 | 344 | ->pluck('foreign_id')->toArray(); |
| 119 | 345 | $uniqueUsersIds = array_unique($boardUsers); |
| 120 | - $uniqueUsers = User::whereIn('ID', $uniqueUsersIds)->with('whichBoards')->get(); | |
| 346 | + $boardTable = (new Board())->getTable(); | |
| 347 | + $uniqueUsers = User::whereIn('ID', $uniqueUsersIds) | |
| 348 | + ->with(['whichBoards' => function ($query) use ($boardTable, $sharedBoardIds) { | |
| 349 | + $query->whereIn($boardTable . '.id', $sharedBoardIds); | |
| 350 | + }]) | |
| 351 | + ->get(); | |
| 121 | 352 | |
| 122 | 353 | $userWiseBoardDesignation = Relation::query()->whereIn('foreign_id', $uniqueUsersIds) |
| 123 | - ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)->get(); | |
| 354 | + ->whereIn('object_id', $sharedBoardIds) | |
| 355 | + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER) | |
| 356 | + ->get(); | |
| 124 | 357 | |
| 125 | 358 | $data = array(); |
| 126 | 359 | $data['userWiseBoardDesignation'] = $userWiseBoardDesignation; |
| 127 | 360 | |
| @@ -135,9 +368,9 @@ | ||
| 135 | 368 | } elseif (PermissionManager::isFluentBoardsAdmin($uniqueUser['ID'])) { |
| 136 | 369 | $uniqueUser['is_super'] = true; |
| 137 | 370 | $uniqueUser['is_wpadmin'] = false; |
| 138 | 371 | } else { |
| 139 | - $uniqueUser['all_boards'] = Arr::get($uniqueUser, 'boards'); | |
| 372 | + $uniqueUser['all_boards'] = $uniqueUser->getRelation('whichBoards'); | |
| 140 | 373 | $uniqueUser['is_super'] = false; |
| 141 | 374 | $uniqueUser['is_wpadmin'] = false; |
| 142 | 375 | } |
| 143 | 376 | } |
| @@ -220,10 +453,10 @@ | ||
| 220 | 453 | $taskType = $requestData['taskType'] ?? null; |
| 221 | 454 | $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : []; |
| 222 | 455 | $orderBy = $requestData['orderBy'] ?? 'created_at'; |
| 223 | 456 | $order = strtoupper($requestData['order'] ?? 'ASC'); |
| 224 | - $per_page = $requestData['per_page'] ?? 15; | |
| 225 | - $page = $requestData['page'] ?? 1; | |
| 457 | + $per_page = Sanitize::perPage($requestData['per_page'] ?? 15, 50); | |
| 458 | + $page = max(1, (int) ($requestData['page'] ?? 1)); | |
| 226 | 459 | $user = User::find($user_id); |
| 227 | 460 | |
| 228 | 461 | $loggedInUserId = get_current_user_id(); |
| 229 | 462 | |
| @@ -243,42 +476,55 @@ | ||
| 243 | 476 | |
| 244 | 477 | // Table view needs the labels column; list view ignores the extra relation. |
| 245 | 478 | $taskRelations = ['stage', 'board', 'labels']; |
| 246 | 479 | |
| 247 | - if($taskType == 'assigned') { | |
| 248 | - $tasksQuery = $user->assignedTasks()->with($taskRelations)->whereNull('archived_at')->whereNull('parent_id')->whereIn('board_id', $allowedBoardIds); | |
| 249 | - } else if($taskType == 'mentioned') { | |
| 250 | - $tasksQuery = $user->mentionedTasks()->with($taskRelations)->whereNull('archived_at')->whereNull('parent_id')->whereIn('board_id', $allowedBoardIds); | |
| 480 | + if ($taskType == 'assigned') { | |
| 481 | + $tasksQuery = $user->assignedTasks() | |
| 482 | + ->with($taskRelations) | |
| 483 | + ->whereNull('archived_at') | |
| 484 | + ->whereNull('parent_id') | |
| 485 | + ->whereIn('board_id', $allowedBoardIds) | |
| 486 | + ->where('status', '!=', 'closed') | |
| 487 | + ->onActiveAvailableBoards(); | |
| 488 | + } else if ($taskType == 'mentioned') { | |
| 489 | + $tasksQuery = $user->mentionedTasks() | |
| 490 | + ->with($taskRelations) | |
| 491 | + ->whereNull('archived_at') | |
| 492 | + ->whereNull('parent_id') | |
| 493 | + ->whereIn('board_id', $allowedBoardIds) | |
| 494 | + ->onActiveAvailableBoards(); | |
| 251 | 495 | } else { |
| 252 | - // Get the task assigned to the user | |
| 253 | - $tasksQuery = $user->tasks()->with($taskRelations)->whereNull('archived_at') ->whereIn('board_id', $allowedBoardIds); | |
| 496 | + // Watched tasks power the date-based and completed profile tabs. | |
| 497 | + $tasksQuery = $user->tasks() | |
| 498 | + ->with($taskRelations) | |
| 499 | + ->whereNull('archived_at') | |
| 500 | + ->whereIn('board_id', $allowedBoardIds) | |
| 501 | + ->onActiveAvailableBoards(); | |
| 254 | 502 | |
| 255 | - switch ($taskType) { | |
| 256 | - case 'upcoming': | |
| 257 | - $tasksQuery->upcoming() | |
| 258 | - ->whereIn('board_id', $allowedBoardIds); | |
| 259 | - break; | |
| 260 | - case 'due_today': | |
| 261 | - $tasksQuery->dueToday() | |
| 262 | - ->whereIn('board_id', $allowedBoardIds); | |
| 263 | - break; | |
| 264 | - case 'overdue': | |
| 265 | - $tasksQuery->overdue() | |
| 266 | - ->whereIn('board_id', $allowedBoardIds); | |
| 267 | - break; | |
| 268 | - case 'completed': | |
| 269 | - $tasksQuery->where('status', 'closed') | |
| 270 | - ->whereIn('board_id', $allowedBoardIds); | |
| 271 | - break; | |
| 272 | - default: | |
| 273 | - $tasksQuery->whereNull('due_at') | |
| 274 | - ->whereIn('board_id', $allowedBoardIds); | |
| 275 | - break; | |
| 503 | + switch ($taskType) { | |
| 504 | + case 'upcoming': | |
| 505 | + $tasksQuery->upcoming() | |
| 506 | + ->whereIn('board_id', $allowedBoardIds); | |
| 507 | + break; | |
| 508 | + case 'due_today': | |
| 509 | + $tasksQuery->dueToday() | |
| 510 | + ->whereIn('board_id', $allowedBoardIds); | |
| 511 | + break; | |
| 512 | + case 'overdue': | |
| 513 | + $tasksQuery->overdue() | |
| 514 | + ->whereIn('board_id', $allowedBoardIds); | |
| 515 | + break; | |
| 516 | + case 'completed': | |
| 517 | + $tasksQuery->where('status', 'closed') | |
| 518 | + ->whereIn('board_id', $allowedBoardIds); | |
| 519 | + break; | |
| 520 | + default: | |
| 521 | + $tasksQuery->whereNull('due_at') | |
| 522 | + ->whereIn('board_id', $allowedBoardIds); | |
| 523 | + break; | |
| 524 | + } | |
| 276 | 525 | } |
| 277 | - } | |
| 278 | 526 | |
| 279 | - | |
| 280 | - | |
| 281 | 527 | $currentUserId = get_current_user_id(); |
| 282 | 528 | if ($currentUserId != $user->ID && !PermissionManager::isAdmin()) { |
| 283 | 529 | $currentUser = User::find($currentUserId); |
| 284 | 530 | $currentUserBoardIds = $currentUser->boards->pluck('id')->toArray(); |
| @@ -352,9 +598,10 @@ | ||
| 352 | 598 | // Keep the count queries aligned with the profile list without loading task models or relations. |
| 353 | 599 | $applyTaskScope = function ($query) use ($allowedBoardIds, $boardIds) { |
| 354 | 600 | $query->whereNull('archived_at') |
| 355 | 601 | ->whereNull('parent_id') |
| 356 | - ->whereIn('board_id', $allowedBoardIds); | |
| 602 | + ->whereIn('board_id', $allowedBoardIds) | |
| 603 | + ->onActiveAvailableBoards(); | |
| 357 | 604 | |
| 358 | 605 | if (!empty($boardIds)) { |
| 359 | 606 | $query->whereIn('board_id', $boardIds); |
| 360 | 607 | } |
| @@ -366,9 +613,11 @@ | ||
| 366 | 613 | }; |
| 367 | 614 | |
| 368 | 615 | return [ |
| 369 | 616 | 'due_today' => (int) $watchedTasks()->dueToday()->count(), |
| 370 | - 'assigned' => (int) $applyTaskScope($user->assignedTasks())->count(), | |
| 617 | + 'assigned' => (int) $applyTaskScope($user->assignedTasks()) | |
| 618 | + ->where('status', '!=', 'closed') | |
| 619 | + ->count(), | |
| 371 | 620 | 'upcoming' => (int) $watchedTasks()->upcoming()->count(), |
| 372 | 621 | 'overdue' => (int) $watchedTasks()->overdue()->count(), |
| 373 | 622 | 'mentioned' => (int) $applyTaskScope($user->mentionedTasks())->count(), |
| 374 | 623 | 'completed' => (int) $watchedTasks()->where('status', 'closed')->count(), |
| @@ -376,38 +625,91 @@ | ||
| 376 | 625 | ]; |
| 377 | 626 | } |
| 378 | 627 | |
| 379 | 628 | |
| 629 | + /** | |
| 630 | + * Get a member's activities scoped to boards the requesting user can access. | |
| 631 | + */ | |
| 380 | 632 | public function getMemberRelatedAcitivies($user_id, $page) |
| 381 | 633 | { |
| 382 | - $activities = Activity::query()->where('created_by', $user_id) | |
| 634 | + $perPage = 40; | |
| 635 | + $user_id = absint($user_id); | |
| 636 | + $page = max(1, absint($page)); | |
| 637 | + $allowedBoardIds = array_values(array_filter(array_map( | |
| 638 | + 'intval', | |
| 639 | + PermissionManager::getBoardIdsForUser(get_current_user_id()) | |
| 640 | + ))); | |
| 641 | + | |
| 642 | + if (empty($allowedBoardIds)) { | |
| 643 | + return [ | |
| 644 | + 'activities' => [], | |
| 645 | + 'pagination' => $this->getEmptyPaginationInfo($page, $perPage), | |
| 646 | + ]; | |
| 647 | + } | |
| 648 | + | |
| 649 | + $allowedTaskIds = Task::query() | |
| 650 | + ->select('id') | |
| 651 | + ->whereIn('board_id', $allowedBoardIds); | |
| 652 | + | |
| 653 | + $activities = Activity::query() | |
| 654 | + ->where('created_by', $user_id) | |
| 655 | + ->where(function ($query) use ($allowedBoardIds, $allowedTaskIds) { | |
| 656 | + $query->where(function ($boardQuery) use ($allowedBoardIds) { | |
| 657 | + $boardQuery->where('object_type', Constant::ACTIVITY_BOARD) | |
| 658 | + ->whereIn('object_id', $allowedBoardIds); | |
| 659 | + })->orWhere(function ($taskQuery) use ($allowedTaskIds) { | |
| 660 | + $taskQuery->where('object_type', Constant::ACTIVITY_TASK) | |
| 661 | + ->whereIn('object_id', $allowedTaskIds); | |
| 662 | + }); | |
| 663 | + }) | |
| 383 | 664 | ->orderBy('created_at', 'desc') |
| 384 | - ->with('user')->paginate(40, ['*'], 'page', $page); | |
| 665 | + ->with('user') | |
| 666 | + ->paginate($perPage, ['*'], 'page', $page); | |
| 385 | 667 | |
| 386 | - $activitiesToShow = array(); | |
| 668 | + $activitiesToShow = []; | |
| 387 | 669 | |
| 388 | 670 | foreach ($activities as $activity) { |
| 389 | 671 | if ($activity->object_type == Constant::ACTIVITY_BOARD) { |
| 390 | 672 | $activity->load('board'); |
| 391 | -// if($activity->settings && $activity->settings['task_id']){ | |
| 392 | -// $activity->task = Task::findOrFail($activity->settings['task_id']); | |
| 393 | -// } | |
| 394 | - if (PermissionManager::userHasPermission($activity->board_id, get_current_user_id())) { | |
| 395 | - $activitiesToShow[] = $activity; | |
| 396 | - } | |
| 673 | + $activitiesToShow[] = $activity; | |
| 397 | 674 | } elseif ($activity->object_type == Constant::ACTIVITY_TASK) { |
| 398 | 675 | $activity->load('task'); |
| 399 | - if ($activity->task && PermissionManager::userHasPermission($activity->task->board_id, get_current_user_id())) { | |
| 400 | - $activitiesToShow[] = $activity; | |
| 401 | - } | |
| 676 | + $activitiesToShow[] = $activity; | |
| 402 | 677 | } |
| 403 | 678 | } |
| 679 | + | |
| 404 | 680 | return [ |
| 405 | - 'activities' => $activitiesToShow, | |
| 406 | - 'pagination' => $activities->toArray(), | |
| 681 | + 'activities' => $activitiesToShow, | |
| 682 | + 'pagination' => $this->getPaginationInfo($activities), | |
| 407 | 683 | ]; |
| 408 | 684 | } |
| 409 | 685 | |
| 686 | + /** | |
| 687 | + * Return pagination metadata without duplicating serialized row data. | |
| 688 | + */ | |
| 689 | + private function getPaginationInfo($paginator) | |
| 690 | + { | |
| 691 | + return [ | |
| 692 | + 'current_page' => $paginator->currentPage(), | |
| 693 | + 'last_page' => $paginator->lastPage(), | |
| 694 | + 'per_page' => (int) $paginator->perPage(), | |
| 695 | + 'total' => $paginator->total(), | |
| 696 | + ]; | |
| 697 | + } | |
| 698 | + | |
| 699 | + /** | |
| 700 | + * Return an empty pagination payload for callers without accessible boards. | |
| 701 | + */ | |
| 702 | + private function getEmptyPaginationInfo($page, $perPage) | |
| 703 | + { | |
| 704 | + return [ | |
| 705 | + 'current_page' => max(1, (int) $page), | |
| 706 | + 'last_page' => 1, | |
| 707 | + 'per_page' => (int) $perPage, | |
| 708 | + 'total' => 0, | |
| 709 | + ]; | |
| 710 | + } | |
| 711 | + | |
| 410 | 712 | private function getTaskById($taskId) |
| 411 | 713 | { |
| 412 | 714 | return Task::findOrFail($taskId); |
| 413 | 715 | } |
| @@ -416,25 +718,23 @@ | ||
| 416 | 718 | { |
| 417 | 719 | if(!$user_id) { |
| 418 | 720 | return []; |
| 419 | 721 | } |
| 420 | - $boardIds = []; | |
| 421 | - $boards = []; | |
| 422 | 722 | $user = User::find($user_id); |
| 423 | 723 | $currentUserId = get_current_user_id(); |
| 424 | - if($currentUserId != $user->ID) { | |
| 425 | - if(!PermissionManager::isAdmin()){ | |
| 426 | - $currentUser = User::find($currentUserId); | |
| 427 | - $currentUserBoardIds = $currentUser->whichBoards->pluck('id')->toArray(); | |
| 428 | - $boardIds = $currentUserBoardIds; | |
| 429 | - } | |
| 724 | + | |
| 725 | + if ($currentUserId == $user->ID || PermissionManager::isAdmin($currentUserId)) { | |
| 726 | + return $user->whichBoards; | |
| 430 | 727 | } |
| 431 | - if (!empty($boardIds)) { | |
| 432 | - $boards = $user->whichBoards()->whereIn('fbs_boards.id', $boardIds)->get(); | |
| 433 | - } else { | |
| 434 | - $boards = $user->whichBoards; | |
| 728 | + | |
| 729 | + $currentUser = User::find($currentUserId); | |
| 730 | + $boardIds = $currentUser ? $currentUser->whichBoards->pluck('id')->toArray() : []; | |
| 731 | + | |
| 732 | + if (empty($boardIds)) { | |
| 733 | + return []; | |
| 435 | 734 | } |
| 436 | - return $boards; | |
| 735 | + | |
| 736 | + return $user->whichBoards()->whereIn('fbs_boards.id', $boardIds)->get(); | |
| 437 | 737 | } |
| 438 | 738 | |
| 439 | 739 | /** |
| 440 | 740 | * Returns four aggregate counts for a member's stats widget. |