[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/vendor/joomla/archive/src/ -> Zip.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer