Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
2.37% |
7 / 295 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
tao_helpers_File | |
2.37% |
7 / 295 |
|
0.00% |
0 / 23 |
11164.14 | |
0.00% |
0 / 1 |
securityCheck | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
concat | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
remove | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
move | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
182 | |||
getMimeTypeList | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
2 | |||
getExtention | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getFileExtention | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMimeType | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
132 | |||
createTempDir | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
delTree | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
isIdentical | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
md5_dir | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
createZip | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
addFilesToZip | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
110 | |||
extractArchive | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
renameInZip | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
checkWhetherArchiveIsBomb | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
excludeFromZip | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
getAllZipNames | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getPathFromUrl | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getSafeFileName | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 | |||
removeSpecChars | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
isDirEmpty | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /* |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2008-2010 (original work) Deutsche Institut für Internationale Pädagogische Forschung |
19 | * (under the project TAO-TRANSFER); |
20 | * 2009-2012 (update and modification) Public Research Centre Henri Tudor |
21 | * (under the project TAO-SUSTAIN & TAO-DEV); |
22 | * |
23 | */ |
24 | |
25 | /** |
26 | * Utility class that focuses on files. |
27 | * |
28 | * @author Lionel Lecaque, <lionel@taotesting.com> |
29 | * @package tao |
30 | |
31 | */ |
32 | |
33 | use oat\oatbox\filesystem\File; |
34 | use Psr\Http\Message\StreamInterface; |
35 | |
36 | class tao_helpers_File extends helpers_File |
37 | { |
38 | public const MIME_SVG = 'image/svg+xml'; |
39 | |
40 | /** |
41 | * Check if the path in parameter can be securly used into the application. |
42 | * (check the cross directory injection, the null byte injection, etc.) |
43 | * Use it when the path may be build from a user variable |
44 | * |
45 | * @author Lionel Lecaque, <lionel@taotesting.com> |
46 | * @param string $path The path to check. |
47 | * @param boolean $traversalSafe (optional, default is false) Check if the path is traversal safe. |
48 | * @return boolean States if the path is secure or not. |
49 | */ |
50 | public static function securityCheck($path, $traversalSafe = false) |
51 | { |
52 | $returnValue = true; |
53 | |
54 | //security check: detect directory traversal (deny the ../) |
55 | if ($traversalSafe) { |
56 | if (preg_match("/\.\.\//", $path)) { |
57 | $returnValue = false; |
58 | common_Logger::w('directory traversal detected in ' . $path); |
59 | } |
60 | } |
61 | |
62 | //security check: detect the null byte poison by finding the null char injection |
63 | if ($returnValue) { |
64 | for ($i = 0; $i < strlen($path); $i++) { |
65 | if (ord($path[$i]) === 0) { |
66 | $returnValue = false; |
67 | common_Logger::w('null char injection detected in ' . $path); |
68 | break; |
69 | } |
70 | } |
71 | } |
72 | |
73 | return (bool) $returnValue; |
74 | } |
75 | |
76 | /** |
77 | * Use this method to cleanly concat components of a path. It will remove extra slashes/backslashes. |
78 | * |
79 | * @author Lionel Lecaque, <lionel@taotesting.com> |
80 | * @param array $paths The path components to concatenate. |
81 | * @return string The concatenated path. |
82 | */ |
83 | public static function concat($paths) |
84 | { |
85 | $returnValue = (string) ''; |
86 | |
87 | foreach ($paths as $path) { |
88 | if (!preg_match("/\/$/", $returnValue) && !preg_match("/^\//", $path) && !empty($returnValue)) { |
89 | $returnValue .= '/'; |
90 | } |
91 | $returnValue .= $path; |
92 | } |
93 | $returnValue = str_replace('//', '/', $returnValue); |
94 | |
95 | return (string) $returnValue; |
96 | } |
97 | |
98 | /** |
99 | * Remove a file. If the recursive parameter is set to true, the target file |
100 | * can be a directory that contains data. |
101 | * |
102 | * @author Lionel Lecaque, <lionel@taotesting.com> |
103 | * @param string $path The path to the file you want to remove. |
104 | * @param boolean $recursive (optional, default is false) Remove file content recursively (only if the path points |
105 | * to a directory). |
106 | * @return boolean Return true if the file is correctly removed, false otherwise. |
107 | */ |
108 | public static function remove($path, $recursive = false) |
109 | { |
110 | $returnValue = (bool) false; |
111 | |
112 | if ($recursive) { |
113 | $returnValue = helpers_File::remove($path); |
114 | } elseif (is_file($path)) { |
115 | $returnValue = @unlink($path); |
116 | } |
117 | // else fail silently |
118 | |
119 | return (bool) $returnValue; |
120 | } |
121 | |
122 | /** |
123 | * Move file from source to destination. |
124 | * |
125 | * @author Lionel Lecaque, <lionel@taotesting.com> |
126 | * @param string $source A path to the source file. |
127 | * @param string $destination A path to the destination file. |
128 | * @return boolean Returns true if the file was successfully moved, false otherwise. |
129 | */ |
130 | public static function move($source, $destination) |
131 | { |
132 | $returnValue = (bool) false; |
133 | |
134 | if (is_dir($source)) { |
135 | if (!file_exists($destination)) { |
136 | mkdir($destination, 0777, true); |
137 | } |
138 | $error = false; |
139 | foreach (scandir($source) as $file) { |
140 | if ($file != '.' && $file != '..') { |
141 | if (is_dir($source . '/' . $file)) { |
142 | if (!self::move($source . '/' . $file, $destination . '/' . $file, true)) { |
143 | $error = true; |
144 | } |
145 | } else { |
146 | if (!self::copy($source . '/' . $file, $destination . '/' . $file, true)) { |
147 | $error = true; |
148 | } |
149 | } |
150 | } |
151 | } |
152 | if (!$error) { |
153 | $returnValue = true; |
154 | } |
155 | self::remove($source, true); |
156 | } else { |
157 | if (file_exists($source) && file_exists($destination)) { |
158 | $returnValue = rename($source, $destination); |
159 | } else { |
160 | if (self::copy($source, $destination, true)) { |
161 | $returnValue = self::remove($source); |
162 | } |
163 | } |
164 | } |
165 | |
166 | return (bool) $returnValue; |
167 | } |
168 | |
169 | /** |
170 | * Retrieve mime-types that are recognized by the TAO platform. |
171 | * |
172 | * @author Lionel Lecaque, <lionel@taotesting.com> |
173 | * @return array An associative array of mime-types where keys are the extension related to the mime-type. Values |
174 | * of the array are mime-types. |
175 | */ |
176 | public static function getMimeTypeList() |
177 | { |
178 | $returnValue = [ |
179 | |
180 | 'txt' => 'text/plain', |
181 | 'htm' => 'text/html', |
182 | 'html' => 'text/html', |
183 | 'xhtml' => 'application/xhtml+xml', |
184 | 'php' => 'text/html', |
185 | 'css' => 'text/css', |
186 | 'js' => 'application/javascript', |
187 | 'json' => 'application/json', |
188 | 'xml' => 'text/xml', |
189 | 'rdf' => 'text/xml', |
190 | 'swf' => 'application/x-shockwave-flash', |
191 | 'flv' => 'video/x-flv', |
192 | 'csv' => 'text/csv', |
193 | 'rtx' => 'text/richtext', |
194 | |
195 | // images |
196 | 'png' => 'image/png', |
197 | 'jpe' => 'image/jpeg', |
198 | 'jpeg' => 'image/jpeg', |
199 | 'jpg' => 'image/jpeg', |
200 | 'gif' => 'image/gif', |
201 | 'bmp' => 'image/bmp', |
202 | 'ico' => 'image/vnd.microsoft.icon', |
203 | 'tiff' => 'image/tiff', |
204 | 'tif' => 'image/tiff', |
205 | 'svg' => self::MIME_SVG, |
206 | 'svgz' => self::MIME_SVG, |
207 | |
208 | // archives |
209 | 'zip' => 'application/zip', |
210 | 'rar' => 'application/x-rar-compressed', |
211 | 'exe' => 'application/x-msdownload', |
212 | 'msi' => 'application/x-msdownload', |
213 | 'cab' => 'application/vnd.ms-cab-compressed', |
214 | |
215 | // audio/video |
216 | 'mp3' => 'audio/mpeg', |
217 | 'oga' => 'audio/ogg', |
218 | 'ogg' => 'audio/ogg', |
219 | 'aac' => 'audio/aac', |
220 | 'qt' => 'video/quicktime', |
221 | 'mov' => 'video/quicktime', |
222 | 'mp4' => 'video/mp4',//(H.264 + AAC) for ie8, etc. |
223 | 'webm' => 'video/webm',//(VP8 + Vorbis) for ie9, ff, chrome, android, opera |
224 | 'ogv' => 'video/ogg',//ff, chrome, opera |
225 | |
226 | // adobe |
227 | 'pdf' => 'application/pdf', |
228 | 'psd' => 'image/vnd.adobe.photoshop', |
229 | 'ai' => 'application/postscript', |
230 | 'eps' => 'application/postscript', |
231 | 'ps' => 'application/postscript', |
232 | |
233 | // ms office |
234 | 'doc' => 'application/msword', |
235 | 'rtf' => 'application/rtf', |
236 | 'xls' => 'application/vnd.ms-excel', |
237 | 'ppt' => 'application/vnd.ms-powerpoint', |
238 | |
239 | // open office |
240 | 'odt' => 'application/vnd.oasis.opendocument.text', |
241 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', |
242 | |
243 | // fonts |
244 | 'woff' => 'application/x-font-woff', |
245 | 'eot' => 'application/vnd.ms-fontobject', |
246 | 'ttf' => 'application/x-font-ttf' |
247 | ]; |
248 | |
249 | return (array) $returnValue; |
250 | } |
251 | |
252 | /** |
253 | * Retrieve file extensions usually associated to a given mime-type. |
254 | * |
255 | * @author Lionel Lecaque, <lionel@taotesting.com> |
256 | * @param string $mimeType A mime-type which is recognized by the platform. |
257 | * @return string The extension usually associated to the mime-type. If it could not be retrieved, an empty string |
258 | * is returned. |
259 | */ |
260 | public static function getExtention($mimeType) |
261 | { |
262 | $returnValue = (string) ''; |
263 | |
264 | foreach (self::getMimeTypeList() as $key => $value) { |
265 | if ($value == trim($mimeType)) { |
266 | $returnValue = $key; |
267 | break; |
268 | } |
269 | } |
270 | |
271 | return (string) $returnValue; |
272 | } |
273 | |
274 | |
275 | /** |
276 | * Retrieve file extensions of a file |
277 | * |
278 | * @param string $path the path of the file we want to get the extension |
279 | * @return string The extension of the parameter file |
280 | */ |
281 | public static function getFileExtention($path) |
282 | { |
283 | |
284 | $ext = pathinfo($path, PATHINFO_EXTENSION); |
285 | |
286 | if ($ext === '') { |
287 | $splitedPath = explode('.', $path); |
288 | $ext = end($splitedPath); |
289 | } |
290 | |
291 | return $ext; |
292 | } |
293 | |
294 | /** |
295 | * Get the mime-type of the file in parameter. |
296 | * different methods are used regarding the configuration of the server. |
297 | * |
298 | * @author Lionel Lecaque, <lionel@taotesting.com> |
299 | * @param string $path |
300 | * @param boolean $ext If set to true, the extension of the file will be used to retrieve the mime-type. If now |
301 | * extension can be found, 'text/plain' is returned by the method. |
302 | * @return string The associated mime-type. |
303 | */ |
304 | public static function getMimeType($path, $ext = false) |
305 | { |
306 | $mime_types = self::getMimeTypeList(); |
307 | |
308 | if (false == $ext) { |
309 | $ext = pathinfo($path, PATHINFO_EXTENSION); |
310 | |
311 | if (array_key_exists($ext, $mime_types)) { |
312 | $mimetype = $mime_types[$ext]; |
313 | } else { |
314 | $mimetype = ''; |
315 | } |
316 | |
317 | if (!in_array($ext, ['css', 'ogg', 'mp3', 'svg', 'svgz'])) { |
318 | if (file_exists($path)) { |
319 | if (function_exists('finfo_open')) { |
320 | $finfo = finfo_open(FILEINFO_MIME); |
321 | $mimetype = finfo_file($finfo, $path); |
322 | finfo_close($finfo); |
323 | } elseif (function_exists('mime_content_type')) { |
324 | $mimetype = mime_content_type($path); |
325 | } |
326 | if (!empty($mimetype)) { |
327 | if (preg_match("/; charset/", $mimetype)) { |
328 | $mimetypeInfos = explode(';', $mimetype); |
329 | $mimetype = $mimetypeInfos[0]; |
330 | } |
331 | } |
332 | } |
333 | } |
334 | } else { |
335 | // find out the mime-type from the extension of the file. |
336 | $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); |
337 | if (array_key_exists($ext, $mime_types)) { |
338 | $mimetype = $mime_types[$ext]; |
339 | } |
340 | } |
341 | |
342 | // If no mime-type found ... |
343 | if (empty($mimetype)) { |
344 | $mimetype = 'application/octet-stream'; |
345 | } |
346 | |
347 | return (string) $mimetype; |
348 | } |
349 | |
350 | /** |
351 | * creates a directory in the system's temp dir. |
352 | * |
353 | * @author Lionel Lecaque, <lionel@taotesting.com> |
354 | * @return string The path to the created folder. |
355 | */ |
356 | public static function createTempDir() |
357 | { |
358 | do { |
359 | $folder = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "tmp" . mt_rand() . DIRECTORY_SEPARATOR; |
360 | } while (file_exists($folder)); |
361 | mkdir($folder); |
362 | return $folder; |
363 | } |
364 | |
365 | /** |
366 | * deletes a directory and its content. |
367 | * |
368 | * @author Lionel Lecaque, <lionel@taotesting.com> |
369 | * @param string directory absolute path of the directory |
370 | * @return boolean true if the directory and its content were deleted, false otherwise. |
371 | */ |
372 | public static function delTree($directory) |
373 | { |
374 | |
375 | $files = array_diff(scandir($directory), ['.','..']); |
376 | foreach ($files as $file) { |
377 | $abspath = $directory . DIRECTORY_SEPARATOR . $file; |
378 | if (is_dir($abspath)) { |
379 | self::delTree($abspath); |
380 | } else { |
381 | unlink($abspath); |
382 | } |
383 | } |
384 | return rmdir($directory); |
385 | } |
386 | |
387 | public static function isIdentical($path1, $path2) |
388 | { |
389 | return self::md5_dir($path1) == self::md5_dir($path2); |
390 | } |
391 | |
392 | // phpcs:disable PSR1.Methods.CamelCapsMethodName |
393 | public static function md5_dir($path) |
394 | { |
395 | if (is_file($path)) { |
396 | $md5 = md5_file($path); |
397 | } elseif (is_dir($path)) { |
398 | $filemd5s = []; |
399 | // using scandir to get files in a fixed order |
400 | $files = scandir($path); |
401 | sort($files); |
402 | foreach ($files as $basename) { |
403 | if ($basename != '.' && $basename != '..') { |
404 | //$fileInfo->getFilename() |
405 | $filemd5s[] = $basename . self::md5_dir(self::concat([$path, $basename])); |
406 | } |
407 | } |
408 | $md5 = md5(implode('', $filemd5s)); |
409 | } else { |
410 | throw new common_Exception(__FUNCTION__ . ' called on non file or directory "' . $path . '"'); |
411 | } |
412 | return $md5; |
413 | } |
414 | // phpcs:enable PSR1.Methods.CamelCapsMethodName |
415 | |
416 | /** |
417 | * Create a zip of a directory or file |
418 | * |
419 | * @param string $src path to the files to zip |
420 | * @param bool $withEmptyDir |
421 | * @return string path to the zip file |
422 | * @throws common_Exception if unable to create the zip |
423 | */ |
424 | public static function createZip($src, $withEmptyDir = false) |
425 | { |
426 | $zipArchive = new \ZipArchive(); |
427 | $path = self::createTempDir() . 'file.zip'; |
428 | if ($zipArchive->open($path, \ZipArchive::CREATE) !== true) { |
429 | throw new common_Exception('Unable to create zipfile ' . $path); |
430 | } |
431 | self::addFilesToZip($zipArchive, $src, DIRECTORY_SEPARATOR, $withEmptyDir); |
432 | $zipArchive->close(); |
433 | return $path; |
434 | } |
435 | |
436 | /** |
437 | * Add files or folders (and their content) to the Zip Archive that will contain all the files to the current export |
438 | * session. |
439 | * For instance, if you want to copy the file 'taoItems/data/i123/item.xml' as 'myitem.xml' to your archive call |
440 | * addFile('path_to_item_location/item.xml', 'myitem.xml'). |
441 | * As a result, you will get a file entry in the final ZIP archive at '/i123/myitem.xml'. |
442 | * |
443 | * @param ZipArchive $zipArchive the archive to add to |
444 | * @param string|StreamInterface $src The path to the source file or folder to copy into the ZIP Archive. |
445 | * @param $dest |
446 | * @param bool $withEmptyDir |
447 | * @return integer The amount of files that were transfered from TAO to the ZIP archive within the method call. |
448 | */ |
449 | public static function addFilesToZip(ZipArchive $zipArchive, $src, $dest, $withEmptyDir = false) |
450 | { |
451 | $returnValue = null; |
452 | |
453 | $done = 0; |
454 | |
455 | if ($src instanceof \Psr\Http\Message\StreamInterface) { |
456 | if ($zipArchive->addFromString(ltrim($dest, "/\\"), $src->getContents())) { |
457 | $done++; |
458 | } |
459 | } elseif (is_resource($src)) { |
460 | fseek($src, 0); |
461 | $content = stream_get_contents($src); |
462 | if ($zipArchive->addFromString(ltrim($dest, "/\\"), $content)) { |
463 | $done++; |
464 | } |
465 | } elseif (is_dir($src)) { |
466 | // Go deeper in folder hierarchy ! |
467 | $src = rtrim($src, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
468 | $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
469 | |
470 | if ($withEmptyDir) { |
471 | $zipArchive->addEmptyDir($dest); |
472 | } |
473 | |
474 | // Recursively copy. |
475 | $content = scandir($src); |
476 | |
477 | foreach ($content as $file) { |
478 | // avoid . , .. , .svn etc ... |
479 | if (!preg_match("/^\./", $file)) { |
480 | $done += self::addFilesToZip($zipArchive, $src . $file, $dest . $file, $withEmptyDir); |
481 | } |
482 | } |
483 | } else { |
484 | // Simply copy the file. Beware of leading slashes |
485 | if ($zipArchive->addFile($src, ltrim($dest, DIRECTORY_SEPARATOR))) { |
486 | $done++; |
487 | } |
488 | } |
489 | |
490 | $returnValue = $done; |
491 | |
492 | return $returnValue; |
493 | } |
494 | |
495 | /** |
496 | * Unzip archive file |
497 | * |
498 | * @param string|File $archiveFile |
499 | * @return string path to temporary directory zipfile was extracted to |
500 | * |
501 | * @throws \common_Exception |
502 | */ |
503 | public static function extractArchive($archiveFile) |
504 | { |
505 | if ($archiveFile instanceof File) { |
506 | if (!$archiveFile->exists()) { |
507 | throw new \common_Exception('Unable to open archive ' . '/' . $archiveFile->getPrefix()); |
508 | } |
509 | $tmpDir = static::createTempDir(); |
510 | $tmpFilePath = $tmpDir . uniqid($archiveFile->getBasename(), true) . '.zip'; |
511 | $tmpFile = fopen($tmpFilePath, 'w'); |
512 | $originalPackage = $archiveFile->readStream(); |
513 | stream_copy_to_stream($originalPackage, $tmpFile); |
514 | fclose($originalPackage); |
515 | fclose($tmpFile); |
516 | $archiveFile = $tmpFilePath; |
517 | } |
518 | |
519 | $archiveObj = new \ZipArchive(); |
520 | $archiveHandle = $archiveObj->open($archiveFile); |
521 | |
522 | if (true !== $archiveHandle) { |
523 | throw new \common_Exception('Unable to open archive ' . $archiveFile); |
524 | } |
525 | |
526 | if (static::checkWhetherArchiveIsBomb($archiveObj)) { |
527 | throw new \common_Exception(sprintf('Source "%s" seems to be a ZIP bomb', $archiveFile)); |
528 | } |
529 | |
530 | $archiveDir = static::createTempDir(); |
531 | if (!$archiveObj->extractTo($archiveDir)) { |
532 | $archiveObj->close(); |
533 | throw new \common_Exception('Unable to extract to ' . $archiveDir); |
534 | } |
535 | $archiveObj->close(); |
536 | |
537 | if (isset($tmpFilePath) && file_exists($tmpFilePath)) { |
538 | unlink($tmpFilePath); |
539 | } |
540 | if (isset($tmpDir) && file_exists($tmpDir)) { |
541 | rmdir($tmpDir); |
542 | } |
543 | |
544 | return $archiveDir; |
545 | } |
546 | |
547 | /** |
548 | * Rename in Zip |
549 | * |
550 | * Rename an item in a ZIP archive. Works for files and directories. |
551 | * |
552 | * In case of renaming directories, the return value of this method will be the amount of files |
553 | * affected by the directory renaming. |
554 | * |
555 | * @param ZipArchive $zipArchive An open ZipArchive object. |
556 | * @param string $oldname |
557 | * @param string $newname |
558 | * @return int The amount of renamed entries. |
559 | */ |
560 | public static function renameInZip(ZipArchive $zipArchive, $oldname, $newname) |
561 | { |
562 | $i = 0; |
563 | $renameCount = 0; |
564 | |
565 | while ( |
566 | ($entryName = $zipArchive->getNameIndex($i)) |
567 | || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED)) |
568 | ) { |
569 | if ($entryName) { |
570 | $newEntryName = str_replace($oldname, $newname, $entryName); |
571 | if ($zipArchive->renameIndex($i, $newEntryName)) { |
572 | $renameCount++; |
573 | } |
574 | } |
575 | |
576 | $i++; |
577 | } |
578 | |
579 | return $renameCount; |
580 | } |
581 | |
582 | /** |
583 | * Helps prevent decompression attacks. |
584 | * Since this method checks archive file size, it needs filename property to be set, |
585 | * so ZipArchive object should be already opened. |
586 | * |
587 | * @param \ZipArchive $archive |
588 | * @param int $minCompressionRatioToBeBomb archive content size / archive size |
589 | * @return bool |
590 | * @throws common_Exception |
591 | * |
592 | * @link https://en.wikipedia.org/wiki/Zip_bomb |
593 | */ |
594 | public static function checkWhetherArchiveIsBomb(\ZipArchive $archive, $minCompressionRatioToBeBomb = 200) |
595 | { |
596 | if (!$archive->filename) { |
597 | throw new common_Exception('ZIP archive should be opened before checking for a ZIP bomb'); |
598 | } |
599 | |
600 | $contentSize = 0; |
601 | for ($fileIndex = 0; $fileIndex < $archive->numFiles; $fileIndex++) { |
602 | $stats = $archive->statIndex($fileIndex); |
603 | $contentSize += $stats['size']; |
604 | } |
605 | |
606 | $archiveFileSize = filesize($archive->filename); |
607 | |
608 | return $archiveFileSize * $minCompressionRatioToBeBomb < $contentSize; |
609 | } |
610 | |
611 | /** |
612 | * Exclude from Zip |
613 | * |
614 | * Exclude entries matching $pattern from a ZIP Archive. |
615 | * |
616 | * @param ZipArchive $zipArchive An open ZipArchive object. |
617 | * @param string $pattern A PCRE pattern. |
618 | * @return int The amount of excluded entries. |
619 | */ |
620 | public static function excludeFromZip(ZipArchive $zipArchive, $pattern) |
621 | { |
622 | $i = 0; |
623 | $exclusionCount = 0; |
624 | |
625 | while ( |
626 | ($entryName = $zipArchive->getNameIndex($i)) |
627 | || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED)) |
628 | ) { |
629 | if ($entryName) { |
630 | // Not previously removed index. |
631 | if (preg_match($pattern, $entryName) === 1 && $zipArchive->deleteIndex($i)) { |
632 | $exclusionCount++; |
633 | } |
634 | } |
635 | |
636 | $i++; |
637 | } |
638 | |
639 | return $exclusionCount; |
640 | } |
641 | |
642 | /** |
643 | * Get All Zip Names |
644 | * |
645 | * Retrieve all ZIP name entries in a ZIP archive. In others words, all the paths in the |
646 | * archive having an entry. |
647 | * |
648 | * @param ZipArchive $zipArchive An open ZipArchive object. |
649 | * @return array An array of strings. |
650 | */ |
651 | public static function getAllZipNames(ZipArchive $zipArchive) |
652 | { |
653 | $i = 0; |
654 | $entries = []; |
655 | |
656 | while ( |
657 | ($entryName = $zipArchive->getNameIndex($i)) |
658 | || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED)) |
659 | ) { |
660 | if ($entryName) { |
661 | $entries[] = $entryName; |
662 | } |
663 | |
664 | $i++; |
665 | } |
666 | |
667 | return $entries; |
668 | } |
669 | |
670 | /** |
671 | * Gets the local path to a publicly available resource |
672 | * no verification if the file should be accessible |
673 | * |
674 | * @param string $url |
675 | * @throws common_Exception |
676 | * @return string |
677 | */ |
678 | public static function getPathFromUrl($url) |
679 | { |
680 | if (substr($url, 0, strlen(ROOT_URL)) != ROOT_URL) { |
681 | throw new common_Exception($url . ' does not lie within the tao instalation path'); |
682 | } |
683 | $subUrl = substr($url, strlen(ROOT_URL)); |
684 | $parts = []; |
685 | foreach (explode('/', $subUrl) as $directory) { |
686 | $parts[] = urldecode($directory); |
687 | } |
688 | $path = ROOT_PATH . implode(DIRECTORY_SEPARATOR, $parts); |
689 | if (self::securityCheck($path)) { |
690 | return $path; |
691 | } else { |
692 | throw new common_Exception($url . ' is not secure'); |
693 | } |
694 | } |
695 | |
696 | /** |
697 | * Get a safe filename for a proposed filename. |
698 | * |
699 | * If directory is specified it will return a filename which is |
700 | * safe to not overwritte an existing file. This function is not injective. |
701 | * |
702 | * @param string $fileName |
703 | * @param string $directory |
704 | * |
705 | * @return string |
706 | */ |
707 | public static function getSafeFileName($fileName, $directory = null) |
708 | { |
709 | $lastDot = strrpos($fileName, '.'); |
710 | $file = $lastDot ? substr($fileName, 0, $lastDot) : $fileName; |
711 | $ending = $lastDot ? substr($fileName, $lastDot + 1) : ''; |
712 | $safeName = self::removeSpecChars($file); |
713 | $safeEnding = empty($ending) |
714 | ? '' |
715 | : '.' . self::removeSpecChars($ending); |
716 | |
717 | if ($directory != null && file_exists($directory . $safeName . $safeEnding)) { |
718 | $count = 1; |
719 | while (file_exists($directory . $safeName . '_' . $count . $safeEnding)) { |
720 | $count++; |
721 | } |
722 | $safeName = $safeName . '_' . $count; |
723 | } |
724 | |
725 | return $safeName . $safeEnding; |
726 | } |
727 | |
728 | /** |
729 | * Remove special characters for safe filenames |
730 | * |
731 | * @author Dieter Raber |
732 | * |
733 | * @param string $string |
734 | * @param string $repl |
735 | * @param string $lower |
736 | * |
737 | * @return string |
738 | */ |
739 | private static function removeSpecChars($string, $repl = '-', $lower = true) |
740 | { |
741 | $spec_chars = [ |
742 | 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'Ae', 'Å' => 'A','Æ' => 'A', 'Ç' => 'C', |
743 | 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', |
744 | 'Ï' => 'I', 'Ð' => 'E', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', |
745 | 'Ö' => 'Oe', 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U','Û' => 'U', 'Ü' => 'Ue', 'Ý' => 'Y', |
746 | 'Þ' => 'T', 'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'ae', |
747 | 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', |
748 | 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ð' => 'e', 'ñ' => 'n', 'ò' => 'o', |
749 | 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'oe', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', |
750 | 'û' => 'u', 'ü' => 'ue', 'ý' => 'y', 'þ' => 't', 'ÿ' => 'y', '?' => $repl, |
751 | '\'' => $repl, '.' => $repl, '/' => $repl, '&' => $repl, ')' => $repl, '(' => $repl, |
752 | '[' => $repl, ']' => $repl, '_' => $repl, ',' => $repl, ':' => $repl, '-' => $repl, |
753 | '!' => $repl, '"' => $repl, '`' => $repl, '°' => $repl, '%' => $repl, ' ' => $repl, |
754 | ' ' => $repl, '{' => $repl, '}' => $repl, '#' => $repl, '’' => $repl |
755 | ]; |
756 | $string = strtr($string, $spec_chars); |
757 | $string = trim(preg_replace("~[^a-z0-9]+~i", $repl, $string), $repl); |
758 | return $lower ? strtolower($string) : $string; |
759 | } |
760 | |
761 | /** |
762 | * Check if the directory is empty |
763 | * |
764 | * @param string $directory |
765 | * @return boolean |
766 | */ |
767 | public static function isDirEmpty($directory) |
768 | { |
769 | $path = self::concat([$directory, '*']); |
770 | return (count(glob($path, GLOB_NOSORT)) === 0); |
771 | } |
772 | } |