1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * This class provides many common file and directory functions such as creating directories, checking existence etc. |
||||
5 | * |
||||
6 | * @package ElkArte Forum |
||||
7 | * @copyright ElkArte Forum contributors |
||||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||||
9 | * |
||||
10 | * @version 2.0 dev |
||||
11 | * |
||||
12 | */ |
||||
13 | |||||
14 | namespace ElkArte\Helper; |
||||
15 | |||||
16 | use Exception; |
||||
17 | |||||
18 | class FileFunctions |
||||
19 | { |
||||
20 | /** @var FileFunctions The instance of the class */ |
||||
21 | private static $_instance; |
||||
22 | |||||
23 | /** |
||||
24 | * chmod control will attempt to make a file or directory writable |
||||
25 | * |
||||
26 | * - Progressively attempts various chmod values until item is writable or failure |
||||
27 | * |
||||
28 | * @param string $item file or directory |
||||
29 | * @return bool |
||||
30 | */ |
||||
31 | public function chmod($item) |
||||
32 | { |
||||
33 | $fileChmod = [0644, 0666]; |
||||
34 | $dirChmod = [0755, 0775, 0777]; |
||||
35 | |||||
36 | // Already writable? |
||||
37 | if ($this->isWritable($item)) |
||||
38 | { |
||||
39 | return true; |
||||
40 | } |
||||
41 | |||||
42 | $modes = $this->isDir($item) ? $dirChmod : $fileChmod; |
||||
43 | foreach ($modes as $mode) |
||||
44 | { |
||||
45 | $this->elk_chmod($item, $mode); |
||||
46 | |||||
47 | if ($this->isWritable($item)) |
||||
48 | { |
||||
49 | clearstatcache(false, $item); |
||||
50 | |||||
51 | return true; |
||||
52 | } |
||||
53 | } |
||||
54 | |||||
55 | return false; |
||||
56 | } |
||||
57 | |||||
58 | /** |
||||
59 | * Simple wrapper around chmod |
||||
60 | * |
||||
61 | * - Checks proper value for mode if one is supplied |
||||
62 | * - Consolidates chmod error suppression to single function |
||||
63 | * |
||||
64 | * @param string $item |
||||
65 | * @param string|int $mode |
||||
66 | * |
||||
67 | * @return bool |
||||
68 | */ |
||||
69 | public function elk_chmod($item, $mode = '') |
||||
70 | { |
||||
71 | $result = false; |
||||
72 | $mode = trim($mode); |
||||
73 | |||||
74 | if (empty($mode) || !is_numeric($mode)) |
||||
75 | { |
||||
76 | $mode = $this->isDir($item) ? 0755 : 0644; |
||||
77 | } |
||||
78 | |||||
79 | // Make sure we have a form of 0777 or '777' or '0777' so its safe for intval '8' |
||||
80 | if (($mode % 10) >= 8) |
||||
81 | { |
||||
82 | $mode = decoct($mode); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
83 | } |
||||
84 | |||||
85 | // All numbers and outside octal range, safely convert to octal |
||||
86 | if (ctype_digit((string) $mode) && preg_match('~[8-9]~', $mode)) |
||||
87 | { |
||||
88 | $mode = decoct($mode); |
||||
89 | } |
||||
90 | |||||
91 | // Happens when passed the octal value 0777 (not string) which is 511 decimal, we work on the |
||||
92 | // assumption no one is trying to do a chmod 511 |
||||
93 | if (in_array($mode, [511, 420, 436], true)) |
||||
94 | { |
||||
95 | $mode = decoct($mode); |
||||
96 | } |
||||
97 | |||||
98 | if ($mode == decoct(octdec($mode))) |
||||
0 ignored issues
–
show
It seems like
octdec($mode) can also be of type double ; however, parameter $num of decoct() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
99 | { |
||||
100 | return @chmod($item, intval($mode, 8)); |
||||
101 | } |
||||
102 | |||||
103 | return $result; |
||||
104 | } |
||||
105 | |||||
106 | /** |
||||
107 | * is_dir() helper using spl functions. is_dir can throw an exception if open_basedir |
||||
108 | * restrictions are in effect. |
||||
109 | * |
||||
110 | * @param string $dir |
||||
111 | * @return bool |
||||
112 | */ |
||||
113 | public function isDir($dir) |
||||
114 | { |
||||
115 | try |
||||
116 | { |
||||
117 | $splDir = new \SplFileInfo($dir); |
||||
118 | if ($splDir->isDir() && $splDir->getType() === 'dir' && !$splDir->isLink()) |
||||
119 | { |
||||
120 | return true; |
||||
121 | } |
||||
122 | } |
||||
123 | catch (\RuntimeException) |
||||
124 | { |
||||
125 | return false; |
||||
126 | } |
||||
127 | |||||
128 | return false; |
||||
129 | } |
||||
130 | |||||
131 | /** |
||||
132 | * file_exists() helper. file_exists can throw an E_WARNING on failure. |
||||
133 | * Returns true if the filename (not a directory or link) exists. |
||||
134 | * |
||||
135 | * @param string $item a file or directory location |
||||
136 | * @return bool |
||||
137 | */ |
||||
138 | public function fileExists($item) |
||||
139 | { |
||||
140 | try |
||||
141 | { |
||||
142 | $fileInfo = new \SplFileInfo($item); |
||||
143 | if ($fileInfo->isFile() && !$fileInfo->isLink()) |
||||
144 | { |
||||
145 | return true; |
||||
146 | } |
||||
147 | } |
||||
148 | catch (\RuntimeException) |
||||
149 | { |
||||
150 | return false; |
||||
151 | } |
||||
152 | |||||
153 | return false; |
||||
154 | } |
||||
155 | |||||
156 | /** |
||||
157 | * fileperms() helper using spl functions. fileperms can throw an e-warning |
||||
158 | * |
||||
159 | * @param string $item |
||||
160 | * @return int|bool |
||||
161 | */ |
||||
162 | public function filePerms($item) |
||||
163 | { |
||||
164 | try |
||||
165 | { |
||||
166 | $fileInfo = new \SplFileInfo($item); |
||||
167 | if ($perms = $fileInfo->getPerms()) |
||||
168 | { |
||||
169 | return $perms; |
||||
170 | } |
||||
171 | } |
||||
172 | catch (\RuntimeException) |
||||
173 | { |
||||
174 | return false; |
||||
175 | } |
||||
176 | |||||
177 | return false; |
||||
178 | } |
||||
179 | |||||
180 | /** |
||||
181 | * filesize() helper. filesize can throw an E_WARNING on failure. |
||||
182 | * Returns the filesize in bytes on success or false on failure. |
||||
183 | * |
||||
184 | * @param string $item a file location |
||||
185 | * @return int|bool |
||||
186 | */ |
||||
187 | public function fileSize($item) |
||||
188 | { |
||||
189 | try |
||||
190 | { |
||||
191 | $fileInfo = new \SplFileInfo($item); |
||||
192 | $size = $fileInfo->getSize(); |
||||
193 | } |
||||
194 | catch (\RuntimeException) |
||||
195 | { |
||||
196 | $size = false; |
||||
197 | } |
||||
198 | |||||
199 | return $size; |
||||
200 | } |
||||
201 | |||||
202 | /** |
||||
203 | * is_writable() helper. is_writable can throw an E_WARNING on failure. |
||||
204 | * Returns true if the filename/directory exists and is writable. |
||||
205 | * |
||||
206 | * @param string $item a file or directory location |
||||
207 | * @return bool |
||||
208 | */ |
||||
209 | public function isWritable($item) |
||||
210 | { |
||||
211 | try |
||||
212 | { |
||||
213 | $fileInfo = new \SplFileInfo($item); |
||||
214 | if ($fileInfo->isWritable()) |
||||
215 | { |
||||
216 | return true; |
||||
217 | } |
||||
218 | } |
||||
219 | catch (\RuntimeException) |
||||
220 | { |
||||
221 | return false; |
||||
222 | } |
||||
223 | |||||
224 | return false; |
||||
225 | } |
||||
226 | |||||
227 | /** |
||||
228 | * Creates a directory as defined by a supplied path |
||||
229 | * |
||||
230 | * What it does: |
||||
231 | * |
||||
232 | * - Attempts to make the directory writable |
||||
233 | * - Will create a full tree structure |
||||
234 | * - Optionally places an .htaccess in created directories for security |
||||
235 | * |
||||
236 | * @param string $path the path to fully create |
||||
237 | * @param bool $makeSecure if to create .htaccess file in created directory |
||||
238 | * @return bool |
||||
239 | * @throws \Exception |
||||
240 | */ |
||||
241 | public function createDirectory($path, $makeSecure = true) |
||||
242 | { |
||||
243 | // Path already exists? |
||||
244 | if (file_exists($path)) |
||||
245 | { |
||||
246 | if ($this->isDir($path)) |
||||
247 | { |
||||
248 | return true; |
||||
249 | } |
||||
250 | |||||
251 | // A file exists at this location with this name |
||||
252 | throw new Exception('attach_dir_duplicate_file'); |
||||
253 | } |
||||
254 | |||||
255 | // Normalize windows and linux path's |
||||
256 | $path = str_replace('\\', DIRECTORY_SEPARATOR, $path); |
||||
257 | $path = rtrim($path, DIRECTORY_SEPARATOR); |
||||
258 | |||||
259 | $tree = explode(DIRECTORY_SEPARATOR, $path); |
||||
260 | $count = empty($tree) ? 0 : count($tree); |
||||
261 | $partialTree = ''; |
||||
262 | |||||
263 | // Make sure we have a valid path format |
||||
264 | $directory = empty($tree) ? false : $this->_initDir($tree, $count); |
||||
265 | if ($directory === false) |
||||
266 | { |
||||
267 | // Maybe it's just the folder name |
||||
268 | $tree = explode(DIRECTORY_SEPARATOR, BOARDDIR . DIRECTORY_SEPARATOR . $path); |
||||
269 | $count = empty($tree) ? 0 : count($tree); |
||||
270 | |||||
271 | $directory = empty($tree) ? false : $this->_initDir($tree, $count); |
||||
272 | if ($directory === false) |
||||
273 | { |
||||
274 | throw new Exception('attachments_no_create'); |
||||
275 | } |
||||
276 | } |
||||
277 | |||||
278 | // Walk down the path until we find a part that exists |
||||
279 | for ($i = $count - 1; $i >= 0; $i--) |
||||
280 | { |
||||
281 | $partialTree = $directory . DIRECTORY_SEPARATOR . implode('/', array_slice($tree, 0, $i + 1)); |
||||
282 | // If this exists, lets ensure it is a directory |
||||
283 | if (file_exists($partialTree)) |
||||
284 | { |
||||
285 | if (!is_dir($partialTree)) |
||||
286 | { |
||||
287 | throw new Exception('attach_dir_duplicate_file'); |
||||
288 | } |
||||
289 | |||||
290 | break; |
||||
291 | } |
||||
292 | } |
||||
293 | |||||
294 | // Can't find this path anywhere |
||||
295 | if ($i < 0) |
||||
296 | { |
||||
297 | throw new Exception('attachments_no_create'); |
||||
298 | } |
||||
299 | |||||
300 | // Walk forward and create the missing parts |
||||
301 | for ($i++; $i < $count; $i++) |
||||
302 | { |
||||
303 | $partialTree .= '/' . $tree[$i]; |
||||
304 | if (!mkdir($partialTree) && !$this->isDir($partialTree)) |
||||
305 | { |
||||
306 | return false; |
||||
307 | } |
||||
308 | |||||
309 | // Make it writable |
||||
310 | if (!$this->chmod($partialTree)) |
||||
311 | { |
||||
312 | throw new Exception('attachments_no_write'); |
||||
313 | } |
||||
314 | |||||
315 | if ($makeSecure) |
||||
316 | { |
||||
317 | secureDirectory($partialTree, true); |
||||
318 | } |
||||
319 | } |
||||
320 | |||||
321 | clearstatcache(false, $partialTree); |
||||
322 | |||||
323 | return true; |
||||
324 | } |
||||
325 | |||||
326 | /** |
||||
327 | * Deletes a file (not a directory) at a given location |
||||
328 | * |
||||
329 | * @param $path |
||||
330 | * @return bool |
||||
331 | */ |
||||
332 | public function delete($path) |
||||
333 | { |
||||
334 | if (!$this->fileExists($path) || !$this->isWritable($path)) |
||||
335 | { |
||||
336 | return false; |
||||
337 | } |
||||
338 | |||||
339 | error_clear_last(); |
||||
340 | return @unlink($path); |
||||
341 | } |
||||
342 | |||||
343 | /** |
||||
344 | * Recursively removes a directory and all files and subdirectories contained within. |
||||
345 | * Use with *caution*, it is thorough, destructive and irreversible. |
||||
346 | * |
||||
347 | * @param string $path |
||||
348 | * @param bool $delete_dir if to remove the directory structure as well |
||||
349 | * @return bool |
||||
350 | */ |
||||
351 | public function rmDir($path, $delete_dir = true) |
||||
352 | { |
||||
353 | // @todo build a list of excluded directories |
||||
354 | if (!$this->isDir($path)) |
||||
355 | { |
||||
356 | return true; |
||||
357 | } |
||||
358 | |||||
359 | $success = true; |
||||
360 | $iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS); |
||||
361 | $files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD); |
||||
362 | |||||
363 | /** @var \FilesystemIterator $file */ |
||||
364 | foreach ($files as $file) |
||||
365 | { |
||||
366 | // If its not writable try to make it so or removal will fail |
||||
367 | if ($file->isWritable() || $this->chmod($file->getRealPath())) |
||||
368 | { |
||||
369 | if ($delete_dir && $file->isDir()) |
||||
370 | { |
||||
371 | $success = $success && rmdir($file->getRealPath()); |
||||
372 | } |
||||
373 | else |
||||
374 | { |
||||
375 | $success = $success && @unlink($file->getRealPath()); |
||||
376 | } |
||||
377 | } |
||||
378 | else |
||||
379 | { |
||||
380 | $success = false; |
||||
381 | } |
||||
382 | } |
||||
383 | |||||
384 | return $success && rmdir($path); |
||||
385 | } |
||||
386 | |||||
387 | /** |
||||
388 | * Helper function for createDirectory |
||||
389 | * |
||||
390 | * What it does: |
||||
391 | * |
||||
392 | * - Gets the directory w/o drive letter for windows |
||||
393 | * |
||||
394 | * @param string[] $tree |
||||
395 | * @param int $count |
||||
396 | * @return false|string|null |
||||
397 | */ |
||||
398 | private function _initDir(&$tree, &$count) |
||||
399 | { |
||||
400 | $directory = ''; |
||||
401 | |||||
402 | // If on Windows servers the first part of the path is the drive (e.g. "C:") |
||||
403 | if (strpos(PHP_OS_FAMILY, 'Win') === 0) |
||||
404 | { |
||||
405 | // Better be sure that the first part of the path is actually a drive letter... |
||||
406 | // ...even if, I should check this in the admin page...isn't it? |
||||
407 | // ...NHAAA Let's leave space for users' complains! :P |
||||
408 | if (preg_match('/^[a-z]:$/i', $tree[0])) |
||||
409 | { |
||||
410 | $directory = array_shift($tree); |
||||
411 | } |
||||
412 | else |
||||
413 | { |
||||
414 | return false; |
||||
415 | } |
||||
416 | |||||
417 | $count--; |
||||
418 | } |
||||
419 | |||||
420 | return $directory; |
||||
421 | } |
||||
422 | |||||
423 | /** |
||||
424 | * Create a full tree listing of files for a given directory path |
||||
425 | * |
||||
426 | * @param string $path |
||||
427 | * @return array |
||||
428 | */ |
||||
429 | public function listTree($path) |
||||
430 | { |
||||
431 | $tree = []; |
||||
432 | if (!$this->isDir($path)) |
||||
433 | { |
||||
434 | return $tree; |
||||
435 | } |
||||
436 | |||||
437 | $iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS); |
||||
438 | $files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD); |
||||
439 | /** @var \SplFileInfo $file */ |
||||
440 | foreach ($files as $file) |
||||
441 | { |
||||
442 | if ($file->isDir()) |
||||
443 | { |
||||
444 | continue; |
||||
445 | } |
||||
446 | |||||
447 | $sub_path = str_replace($path, '', $file->getPath()); |
||||
448 | |||||
449 | $tree[] = [ |
||||
450 | 'filename' => $sub_path === '' ? $file->getFilename() : $sub_path . '/' . $file->getFilename(), |
||||
451 | 'size' => $file->getSize(), |
||||
452 | 'skipped' => false, |
||||
453 | ]; |
||||
454 | } |
||||
455 | |||||
456 | return $tree; |
||||
457 | } |
||||
458 | |||||
459 | /** |
||||
460 | * Being a singleton, use this static method to retrieve the instance of the class |
||||
461 | * |
||||
462 | * @return FileFunctions An instance of the class. |
||||
463 | */ |
||||
464 | public static function instance() |
||||
465 | { |
||||
466 | if (self::$_instance === null) |
||||
467 | { |
||||
468 | self::$_instance = new FileFunctions(); |
||||
469 | } |
||||
470 | |||||
471 | return self::$_instance; |
||||
472 | } |
||||
473 | } |
||||
474 |