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.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/Http/Controllers/OptionsController.php +92 -14 1.95.2 → 2.2.0 View file →
@@ -8,8 +8,9 @@
8 8 use FluentBoards\App\Models\User;
9 9 use FluentBoards\App\Models\Board;
10 10 use FluentBoards\App\Models\Relation;
11 11 use FluentBoards\App\Services\BoardService;
12 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
12 13 use FluentBoards\App\Services\Helper;
13 14 use FluentBoards\App\Services\OptionService;
14 15 use FluentBoards\App\Services\Constant;
15 16 use FluentBoards\App\Services\PermissionManager;
@@ -55,11 +56,44 @@
55 56
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
60 + if ('users' === $optionKey && $options) {
61 + // Resolve plugin admins once for this result set instead of querying per user.
62 + $adminIds = Meta::where('object_type', Constant::FLUENT_BOARD_ADMIN)
63 + ->whereIn('object_id', array_column($options, 'id'))
64 + ->pluck('object_id')
65 + ->toArray();
66 + $adminLookup = array_fill_keys($adminIds, true);
59 67
68 + foreach ($options as &$option) {
69 + $option['is_admin'] = isset($adminLookup[$option['id']])
70 + || PermissionManager::isWPAdmin($option['id']);
71 + }
72 + unset($option);
73 + }
74 +
75 +
76 + } elseif ('board_create_users' === $optionKey) {
77 + if (!PermissionManager::userHasBoardCreationPermission()) {
78 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
79 + }
80 +
81 + if (!defined('FLUENT_BOARDS_PRO')) {
82 + // Bound the lookup: this popover searches on focus (empty query too).
83 + $users = PermissionManager::getAll_WP_Admins($search, 20);
84 + } else {
85 + $users = Helper::searchWordPressUsers($search);
86 + }
87 +
88 + $options = $this->addUserDataAsSelectorOption($users);
89 + // Board creation can be delegated to members without `list_users`;
90 + // don't leak full account emails to them.
91 + $options = $this->maskSelectorEmailsForViewer($options);
92 +
60 93 } elseif ('boards' === $optionKey) {
61 94 $boards = Board::query()
95 + ->byAccessUser(get_current_user_id())
62 96 ->when($search, function ($query) use ($search) {
63 97 // $search is already sanitized with sanitize_text_field above
64 98 return $query->where('title', 'LIKE', '%' . $search . '%');
65 99 })->take(20)->get();
@@ -100,9 +134,9 @@
100 134 throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
101 135 }
102 136
103 137 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
104 - $options = $this->addUserDataAsSelectorOption($users);
138 + $options = $this->maskSelectorEmailsForViewer($this->addUserDataAsSelectorOption($users));
105 139 } else {
106 140 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
107 141 }
108 142
@@ -128,8 +162,42 @@
128 162 }
129 163 return $options;
130 164 }
131 165
166 + /**
167 + * Obfuscate emails in selector options for viewers who lack the `list_users`
168 + * capability, keeping the current user's own email visible.
169 + */
170 + private function maskSelectorEmailsForViewer($options)
171 + {
172 + if (current_user_can('list_users')) {
173 + return $options;
174 + }
175 +
176 + $currentUser = wp_get_current_user();
177 + $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
178 +
179 + foreach ($options as $index => $option) {
180 + $email = $option['email'] ?? '';
181 +
182 + if ($email === '' || $email === $currentUserEmail) {
183 + continue;
184 + }
185 +
186 + $maskedEmail = Helper::obfuscateEmail($email);
187 +
188 + // When there's no display name the raw email doubles as the name.
189 + if (($option['name'] ?? '') === $email) {
190 + $options[$index]['name'] = $maskedEmail;
191 + }
192 +
193 + $options[$index]['email'] = $maskedEmail;
194 + $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
195 + }
196 +
197 + return $options;
198 + }
199 +
132 200 public function getCurrentUserPermissions()
133 201 {
134 202 try {
135 203 $currentUserBoards = Relation::query()
@@ -412,21 +480,29 @@
412 480 }
413 481
414 482 $boardId = $request->getSafe('boardId', 'intval');
415 483
416 - $memberUserIds = Relation::where('object_type', 'board_user')
484 + if ($boardId && !PermissionManager::userHasPermission($boardId)) {
485 + return $this->sendError([
486 + 'message' => __('You do not have permission to access this route', 'fluent-boards')
487 + ], 403);
488 + }
489 +
490 + $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
417 491 ->select(['foreign_id'])
418 492 ->groupBy('foreign_id');
419 493
420 494 if ($boardId) {
421 - $memberUserIds = $memberUserIds->where('object_id', $boardId);
495 + $memberUserIdsQuery->where('object_id', $boardId);
496 + } elseif (!PermissionManager::isAdmin()) {
497 + $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser()));
498 + $memberUserIdsQuery->whereIn('object_id', $boardIds);
422 499 }
423 500
424 - $members = [];
425 -
426 - $memberUserIds = $memberUserIds->get()
501 + $memberUserIds = $memberUserIdsQuery->get()
427 502 ->pluck('foreign_id')->toArray();
428 503
504 + $members = [];
429 505
430 506 if ($memberUserIds) {
431 507 $memberUsers = get_users([
432 508 'include' => $memberUserIds
@@ -476,9 +552,9 @@
476 552 'members' => $members
477 553 ];
478 554 }
479 555
480 - public function quickSearch()
556 + public function globalSearch()
481 557 {
482 558 $currentUserId = get_current_user_id();
483 559
484 560 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
@@ -492,8 +568,9 @@
492 568 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
493 569 $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
494 570 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
495 571 $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
572 + $perPage = max(1, min(100, $perPage));
496 573
497 574 // Build base queries
498 575 $firstThreeChars = substr($query, 0, 3);
499 576 $firstNineChars = substr($query, 0, 9);
@@ -558,9 +635,9 @@
558 635 $formattedBoards[] = [
559 636 'type' => 'board',
560 637 'id' => $board->id,
561 638 'title' => $board->title,
562 - 'description' => $board->description,
639 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
563 640 ];
564 641 }
565 642 foreach ($tasks as $task) {
566 643 if (!in_array($task->board_id, $allActiveBoardsIds)) {
@@ -571,9 +648,9 @@
571 648 $formattedTasks[] = [
572 649 'type' => 'task',
573 650 'id' => $task->id,
574 651 'title' => $task->title,
575 - 'description' => $task->description,
652 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
576 653 'board_id' => $task->board_id,
577 654 'board' => [
578 655 'id' => $board->id,
579 656 'title' => $board->title,
@@ -669,12 +746,12 @@
669 746 $canAutoInstallKit = $this->canAutoInstallFluentKit();
670 747 $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
671 748 $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
672 749 $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
673 - $kitActionText = __('Get FluentKit from GitHub', 'fluent-boards');
750 + $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
674 751
675 752 if ($canAutoInstallKit) {
676 - $kitActionText = $kitPluginExists ? __('Activate FluentKit', 'fluent-boards') : __('Install FluentKit', 'fluent-boards');
753 + $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
677 754 }
678 755
679 756 $addOns = [
680 757 'fluent-crm' => [
@@ -718,9 +795,9 @@
718 795 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
719 796 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
720 797 ],
721 798 'fluent-toolkit' => [
722 - 'title' => __('FluentKit', 'fluent-boards'),
799 + 'title' => __('FluentHub', 'fluent-boards'),
723 800 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
724 801 'is_installed' => $kitLoaded,
725 802 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
726 803 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
@@ -727,9 +804,9 @@
727 804 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
728 805 'action_text' => $kitActionText,
729 806 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
730 807 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
731 - 'description' => __('Fluent Boards MCP tools become available after FluentKit is installed and active.', 'fluent-boards'),
808 + 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
732 809 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
733 810 ],
734 811 ];
735 812
@@ -1004,9 +1081,10 @@
1004 1081 $pages = [];
1005 1082 foreach ($allPages as $page) {
1006 1083 $pages[] = [
1007 1084 'id' => $page->ID,
1008 - 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards')
1085 + 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1086 + 'url' => esc_url_raw(get_permalink($page->ID))
1009 1087 ];
1010 1088 }
1011 1089
1012 1090 return $this->sendSuccess([