PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.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 1.41 All 42 releases
← All changes | app/Http/Controllers/OptionsController.php +118 -11 1.91.62.1.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;
@@ -56,10 +57,28 @@
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
59 60
61 + } elseif ('board_create_users' === $optionKey) {
62 + if (!PermissionManager::userHasBoardCreationPermission()) {
63 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
64 + }
65 +
66 + if (!defined('FLUENT_BOARDS_PRO')) {
67 + // Bound the lookup: this popover searches on focus (empty query too).
68 + $users = PermissionManager::getAll_WP_Admins($search, 20);
69 + } else {
70 + $users = Helper::searchWordPressUsers($search);
71 + }
72 +
73 + $options = $this->addUserDataAsSelectorOption($users);
74 + // Board creation can be delegated to members without `list_users`;
75 + // don't leak full account emails to them.
76 + $options = $this->maskSelectorEmailsForViewer($options);
77 +
60 78 } elseif ('boards' === $optionKey) {
61 79 $boards = Board::query()
80 + ->byAccessUser(get_current_user_id())
62 81 ->when($search, function ($query) use ($search) {
63 82 // $search is already sanitized with sanitize_text_field above
64 83 return $query->where('title', 'LIKE', '%' . $search . '%');
65 84 })->take(20)->get();
@@ -72,8 +91,12 @@
72 91 'right_side_value' => $board->slug,
73 92 ];
74 93 }
75 94 } elseif ('tasks' === $optionKey) {
95 + if (!PermissionManager::userHasPermission($boardId)) {
96 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
97 + }
98 +
76 99 // $boardId is already sanitized with intval above
77 100 $tasks = Task::where('board_id', $boardId)
78 101 ->whereNull('archived_at')
79 102 ->whereNull('parent_id')
@@ -91,10 +114,14 @@
91 114 ];
92 115 }
93 116
94 117 } elseif ('assigned_in_task' == $optionKey) {
118 + if (!PermissionManager::userHasPermission($boardId)) {
119 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
120 + }
121 +
95 122 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
96 - $options = $this->addUserDataAsSelectorOption($users);
123 + $options = $this->maskSelectorEmailsForViewer($this->addUserDataAsSelectorOption($users));
97 124 } else {
98 125 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
99 126 }
100 127
@@ -120,8 +147,42 @@
120 147 }
121 148 return $options;
122 149 }
123 150
151 + /**
152 + * Obfuscate emails in selector options for viewers who lack the `list_users`
153 + * capability, keeping the current user's own email visible.
154 + */
155 + private function maskSelectorEmailsForViewer($options)
156 + {
157 + if (current_user_can('list_users')) {
158 + return $options;
159 + }
160 +
161 + $currentUser = wp_get_current_user();
162 + $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
163 +
164 + foreach ($options as $index => $option) {
165 + $email = $option['email'] ?? '';
166 +
167 + if ($email === '' || $email === $currentUserEmail) {
168 + continue;
169 + }
170 +
171 + $maskedEmail = Helper::obfuscateEmail($email);
172 +
173 + // When there's no display name the raw email doubles as the name.
174 + if (($option['name'] ?? '') === $email) {
175 + $options[$index]['name'] = $maskedEmail;
176 + }
177 +
178 + $options[$index]['email'] = $maskedEmail;
179 + $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
180 + }
181 +
182 + return $options;
183 + }
184 +
124 185 public function getCurrentUserPermissions()
125 186 {
126 187 try {
127 188 $currentUserBoards = Relation::query()
@@ -404,21 +465,29 @@
404 465 }
405 466
406 467 $boardId = $request->getSafe('boardId', 'intval');
407 468
408 - $memberUserIds = Relation::where('object_type', 'board_user')
469 + if ($boardId && !PermissionManager::userHasPermission($boardId)) {
470 + return $this->sendError([
471 + 'message' => __('You do not have permission to access this route', 'fluent-boards')
472 + ], 403);
473 + }
474 +
475 + $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
409 476 ->select(['foreign_id'])
410 477 ->groupBy('foreign_id');
411 478
412 479 if ($boardId) {
413 - $memberUserIds = $memberUserIds->where('object_id', $boardId);
480 + $memberUserIdsQuery->where('object_id', $boardId);
481 + } elseif (!PermissionManager::isAdmin()) {
482 + $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser()));
483 + $memberUserIdsQuery->whereIn('object_id', $boardIds);
414 484 }
415 485
416 - $members = [];
417 -
418 - $memberUserIds = $memberUserIds->get()
486 + $memberUserIds = $memberUserIdsQuery->get()
419 487 ->pluck('foreign_id')->toArray();
420 488
489 + $members = [];
421 490
422 491 if ($memberUserIds) {
423 492 $memberUsers = get_users([
424 493 'include' => $memberUserIds
@@ -468,9 +537,9 @@
468 537 'members' => $members
469 538 ];
470 539 }
471 540
472 - public function quickSearch()
541 + public function globalSearch()
473 542 {
474 543 $currentUserId = get_current_user_id();
475 544
476 545 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
@@ -484,8 +553,9 @@
484 553 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
485 554 $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
486 555 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
487 556 $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
557 + $perPage = max(1, min(100, $perPage));
488 558
489 559 // Build base queries
490 560 $firstThreeChars = substr($query, 0, 3);
491 561 $firstNineChars = substr($query, 0, 9);
@@ -550,9 +620,9 @@
550 620 $formattedBoards[] = [
551 621 'type' => 'board',
552 622 'id' => $board->id,
553 623 'title' => $board->title,
554 - 'description' => $board->description,
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
555 625 ];
556 626 }
557 627 foreach ($tasks as $task) {
558 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
@@ -563,9 +633,9 @@
563 633 $formattedTasks[] = [
564 634 'type' => 'task',
565 635 'id' => $task->id,
566 636 'title' => $task->title,
567 - 'description' => $task->description,
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
568 638 'board_id' => $task->board_id,
569 639 'board' => [
570 640 'id' => $board->id,
571 641 'title' => $board->title,
@@ -657,8 +727,18 @@
657 727
658 728
659 729 public function getAddonsSettings()
660 730 {
731 + $canAutoInstallKit = $this->canAutoInstallFluentKit();
732 + $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
733 + $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
734 + $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
735 + $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
736 +
737 + if ($canAutoInstallKit) {
738 + $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
739 + }
740 +
661 741 $addOns = [
662 742 'fluent-crm' => [
663 743 'title' => __('FluentCRM', 'fluent-boards'),
664 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -699,8 +779,21 @@
699 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
700 780 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
701 781 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
702 782 ],
783 + 'fluent-toolkit' => [
784 + 'title' => __('FluentHub', 'fluent-boards'),
785 + 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
786 + 'is_installed' => $kitLoaded,
787 + 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
788 + 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
789 + 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
790 + 'action_text' => $kitActionText,
791 + 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
792 + 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
793 + 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
794 + 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
795 + ],
703 796 ];
704 797
705 798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
706 799
@@ -815,8 +908,19 @@
815 908 {
816 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
817 910 }
818 911
912 + private function canAutoInstallFluentKit()
913 + {
914 + $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
915 +
916 + if (!$canAutoInstall) {
917 + $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
918 + }
919 +
920 + return $canAutoInstall;
921 + }
922 +
819 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
820 924 {
821 925 if (!empty($plugin_to_install['repo-slug'])) {
822 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -962,9 +1066,10 @@
962 1066 $pages = [];
963 1067 foreach ($allPages as $page) {
964 1068 $pages[] = [
965 1069 'id' => $page->ID,
966 - 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards')
1070 + 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1071 + 'url' => esc_url_raw(get_permalink($page->ID))
967 1072 ];
968 1073 }
969 1074
970 1075 return $this->sendSuccess([
@@ -1015,9 +1120,11 @@
1015 1120 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
1016 1121
1017 1122 $scheduleHandler = new ProScheduleHandler();
1018 1123
1019 - if ($savedGeneralSettings['daily_reminder_enabled'] || $savedGeneralSettings['daily_reminder_enabled'] == 'true') {
1124 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1125 +
1126 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
1020 1127 // force schedule from this settings update
1021 1128 $scheduleHandler->clearDailyTaskReminderScheduler();
1022 1129 $scheduleHandler->scheduleDailyTaskReminder();
1023 1130 }