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 +162 -24 1.22 → 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 78 $app->addCustomAction('comment_created', 'ActivityHandler@logCommentCreateActivity', 10, 1);
27 -$app->addCustomAction('comment_updated', 'ActivityHandler@logCommentUpdateActivity', 10, 3);
28 -$app->addCustomAction('comment_deleted', 'ActivityHandler@logCommentDeleteActivity', 10, 2);
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,25 +126,28 @@
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 136 $app->addCustomAction('comment_created', 'NotificationHandler@addCommentNotification', 10, 1);
69 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);
82 151 $app->addCustomAction('one_time_schedule_send_email_for_mention', 'ScheduleHandler@sendEmailForMention', 10, 3);
83 152 $app->addCustomAction('one_time_schedule_send_email_for_add_assignee', 'ScheduleHandler@sendEmailForAddAssignee', 10, 3);
84 153 $app->addCustomAction('one_time_schedule_send_email_for_remove_assignee', 'ScheduleHandler@sendEmailForRemoveAssignee', 10, 3);
@@ -90,18 +159,20 @@
90 159 $app->addCustomAction('one_time_schedule_send_email_for_removed_from_task', 'ScheduleHandler@sendEmailForChangeStage', 10, 3);
91 160
92 161 $app->addCustomAction('task_attachment_added', 'ActivityHandler@taskAttachmentAdded', 10, 1);
93 162 $app->addCustomAction('task_attachment_deleted', 'ActivityHandler@taskAttachmentDeleted', 10, 1);
94 -$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
95 165 $app->addAction('deleted_user', 'BoardHandler@deleteUserRelatedData', 10, 3);
96 -$app->addAction('delete_attachment', 'FileHandler@mediaFileDeleted', 10, 1);
166 +$app->addAction('fluent_boards/delete_remote_attachment_files', 'FileHandler@deleteRemoteAttachmentFiles', 10, 1);
97 167
98 -$app->addCustomAction('send_invitation', 'ScheduleHandler@sendInvitationViaEmail', 10, 3);
99 168 $app->addCustomAction('task_created', 'TaskHandler@onTaskCreated', 10, 1);
100 169 $app->addCustomAction('comment_created', 'TaskHandler@onCommentCreated', 10, 1);
101 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);
102 174
103 -
104 175 if (defined('WP_CLI') && WP_CLI) {
105 176 \WP_CLI::add_command('fluent_boards', '\FluentBoards\App\Hooks\Cli\Commands');
106 177 }
107 178
@@ -106,22 +177,24 @@
106 177 }
107 178
108 179
109 180 add_action('init', function () {
181 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing parameter check, no data modification
110 182 if (!isset($_REQUEST['fbs'])) {
111 183 return;
112 184 }
113 185 });
114 186
115 -
116 187 /*
117 188 * IMPORTANT
118 189 * External Pages Handler
119 190 * Each Request must have fbs=1 as a query parameter, then the plugin will handle the request.
120 191 */
192 +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in individual handlers
121 193 if(isset($_GET['fbs']) && $_GET['fbs'] == 1) {
122 194
123 195 // For viewing attachment
196 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in handler
124 197 if(isset($_GET['route']) && ($_GET['route'] == 'task')) {
125 198 add_action('init', function() {
126 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
127 200 });
@@ -126,10 +199,12 @@
126 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
127 200 });
128 201 }
129 202
203 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public file serving endpoint, security validated by hash in handler
130 204 if(isset($_GET['fbs_comment_image'])) {
131 - if(isset($_GET['fbs_type']) == 'public_url') {
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') {
132 207 add_action('init', function() {
133 208 (new \FluentBoards\App\Hooks\Handlers\ExternalPages())->view_uploaded_comment_image();
134 209 });
135 210 } else {
@@ -139,5 +214,68 @@
139 214 }
140 215
141 216 }
142 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);
143 281 }