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 | } |