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/Handlers/ExternalPages.php +89 -68 1.31 → 2.2.0 View file →
@@ -1,14 +1,12 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Hooks\Handlers;
4 4
5 -use FluentBoards\App\Models\Attachment;
6 -use FluentBoards\App\App;
7 5 use FluentBoards\App\Models\CommentImage;
8 -use FluentBoards\App\Models\Meta;
9 6 use FluentBoards\App\Models\Task;
10 -use FluentBoards\App\Services\Constant;
7 +use FluentBoards\App\Services\AttachmentAccessService;
8 +use FluentBoards\App\Services\PermissionManager;
11 9 use FluentBoards\App\Services\Libs\FileSystem;
12 10
13 11 class ExternalPages
14 12 {
@@ -13,89 +11,59 @@
13 11 class ExternalPages
14 12 {
15 13 public function view_uploaded_comment_image()
16 14 {
17 - $attachmentHash = sanitize_text_field($_REQUEST['fbs_comment_image']);
18 - $boardId = sanitize_text_field($_REQUEST['fbs_bid']);
15 + nocache_headers();
16 + header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
19 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 +
20 22 if (empty($attachmentHash)) {
21 - die('Invalid Attachment Hash');
23 + wp_die(esc_html__('Invalid Attachment Hash', 'fluent-boards'), '', ['response' => 404]);
22 24 }
23 25
24 26 $attachment = $this->getUploadedImageByHash($attachmentHash);
25 27
26 28 if (!$attachment) {
27 - die('Invalid Attachment Hash');
29 + wp_die(esc_html__('Invalid Attachment Hash', 'fluent-boards'), '', ['response' => 404]);
28 30 }
29 31
30 - $fileName = $attachment->file_path;
31 - $boardId = $boardId;
32 - $filePath = $fileName;
33 - if(!file_exists($fileName)){
34 - $filePath = FileSystem::setSubDir('board_' . $boardId)->getDir() . DIRECTORY_SEPARATOR . $fileName;
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 35 }
36 36
37 - if (!file_exists($filePath)) {
38 - die('File could not be found.');
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]);
39 41 }
40 42
41 - $this->serveLocalAttachment($attachment, $filePath);
42 - }
43 -
44 - public function view_comment_image()
45 - {
46 - $attachmentHash = sanitize_text_field($_REQUEST['fbs_comment_image']);
47 -
48 - if (empty($attachmentHash)) {
49 - die('Invalid Attachment Hash');
50 - }
51 -
52 - $attachment = $this->getUploadedImageByHash($attachmentHash);
53 -
54 - if (in_array($attachment->object_type, [Constant::COMMENT_IMAGE])) {
55 - $attachment->load('comment');
56 - } elseif (in_array($attachment->object_type, [Constant::TASK_DESCRIPTION])) {
57 - $attachment['task'] = Task::find($attachment->object_id);
58 - }
59 -
60 - if (!$attachment) {
61 - die('Invalid Attachment Hash');
62 - }
63 -
64 - // check signature hash
65 - if (!$this->validateAttachmentSignature($attachment)) {
66 - $dieMessage = __('Sorry, Your secure sign is invalid, Please reload the previous page and get new signed url', 'fluent-support');
67 - die($dieMessage);
68 - }
69 -
70 - //If external file
71 43 if ('local' !== $attachment->driver) {
72 44 if(!empty($attachment->file_path)){
73 45 $this->redirectToExternalAttachment($attachment->full_url);
74 46 }else{
75 - die('File could not be found');
47 + wp_die(esc_html__('File could not be found', 'fluent-boards'), '', ['response' => 404]);
76 48 }
49 + return;
77 50 }
78 51
79 - //Handle Local file
80 - if (in_array($attachment->object_type, [Constant::COMMENT_IMAGE])) {
81 - $fileName = $attachment->file_path;
82 - $boardId = $attachment->comment->board_id;
83 - } elseif (in_array($attachment->object_type, [Constant::TASK_DESCRIPTION])) {
84 - $fileName = $attachment->file_path;
85 - $boardId = $attachment->task->board_id;
86 - }
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);
87 55
88 - $filePath = $fileName;
89 - if(!file_exists($fileName)){
90 - $filePath = FileSystem::setSubDir('board_' . $boardId)->getDir() . DIRECTORY_SEPARATOR . $fileName;
56 + if (!$filePath || !is_readable($filePath)) {
57 + wp_die(esc_html__('File could not be found.', 'fluent-boards'), '', ['response' => 404]);
91 58 }
92 59
93 - if (!file_exists($filePath)) {
94 - die('File could not be found.');
95 - }
60 + $this->serveLocalAttachment($attachment, $filePath);
61 + }
96 62
97 - $this->serveLocalAttachment($attachment, $filePath);
63 + public function view_comment_image()
64 + {
65 + $this->view_uploaded_comment_image();
98 66 }
99 67
100 68 private function getUploadedImageByHash($attachmentHash)
101 69 {
@@ -103,17 +71,70 @@
103 71 }
104 72
105 73 private function serveLocalAttachment($attachment, $filePath)
106 74 {
107 - ob_get_clean();
75 + if (ob_get_level()) {
76 + ob_end_clean();
77 + }
108 78 header("Content-Type: {$attachment->attachment_type}");
109 - header("Content-Disposition: inline; filename=\"{$attachment->title}\"");;
110 - echo readfile($filePath);
79 + header("Content-Disposition: inline; filename=\"{$attachment->title}\"");
80 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Serving binary file content directly to browser, WP_Filesystem not suitable for this use case
81 + readfile($filePath);
111 82 die();
112 83 }
113 84
114 - private function validateAttachmentSignature($attachment)
85 + public function redirectToPage()
115 86 {
116 - $sign = md5($attachment->id . date('YmdH'));
117 - return $sign === $_REQUEST['secure_sign'];
87 + nocache_headers();
88 + header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
89 +
90 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect; current board access is checked below
91 + $taskId = isset($_GET['taskId']) && is_scalar($_GET['taskId']) ? absint(wp_unslash($_GET['taskId'])) : 0;
92 +
93 + if (!$taskId) {
94 + wp_die(esc_html__('Task not found', 'fluent-boards'), '', ['response' => 404]);
95 + }
96 +
97 + // Authenticate before looking up task metadata, retaining the email link after login.
98 + if (!is_user_logged_in()) {
99 + $returnUrl = add_query_arg(['redirect' => 'to_task', 'taskId' => $taskId], site_url('/'));
100 + // Match WordPress login redirects, including trusted SSO login_url filters.
101 + wp_redirect(wp_login_url($returnUrl));
102 + exit;
103 + }
104 +
105 + $task = Task::find($taskId);
106 + if (!$task || !PermissionManager::userHasPermission($task->board_id)) {
107 + wp_die(esc_html__('Task not found', 'fluent-boards'), '', ['response' => 404]);
108 + }
109 + if ($this->isFrontendEnabled() == 'no') {
110 + $urlBase = apply_filters('fluent_boards/app_url', admin_url('admin.php?page=fluent-boards#/'));
111 + $page_url = $urlBase . 'boards/' . $task->board_id . '/tasks/' . $task->id . '-' .substr($task->title, 0, 10);
112 + wp_redirect($page_url);
113 + exit;
114 + } else {
115 + $urlBase = apply_filters('fluent_boards/app_url', admin_url('admin.php?page=fluent-boards#/'));
116 + $page_url = $urlBase . 'boards/' . $task->board_id . '/tasks/' . $task->id . '-' .substr($task->title, 0, 10);
117 + wp_redirect($page_url);
118 + exit;
119 + }
120 +
121 + die();
118 122 }
119 -}
123 +
124 + private function isFrontendEnabled()
125 + {
126 + $storedSettings = get_option('fluent_boards_modules', []);
127 + $settings = is_string($storedSettings) ? maybe_unserialize($storedSettings) : $storedSettings;
128 +
129 + if (is_array($settings) && isset($settings['frontend']['enabled'])) {
130 + return $settings['frontend']['enabled'];
131 + }
132 +
133 + return 'no';
134 + }
135 + private function redirectToExternalAttachment($redirectUrl)
136 + {
137 + wp_redirect($redirectUrl, 307);
138 + exit();
139 + }
140 +}