PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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 1.41 1.45 All 41 releases
← All changes | app/Services/Libs/FileSystem.php +138 -7 1.11trunk View file →
@@ -23,11 +23,19 @@
23 23 $fileName = end($arr);
24 24 if ($this->subDir) {
25 25 $fileName = $this->subDir . DIRECTORY_SEPARATOR . $fileName;
26 26 }
27 - return file_get_contents(
28 - $this->getDir() . DIRECTORY_SEPARATOR . $fileName
29 - );
27 +
28 + $filePath = $this->getDir() . DIRECTORY_SEPARATOR . $fileName;
29 +
30 + // Use WordPress Filesystem API
31 + global $wp_filesystem;
32 + if (!function_exists('WP_Filesystem')) {
33 + require_once(ABSPATH . 'wp-admin/includes/file.php');
34 + }
35 + WP_Filesystem();
36 +
37 + return $wp_filesystem->get_contents($filePath);
30 38 }
31 39
32 40 /**
33 41 * Get custom upload dir name of this application
@@ -55,8 +63,78 @@
55 63 return $this->_getDir() . DIRECTORY_SEPARATOR . $file;
56 64 }
57 65
58 66 /**
67 + * Resolve a stored attachment path only when it belongs to Fluent Boards uploads.
68 + *
69 + * @param string $storedPath
70 + * @param int|null $boardId
71 + * @return string|null
72 + */
73 + public function _resolveLocalAttachmentPath($storedPath, $boardId = null)
74 + {
75 + if (!$storedPath) {
76 + return null;
77 + }
78 +
79 + $storedFilename = rawurldecode((string) $storedPath);
80 + $isBareFilename = $storedFilename !== ''
81 + && strpos($storedFilename, '/') === false
82 + && strpos($storedFilename, '\\') === false;
83 +
84 + if ($isBareFilename && $boardId === null) {
85 + return null;
86 + }
87 +
88 + $filePath = $isBareFilename ? null : realpath($storedPath);
89 + $allowedDirectory = $this->_getDir();
90 + $pluginRoot = realpath($allowedDirectory);
91 +
92 + if (!$pluginRoot) {
93 + return null;
94 + }
95 +
96 + $pluginRoot = rtrim($pluginRoot, DIRECTORY_SEPARATOR);
97 + $allowedRoot = $pluginRoot;
98 +
99 + if ($boardId !== null) {
100 + if (!is_int($boardId) && !is_string($boardId)) {
101 + return null;
102 + }
103 +
104 + $boardId = filter_var($boardId, FILTER_VALIDATE_INT, [
105 + 'options' => ['min_range' => 1],
106 + ]);
107 + if ($boardId === false) {
108 + return null;
109 + }
110 +
111 + $allowedDirectory .= DIRECTORY_SEPARATOR . 'board_' . $boardId;
112 + if (is_link($allowedDirectory)) {
113 + return null;
114 + }
115 +
116 + $allowedRoot = realpath($allowedDirectory);
117 + $expectedBoardRoot = $pluginRoot . DIRECTORY_SEPARATOR . 'board_' . $boardId;
118 + if (!$allowedRoot || $allowedRoot !== $expectedBoardRoot) {
119 + return null;
120 + }
121 + }
122 +
123 + if ($isBareFilename) {
124 + $filePath = realpath($allowedDirectory . DIRECTORY_SEPARATOR . $storedFilename);
125 + }
126 +
127 + if (!$filePath || !$allowedRoot || !is_file($filePath)) {
128 + return null;
129 + }
130 +
131 + $allowedRoot = rtrim($allowedRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
132 +
133 + return strpos($filePath, $allowedRoot) === 0 ? $filePath : null;
134 + }
135 +
136 + /**
59 137 * Upload files into custom upload dir of this application
60 138 * @return array
61 139 */
62 140 public function _uploadFromRequest()
@@ -76,14 +154,32 @@
76 154 }
77 155
78 156 $this->overrideUploadDir();
79 157
80 - $uploadOverrides = ['test_form' => false];
158 + $uploadOverrides = [
159 + '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(),
163 + ];
164 + $uploadedFiles = []; // Initialize the array
81 165
166 + if(is_object($files)) {
167 + $files = [$files];
168 + }
169 +
82 170 foreach ((array)$files as $file) {
171 +
83 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 +
84 178 $extraData = Arr::only($filesArray, ['name', 'size']);
85 179 $uploadsData = \wp_handle_upload($filesArray, $uploadOverrides);
180 + // Add the full path to the file
181 + $uploadsData['full_path'] = $this->_getAbsolutePathOfFile($uploadsData['file']);
86 182 $uploadedFiles[] = array_merge($extraData, $uploadsData);
87 183 }
88 184
89 185 return $uploadedFiles;
@@ -100,9 +196,12 @@
100 196
101 197 foreach ($files as $file) {
102 198 $arr = explode('/', $file);
103 199 $fileName = end($arr);
104 - @unlink($this->getDir() . '/' . $fileName);
200 + $filePath = $this->getDir() . '/' . $fileName;
201 + if (file_exists($filePath)) {
202 + wp_delete_file($filePath);
203 + }
105 204 }
106 205 }
107 206
108 207 /**
@@ -134,8 +233,9 @@
134 233
135 234 if ($this->subDir) {
136 235 $fbsUploadDir .= DIRECTORY_SEPARATOR . $this->subDir;
137 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
138 238 @mkdir($param['basedir'] . $fbsUploadDir, 0755);
139 239 }
140 240 }
141 241
@@ -152,14 +252,45 @@
152 252 * @return array $file
153 253 */
154 254 public function _renameFileName($file)
155 255 {
156 - $currentTimeStamp = (new \DateTimeImmutable())->getTimestamp();
157 - $prefix = $currentTimeStamp . '-';
256 + $prefix = wp_generate_uuid4() . '-';
158 257 $prefix = apply_filters('fluent_boards/uploaded_file_name_prefix', $prefix);
159 258 $file['name'] = $prefix . $file['name'];
160 259
161 260 return $file;
261 + }
262 +
263 + public function _deleteDir($dir)
264 + {
265 + $dir = $this->getDir() . DIRECTORY_SEPARATOR . $dir;
266 + if (is_dir($dir)) {
267 + $this->deleteContents($dir);
268 + }
269 + }
270 +
271 + /**
272 + * Delete a directory and its content
273 + * Lead Note: this method should be called via scheduler or queue
274 + * @param string $dir
275 + * @return bool
276 + */
277 + private function deleteContents($dir)
278 + {
279 + $files = array_diff(scandir($dir), ['.', '..']);
280 + foreach ($files as $file) {
281 + if(is_dir("$dir/$file")) {
282 + // Recursively delete subdirectory
283 + $this->deleteContents("$dir/$file");
284 + } else {
285 + // Delete file
286 + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Recursive directory cleanup, direct file operations required
287 + unlink("$dir/$file");
288 + }
289 + }
290 + // Delete the directory itself. If the directory is not empty, it will not be deleted.
291 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Recursive directory removal, WP_Filesystem not suitable for this operation
292 + return rmdir($dir);
162 293 }
163 294
164 295 public static function __callStatic($method, $params)
165 296 {