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/Hooks/actions.php +181 -24 1.11 → 2.2.0 View file →
@@ -2,8 +2,9 @@
2 2 //if accessed directly exit
3 3 if (!defined('ABSPATH')) exit;
4 4
5 5 use FluentBoards\App\Services\Helper;
6 +use FluentBoards\App\Services\Libs\FileSystem;
6 7
7 8 /**
8 9 * All registered action's handlers should be in app\Hooks\Handlers,
9 10 * addAction is similar to add_action and addCustomAction is just a
@@ -17,43 +18,108 @@
17 18 * @var $app FluentBoards\Framework\Foundation\Application
18 19 */
19 20
20 21 (new \FluentBoards\App\Hooks\Handlers\AdminMenuHandler())->register();
22 +$app->addAction('admin_init', 'UpdateHandler@maybeUpgradeIndexes', 10, 0);
21 23
24 +add_action('fluent_boards/check_upload_protection', function () {
25 + FileSystem::checkUploadProtection();
26 +});
27 +
28 +// Cover existing uploads after updates, even before another file is uploaded.
29 +add_action('admin_init', function () {
30 + if (!current_user_can('manage_options') || wp_doing_ajax()) {
31 + return;
32 + }
33 +
34 + $protectionReady = FileSystem::installUploadProtectionRules(false);
35 + $blocked = FileSystem::checkUploadProtection(false);
36 + if (!$protectionReady || $blocked !== true) {
37 + add_action('admin_notices', function () use ($protectionReady, $blocked) {
38 + if ($blocked === false) {
39 + $message = __('Fluent Boards uploads are accessible by direct URL without a login. Ask your hosting provider to block direct web access to this directory and its subdirectories.', 'fluent-boards');
40 + } elseif (!$protectionReady) {
41 + $message = __('Fluent Boards could not prepare upload-directory protection. Ask your hosting provider to check filesystem permissions and block direct access to uploads.', 'fluent-boards');
42 + } else {
43 + $message = __('Fluent Boards could not verify upload-directory protection. Writing .htaccess rules does not confirm that your web server enforces them.', 'fluent-boards');
44 + }
45 + $uploads = wp_upload_dir();
46 + $folder = apply_filters('fluent_boards/upload_folder_name', FLUENT_BOARDS_UPLOAD_DIR);
47 + $url = trailingslashit($uploads['baseurl']) . trim($folder, '/');
48 + echo '<div class="notice notice-warning"><p>' . esc_html($message) . '</p><p>'
49 + . esc_html__('Nginx requires a server rule denying this upload URL and every path beneath it. Apache must honor the generated .htaccess authorization rules. Verify that direct file requests return 403 or 404. This check retries every five minutes.', 'fluent-boards')
50 + . '</p><p><code>' . esc_html($url) . '</code></p></div>';
51 + });
52 + }
53 +});
54 +
55 +/*
56 + * Add custom CSS for fbs_notice.
57 + */
58 +add_action('admin_head', function () {
59 + echo '<style>
60 + .fbs_notice {
61 + background: var(--fbs-primary-bg, #ffffff);
62 + border: 1px solid var(--fbs-primary-border, #E1E4EA);
63 + border-left: 3px solid #FB3748;
64 + padding: 10px 12px !important;
65 + border-radius: 8px;
66 + margin-bottom: 5px;
67 + }
68 + </style>';
69 +});
70 +
22 71 //$app->addCustomAction('task_prop_changed', 'ActivityHandler@logActivity', 10, 3);
23 72 $app->addCustomAction('task_board_changed', 'ActivityHandler@logMoveTaskToAnotherBoardActivity', 10, 2); //will call from service after board change code merged
24 -$app->addCustomAction('task_date_changed', 'ActivityHandler@logDueDateActivity', 10, 3);
73 +$app->addCustomAction('task_due_date_changed', 'ActivityHandler@logDueDateActivity', 10, 2);
74 +$app->addCustomAction('task_due_date_removed', 'ActivityHandler@logDueDateRemoveActivity', 10, 1);
75 +$app->addCustomAction('task_start_date_changed', 'ActivityHandler@logStartDateActivity', 10, 2);
76 +
25 77 $app->addCustomAction('task_priority_changed', 'ActivityHandler@logPriorityChangeActivity', 10, 2);
26 -$app->addCustomAction('task_comment_added', 'ActivityHandler@logCommentCreateActivity', 10, 1);
27 -$app->addCustomAction('task_comment_updated', 'ActivityHandler@logCommentUpdateActivity', 10, 3);
28 -$app->addCustomAction('task_comment_deleted', 'ActivityHandler@logCommentDeleteActivity', 10, 2);
78 +$app->addCustomAction('comment_created', 'ActivityHandler@logCommentCreateActivity', 10, 1);
79 +$app->addCustomAction('comment_updated', 'ActivityHandler@logCommentUpdateActivity', 10, 2);
80 +$app->addCustomAction('comment_deleted', 'ActivityHandler@logCommentDeleteActivity', 10, 1);
29 81 $app->addCustomAction('subtask_added', 'ActivityHandler@logSubtaskAddedActivity', 10, 2);
82 +$app->addCustomAction('subtask_cloned', 'ActivityHandler@logSubtaskCloneActivity', 10, 2);
83 +$app->addCustomAction('subtask_group_created', 'ActivityHandler@logSubtaskGroupAddedActivity', 10, 2);
30 84 $app->addCustomAction('subtask_deleted_activity', 'ActivityHandler@logSubtaskDeletedActivity', 10, 2);
85 +$app->addCustomAction('subtask_group_deleted_activity', 'ActivityHandler@logSubtaskGroupDeletedActivity', 10, 2);
86 +$app->addCustomAction('subtask_group_title_updated', 'ActivityHandler@logSubtaskGroupTitleUpdatedActivity', 10, 2);
31 87 $app->addCustomAction('task_completed_activity', 'ActivityHandler@logTaskCompletedOrReopenActivity', 10, 2);
32 -$app->addCustomAction('task_moved_to_new_stage', 'ActivityHandler@logTaskStageUpdatedActivity', 10, 2);
33 -$app->addCustomAction('task_assignee_changed', 'ActivityHandler@logAssigneeChangeActivity', 10, 3);
88 +$app->addCustomAction('task_stage_updated', 'ActivityHandler@logTaskStageUpdatedActivity', 10, 3);
89 +$app->addCustomAction('task_assignee_added', 'ActivityHandler@logAssigneeAddedActivity', 10, 2);
90 +$app->addCustomAction('task_assignee_removed', 'ActivityHandler@logAssigneeRemovedActivity', 10, 2);
34 91 $app->addCustomAction('task_added_from_fluent_form', 'ActivityHandler@taskAddedFromFluentForms', 10, 1);
92 +$app->addCustomAction('repeat_task_created', 'ActivityHandler@logRepeatTaskCreatedActivity', 10, 2);
93 +$app->addCustomAction('repeat_task_set', 'ActivityHandler@logRepeatTaskSet', 10, 1);
94 +$app->addCustomAction('repeat_task_updated', 'ActivityHandler@logRepeatTaskUpdated', 10, 1);
35 95 $app->addCustomAction('task_created', 'ActivityHandler@logTaskCreationActivity', 10, 1);
36 96 $app->addCustomAction('task_content_updated', 'ActivityHandler@logTaskContentUpdatedActivity', 10, 3);
37 97 $app->addCustomAction('task_deleted', 'TaskHandler@taskDeleted', 10, 1);
98 +$app->addCustomAction('task_archived', 'ActivityHandler@taskArchived', 10, 1);
99 +$app->addCustomAction('task_custom_field_changed', 'ActivityHandler@logCustomFieldActivity', 10, 5);
100 +$app->addCustomAction('board_custom_field_created', 'BoardHandler@customFieldCreated', 10, 1);
101 +$app->addCustomAction('board_custom_field_updated', 'BoardHandler@customFieldUpdated', 10, 2);
38 102
39 103 $app->addCustomAction('board_created', 'BoardHandler@boardCreated', 10, 1);
104 +$app->addCustomAction('board_created_from_automation', 'BoardHandler@boardCreatedFromAutomation', 10, 1);
40 105 $app->addCustomAction('before_board_deleted', 'BoardHandler@beforeBoardDeleted', 10, 2);
41 -$app->addCustomAction('board_deleted', 'BoardHandler@boardDeleted', 10, 1);
42 106 $app->addCustomAction('board_updated', 'BoardHandler@boardUpdated', 10, 2);
43 -$app->addCustomAction('board_stage_updated', 'BoardHandler@boardStageUpdated', 10, 3);
107 +$app->addCustomAction('stage_updated', 'BoardHandler@boardStageUpdated', 10, 3);
44 108 $app->addCustomAction('board_stage_moved', 'BoardHandler@boardStageMoved', 10, 2);
45 -$app->addCustomAction('board_stage_dragged', 'BoardHandler@boardStageDragged', 10, 1);
46 109 $app->addCustomAction('board_stage_added', 'BoardHandler@boardStageAdded', 10, 2);
47 110 $app->addCustomAction('stage_deleted', 'BoardHandler@boardStageDeleted', 10, 2);
48 -$app->addCustomAction('board_stage_archived', 'BoardHandler@boardStageArchived', 10, 2);
111 +$app->addCustomAction('stage_archived', 'BoardHandler@boardStageArchived', 10, 2);
49 112 $app->addCustomAction('board_stage_restored', 'BoardHandler@boardArchivedStageRestore', 10, 2);
50 113 $app->addCustomAction('task_created', 'BoardHandler@taskCreatedOnBoard', 10, 1);
51 114 $app->addCustomAction('before_task_deleted', 'BoardHandler@beforeTaskDeleted', 10, 2);
52 115 $app->addCustomAction('task_deleted', 'BoardHandler@taskdeleted', 10, 1);
53 116 $app->addCustomAction('task_moved_from_board', 'BoardHandler@taskMovedFromBoard', 10, 3);
54 -$app->addCustomAction('board_task_archived', 'BoardHandler@taskarchivedOnBoard', 10, 1);
117 +$app->addCustomAction('task_moved_from_board', 'ActivityHandler@taskMovedFromBoard', 10, 3);
118 +$app->addCustomAction('task_archived', 'BoardHandler@taskArchivedOnBoard', 10, 1);
119 +$app->addCustomAction('tasks_moved_between_stages', 'BoardHandler@tasksMovedBetweenStages', 10, 4);
55 120 $app->addCustomAction('board_member_added', 'BoardHandler@boardMemberAdded', 10, 2);
121 +$app->addCustomAction('board_viewer_added', 'BoardHandler@boardViewerAdded', 10, 2);
56 122 $app->addCustomAction('board_member_removed', 'BoardHandler@boardMemberRemoved', 10, 2);
57 123 $app->addCustomAction('board_admin_added', 'BoardHandler@boardAdminAdded', 10, 2);
58 124 $app->addCustomAction('board_admin_removed', 'BoardHandler@boardAdminRemoved', 10, 2);
59 125 $app->addCustomAction('associate_user_add_change_remove_activity', 'ActivityHandler@associateUserAddChangeRemoveActivity', 10, 3);
@@ -60,26 +126,30 @@
60 126 $app->addCustomAction('task_assignee_leave', 'ActivityHandler@taskAssigneeLeave', 10, 1);
61 127 $app->addCustomAction('task_assignee_join', 'ActivityHandler@taskAssigneeJoin', 10, 1);
62 128 $app->addCustomAction('task_label', 'ActivityHandler@taskLabelActivity', 10, 3);
63 129 $app->addCustomAction('label_manage_for_task_activity', 'ActivityHandler@labelManageForTaskActivity', 10, 2);
64 -$app->addCustomAction('board_label', 'BoardHandler@boardLabelActivity', 10, 2);
130 +$app->addCustomAction('board_label_created', 'BoardHandler@boardLabelCreatedActivity', 10, 1);
131 +$app->addCustomAction('board_label_updated', 'BoardHandler@boardLabelUpdatedActivity', 10, 1);
132 +$app->addCustomAction('board_label_deleted', 'BoardHandler@boardLabelDeletedActivity', 10, 1);
65 133 $app->addCustomAction('board_stages_reordered', 'BoardHandler@boardStagesReOrdered', 10, 2);
134 +$app->addCustomAction('default_assignees_updated', 'BoardHandler@defaultAssigneesUpdated', 10, 2);
66 135
67 -
68 -$app->addCustomAction('task_comment_added', 'NotificationHandler@addCommentNotification', 10, 1);
69 -$app->addCustomAction('mention_comment_notification', 'NotificationHandler@mentionInCommentNotification', 10, 3);
136 +$app->addCustomAction('comment_created', 'NotificationHandler@addCommentNotification', 10, 1);
137 +$app->addCustomAction('mention_comment_notification', 'NotificationHandler@mentionInCommentNotification', 10, 2);
70 138 $app->addCustomAction('subtask_added', 'NotificationHandler@addSubtaskNotification', 10, 2);
71 -$app->addCustomAction('task_date_changed', 'NotificationHandler@changeDateNotification', 10, 2);
72 -$app->addCustomAction('task_moved_to_new_stage', 'NotificationHandler@changeStageNotification', 10, 2);
139 +$app->addCustomAction('task_due_date_changed', 'NotificationHandler@changeDueDateNotification', 10, 2);
140 +$app->addCustomAction('task_start_date_changed', 'NotificationHandler@changeStartDateNotification', 10, 2);
141 +$app->addCustomAction('task_stage_updated', 'NotificationHandler@changeStageNotification', 10, 2);
73 142 $app->addCustomAction('task_priority_changed', 'NotificationHandler@changePriorityNotification', 10, 2);
74 -$app->addCustomAction('board_task_archived', 'NotificationHandler@taskArchiveNotification', 10, 1);
75 -$app->addCustomAction('task_content_updated', 'NotificationHandler@changeTitleOrDescriptionNotification', 10, 3);
143 +$app->addCustomAction('task_archived', 'NotificationHandler@taskArchiveNotification', 10, 1);
144 +$app->addCustomAction('task_content_updated', 'NotificationHandler@changeTitleNotification', 10, 3);
76 145 //$app->addCustomAction('description_changed_notification', 'NotificationHandler@changeDescriptionNotification', 10, 1);
77 146 $app->addCustomAction('board_changed_notification', 'NotificationHandler@changeBoardNotification', 10, 2);
78 -$app->addCustomAction('task_assignee_changed', 'NotificationHandler@assigneeAddedNotification', 10, 3);
147 +$app->addCustomAction('task_assignee_added', 'NotificationHandler@assigneeAddedNotification', 10, 2);
148 +$app->addCustomAction('task_assignee_removed', 'NotificationHandler@assigneeRemovedNotification', 10, 2);
79 149
80 -
81 150 $app->addCustomAction('one_time_schedule_send_email_for_comment', 'ScheduleHandler@sendEmailForComment', 10, 3);
151 +$app->addCustomAction('one_time_schedule_send_email_for_mention', 'ScheduleHandler@sendEmailForMention', 10, 3);
82 152 $app->addCustomAction('one_time_schedule_send_email_for_add_assignee', 'ScheduleHandler@sendEmailForAddAssignee', 10, 3);
83 153 $app->addCustomAction('one_time_schedule_send_email_for_remove_assignee', 'ScheduleHandler@sendEmailForRemoveAssignee', 10, 3);
84 154 $app->addCustomAction('one_time_schedule_send_email_for_stage_change', 'ScheduleHandler@sendEmailForChangeStage', 10, 3);
85 155
@@ -89,12 +159,19 @@
89 159 $app->addCustomAction('one_time_schedule_send_email_for_removed_from_task', 'ScheduleHandler@sendEmailForChangeStage', 10, 3);
90 160
91 161 $app->addCustomAction('task_attachment_added', 'ActivityHandler@taskAttachmentAdded', 10, 1);
92 162 $app->addCustomAction('task_attachment_deleted', 'ActivityHandler@taskAttachmentDeleted', 10, 1);
93 -$app->addCustomAction('task_attachment_deleted', 'TaskHandler@taskAttachmentDeleted', 10, 1);
163 +$app->addCustomAction('task_attachment_deleted', 'TaskHandler@taskAttachmentDeleted', 10, 2);
164 +// Removed: direct call is made from Comment::deleting to TaskHandler@commentImageDeleted
94 165 $app->addAction('deleted_user', 'BoardHandler@deleteUserRelatedData', 10, 3);
95 -$app->addAction('delete_attachment', 'FileHandler@mediaFileDeleted', 10, 1);
166 +$app->addAction('fluent_boards/delete_remote_attachment_files', 'FileHandler@deleteRemoteAttachmentFiles', 10, 1);
96 167
168 +$app->addCustomAction('task_created', 'TaskHandler@onTaskCreated', 10, 1);
169 +$app->addCustomAction('comment_created', 'TaskHandler@onCommentCreated', 10, 1);
170 +$app->addCustomAction('assign_another_user', 'TaskHandler@onAssignAnotherUser', 10, 2);
171 +$app->addCustomAction('board_background_updated', 'BoardHandler@backgroundUpdated', 10, 2);
172 +$app->addCustomAction('task_cloned', 'TaskHandler@taskCloned', 10, 2);
173 +$app->addCustomAction('task_cloned_activity', 'ActivityHandler@taskCloned', 10, 2);
97 174
98 175 if (defined('WP_CLI') && WP_CLI) {
99 176 \WP_CLI::add_command('fluent_boards', '\FluentBoards\App\Hooks\Cli\Commands');
100 177 }
@@ -100,22 +177,24 @@
100 177 }
101 178
102 179
103 180 add_action('init', function () {
181 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing parameter check, no data modification
104 182 if (!isset($_REQUEST['fbs'])) {
105 183 return;
106 184 }
107 185 });
108 186
109 -
110 187 /*
111 188 * IMPORTANT
112 189 * External Pages Handler
113 190 * Each Request must have fbs=1 as a query parameter, then the plugin will handle the request.
114 191 */
192 +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in individual handlers
115 193 if(isset($_GET['fbs']) && $_GET['fbs'] == 1) {
116 194
117 195 // For viewing attachment
196 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in handler
118 197 if(isset($_GET['route']) && ($_GET['route'] == 'task')) {
119 198 add_action('init', function() {
120 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
121 200 });
@@ -120,5 +199,83 @@
120 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
121 200 });
122 201 }
123 202
203 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public file serving endpoint, security validated by hash in handler
204 + if(isset($_GET['fbs_comment_image'])) {
205 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing check, security validated in handler
206 + if(isset($_GET['fbs_type']) && $_GET['fbs_type'] == 'public_url') {
207 + add_action('init', function() {
208 + (new \FluentBoards\App\Hooks\Handlers\ExternalPages())->view_uploaded_comment_image();
209 + });
210 + } else {
211 + add_action('init', function() {
212 + (new \FluentBoards\App\Hooks\Handlers\ExternalPages())->view_comment_image();
213 + });
214 + }
215 +
216 + }
217 +
218 +}
219 +
220 +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public redirect endpoint, no sensitive operations
221 +if(isset($_GET['redirect']) && $_GET['redirect'] == 'to_task') {
222 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public redirect endpoint, validation performed in handler
223 + if (isset($_GET['taskId'])) {
224 + add_action('init', function() {
225 + (new \FluentBoards\App\Hooks\Handlers\ExternalPages())->redirectToPage();
226 + });
227 + }
228 +}
229 +// Stage Archive Handler - Archive tasks when stage is archived, restore when stage is restored
230 +$app->addAction('fluent_boards/stage_archived_with_tasks', 'StageArchiveHandler@onStageArchived', 10, 2);
231 +$app->addAction('fluent_boards/stage_restored_with_tasks', 'StageArchiveHandler@onStageRestored', 10, 2);
232 +
233 +/*
234 + * MCP - Register Fluent Boards abilities for the WordPress Abilities API.
235 + *
236 + * This mirrors FluentCRM's MCP architecture: on WordPress installs without the
237 + * Abilities API / WP MCP Adapter, `wp_register_ability` is undefined and the
238 + * integration skips silently. Admins can opt out with the `mcp_enabled` option.
239 + */
240 +add_filter('fluent_kit/mcp_products', [\FluentBoards\App\Modules\MCP\MCPInit::class, 'registerToolkitProduct'], 10, 2);
241 +add_filter('fluent_kit/mcp_toggle_handlers', function ($handlers) {
242 + $handlers['fluent-boards'] = [
243 + 'get_enabled' => function () {
244 + return fluent_boards_get_option('mcp_enabled', 'yes') === 'yes';
245 + },
246 + 'set_enabled' => function ($enabled) {
247 + fluent_boards_update_option('mcp_enabled', $enabled ? 'yes' : 'no');
248 + },
249 + ];
250 +
251 + return $handlers;
252 +});
253 +
254 +\FluentBoards\App\Services\MCPAccessService::registerHooks();
255 +
256 +add_action('init', function () {
257 + if (!function_exists('wp_register_ability')) {
258 + return;
259 + }
260 +
261 + if (fluent_boards_get_option('mcp_enabled', 'yes') !== 'yes') {
262 + return;
263 + }
264 +
265 + (new \FluentBoards\App\Modules\MCP\MCPInit())->init();
266 +}, 5);
267 +
268 +$invalidateMcpContext = [\FluentBoards\App\Modules\MCP\Tools\ContextTools::class, 'invalidateCache'];
269 +foreach ([
270 + 'fluent_boards/board_created',
271 + 'fluent_boards/board_updated',
272 + 'fluent_boards/before_board_deleted',
273 + 'fluent_boards/board_stage_added',
274 + 'fluent_boards/stage_updated',
275 + 'fluent_boards/stage_deleted',
276 + 'fluent_boards/board_label_created',
277 + 'fluent_boards/board_label_updated',
278 + 'fluent_boards/board_label_deleted',
279 +] as $mcpContextHook) {
280 + add_action($mcpContextHook, $invalidateMcpContext);
124 281 }