[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Part of the Joomla Framework Archive Package 4 * 5 * @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE 7 */ 8 9 namespace Joomla\Archive; 10 11 use Joomla\Filesystem\File; 12 use Joomla\Filesystem\Folder; 13 use Joomla\Filesystem\Path; 14 15 /** 16 * ZIP format adapter for the Archive package 17 * 18 * The ZIP compression code is partially based on code from: 19 * Eric Mueller <[email protected]> 20 * http://www.zend.com/codex.php?id=535&single=1 21 * 22 * Deins125 <[email protected]> 23 * http://www.zend.com/codex.php?id=470&single=1 24 * 25 * The ZIP compression date code is partially based on code from 26 * Peter Listiak <[email protected]> 27 * 28 * This class is inspired from and draws heavily in code and concept from the Compress package of 29 * The Horde Project <http://www.horde.org> 30 * 31 * @contributor Chuck Hagenbuch <[email protected]> 32 * @contributor Michael Slusarz <[email protected]> 33 * @contributor Michael Cochrane <[email protected]> 34 * 35 * @since 1.0 36 */ 37 class Zip implements ExtractableInterface 38 { 39 /** 40 * ZIP compression methods. 41 * 42 * @var array 43 * @since 1.0 44 */ 45 private const METHODS = [ 46 0x0 => 'None', 47 0x1 => 'Shrunk', 48 0x2 => 'Super Fast', 49 0x3 => 'Fast', 50 0x4 => 'Normal', 51 0x5 => 'Maximum', 52 0x6 => 'Imploded', 53 0x8 => 'Deflated', 54 ]; 55 56 /** 57 * Beginning of central directory record. 58 * 59 * @var string 60 * @since 1.0 61 */ 62 private const CTRL_DIR_HEADER = "\x50\x4b\x01\x02"; 63 64 /** 65 * End of central directory record. 66 * 67 * @var string 68 * @since 1.0 69 */ 70 private const CTRL_DIR_END = "\x50\x4b\x05\x06\x00\x00\x00\x00"; 71 72 /** 73 * Beginning of file contents. 74 * 75 * @var string 76 * @since 1.0 77 */ 78 private const FILE_HEADER = "\x50\x4b\x03\x04"; 79 80 /** 81 * ZIP file data buffer 82 * 83 * @var string 84 * @since 1.0 85 */ 86 private $data; 87 88 /** 89 * ZIP file metadata array 90 * 91 * @var array 92 * @since 1.0 93 */ 94 private $metadata; 95 96 /** 97 * Holds the options array. 98 * 99 * @var array|\ArrayAccess 100 * @since 1.0 101 */ 102 protected $options = []; 103 104 /** 105 * Create a new Archive object. 106 * 107 * @param array|\ArrayAccess $options An array of options or an object that implements \ArrayAccess 108 * 109 * @since 1.0 110 * @throws \InvalidArgumentException 111 */ 112 public function __construct($options = []) 113 { 114 if (!\is_array($options) && !($options instanceof \ArrayAccess)) 115 { 116 throw new \InvalidArgumentException( 117 'The options param must be an array or implement the ArrayAccess interface.' 118 ); 119 } 120 121 $this->options = $options; 122 } 123 124 /** 125 * Create a ZIP compressed file from an array of file data. 126 * 127 * @param string $archive Path to save archive. 128 * @param array $files Array of files to add to archive. 129 * 130 * @return boolean True if successful. 131 * 132 * @since 1.0 133 * @todo Finish Implementation 134 */ 135 public function create($archive, $files) 136 { 137 $contents = []; 138 $ctrldir = []; 139 140 foreach ($files as $file) 141 { 142 $this->addToZipFile($file, $contents, $ctrldir); 143 } 144 145 return $this->createZipFile($contents, $ctrldir, $archive); 146 } 147 148 /** 149 * Extract a ZIP compressed file to a given path 150 * 151 * @param string $archive Path to ZIP archive to extract 152 * @param string $destination Path to extract archive into 153 * 154 * @return boolean True if successful 155 * 156 * @since 1.0 157 * @throws \RuntimeException 158 */ 159 public function extract($archive, $destination) 160 { 161 if (!is_file($archive)) 162 { 163 throw new \RuntimeException('Archive does not exist at ' . $archive); 164 } 165 166 if (static::hasNativeSupport()) 167 { 168 return $this->extractNative($archive, $destination); 169 } 170 171 return $this->extractCustom($archive, $destination); 172 } 173 174 /** 175 * Tests whether this adapter can unpack files on this computer. 176 * 177 * @return boolean True if supported 178 * 179 * @since 1.0 180 */ 181 public static function isSupported() 182 { 183 return self::hasNativeSupport() || \extension_loaded('zlib'); 184 } 185 186 /** 187 * Method to determine if the server has native zip support for faster handling 188 * 189 * @return boolean True if php has native ZIP support 190 * 191 * @since 1.0 192 */ 193 public static function hasNativeSupport() 194 { 195 return \extension_loaded('zip'); 196 } 197 198 /** 199 * Checks to see if the data is a valid ZIP file. 200 * 201 * @param string $data ZIP archive data buffer. 202 * 203 * @return boolean True if valid, false if invalid. 204 * 205 * @since 1.0 206 */ 207 public function checkZipData($data) 208 { 209 return strpos($data, self::FILE_HEADER) !== false; 210 } 211 212 /** 213 * Extract a ZIP compressed file to a given path using a php based algorithm that only requires zlib support 214 * 215 * @param string $archive Path to ZIP archive to extract. 216 * @param string $destination Path to extract archive into. 217 * 218 * @return boolean True if successful 219 * 220 * @since 1.0 221 * @throws \RuntimeException 222 */ 223 protected function extractCustom($archive, $destination) 224 { 225 $this->metadata = null; 226 $this->data = file_get_contents($archive); 227 228 if (!$this->data) 229 { 230 throw new \RuntimeException('Unable to read archive'); 231 } 232 233 if (!$this->readZipInfo($this->data)) 234 { 235 throw new \RuntimeException('Get ZIP Information failed'); 236 } 237 238 foreach ($this->metadata as $i => $metadata) 239 { 240 $lastPathCharacter = substr($metadata['name'], -1, 1); 241 242 if ($lastPathCharacter !== '/' && $lastPathCharacter !== '\\') 243 { 244 $buffer = $this->getFileData($i); 245 $path = Path::clean($destination . '/' . $metadata['name']); 246 247 if (!$this->isBelow($destination, $destination . '/' . $metadata['name'])) 248 { 249 throw new \OutOfBoundsException('Unable to write outside of destination path', 100); 250 } 251 252 // Make sure the destination folder exists 253 if (!Folder::create(\dirname($path))) 254 { 255 throw new \RuntimeException('Unable to create destination folder'); 256 } 257 258 if (!File::write($path, $buffer)) 259 { 260 throw new \RuntimeException('Unable to write file'); 261 } 262 } 263 } 264 265 return true; 266 } 267 268 /** 269 * Extract a ZIP compressed file to a given path using native php api calls for speed 270 * 271 * @param string $archive Path to ZIP archive to extract 272 * @param string $destination Path to extract archive into 273 * 274 * @return boolean True on success 275 * 276 * @throws \RuntimeException 277 * @since 1.0 278 */ 279 protected function extractNative($archive, $destination) 280 { 281 $zip = new \ZipArchive; 282 283 if ($zip->open($archive) !== true) 284 { 285 throw new \RuntimeException('Unable to open archive'); 286 } 287 288 // Make sure the destination folder exists 289 if (!Folder::create($destination)) 290 { 291 throw new \RuntimeException('Unable to create destination folder ' . \dirname($destination)); 292 } 293 294 // Read files in the archive 295 for ($index = 0; $index < $zip->numFiles; $index++) 296 { 297 $file = $zip->getNameIndex($index); 298 299 if (substr($file, -1) === '/') 300 { 301 continue; 302 } 303 304 $buffer = $zip->getFromIndex($index); 305 306 if ($buffer === false) 307 { 308 throw new \RuntimeException('Unable to read ZIP entry'); 309 } 310 311 if (!$this->isBelow($destination, $destination . '/' . $file)) 312 { 313 throw new \RuntimeException('Unable to write outside of destination path', 100); 314 } 315 316 if (File::write($destination . '/' . $file, $buffer) === false) 317 { 318 throw new \RuntimeException('Unable to write ZIP entry to file ' . $destination . '/' . $file); 319 } 320 } 321 322 $zip->close(); 323 324 return true; 325 } 326 327 /** 328 * Get the list of files/data from a ZIP archive buffer. 329 * 330 * <pre> 331 * KEY: Position in zipfile 332 * VALUES: 'attr' -- File attributes 333 * 'crc' -- CRC checksum 334 * 'csize' -- Compressed file size 335 * 'date' -- File modification time 336 * 'name' -- Filename 337 * 'method'-- Compression method 338 * 'size' -- Original file size 339 * 'type' -- File type 340 * </pre> 341 * 342 * @param string $data The ZIP archive buffer. 343 * 344 * @return boolean True on success 345 * 346 * @since 1.0 347 * @throws \RuntimeException 348 */ 349 private function readZipInfo($data) 350 { 351 $entries = []; 352 353 // Find the last central directory header entry 354 $fhLast = strpos($data, self::CTRL_DIR_END); 355 356 do 357 { 358 $last = $fhLast; 359 } 360 while (($fhLast = strpos($data, self::CTRL_DIR_END, $fhLast + 1)) !== false); 361 362 // Find the central directory offset 363 $offset = 0; 364 365 if ($last) 366 { 367 $endOfCentralDirectory = unpack( 368 'vNumberOfDisk/vNoOfDiskWithStartOfCentralDirectory/vNoOfCentralDirectoryEntriesOnDisk/' . 369 'vTotalCentralDirectoryEntries/VSizeOfCentralDirectory/VCentralDirectoryOffset/vCommentLength', 370 substr($data, $last + 4) 371 ); 372 $offset = $endOfCentralDirectory['CentralDirectoryOffset']; 373 } 374 375 // Get details from central directory structure. 376 $fhStart = strpos($data, self::CTRL_DIR_HEADER, $offset); 377 $dataLength = \strlen($data); 378 379 do 380 { 381 if ($dataLength < $fhStart + 31) 382 { 383 throw new \RuntimeException('Invalid ZIP Data'); 384 } 385 386 $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength', substr($data, $fhStart + 10, 20)); 387 $name = substr($data, $fhStart + 46, $info['Length']); 388 389 $entries[$name] = [ 390 'attr' => null, 391 'crc' => sprintf('%08s', dechex($info['CRC32'])), 392 'csize' => $info['Compressed'], 393 'date' => null, 394 '_dataStart' => null, 395 'name' => $name, 396 'method' => self::METHODS[$info['Method']], 397 '_method' => $info['Method'], 398 'size' => $info['Uncompressed'], 399 'type' => null, 400 ]; 401 402 $entries[$name]['date'] = mktime( 403 ($info['Time'] >> 11) & 0x1f, 404 ($info['Time'] >> 5) & 0x3f, 405 ($info['Time'] << 1) & 0x3e, 406 ($info['Time'] >> 21) & 0x07, 407 ($info['Time'] >> 16) & 0x1f, 408 (($info['Time'] >> 25) & 0x7f) + 1980 409 ); 410 411 if ($dataLength < $fhStart + 43) 412 { 413 throw new \RuntimeException('Invalid ZIP data'); 414 } 415 416 $info = unpack('vInternal/VExternal/VOffset', substr($data, $fhStart + 36, 10)); 417 418 $entries[$name]['type'] = ($info['Internal'] & 0x01) ? 'text' : 'binary'; 419 $entries[$name]['attr'] = (($info['External'] & 0x10) ? 'D' : '-') . (($info['External'] & 0x20) ? 'A' : '-') 420 . (($info['External'] & 0x03) ? 'S' : '-') . (($info['External'] & 0x02) ? 'H' : '-') . (($info['External'] & 0x01) ? 'R' : '-'); 421 $entries[$name]['offset'] = $info['Offset']; 422 423 // Get details from local file header since we have the offset 424 $lfhStart = strpos($data, self::FILE_HEADER, $entries[$name]['offset']); 425 426 if ($dataLength < $lfhStart + 34) 427 { 428 throw new \RuntimeException('Invalid ZIP Data'); 429 } 430 431 $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength/vExtraLength', substr($data, $lfhStart + 8, 25)); 432 $name = substr($data, $lfhStart + 30, $info['Length']); 433 $entries[$name]['_dataStart'] = $lfhStart + 30 + $info['Length'] + $info['ExtraLength']; 434 435 // Bump the max execution time because not using the built in php zip libs makes this process slow. 436 @set_time_limit(ini_get('max_execution_time')); 437 } 438 while (($fhStart = strpos($data, self::CTRL_DIR_HEADER, $fhStart + 46)) !== false); 439 440 $this->metadata = array_values($entries); 441 442 return true; 443 } 444 445 /** 446 * Returns the file data for a file by offset in the ZIP archive 447 * 448 * @param integer $key The position of the file in the archive. 449 * 450 * @return string Uncompressed file data buffer. 451 * 452 * @since 1.0 453 */ 454 private function getFileData(int $key): string 455 { 456 if ($this->metadata[$key]['_method'] == 0x8) 457 { 458 return gzinflate(substr($this->data, $this->metadata[$key]['_dataStart'], $this->metadata[$key]['csize'])); 459 } 460 461 if ($this->metadata[$key]['_method'] == 0x0) 462 { 463 // Files that aren't compressed. 464 return substr($this->data, $this->metadata[$key]['_dataStart'], $this->metadata[$key]['csize']); 465 } 466 467 if ($this->metadata[$key]['_method'] == 0x12) 468 { 469 // If bz2 extension is loaded use it 470 if (\extension_loaded('bz2')) 471 { 472 return bzdecompress(substr($this->data, $this->metadata[$key]['_dataStart'], $this->metadata[$key]['csize'])); 473 } 474 } 475 476 return ''; 477 } 478 479 /** 480 * Converts a UNIX timestamp to a 4-byte DOS date and time format (date in high 2-bytes, time in low 2-bytes allowing magnitude comparison). 481 * 482 * @param integer $unixtime The current UNIX timestamp. 483 * 484 * @return integer The current date in a 4-byte DOS format. 485 * 486 * @since 1.0 487 */ 488 protected function unix2DosTime($unixtime = null) 489 { 490 $timearray = $unixtime === null ? getdate() : getdate($unixtime); 491 492 if ($timearray['year'] < 1980) 493 { 494 $timearray['year'] = 1980; 495 $timearray['mon'] = 1; 496 $timearray['mday'] = 1; 497 $timearray['hours'] = 0; 498 $timearray['minutes'] = 0; 499 $timearray['seconds'] = 0; 500 } 501 502 return (($timearray['year'] - 1980) << 25) | ($timearray['mon'] << 21) | ($timearray['mday'] << 16) | ($timearray['hours'] << 11) | 503 ($timearray['minutes'] << 5) | ($timearray['seconds'] >> 1); 504 } 505 506 /** 507 * Adds a "file" to the ZIP archive. 508 * 509 * @param array $file File data array to add 510 * @param array $contents An array of existing zipped files. 511 * @param array $ctrldir An array of central directory information. 512 * 513 * @return void 514 * 515 * @since 1.0 516 * @todo Review and finish implementation 517 */ 518 private function addToZipFile(array &$file, array &$contents, array &$ctrldir): void 519 { 520 $data = &$file['data']; 521 $name = str_replace('\\', '/', $file['name']); 522 523 // See if time/date information has been provided. 524 $ftime = null; 525 526 if (isset($file['time'])) 527 { 528 $ftime = $file['time']; 529 } 530 531 // Get the hex time. 532 $dtime = dechex($this->unix2DosTime($ftime)); 533 $hexdtime = \chr(hexdec($dtime[6] . $dtime[7])) . \chr(hexdec($dtime[4] . $dtime[5])) . \chr(hexdec($dtime[2] . $dtime[3])) 534 . \chr(hexdec($dtime[0] . $dtime[1])); 535 536 // Begin creating the ZIP data. 537 $fr = self::FILE_HEADER; 538 539 // Version needed to extract. 540 $fr .= "\x14\x00"; 541 542 // General purpose bit flag. 543 $fr .= "\x00\x00"; 544 545 // Compression method. 546 $fr .= "\x08\x00"; 547 548 // Last modification time/date. 549 $fr .= $hexdtime; 550 551 // "Local file header" segment. 552 $uncLen = \strlen($data); 553 $crc = crc32($data); 554 $zdata = gzcompress($data); 555 $zdata = substr(substr($zdata, 0, -4), 2); 556 $cLen = \strlen($zdata); 557 558 // CRC 32 information. 559 $fr .= pack('V', $crc); 560 561 // Compressed filesize. 562 $fr .= pack('V', $cLen); 563 564 // Uncompressed filesize. 565 $fr .= pack('V', $uncLen); 566 567 // Length of filename. 568 $fr .= pack('v', \strlen($name)); 569 570 // Extra field length. 571 $fr .= pack('v', 0); 572 573 // File name. 574 $fr .= $name; 575 576 // "File data" segment. 577 $fr .= $zdata; 578 579 // Add this entry to array. 580 $oldOffset = \strlen(implode('', $contents)); 581 $contents[] = &$fr; 582 583 // Add to central directory record. 584 $cdrec = self::CTRL_DIR_HEADER; 585 586 // Version made by. 587 $cdrec .= "\x00\x00"; 588 589 // Version needed to extract 590 $cdrec .= "\x14\x00"; 591 592 // General purpose bit flag 593 $cdrec .= "\x00\x00"; 594 595 // Compression method 596 $cdrec .= "\x08\x00"; 597 598 // Last mod time/date. 599 $cdrec .= $hexdtime; 600 601 // CRC 32 information. 602 $cdrec .= pack('V', $crc); 603 604 // Compressed filesize. 605 $cdrec .= pack('V', $cLen); 606 607 // Uncompressed filesize. 608 $cdrec .= pack('V', $uncLen); 609 610 // Length of filename. 611 $cdrec .= pack('v', \strlen($name)); 612 613 // Extra field length. 614 $cdrec .= pack('v', 0); 615 616 // File comment length. 617 $cdrec .= pack('v', 0); 618 619 // Disk number start. 620 $cdrec .= pack('v', 0); 621 622 // Internal file attributes. 623 $cdrec .= pack('v', 0); 624 625 // External file attributes -'archive' bit set. 626 $cdrec .= pack('V', 32); 627 628 // Relative offset of local header. 629 $cdrec .= pack('V', $oldOffset); 630 631 // File name. 632 $cdrec .= $name; 633 634 // Save to central directory array. 635 $ctrldir[] = &$cdrec; 636 } 637 638 /** 639 * Creates the ZIP file. 640 * 641 * Official ZIP file format: http://www.pkware.com/appnote.txt 642 * 643 * @param array $contents An array of existing zipped files. 644 * @param array $ctrlDir An array of central directory information. 645 * @param string $path The path to store the archive. 646 * 647 * @return boolean True if successful 648 * 649 * @since 1.0 650 * @todo Review and finish implementation 651 */ 652 private function createZipFile(array $contents, array $ctrlDir, string $path): bool 653 { 654 $data = implode('', $contents); 655 $dir = implode('', $ctrlDir); 656 657 /* 658 * Buffer data: 659 * Total # of entries "on this disk". 660 * Total # of entries overall. 661 * Size of central directory. 662 * Offset to start of central dir. 663 * ZIP file comment length. 664 */ 665 $buffer = $data . $dir . self::CTRL_DIR_END . 666 pack('v', \count($ctrlDir)) . 667 pack('v', \count($ctrlDir)) . 668 pack('V', \strlen($dir)) . 669 pack('V', \strlen($data)) . 670 "\x00\x00"; 671 672 return File::write($path, $buffer); 673 } 674 675 /** 676 * Check if a path is below a given destination path 677 * 678 * @param string $destination The destination path 679 * @param string $path The path to be checked 680 * 681 * @return boolean 682 * 683 * @since 1.1.10 684 */ 685 private function isBelow($destination, $path): bool 686 { 687 $absoluteRoot = Path::clean(Path::resolve($destination)); 688 $absolutePath = Path::clean(Path::resolve($path)); 689 690 return strpos($absolutePath, $absoluteRoot) === 0; 691 } 692 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |