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.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 / FileHandler.php

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

215 lines 8.1 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 DateTimeImmutable;
6 use Exception;
7 use FluentBoards\App\Services\Libs\FileSystem;
8 use FluentBoards\App\Models\Attachment;
9 use function Sodium\add;
10
11 class FileHandler
12 {
13 // private function validateFile($file)
14 // {
15 // if (!$file) {
16 // throw new Exception('File is empty.');
17 // }
18 // if (!$this->isFileTypeSupported($file)) {
19 // throw new Exception('File type not supported');
20 // }
21 // if ($file['size'] > $this->getFileUploadLimit()) {
22 // throw new Exception('File size is too large');
23 // }
24 // }
25
26 private function getFileUploadLimit() {
27 // Logic for calculating file upload limit as in your original code
28 return min(
29 wp_convert_hr_to_bytes(ini_get('upload_max_filesize')),
30 wp_convert_hr_to_bytes(ini_get('post_max_size')),
31 wp_max_upload_size()
32 );
33 }
34
35 /**
36 * Delete an attachment's stored file once no attachment row references it.
37 *
38 * Local files are removed here, but only when the stored path belongs to
39 * Fluent Boards. Files on a cloud driver are handed to the storage add-on
40 * through the `fluent_boards/delete_remote_attachment_file` action, because
41 * the core plugin holds no bucket credentials.
42 *
43 * @param mixed $attachment Attachment row or snapshot (driver, file_path, attachment_type).
44 * @param int|null $boardId
45 * @param array|null $sharedReferences Preloaded local references from getSharedLocalReferences().
46 * @return bool
47 */
48 public function deleteAttachmentFile($attachment, $boardId = null, $sharedReferences = null)
49 {
50 if (
51 !$attachment ||
52 $attachment->attachment_type === 'url' ||
53 empty($attachment->file_path)
54 ) {
55 return false;
56 }
57
58 if (!empty($attachment->driver) && $attachment->driver !== 'local') {
59 // Cloned tasks share the remote object, so keep it while another row points at it.
60 if (Attachment::where('file_path', $attachment->file_path)->exists()) {
61 return false;
62 }
63
64 do_action('fluent_boards/delete_remote_attachment_file', $attachment);
65
66 return true;
67 }
68
69 $storedFilename = rawurldecode((string) $attachment->file_path);
70 $isBareFilename = $storedFilename !== ''
71 && strpos($storedFilename, '/') === false
72 && strpos($storedFilename, '\\') === false;
73 $filePath = FileSystem::resolveLocalAttachmentPath($attachment->file_path, $boardId);
74
75 if (!$filePath) {
76 return false;
77 }
78
79 // Legacy filenames need the board-qualified URL to distinguish same-named files across boards.
80 if ($isBareFilename) {
81 if (
82 empty($attachment->full_url) ||
83 $this->hasSharedLocalReference('full_url', $attachment->full_url, $sharedReferences)
84 ) {
85 return false;
86 }
87 } elseif ($this->hasSharedLocalReference('file_path', $attachment->file_path, $sharedReferences)) {
88 return false;
89 }
90
91 return (bool) wp_delete_file($filePath);
92 }
93
94 /**
95 * Batch the shared-reference lookups deleteAttachmentFile() makes for local files.
96 *
97 * @param iterable $attachments Deleted attachment rows or snapshots.
98 * @return array ['file_path' => [value => true], 'full_url' => [value => true]]
99 */
100 public function getSharedLocalReferences($attachments)
101 {
102 $values = ['file_path' => [], 'full_url' => []];
103 foreach ($attachments as $attachment) {
104 if (!empty($attachment->file_path)) {
105 $values['file_path'][] = (string) $attachment->file_path;
106 }
107 if (!empty($attachment->full_url)) {
108 $values['full_url'][] = (string) $attachment->full_url;
109 }
110 }
111
112 $references = ['file_path' => [], 'full_url' => []];
113 foreach ($values as $column => $columnValues) {
114 foreach (array_chunk(array_values(array_unique($columnValues)), 200) as $chunk) {
115 foreach (Attachment::whereIn($column, $chunk)->pluck($column)->toArray() as $value) {
116 $references[$column][(string) $value] = true;
117 }
118 }
119 }
120
121 return $references;
122 }
123
124 private function hasSharedLocalReference($column, $value, $sharedReferences)
125 {
126 if (is_array($sharedReferences)) {
127 return isset($sharedReferences[$column][(string) $value]);
128 }
129
130 return Attachment::where($column, $value)->exists();
131 }
132
133
134 /**
135 * Action Scheduler callback: delete the remote files of attachment rows that
136 * were removed in bulk (a deleted board), outside the request that deleted them.
137 *
138 * @param array $files List of ['file_path' => string, 'driver' => string].
139 * @return void
140 */
141 public function deleteRemoteAttachmentFiles($files)
142 {
143 foreach ((array) $files as $file) {
144 if (!is_array($file) || empty($file['file_path']) || empty($file['driver'])) {
145 continue;
146 }
147
148 $this->deleteAttachmentFile((object) [
149 'attachment_type' => 'file',
150 'driver' => sanitize_key($file['driver']),
151 'file_path' => (string) $file['file_path'],
152 ]);
153 }
154 }
155
156 /**
157 * Summary of isFileTypeSupported checking file type it will allow only file which is readable by browser
158 * @param mixed $file
159 * TODO: Refactorable: This can be in a Helper class. and we may pass it to frontend via wp_localize_script appvars
160 * so that we can check similarly for better experience.
161 * @return bool
162 */
163 public function isFileTypeSupported($file)
164 {
165 // Define supported file types that are generally allowed by user
166 $allowedMimeTypes = get_allowed_mime_types();
167
168 // Check if the file type is supported
169 return in_array(strtolower($file['type']), $allowedMimeTypes);
170 }
171
172 /**
173 * @throws Exception
174 */
175 public function handleMediaFileUpload($data)
176 {
177 // Check if file was uploaded
178 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification handled by REST API/controller layer
179 if (!isset($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) {
180 throw new Exception(esc_html__('No file was uploaded. Please try again.', 'fluent-boards'));
181 }
182
183 // Sanitize filename from request for validation
184 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification handled by REST API/controller layer
185 $filename = sanitize_file_name(wp_unslash($_FILES['file']['name']));
186
187 // Validate and sanitize tmp_name before use
188 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification handled by REST API/controller layer
189 $tmp_name = sanitize_text_field(wp_unslash($_FILES['file']['tmp_name']));
190
191 if (empty($tmp_name) || !file_exists($tmp_name) || !is_uploaded_file($tmp_name)) {
192 throw new Exception(esc_html__('Invalid upload. Please try again.', 'fluent-boards'));
193 }
194
195 // Check if the uploaded file is an image
196 $wp_filetype = wp_check_filetype_and_ext($tmp_name, $filename);
197
198 if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
199 throw new Exception(esc_html__('The uploaded file is not a valid image. Please try again.', 'fluent-boards'));
200 }
201 require_once(ABSPATH . 'wp-admin/includes/image.php');
202 require_once( ABSPATH . 'wp-admin/includes/file.php' );
203 require_once( ABSPATH . 'wp-admin/includes/media.php' );
204 $attachment_id = media_handle_upload( 'file', 0, [] );
205
206 $attachment = wp_prepare_attachment_for_js( $attachment_id);
207 if(!$attachment) {
208 throw new Exception(esc_html__('The uploaded file is not a valid image. Please try again.', 'fluent-boards'));
209 } else {
210 return $attachment;
211 }
212 }
213
214 }
215