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
← All changes | app/Services/Libs/FileSystem.php +112 -50 2.1.0 → 2.2.1 View file →
@@ -2,8 +2,10 @@
2 2
3 3 namespace FluentBoards\App\Services\Libs;
4 4
5 5 use FluentBoards\Framework\Support\Arr;
6 +use FluentBoards\App\Services\UploadService;
7 +use Exception;
6 8
7 9 class FileSystem
8 10 {
9 11 protected $subDir = '';
@@ -20,11 +22,8 @@
20 22 public function _get($file)
21 23 {
22 24 $arr = explode('/', $file);
23 25 $fileName = end($arr);
24 - if ($this->subDir) {
25 - $fileName = $this->subDir . DIRECTORY_SEPARATOR . $fileName;
26 - }
27 26
28 27 $filePath = $this->getDir() . DIRECTORY_SEPARATOR . $fileName;
29 28
30 29 // Use WordPress Filesystem API
@@ -54,8 +53,62 @@
54 53 return $uploadDir['basedir'] . DIRECTORY_SEPARATOR . $fbsUploadDir;
55 54 }
56 55
57 56 /**
57 + * Legacy Core/Pro entry point: prepare storage, not a verified security boundary.
58 + * Rule writes are best effort; $allowProbe is retained for caller compatibility.
59 + */
60 + public function _installUploadProtectionRules($allowProbe = true)
61 + {
62 + $directory = (new static())->_getDir();
63 + if (!wp_mkdir_p($directory)) {
64 + return false;
65 + }
66 + $path = $directory . '/.htaccess';
67 + if (!$this->hasUploadProtectionRules($path)) {
68 + if (!function_exists('insert_with_markers')) {
69 + require_once ABSPATH . 'wp-admin/includes/misc.php';
70 + }
71 + insert_with_markers($path, 'Fluent Boards', $this->uploadProtectionRules());
72 + }
73 + return true;
74 + }
75 +
76 + protected function uploadProtectionRules()
77 + {
78 + return [
79 + '<IfModule mod_authz_core.c>',
80 + 'Require all denied',
81 + '</IfModule>',
82 + '<IfModule !mod_authz_core.c>',
83 + 'Order deny,allow',
84 + 'Deny from all',
85 + '</IfModule>',
86 + ];
87 + }
88 +
89 + protected function hasUploadProtectionRules($path)
90 + {
91 + if (!is_file($path) || !is_readable($path)) {
92 + return false;
93 + }
94 + $contents = str_replace("\r\n", "\n", file_get_contents($path));
95 + if (preg_match_all('/^# BEGIN Fluent Boards\n(.*?)^# END Fluent Boards$/ms', $contents, $blocks) !== 1) {
96 + return false;
97 + }
98 + $rules = array_values(array_filter(array_map('trim', explode("\n", $blocks[1][0])), function ($line) {
99 + return $line !== '' && strpos($line, '#') !== 0;
100 + }));
101 + return $rules === $this->uploadProtectionRules();
102 + }
103 +
104 + /** Legacy adapter for older Pro callers; protection is no longer probed over HTTP. */
105 + public function _checkUploadProtection($allowProbe = true)
106 + {
107 + return null;
108 + }
109 +
110 + /**
58 111 * Get absolute path of file using custom upload dir name of this application
59 112 * @return string [file path]
60 113 */
61 114 public function _getAbsolutePathOfFile($file)
@@ -133,9 +186,9 @@
133 186 return strpos($filePath, $allowedRoot) === 0 ? $filePath : null;
134 187 }
135 188
136 189 /**
137 - * Upload files into custom upload dir of this application
190 + * Upload request files into the application's custom upload directory.
138 191 * @return array
139 192 */
140 193 public function _uploadFromRequest()
141 194 {
@@ -142,45 +195,59 @@
142 195 return $this->_put(FluentBoards('request')->files());
143 196 }
144 197
145 198 /**
146 - * Upload files into custom upload dir of this application
199 + * Prepare upload-directory protection before accepting local files.
147 200 * @param array $files
148 201 * @return array
149 202 */
150 203 public function _put($files)
151 204 {
205 + if (!$this->_installUploadProtectionRules()) {
206 + throw new Exception(__('Could not create the upload directory. Please check directory permissions.', 'fluent-boards'));
207 + }
208 +
152 209 if (!function_exists('wp_handle_upload')) {
153 210 require_once(ABSPATH . 'wp-admin/includes/file.php');
154 211 }
155 212
156 - $this->overrideUploadDir();
157 -
158 213 $uploadOverrides = [
159 214 'test_form' => false,
160 - // Accept the same allow-list FluentBoards validates against, so wp_handle_upload
161 - // doesn't reject broader types (e.g. .json, .md) by extension.
162 - 'mimes' => \FluentBoards\App\Services\UploadService::getAllowedMimeMap(),
215 + 'mimes' => UploadService::getAllowedMimeMap(),
163 216 ];
164 - $uploadedFiles = []; // Initialize the array
217 + $uploadedFiles = [];
218 + $files = is_object($files) ? [$files] : (array) $files;
165 219
166 - if(is_object($files)) {
167 - $files = [$files];
168 - }
220 + $this->_overrideUploadDir();
221 + try {
222 + foreach ($files as $file) {
223 + $filesArray = $file->toArray();
224 + $filesArray['tmp_name'] = $file->getRealPath();
225 + $extraData = Arr::only($filesArray, ['name', 'size']);
226 + if (empty($filesArray['error']) && is_file($filesArray['tmp_name'])) {
227 + $filesArray['size'] = filesize($filesArray['tmp_name']);
228 + $filesArray['size_in_bytes'] = $filesArray['size'];
229 + (new UploadService())->validateFile($filesArray);
230 + }
231 + $uploadsData = wp_handle_upload($filesArray, $uploadOverrides);
232 + if (!empty($uploadsData['error']) || empty($uploadsData['file'])) {
233 + throw new Exception($uploadsData['error'] ?? __('File upload failed.', 'fluent-boards'));
234 + }
169 235
170 - foreach ((array)$files as $file) {
171 -
172 - $filesArray = $file->toArray();
173 -
174 - // tmp_name is the path to the uploaded file which is required for wp_handle_upload, new framework update
175 - // changed the way to get the tmp_name Reference to line # 448 in File.php
176 - $filesArray['tmp_name'] = $file->getRealPath();
177 -
178 - $extraData = Arr::only($filesArray, ['name', 'size']);
179 - $uploadsData = \wp_handle_upload($filesArray, $uploadOverrides);
180 - // Add the full path to the file
181 - $uploadsData['full_path'] = $this->_getAbsolutePathOfFile($uploadsData['file']);
182 - $uploadedFiles[] = array_merge($extraData, $uploadsData);
236 + // Keep WordPress's actual path; callers also expect a legacy bare filename.
237 + $uploadsData['full_path'] = $uploadsData['file'];
238 + $uploadsData['file'] = basename($uploadsData['file']);
239 + $uploadedFiles[] = array_merge($extraData, $uploadsData);
240 + }
241 + } catch (Exception $exception) {
242 + // The batch fails as a whole, so remove files this call already stored.
243 + foreach ($uploadedFiles as $uploadedFile) {
244 + wp_delete_file($uploadedFile['full_path']);
245 + }
246 + throw $exception;
247 + } finally {
248 + remove_filter('wp_handle_upload_prefilter', [$this, '_renameFileName']);
249 + remove_filter('upload_dir', [$this, '_setCustomUploadDir']);
183 250 }
184 251
185 252 return $uploadedFiles;
186 253 }
@@ -208,19 +275,10 @@
208 275 * Register filters for custom upload dir
209 276 */
210 277 public function _overrideUploadDir()
211 278 {
212 - add_filter('wp_handle_upload_prefilter', function ($file) {
213 - add_filter('upload_dir', [$this, '_setCustomUploadDir']);
214 -
215 - add_filter('wp_handle_upload', function ($fileinfo) {
216 - remove_filter('upload_dir', [$this, '_setCustomUploadDir']);
217 - $fileinfo['file'] = basename($fileinfo['file']);
218 - return $fileinfo;
219 - });
220 -
221 - return $this->_renameFileName($file);
222 - });
279 + add_filter('wp_handle_upload_prefilter', [$this, '_renameFileName']);
280 + add_filter('upload_dir', [$this, '_setCustomUploadDir']);
223 281 }
224 282
225 283 /**
226 284 * Set plugin's custom upload dir
@@ -231,19 +289,17 @@
231 289 {
232 290 $fbsUploadDir = apply_filters('fluent_boards/upload_folder_name', FLUENT_BOARDS_UPLOAD_DIR);
233 291
234 292 if ($this->subDir) {
235 - $fbsUploadDir .= DIRECTORY_SEPARATOR . $this->subDir;
236 - if (!is_dir($param['basedir'] . $fbsUploadDir)) {
237 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- Creating custom upload subdirectory during upload process, WP_Filesystem not initialized
238 - @mkdir($param['basedir'] . $fbsUploadDir, 0755);
239 - }
293 + $fbsUploadDir .= '/' . $this->subDir;
240 294 }
241 295
242 - $param['url'] = $param['baseurl'] . DIRECTORY_SEPARATOR . $fbsUploadDir;
296 + $param['url'] = trailingslashit($param['baseurl']) . trim($fbsUploadDir, '/');
297 + $param['path'] = trailingslashit($param['basedir']) . trim($fbsUploadDir, '/');
298 + if (!wp_mkdir_p($param['path'])) {
299 + $param['error'] = __('Could not create the upload directory.', 'fluent-boards');
300 + }
243 301
244 - $param['path'] = $param['basedir'] . DIRECTORY_SEPARATOR . $fbsUploadDir;
245 -
246 302 return $param;
247 303 }
248 304
249 305 /**
@@ -261,12 +317,18 @@
261 317 }
262 318
263 319 public function _deleteDir($dir)
264 320 {
265 - $dir = $this->getDir() . DIRECTORY_SEPARATOR . $dir;
266 - if (is_dir($dir)) {
267 - $this->deleteContents($dir);
321 + $root = realpath($this->_getDir());
322 + $path = $root ? $root . DIRECTORY_SEPARATOR . $dir : '';
323 + $resolved = $path ? realpath($path) : false;
324 + // Never remove the storage root, escape it, or follow a directory symlink.
325 + if (!$root || !$resolved || is_link($path)
326 + || strpos($resolved, $root . DIRECTORY_SEPARATOR) !== 0) {
327 + return false;
268 328 }
329 +
330 + return is_dir($resolved) ? $this->deleteContents($resolved) : false;
269 331 }
270 332
271 333 /**
272 334 * Delete a directory and its content
@@ -277,9 +339,9 @@
277 339 private function deleteContents($dir)
278 340 {
279 341 $files = array_diff(scandir($dir), ['.', '..']);
280 342 foreach ($files as $file) {
281 - if(is_dir("$dir/$file")) {
343 + if (!is_link("$dir/$file") && is_dir("$dir/$file")) {
282 344 // Recursively delete subdirectory
283 345 $this->deleteContents("$dir/$file");
284 346 } else {
285 347 // Delete file