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/FileHandler.php +130 -40 1.21 → 2.2.0 View file →
@@ -3,12 +3,10 @@
3 3 namespace FluentBoards\App\Hooks\Handlers;
4 4
5 5 use DateTimeImmutable;
6 6 use Exception;
7 -use FluentBoards\App\Models\Meta;
8 -use FluentBoards\App\Services\Constant;
9 7 use FluentBoards\App\Services\Libs\FileSystem;
10 -use FluentBoardsPro\App\Models\Attachment;
8 +use FluentBoards\App\Models\Attachment;
11 9 use function Sodium\add;
12 10
13 11 class FileHandler
14 12 {
@@ -34,43 +32,129 @@
34 32 );
35 33 }
36 34
37 35 /**
38 - * Summary of deleteFileByUrl
39 - * @param mixed $file_url
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().
40 46 * @return bool
41 47 */
42 - public function deleteFileByUrl($file_url)
48 + public function deleteAttachmentFile($attachment, $boardId = null, $sharedReferences = null)
43 49 {
44 - $exists = Attachment::where('full_url', $file_url)->exists();
45 - if($exists) {
46 - return;
50 + if (
51 + !$attachment ||
52 + $attachment->attachment_type === 'url' ||
53 + empty($attachment->file_path)
54 + ) {
55 + return false;
47 56 }
48 - // Convert the URL to the local file path
49 - $upload_dir = wp_upload_dir();
50 - $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_url);
51 57
52 - // Check if the file exists
53 - if (file_exists($file_path)) {
54 - // Delete the file
55 - $deleted = unlink($file_path);
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 + }
56 63
57 - // Optionally, you can also remove the file from the media library
58 - // Note: This won't delete the file physically, but it will remove it from the media library
59 - $attachment_id = attachment_url_to_postid($file_url);
60 - if ($attachment_id) {
61 - wp_delete_attachment($attachment_id, true);
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;
62 86 }
87 + } elseif ($this->hasSharedLocalReference('file_path', $attachment->file_path, $sharedReferences)) {
88 + return false;
89 + }
63 90
64 - // Return true if the file was successfully deleted
65 - return $deleted;
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 + }
66 110 }
67 - // Return false if the file does not exist
68 - return false;
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;
69 122 }
70 123
124 + private function hasSharedLocalReference($column, $value, $sharedReferences)
125 + {
126 + if (is_array($sharedReferences)) {
127 + return isset($sharedReferences[$column][(string) $value]);
128 + }
71 129
130 + return Attachment::where($column, $value)->exists();
131 + }
132 +
133 +
72 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 + /**
73 157 * Summary of isFileTypeSupported checking file type it will allow only file which is readable by browser
74 158 * @param mixed $file
75 159 * TODO: Refactorable: This can be in a Helper class. and we may pass it to frontend via wp_localize_script appvars
76 160 * so that we can check similarly for better experience.
@@ -89,13 +173,31 @@
89 173 * @throws Exception
90 174 */
91 175 public function handleMediaFileUpload($data)
92 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 +
93 195 // Check if the uploaded file is an image
94 - $wp_filetype = wp_check_filetype_and_ext( $_FILES['file']['tmp_name'], $_FILES['file']['name'] );
196 + $wp_filetype = wp_check_filetype_and_ext($tmp_name, $filename);
95 197
96 198 if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
97 - throw new Exception('The uploaded file is not a valid image. Please try again.');
199 + throw new Exception(esc_html__('The uploaded file is not a valid image. Please try again.', 'fluent-boards'));
98 200 }
99 201 require_once(ABSPATH . 'wp-admin/includes/image.php');
100 202 require_once( ABSPATH . 'wp-admin/includes/file.php' );
101 203 require_once( ABSPATH . 'wp-admin/includes/media.php' );
@@ -102,23 +204,11 @@
102 204 $attachment_id = media_handle_upload( 'file', 0, [] );
103 205
104 206 $attachment = wp_prepare_attachment_for_js( $attachment_id);
105 207 if(!$attachment) {
106 - throw new Exception('The uploaded file is not a valid image. Please try again.');
208 + throw new Exception(esc_html__('The uploaded file is not a valid image. Please try again.', 'fluent-boards'));
107 209 } else {
108 210 return $attachment;
109 211 }
110 212 }
111 213
112 - /**
113 - * This function will delete meta where default board default image is used.
114 - * @param $id
115 - * @return void
116 - */
117 - public function mediaFileDeleted($id)
118 - {
119 - $boardImage = Meta::where('object_id', $id)->where('object_type', Constant::BOARD_DEFAULT_IMAGE)->first();
120 - if($boardImage) {
121 - $boardImage->delete();
122 - }
123 - }
124 214 }