PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.1
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.1
2.2.1 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 All 44 releases
fluent-boards / app / Hooks / Handlers / ExternalPages.php

ExternalPages.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.2.1, at app/Hooks/Handlers/ExternalPages.php

147 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Hooks\Handlers;
4
5 use FluentBoards\App\Models\CommentImage;
6 use FluentBoards\App\Models\Task;
7 use FluentBoards\App\Services\AttachmentAccessService;
8 use FluentBoards\App\Services\PermissionManager;
9 use FluentBoards\App\Services\Libs\FileSystem;
10
11 class ExternalPages
12 {
13 public function view_uploaded_comment_image()
14 {
15 nocache_headers();
16 header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
17
18 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only endpoint; current permissions are checked for every request
19 $attachmentHash = isset($_REQUEST['fbs_comment_image']) && is_string($_REQUEST['fbs_comment_image'])
20 ? sanitize_text_field(wp_unslash($_REQUEST['fbs_comment_image'])) : '';
21
22 if (empty($attachmentHash)) {
23 wp_die(esc_html__('Invalid Attachment Hash', 'fluent-boards'), '', ['response' => 404]);
24 }
25
26 $attachment = $this->getUploadedImageByHash($attachmentHash);
27
28 if (!$attachment) {
29 wp_die(esc_html__('Invalid Attachment Hash', 'fluent-boards'), '', ['response' => 404]);
30 }
31
32 $boardId = (new AttachmentAccessService())->getAccessibleBoardId($attachment);
33 if (!$boardId) {
34 wp_die(esc_html__('You do not have permission to view this image.', 'fluent-boards'), '', ['response' => 403]);
35 }
36
37 // Old URLs may include a board ID, but it can never choose the file directory.
38 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Optional compatibility parameter is checked against the attachment's board
39 if (isset($_REQUEST['fbs_bid']) && (!is_scalar($_REQUEST['fbs_bid']) || absint(wp_unslash($_REQUEST['fbs_bid'])) !== $boardId)) {
40 wp_die(esc_html__('You do not have permission to view this image.', 'fluent-boards'), '', ['response' => 403]);
41 }
42
43 if ('local' !== $attachment->driver) {
44 if(!empty($attachment->file_path)){
45 $this->redirectToExternalAttachment($attachment->full_url);
46 }else{
47 wp_die(esc_html__('File could not be found', 'fluent-boards'), '', ['response' => 404]);
48 }
49 return;
50 }
51
52 // Preserve legacy absolute paths inside plugin uploads; bare filenames use the trusted board.
53 $filePath = FileSystem::resolveLocalAttachmentPath($attachment->file_path)
54 ?: FileSystem::resolveLocalAttachmentPath($attachment->file_path, $boardId);
55
56 if (!$filePath || !is_readable($filePath)) {
57 wp_die(esc_html__('File could not be found.', 'fluent-boards'), '', ['response' => 404]);
58 }
59
60 $this->serveLocalAttachment($attachment, $filePath);
61 }
62
63 public function view_comment_image()
64 {
65 $this->view_uploaded_comment_image();
66 }
67
68 private function getUploadedImageByHash($attachmentHash)
69 {
70 return CommentImage::where('file_hash', $attachmentHash)->first();
71 }
72
73 private function serveLocalAttachment($attachment, $filePath)
74 {
75 if (ob_get_level()) {
76 ob_end_clean();
77 }
78 // Stored MIME values and titles may come from legacy or imported records.
79 $mime = wp_get_image_mime($filePath);
80 $safeImages = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/heic'];
81 $inline = in_array($mime, $safeImages, true);
82 $filename = sanitize_file_name($attachment->title ?: basename($filePath));
83 header('Content-Type: ' . ($inline ? $mime : 'application/octet-stream'));
84 header('X-Content-Type-Options: nosniff');
85 header('Content-Disposition: ' . ($inline ? 'inline' : 'attachment') . '; filename="' . ($filename ?: 'attachment') . '"');
86 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Serving binary file content directly to browser, WP_Filesystem not suitable for this use case
87 readfile($filePath);
88 die();
89 }
90
91 public function redirectToPage()
92 {
93 nocache_headers();
94 header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
95
96 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect; current board access is checked below
97 $taskId = isset($_GET['taskId']) && is_scalar($_GET['taskId']) ? absint(wp_unslash($_GET['taskId'])) : 0;
98
99 if (!$taskId) {
100 wp_die(esc_html__('Task not found', 'fluent-boards'), '', ['response' => 404]);
101 }
102
103 // Authenticate before looking up task metadata, retaining the email link after login.
104 if (!is_user_logged_in()) {
105 $returnUrl = add_query_arg(['redirect' => 'to_task', 'taskId' => $taskId], site_url('/'));
106 // Match WordPress login redirects, including trusted SSO login_url filters.
107 wp_redirect(wp_login_url($returnUrl));
108 exit;
109 }
110
111 $task = Task::find($taskId);
112 if (!$task || !PermissionManager::userHasPermission($task->board_id)) {
113 wp_die(esc_html__('Task not found', 'fluent-boards'), '', ['response' => 404]);
114 }
115 if ($this->isFrontendEnabled() == 'no') {
116 $urlBase = apply_filters('fluent_boards/app_url', admin_url('admin.php?page=fluent-boards#/'));
117 $page_url = $urlBase . 'boards/' . $task->board_id . '/tasks/' . $task->id . '-' .substr($task->title, 0, 10);
118 wp_redirect($page_url);
119 exit;
120 } else {
121 $urlBase = apply_filters('fluent_boards/app_url', admin_url('admin.php?page=fluent-boards#/'));
122 $page_url = $urlBase . 'boards/' . $task->board_id . '/tasks/' . $task->id . '-' .substr($task->title, 0, 10);
123 wp_redirect($page_url);
124 exit;
125 }
126
127 die();
128 }
129
130 private function isFrontendEnabled()
131 {
132 $storedSettings = get_option('fluent_boards_modules', []);
133 $settings = is_string($storedSettings) ? maybe_unserialize($storedSettings) : $storedSettings;
134
135 if (is_array($settings) && isset($settings['frontend']['enabled'])) {
136 return $settings['frontend']['enabled'];
137 }
138
139 return 'no';
140 }
141 private function redirectToExternalAttachment($redirectUrl)
142 {
143 wp_redirect($redirectUrl, 307);
144 exit();
145 }
146 }
147