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 +144 -12 1.30 → 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,12 +18,61 @@
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 73 $app->addCustomAction('task_due_date_changed', 'ActivityHandler@logDueDateActivity', 10, 2);
74 +$app->addCustomAction('task_due_date_removed', 'ActivityHandler@logDueDateRemoveActivity', 10, 1);
25 75 $app->addCustomAction('task_start_date_changed', 'ActivityHandler@logStartDateActivity', 10, 2);
26 76
27 77 $app->addCustomAction('task_priority_changed', 'ActivityHandler@logPriorityChangeActivity', 10, 2);
28 78 $app->addCustomAction('comment_created', 'ActivityHandler@logCommentCreateActivity', 10, 1);
@@ -28,25 +78,35 @@
28 78 $app->addCustomAction('comment_created', 'ActivityHandler@logCommentCreateActivity', 10, 1);
29 79 $app->addCustomAction('comment_updated', 'ActivityHandler@logCommentUpdateActivity', 10, 2);
30 80 $app->addCustomAction('comment_deleted', 'ActivityHandler@logCommentDeleteActivity', 10, 1);
31 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);
32 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);
33 87 $app->addCustomAction('task_completed_activity', 'ActivityHandler@logTaskCompletedOrReopenActivity', 10, 2);
34 -$app->addCustomAction('task_stage_updated', 'ActivityHandler@logTaskStageUpdatedActivity', 10, 2);
88 +$app->addCustomAction('task_stage_updated', 'ActivityHandler@logTaskStageUpdatedActivity', 10, 3);
35 89 $app->addCustomAction('task_assignee_added', 'ActivityHandler@logAssigneeAddedActivity', 10, 2);
36 90 $app->addCustomAction('task_assignee_removed', 'ActivityHandler@logAssigneeRemovedActivity', 10, 2);
37 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);
38 95 $app->addCustomAction('task_created', 'ActivityHandler@logTaskCreationActivity', 10, 1);
39 96 $app->addCustomAction('task_content_updated', 'ActivityHandler@logTaskContentUpdatedActivity', 10, 3);
40 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);
41 102
42 103 $app->addCustomAction('board_created', 'BoardHandler@boardCreated', 10, 1);
104 +$app->addCustomAction('board_created_from_automation', 'BoardHandler@boardCreatedFromAutomation', 10, 1);
43 105 $app->addCustomAction('before_board_deleted', 'BoardHandler@beforeBoardDeleted', 10, 2);
44 -$app->addCustomAction('board_deleted', 'BoardHandler@boardDeleted', 10, 1);
45 106 $app->addCustomAction('board_updated', 'BoardHandler@boardUpdated', 10, 2);
46 107 $app->addCustomAction('stage_updated', 'BoardHandler@boardStageUpdated', 10, 3);
47 108 $app->addCustomAction('board_stage_moved', 'BoardHandler@boardStageMoved', 10, 2);
48 -$app->addCustomAction('board_stage_dragged', 'BoardHandler@boardStageDragged', 10, 1);
49 109 $app->addCustomAction('board_stage_added', 'BoardHandler@boardStageAdded', 10, 2);
50 110 $app->addCustomAction('stage_deleted', 'BoardHandler@boardStageDeleted', 10, 2);
51 111 $app->addCustomAction('stage_archived', 'BoardHandler@boardStageArchived', 10, 2);
52 112 $app->addCustomAction('board_stage_restored', 'BoardHandler@boardArchivedStageRestore', 10, 2);
@@ -53,10 +113,13 @@
53 113 $app->addCustomAction('task_created', 'BoardHandler@taskCreatedOnBoard', 10, 1);
54 114 $app->addCustomAction('before_task_deleted', 'BoardHandler@beforeTaskDeleted', 10, 2);
55 115 $app->addCustomAction('task_deleted', 'BoardHandler@taskdeleted', 10, 1);
56 116 $app->addCustomAction('task_moved_from_board', 'BoardHandler@taskMovedFromBoard', 10, 3);
57 -$app->addCustomAction('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);
58 120 $app->addCustomAction('board_member_added', 'BoardHandler@boardMemberAdded', 10, 2);
121 +$app->addCustomAction('board_viewer_added', 'BoardHandler@boardViewerAdded', 10, 2);
59 122 $app->addCustomAction('board_member_removed', 'BoardHandler@boardMemberRemoved', 10, 2);
60 123 $app->addCustomAction('board_admin_added', 'BoardHandler@boardAdminAdded', 10, 2);
61 124 $app->addCustomAction('board_admin_removed', 'BoardHandler@boardAdminRemoved', 10, 2);
62 125 $app->addCustomAction('associate_user_add_change_remove_activity', 'ActivityHandler@associateUserAddChangeRemoveActivity', 10, 3);
@@ -67,10 +130,10 @@
67 130 $app->addCustomAction('board_label_created', 'BoardHandler@boardLabelCreatedActivity', 10, 1);
68 131 $app->addCustomAction('board_label_updated', 'BoardHandler@boardLabelUpdatedActivity', 10, 1);
69 132 $app->addCustomAction('board_label_deleted', 'BoardHandler@boardLabelDeletedActivity', 10, 1);
70 133 $app->addCustomAction('board_stages_reordered', 'BoardHandler@boardStagesReOrdered', 10, 2);
134 +$app->addCustomAction('default_assignees_updated', 'BoardHandler@defaultAssigneesUpdated', 10, 2);
71 135
72 -
73 136 $app->addCustomAction('comment_created', 'NotificationHandler@addCommentNotification', 10, 1);
74 137 $app->addCustomAction('mention_comment_notification', 'NotificationHandler@mentionInCommentNotification', 10, 2);
75 138 $app->addCustomAction('subtask_added', 'NotificationHandler@addSubtaskNotification', 10, 2);
76 139 $app->addCustomAction('task_due_date_changed', 'NotificationHandler@changeDueDateNotification', 10, 2);
@@ -77,9 +140,9 @@
77 140 $app->addCustomAction('task_start_date_changed', 'NotificationHandler@changeStartDateNotification', 10, 2);
78 141 $app->addCustomAction('task_stage_updated', 'NotificationHandler@changeStageNotification', 10, 2);
79 142 $app->addCustomAction('task_priority_changed', 'NotificationHandler@changePriorityNotification', 10, 2);
80 143 $app->addCustomAction('task_archived', 'NotificationHandler@taskArchiveNotification', 10, 1);
81 -$app->addCustomAction('task_content_updated', 'NotificationHandler@changeTitleOrDescriptionNotification', 10, 3);
144 +$app->addCustomAction('task_content_updated', 'NotificationHandler@changeTitleNotification', 10, 3);
82 145 //$app->addCustomAction('description_changed_notification', 'NotificationHandler@changeDescriptionNotification', 10, 1);
83 146 $app->addCustomAction('board_changed_notification', 'NotificationHandler@changeBoardNotification', 10, 2);
84 147 $app->addCustomAction('task_assignee_added', 'NotificationHandler@assigneeAddedNotification', 10, 2);
85 148 $app->addCustomAction('task_assignee_removed', 'NotificationHandler@assigneeRemovedNotification', 10, 2);
@@ -96,18 +159,20 @@
96 159 $app->addCustomAction('one_time_schedule_send_email_for_removed_from_task', 'ScheduleHandler@sendEmailForChangeStage', 10, 3);
97 160
98 161 $app->addCustomAction('task_attachment_added', 'ActivityHandler@taskAttachmentAdded', 10, 1);
99 162 $app->addCustomAction('task_attachment_deleted', 'ActivityHandler@taskAttachmentDeleted', 10, 1);
100 -$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
101 165 $app->addAction('deleted_user', 'BoardHandler@deleteUserRelatedData', 10, 3);
102 -$app->addAction('delete_attachment', 'FileHandler@mediaFileDeleted', 10, 1);
166 +$app->addAction('fluent_boards/delete_remote_attachment_files', 'FileHandler@deleteRemoteAttachmentFiles', 10, 1);
103 167
104 -$app->addCustomAction('send_invitation', 'ScheduleHandler@sendInvitationViaEmail', 10, 3);
105 168 $app->addCustomAction('task_created', 'TaskHandler@onTaskCreated', 10, 1);
106 169 $app->addCustomAction('comment_created', 'TaskHandler@onCommentCreated', 10, 1);
107 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);
108 174
109 -
110 175 if (defined('WP_CLI') && WP_CLI) {
111 176 \WP_CLI::add_command('fluent_boards', '\FluentBoards\App\Hooks\Cli\Commands');
112 177 }
113 178
@@ -112,22 +177,24 @@
112 177 }
113 178
114 179
115 180 add_action('init', function () {
181 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing parameter check, no data modification
116 182 if (!isset($_REQUEST['fbs'])) {
117 183 return;
118 184 }
119 185 });
120 186
121 -
122 187 /*
123 188 * IMPORTANT
124 189 * External Pages Handler
125 190 * Each Request must have fbs=1 as a query parameter, then the plugin will handle the request.
126 191 */
192 +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in individual handlers
127 193 if(isset($_GET['fbs']) && $_GET['fbs'] == 1) {
128 194
129 195 // For viewing attachment
196 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in handler
130 197 if(isset($_GET['route']) && ($_GET['route'] == 'task')) {
131 198 add_action('init', function() {
132 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
133 200 });
@@ -132,10 +199,12 @@
132 199 (new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook();
133 200 });
134 201 }
135 202
203 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public file serving endpoint, security validated by hash in handler
136 204 if(isset($_GET['fbs_comment_image'])) {
137 - 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') {
138 207 add_action('init', function() {
139 208 (new \FluentBoards\App\Hooks\Handlers\ExternalPages())->view_uploaded_comment_image();
140 209 });
141 210 } else {
@@ -145,5 +214,68 @@
145 214 }
146 215
147 216 }
148 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);
149 281 }