[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |