[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_joomlaupdate/ -> extract.php (source)

   1  <?php
   2  
   3  /**
   4   * @package         Joomla.Administrator
   5   * @subpackage      com_joomlaupdate
   6   *
   7   * @copyright       (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license         GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  /**
  14   * Should you want to debug this file, please add a new line ABOVE this comment with the following
  15   * contents (excluding the space star space at the start of this line):
  16   *
  17   * define('_JOOMLA_UPDATE_DEBUG', 1);
  18   *
  19   * This will do two things:
  20   * - it will create the joomla_update.txt file in your site's temporary directory (default: tmp).
  21   *   This file contains a debug log, detailing everything extract.php is doing during the extraction
  22   *   of the Joomla update ZIP file.
  23   * - It will prevent extract.php from being overwritten during the update with a new version. This
  24   *   is useful if you are testing any changes in extract.php you do not want accidentally
  25   *   overwritten, or if you are given a modified extract.php by a Joomla core contributor with
  26   *   changes which might fix your update problem.
  27   */
  28  define('_JOOMLA_UPDATE', 1);
  29  
  30  /**
  31   * ZIP archive extraction class
  32   *
  33   * This is a derivative work of Akeeba Restore which is Copyright (c)2008-2021 Nicholas K.
  34   * Dionysopoulos and Akeeba Ltd, distributed under the terms of GNU General Public License version 3
  35   * or later.
  36   *
  37   * The author of the original work has decided to relicense the derivative work under the terms of
  38   * the GNU General Public License version 2 or later and share the copyright of the derivative work
  39   * with Open Source Matters, Inc (OSM), granting OSM non-exclusive rights to this work per the terms
  40   * of the Joomla Contributor Agreement (JCA) the author signed back in 2011 and which is still in
  41   * effect. This is affirmed by the cryptographically signed commits in the Git repository containing
  42   * this file, the copyright messages and this notice here.
  43   *
  44   * @since  4.0.4
  45   */
  46  class ZIPExtraction
  47  {
  48      /**
  49       * How much data to read at once when processing files
  50       *
  51       * @var   int
  52       * @since 4.0.4
  53       */
  54      private const CHUNK_SIZE = 524288;
  55  
  56      /**
  57       * Maximum execution time (seconds).
  58       *
  59       * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large,
  60       * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an
  61       * issue in the context of updating Joomla as the ZIP archive contains fairly small files.
  62       *
  63       * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and
  64       * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think
  65       * something is broken. A value between 3 and 7 seconds is, therefore, recommended.
  66       *
  67       * @var   int
  68       * @since 4.0.4
  69       */
  70      private const MAX_EXEC_TIME = 4;
  71  
  72      /**
  73       * Run-time execution bias (percentage points).
  74       *
  75       * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already
  76       * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page
  77       * load, return the result to the client and wait for it to call us again so we can resume the extraction.
  78       *
  79       * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server
  80       * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large,
  81       * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the
  82       * extraction failing.
  83       *
  84       * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90%
  85       * without much fear that something will break.
  86       *
  87       * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of
  88       * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds.
  89       *
  90       * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files.
  91       *
  92       * @var   int
  93       * @since 4.0.4
  94       */
  95      private const RUNTIME_BIAS = 90;
  96  
  97      /**
  98       * Minimum execution time (seconds).
  99       *
 100       * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does
 101       * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection
 102       * which could be triggered if we ended up sending too many requests in a limited amount of time.
 103       *
 104       * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the
 105       * effect of almost always adding dead time in each request, unless a really large file is being extracted from the
 106       * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to
 107       * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage
 108       * spike over a short period of time.
 109       *
 110       * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate
 111       * limiting without annoying the user too much but also catering for the most badly configured of shared
 112       * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there.
 113       *
 114       * @var   int
 115       * @since 4.0.4
 116       */
 117      private const MIN_EXEC_TIME = 3;
 118  
 119      /**
 120       * Internal state when extracting files: we need to be initialised
 121       *
 122       * @var   int
 123       * @since 4.0.4
 124       */
 125      private const AK_STATE_INITIALIZE = -1;
 126  
 127      /**
 128       * Internal state when extracting files: no file currently being extracted
 129       *
 130       * @var   int
 131       * @since 4.0.4
 132       */
 133      private const AK_STATE_NOFILE = 0;
 134  
 135      /**
 136       * Internal state when extracting files: reading the file header
 137       *
 138       * @var   int
 139       * @since 4.0.4
 140       */
 141      private const AK_STATE_HEADER = 1;
 142  
 143      /**
 144       * Internal state when extracting files: reading file data
 145       *
 146       * @var   int
 147       * @since 4.0.4
 148       */
 149      private const AK_STATE_DATA = 2;
 150  
 151      /**
 152       * Internal state when extracting files: file data has been read thoroughly
 153       *
 154       * @var   int
 155       * @since 4.0.4
 156       */
 157      private const AK_STATE_DATAREAD = 3;
 158  
 159      /**
 160       * Internal state when extracting files: post-processing the file
 161       *
 162       * @var   int
 163       * @since 4.0.4
 164       */
 165      private const AK_STATE_POSTPROC = 4;
 166  
 167      /**
 168       * Internal state when extracting files: done with this file
 169       *
 170       * @var   int
 171       * @since 4.0.4
 172       */
 173      private const AK_STATE_DONE = 5;
 174  
 175      /**
 176       * Internal state when extracting files: finished extracting the ZIP file
 177       *
 178       * @var   int
 179       * @since 4.0.4
 180       */
 181      private const AK_STATE_FINISHED = 999;
 182  
 183      /**
 184       * Internal logging level: debug
 185       *
 186       * @var   int
 187       * @since 4.0.4
 188       */
 189      private const LOG_DEBUG = 1;
 190  
 191      /**
 192       * Internal logging level: information
 193       *
 194       * @var   int
 195       * @since 4.0.4
 196       */
 197      private const LOG_INFO = 10;
 198  
 199      /**
 200       * Internal logging level: warning
 201       *
 202       * @var   int
 203       * @since 4.0.4
 204       */
 205      private const LOG_WARNING = 50;
 206  
 207      /**
 208       * Internal logging level: error
 209       *
 210       * @var   int
 211       * @since 4.0.4
 212       */
 213      private const LOG_ERROR = 90;
 214  
 215      /**
 216       * Singleton instance
 217       *
 218       * @var   null|self
 219       * @since 4.0.4
 220       */
 221      private static $instance = null;
 222  
 223      /**
 224       * Debug log file pointer resource
 225       *
 226       * @var   null|resource|boolean
 227       * @since 4.0.4
 228       */
 229      private static $logFP = null;
 230  
 231      /**
 232       * Debug log filename
 233       *
 234       * @var   null|string
 235       * @since 4.0.4
 236       */
 237      private static $logFilePath = null;
 238  
 239      /**
 240       * The total size of the ZIP archive
 241       *
 242       * @var   integer
 243       * @since 4.0.4
 244       */
 245      public $totalSize = 0;
 246  
 247      /**
 248       * Which files to skip
 249       *
 250       * @var   array
 251       * @since 4.0.4
 252       */
 253      public $skipFiles = [];
 254  
 255      /**
 256       * Current tally of compressed size read
 257       *
 258       * @var   integer
 259       * @since 4.0.4
 260       */
 261      public $compressedTotal = 0;
 262  
 263      /**
 264       * Current tally of bytes written to disk
 265       *
 266       * @var   integer
 267       * @since 4.0.4
 268       */
 269      public $uncompressedTotal = 0;
 270  
 271      /**
 272       * Current tally of files extracted
 273       *
 274       * @var   integer
 275       * @since 4.0.4
 276       */
 277      public $filesProcessed = 0;
 278  
 279      /**
 280       * Maximum execution time allowance per step
 281       *
 282       * @var   integer
 283       * @since 4.0.4
 284       */
 285      private $maxExecTime = null;
 286  
 287      /**
 288       * Timestamp of execution start
 289       *
 290       * @var   integer
 291       * @since 4.0.4
 292       */
 293      private $startTime;
 294  
 295      /**
 296       * The last error message
 297       *
 298       * @var   string|null
 299       * @since 4.0.4
 300       */
 301      private $lastErrorMessage = null;
 302  
 303      /**
 304       * Archive filename
 305       *
 306       * @var   string
 307       * @since 4.0.4
 308       */
 309      private $filename = null;
 310  
 311      /**
 312       * Current archive part number
 313       *
 314       * @var   boolean
 315       * @since 4.0.4
 316       */
 317      private $archiveFileIsBeingRead = false;
 318  
 319      /**
 320       * The offset inside the current part
 321       *
 322       * @var   integer
 323       * @since 4.0.4
 324       */
 325      private $currentOffset = 0;
 326  
 327      /**
 328       * Absolute path to prepend to extracted files
 329       *
 330       * @var   string
 331       * @since 4.0.4
 332       */
 333      private $addPath = '';
 334  
 335      /**
 336       * File pointer to the current archive part file
 337       *
 338       * @var   resource|null
 339       * @since 4.0.4
 340       */
 341      private $fp = null;
 342  
 343      /**
 344       * Run state when processing the current archive file
 345       *
 346       * @var   integer
 347       * @since 4.0.4
 348       */
 349      private $runState = self::AK_STATE_INITIALIZE;
 350  
 351      /**
 352       * File header data, as read by the readFileHeader() method
 353       *
 354       * @var   stdClass
 355       * @since 4.0.4
 356       */
 357      private $fileHeader = null;
 358  
 359      /**
 360       * How much of the uncompressed data we've read so far
 361       *
 362       * @var   integer
 363       * @since 4.0.4
 364       */
 365      private $dataReadLength = 0;
 366  
 367      /**
 368       * Unwritable files in these directories are always ignored and do not cause errors when not
 369       * extracted.
 370       *
 371       * @var   array
 372       * @since 4.0.4
 373       */
 374      private $ignoreDirectories = [];
 375  
 376      /**
 377       * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring)
 378       *
 379       * @var   boolean
 380       * @since 4.0.4
 381       */
 382      private $expectDataDescriptor = false;
 383  
 384      /**
 385       * The UNIX last modification timestamp of the file last extracted
 386       *
 387       * @var   integer
 388       * @since 4.0.4
 389       */
 390      private $lastExtractedFileTimestamp = 0;
 391  
 392      /**
 393       * The file path of the file last extracted
 394       *
 395       * @var   string
 396       * @since 4.0.4
 397       */
 398      private $lastExtractedFilename = null;
 399  
 400      /**
 401       * Public constructor.
 402       *
 403       * Sets up the internal timer.
 404       *
 405       * @since   4.0.4
 406       */
 407      public function __construct()
 408      {
 409          $this->setupMaxExecTime();
 410  
 411          // Initialize start time
 412          $this->startTime = microtime(true);
 413      }
 414  
 415      /**
 416       * Singleton implementation.
 417       *
 418       * @return  static
 419       * @since   4.0.4
 420       */
 421      public static function getInstance(): self
 422      {
 423          if (is_null(self::$instance)) {
 424              self::$instance = new self();
 425          }
 426  
 427          return self::$instance;
 428      }
 429  
 430      /**
 431       * Returns a serialised copy of the object.
 432       *
 433       * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a
 434       * call to shutdown() first so any open files are closed first.
 435       *
 436       * @return  string  The serialised data, potentially base64 encoded.
 437       * @since   4.0.4
 438       */
 439      public static function getSerialised(): string
 440      {
 441          $clone = clone self::getInstance();
 442          $clone->shutdown();
 443          $serialized = serialize($clone);
 444  
 445          return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized;
 446      }
 447  
 448      /**
 449       * Restores a serialised instance into the singleton implementation and returns it.
 450       *
 451       * If the serialised data is corrupt it will return null.
 452       *
 453       * @param   string  $serialised  The serialised data, potentially base64 encoded, to deserialize.
 454       *
 455       * @return  static|null  The instance of the object, NULL if it cannot be deserialised.
 456       * @since   4.0.4
 457       */
 458      public static function unserialiseInstance(string $serialised): ?self
 459      {
 460          if (function_exists('base64_encode') && function_exists('base64_decode')) {
 461              $serialised = base64_decode($serialised);
 462          }
 463  
 464          $instance = @unserialize($serialised, [
 465                  'allowed_classes' => [
 466                      self::class,
 467                      stdClass::class,
 468                  ],
 469              ]);
 470  
 471          if (($instance === false) || !is_object($instance) || !($instance instanceof self)) {
 472              return null;
 473          }
 474  
 475          self::$instance = $instance;
 476  
 477          return self::$instance;
 478      }
 479  
 480      /**
 481       * Wakeup function, called whenever the class is deserialized.
 482       *
 483       * This method does the following:
 484       * - Restart the timer.
 485       * - Reopen the archive file, if one is defined.
 486       * - Seek to the correct offset of the file.
 487       *
 488       * @return  void
 489       * @since   4.0.4
 490       * @internal
 491       */
 492      public function __wakeup(): void
 493      {
 494          // Reset the timer when deserializing the object.
 495          $this->startTime = microtime(true);
 496  
 497          if (!$this->archiveFileIsBeingRead) {
 498              return;
 499          }
 500  
 501          $this->fp = @fopen($this->filename, 'rb');
 502  
 503          if ((is_resource($this->fp)) && ($this->currentOffset > 0)) {
 504              @fseek($this->fp, $this->currentOffset);
 505          }
 506      }
 507  
 508      /**
 509       * Enforce the minimum execution time.
 510       *
 511       * @return  void
 512       * @since   4.0.4
 513       */
 514      public function enforceMinimumExecutionTime()
 515      {
 516          $elapsed     = $this->getRunningTime() * 1000;
 517          $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1));
 518  
 519          // Only run a sleep delay if we haven't reached the minimum execution time
 520          if (($minExecTime <= $elapsed) || ($elapsed <= 0)) {
 521              return;
 522          }
 523  
 524          $sleepMillisec = intval($minExecTime - $elapsed);
 525  
 526          /**
 527           * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high
 528           * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all
 529           * other cases we will try to use usleep or time_nanosleep instead.
 530           */
 531          $longSleep          = $sleepMillisec > 1000;
 532          $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep');
 533  
 534          if (!$longSleep && $miniSleepSupported) {
 535              if (function_exists('usleep') && ($sleepMillisec < 1000)) {
 536                  usleep(1000 * $sleepMillisec);
 537  
 538                  return;
 539              }
 540  
 541              if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) {
 542                  time_nanosleep(0, 1000000 * $sleepMillisec);
 543  
 544                  return;
 545              }
 546          }
 547  
 548          if (function_exists('sleep')) {
 549              sleep(ceil($sleepMillisec / 1000));
 550  
 551              return;
 552          }
 553  
 554          if (function_exists('time_sleep_until')) {
 555              time_sleep_until(time() + ceil($sleepMillisec / 1000));
 556          }
 557      }
 558  
 559      /**
 560       * Set the filepath to the ZIP archive which will be extracted.
 561       *
 562       * @param   string  $value  The filepath to the archive. Only LOCAL files are allowed!
 563       *
 564       * @return  void
 565       * @since   4.0.4
 566       */
 567      public function setFilename(string $value)
 568      {
 569          // Security check: disallow remote filenames
 570          if (!empty($value) && strpos($value, '://') !== false) {
 571              $this->setError('Invalid archive location');
 572  
 573              return;
 574          }
 575  
 576          $this->filename = $value;
 577          $this->initializeLog(dirname($this->filename));
 578      }
 579  
 580      /**
 581       * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to.
 582       *
 583       * @param   string  $addPath  The path where the archive will be extracted.
 584       *
 585       * @return  void
 586       * @since   4.0.4
 587       */
 588      public function setAddPath(string $addPath): void
 589      {
 590          $this->addPath = $addPath;
 591          $this->addPath = str_replace('\\', '/', $this->addPath);
 592          $this->addPath = rtrim($this->addPath, '/');
 593  
 594          if (!empty($this->addPath)) {
 595              $this->addPath .= '/';
 596          }
 597      }
 598  
 599      /**
 600       * Set the list of files to skip when extracting the ZIP file.
 601       *
 602       * @param   array  $skipFiles  A list of files to skip when extracting the ZIP archive
 603       *
 604       * @return  void
 605       * @since   4.0.4
 606       */
 607      public function setSkipFiles(array $skipFiles): void
 608      {
 609          $this->skipFiles = array_values($skipFiles);
 610      }
 611  
 612      /**
 613       * Set the directories to skip over when extracting the ZIP archive
 614       *
 615       * @param   array  $ignoreDirectories  The list of directories to ignore.
 616       *
 617       * @return  void
 618       * @since   4.0.4
 619       */
 620      public function setIgnoreDirectories(array $ignoreDirectories): void
 621      {
 622          $this->ignoreDirectories = array_values($ignoreDirectories);
 623      }
 624  
 625      /**
 626       * Prepares for the archive extraction
 627       *
 628       * @return  void
 629       * @since   4.0.4
 630       */
 631      public function initialize(): void
 632      {
 633          $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename));
 634          $this->totalSize              = @filesize($this->filename) ?: 0;
 635          $this->archiveFileIsBeingRead = false;
 636          $this->currentOffset          = 0;
 637          $this->runState               = self::AK_STATE_NOFILE;
 638  
 639          $this->readArchiveHeader();
 640  
 641          if (!empty($this->getError())) {
 642              $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR);
 643  
 644              return;
 645          }
 646  
 647          $this->archiveFileIsBeingRead = true;
 648          $this->runState               = self::AK_STATE_NOFILE;
 649  
 650          $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG);
 651      }
 652  
 653      /**
 654       * Executes a step of the archive extraction
 655       *
 656       * @return  boolean  True if we are done extracting or an error occurred
 657       * @since   4.0.4
 658       */
 659      public function step(): bool
 660      {
 661          $status = true;
 662  
 663          $this->debugMsg('Starting a new step', self::LOG_INFO);
 664  
 665          while ($status && ($this->getTimeLeft() > 0)) {
 666              switch ($this->runState) {
 667                  case self::AK_STATE_INITIALIZE:
 668                      $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG);
 669                      $this->initialize();
 670                      break;
 671  
 672                  case self::AK_STATE_NOFILE:
 673                      $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG);
 674                      $status = $this->readFileHeader();
 675  
 676                      if ($status) {
 677                          $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG);
 678  
 679                          // Update running tallies when we start extracting a file
 680                          $this->filesProcessed++;
 681                          $this->compressedTotal   += array_key_exists('compressed', get_object_vars($this->fileHeader))
 682                              ? $this->fileHeader->compressed : 0;
 683                          $this->uncompressedTotal += $this->fileHeader->uncompressed;
 684                      }
 685  
 686                      break;
 687  
 688                  case self::AK_STATE_HEADER:
 689                  case self::AK_STATE_DATA:
 690                      $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA';
 691                      $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG);
 692  
 693                      $status = $this->processFileData();
 694                      break;
 695  
 696                  case self::AK_STATE_DATAREAD:
 697                  case self::AK_STATE_POSTPROC:
 698                      $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC';
 699                      $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG);
 700  
 701                      $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp);
 702                      $this->processLastExtractedFile();
 703  
 704                      $status         = true;
 705                      $this->runState = self::AK_STATE_DONE;
 706                      break;
 707  
 708                  case self::AK_STATE_DONE:
 709                  default:
 710                      $this->debugMsg('Current run state: DONE', self::LOG_DEBUG);
 711                      $this->runState = self::AK_STATE_NOFILE;
 712  
 713                      break;
 714  
 715                  case self::AK_STATE_FINISHED:
 716                      $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG);
 717                      $status = false;
 718                      break;
 719              }
 720  
 721              if ($this->getTimeLeft() <= 0) {
 722                  $this->debugMsg('Ran out of time; the step will break.');
 723              } elseif (!$status) {
 724                  $this->debugMsg('Last step status is false; the step will break.');
 725              }
 726          }
 727  
 728          $error = $this->getError() ?? null;
 729  
 730          if (!empty($error)) {
 731              $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR);
 732          }
 733  
 734          // Did we just finish or run into an error?
 735          if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) {
 736              $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG);
 737  
 738              // Reset internal state, prevents __wakeup from trying to open a non-existent file
 739              $this->archiveFileIsBeingRead = false;
 740  
 741              return true;
 742          }
 743  
 744          $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG);
 745  
 746          return false;
 747      }
 748  
 749      /**
 750       * Get the most recent error message
 751       *
 752       * @return  string|null  The message string, null if there's no error
 753       * @since   4.0.4
 754       */
 755      public function getError(): ?string
 756      {
 757          return $this->lastErrorMessage;
 758      }
 759  
 760      /**
 761       * Gets the number of seconds left, before we hit the "must break" threshold
 762       *
 763       * @return  float
 764       * @since   4.0.4
 765       */
 766      private function getTimeLeft(): float
 767      {
 768          return $this->maxExecTime - $this->getRunningTime();
 769      }
 770  
 771      /**
 772       * Gets the time elapsed since object creation/unserialization, effectively how
 773       * long Akeeba Engine has been processing data
 774       *
 775       * @return  float
 776       * @since   4.0.4
 777       */
 778      private function getRunningTime(): float
 779      {
 780          return microtime(true) - $this->startTime;
 781      }
 782  
 783      /**
 784       * Process the last extracted file or directory
 785       *
 786       * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp.
 787       *
 788       * @return  void
 789       * @since   4.0.4
 790       */
 791      private function processLastExtractedFile(): void
 792      {
 793          $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG);
 794  
 795          if (@is_file($this->lastExtractedFilename)) {
 796              @chmod($this->lastExtractedFilename, 0644);
 797  
 798              clearFileInOPCache($this->lastExtractedFilename);
 799          } else {
 800              @chmod($this->lastExtractedFilename, 0755);
 801          }
 802  
 803          if ($this->lastExtractedFileTimestamp > 0) {
 804              @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp);
 805          }
 806      }
 807  
 808      /**
 809       * Set the last extracted filename
 810       *
 811       * @param   string|null  $lastExtractedFilename  The last extracted filename
 812       *
 813       * @return  void
 814       * @since   4.0.4
 815       */
 816      private function setLastExtractedFilename(?string $lastExtractedFilename): void
 817      {
 818          $this->lastExtractedFilename = $lastExtractedFilename;
 819      }
 820  
 821      /**
 822       * Set the last modification UNIX timestamp for the last extracted file
 823       *
 824       * @param   int  $lastExtractedFileTimestamp  The timestamp
 825       *
 826       * @return  void
 827       * @since   4.0.4
 828       */
 829      private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void
 830      {
 831          $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp;
 832      }
 833  
 834      /**
 835       * Sleep function, called whenever the class is serialized
 836       *
 837       * @return  void
 838       * @since   4.0.4
 839       * @internal
 840       */
 841      private function shutdown(): void
 842      {
 843          if (is_resource(self::$logFP)) {
 844              @fclose(self::$logFP);
 845          }
 846  
 847          if (!is_resource($this->fp)) {
 848              return;
 849          }
 850  
 851          $this->currentOffset = @ftell($this->fp);
 852  
 853          @fclose($this->fp);
 854      }
 855  
 856      /**
 857       * Unicode-safe binary data length
 858       *
 859       * @param   string|null  $string  The binary data to get the length for
 860       *
 861       * @return  integer
 862       * @since   4.0.4
 863       */
 864      private function binStringLength(?string $string): int
 865      {
 866          if (is_null($string)) {
 867              return 0;
 868          }
 869  
 870          if (function_exists('mb_strlen')) {
 871              return mb_strlen($string, '8bit') ?: 0;
 872          }
 873  
 874          return strlen($string) ?: 0;
 875      }
 876  
 877      /**
 878       * Add an error message
 879       *
 880       * @param   string  $error  Error message
 881       *
 882       * @return  void
 883       * @since   4.0.4
 884       */
 885      private function setError(string $error): void
 886      {
 887          $this->lastErrorMessage = $error;
 888      }
 889  
 890      /**
 891       * Reads data from the archive.
 892       *
 893       * @param   resource  $fp      The file pointer to read data from
 894       * @param   int|null  $length  The volume of data to read, in bytes
 895       *
 896       * @return  string  The data read from the file
 897       * @since   4.0.4
 898       */
 899      private function fread($fp, ?int $length = null): string
 900      {
 901          $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX;
 902          $data       = fread($fp, $readLength);
 903  
 904          if ($data === false) {
 905              $this->debugMsg('No more data could be read from the file', self::LOG_WARNING);
 906  
 907              $data = '';
 908          }
 909  
 910          return $data;
 911      }
 912  
 913      /**
 914       * Read the header of the archive, making sure it's a valid ZIP file.
 915       *
 916       * @return  void
 917       * @since   4.0.4
 918       */
 919      private function readArchiveHeader(): void
 920      {
 921          $this->debugMsg('Reading the archive header.', self::LOG_DEBUG);
 922  
 923          // Open the first part
 924          $this->openArchiveFile();
 925  
 926          // Fail for unreadable files
 927          if ($this->fp === false) {
 928              return;
 929          }
 930  
 931          // Read the header data.
 932          $sigBinary  = fread($this->fp, 4);
 933          $headerData = unpack('Vsig', $sigBinary);
 934  
 935          // We only support single part ZIP files
 936          if ($headerData['sig'] != 0x04034b50) {
 937              $this->setError('The archive file is corrupt: bad header');
 938  
 939              return;
 940          }
 941  
 942          // Roll back the file pointer
 943          fseek($this->fp, -4, SEEK_CUR);
 944  
 945          $this->currentOffset  = @ftell($this->fp);
 946          $this->dataReadLength = 0;
 947      }
 948  
 949      /**
 950       * Concrete classes must use this method to read the file header
 951       *
 952       * @return boolean True if reading the file was successful, false if an error occurred or we
 953       *                 reached end of archive.
 954       * @since   4.0.4
 955       */
 956      private function readFileHeader(): bool
 957      {
 958          $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG);
 959  
 960          if (!is_resource($this->fp)) {
 961              $this->setError('The archive is not open for reading.');
 962  
 963              return false;
 964          }
 965  
 966          // Unexpected end of file
 967          if ($this->isEOF()) {
 968              $this->debugMsg('EOF when reading file header data', self::LOG_WARNING);
 969              $this->setError('The archive is corrupt or truncated');
 970  
 971              return false;
 972          }
 973  
 974          $this->currentOffset = ftell($this->fp);
 975  
 976          if ($this->expectDataDescriptor) {
 977              $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG);
 978  
 979              /**
 980               * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data
 981               * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor
 982               * header (0x08074b50).
 983               */
 984              $junk       = @fread($this->fp, 4);
 985              $junk       = unpack('Vsig', $junk);
 986              $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8;
 987              $junk       = @fread($this->fp, $readLength);
 988  
 989              // And check for EOF, too
 990              if ($this->isEOF()) {
 991                  $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING);
 992                  $this->setError('The archive is corrupt or truncated');
 993  
 994                  return false;
 995              }
 996          }
 997  
 998          // Get and decode Local File Header
 999          $headerBinary = fread($this->fp, 30);
1000          $format       = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/'
1001              . 'Vuncomp/vfnamelen/veflen';
1002          $headerData   = unpack($format, $headerBinary);
1003  
1004          // Check signature
1005          if (!($headerData['sig'] == 0x04034b50)) {
1006              // The signature is not the one used for files. Is this a central directory record (i.e. we're done)?
1007              if ($headerData['sig'] == 0x02014b50) {
1008                  $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG);
1009  
1010                  // End of ZIP file detected. We'll just skip to the end of file...
1011                  @fseek($this->fp, 0, SEEK_END);
1012                  $this->runState = self::AK_STATE_FINISHED;
1013  
1014                  return false;
1015              }
1016  
1017              $this->setError('The archive file is corrupt or truncated');
1018  
1019              return false;
1020          }
1021  
1022          // If bit 3 of the bitflag is set, expectDataDescriptor is true
1023          $this->expectDataDescriptor  = ($headerData['bitflag'] & 4) == 4;
1024          $this->fileHeader            = new stdClass();
1025          $this->fileHeader->timestamp = 0;
1026  
1027          // Read the last modified date and time
1028          $lastmodtime = $headerData['lastmodtime'];
1029          $lastmoddate = $headerData['lastmoddate'];
1030  
1031          if ($lastmoddate && $lastmodtime) {
1032              $vHour    = ($lastmodtime & 0xF800) >> 11;
1033              $vMInute  = ($lastmodtime & 0x07E0) >> 5;
1034              $vSeconds = ($lastmodtime & 0x001F) * 2;
1035              $vYear    = (($lastmoddate & 0xFE00) >> 9) + 1980;
1036              $vMonth   = ($lastmoddate & 0x01E0) >> 5;
1037              $vDay     = $lastmoddate & 0x001F;
1038  
1039              $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear);
1040          }
1041  
1042          $isBannedFile = false;
1043  
1044          $this->fileHeader->compressed   = $headerData['compsize'];
1045          $this->fileHeader->uncompressed = $headerData['uncomp'];
1046          $nameFieldLength                = $headerData['fnamelen'];
1047          $extraFieldLength               = $headerData['eflen'];
1048  
1049          // Read filename field
1050          $this->fileHeader->file = fread($this->fp, $nameFieldLength);
1051  
1052          // Read extra field if present
1053          if ($extraFieldLength > 0) {
1054              $extrafield = fread($this->fp, $extraFieldLength);
1055          }
1056  
1057          // Decide filetype -- Check for directories
1058          $this->fileHeader->type = 'file';
1059  
1060          if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) {
1061              $this->fileHeader->type = 'dir';
1062          }
1063  
1064          // Decide filetype -- Check for symbolic links
1065          if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) {
1066              $this->fileHeader->type = 'link';
1067          }
1068  
1069          switch ($headerData['compmethod']) {
1070              case 0:
1071                  $this->fileHeader->compression = 'none';
1072                  break;
1073              case 8:
1074                  $this->fileHeader->compression = 'gzip';
1075                  break;
1076              default:
1077                  $messageTemplate = 'This script cannot handle ZIP compression method %d. '
1078                      . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.';
1079                  $actualMessage   = sprintf($messageTemplate, $headerData['compmethod']);
1080                  $this->setError($actualMessage);
1081  
1082                  return false;
1083                  break;
1084          }
1085  
1086          // Find hard-coded banned files
1087          if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) {
1088              $isBannedFile = true;
1089          }
1090  
1091          // Also try to find banned files passed in class configuration
1092          if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) {
1093              $isBannedFile = true;
1094          }
1095  
1096          // If we have a banned file, let's skip it
1097          if ($isBannedFile) {
1098              $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file);
1099              $this->debugMsg($debugMessage, self::LOG_DEBUG);
1100  
1101              // Advance the file pointer, skipping exactly the size of the compressed data
1102              $seekleft = $this->fileHeader->compressed;
1103  
1104              while ($seekleft > 0) {
1105                  // Ensure that we can seek past archive part boundaries
1106                  $curSize = @filesize($this->filename);
1107                  $curPos  = @ftell($this->fp);
1108                  $canSeek = $curSize - $curPos;
1109                  $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek;
1110                  @fseek($this->fp, $canSeek, SEEK_CUR);
1111                  $seekleft -= $canSeek;
1112  
1113                  if ($seekleft) {
1114                      $this->setError('The archive is corrupt or truncated');
1115  
1116                      return false;
1117                  }
1118              }
1119  
1120              $this->currentOffset = @ftell($this->fp);
1121              $this->runState      = self::AK_STATE_DONE;
1122  
1123              return true;
1124          }
1125  
1126          // Last chance to prepend a path to the filename
1127          if (!empty($this->addPath)) {
1128              $this->fileHeader->file = $this->addPath . $this->fileHeader->file;
1129          }
1130  
1131          // Get the translated path name
1132          if ($this->fileHeader->type == 'file') {
1133              $this->fileHeader->realFile = $this->fileHeader->file;
1134              $this->setLastExtractedFilename($this->fileHeader->file);
1135          } elseif ($this->fileHeader->type == 'dir') {
1136              $this->fileHeader->timestamp = 0;
1137  
1138              $dir = $this->fileHeader->file;
1139  
1140              if (!@is_dir($dir)) {
1141                  mkdir($dir, 0755, true);
1142              }
1143  
1144              $this->setLastExtractedFilename(null);
1145          } else {
1146              // Symlink; do not post-process
1147              $this->fileHeader->timestamp = 0;
1148              $this->setLastExtractedFilename(null);
1149          }
1150  
1151          $this->createDirectory();
1152  
1153          // Header is read
1154          $this->runState = self::AK_STATE_HEADER;
1155  
1156          return true;
1157      }
1158  
1159      /**
1160       * Creates the directory this file points to
1161       *
1162       * @return  void
1163       * @since   4.0.4
1164       */
1165      private function createDirectory(): void
1166      {
1167          // Do we need to create a directory?
1168          if (empty($this->fileHeader->realFile)) {
1169              $this->fileHeader->realFile = $this->fileHeader->file;
1170          }
1171  
1172          $lastSlash = strrpos($this->fileHeader->realFile, '/');
1173          $dirName   = substr($this->fileHeader->realFile, 0, $lastSlash);
1174          $perms     = 0755;
1175          $ignore    = $this->isIgnoredDirectory($dirName);
1176  
1177          if (@is_dir($dirName)) {
1178              return;
1179          }
1180  
1181          if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) {
1182              $this->setError(sprintf('Could not create %s folder', $dirName));
1183          }
1184      }
1185  
1186      /**
1187       * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when
1188       * it's finished processing the file data.
1189       *
1190       * @return   boolean  True if processing the file data was successful, false if an error occurred
1191       * @since   4.0.4
1192       */
1193      private function processFileData(): bool
1194      {
1195          switch ($this->fileHeader->type) {
1196              case 'dir':
1197                  $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG);
1198  
1199                  return $this->processTypeDir();
1200                  break;
1201  
1202              case 'link':
1203                  $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG);
1204  
1205                  return $this->processTypeLink();
1206                  break;
1207  
1208              case 'file':
1209                  switch ($this->fileHeader->compression) {
1210                      case 'none':
1211                          $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG);
1212  
1213                          return $this->processTypeFileUncompressed();
1214                          break;
1215  
1216                      case 'gzip':
1217                      case 'bzip2':
1218                          $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG);
1219  
1220                          return $this->processTypeFileCompressed();
1221                          break;
1222  
1223                      case 'default':
1224                          $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression));
1225  
1226                          return false;
1227                          break;
1228                  }
1229                  break;
1230          }
1231  
1232          $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type));
1233  
1234          return false;
1235      }
1236  
1237      /**
1238       * Opens the next part file for reading
1239       *
1240       * @return  void
1241       * @since   4.0.4
1242       */
1243      private function openArchiveFile(): void
1244      {
1245          $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG);
1246  
1247          if ($this->archiveFileIsBeingRead) {
1248              return;
1249          }
1250  
1251          if (is_resource($this->fp)) {
1252              @fclose($this->fp);
1253          }
1254  
1255          $this->fp = @fopen($this->filename, 'rb');
1256  
1257          if ($this->fp === false) {
1258              $message = 'Could not open archive for reading. Check that the file exists, is '
1259                  . 'readable by the web server and is not in a directory made out of reach by chroot, '
1260                  . 'open_basedir restrictions or any other restriction put in place by your host.';
1261              $this->setError($message);
1262  
1263              return;
1264          }
1265  
1266          fseek($this->fp, 0);
1267          $this->currentOffset = 0;
1268      }
1269  
1270      /**
1271       * Returns true if we have reached the end of file
1272       *
1273       * @return   boolean  True if we have reached End Of File
1274       * @since   4.0.4
1275       */
1276      private function isEOF(): bool
1277      {
1278          /**
1279           * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a
1280           * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part
1281           * after the logical OR.
1282           */
1283          return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename));
1284      }
1285  
1286      /**
1287       * Handles the permissions of the parent directory to a file and the file itself to make it writeable.
1288       *
1289       * @param   string  $path  A path to a file
1290       *
1291       * @return  void
1292       * @since   4.0.4
1293       */
1294      private function setCorrectPermissions(string $path): void
1295      {
1296          static $rootDir = null;
1297  
1298          if (is_null($rootDir)) {
1299              $rootDir = rtrim($this->addPath, '/\\');
1300          }
1301  
1302          $directory = rtrim(dirname($path), '/\\');
1303  
1304          // Is this an unwritable directory?
1305          if (($directory != $rootDir) && !is_writeable($directory)) {
1306              @chmod($directory, 0755);
1307          }
1308  
1309          @chmod($path, 0644);
1310      }
1311  
1312      /**
1313       * Is this file or directory contained in a directory we've decided to ignore
1314       * write errors for? This is useful to let the extraction work despite write
1315       * errors in the log, logs and tmp directories which MIGHT be used by the system
1316       * on some low quality hosts and Plesk-powered hosts.
1317       *
1318       * @param   string  $shortFilename  The relative path of the file/directory in the package
1319       *
1320       * @return  boolean  True if it belongs in an ignored directory
1321       * @since   4.0.4
1322       */
1323      private function isIgnoredDirectory(string $shortFilename): bool
1324      {
1325          $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename);
1326  
1327          return in_array($check, $this->ignoreDirectories);
1328      }
1329  
1330      /**
1331       * Process the file data of a directory entry
1332       *
1333       * @return  boolean
1334       * @since   4.0.4
1335       */
1336      private function processTypeDir(): bool
1337      {
1338          // Directory entries do not have file data, therefore we're done processing the entry.
1339          $this->runState = self::AK_STATE_DATAREAD;
1340  
1341          return true;
1342      }
1343  
1344      /**
1345       * Process the file data of a link entry
1346       *
1347       * @return  boolean
1348       * @since   4.0.4
1349       */
1350      private function processTypeLink(): bool
1351      {
1352          $toReadBytes = 0;
1353          $leftBytes   = $this->fileHeader->compressed;
1354          $data        = '';
1355  
1356          while ($leftBytes > 0) {
1357              $toReadBytes     = min($leftBytes, self::CHUNK_SIZE);
1358              $mydata          = $this->fread($this->fp, $toReadBytes);
1359              $reallyReadBytes = $this->binStringLength($mydata);
1360              $data            .= $mydata;
1361              $leftBytes       -= $reallyReadBytes;
1362  
1363              if ($reallyReadBytes < $toReadBytes) {
1364                  // We read less than requested!
1365                  if ($this->isEOF()) {
1366                      $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING);
1367                      $this->setError('The archive file is corrupt or truncated');
1368  
1369                      return false;
1370                  }
1371              }
1372          }
1373  
1374          $filename = $this->fileHeader->realFile ?? $this->fileHeader->file;
1375  
1376          // Try to remove an existing file or directory by the same name
1377          if (file_exists($filename)) {
1378              clearFileInOPCache($filename);
1379              @unlink($filename);
1380              @rmdir($filename);
1381          }
1382  
1383          // Remove any trailing slash
1384          if (substr($filename, -1) == '/') {
1385              $filename = substr($filename, 0, -1);
1386          }
1387  
1388          // Create the symlink
1389          @symlink($data, $filename);
1390  
1391          $this->runState = self::AK_STATE_DATAREAD;
1392  
1393          // No matter if the link was created!
1394          return true;
1395      }
1396  
1397      /**
1398       * Processes an uncompressed (stored) file
1399       *
1400       * @return  boolean
1401       * @since   4.0.4
1402       */
1403      private function processTypeFileUncompressed(): bool
1404      {
1405          // Uncompressed files are being processed in small chunks, to avoid timeouts
1406          if ($this->dataReadLength == 0) {
1407              // Before processing file data, ensure permissions are adequate
1408              $this->setCorrectPermissions($this->fileHeader->file);
1409          }
1410  
1411          // Open the output file
1412          $ignore = $this->isIgnoredDirectory($this->fileHeader->file);
1413  
1414          $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab';
1415          $outfp     = @fopen($this->fileHeader->realFile, $writeMode);
1416  
1417          // Can we write to the file?
1418          if (($outfp === false) && (!$ignore)) {
1419              // An error occurred
1420              $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile));
1421  
1422              return false;
1423          }
1424  
1425          // Does the file have any data, at all?
1426          if ($this->fileHeader->compressed == 0) {
1427              // No file data!
1428              if (is_resource($outfp)) {
1429                  @fclose($outfp);
1430              }
1431  
1432              $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG);
1433  
1434              $this->runState = self::AK_STATE_DATAREAD;
1435  
1436              return true;
1437          }
1438  
1439          $leftBytes = $this->fileHeader->compressed - $this->dataReadLength;
1440  
1441          // Loop while there's data to read and enough time to do it
1442          while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) {
1443              $toReadBytes          = min($leftBytes, self::CHUNK_SIZE);
1444              $data                 = $this->fread($this->fp, $toReadBytes);
1445              $reallyReadBytes      = $this->binStringLength($data);
1446              $leftBytes            -= $reallyReadBytes;
1447              $this->dataReadLength += $reallyReadBytes;
1448  
1449              if ($reallyReadBytes < $toReadBytes) {
1450                  // We read less than requested! Why? Did we hit local EOF?
1451                  if ($this->isEOF()) {
1452                      // Nope. The archive is corrupt
1453                      $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING);
1454                      $this->setError('The archive file is corrupt or truncated');
1455  
1456                      return false;
1457                  }
1458              }
1459  
1460              if (is_resource($outfp)) {
1461                  @fwrite($outfp, $data);
1462              }
1463  
1464              if ($this->getTimeLeft()) {
1465                  $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG);
1466              }
1467          }
1468  
1469          // Close the file pointer
1470          if (is_resource($outfp)) {
1471              @fclose($outfp);
1472          }
1473  
1474          // Was this a pre-timeout bail out?
1475          if ($leftBytes > 0) {
1476              $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG);
1477              $this->runState = self::AK_STATE_DATA;
1478  
1479              return true;
1480          }
1481  
1482          // Oh! We just finished!
1483          $this->runState       = self::AK_STATE_DATAREAD;
1484          $this->dataReadLength = 0;
1485  
1486          return true;
1487      }
1488  
1489      /**
1490       * Processes a compressed file
1491       *
1492       * @return  boolean
1493       * @since   4.0.4
1494       */
1495      private function processTypeFileCompressed(): bool
1496      {
1497          // Before processing file data, ensure permissions are adequate
1498          $this->setCorrectPermissions($this->fileHeader->file);
1499  
1500          // Open the output file
1501          $outfp = @fopen($this->fileHeader->realFile, 'wb');
1502  
1503          // Can we write to the file?
1504          $ignore = $this->isIgnoredDirectory($this->fileHeader->file);
1505  
1506          if (($outfp === false) && (!$ignore)) {
1507              // An error occurred
1508              $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile));
1509  
1510              return false;
1511          }
1512  
1513          // Does the file have any data, at all?
1514          if ($this->fileHeader->compressed == 0) {
1515              $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG);
1516  
1517              // No file data!
1518              if (is_resource($outfp)) {
1519                  @fclose($outfp);
1520              }
1521  
1522              $this->runState = self::AK_STATE_DATAREAD;
1523  
1524              return true;
1525          }
1526  
1527          // Simple compressed files are processed as a whole; we can't do chunk processing
1528          $zipData = $this->fread($this->fp, $this->fileHeader->compressed);
1529  
1530          while ($this->binStringLength($zipData) < $this->fileHeader->compressed) {
1531              // End of local file before reading all data?
1532              if ($this->isEOF()) {
1533                  $this->debugMsg('EOF reading compressed data', self::LOG_WARNING);
1534                  $this->setError('The archive file is corrupt or truncated');
1535  
1536                  return false;
1537              }
1538          }
1539  
1540          switch ($this->fileHeader->compression) {
1541              case 'gzip':
1542                  /** @noinspection PhpComposerExtensionStubsInspection */
1543                  $unzipData = gzinflate($zipData);
1544                  break;
1545  
1546              case 'bzip2':
1547                  /** @noinspection PhpComposerExtensionStubsInspection */
1548                  $unzipData = bzdecompress($zipData);
1549                  break;
1550  
1551              default:
1552                  $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression));
1553  
1554                  return false;
1555                  break;
1556          }
1557  
1558          unset($zipData);
1559  
1560          // Write to the file.
1561          if (is_resource($outfp)) {
1562              @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed);
1563              @fclose($outfp);
1564          }
1565  
1566          unset($unzipData);
1567  
1568          $this->runState = self::AK_STATE_DATAREAD;
1569  
1570          return true;
1571      }
1572  
1573      /**
1574       * Set up the maximum execution time
1575       *
1576       * @return  void
1577       * @since   4.0.4
1578       */
1579      private function setupMaxExecTime(): void
1580      {
1581          $configMaxTime     = self::MAX_EXEC_TIME;
1582          $bias              = self::RUNTIME_BIAS / 100;
1583          $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias;
1584      }
1585  
1586      /**
1587       * Get the PHP maximum execution time.
1588       *
1589       * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds.
1590       *
1591       * @return  integer
1592       * @since   4.0.4
1593       */
1594      private function getPhpMaxExecTime(): int
1595      {
1596          if (!@function_exists('ini_get')) {
1597              return 10;
1598          }
1599  
1600          $phpMaxTime = @ini_get("maximum_execution_time");
1601          $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10;
1602  
1603          return max(1, $phpMaxTime);
1604      }
1605  
1606      /**
1607       * Write a message to the debug error log
1608       *
1609       * @param   string  $message   The message to log
1610       * @param   int     $priority  The message's log priority
1611       *
1612       * @return  void
1613       * @since   4.0.4
1614       */
1615      private function debugMsg(string $message, int $priority = self::LOG_INFO): void
1616      {
1617          if (!defined('_JOOMLA_UPDATE_DEBUG')) {
1618              return;
1619          }
1620  
1621          if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) {
1622              self::$logFP = @fopen(self::$logFilePath, 'at');
1623          }
1624  
1625          if (!is_resource(self::$logFP)) {
1626              return;
1627          }
1628  
1629          switch ($priority) {
1630              case self::LOG_DEBUG:
1631                  $priorityString = 'DEBUG';
1632                  break;
1633  
1634              case self::LOG_INFO:
1635                  $priorityString = 'INFO';
1636                  break;
1637  
1638              case self::LOG_WARNING:
1639                  $priorityString = 'WARNING';
1640                  break;
1641  
1642              case self::LOG_ERROR:
1643                  $priorityString = 'ERROR';
1644                  break;
1645          }
1646  
1647          fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message));
1648      }
1649  
1650      /**
1651       * Initialise the debug log file
1652       *
1653       * @param   string  $logPath  The path where the log file will be written to
1654       *
1655       * @return  void
1656       * @since   4.0.4
1657       */
1658      private function initializeLog(string $logPath): void
1659      {
1660          if (!defined('_JOOMLA_UPDATE_DEBUG')) {
1661              return;
1662          }
1663  
1664          $logPath = $logPath ?: dirname($this->filename);
1665          $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt';
1666  
1667          self::$logFilePath = $logFile;
1668      }
1669  }
1670  
1671  // Skip over the mini-controller for testing purposes
1672  if (defined('_JOOMLA_UPDATE_TESTING')) {
1673      return;
1674  }
1675  
1676  /**
1677   * Invalidate a file in OPcache.
1678   *
1679   * Only applies if the file has a .php extension.
1680   *
1681   * @param   string  $file  The filepath to clear from OPcache
1682   *
1683   * @return  boolean
1684   * @since   4.0.4
1685   */
1686  function clearFileInOPCache(string $file): bool
1687  {
1688      static $hasOpCache = null;
1689  
1690      if (is_null($hasOpCache)) {
1691          $hasOpCache = ini_get('opcache.enable')
1692              && function_exists('opcache_invalidate')
1693              && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0);
1694      }
1695  
1696      if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) {
1697          return opcache_invalidate($file, true);
1698      }
1699  
1700      return false;
1701  }
1702  
1703  /**
1704   * A timing safe equals comparison.
1705   *
1706   * Uses the built-in hash_equals() method if it exists. It SHOULD exist, as it's available since PHP 5.6 whereas even
1707   * Joomla 4.0 requires PHP 7.2 or later. If for any reason the built-in function is not available (for example, a host
1708   * has disabled it because they do not understand the first thing about security) we will fall back to a safe, userland
1709   * implementation.
1710   *
1711   * @param   string  $known  The known value to check against
1712   * @param   string  $user   The user submitted value to check
1713   *
1714   * @return  boolean  True if the two strings are identical.
1715   * @since   4.0.4
1716   *
1717   * @see     http://blog.ircmaxell.com/2014/11/its-all-about-time.html
1718   */
1719  function timingSafeEquals($known, $user)
1720  {
1721      if (function_exists('hash_equals')) {
1722          return hash_equals($known, $user);
1723      }
1724  
1725      $safeLen = strlen($known);
1726      $userLen = strlen($user);
1727  
1728      if ($userLen != $safeLen) {
1729          return false;
1730      }
1731  
1732      $result = 0;
1733  
1734      for ($i = 0; $i < $userLen; $i++) {
1735          $result |= (ord($known[$i]) ^ ord($user[$i]));
1736      }
1737  
1738      // They are only identical strings if $result is exactly 0...
1739      return $result === 0;
1740  }
1741  
1742  /**
1743   * Gets the configuration parameters from the update.php file and validates the password sent with
1744   * the request.
1745   *
1746   * @return  array|null  The configuration parameters to use. NULL if this is an invalid request.
1747   * @since   4.0.4
1748   */
1749  function getConfiguration(): ?array
1750  {
1751      // Make sure the locale is correct for basename() to work
1752      if (function_exists('setlocale')) {
1753          @setlocale(LC_ALL, 'en_US.UTF8');
1754      }
1755  
1756      // Require update.php or fail
1757      $setupFile = __DIR__ . '/update.php';
1758  
1759      if (!file_exists($setupFile)) {
1760          return null;
1761      }
1762  
1763      /**
1764       * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to
1765       * remove it from the server.
1766       *
1767       * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple
1768       * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can
1769       * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of
1770       * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders
1771       * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the
1772       * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky).
1773       *
1774       * It is still advisable to remove the update.php file once you are done with the extraction. This check
1775       * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user
1776       * action to remove the update.php file from their server.
1777       */
1778      clearstatcache(true);
1779      $setupFileCreationTime = filectime($setupFile);
1780  
1781      if (abs(time() - $setupFileCreationTime) > 5400) {
1782          return null;
1783      }
1784  
1785      // Load update.php. It pulls a variable named $restoration_setup into the local scope.
1786      clearFileInOPCache($setupFile);
1787  
1788      require_once $setupFile;
1789  
1790      /** @var array $extractionSetup */
1791  
1792      // The file exists but no configuration is present?
1793      if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) {
1794          return null;
1795      }
1796  
1797      /**
1798       * Immediately reject any attempt to run extract.php without a password.
1799       *
1800       * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script
1801       * to run without a password.
1802       */
1803      $password     = $extractionSetup['security.password'] ?? null;
1804      $userPassword = $_REQUEST['password'] ?? '';
1805      $userPassword = !is_string($userPassword) ? '' : trim($userPassword);
1806  
1807      if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) {
1808          return null;
1809      }
1810  
1811      // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html
1812      if (!timingSafeEquals($password, $userPassword)) {
1813          return null;
1814      }
1815  
1816      // An "instance" variable will resume the engine from the serialised instance
1817      $serialized = $_REQUEST['instance'] ?? null;
1818  
1819      if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) {
1820          // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS!
1821          return null;
1822      }
1823  
1824      return $extractionSetup;
1825  }
1826  
1827  // Import configuration
1828  $retArray = [
1829      'status'  => true,
1830      'message' => null,
1831  ];
1832  
1833  $configuration = getConfiguration();
1834  $enabled       = !empty($configuration);
1835  
1836  /**
1837   * Sets the PHP timeout to 3600 seconds
1838   *
1839   * @return  void
1840   * @since   4.2.0
1841   */
1842  function setLongTimeout()
1843  {
1844      if (!function_exists('ini_set')) {
1845          return;
1846      }
1847  
1848      ini_set('max_execution_time', 3600);
1849  }
1850  
1851  /**
1852   * Sets the memory limit to 1GiB
1853   *
1854   * @return  void
1855   * @since   4.2.0
1856   */
1857  function setHugeMemoryLimit()
1858  {
1859      if (!function_exists('ini_set')) {
1860          return;
1861      }
1862  
1863      ini_set('memory_limit', 1073741824);
1864  }
1865  
1866  if ($enabled) {
1867      // Try to set a very large memory and timeout limit
1868      setLongTimeout();
1869      setHugeMemoryLimit();
1870  
1871      $sourcePath = $configuration['setup.sourcepath'] ?? '';
1872      $sourceFile = $configuration['setup.sourcefile'] ?? '';
1873      $destDir    = ($configuration['setup.destdir'] ?? null) ?: __DIR__;
1874      $basePath   = rtrim(str_replace('\\', '/', __DIR__), '/');
1875      $basePath   = empty($basePath) ? $basePath : ($basePath . '/');
1876      $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile;
1877      $engine     = ZIPExtraction::getInstance();
1878  
1879      $engine->setFilename($sourceFile);
1880      $engine->setAddPath($destDir);
1881      $skipFiles = [
1882          'administrator/components/com_joomlaupdate/restoration.php',
1883          'administrator/components/com_joomlaupdate/update.php',
1884      ];
1885  
1886      if (defined('_JOOMLA_UPDATE_DEBUG')) {
1887          $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php';
1888      }
1889  
1890      $engine->setSkipFiles($skipFiles);
1891      $engine->setIgnoreDirectories([
1892              'tmp', 'administrator/logs',
1893          ]);
1894  
1895      $task = $_REQUEST['task'] ?? null;
1896  
1897      switch ($task) {
1898          case 'startExtract':
1899          case 'stepExtract':
1900              $done  = $engine->step();
1901              $error = $engine->getError();
1902  
1903              if ($error != '') {
1904                  $retArray['status']  = false;
1905                  $retArray['done']    = true;
1906                  $retArray['message'] = $error;
1907              } elseif ($done) {
1908                  $retArray['files']    = $engine->filesProcessed;
1909                  $retArray['bytesIn']  = $engine->compressedTotal;
1910                  $retArray['bytesOut'] = $engine->uncompressedTotal;
1911                  $retArray['percent']  = 100;
1912                  $retArray['status']   = true;
1913                  $retArray['done']     = true;
1914  
1915                  $retArray['percent'] = min($retArray['percent'], 100);
1916              } else {
1917                  $retArray['files']    = $engine->filesProcessed;
1918                  $retArray['bytesIn']  = $engine->compressedTotal;
1919                  $retArray['bytesOut'] = $engine->uncompressedTotal;
1920                  $retArray['percent']  = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0;
1921                  $retArray['status']   = true;
1922                  $retArray['done']     = false;
1923                  $retArray['instance'] = ZIPExtraction::getSerialised();
1924              }
1925  
1926              $engine->enforceMinimumExecutionTime();
1927  
1928              break;
1929  
1930          case 'finalizeUpdate':
1931              $root = $configuration['setup.destdir'] ?? '';
1932  
1933              // Remove the administrator/cache/autoload_psr4.php file
1934              $filename = $root . (empty($root) ? '' : '/') . 'administrator/cache/autoload_psr4.php';
1935  
1936              if (file_exists($filename)) {
1937                  clearFileInOPCache($filename);
1938                  clearstatcache(true, $filename);
1939  
1940                  @unlink($filename);
1941              }
1942  
1943              // Remove update.php
1944              clearFileInOPCache($basePath . 'update.php');
1945              @unlink($basePath . 'update.php');
1946  
1947              // Import a custom finalisation file
1948              $filename = dirname(__FILE__) . '/finalisation.php';
1949  
1950              if (file_exists($filename)) {
1951                  clearFileInOPCache($filename);
1952  
1953                  include_once $filename;
1954              }
1955  
1956              // Run a custom finalisation script
1957              if (function_exists('finalizeUpdate')) {
1958                  finalizeUpdate($root, $basePath);
1959              }
1960  
1961              $engine->enforceMinimumExecutionTime();
1962  
1963              break;
1964  
1965          default:
1966              // Invalid task!
1967              $enabled = false;
1968              break;
1969      }
1970  }
1971  
1972  // This could happen even if $enabled was true, e.g. if we were asked for an invalid task.
1973  if (!$enabled) {
1974      // Maybe we weren't authorized or the task was invalid?
1975      $retArray['status']  = false;
1976      $retArray['message'] = 'Invalid login';
1977  }
1978  
1979  // JSON encode the message
1980  echo json_encode($retArray);


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