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 -26 1.13 → 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,15 +159,20 @@
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
97 -$app->addCustomAction('send_invitation', 'ScheduleHandler@sendInvitationViaEmail', 10, 3);
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);
98 174
99 -
100 175 if (defined('WP_CLI') && WP_CLI) {
101 176 \WP_CLI::add_command('fluent_boards', '\FluentBoards\App\Hooks\Cli\Commands');
102 177 }
103 178
@@ -102,22 +177,24 @@
102 177 }
103 178
104 179
105 180 add_action('init', function () {
181 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing parameter check, no data modification
106 182 if (!isset($_REQUEST['fbs'])) {
107 183 return;
108 184 }
109 185 });
110 186
111 -
112 187 /*
113 188 * IMPORTANT
114 189 * External Pages Handler
115 190 * Each Request must have fbs=1 as a query parameter, then the plugin will handle the request.
116 191 */
192 +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in individual handlers
117 193 if(isset($_GET['fbs']) && $_GET['fbs'] == 1) {
118 194
119 195 // For viewing attachment
196 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in handler
120 197 if(isset($_GET['route']) && ($_GET['route'] == 'task')) {
121 198 add_action('init', function() {
122 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
123 200 });
@@ -122,5 +199,83 @@
122 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
123 200 });
124 201 }
125 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);
126 281 }