| 1 |
<?php |
| 2 |
//if accessed directly exit |
| 3 |
if (!defined('ABSPATH')) exit; |
| 4 |
|
| 5 |
use FluentBoards\App\Services\Helper; |
| 6 |
use FluentBoards\App\Services\Libs\FileSystem; |
| 7 |
|
| 8 |
/** |
| 9 |
* All registered action's handlers should be in app\Hooks\Handlers, |
| 10 |
* addAction is similar to add_action and addCustomAction is just a |
| 11 |
* wrapper over add_action which will add a prefix to the hook name |
| 12 |
* using the plugin slug to make it unique in all WordPress plugins, |
| 13 |
* ex: $app->addCustomAction('foo', ['FooHandler', 'handleFoo']) is |
| 14 |
* equivalent to add_action('slug-foo', ['FooHandler', 'handleFoo']). |
| 15 |
*/ |
| 16 |
|
| 17 |
/** |
| 18 |
* @var $app FluentBoards\Framework\Foundation\Application |
| 19 |
*/ |
| 20 |
|
| 21 |
(new \FluentBoards\App\Hooks\Handlers\AdminMenuHandler())->register(); |
| 22 |
$app->addAction('admin_init', 'UpdateHandler@maybeUpgradeIndexes', 10, 0); |
| 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 |
|
| 71 |
//$app->addCustomAction('task_prop_changed', 'ActivityHandler@logActivity', 10, 3); |
| 72 |
$app->addCustomAction('task_board_changed', 'ActivityHandler@logMoveTaskToAnotherBoardActivity', 10, 2); //will call from service after board change code merged |
| 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 |
|
| 77 |
$app->addCustomAction('task_priority_changed', 'ActivityHandler@logPriorityChangeActivity', 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); |
| 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); |
| 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); |
| 87 |
$app->addCustomAction('task_completed_activity', 'ActivityHandler@logTaskCompletedOrReopenActivity', 10, 2); |
| 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); |
| 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); |
| 95 |
$app->addCustomAction('task_created', 'ActivityHandler@logTaskCreationActivity', 10, 1); |
| 96 |
$app->addCustomAction('task_content_updated', 'ActivityHandler@logTaskContentUpdatedActivity', 10, 3); |
| 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); |
| 102 |
|
| 103 |
$app->addCustomAction('board_created', 'BoardHandler@boardCreated', 10, 1); |
| 104 |
$app->addCustomAction('board_created_from_automation', 'BoardHandler@boardCreatedFromAutomation', 10, 1); |
| 105 |
$app->addCustomAction('before_board_deleted', 'BoardHandler@beforeBoardDeleted', 10, 2); |
| 106 |
$app->addCustomAction('board_updated', 'BoardHandler@boardUpdated', 10, 2); |
| 107 |
$app->addCustomAction('stage_updated', 'BoardHandler@boardStageUpdated', 10, 3); |
| 108 |
$app->addCustomAction('board_stage_moved', 'BoardHandler@boardStageMoved', 10, 2); |
| 109 |
$app->addCustomAction('board_stage_added', 'BoardHandler@boardStageAdded', 10, 2); |
| 110 |
$app->addCustomAction('stage_deleted', 'BoardHandler@boardStageDeleted', 10, 2); |
| 111 |
$app->addCustomAction('stage_archived', 'BoardHandler@boardStageArchived', 10, 2); |
| 112 |
$app->addCustomAction('board_stage_restored', 'BoardHandler@boardArchivedStageRestore', 10, 2); |
| 113 |
$app->addCustomAction('task_created', 'BoardHandler@taskCreatedOnBoard', 10, 1); |
| 114 |
$app->addCustomAction('before_task_deleted', 'BoardHandler@beforeTaskDeleted', 10, 2); |
| 115 |
$app->addCustomAction('task_deleted', 'BoardHandler@taskdeleted', 10, 1); |
| 116 |
$app->addCustomAction('task_moved_from_board', 'BoardHandler@taskMovedFromBoard', 10, 3); |
| 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); |
| 120 |
$app->addCustomAction('board_member_added', 'BoardHandler@boardMemberAdded', 10, 2); |
| 121 |
$app->addCustomAction('board_viewer_added', 'BoardHandler@boardViewerAdded', 10, 2); |
| 122 |
$app->addCustomAction('board_member_removed', 'BoardHandler@boardMemberRemoved', 10, 2); |
| 123 |
$app->addCustomAction('board_admin_added', 'BoardHandler@boardAdminAdded', 10, 2); |
| 124 |
$app->addCustomAction('board_admin_removed', 'BoardHandler@boardAdminRemoved', 10, 2); |
| 125 |
$app->addCustomAction('associate_user_add_change_remove_activity', 'ActivityHandler@associateUserAddChangeRemoveActivity', 10, 3); |
| 126 |
$app->addCustomAction('task_assignee_leave', 'ActivityHandler@taskAssigneeLeave', 10, 1); |
| 127 |
$app->addCustomAction('task_assignee_join', 'ActivityHandler@taskAssigneeJoin', 10, 1); |
| 128 |
$app->addCustomAction('task_label', 'ActivityHandler@taskLabelActivity', 10, 3); |
| 129 |
$app->addCustomAction('label_manage_for_task_activity', 'ActivityHandler@labelManageForTaskActivity', 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); |
| 133 |
$app->addCustomAction('board_stages_reordered', 'BoardHandler@boardStagesReOrdered', 10, 2); |
| 134 |
$app->addCustomAction('default_assignees_updated', 'BoardHandler@defaultAssigneesUpdated', 10, 2); |
| 135 |
|
| 136 |
$app->addCustomAction('comment_created', 'NotificationHandler@addCommentNotification', 10, 1); |
| 137 |
$app->addCustomAction('mention_comment_notification', 'NotificationHandler@mentionInCommentNotification', 10, 2); |
| 138 |
$app->addCustomAction('subtask_added', 'NotificationHandler@addSubtaskNotification', 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); |
| 142 |
$app->addCustomAction('task_priority_changed', 'NotificationHandler@changePriorityNotification', 10, 2); |
| 143 |
$app->addCustomAction('task_archived', 'NotificationHandler@taskArchiveNotification', 10, 1); |
| 144 |
$app->addCustomAction('task_content_updated', 'NotificationHandler@changeTitleNotification', 10, 3); |
| 145 |
//$app->addCustomAction('description_changed_notification', 'NotificationHandler@changeDescriptionNotification', 10, 1); |
| 146 |
$app->addCustomAction('board_changed_notification', 'NotificationHandler@changeBoardNotification', 10, 2); |
| 147 |
$app->addCustomAction('task_assignee_added', 'NotificationHandler@assigneeAddedNotification', 10, 2); |
| 148 |
$app->addCustomAction('task_assignee_removed', 'NotificationHandler@assigneeRemovedNotification', 10, 2); |
| 149 |
|
| 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); |
| 152 |
$app->addCustomAction('one_time_schedule_send_email_for_add_assignee', 'ScheduleHandler@sendEmailForAddAssignee', 10, 3); |
| 153 |
$app->addCustomAction('one_time_schedule_send_email_for_remove_assignee', 'ScheduleHandler@sendEmailForRemoveAssignee', 10, 3); |
| 154 |
$app->addCustomAction('one_time_schedule_send_email_for_stage_change', 'ScheduleHandler@sendEmailForChangeStage', 10, 3); |
| 155 |
|
| 156 |
//Todo:: update handler |
| 157 |
$app->addCustomAction('one_time_schedule_send_email_for_due_date_update', 'ScheduleHandler@sendEmailForDueDateUpdate', 10, 3); |
| 158 |
$app->addCustomAction('one_time_schedule_send_email_for_task_archived', 'ScheduleHandler@sendEmailForArchivedTask', 10, 3); |
| 159 |
$app->addCustomAction('one_time_schedule_send_email_for_removed_from_task', 'ScheduleHandler@sendEmailForChangeStage', 10, 3); |
| 160 |
|
| 161 |
$app->addCustomAction('task_attachment_added', 'ActivityHandler@taskAttachmentAdded', 10, 1); |
| 162 |
$app->addCustomAction('task_attachment_deleted', 'ActivityHandler@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 |
| 165 |
$app->addAction('deleted_user', 'BoardHandler@deleteUserRelatedData', 10, 3); |
| 166 |
$app->addAction('fluent_boards/delete_remote_attachment_files', 'FileHandler@deleteRemoteAttachmentFiles', 10, 1); |
| 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); |
| 174 |
|
| 175 |
if (defined('WP_CLI') && WP_CLI) { |
| 176 |
\WP_CLI::add_command('fluent_boards', '\FluentBoards\App\Hooks\Cli\Commands'); |
| 177 |
} |
| 178 |
|
| 179 |
|
| 180 |
add_action('init', function () { |
| 181 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing parameter check, no data modification |
| 182 |
if (!isset($_REQUEST['fbs'])) { |
| 183 |
return; |
| 184 |
} |
| 185 |
}); |
| 186 |
|
| 187 |
/* |
| 188 |
* IMPORTANT |
| 189 |
* External Pages Handler |
| 190 |
* Each Request must have fbs=1 as a query parameter, then the plugin will handle the request. |
| 191 |
*/ |
| 192 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in individual handlers |
| 193 |
if(isset($_GET['fbs']) && $_GET['fbs'] == 1) { |
| 194 |
|
| 195 |
// For viewing attachment |
| 196 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing logic, security validated in handler |
| 197 |
if(isset($_GET['route']) && ($_GET['route'] == 'task')) { |
| 198 |
add_action('init', function() { |
| 199 |
(new \FluentBoardsPro\App\Hooks\Handlers\ExternalPages())->handleTaskWebhook(); |
| 200 |
}); |
| 201 |
} |
| 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); |
| 281 |
} |
| 282 |
|