1<?php 2/** 3 * Akeeba Restore 4 * 5 * An archive extraction engine for ZIP, JPA and JPS archives. 6 * 7 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 8 * @license GNU GPL v2 or - at your option - any later version 9 * @note This file has been modified by the Joomla! Project and no longer reflects the original work of its author. 10 */ 11 12/** 13 * Akeeba Restore 14 * A JSON-powered JPA, JPS and ZIP archive extraction library 15 * 16 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 17 * @license GNU GPL v2 or - at your option - any later version 18 * @package akeebabackup 19 * @subpackage kickstart 20 */ 21 22define('_AKEEBA_RESTORATION', 1); 23defined('DS') or define('DS', DIRECTORY_SEPARATOR); 24 25// Unarchiver run states 26define('AK_STATE_NOFILE', 0); // File header not read yet 27define('AK_STATE_HEADER', 1); // File header read; ready to process data 28define('AK_STATE_DATA', 2); // Processing file data 29define('AK_STATE_DATAREAD', 3); // Finished processing file data; ready to post-process 30define('AK_STATE_POSTPROC', 4); // Post-processing 31define('AK_STATE_DONE', 5); // Done with post-processing 32 33/* Windows system detection */ 34if (!defined('_AKEEBA_IS_WINDOWS')) 35{ 36 if (function_exists('php_uname')) 37 { 38 define('_AKEEBA_IS_WINDOWS', stristr(php_uname(), 'windows')); 39 } 40 else 41 { 42 define('_AKEEBA_IS_WINDOWS', DIRECTORY_SEPARATOR == '\\'); 43 } 44} 45 46// Get the file's root 47if (!defined('KSROOTDIR')) 48{ 49 define('KSROOTDIR', dirname(__FILE__)); 50} 51if (!defined('KSLANGDIR')) 52{ 53 define('KSLANGDIR', KSROOTDIR); 54} 55 56// Make sure the locale is correct for basename() to work 57if (function_exists('setlocale')) 58{ 59 @setlocale(LC_ALL, 'en_US.UTF8'); 60} 61 62// fnmatch not available on non-POSIX systems 63// Thanks to soywiz@php.net for this usefull alternative function [http://gr2.php.net/fnmatch] 64if (!function_exists('fnmatch')) 65{ 66 function fnmatch($pattern, $string) 67 { 68 return @preg_match( 69 '/^' . strtr(addcslashes($pattern, '/\\.+^$(){}=!<>|'), 70 array('*' => '.*', '?' => '.?')) . '$/i', $string 71 ); 72 } 73} 74 75// Unicode-safe binary data length function 76if (!function_exists('akstringlen')) 77{ 78 if (function_exists('mb_strlen')) 79 { 80 function akstringlen($string) 81 { 82 return mb_strlen($string, '8bit'); 83 } 84 } 85 else 86 { 87 function akstringlen($string) 88 { 89 return strlen($string); 90 } 91 } 92} 93 94if (!function_exists('aksubstr')) 95{ 96 if (function_exists('mb_strlen')) 97 { 98 function aksubstr($string, $start, $length = null) 99 { 100 return mb_substr($string, $start, $length, '8bit'); 101 } 102 } 103 else 104 { 105 function aksubstr($string, $start, $length = null) 106 { 107 return substr($string, $start, $length); 108 } 109 } 110} 111 112/** 113 * Gets a query parameter from GET or POST data 114 * 115 * @param $key 116 * @param $default 117 */ 118function getQueryParam($key, $default = null) 119{ 120 $value = $default; 121 122 if (array_key_exists($key, $_REQUEST)) 123 { 124 $value = $_REQUEST[$key]; 125 } 126 127 if (PHP_VERSION_ID < 50400 && get_magic_quotes_gpc() && !is_null($value)) 128 { 129 $value = stripslashes($value); 130 } 131 132 return $value; 133} 134 135// Debugging function 136function debugMsg($msg) 137{ 138 if (!defined('KSDEBUG')) 139 { 140 return; 141 } 142 143 $fp = fopen('debug.txt', 'at'); 144 145 fwrite($fp, $msg . "\n"); 146 fclose($fp); 147 148 // Echo to stdout if KSDEBUGCLI is defined 149 if (defined('KSDEBUGCLI')) 150 { 151 echo $msg . "\n"; 152 } 153} 154 155/** 156 * Akeeba Restore 157 * A JSON-powered JPA, JPS and ZIP archive extraction library 158 * 159 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 160 * @license GNU GPL v2 or - at your option - any later version 161 * @package akeebabackup 162 * @subpackage kickstart 163 */ 164 165/** 166 * The base class of Akeeba Engine objects. Allows for error and warnings logging 167 * and propagation. Largely based on the Joomla! 1.5 JObject class. 168 */ 169abstract class AKAbstractObject 170{ 171 /** @var array The queue size of the $_errors array. Set to 0 for infinite size. */ 172 protected $_errors_queue_size = 0; 173 /** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */ 174 protected $_warnings_queue_size = 0; 175 /** @var array An array of errors */ 176 private $_errors = array(); 177 /** @var array An array of warnings */ 178 private $_warnings = array(); 179 180 /** 181 * Public constructor, makes sure we are instantiated only by the factory class 182 */ 183 public function __construct() 184 { 185 /* 186 // Assisted Singleton pattern 187 if(function_exists('debug_backtrace')) 188 { 189 $caller=debug_backtrace(); 190 if( 191 ($caller[1]['class'] != 'AKFactory') && 192 ($caller[2]['class'] != 'AKFactory') && 193 ($caller[3]['class'] != 'AKFactory') && 194 ($caller[4]['class'] != 'AKFactory') 195 ) { 196 var_dump(debug_backtrace()); 197 trigger_error("You can't create direct descendants of ".__CLASS__, E_USER_ERROR); 198 } 199 } 200 */ 201 } 202 203 /** 204 * Get the most recent error message 205 * 206 * @param integer $i Optional error index 207 * 208 * @return string Error message 209 */ 210 public function getError($i = null) 211 { 212 return $this->getItemFromArray($this->_errors, $i); 213 } 214 215 /** 216 * Returns the last item of a LIFO string message queue, or a specific item 217 * if so specified. 218 * 219 * @param array $array An array of strings, holding messages 220 * @param int $i Optional message index 221 * 222 * @return mixed The message string, or false if the key doesn't exist 223 */ 224 private function getItemFromArray($array, $i = null) 225 { 226 // Find the item 227 if ($i === null) 228 { 229 // Default, return the last item 230 $item = end($array); 231 } 232 else if (!array_key_exists($i, $array)) 233 { 234 // If $i has been specified but does not exist, return false 235 return false; 236 } 237 else 238 { 239 $item = $array[$i]; 240 } 241 242 return $item; 243 } 244 245 /** 246 * Return all errors, if any 247 * 248 * @return array Array of error messages 249 */ 250 public function getErrors() 251 { 252 return $this->_errors; 253 } 254 255 /** 256 * Resets all error messages 257 */ 258 public function resetErrors() 259 { 260 $this->_errors = array(); 261 } 262 263 /** 264 * Get the most recent warning message 265 * 266 * @param integer $i Optional warning index 267 * 268 * @return string Error message 269 */ 270 public function getWarning($i = null) 271 { 272 return $this->getItemFromArray($this->_warnings, $i); 273 } 274 275 /** 276 * Return all warnings, if any 277 * 278 * @return array Array of error messages 279 */ 280 public function getWarnings() 281 { 282 return $this->_warnings; 283 } 284 285 /** 286 * Resets all warning messages 287 */ 288 public function resetWarnings() 289 { 290 $this->_warnings = array(); 291 } 292 293 /** 294 * Propagates errors and warnings to a foreign object. The foreign object SHOULD 295 * implement the setError() and/or setWarning() methods but DOESN'T HAVE TO be of 296 * AKAbstractObject type. For example, this can even be used to propagate to a 297 * JObject instance in Joomla!. Propagated items will be removed from ourselves. 298 * 299 * @param object $object The object to propagate errors and warnings to. 300 */ 301 public function propagateToObject(&$object) 302 { 303 // Skip non-objects 304 if (!is_object($object)) 305 { 306 return; 307 } 308 309 if (method_exists($object, 'setError')) 310 { 311 if (!empty($this->_errors)) 312 { 313 foreach ($this->_errors as $error) 314 { 315 $object->setError($error); 316 } 317 $this->_errors = array(); 318 } 319 } 320 321 if (method_exists($object, 'setWarning')) 322 { 323 if (!empty($this->_warnings)) 324 { 325 foreach ($this->_warnings as $warning) 326 { 327 $object->setWarning($warning); 328 } 329 $this->_warnings = array(); 330 } 331 } 332 } 333 334 /** 335 * Propagates errors and warnings from a foreign object. Each propagated list is 336 * then cleared on the foreign object, as long as it implements resetErrors() and/or 337 * resetWarnings() methods. 338 * 339 * @param object $object The object to propagate errors and warnings from 340 */ 341 public function propagateFromObject(&$object) 342 { 343 if (method_exists($object, 'getErrors')) 344 { 345 $errors = $object->getErrors(); 346 if (!empty($errors)) 347 { 348 foreach ($errors as $error) 349 { 350 $this->setError($error); 351 } 352 } 353 if (method_exists($object, 'resetErrors')) 354 { 355 $object->resetErrors(); 356 } 357 } 358 359 if (method_exists($object, 'getWarnings')) 360 { 361 $warnings = $object->getWarnings(); 362 if (!empty($warnings)) 363 { 364 foreach ($warnings as $warning) 365 { 366 $this->setWarning($warning); 367 } 368 } 369 if (method_exists($object, 'resetWarnings')) 370 { 371 $object->resetWarnings(); 372 } 373 } 374 } 375 376 /** 377 * Add an error message 378 * 379 * @param string $error Error message 380 */ 381 public function setError($error) 382 { 383 if ($this->_errors_queue_size > 0) 384 { 385 if (count($this->_errors) >= $this->_errors_queue_size) 386 { 387 array_shift($this->_errors); 388 } 389 } 390 391 $this->_errors[] = $error; 392 } 393 394 /** 395 * Add an error message 396 * 397 * @param string $error Error message 398 */ 399 public function setWarning($warning) 400 { 401 if ($this->_warnings_queue_size > 0) 402 { 403 if (count($this->_warnings) >= $this->_warnings_queue_size) 404 { 405 array_shift($this->_warnings); 406 } 407 } 408 409 $this->_warnings[] = $warning; 410 } 411 412 /** 413 * Sets the size of the error queue (acts like a LIFO buffer) 414 * 415 * @param int $newSize The new queue size. Set to 0 for infinite length. 416 */ 417 protected function setErrorsQueueSize($newSize = 0) 418 { 419 $this->_errors_queue_size = (int) $newSize; 420 } 421 422 /** 423 * Sets the size of the warnings queue (acts like a LIFO buffer) 424 * 425 * @param int $newSize The new queue size. Set to 0 for infinite length. 426 */ 427 protected function setWarningsQueueSize($newSize = 0) 428 { 429 $this->_warnings_queue_size = (int) $newSize; 430 } 431 432} 433 434/** 435 * Akeeba Restore 436 * A JSON-powered JPA, JPS and ZIP archive extraction library 437 * 438 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 439 * @license GNU GPL v2 or - at your option - any later version 440 * @package akeebabackup 441 * @subpackage kickstart 442 */ 443 444/** 445 * The superclass of all Akeeba Kickstart parts. The "parts" are intelligent stateful 446 * classes which perform a single procedure and have preparation, running and 447 * finalization phases. The transition between phases is handled automatically by 448 * this superclass' tick() final public method, which should be the ONLY public API 449 * exposed to the rest of the Akeeba Engine. 450 */ 451abstract class AKAbstractPart extends AKAbstractObject 452{ 453 /** 454 * Indicates whether this part has finished its initialisation cycle 455 * 456 * @var boolean 457 */ 458 protected $isPrepared = false; 459 460 /** 461 * Indicates whether this part has more work to do (it's in running state) 462 * 463 * @var boolean 464 */ 465 protected $isRunning = false; 466 467 /** 468 * Indicates whether this part has finished its finalization cycle 469 * 470 * @var boolean 471 */ 472 protected $isFinished = false; 473 474 /** 475 * Indicates whether this part has finished its run cycle 476 * 477 * @var boolean 478 */ 479 protected $hasRan = false; 480 481 /** 482 * The name of the engine part (a.k.a. Domain), used in return table 483 * generation. 484 * 485 * @var string 486 */ 487 protected $active_domain = ""; 488 489 /** 490 * The step this engine part is in. Used verbatim in return table and 491 * should be set by the code in the _run() method. 492 * 493 * @var string 494 */ 495 protected $active_step = ""; 496 497 /** 498 * A more detailed description of the step this engine part is in. Used 499 * verbatim in return table and should be set by the code in the _run() 500 * method. 501 * 502 * @var string 503 */ 504 protected $active_substep = ""; 505 506 /** 507 * Any configuration variables, in the form of an array. 508 * 509 * @var array 510 */ 511 protected $_parametersArray = array(); 512 513 /** @var string The database root key */ 514 protected $databaseRoot = array(); 515 /** @var array An array of observers */ 516 protected $observers = array(); 517 /** @var int Last reported warnings's position in array */ 518 private $warnings_pointer = -1; 519 520 /** 521 * The public interface to an engine part. This method takes care for 522 * calling the correct method in order to perform the initialisation - 523 * run - finalisation cycle of operation and return a proper response array. 524 * 525 * @return array A Response Array 526 */ 527 final public function tick() 528 { 529 // Call the right action method, depending on engine part state 530 switch ($this->getState()) 531 { 532 case "init": 533 $this->_prepare(); 534 break; 535 case "prepared": 536 $this->_run(); 537 break; 538 case "running": 539 $this->_run(); 540 break; 541 case "postrun": 542 $this->_finalize(); 543 break; 544 } 545 546 // Send a Return Table back to the caller 547 $out = $this->_makeReturnTable(); 548 549 return $out; 550 } 551 552 /** 553 * Returns the state of this engine part. 554 * 555 * @return string The state of this engine part. It can be one of 556 * error, init, prepared, running, postrun, finished. 557 */ 558 final public function getState() 559 { 560 if ($this->getError()) 561 { 562 return "error"; 563 } 564 565 if (!($this->isPrepared)) 566 { 567 return "init"; 568 } 569 570 if (!($this->isFinished) && !($this->isRunning) && !($this->hasRun) && ($this->isPrepared)) 571 { 572 return "prepared"; 573 } 574 575 if (!($this->isFinished) && $this->isRunning && !($this->hasRun)) 576 { 577 return "running"; 578 } 579 580 if (!($this->isFinished) && !($this->isRunning) && $this->hasRun) 581 { 582 return "postrun"; 583 } 584 585 if ($this->isFinished) 586 { 587 return "finished"; 588 } 589 } 590 591 /** 592 * Runs the preparation for this part. Should set _isPrepared 593 * to true 594 */ 595 abstract protected function _prepare(); 596 597 /** 598 * Runs the main functionality loop for this part. Upon calling, 599 * should set the _isRunning to true. When it finished, should set 600 * the _hasRan to true. If an error is encountered, setError should 601 * be used. 602 */ 603 abstract protected function _run(); 604 605 /** 606 * Runs the finalisation process for this part. Should set 607 * _isFinished to true. 608 */ 609 abstract protected function _finalize(); 610 611 /** 612 * Constructs a Response Array based on the engine part's state. 613 * 614 * @return array The Response Array for the current state 615 */ 616 final protected function _makeReturnTable() 617 { 618 // Get a list of warnings 619 $warnings = $this->getWarnings(); 620 // Report only new warnings if there is no warnings queue size 621 if ($this->_warnings_queue_size == 0) 622 { 623 if (($this->warnings_pointer > 0) && ($this->warnings_pointer < (count($warnings)))) 624 { 625 $warnings = array_slice($warnings, $this->warnings_pointer + 1); 626 $this->warnings_pointer += count($warnings); 627 } 628 else 629 { 630 $this->warnings_pointer = count($warnings); 631 } 632 } 633 634 $out = array( 635 'HasRun' => (!($this->isFinished)), 636 'Domain' => $this->active_domain, 637 'Step' => $this->active_step, 638 'Substep' => $this->active_substep, 639 'Error' => $this->getError(), 640 'Warnings' => $warnings 641 ); 642 643 return $out; 644 } 645 646 /** 647 * Returns a copy of the class's status array 648 * 649 * @return array 650 */ 651 public function getStatusArray() 652 { 653 return $this->_makeReturnTable(); 654 } 655 656 /** 657 * Sends any kind of setup information to the engine part. Using this, 658 * we avoid passing parameters to the constructor of the class. These 659 * parameters should be passed as an indexed array and should be taken 660 * into account during the preparation process only. This function will 661 * set the error flag if it's called after the engine part is prepared. 662 * 663 * @param array $parametersArray The parameters to be passed to the 664 * engine part. 665 */ 666 final public function setup($parametersArray) 667 { 668 if ($this->isPrepared) 669 { 670 $this->setState('error', "Can't modify configuration after the preparation of " . $this->active_domain); 671 } 672 else 673 { 674 $this->_parametersArray = $parametersArray; 675 if (array_key_exists('root', $parametersArray)) 676 { 677 $this->databaseRoot = $parametersArray['root']; 678 } 679 } 680 } 681 682 /** 683 * Sets the engine part's internal state, in an easy to use manner 684 * 685 * @param string $state One of init, prepared, running, postrun, finished, error 686 * @param string $errorMessage The reported error message, should the state be set to error 687 */ 688 protected function setState($state = 'init', $errorMessage = 'Invalid setState argument') 689 { 690 switch ($state) 691 { 692 case 'init': 693 $this->isPrepared = false; 694 $this->isRunning = false; 695 $this->isFinished = false; 696 $this->hasRun = false; 697 break; 698 699 case 'prepared': 700 $this->isPrepared = true; 701 $this->isRunning = false; 702 $this->isFinished = false; 703 $this->hasRun = false; 704 break; 705 706 case 'running': 707 $this->isPrepared = true; 708 $this->isRunning = true; 709 $this->isFinished = false; 710 $this->hasRun = false; 711 break; 712 713 case 'postrun': 714 $this->isPrepared = true; 715 $this->isRunning = false; 716 $this->isFinished = false; 717 $this->hasRun = true; 718 break; 719 720 case 'finished': 721 $this->isPrepared = true; 722 $this->isRunning = false; 723 $this->isFinished = true; 724 $this->hasRun = false; 725 break; 726 727 case 'error': 728 default: 729 $this->setError($errorMessage); 730 break; 731 } 732 } 733 734 final public function getDomain() 735 { 736 return $this->active_domain; 737 } 738 739 final public function getStep() 740 { 741 return $this->active_step; 742 } 743 744 final public function getSubstep() 745 { 746 return $this->active_substep; 747 } 748 749 /** 750 * Attaches an observer object 751 * 752 * @param AKAbstractPartObserver $obs 753 */ 754 function attach(AKAbstractPartObserver $obs) 755 { 756 $this->observers["$obs"] = $obs; 757 } 758 759 /** 760 * Detaches an observer object 761 * 762 * @param AKAbstractPartObserver $obs 763 */ 764 function detach(AKAbstractPartObserver $obs) 765 { 766 delete($this->observers["$obs"]); 767 } 768 769 /** 770 * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately, 771 * in fear of timing out. 772 */ 773 protected function setBreakFlag() 774 { 775 AKFactory::set('volatile.breakflag', true); 776 } 777 778 final protected function setDomain($new_domain) 779 { 780 $this->active_domain = $new_domain; 781 } 782 783 final protected function setStep($new_step) 784 { 785 $this->active_step = $new_step; 786 } 787 788 final protected function setSubstep($new_substep) 789 { 790 $this->active_substep = $new_substep; 791 } 792 793 /** 794 * Notifies observers each time something interesting happened to the part 795 * 796 * @param mixed $message The event object 797 */ 798 protected function notify($message) 799 { 800 foreach ($this->observers as $obs) 801 { 802 $obs->update($this, $message); 803 } 804 } 805} 806 807/** 808 * Akeeba Restore 809 * A JSON-powered JPA, JPS and ZIP archive extraction library 810 * 811 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 812 * @license GNU GPL v2 or - at your option - any later version 813 * @package akeebabackup 814 * @subpackage kickstart 815 */ 816 817/** 818 * The base class of unarchiver classes 819 */ 820abstract class AKAbstractUnarchiver extends AKAbstractPart 821{ 822 /** @var array List of the names of all archive parts */ 823 public $archiveList = array(); 824 /** @var int The total size of all archive parts */ 825 public $totalSize = array(); 826 /** @var array Which files to rename */ 827 public $renameFiles = array(); 828 /** @var array Which directories to rename */ 829 public $renameDirs = array(); 830 /** @var array Which files to skip */ 831 public $skipFiles = array(); 832 /** @var string Archive filename */ 833 protected $filename = null; 834 /** @var integer Current archive part number */ 835 protected $currentPartNumber = -1; 836 /** @var integer The offset inside the current part */ 837 protected $currentPartOffset = 0; 838 /** @var bool Should I restore permissions? */ 839 protected $flagRestorePermissions = false; 840 /** @var AKAbstractPostproc Post processing class */ 841 protected $postProcEngine = null; 842 /** @var string Absolute path to prepend to extracted files */ 843 protected $addPath = ''; 844 /** @var string Absolute path to remove from extracted files */ 845 protected $removePath = ''; 846 /** @var integer Chunk size for processing */ 847 protected $chunkSize = 524288; 848 849 /** @var resource File pointer to the current archive part file */ 850 protected $fp = null; 851 852 /** @var int Run state when processing the current archive file */ 853 protected $runState = null; 854 855 /** @var stdClass File header data, as read by the readFileHeader() method */ 856 protected $fileHeader = null; 857 858 /** @var int How much of the uncompressed data we've read so far */ 859 protected $dataReadLength = 0; 860 861 /** @var array Unwriteable files in these directories are always ignored and do not cause errors when not extracted */ 862 protected $ignoreDirectories = array(); 863 864 /** 865 * Public constructor 866 */ 867 public function __construct() 868 { 869 parent::__construct(); 870 } 871 872 /** 873 * Wakeup function, called whenever the class is unserialized 874 */ 875 public function __wakeup() 876 { 877 if ($this->currentPartNumber >= 0 && !empty($this->archiveList[$this->currentPartNumber])) 878 { 879 $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); 880 if ((is_resource($this->fp)) && ($this->currentPartOffset > 0)) 881 { 882 @fseek($this->fp, $this->currentPartOffset); 883 } 884 } 885 } 886 887 /** 888 * Sleep function, called whenever the class is serialized 889 */ 890 public function shutdown() 891 { 892 if (is_resource($this->fp)) 893 { 894 $this->currentPartOffset = @ftell($this->fp); 895 @fclose($this->fp); 896 } 897 } 898 899 /** 900 * Is this file or directory contained in a directory we've decided to ignore 901 * write errors for? This is useful to let the extraction work despite write 902 * errors in the log, logs and tmp directories which MIGHT be used by the system 903 * on some low quality hosts and Plesk-powered hosts. 904 * 905 * @param string $shortFilename The relative path of the file/directory in the package 906 * 907 * @return boolean True if it belongs in an ignored directory 908 */ 909 public function isIgnoredDirectory($shortFilename) 910 { 911 // return false; 912 913 if (substr($shortFilename, -1) == '/') 914 { 915 $check = rtrim($shortFilename, '/'); 916 } 917 else 918 { 919 $check = dirname($shortFilename); 920 } 921 922 return in_array($check, $this->ignoreDirectories); 923 } 924 925 /** 926 * Implements the abstract _prepare() method 927 */ 928 final protected function _prepare() 929 { 930 parent::__construct(); 931 932 if (count($this->_parametersArray) > 0) 933 { 934 foreach ($this->_parametersArray as $key => $value) 935 { 936 switch ($key) 937 { 938 // Archive's absolute filename 939 case 'filename': 940 $this->filename = $value; 941 942 // Sanity check 943 if (!empty($value)) 944 { 945 $value = strtolower($value); 946 947 if (strlen($value) > 6) 948 { 949 if ( 950 (substr($value, 0, 7) == 'http://') 951 || (substr($value, 0, 8) == 'https://') 952 || (substr($value, 0, 6) == 'ftp://') 953 || (substr($value, 0, 7) == 'ssh2://') 954 || (substr($value, 0, 6) == 'ssl://') 955 ) 956 { 957 $this->setState('error', 'Invalid archive location'); 958 } 959 } 960 } 961 962 963 break; 964 965 // Should I restore permissions? 966 case 'restore_permissions': 967 $this->flagRestorePermissions = $value; 968 break; 969 970 // Should I use FTP? 971 case 'post_proc': 972 $this->postProcEngine = AKFactory::getpostProc($value); 973 break; 974 975 // Path to add in the beginning 976 case 'add_path': 977 $this->addPath = $value; 978 $this->addPath = str_replace('\\', '/', $this->addPath); 979 $this->addPath = rtrim($this->addPath, '/'); 980 if (!empty($this->addPath)) 981 { 982 $this->addPath .= '/'; 983 } 984 break; 985 986 // Path to remove from the beginning 987 case 'remove_path': 988 $this->removePath = $value; 989 $this->removePath = str_replace('\\', '/', $this->removePath); 990 $this->removePath = rtrim($this->removePath, '/'); 991 if (!empty($this->removePath)) 992 { 993 $this->removePath .= '/'; 994 } 995 break; 996 997 // Which files to rename (hash array) 998 case 'rename_files': 999 $this->renameFiles = $value; 1000 break; 1001 1002 // Which files to rename (hash array) 1003 case 'rename_dirs': 1004 $this->renameDirs = $value; 1005 break; 1006 1007 // Which files to skip (indexed array) 1008 case 'skip_files': 1009 $this->skipFiles = $value; 1010 break; 1011 1012 // Which directories to ignore when we can't write files in them (indexed array) 1013 case 'ignoredirectories': 1014 $this->ignoreDirectories = $value; 1015 break; 1016 } 1017 } 1018 } 1019 1020 $this->scanArchives(); 1021 1022 $this->readArchiveHeader(); 1023 $errMessage = $this->getError(); 1024 if (!empty($errMessage)) 1025 { 1026 $this->setState('error', $errMessage); 1027 } 1028 else 1029 { 1030 $this->runState = AK_STATE_NOFILE; 1031 $this->setState('prepared'); 1032 } 1033 } 1034 1035 /** 1036 * Scans for archive parts 1037 */ 1038 private function scanArchives() 1039 { 1040 if (defined('KSDEBUG')) 1041 { 1042 @unlink('debug.txt'); 1043 } 1044 debugMsg('Preparing to scan archives'); 1045 1046 $privateArchiveList = array(); 1047 1048 // Get the components of the archive filename 1049 $dirname = dirname($this->filename); 1050 $base_extension = $this->getBaseExtension(); 1051 $basename = basename($this->filename, $base_extension); 1052 $this->totalSize = 0; 1053 1054 // Scan for multiple parts until we don't find any more of them 1055 $count = 0; 1056 $found = true; 1057 $this->archiveList = array(); 1058 while ($found) 1059 { 1060 ++$count; 1061 $extension = substr($base_extension, 0, 2) . sprintf('%02d', $count); 1062 $filename = $dirname . DIRECTORY_SEPARATOR . $basename . $extension; 1063 $found = file_exists($filename); 1064 if ($found) 1065 { 1066 debugMsg('- Found archive ' . $filename); 1067 // Add yet another part, with a numeric-appended filename 1068 $this->archiveList[] = $filename; 1069 1070 $filesize = @filesize($filename); 1071 $this->totalSize += $filesize; 1072 1073 $privateArchiveList[] = array($filename, $filesize); 1074 } 1075 else 1076 { 1077 debugMsg('- Found archive ' . $this->filename); 1078 // Add the last part, with the regular extension 1079 $this->archiveList[] = $this->filename; 1080 1081 $filename = $this->filename; 1082 $filesize = @filesize($filename); 1083 $this->totalSize += $filesize; 1084 1085 $privateArchiveList[] = array($filename, $filesize); 1086 } 1087 } 1088 debugMsg('Total archive parts: ' . $count); 1089 1090 $this->currentPartNumber = -1; 1091 $this->currentPartOffset = 0; 1092 $this->runState = AK_STATE_NOFILE; 1093 1094 // Send start of file notification 1095 $message = new stdClass; 1096 $message->type = 'totalsize'; 1097 $message->content = new stdClass; 1098 $message->content->totalsize = $this->totalSize; 1099 $message->content->filelist = $privateArchiveList; 1100 $this->notify($message); 1101 } 1102 1103 /** 1104 * Returns the base extension of the file, e.g. '.jpa' 1105 * 1106 * @return string 1107 */ 1108 private function getBaseExtension() 1109 { 1110 static $baseextension; 1111 1112 if (empty($baseextension)) 1113 { 1114 $basename = basename($this->filename); 1115 $lastdot = strrpos($basename, '.'); 1116 $baseextension = substr($basename, $lastdot); 1117 } 1118 1119 return $baseextension; 1120 } 1121 1122 /** 1123 * Concrete classes are supposed to use this method in order to read the archive's header and 1124 * prepare themselves to the point of being ready to extract the first file. 1125 */ 1126 protected abstract function readArchiveHeader(); 1127 1128 protected function _run() 1129 { 1130 if ($this->getState() == 'postrun') 1131 { 1132 return; 1133 } 1134 1135 $this->setState('running'); 1136 1137 $timer = AKFactory::getTimer(); 1138 1139 $status = true; 1140 while ($status && ($timer->getTimeLeft() > 0)) 1141 { 1142 switch ($this->runState) 1143 { 1144 case AK_STATE_NOFILE: 1145 debugMsg(__CLASS__ . '::_run() - Reading file header'); 1146 $status = $this->readFileHeader(); 1147 if ($status) 1148 { 1149 // Send start of file notification 1150 $message = new stdClass; 1151 $message->type = 'startfile'; 1152 $message->content = new stdClass; 1153 $message->content->realfile = $this->fileHeader->file; 1154 $message->content->file = $this->fileHeader->file; 1155 $message->content->uncompressed = $this->fileHeader->uncompressed; 1156 1157 if (array_key_exists('realfile', get_object_vars($this->fileHeader))) 1158 { 1159 $message->content->realfile = $this->fileHeader->realFile; 1160 } 1161 1162 if (array_key_exists('compressed', get_object_vars($this->fileHeader))) 1163 { 1164 $message->content->compressed = $this->fileHeader->compressed; 1165 } 1166 else 1167 { 1168 $message->content->compressed = 0; 1169 } 1170 1171 debugMsg(__CLASS__ . '::_run() - Preparing to extract ' . $message->content->realfile); 1172 1173 $this->notify($message); 1174 } 1175 else 1176 { 1177 debugMsg(__CLASS__ . '::_run() - Could not read file header'); 1178 } 1179 break; 1180 1181 case AK_STATE_HEADER: 1182 case AK_STATE_DATA: 1183 debugMsg(__CLASS__ . '::_run() - Processing file data'); 1184 $status = $this->processFileData(); 1185 break; 1186 1187 case AK_STATE_DATAREAD: 1188 case AK_STATE_POSTPROC: 1189 debugMsg(__CLASS__ . '::_run() - Calling post-processing class'); 1190 $this->postProcEngine->timestamp = $this->fileHeader->timestamp; 1191 $status = $this->postProcEngine->process(); 1192 $this->propagateFromObject($this->postProcEngine); 1193 $this->runState = AK_STATE_DONE; 1194 break; 1195 1196 case AK_STATE_DONE: 1197 default: 1198 if ($status) 1199 { 1200 debugMsg(__CLASS__ . '::_run() - Finished extracting file'); 1201 // Send end of file notification 1202 $message = new stdClass; 1203 $message->type = 'endfile'; 1204 $message->content = new stdClass; 1205 if (array_key_exists('realfile', get_object_vars($this->fileHeader))) 1206 { 1207 $message->content->realfile = $this->fileHeader->realFile; 1208 } 1209 else 1210 { 1211 $message->content->realfile = $this->fileHeader->file; 1212 } 1213 $message->content->file = $this->fileHeader->file; 1214 if (array_key_exists('compressed', get_object_vars($this->fileHeader))) 1215 { 1216 $message->content->compressed = $this->fileHeader->compressed; 1217 } 1218 else 1219 { 1220 $message->content->compressed = 0; 1221 } 1222 $message->content->uncompressed = $this->fileHeader->uncompressed; 1223 $this->notify($message); 1224 } 1225 $this->runState = AK_STATE_NOFILE; 1226 break; 1227 } 1228 } 1229 1230 $error = $this->getError(); 1231 if (!$status && ($this->runState == AK_STATE_NOFILE) && empty($error)) 1232 { 1233 debugMsg(__CLASS__ . '::_run() - Just finished'); 1234 // We just finished 1235 $this->setState('postrun'); 1236 } 1237 elseif (!empty($error)) 1238 { 1239 debugMsg(__CLASS__ . '::_run() - Halted with an error:'); 1240 debugMsg($error); 1241 $this->setState('error', $error); 1242 } 1243 } 1244 1245 /** 1246 * Concrete classes must use this method to read the file header 1247 * 1248 * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive 1249 */ 1250 protected abstract function readFileHeader(); 1251 1252 /** 1253 * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when 1254 * it's finished processing the file data. 1255 * 1256 * @return bool True if processing the file data was successful, false if an error occurred 1257 */ 1258 protected abstract function processFileData(); 1259 1260 protected function _finalize() 1261 { 1262 // Nothing to do 1263 $this->setState('finished'); 1264 } 1265 1266 /** 1267 * Opens the next part file for reading 1268 */ 1269 protected function nextFile() 1270 { 1271 debugMsg('Current part is ' . $this->currentPartNumber . '; opening the next part'); 1272 ++$this->currentPartNumber; 1273 1274 if ($this->currentPartNumber > (count($this->archiveList) - 1)) 1275 { 1276 $this->setState('postrun'); 1277 1278 return false; 1279 } 1280 else 1281 { 1282 if (is_resource($this->fp)) 1283 { 1284 @fclose($this->fp); 1285 } 1286 debugMsg('Opening file ' . $this->archiveList[$this->currentPartNumber]); 1287 $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); 1288 if ($this->fp === false) 1289 { 1290 debugMsg('Could not open file - crash imminent'); 1291 $this->setError(AKText::sprintf('ERR_COULD_NOT_OPEN_ARCHIVE_PART', $this->archiveList[$this->currentPartNumber])); 1292 } 1293 fseek($this->fp, 0); 1294 $this->currentPartOffset = 0; 1295 1296 return true; 1297 } 1298 } 1299 1300 /** 1301 * Returns true if we have reached the end of file 1302 * 1303 * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of 1304 * the archive set 1305 * 1306 * @return bool True if we have reached End Of File 1307 */ 1308 protected function isEOF($local = false) 1309 { 1310 $eof = @feof($this->fp); 1311 1312 if (!$eof) 1313 { 1314 // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why 1315 // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report 1316 // true. Incredible! :( 1317 $position = @ftell($this->fp); 1318 $filesize = @filesize($this->archiveList[$this->currentPartNumber]); 1319 if ($filesize <= 0) 1320 { 1321 // 2Gb or more files on a 32 bit version of PHP tend to get screwed up. Meh. 1322 $eof = false; 1323 } 1324 elseif ($position >= $filesize) 1325 { 1326 $eof = true; 1327 } 1328 } 1329 1330 if ($local) 1331 { 1332 return $eof; 1333 } 1334 else 1335 { 1336 return $eof && ($this->currentPartNumber >= (count($this->archiveList) - 1)); 1337 } 1338 } 1339 1340 /** 1341 * Tries to make a directory user-writable so that we can write a file to it 1342 * 1343 * @param $path string A path to a file 1344 */ 1345 protected function setCorrectPermissions($path) 1346 { 1347 static $rootDir = null; 1348 1349 if (is_null($rootDir)) 1350 { 1351 $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir', ''), '/\\'); 1352 } 1353 1354 $directory = rtrim(dirname($path), '/\\'); 1355 if ($directory != $rootDir) 1356 { 1357 // Is this an unwritable directory? 1358 if (!is_writeable($directory)) 1359 { 1360 $this->postProcEngine->chmod($directory, 0755); 1361 } 1362 } 1363 $this->postProcEngine->chmod($path, 0644); 1364 } 1365 1366 /** 1367 * Reads data from the archive and notifies the observer with the 'reading' message 1368 * 1369 * @param $fp 1370 * @param $length 1371 */ 1372 protected function fread($fp, $length = null) 1373 { 1374 if (is_numeric($length)) 1375 { 1376 if ($length > 0) 1377 { 1378 $data = fread($fp, $length); 1379 } 1380 else 1381 { 1382 $data = fread($fp, PHP_INT_MAX); 1383 } 1384 } 1385 else 1386 { 1387 $data = fread($fp, PHP_INT_MAX); 1388 } 1389 if ($data === false) 1390 { 1391 $data = ''; 1392 } 1393 1394 // Send start of file notification 1395 $message = new stdClass; 1396 $message->type = 'reading'; 1397 $message->content = new stdClass; 1398 $message->content->length = strlen($data); 1399 $this->notify($message); 1400 1401 return $data; 1402 } 1403 1404 /** 1405 * Removes the configured $removePath from the path $path 1406 * 1407 * @param string $path The path to reduce 1408 * 1409 * @return string The reduced path 1410 */ 1411 protected function removePath($path) 1412 { 1413 if (empty($this->removePath)) 1414 { 1415 return $path; 1416 } 1417 1418 if (strpos($path, $this->removePath) === 0) 1419 { 1420 $path = substr($path, strlen($this->removePath)); 1421 $path = ltrim($path, '/\\'); 1422 } 1423 1424 return $path; 1425 } 1426} 1427 1428/** 1429 * Akeeba Restore 1430 * A JSON-powered JPA, JPS and ZIP archive extraction library 1431 * 1432 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 1433 * @license GNU GPL v2 or - at your option - any later version 1434 * @package akeebabackup 1435 * @subpackage kickstart 1436 */ 1437 1438/** 1439 * File post processor engines base class 1440 */ 1441abstract class AKAbstractPostproc extends AKAbstractObject 1442{ 1443 /** @var int The UNIX timestamp of the file's desired modification date */ 1444 public $timestamp = 0; 1445 /** @var string The current (real) file path we'll have to process */ 1446 protected $filename = null; 1447 /** @var int The requested permissions */ 1448 protected $perms = 0755; 1449 /** @var string The temporary file path we gave to the unarchiver engine */ 1450 protected $tempFilename = null; 1451 1452 /** 1453 * Processes the current file, e.g. moves it from temp to final location by FTP 1454 */ 1455 abstract public function process(); 1456 1457 /** 1458 * The unarchiver tells us the path to the filename it wants to extract and we give it 1459 * a different path instead. 1460 * 1461 * @param string $filename The path to the real file 1462 * @param int $perms The permissions we need the file to have 1463 * 1464 * @return string The path to the temporary file 1465 */ 1466 abstract public function processFilename($filename, $perms = 0755); 1467 1468 /** 1469 * Recursively creates a directory if it doesn't exist 1470 * 1471 * @param string $dirName The directory to create 1472 * @param int $perms The permissions to give to that directory 1473 */ 1474 abstract public function createDirRecursive($dirName, $perms); 1475 1476 abstract public function chmod($file, $perms); 1477 1478 abstract public function unlink($file); 1479 1480 abstract public function rmdir($directory); 1481 1482 abstract public function rename($from, $to); 1483} 1484 1485 1486/** 1487 * Akeeba Restore 1488 * A JSON-powered JPA, JPS and ZIP archive extraction library 1489 * 1490 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 1491 * @license GNU GPL v2 or - at your option - any later version 1492 * @package akeebabackup 1493 * @subpackage kickstart 1494 */ 1495 1496/** 1497 * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify) 1498 * 1499 * @author Nicholas 1500 * 1501 */ 1502abstract class AKAbstractPartObserver 1503{ 1504 abstract public function update($object, $message); 1505} 1506 1507 1508/** 1509 * Akeeba Restore 1510 * A JSON-powered JPA, JPS and ZIP archive extraction library 1511 * 1512 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 1513 * @license GNU GPL v2 or - at your option - any later version 1514 * @package akeebabackup 1515 * @subpackage kickstart 1516 */ 1517 1518/** 1519 * Direct file writer 1520 */ 1521class AKPostprocDirect extends AKAbstractPostproc 1522{ 1523 public function process() 1524 { 1525 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 1526 if ($restorePerms) 1527 { 1528 @chmod($this->filename, $this->perms); 1529 } 1530 else 1531 { 1532 if (@is_file($this->filename)) 1533 { 1534 @chmod($this->filename, 0644); 1535 } 1536 else 1537 { 1538 @chmod($this->filename, 0755); 1539 } 1540 } 1541 if ($this->timestamp > 0) 1542 { 1543 @touch($this->filename, $this->timestamp); 1544 } 1545 1546 return true; 1547 } 1548 1549 public function processFilename($filename, $perms = 0755) 1550 { 1551 $this->perms = $perms; 1552 $this->filename = $filename; 1553 1554 return $filename; 1555 } 1556 1557 public function createDirRecursive($dirName, $perms) 1558 { 1559 if (AKFactory::get('kickstart.setup.dryrun', '0')) 1560 { 1561 return true; 1562 } 1563 if (@mkdir($dirName, 0755, true)) 1564 { 1565 @chmod($dirName, 0755); 1566 1567 return true; 1568 } 1569 1570 $root = AKFactory::get('kickstart.setup.destdir'); 1571 $root = rtrim(str_replace('\\', '/', $root), '/'); 1572 $dir = rtrim(str_replace('\\', '/', $dirName), '/'); 1573 if (strpos($dir, $root) === 0) 1574 { 1575 $dir = ltrim(substr($dir, strlen($root)), '/'); 1576 $root .= '/'; 1577 } 1578 else 1579 { 1580 $root = ''; 1581 } 1582 1583 if (empty($dir)) 1584 { 1585 return true; 1586 } 1587 1588 $dirArray = explode('/', $dir); 1589 $path = ''; 1590 foreach ($dirArray as $dir) 1591 { 1592 $path .= $dir . '/'; 1593 $ret = is_dir($root . $path) ? true : @mkdir($root . $path); 1594 if (!$ret) 1595 { 1596 // Is this a file instead of a directory? 1597 if (is_file($root . $path)) 1598 { 1599 @unlink($root . $path); 1600 $ret = @mkdir($root . $path); 1601 } 1602 if (!$ret) 1603 { 1604 $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $path)); 1605 1606 return false; 1607 } 1608 } 1609 // Try to set new directory permissions to 0755 1610 @chmod($root . $path, $perms); 1611 } 1612 1613 return true; 1614 } 1615 1616 public function chmod($file, $perms) 1617 { 1618 if (AKFactory::get('kickstart.setup.dryrun', '0')) 1619 { 1620 return true; 1621 } 1622 1623 return @chmod($file, $perms); 1624 } 1625 1626 public function unlink($file) 1627 { 1628 return @unlink($file); 1629 } 1630 1631 public function rmdir($directory) 1632 { 1633 return @rmdir($directory); 1634 } 1635 1636 public function rename($from, $to) 1637 { 1638 return @rename($from, $to); 1639 } 1640 1641} 1642 1643/** 1644 * Akeeba Restore 1645 * A JSON-powered JPA, JPS and ZIP archive extraction library 1646 * 1647 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 1648 * @license GNU GPL v2 or - at your option - any later version 1649 * @package akeebabackup 1650 * @subpackage kickstart 1651 */ 1652 1653/** 1654 * FTP file writer 1655 */ 1656class AKPostprocFTP extends AKAbstractPostproc 1657{ 1658 /** @var bool Should I use FTP over implicit SSL? */ 1659 public $useSSL = false; 1660 /** @var bool use Passive mode? */ 1661 public $passive = true; 1662 /** @var string FTP host name */ 1663 public $host = ''; 1664 /** @var int FTP port */ 1665 public $port = 21; 1666 /** @var string FTP user name */ 1667 public $user = ''; 1668 /** @var string FTP password */ 1669 public $pass = ''; 1670 /** @var string FTP initial directory */ 1671 public $dir = ''; 1672 /** @var resource The FTP handle */ 1673 private $handle = null; 1674 /** @var string The temporary directory where the data will be stored */ 1675 private $tempDir = ''; 1676 1677 public function __construct() 1678 { 1679 parent::__construct(); 1680 1681 $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); 1682 $this->passive = AKFactory::get('kickstart.ftp.passive', true); 1683 $this->host = AKFactory::get('kickstart.ftp.host', ''); 1684 $this->port = AKFactory::get('kickstart.ftp.port', 21); 1685 if (trim($this->port) == '') 1686 { 1687 $this->port = 21; 1688 } 1689 $this->user = AKFactory::get('kickstart.ftp.user', ''); 1690 $this->pass = AKFactory::get('kickstart.ftp.pass', ''); 1691 $this->dir = AKFactory::get('kickstart.ftp.dir', ''); 1692 $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); 1693 1694 $connected = $this->connect(); 1695 1696 if ($connected) 1697 { 1698 if (!empty($this->tempDir)) 1699 { 1700 $tempDir = rtrim($this->tempDir, '/\\') . '/'; 1701 $writable = $this->isDirWritable($tempDir); 1702 } 1703 else 1704 { 1705 $tempDir = ''; 1706 $writable = false; 1707 } 1708 1709 if (!$writable) 1710 { 1711 // Default temporary directory is the current root 1712 $tempDir = KSROOTDIR; 1713 if (empty($tempDir)) 1714 { 1715 // Oh, we have no directory reported! 1716 $tempDir = '.'; 1717 } 1718 $absoluteDirToHere = $tempDir; 1719 $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); 1720 if (!empty($tempDir)) 1721 { 1722 $tempDir .= '/'; 1723 } 1724 $this->tempDir = $tempDir; 1725 // Is this directory writable? 1726 $writable = $this->isDirWritable($tempDir); 1727 } 1728 1729 if (!$writable) 1730 { 1731 // Nope. Let's try creating a temporary directory in the site's root. 1732 $tempDir = $absoluteDirToHere . '/kicktemp'; 1733 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 1734 $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); 1735 // Try making it writable... 1736 $this->fixPermissions($tempDir); 1737 $writable = $this->isDirWritable($tempDir); 1738 } 1739 1740 // Was the new directory writable? 1741 if (!$writable) 1742 { 1743 // Let's see if the user has specified one 1744 $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); 1745 if (!empty($userdir)) 1746 { 1747 // Is it an absolute or a relative directory? 1748 $absolute = false; 1749 $absolute = $absolute || (substr($userdir, 0, 1) == '/'); 1750 $absolute = $absolute || (substr($userdir, 1, 1) == ':'); 1751 $absolute = $absolute || (substr($userdir, 2, 1) == ':'); 1752 if (!$absolute) 1753 { 1754 // Make absolute 1755 $tempDir = $absoluteDirToHere . $userdir; 1756 } 1757 else 1758 { 1759 // it's already absolute 1760 $tempDir = $userdir; 1761 } 1762 // Does the directory exist? 1763 if (is_dir($tempDir)) 1764 { 1765 // Yeah. Is it writable? 1766 $writable = $this->isDirWritable($tempDir); 1767 } 1768 } 1769 } 1770 $this->tempDir = $tempDir; 1771 1772 if (!$writable) 1773 { 1774 // No writable directory found!!! 1775 $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); 1776 } 1777 else 1778 { 1779 AKFactory::set('kickstart.ftp.tempdir', $tempDir); 1780 $this->tempDir = $tempDir; 1781 } 1782 } 1783 } 1784 1785 public function connect() 1786 { 1787 // Connect to server, using SSL if so required 1788 if ($this->useSSL) 1789 { 1790 $this->handle = @ftp_ssl_connect($this->host, $this->port); 1791 } 1792 else 1793 { 1794 $this->handle = @ftp_connect($this->host, $this->port); 1795 } 1796 if ($this->handle === false) 1797 { 1798 $this->setError(AKText::_('WRONG_FTP_HOST')); 1799 1800 return false; 1801 } 1802 1803 // Login 1804 if (!@ftp_login($this->handle, $this->user, $this->pass)) 1805 { 1806 $this->setError(AKText::_('WRONG_FTP_USER')); 1807 @ftp_close($this->handle); 1808 1809 return false; 1810 } 1811 1812 // Change to initial directory 1813 if (!@ftp_chdir($this->handle, $this->dir)) 1814 { 1815 $this->setError(AKText::_('WRONG_FTP_PATH1')); 1816 @ftp_close($this->handle); 1817 1818 return false; 1819 } 1820 1821 // Enable passive mode if the user requested it 1822 if ($this->passive) 1823 { 1824 @ftp_pasv($this->handle, true); 1825 } 1826 else 1827 { 1828 @ftp_pasv($this->handle, false); 1829 } 1830 1831 // Try to download ourselves 1832 $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); 1833 $tempHandle = fopen('php://temp', 'r+'); 1834 if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) 1835 { 1836 $this->setError(AKText::_('WRONG_FTP_PATH2')); 1837 @ftp_close($this->handle); 1838 fclose($tempHandle); 1839 1840 return false; 1841 } 1842 fclose($tempHandle); 1843 1844 return true; 1845 } 1846 1847 private function isDirWritable($dir) 1848 { 1849 $fp = @fopen($dir . '/kickstart.dat', 'wb'); 1850 if ($fp === false) 1851 { 1852 return false; 1853 } 1854 else 1855 { 1856 @fclose($fp); 1857 unlink($dir . '/kickstart.dat'); 1858 1859 return true; 1860 } 1861 } 1862 1863 public function createDirRecursive($dirName, $perms) 1864 { 1865 // Strip absolute filesystem path to website's root 1866 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 1867 if (!empty($removePath)) 1868 { 1869 // UNIXize the paths 1870 $removePath = str_replace('\\', '/', $removePath); 1871 $dirName = str_replace('\\', '/', $dirName); 1872 // Make sure they both end in a slash 1873 $removePath = rtrim($removePath, '/\\') . '/'; 1874 $dirName = rtrim($dirName, '/\\') . '/'; 1875 // Process the path removal 1876 $left = substr($dirName, 0, strlen($removePath)); 1877 if ($left == $removePath) 1878 { 1879 $dirName = substr($dirName, strlen($removePath)); 1880 } 1881 } 1882 if (empty($dirName)) 1883 { 1884 $dirName = ''; 1885 } // 'cause the substr() above may return FALSE. 1886 1887 $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); 1888 if ($this->is_dir($check)) 1889 { 1890 return true; 1891 } 1892 1893 $alldirs = explode('/', $dirName); 1894 $previousDir = '/' . trim($this->dir); 1895 foreach ($alldirs as $curdir) 1896 { 1897 $check = $previousDir . '/' . $curdir; 1898 if (!$this->is_dir($check)) 1899 { 1900 // Proactively try to delete a file by the same name 1901 @ftp_delete($this->handle, $check); 1902 1903 if (@ftp_mkdir($this->handle, $check) === false) 1904 { 1905 // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! 1906 $this->fixPermissions($removePath . $check); 1907 if (@ftp_mkdir($this->handle, $check) === false) 1908 { 1909 // Can we fall back to pure PHP mode, sire? 1910 if (!@mkdir($check)) 1911 { 1912 $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); 1913 1914 return false; 1915 } 1916 else 1917 { 1918 // Since the directory was built by PHP, change its permissions 1919 $trustMeIKnowWhatImDoing = 1920 500 + 10 + 1; // working around overzealous scanners written by bozos 1921 @chmod($check, $trustMeIKnowWhatImDoing); 1922 1923 return true; 1924 } 1925 } 1926 } 1927 @ftp_chmod($this->handle, $perms, $check); 1928 } 1929 $previousDir = $check; 1930 } 1931 1932 return true; 1933 } 1934 1935 private function is_dir($dir) 1936 { 1937 return @ftp_chdir($this->handle, $dir); 1938 } 1939 1940 private function fixPermissions($path) 1941 { 1942 // Turn off error reporting 1943 if (!defined('KSDEBUG')) 1944 { 1945 $oldErrorReporting = @error_reporting(E_NONE); 1946 } 1947 1948 // Get UNIX style paths 1949 $relPath = str_replace('\\', '/', $path); 1950 $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); 1951 $basePath = rtrim($basePath, '/'); 1952 if (!empty($basePath)) 1953 { 1954 $basePath .= '/'; 1955 } 1956 // Remove the leading relative root 1957 if (substr($relPath, 0, strlen($basePath)) == $basePath) 1958 { 1959 $relPath = substr($relPath, strlen($basePath)); 1960 } 1961 $dirArray = explode('/', $relPath); 1962 $pathBuilt = rtrim($basePath, '/'); 1963 foreach ($dirArray as $dir) 1964 { 1965 if (empty($dir)) 1966 { 1967 continue; 1968 } 1969 $oldPath = $pathBuilt; 1970 $pathBuilt .= '/' . $dir; 1971 if (is_dir($oldPath . $dir)) 1972 { 1973 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 1974 @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); 1975 } 1976 else 1977 { 1978 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 1979 if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) 1980 { 1981 @unlink($oldPath . $dir); 1982 } 1983 } 1984 } 1985 1986 // Restore error reporting 1987 if (!defined('KSDEBUG')) 1988 { 1989 @error_reporting($oldErrorReporting); 1990 } 1991 } 1992 1993 function __wakeup() 1994 { 1995 $this->connect(); 1996 } 1997 1998 public function process() 1999 { 2000 if (is_null($this->tempFilename)) 2001 { 2002 // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. 2003 // the entity was a directory or symlink 2004 return true; 2005 } 2006 2007 $remotePath = dirname($this->filename); 2008 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2009 if (!empty($removePath)) 2010 { 2011 $removePath = ltrim($removePath, "/"); 2012 $remotePath = ltrim($remotePath, "/"); 2013 $left = substr($remotePath, 0, strlen($removePath)); 2014 if ($left == $removePath) 2015 { 2016 $remotePath = substr($remotePath, strlen($removePath)); 2017 } 2018 } 2019 2020 $absoluteFSPath = dirname($this->filename); 2021 $relativeFTPPath = trim($remotePath, '/'); 2022 $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); 2023 $onlyFilename = basename($this->filename); 2024 2025 $remoteName = $absoluteFTPPath . '/' . $onlyFilename; 2026 2027 $ret = @ftp_chdir($this->handle, $absoluteFTPPath); 2028 if ($ret === false) 2029 { 2030 $ret = $this->createDirRecursive($absoluteFSPath, 0755); 2031 if ($ret === false) 2032 { 2033 $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); 2034 2035 return false; 2036 } 2037 $ret = @ftp_chdir($this->handle, $absoluteFTPPath); 2038 if ($ret === false) 2039 { 2040 $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); 2041 2042 return false; 2043 } 2044 } 2045 2046 $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); 2047 if ($ret === false) 2048 { 2049 // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! 2050 $this->fixPermissions($this->filename); 2051 $this->unlink($this->filename); 2052 2053 $fp = @fopen($this->tempFilename, 'rb'); 2054 if ($fp !== false) 2055 { 2056 $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); 2057 @fclose($fp); 2058 } 2059 else 2060 { 2061 $ret = false; 2062 } 2063 } 2064 @unlink($this->tempFilename); 2065 2066 if ($ret === false) 2067 { 2068 $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); 2069 2070 return false; 2071 } 2072 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 2073 if ($restorePerms) 2074 { 2075 @ftp_chmod($this->_handle, $this->perms, $remoteName); 2076 } 2077 else 2078 { 2079 @ftp_chmod($this->_handle, 0644, $remoteName); 2080 } 2081 2082 return true; 2083 } 2084 2085 /* 2086 * Tries to fix directory/file permissions in the PHP level, so that 2087 * the FTP operation doesn't fail. 2088 * @param $path string The full path to a directory or file 2089 */ 2090 2091 public function unlink($file) 2092 { 2093 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2094 if (!empty($removePath)) 2095 { 2096 $left = substr($file, 0, strlen($removePath)); 2097 if ($left == $removePath) 2098 { 2099 $file = substr($file, strlen($removePath)); 2100 } 2101 } 2102 2103 $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); 2104 2105 return @ftp_delete($this->handle, $check); 2106 } 2107 2108 public function processFilename($filename, $perms = 0755) 2109 { 2110 // Catch some error conditions... 2111 if ($this->getError()) 2112 { 2113 return false; 2114 } 2115 2116 // If a null filename is passed, it means that we shouldn't do any post processing, i.e. 2117 // the entity was a directory or symlink 2118 if (is_null($filename)) 2119 { 2120 $this->filename = null; 2121 $this->tempFilename = null; 2122 2123 return null; 2124 } 2125 2126 // Strip absolute filesystem path to website's root 2127 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2128 if (!empty($removePath)) 2129 { 2130 $left = substr($filename, 0, strlen($removePath)); 2131 if ($left == $removePath) 2132 { 2133 $filename = substr($filename, strlen($removePath)); 2134 } 2135 } 2136 2137 // Trim slash on the left 2138 $filename = ltrim($filename, '/'); 2139 2140 $this->filename = $filename; 2141 $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); 2142 $this->perms = $perms; 2143 2144 if (empty($this->tempFilename)) 2145 { 2146 // Oops! Let's try something different 2147 $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; 2148 } 2149 2150 return $this->tempFilename; 2151 } 2152 2153 public function close() 2154 { 2155 @ftp_close($this->handle); 2156 } 2157 2158 public function chmod($file, $perms) 2159 { 2160 return @ftp_chmod($this->handle, $perms, $file); 2161 } 2162 2163 public function rmdir($directory) 2164 { 2165 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2166 if (!empty($removePath)) 2167 { 2168 $left = substr($directory, 0, strlen($removePath)); 2169 if ($left == $removePath) 2170 { 2171 $directory = substr($directory, strlen($removePath)); 2172 } 2173 } 2174 2175 $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); 2176 2177 return @ftp_rmdir($this->handle, $check); 2178 } 2179 2180 public function rename($from, $to) 2181 { 2182 $originalFrom = $from; 2183 $originalTo = $to; 2184 2185 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2186 if (!empty($removePath)) 2187 { 2188 $left = substr($from, 0, strlen($removePath)); 2189 if ($left == $removePath) 2190 { 2191 $from = substr($from, strlen($removePath)); 2192 } 2193 } 2194 $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); 2195 2196 if (!empty($removePath)) 2197 { 2198 $left = substr($to, 0, strlen($removePath)); 2199 if ($left == $removePath) 2200 { 2201 $to = substr($to, strlen($removePath)); 2202 } 2203 } 2204 $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); 2205 2206 $result = @ftp_rename($this->handle, $from, $to); 2207 if ($result !== true) 2208 { 2209 return @rename($from, $to); 2210 } 2211 else 2212 { 2213 return true; 2214 } 2215 } 2216 2217} 2218 2219 2220/** 2221 * Akeeba Restore 2222 * A JSON-powered JPA, JPS and ZIP archive extraction library 2223 * 2224 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 2225 * @license GNU GPL v2 or - at your option - any later version 2226 * @package akeebabackup 2227 * @subpackage kickstart 2228 */ 2229 2230/** 2231 * FTP file writer 2232 */ 2233class AKPostprocSFTP extends AKAbstractPostproc 2234{ 2235 /** @var bool Should I use FTP over implicit SSL? */ 2236 public $useSSL = false; 2237 /** @var bool use Passive mode? */ 2238 public $passive = true; 2239 /** @var string FTP host name */ 2240 public $host = ''; 2241 /** @var int FTP port */ 2242 public $port = 21; 2243 /** @var string FTP user name */ 2244 public $user = ''; 2245 /** @var string FTP password */ 2246 public $pass = ''; 2247 /** @var string FTP initial directory */ 2248 public $dir = ''; 2249 2250 /** @var resource SFTP resource handle */ 2251 private $handle = null; 2252 2253 /** @var resource SSH2 connection resource handle */ 2254 private $_connection = null; 2255 2256 /** @var string Current remote directory, including the remote directory string */ 2257 private $_currentdir; 2258 2259 /** @var string The temporary directory where the data will be stored */ 2260 private $tempDir = ''; 2261 2262 public function __construct() 2263 { 2264 parent::__construct(); 2265 2266 $this->host = AKFactory::get('kickstart.ftp.host', ''); 2267 $this->port = AKFactory::get('kickstart.ftp.port', 22); 2268 2269 if (trim($this->port) == '') 2270 { 2271 $this->port = 22; 2272 } 2273 2274 $this->user = AKFactory::get('kickstart.ftp.user', ''); 2275 $this->pass = AKFactory::get('kickstart.ftp.pass', ''); 2276 $this->dir = AKFactory::get('kickstart.ftp.dir', ''); 2277 $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); 2278 2279 $connected = $this->connect(); 2280 2281 if ($connected) 2282 { 2283 if (!empty($this->tempDir)) 2284 { 2285 $tempDir = rtrim($this->tempDir, '/\\') . '/'; 2286 $writable = $this->isDirWritable($tempDir); 2287 } 2288 else 2289 { 2290 $tempDir = ''; 2291 $writable = false; 2292 } 2293 2294 if (!$writable) 2295 { 2296 // Default temporary directory is the current root 2297 $tempDir = KSROOTDIR; 2298 if (empty($tempDir)) 2299 { 2300 // Oh, we have no directory reported! 2301 $tempDir = '.'; 2302 } 2303 $absoluteDirToHere = $tempDir; 2304 $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); 2305 if (!empty($tempDir)) 2306 { 2307 $tempDir .= '/'; 2308 } 2309 $this->tempDir = $tempDir; 2310 // Is this directory writable? 2311 $writable = $this->isDirWritable($tempDir); 2312 } 2313 2314 if (!$writable) 2315 { 2316 // Nope. Let's try creating a temporary directory in the site's root. 2317 $tempDir = $absoluteDirToHere . '/kicktemp'; 2318 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 2319 $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); 2320 // Try making it writable... 2321 $this->fixPermissions($tempDir); 2322 $writable = $this->isDirWritable($tempDir); 2323 } 2324 2325 // Was the new directory writable? 2326 if (!$writable) 2327 { 2328 // Let's see if the user has specified one 2329 $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); 2330 if (!empty($userdir)) 2331 { 2332 // Is it an absolute or a relative directory? 2333 $absolute = false; 2334 $absolute = $absolute || (substr($userdir, 0, 1) == '/'); 2335 $absolute = $absolute || (substr($userdir, 1, 1) == ':'); 2336 $absolute = $absolute || (substr($userdir, 2, 1) == ':'); 2337 if (!$absolute) 2338 { 2339 // Make absolute 2340 $tempDir = $absoluteDirToHere . $userdir; 2341 } 2342 else 2343 { 2344 // it's already absolute 2345 $tempDir = $userdir; 2346 } 2347 // Does the directory exist? 2348 if (is_dir($tempDir)) 2349 { 2350 // Yeah. Is it writable? 2351 $writable = $this->isDirWritable($tempDir); 2352 } 2353 } 2354 } 2355 $this->tempDir = $tempDir; 2356 2357 if (!$writable) 2358 { 2359 // No writable directory found!!! 2360 $this->setError(AKText::_('SFTP_TEMPDIR_NOT_WRITABLE')); 2361 } 2362 else 2363 { 2364 AKFactory::set('kickstart.ftp.tempdir', $tempDir); 2365 $this->tempDir = $tempDir; 2366 } 2367 } 2368 } 2369 2370 public function connect() 2371 { 2372 $this->_connection = false; 2373 2374 if (!function_exists('ssh2_connect')) 2375 { 2376 $this->setError(AKText::_('SFTP_NO_SSH2')); 2377 2378 return false; 2379 } 2380 2381 $this->_connection = @ssh2_connect($this->host, $this->port); 2382 2383 if (!@ssh2_auth_password($this->_connection, $this->user, $this->pass)) 2384 { 2385 $this->setError(AKText::_('SFTP_WRONG_USER')); 2386 2387 $this->_connection = false; 2388 2389 return false; 2390 } 2391 2392 $this->handle = @ssh2_sftp($this->_connection); 2393 2394 // I must have an absolute directory 2395 if (!$this->dir) 2396 { 2397 $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); 2398 2399 return false; 2400 } 2401 2402 // Change to initial directory 2403 if (!$this->sftp_chdir('/')) 2404 { 2405 $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); 2406 2407 unset($this->_connection); 2408 unset($this->handle); 2409 2410 return false; 2411 } 2412 2413 // Try to download ourselves 2414 $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); 2415 $basePath = '/' . trim($this->dir, '/'); 2416 2417 if (@fopen("ssh2.sftp://{$this->handle}$basePath/$testFilename", 'r+') === false) 2418 { 2419 $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); 2420 2421 unset($this->_connection); 2422 unset($this->handle); 2423 2424 return false; 2425 } 2426 2427 return true; 2428 } 2429 2430 /** 2431 * Changes to the requested directory in the remote server. You give only the 2432 * path relative to the initial directory and it does all the rest by itself, 2433 * including doing nothing if the remote directory is the one we want. 2434 * 2435 * @param string $dir The (realtive) remote directory 2436 * 2437 * @return bool True if successful, false otherwise. 2438 */ 2439 private function sftp_chdir($dir) 2440 { 2441 // Strip absolute filesystem path to website's root 2442 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2443 if (!empty($removePath)) 2444 { 2445 // UNIXize the paths 2446 $removePath = str_replace('\\', '/', $removePath); 2447 $dir = str_replace('\\', '/', $dir); 2448 2449 // Make sure they both end in a slash 2450 $removePath = rtrim($removePath, '/\\') . '/'; 2451 $dir = rtrim($dir, '/\\') . '/'; 2452 2453 // Process the path removal 2454 $left = substr($dir, 0, strlen($removePath)); 2455 2456 if ($left == $removePath) 2457 { 2458 $dir = substr($dir, strlen($removePath)); 2459 } 2460 } 2461 2462 if (empty($dir)) 2463 { 2464 // Because the substr() above may return FALSE. 2465 $dir = ''; 2466 } 2467 2468 // Calculate "real" (absolute) SFTP path 2469 $realdir = substr($this->dir, -1) == '/' ? substr($this->dir, 0, strlen($this->dir) - 1) : $this->dir; 2470 $realdir .= '/' . $dir; 2471 $realdir = substr($realdir, 0, 1) == '/' ? $realdir : '/' . $realdir; 2472 2473 if ($this->_currentdir == $realdir) 2474 { 2475 // Already there, do nothing 2476 return true; 2477 } 2478 2479 $result = @ssh2_sftp_stat($this->handle, $realdir); 2480 2481 if ($result === false) 2482 { 2483 return false; 2484 } 2485 else 2486 { 2487 // Update the private "current remote directory" variable 2488 $this->_currentdir = $realdir; 2489 2490 return true; 2491 } 2492 } 2493 2494 private function isDirWritable($dir) 2495 { 2496 if (@fopen("ssh2.sftp://{$this->handle}$dir/kickstart.dat", 'wb') === false) 2497 { 2498 return false; 2499 } 2500 else 2501 { 2502 @ssh2_sftp_unlink($this->handle, $dir . '/kickstart.dat'); 2503 2504 return true; 2505 } 2506 } 2507 2508 public function createDirRecursive($dirName, $perms) 2509 { 2510 // Strip absolute filesystem path to website's root 2511 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2512 if (!empty($removePath)) 2513 { 2514 // UNIXize the paths 2515 $removePath = str_replace('\\', '/', $removePath); 2516 $dirName = str_replace('\\', '/', $dirName); 2517 // Make sure they both end in a slash 2518 $removePath = rtrim($removePath, '/\\') . '/'; 2519 $dirName = rtrim($dirName, '/\\') . '/'; 2520 // Process the path removal 2521 $left = substr($dirName, 0, strlen($removePath)); 2522 if ($left == $removePath) 2523 { 2524 $dirName = substr($dirName, strlen($removePath)); 2525 } 2526 } 2527 if (empty($dirName)) 2528 { 2529 $dirName = ''; 2530 } // 'cause the substr() above may return FALSE. 2531 2532 $check = '/' . trim($this->dir, '/ ') . '/' . trim($dirName, '/'); 2533 2534 if ($this->is_dir($check)) 2535 { 2536 return true; 2537 } 2538 2539 $alldirs = explode('/', $dirName); 2540 $previousDir = '/' . trim($this->dir, '/ '); 2541 2542 foreach ($alldirs as $curdir) 2543 { 2544 if (!$curdir) 2545 { 2546 continue; 2547 } 2548 2549 $check = $previousDir . '/' . $curdir; 2550 2551 if (!$this->is_dir($check)) 2552 { 2553 // Proactively try to delete a file by the same name 2554 @ssh2_sftp_unlink($this->handle, $check); 2555 2556 if (@ssh2_sftp_mkdir($this->handle, $check) === false) 2557 { 2558 // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! 2559 $this->fixPermissions($check); 2560 2561 if (@ssh2_sftp_mkdir($this->handle, $check) === false) 2562 { 2563 // Can we fall back to pure PHP mode, sire? 2564 if (!@mkdir($check)) 2565 { 2566 $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); 2567 2568 return false; 2569 } 2570 else 2571 { 2572 // Since the directory was built by PHP, change its permissions 2573 $trustMeIKnowWhatImDoing = 2574 500 + 10 + 1; // working around overzealous scanners written by bozos 2575 @chmod($check, $trustMeIKnowWhatImDoing); 2576 2577 return true; 2578 } 2579 } 2580 } 2581 2582 @ssh2_sftp_chmod($this->handle, $check, $perms); 2583 } 2584 2585 $previousDir = $check; 2586 } 2587 2588 return true; 2589 } 2590 2591 private function is_dir($dir) 2592 { 2593 return $this->sftp_chdir($dir); 2594 } 2595 2596 private function fixPermissions($path) 2597 { 2598 // Turn off error reporting 2599 if (!defined('KSDEBUG')) 2600 { 2601 $oldErrorReporting = @error_reporting(E_NONE); 2602 } 2603 2604 // Get UNIX style paths 2605 $relPath = str_replace('\\', '/', $path); 2606 $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); 2607 $basePath = rtrim($basePath, '/'); 2608 2609 if (!empty($basePath)) 2610 { 2611 $basePath .= '/'; 2612 } 2613 2614 // Remove the leading relative root 2615 if (substr($relPath, 0, strlen($basePath)) == $basePath) 2616 { 2617 $relPath = substr($relPath, strlen($basePath)); 2618 } 2619 2620 $dirArray = explode('/', $relPath); 2621 $pathBuilt = rtrim($basePath, '/'); 2622 2623 foreach ($dirArray as $dir) 2624 { 2625 if (empty($dir)) 2626 { 2627 continue; 2628 } 2629 2630 $oldPath = $pathBuilt; 2631 $pathBuilt .= '/' . $dir; 2632 2633 if (is_dir($oldPath . '/' . $dir)) 2634 { 2635 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 2636 @chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing); 2637 } 2638 else 2639 { 2640 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 2641 if (@chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing) === false) 2642 { 2643 @unlink($oldPath . $dir); 2644 } 2645 } 2646 } 2647 2648 // Restore error reporting 2649 if (!defined('KSDEBUG')) 2650 { 2651 @error_reporting($oldErrorReporting); 2652 } 2653 } 2654 2655 function __wakeup() 2656 { 2657 $this->connect(); 2658 } 2659 2660 /* 2661 * Tries to fix directory/file permissions in the PHP level, so that 2662 * the FTP operation doesn't fail. 2663 * @param $path string The full path to a directory or file 2664 */ 2665 2666 public function process() 2667 { 2668 if (is_null($this->tempFilename)) 2669 { 2670 // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. 2671 // the entity was a directory or symlink 2672 return true; 2673 } 2674 2675 $remotePath = dirname($this->filename); 2676 $absoluteFSPath = dirname($this->filename); 2677 $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); 2678 $onlyFilename = basename($this->filename); 2679 2680 $remoteName = $absoluteFTPPath . '/' . $onlyFilename; 2681 2682 $ret = $this->sftp_chdir($absoluteFTPPath); 2683 2684 if ($ret === false) 2685 { 2686 $ret = $this->createDirRecursive($absoluteFSPath, 0755); 2687 2688 if ($ret === false) 2689 { 2690 $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); 2691 2692 return false; 2693 } 2694 2695 $ret = $this->sftp_chdir($absoluteFTPPath); 2696 2697 if ($ret === false) 2698 { 2699 $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); 2700 2701 return false; 2702 } 2703 } 2704 2705 // Create the file 2706 $ret = $this->write($this->tempFilename, $remoteName); 2707 2708 // If I got a -1 it means that I wasn't able to open the file, so I have to stop here 2709 if ($ret === -1) 2710 { 2711 $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); 2712 2713 return false; 2714 } 2715 2716 if ($ret === false) 2717 { 2718 // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! 2719 $this->fixPermissions($this->filename); 2720 $this->unlink($this->filename); 2721 2722 $ret = $this->write($this->tempFilename, $remoteName); 2723 } 2724 2725 @unlink($this->tempFilename); 2726 2727 if ($ret === false) 2728 { 2729 $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); 2730 2731 return false; 2732 } 2733 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 2734 2735 if ($restorePerms) 2736 { 2737 $this->chmod($remoteName, $this->perms); 2738 } 2739 else 2740 { 2741 $this->chmod($remoteName, 0644); 2742 } 2743 2744 return true; 2745 } 2746 2747 private function write($local, $remote) 2748 { 2749 $fp = @fopen("ssh2.sftp://{$this->handle}$remote", 'w'); 2750 $localfp = @fopen($local, 'rb'); 2751 2752 if ($fp === false) 2753 { 2754 return -1; 2755 } 2756 2757 if ($localfp === false) 2758 { 2759 @fclose($fp); 2760 2761 return -1; 2762 } 2763 2764 $res = true; 2765 2766 while (!feof($localfp) && ($res !== false)) 2767 { 2768 $buffer = @fread($localfp, 65567); 2769 $res = @fwrite($fp, $buffer); 2770 } 2771 2772 @fclose($fp); 2773 @fclose($localfp); 2774 2775 return $res; 2776 } 2777 2778 public function unlink($file) 2779 { 2780 $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); 2781 2782 return @ssh2_sftp_unlink($this->handle, $check); 2783 } 2784 2785 public function chmod($file, $perms) 2786 { 2787 return @ssh2_sftp_chmod($this->handle, $file, $perms); 2788 } 2789 2790 public function processFilename($filename, $perms = 0755) 2791 { 2792 // Catch some error conditions... 2793 if ($this->getError()) 2794 { 2795 return false; 2796 } 2797 2798 // If a null filename is passed, it means that we shouldn't do any post processing, i.e. 2799 // the entity was a directory or symlink 2800 if (is_null($filename)) 2801 { 2802 $this->filename = null; 2803 $this->tempFilename = null; 2804 2805 return null; 2806 } 2807 2808 // Strip absolute filesystem path to website's root 2809 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 2810 if (!empty($removePath)) 2811 { 2812 $left = substr($filename, 0, strlen($removePath)); 2813 if ($left == $removePath) 2814 { 2815 $filename = substr($filename, strlen($removePath)); 2816 } 2817 } 2818 2819 // Trim slash on the left 2820 $filename = ltrim($filename, '/'); 2821 2822 $this->filename = $filename; 2823 $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); 2824 $this->perms = $perms; 2825 2826 if (empty($this->tempFilename)) 2827 { 2828 // Oops! Let's try something different 2829 $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; 2830 } 2831 2832 return $this->tempFilename; 2833 } 2834 2835 public function close() 2836 { 2837 unset($this->_connection); 2838 unset($this->handle); 2839 } 2840 2841 public function rmdir($directory) 2842 { 2843 $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); 2844 2845 return @ssh2_sftp_rmdir($this->handle, $check); 2846 } 2847 2848 public function rename($from, $to) 2849 { 2850 $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); 2851 $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); 2852 2853 $result = @ssh2_sftp_rename($this->handle, $from, $to); 2854 2855 if ($result !== true) 2856 { 2857 return @rename($from, $to); 2858 } 2859 else 2860 { 2861 return true; 2862 } 2863 } 2864 2865} 2866 2867 2868/** 2869 * Akeeba Restore 2870 * A JSON-powered JPA, JPS and ZIP archive extraction library 2871 * 2872 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 2873 * @license GNU GPL v2 or - at your option - any later version 2874 * @package akeebabackup 2875 * @subpackage kickstart 2876 */ 2877 2878/** 2879 * Hybrid direct / FTP mode file writer 2880 */ 2881class AKPostprocHybrid extends AKAbstractPostproc 2882{ 2883 2884 /** @var bool Should I use the FTP layer? */ 2885 public $useFTP = false; 2886 2887 /** @var bool Should I use FTP over implicit SSL? */ 2888 public $useSSL = false; 2889 2890 /** @var bool use Passive mode? */ 2891 public $passive = true; 2892 2893 /** @var string FTP host name */ 2894 public $host = ''; 2895 2896 /** @var int FTP port */ 2897 public $port = 21; 2898 2899 /** @var string FTP user name */ 2900 public $user = ''; 2901 2902 /** @var string FTP password */ 2903 public $pass = ''; 2904 2905 /** @var string FTP initial directory */ 2906 public $dir = ''; 2907 2908 /** @var resource The FTP handle */ 2909 private $handle = null; 2910 2911 /** @var string The temporary directory where the data will be stored */ 2912 private $tempDir = ''; 2913 2914 /** @var null The FTP connection handle */ 2915 private $_handle = null; 2916 2917 /** 2918 * Public constructor. Tries to connect to the FTP server. 2919 */ 2920 public function __construct() 2921 { 2922 parent::__construct(); 2923 2924 $this->useFTP = true; 2925 $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); 2926 $this->passive = AKFactory::get('kickstart.ftp.passive', true); 2927 $this->host = AKFactory::get('kickstart.ftp.host', ''); 2928 $this->port = AKFactory::get('kickstart.ftp.port', 21); 2929 $this->user = AKFactory::get('kickstart.ftp.user', ''); 2930 $this->pass = AKFactory::get('kickstart.ftp.pass', ''); 2931 $this->dir = AKFactory::get('kickstart.ftp.dir', ''); 2932 $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); 2933 2934 if (trim($this->port) == '') 2935 { 2936 $this->port = 21; 2937 } 2938 2939 // If FTP is not configured, skip it altogether 2940 if (empty($this->host) || empty($this->user) || empty($this->pass)) 2941 { 2942 $this->useFTP = false; 2943 } 2944 2945 // Try to connect to the FTP server 2946 $connected = $this->connect(); 2947 2948 // If the connection fails, skip FTP altogether 2949 if (!$connected) 2950 { 2951 $this->useFTP = false; 2952 } 2953 2954 if ($connected) 2955 { 2956 if (!empty($this->tempDir)) 2957 { 2958 $tempDir = rtrim($this->tempDir, '/\\') . '/'; 2959 $writable = $this->isDirWritable($tempDir); 2960 } 2961 else 2962 { 2963 $tempDir = ''; 2964 $writable = false; 2965 } 2966 2967 if (!$writable) 2968 { 2969 // Default temporary directory is the current root 2970 $tempDir = KSROOTDIR; 2971 if (empty($tempDir)) 2972 { 2973 // Oh, we have no directory reported! 2974 $tempDir = '.'; 2975 } 2976 $absoluteDirToHere = $tempDir; 2977 $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); 2978 if (!empty($tempDir)) 2979 { 2980 $tempDir .= '/'; 2981 } 2982 $this->tempDir = $tempDir; 2983 // Is this directory writable? 2984 $writable = $this->isDirWritable($tempDir); 2985 } 2986 2987 if (!$writable) 2988 { 2989 // Nope. Let's try creating a temporary directory in the site's root. 2990 $tempDir = $absoluteDirToHere . '/kicktemp'; 2991 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 2992 $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); 2993 // Try making it writable... 2994 $this->fixPermissions($tempDir); 2995 $writable = $this->isDirWritable($tempDir); 2996 } 2997 2998 // Was the new directory writable? 2999 if (!$writable) 3000 { 3001 // Let's see if the user has specified one 3002 $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); 3003 if (!empty($userdir)) 3004 { 3005 // Is it an absolute or a relative directory? 3006 $absolute = false; 3007 $absolute = $absolute || (substr($userdir, 0, 1) == '/'); 3008 $absolute = $absolute || (substr($userdir, 1, 1) == ':'); 3009 $absolute = $absolute || (substr($userdir, 2, 1) == ':'); 3010 if (!$absolute) 3011 { 3012 // Make absolute 3013 $tempDir = $absoluteDirToHere . $userdir; 3014 } 3015 else 3016 { 3017 // it's already absolute 3018 $tempDir = $userdir; 3019 } 3020 // Does the directory exist? 3021 if (is_dir($tempDir)) 3022 { 3023 // Yeah. Is it writable? 3024 $writable = $this->isDirWritable($tempDir); 3025 } 3026 } 3027 } 3028 $this->tempDir = $tempDir; 3029 3030 if (!$writable) 3031 { 3032 // No writable directory found!!! 3033 $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); 3034 } 3035 else 3036 { 3037 AKFactory::set('kickstart.ftp.tempdir', $tempDir); 3038 $this->tempDir = $tempDir; 3039 } 3040 } 3041 } 3042 3043 /** 3044 * Tries to connect to the FTP server 3045 * 3046 * @return bool 3047 */ 3048 public function connect() 3049 { 3050 if (!$this->useFTP) 3051 { 3052 return false; 3053 } 3054 3055 // Connect to server, using SSL if so required 3056 if ($this->useSSL) 3057 { 3058 $this->handle = @ftp_ssl_connect($this->host, $this->port); 3059 } 3060 else 3061 { 3062 $this->handle = @ftp_connect($this->host, $this->port); 3063 } 3064 if ($this->handle === false) 3065 { 3066 $this->setError(AKText::_('WRONG_FTP_HOST')); 3067 3068 return false; 3069 } 3070 3071 // Login 3072 if (!@ftp_login($this->handle, $this->user, $this->pass)) 3073 { 3074 $this->setError(AKText::_('WRONG_FTP_USER')); 3075 @ftp_close($this->handle); 3076 3077 return false; 3078 } 3079 3080 // Change to initial directory 3081 if (!@ftp_chdir($this->handle, $this->dir)) 3082 { 3083 $this->setError(AKText::_('WRONG_FTP_PATH1')); 3084 @ftp_close($this->handle); 3085 3086 return false; 3087 } 3088 3089 // Enable passive mode if the user requested it 3090 if ($this->passive) 3091 { 3092 @ftp_pasv($this->handle, true); 3093 } 3094 else 3095 { 3096 @ftp_pasv($this->handle, false); 3097 } 3098 3099 // Try to download ourselves 3100 $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); 3101 $tempHandle = fopen('php://temp', 'r+'); 3102 3103 if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) 3104 { 3105 $this->setError(AKText::_('WRONG_FTP_PATH2')); 3106 @ftp_close($this->handle); 3107 fclose($tempHandle); 3108 3109 return false; 3110 } 3111 3112 fclose($tempHandle); 3113 3114 return true; 3115 } 3116 3117 /** 3118 * Is the directory writeable? 3119 * 3120 * @param string $dir The directory ti check 3121 * 3122 * @return bool 3123 */ 3124 private function isDirWritable($dir) 3125 { 3126 $fp = @fopen($dir . '/kickstart.dat', 'wb'); 3127 3128 if ($fp === false) 3129 { 3130 return false; 3131 } 3132 3133 @fclose($fp); 3134 unlink($dir . '/kickstart.dat'); 3135 3136 return true; 3137 } 3138 3139 /** 3140 * Create a directory, recursively 3141 * 3142 * @param string $dirName The directory to create 3143 * @param int $perms The permissions to give to the directory 3144 * 3145 * @return bool 3146 */ 3147 public function createDirRecursive($dirName, $perms) 3148 { 3149 // Strip absolute filesystem path to website's root 3150 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3151 3152 if (!empty($removePath)) 3153 { 3154 // UNIXize the paths 3155 $removePath = str_replace('\\', '/', $removePath); 3156 $dirName = str_replace('\\', '/', $dirName); 3157 // Make sure they both end in a slash 3158 $removePath = rtrim($removePath, '/\\') . '/'; 3159 $dirName = rtrim($dirName, '/\\') . '/'; 3160 // Process the path removal 3161 $left = substr($dirName, 0, strlen($removePath)); 3162 3163 if ($left == $removePath) 3164 { 3165 $dirName = substr($dirName, strlen($removePath)); 3166 } 3167 } 3168 3169 // 'cause the substr() above may return FALSE. 3170 if (empty($dirName)) 3171 { 3172 $dirName = ''; 3173 } 3174 3175 $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); 3176 $checkFS = $removePath . trim($dirName, '/'); 3177 3178 if ($this->is_dir($check)) 3179 { 3180 return true; 3181 } 3182 3183 $alldirs = explode('/', $dirName); 3184 $previousDir = '/' . trim($this->dir); 3185 $previousDirFS = rtrim($removePath, '/\\'); 3186 3187 foreach ($alldirs as $curdir) 3188 { 3189 $check = $previousDir . '/' . $curdir; 3190 $checkFS = $previousDirFS . '/' . $curdir; 3191 3192 if (!is_dir($checkFS) && !$this->is_dir($check)) 3193 { 3194 // Proactively try to delete a file by the same name 3195 if (!@unlink($checkFS) && $this->useFTP) 3196 { 3197 @ftp_delete($this->handle, $check); 3198 } 3199 3200 $createdDir = @mkdir($checkFS, 0755); 3201 3202 if (!$createdDir && $this->useFTP) 3203 { 3204 $createdDir = @ftp_mkdir($this->handle, $check); 3205 } 3206 3207 if ($createdDir === false) 3208 { 3209 // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! 3210 $this->fixPermissions($checkFS); 3211 3212 $createdDir = @mkdir($checkFS, 0755); 3213 if (!$createdDir && $this->useFTP) 3214 { 3215 $createdDir = @ftp_mkdir($this->handle, $check); 3216 } 3217 3218 if ($createdDir === false) 3219 { 3220 $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); 3221 3222 return false; 3223 } 3224 } 3225 3226 if (!@chmod($checkFS, $perms) && $this->useFTP) 3227 { 3228 @ftp_chmod($this->handle, $perms, $check); 3229 } 3230 } 3231 3232 $previousDir = $check; 3233 $previousDirFS = $checkFS; 3234 } 3235 3236 return true; 3237 } 3238 3239 private function is_dir($dir) 3240 { 3241 if ($this->useFTP) 3242 { 3243 return @ftp_chdir($this->handle, $dir); 3244 } 3245 3246 return false; 3247 } 3248 3249 /** 3250 * Tries to fix directory/file permissions in the PHP level, so that 3251 * the FTP operation doesn't fail. 3252 * 3253 * @param $path string The full path to a directory or file 3254 */ 3255 private function fixPermissions($path) 3256 { 3257 // Turn off error reporting 3258 if (!defined('KSDEBUG')) 3259 { 3260 $oldErrorReporting = error_reporting(0); 3261 } 3262 3263 // Get UNIX style paths 3264 $relPath = str_replace('\\', '/', $path); 3265 $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); 3266 $basePath = rtrim($basePath, '/'); 3267 3268 if (!empty($basePath)) 3269 { 3270 $basePath .= '/'; 3271 } 3272 3273 // Remove the leading relative root 3274 if (substr($relPath, 0, strlen($basePath)) == $basePath) 3275 { 3276 $relPath = substr($relPath, strlen($basePath)); 3277 } 3278 3279 $dirArray = explode('/', $relPath); 3280 $pathBuilt = rtrim($basePath, '/'); 3281 3282 foreach ($dirArray as $dir) 3283 { 3284 if (empty($dir)) 3285 { 3286 continue; 3287 } 3288 3289 $oldPath = $pathBuilt; 3290 $pathBuilt .= '/' . $dir; 3291 3292 if (is_dir($oldPath . $dir)) 3293 { 3294 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 3295 @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); 3296 } 3297 else 3298 { 3299 $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos 3300 if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) 3301 { 3302 @unlink($oldPath . $dir); 3303 } 3304 } 3305 } 3306 3307 // Restore error reporting 3308 if (!defined('KSDEBUG')) 3309 { 3310 @error_reporting($oldErrorReporting); 3311 } 3312 } 3313 3314 /** 3315 * Called after unserialisation, tries to reconnect to FTP 3316 */ 3317 function __wakeup() 3318 { 3319 if ($this->useFTP) 3320 { 3321 $this->connect(); 3322 } 3323 } 3324 3325 function __destruct() 3326 { 3327 if (!$this->useFTP) 3328 { 3329 @ftp_close($this->handle); 3330 } 3331 } 3332 3333 /** 3334 * Post-process an extracted file, using FTP or direct file writes to move it 3335 * 3336 * @return bool 3337 */ 3338 public function process() 3339 { 3340 if (is_null($this->tempFilename)) 3341 { 3342 // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. 3343 // the entity was a directory or symlink 3344 return true; 3345 } 3346 3347 $remotePath = dirname($this->filename); 3348 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3349 $root = rtrim($removePath, '/\\'); 3350 3351 if (!empty($removePath)) 3352 { 3353 $removePath = ltrim($removePath, "/"); 3354 $remotePath = ltrim($remotePath, "/"); 3355 $left = substr($remotePath, 0, strlen($removePath)); 3356 3357 if ($left == $removePath) 3358 { 3359 $remotePath = substr($remotePath, strlen($removePath)); 3360 } 3361 } 3362 3363 $absoluteFSPath = dirname($this->filename); 3364 $relativeFTPPath = trim($remotePath, '/'); 3365 $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); 3366 $onlyFilename = basename($this->filename); 3367 3368 $remoteName = $absoluteFTPPath . '/' . $onlyFilename; 3369 3370 // Does the directory exist? 3371 if (!is_dir($root . '/' . $absoluteFSPath)) 3372 { 3373 $ret = $this->createDirRecursive($absoluteFSPath, 0755); 3374 3375 if (($ret === false) && ($this->useFTP)) 3376 { 3377 $ret = @ftp_chdir($this->handle, $absoluteFTPPath); 3378 } 3379 3380 if ($ret === false) 3381 { 3382 $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); 3383 3384 return false; 3385 } 3386 } 3387 3388 if ($this->useFTP) 3389 { 3390 $ret = @ftp_chdir($this->handle, $absoluteFTPPath); 3391 } 3392 3393 // Try copying directly 3394 $ret = @copy($this->tempFilename, $root . '/' . $this->filename); 3395 3396 if ($ret === false) 3397 { 3398 $this->fixPermissions($this->filename); 3399 $this->unlink($this->filename); 3400 3401 $ret = @copy($this->tempFilename, $root . '/' . $this->filename); 3402 } 3403 3404 if ($this->useFTP && ($ret === false)) 3405 { 3406 $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); 3407 3408 if ($ret === false) 3409 { 3410 // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! 3411 $this->fixPermissions($this->filename); 3412 $this->unlink($this->filename); 3413 3414 $fp = @fopen($this->tempFilename, 'rb'); 3415 if ($fp !== false) 3416 { 3417 $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); 3418 @fclose($fp); 3419 } 3420 else 3421 { 3422 $ret = false; 3423 } 3424 } 3425 } 3426 3427 @unlink($this->tempFilename); 3428 3429 if ($ret === false) 3430 { 3431 $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); 3432 3433 return false; 3434 } 3435 3436 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 3437 $perms = $restorePerms ? $this->perms : 0644; 3438 3439 $ret = @chmod($root . '/' . $this->filename, $perms); 3440 3441 if ($this->useFTP && ($ret === false)) 3442 { 3443 @ftp_chmod($this->_handle, $perms, $remoteName); 3444 } 3445 3446 return true; 3447 } 3448 3449 public function unlink($file) 3450 { 3451 $ret = @unlink($file); 3452 3453 if (!$ret && $this->useFTP) 3454 { 3455 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3456 if (!empty($removePath)) 3457 { 3458 $left = substr($file, 0, strlen($removePath)); 3459 if ($left == $removePath) 3460 { 3461 $file = substr($file, strlen($removePath)); 3462 } 3463 } 3464 3465 $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); 3466 3467 $ret = @ftp_delete($this->handle, $check); 3468 } 3469 3470 return $ret; 3471 } 3472 3473 /** 3474 * Create a temporary filename 3475 * 3476 * @param string $filename The original filename 3477 * @param int $perms The file permissions 3478 * 3479 * @return string 3480 */ 3481 public function processFilename($filename, $perms = 0755) 3482 { 3483 // Catch some error conditions... 3484 if ($this->getError()) 3485 { 3486 return false; 3487 } 3488 3489 // If a null filename is passed, it means that we shouldn't do any post processing, i.e. 3490 // the entity was a directory or symlink 3491 if (is_null($filename)) 3492 { 3493 $this->filename = null; 3494 $this->tempFilename = null; 3495 3496 return null; 3497 } 3498 3499 // Strip absolute filesystem path to website's root 3500 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3501 3502 if (!empty($removePath)) 3503 { 3504 $left = substr($filename, 0, strlen($removePath)); 3505 3506 if ($left == $removePath) 3507 { 3508 $filename = substr($filename, strlen($removePath)); 3509 } 3510 } 3511 3512 // Trim slash on the left 3513 $filename = ltrim($filename, '/'); 3514 3515 $this->filename = $filename; 3516 $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); 3517 $this->perms = $perms; 3518 3519 if (empty($this->tempFilename)) 3520 { 3521 // Oops! Let's try something different 3522 $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; 3523 } 3524 3525 return $this->tempFilename; 3526 } 3527 3528 /** 3529 * Closes the FTP connection 3530 */ 3531 public function close() 3532 { 3533 if (!$this->useFTP) 3534 { 3535 @ftp_close($this->handle); 3536 } 3537 } 3538 3539 public function chmod($file, $perms) 3540 { 3541 if (AKFactory::get('kickstart.setup.dryrun', '0')) 3542 { 3543 return true; 3544 } 3545 3546 $ret = @chmod($file, $perms); 3547 3548 if (!$ret && $this->useFTP) 3549 { 3550 // Strip absolute filesystem path to website's root 3551 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3552 3553 if (!empty($removePath)) 3554 { 3555 $left = substr($file, 0, strlen($removePath)); 3556 3557 if ($left == $removePath) 3558 { 3559 $file = substr($file, strlen($removePath)); 3560 } 3561 } 3562 3563 // Trim slash on the left 3564 $file = ltrim($file, '/'); 3565 3566 $ret = @ftp_chmod($this->handle, $perms, $file); 3567 } 3568 3569 return $ret; 3570 } 3571 3572 public function rmdir($directory) 3573 { 3574 $ret = @rmdir($directory); 3575 3576 if (!$ret && $this->useFTP) 3577 { 3578 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3579 if (!empty($removePath)) 3580 { 3581 $left = substr($directory, 0, strlen($removePath)); 3582 if ($left == $removePath) 3583 { 3584 $directory = substr($directory, strlen($removePath)); 3585 } 3586 } 3587 3588 $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); 3589 3590 $ret = @ftp_rmdir($this->handle, $check); 3591 } 3592 3593 return $ret; 3594 } 3595 3596 public function rename($from, $to) 3597 { 3598 $ret = @rename($from, $to); 3599 3600 if (!$ret && $this->useFTP) 3601 { 3602 $originalFrom = $from; 3603 $originalTo = $to; 3604 3605 $removePath = AKFactory::get('kickstart.setup.destdir', ''); 3606 if (!empty($removePath)) 3607 { 3608 $left = substr($from, 0, strlen($removePath)); 3609 if ($left == $removePath) 3610 { 3611 $from = substr($from, strlen($removePath)); 3612 } 3613 } 3614 $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); 3615 3616 if (!empty($removePath)) 3617 { 3618 $left = substr($to, 0, strlen($removePath)); 3619 if ($left == $removePath) 3620 { 3621 $to = substr($to, strlen($removePath)); 3622 } 3623 } 3624 $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); 3625 3626 $ret = @ftp_rename($this->handle, $from, $to); 3627 } 3628 3629 return $ret; 3630 } 3631} 3632 3633/** 3634 * Akeeba Restore 3635 * A JSON-powered JPA, JPS and ZIP archive extraction library 3636 * 3637 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 3638 * @license GNU GPL v2 or - at your option - any later version 3639 * @package akeebabackup 3640 * @subpackage kickstart 3641 */ 3642 3643/** 3644 * JPA archive extraction class 3645 */ 3646class AKUnarchiverJPA extends AKAbstractUnarchiver 3647{ 3648 protected $archiveHeaderData = array(); 3649 3650 protected function readArchiveHeader() 3651 { 3652 debugMsg('Preparing to read archive header'); 3653 // Initialize header data array 3654 $this->archiveHeaderData = new stdClass(); 3655 3656 // Open the first part 3657 debugMsg('Opening the first part'); 3658 $this->nextFile(); 3659 3660 // Fail for unreadable files 3661 if ($this->fp === false) 3662 { 3663 debugMsg('Could not open the first part'); 3664 3665 return false; 3666 } 3667 3668 // Read the signature 3669 $sig = fread($this->fp, 3); 3670 3671 if ($sig != 'JPA') 3672 { 3673 // Not a JPA file 3674 debugMsg('Invalid archive signature'); 3675 $this->setError(AKText::_('ERR_NOT_A_JPA_FILE')); 3676 3677 return false; 3678 } 3679 3680 // Read and parse header length 3681 $header_length_array = unpack('v', fread($this->fp, 2)); 3682 $header_length = $header_length_array[1]; 3683 3684 // Read and parse the known portion of header data (14 bytes) 3685 $bin_data = fread($this->fp, 14); 3686 $header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data); 3687 3688 // Load any remaining header data (forward compatibility) 3689 $rest_length = $header_length - 19; 3690 3691 if ($rest_length > 0) 3692 { 3693 $junk = fread($this->fp, $rest_length); 3694 } 3695 else 3696 { 3697 $junk = ''; 3698 } 3699 3700 // Temporary array with all the data we read 3701 $temp = array( 3702 'signature' => $sig, 3703 'length' => $header_length, 3704 'major' => $header_data['major'], 3705 'minor' => $header_data['minor'], 3706 'filecount' => $header_data['count'], 3707 'uncompressedsize' => $header_data['uncsize'], 3708 'compressedsize' => $header_data['csize'], 3709 'unknowndata' => $junk 3710 ); 3711 3712 // Array-to-object conversion 3713 foreach ($temp as $key => $value) 3714 { 3715 $this->archiveHeaderData->{$key} = $value; 3716 } 3717 3718 debugMsg('Header data:'); 3719 debugMsg('Length : ' . $header_length); 3720 debugMsg('Major : ' . $header_data['major']); 3721 debugMsg('Minor : ' . $header_data['minor']); 3722 debugMsg('File count : ' . $header_data['count']); 3723 debugMsg('Uncompressed size : ' . $header_data['uncsize']); 3724 debugMsg('Compressed size : ' . $header_data['csize']); 3725 3726 $this->currentPartOffset = @ftell($this->fp); 3727 3728 $this->dataReadLength = 0; 3729 3730 return true; 3731 } 3732 3733 /** 3734 * Concrete classes must use this method to read the file header 3735 * 3736 * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive 3737 */ 3738 protected function readFileHeader() 3739 { 3740 // If the current part is over, proceed to the next part please 3741 if ($this->isEOF(true)) 3742 { 3743 debugMsg('Archive part EOF; moving to next file'); 3744 $this->nextFile(); 3745 } 3746 3747 $this->currentPartOffset = ftell($this->fp); 3748 3749 debugMsg("Reading file signature; part {$this->currentPartNumber}, offset {$this->currentPartOffset}"); 3750 // Get and decode Entity Description Block 3751 $signature = fread($this->fp, 3); 3752 3753 $this->fileHeader = new stdClass(); 3754 $this->fileHeader->timestamp = 0; 3755 3756 // Check signature 3757 if ($signature != 'JPF') 3758 { 3759 if ($this->isEOF(true)) 3760 { 3761 // This file is finished; make sure it's the last one 3762 $this->nextFile(); 3763 3764 if (!$this->isEOF(false)) 3765 { 3766 debugMsg('Invalid file signature before end of archive encountered'); 3767 $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); 3768 3769 return false; 3770 } 3771 3772 // We're just finished 3773 return false; 3774 } 3775 else 3776 { 3777 $screwed = true; 3778 3779 if (AKFactory::get('kickstart.setup.ignoreerrors', false)) 3780 { 3781 debugMsg('Invalid file block signature; launching heuristic file block signature scanner'); 3782 $screwed = !$this->heuristicFileHeaderLocator(); 3783 3784 if (!$screwed) 3785 { 3786 $signature = 'JPF'; 3787 } 3788 else 3789 { 3790 debugMsg('Heuristics failed. Brace yourself for the imminent crash.'); 3791 } 3792 } 3793 3794 if ($screwed) 3795 { 3796 debugMsg('Invalid file block signature'); 3797 // This is not a file block! The archive is corrupt. 3798 $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); 3799 3800 return false; 3801 } 3802 } 3803 } 3804 // This a JPA Entity Block. Process the header. 3805 3806 $isBannedFile = false; 3807 3808 // Read length of EDB and of the Entity Path Data 3809 $length_array = unpack('vblocksize/vpathsize', fread($this->fp, 4)); 3810 // Read the path data 3811 if ($length_array['pathsize'] > 0) 3812 { 3813 $file = fread($this->fp, $length_array['pathsize']); 3814 } 3815 else 3816 { 3817 $file = ''; 3818 } 3819 3820 // Handle file renaming 3821 $isRenamed = false; 3822 if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) 3823 { 3824 if (array_key_exists($file, $this->renameFiles)) 3825 { 3826 $file = $this->renameFiles[$file]; 3827 $isRenamed = true; 3828 } 3829 } 3830 3831 // Handle directory renaming 3832 $isDirRenamed = false; 3833 if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) 3834 { 3835 if (array_key_exists(dirname($file), $this->renameDirs)) 3836 { 3837 $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); 3838 $isRenamed = true; 3839 $isDirRenamed = true; 3840 } 3841 } 3842 3843 // Read and parse the known data portion 3844 $bin_data = fread($this->fp, 14); 3845 $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data); 3846 // Read any unknown data 3847 $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']); 3848 3849 if ($restBytes > 0) 3850 { 3851 // Start reading the extra fields 3852 while ($restBytes >= 4) 3853 { 3854 $extra_header_data = fread($this->fp, 4); 3855 $extra_header = unpack('vsignature/vlength', $extra_header_data); 3856 $restBytes -= 4; 3857 $extra_header['length'] -= 4; 3858 3859 switch ($extra_header['signature']) 3860 { 3861 case 256: 3862 // File modified timestamp 3863 if ($extra_header['length'] > 0) 3864 { 3865 $bindata = fread($this->fp, $extra_header['length']); 3866 $restBytes -= $extra_header['length']; 3867 $timestamps = unpack('Vmodified', substr($bindata, 0, 4)); 3868 $filectime = $timestamps['modified']; 3869 $this->fileHeader->timestamp = $filectime; 3870 } 3871 break; 3872 3873 default: 3874 // Unknown field 3875 if ($extra_header['length'] > 0) 3876 { 3877 $junk = fread($this->fp, $extra_header['length']); 3878 $restBytes -= $extra_header['length']; 3879 } 3880 break; 3881 } 3882 } 3883 3884 if ($restBytes > 0) 3885 { 3886 $junk = fread($this->fp, $restBytes); 3887 } 3888 } 3889 3890 $compressionType = $header_data['compression']; 3891 3892 // Populate the return array 3893 $this->fileHeader->file = $file; 3894 $this->fileHeader->compressed = $header_data['compsize']; 3895 $this->fileHeader->uncompressed = $header_data['uncompsize']; 3896 3897 switch ($header_data['type']) 3898 { 3899 case 0: 3900 $this->fileHeader->type = 'dir'; 3901 break; 3902 3903 case 1: 3904 $this->fileHeader->type = 'file'; 3905 break; 3906 3907 case 2: 3908 $this->fileHeader->type = 'link'; 3909 break; 3910 } 3911 3912 switch ($compressionType) 3913 { 3914 case 0: 3915 $this->fileHeader->compression = 'none'; 3916 break; 3917 case 1: 3918 $this->fileHeader->compression = 'gzip'; 3919 break; 3920 case 2: 3921 $this->fileHeader->compression = 'bzip2'; 3922 break; 3923 } 3924 3925 $this->fileHeader->permissions = $header_data['perms']; 3926 3927 // Find hard-coded banned files 3928 if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) 3929 { 3930 $isBannedFile = true; 3931 } 3932 3933 // Also try to find banned files passed in class configuration 3934 if ((count($this->skipFiles) > 0) && (!$isRenamed)) 3935 { 3936 if (in_array($this->fileHeader->file, $this->skipFiles)) 3937 { 3938 $isBannedFile = true; 3939 } 3940 } 3941 3942 // If we have a banned file, let's skip it 3943 if ($isBannedFile) 3944 { 3945 debugMsg('Skipping file ' . $this->fileHeader->file); 3946 // Advance the file pointer, skipping exactly the size of the compressed data 3947 $seekleft = $this->fileHeader->compressed; 3948 while ($seekleft > 0) 3949 { 3950 // Ensure that we can seek past archive part boundaries 3951 $curSize = @filesize($this->archiveList[$this->currentPartNumber]); 3952 $curPos = @ftell($this->fp); 3953 $canSeek = $curSize - $curPos; 3954 if ($canSeek > $seekleft) 3955 { 3956 $canSeek = $seekleft; 3957 } 3958 @fseek($this->fp, $canSeek, SEEK_CUR); 3959 $seekleft -= $canSeek; 3960 if ($seekleft) 3961 { 3962 $this->nextFile(); 3963 } 3964 } 3965 3966 $this->currentPartOffset = @ftell($this->fp); 3967 $this->runState = AK_STATE_DONE; 3968 3969 return true; 3970 } 3971 3972 // Remove the removePath, if any 3973 $this->fileHeader->file = $this->removePath($this->fileHeader->file); 3974 3975 // Last chance to prepend a path to the filename 3976 if (!empty($this->addPath) && !$isDirRenamed) 3977 { 3978 $this->fileHeader->file = $this->addPath . $this->fileHeader->file; 3979 } 3980 3981 // Get the translated path name 3982 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 3983 if ($this->fileHeader->type == 'file') 3984 { 3985 // Regular file; ask the postproc engine to process its filename 3986 if ($restorePerms) 3987 { 3988 $this->fileHeader->realFile = 3989 $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); 3990 } 3991 else 3992 { 3993 $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); 3994 } 3995 } 3996 elseif ($this->fileHeader->type == 'dir') 3997 { 3998 $dir = $this->fileHeader->file; 3999 4000 // Directory; just create it 4001 if ($restorePerms) 4002 { 4003 $this->postProcEngine->createDirRecursive($this->fileHeader->file, $this->fileHeader->permissions); 4004 } 4005 else 4006 { 4007 $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); 4008 } 4009 $this->postProcEngine->processFilename(null); 4010 } 4011 else 4012 { 4013 // Symlink; do not post-process 4014 $this->postProcEngine->processFilename(null); 4015 } 4016 4017 $this->createDirectory(); 4018 4019 // Header is read 4020 $this->runState = AK_STATE_HEADER; 4021 4022 $this->dataReadLength = 0; 4023 4024 return true; 4025 } 4026 4027 protected function heuristicFileHeaderLocator() 4028 { 4029 $ret = false; 4030 $fullEOF = false; 4031 4032 while (!$ret && !$fullEOF) 4033 { 4034 $this->currentPartOffset = @ftell($this->fp); 4035 4036 if ($this->isEOF(true)) 4037 { 4038 $this->nextFile(); 4039 } 4040 4041 if ($this->isEOF(false)) 4042 { 4043 $fullEOF = true; 4044 continue; 4045 } 4046 4047 // Read 512Kb 4048 $chunk = fread($this->fp, 524288); 4049 $size_read = mb_strlen($chunk, '8bit'); 4050 //$pos = strpos($chunk, 'JPF'); 4051 $pos = mb_strpos($chunk, 'JPF', 0, '8bit'); 4052 4053 if ($pos !== false) 4054 { 4055 // We found it! 4056 $this->currentPartOffset += $pos + 3; 4057 @fseek($this->fp, $this->currentPartOffset, SEEK_SET); 4058 $ret = true; 4059 } 4060 else 4061 { 4062 // Not yet found :( 4063 $this->currentPartOffset = @ftell($this->fp); 4064 } 4065 } 4066 4067 return $ret; 4068 } 4069 4070 /** 4071 * Creates the directory this file points to 4072 */ 4073 protected function createDirectory() 4074 { 4075 if (AKFactory::get('kickstart.setup.dryrun', '0')) 4076 { 4077 return true; 4078 } 4079 4080 // Do we need to create a directory? 4081 if (empty($this->fileHeader->realFile)) 4082 { 4083 $this->fileHeader->realFile = $this->fileHeader->file; 4084 } 4085 4086 $lastSlash = strrpos($this->fileHeader->realFile, '/'); 4087 $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); 4088 $perms = $this->flagRestorePermissions ? $this->fileHeader->permissions : 0755; 4089 $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); 4090 4091 if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) 4092 { 4093 $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); 4094 4095 return false; 4096 } 4097 else 4098 { 4099 return true; 4100 } 4101 } 4102 4103 /** 4104 * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when 4105 * it's finished processing the file data. 4106 * 4107 * @return bool True if processing the file data was successful, false if an error occurred 4108 */ 4109 protected function processFileData() 4110 { 4111 switch ($this->fileHeader->type) 4112 { 4113 case 'dir': 4114 return $this->processTypeDir(); 4115 break; 4116 4117 case 'link': 4118 return $this->processTypeLink(); 4119 break; 4120 4121 case 'file': 4122 switch ($this->fileHeader->compression) 4123 { 4124 case 'none': 4125 return $this->processTypeFileUncompressed(); 4126 break; 4127 4128 case 'gzip': 4129 case 'bzip2': 4130 return $this->processTypeFileCompressedSimple(); 4131 break; 4132 4133 } 4134 break; 4135 4136 default: 4137 debugMsg('Unknown file type ' . $this->fileHeader->type); 4138 break; 4139 } 4140 } 4141 4142 /** 4143 * Process the file data of a directory entry 4144 * 4145 * @return bool 4146 */ 4147 private function processTypeDir() 4148 { 4149 // Directory entries in the JPA do not have file data, therefore we're done processing the entry 4150 $this->runState = AK_STATE_DATAREAD; 4151 4152 return true; 4153 } 4154 4155 /** 4156 * Process the file data of a link entry 4157 * 4158 * @return bool 4159 */ 4160 private function processTypeLink() 4161 { 4162 $readBytes = 0; 4163 $toReadBytes = 0; 4164 $leftBytes = $this->fileHeader->compressed; 4165 $data = ''; 4166 4167 while ($leftBytes > 0) 4168 { 4169 $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; 4170 $mydata = $this->fread($this->fp, $toReadBytes); 4171 $reallyReadBytes = akstringlen($mydata); 4172 $data .= $mydata; 4173 $leftBytes -= $reallyReadBytes; 4174 4175 if ($reallyReadBytes < $toReadBytes) 4176 { 4177 // We read less than requested! Why? Did we hit local EOF? 4178 if ($this->isEOF(true) && !$this->isEOF(false)) 4179 { 4180 // Yeap. Let's go to the next file 4181 $this->nextFile(); 4182 } 4183 else 4184 { 4185 debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); 4186 // Nope. The archive is corrupt 4187 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 4188 4189 return false; 4190 } 4191 } 4192 } 4193 4194 $filename = isset($this->fileHeader->realFile) ? $this->fileHeader->realFile : $this->fileHeader->file; 4195 4196 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4197 { 4198 // Try to remove an existing file or directory by the same name 4199 if (file_exists($filename)) 4200 { 4201 @unlink($filename); 4202 @rmdir($filename); 4203 } 4204 4205 // Remove any trailing slash 4206 if (substr($filename, -1) == '/') 4207 { 4208 $filename = substr($filename, 0, -1); 4209 } 4210 // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( 4211 @symlink($data, $filename); 4212 } 4213 4214 $this->runState = AK_STATE_DATAREAD; 4215 4216 return true; // No matter if the link was created! 4217 } 4218 4219 private function processTypeFileUncompressed() 4220 { 4221 // Uncompressed files are being processed in small chunks, to avoid timeouts 4222 if (($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun', '0')) 4223 { 4224 // Before processing file data, ensure permissions are adequate 4225 $this->setCorrectPermissions($this->fileHeader->file); 4226 } 4227 4228 // Open the output file 4229 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4230 { 4231 $ignore = 4232 AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); 4233 4234 if ($this->dataReadLength == 0) 4235 { 4236 $outfp = @fopen($this->fileHeader->realFile, 'wb'); 4237 } 4238 else 4239 { 4240 $outfp = @fopen($this->fileHeader->realFile, 'ab'); 4241 } 4242 4243 // Can we write to the file? 4244 if (($outfp === false) && (!$ignore)) 4245 { 4246 // An error occurred 4247 debugMsg('Could not write to output file'); 4248 $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); 4249 4250 return false; 4251 } 4252 } 4253 4254 // Does the file have any data, at all? 4255 if ($this->fileHeader->compressed == 0) 4256 { 4257 // No file data! 4258 if (!AKFactory::get('kickstart.setup.dryrun', '0') && is_resource($outfp)) 4259 { 4260 @fclose($outfp); 4261 } 4262 4263 $this->runState = AK_STATE_DATAREAD; 4264 4265 return true; 4266 } 4267 4268 // Reference to the global timer 4269 $timer = AKFactory::getTimer(); 4270 4271 $toReadBytes = 0; 4272 $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; 4273 4274 // Loop while there's data to read and enough time to do it 4275 while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) 4276 { 4277 $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; 4278 $data = $this->fread($this->fp, $toReadBytes); 4279 $reallyReadBytes = akstringlen($data); 4280 $leftBytes -= $reallyReadBytes; 4281 $this->dataReadLength += $reallyReadBytes; 4282 4283 if ($reallyReadBytes < $toReadBytes) 4284 { 4285 // We read less than requested! Why? Did we hit local EOF? 4286 if ($this->isEOF(true) && !$this->isEOF(false)) 4287 { 4288 // Yeap. Let's go to the next file 4289 $this->nextFile(); 4290 } 4291 else 4292 { 4293 // Nope. The archive is corrupt 4294 debugMsg('Not enough data in file. The archive is truncated or corrupt.'); 4295 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 4296 4297 return false; 4298 } 4299 } 4300 4301 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4302 { 4303 if (is_resource($outfp)) 4304 { 4305 @fwrite($outfp, $data); 4306 } 4307 } 4308 } 4309 4310 // Close the file pointer 4311 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4312 { 4313 if (is_resource($outfp)) 4314 { 4315 @fclose($outfp); 4316 } 4317 } 4318 4319 // Was this a pre-timeout bail out? 4320 if ($leftBytes > 0) 4321 { 4322 $this->runState = AK_STATE_DATA; 4323 } 4324 else 4325 { 4326 // Oh! We just finished! 4327 $this->runState = AK_STATE_DATAREAD; 4328 $this->dataReadLength = 0; 4329 } 4330 4331 return true; 4332 } 4333 4334 private function processTypeFileCompressedSimple() 4335 { 4336 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4337 { 4338 // Before processing file data, ensure permissions are adequate 4339 $this->setCorrectPermissions($this->fileHeader->file); 4340 4341 // Open the output file 4342 $outfp = @fopen($this->fileHeader->realFile, 'wb'); 4343 4344 // Can we write to the file? 4345 $ignore = 4346 AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); 4347 4348 if (($outfp === false) && (!$ignore)) 4349 { 4350 // An error occurred 4351 debugMsg('Could not write to output file'); 4352 $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); 4353 4354 return false; 4355 } 4356 } 4357 4358 // Does the file have any data, at all? 4359 if ($this->fileHeader->compressed == 0) 4360 { 4361 // No file data! 4362 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 4363 { 4364 if (is_resource($outfp)) 4365 { 4366 @fclose($outfp); 4367 } 4368 } 4369 $this->runState = AK_STATE_DATAREAD; 4370 4371 return true; 4372 } 4373 4374 // Simple compressed files are processed as a whole; we can't do chunk processing 4375 $zipData = $this->fread($this->fp, $this->fileHeader->compressed); 4376 while (akstringlen($zipData) < $this->fileHeader->compressed) 4377 { 4378 // End of local file before reading all data, but have more archive parts? 4379 if ($this->isEOF(true) && !$this->isEOF(false)) 4380 { 4381 // Yeap. Read from the next file 4382 $this->nextFile(); 4383 $bytes_left = $this->fileHeader->compressed - akstringlen($zipData); 4384 $zipData .= $this->fread($this->fp, $bytes_left); 4385 } 4386 else 4387 { 4388 debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); 4389 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 4390 4391 return false; 4392 } 4393 } 4394 4395 if ($this->fileHeader->compression == 'gzip') 4396 { 4397 $unzipData = gzinflate($zipData); 4398 } 4399 elseif ($this->fileHeader->compression == 'bzip2') 4400 { 4401 $unzipData = bzdecompress($zipData); 4402 } 4403 unset($zipData); 4404 4405 // Write to the file. 4406 if (!AKFactory::get('kickstart.setup.dryrun', '0') && is_resource($outfp)) 4407 { 4408 @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); 4409 @fclose($outfp); 4410 } 4411 unset($unzipData); 4412 4413 $this->runState = AK_STATE_DATAREAD; 4414 4415 return true; 4416 } 4417} 4418 4419/** 4420 * Akeeba Restore 4421 * A JSON-powered JPA, JPS and ZIP archive extraction library 4422 * 4423 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 4424 * @license GNU GPL v2 or - at your option - any later version 4425 * @package akeebabackup 4426 * @subpackage kickstart 4427 */ 4428 4429/** 4430 * ZIP archive extraction class 4431 * 4432 * Since the file data portion of ZIP and JPA are similarly structured (it's empty for dirs, 4433 * linked node name for symlinks, dumped binary data for no compressions and dumped gzipped 4434 * binary data for gzip compression) we just have to subclass AKUnarchiverJPA and change the 4435 * header reading bits. Reusable code ;) 4436 */ 4437class AKUnarchiverZIP extends AKUnarchiverJPA 4438{ 4439 var $expectDataDescriptor = false; 4440 4441 protected function readArchiveHeader() 4442 { 4443 debugMsg('Preparing to read archive header'); 4444 // Initialize header data array 4445 $this->archiveHeaderData = new stdClass(); 4446 4447 // Open the first part 4448 debugMsg('Opening the first part'); 4449 $this->nextFile(); 4450 4451 // Fail for unreadable files 4452 if ($this->fp === false) 4453 { 4454 debugMsg('The first part is not readable'); 4455 4456 return false; 4457 } 4458 4459 // Read a possible multipart signature 4460 $sigBinary = fread($this->fp, 4); 4461 $headerData = unpack('Vsig', $sigBinary); 4462 4463 // Roll back if it's not a multipart archive 4464 if ($headerData['sig'] == 0x04034b50) 4465 { 4466 debugMsg('The archive is not multipart'); 4467 fseek($this->fp, -4, SEEK_CUR); 4468 } 4469 else 4470 { 4471 debugMsg('The archive is multipart'); 4472 } 4473 4474 $multiPartSigs = array( 4475 0x08074b50, // Multi-part ZIP 4476 0x30304b50, // Multi-part ZIP (alternate) 4477 0x04034b50 // Single file 4478 ); 4479 if (!in_array($headerData['sig'], $multiPartSigs)) 4480 { 4481 debugMsg('Invalid header signature ' . dechex($headerData['sig'])); 4482 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 4483 4484 return false; 4485 } 4486 4487 $this->currentPartOffset = @ftell($this->fp); 4488 debugMsg('Current part offset after reading header: ' . $this->currentPartOffset); 4489 4490 $this->dataReadLength = 0; 4491 4492 return true; 4493 } 4494 4495 /** 4496 * Concrete classes must use this method to read the file header 4497 * 4498 * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive 4499 */ 4500 protected function readFileHeader() 4501 { 4502 // If the current part is over, proceed to the next part please 4503 if ($this->isEOF(true)) 4504 { 4505 debugMsg('Opening next archive part'); 4506 $this->nextFile(); 4507 } 4508 4509 $this->currentPartOffset = ftell($this->fp); 4510 4511 if ($this->expectDataDescriptor) 4512 { 4513 // The last file had bit 3 of the general purpose bit flag set. This means that we have a 4514 // 12 byte data descriptor we need to skip. To make things worse, there might also be a 4 4515 // byte optional data descriptor header (0x08074b50). 4516 $junk = @fread($this->fp, 4); 4517 $junk = unpack('Vsig', $junk); 4518 if ($junk['sig'] == 0x08074b50) 4519 { 4520 // Yes, there was a signature 4521 $junk = @fread($this->fp, 12); 4522 debugMsg('Data descriptor (w/ header) skipped at ' . (ftell($this->fp) - 12)); 4523 } 4524 else 4525 { 4526 // No, there was no signature, just read another 8 bytes 4527 $junk = @fread($this->fp, 8); 4528 debugMsg('Data descriptor (w/out header) skipped at ' . (ftell($this->fp) - 8)); 4529 } 4530 4531 // And check for EOF, too 4532 if ($this->isEOF(true)) 4533 { 4534 debugMsg('EOF before reading header'); 4535 4536 $this->nextFile(); 4537 } 4538 } 4539 4540 // Get and decode Local File Header 4541 $headerBinary = fread($this->fp, 30); 4542 $headerData = 4543 unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary); 4544 4545 // Check signature 4546 if (!($headerData['sig'] == 0x04034b50)) 4547 { 4548 debugMsg('Not a file signature at ' . (ftell($this->fp) - 4)); 4549 4550 // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? 4551 if ($headerData['sig'] == 0x02014b50) 4552 { 4553 debugMsg('EOCD signature at ' . (ftell($this->fp) - 4)); 4554 // End of ZIP file detected. We'll just skip to the end of file... 4555 while ($this->nextFile()) 4556 { 4557 }; 4558 @fseek($this->fp, 0, SEEK_END); // Go to EOF 4559 return false; 4560 } 4561 else 4562 { 4563 debugMsg('Invalid signature ' . dechex($headerData['sig']) . ' at ' . ftell($this->fp)); 4564 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 4565 4566 return false; 4567 } 4568 } 4569 4570 // If bit 3 of the bitflag is set, expectDataDescriptor is true 4571 $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; 4572 4573 $this->fileHeader = new stdClass(); 4574 $this->fileHeader->timestamp = 0; 4575 4576 // Read the last modified data and time 4577 $lastmodtime = $headerData['lastmodtime']; 4578 $lastmoddate = $headerData['lastmoddate']; 4579 4580 if ($lastmoddate && $lastmodtime) 4581 { 4582 // ----- Extract time 4583 $v_hour = ($lastmodtime & 0xF800) >> 11; 4584 $v_minute = ($lastmodtime & 0x07E0) >> 5; 4585 $v_seconde = ($lastmodtime & 0x001F) * 2; 4586 4587 // ----- Extract date 4588 $v_year = (($lastmoddate & 0xFE00) >> 9) + 1980; 4589 $v_month = ($lastmoddate & 0x01E0) >> 5; 4590 $v_day = $lastmoddate & 0x001F; 4591 4592 // ----- Get UNIX date format 4593 $this->fileHeader->timestamp = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); 4594 } 4595 4596 $isBannedFile = false; 4597 4598 $this->fileHeader->compressed = $headerData['compsize']; 4599 $this->fileHeader->uncompressed = $headerData['uncomp']; 4600 $nameFieldLength = $headerData['fnamelen']; 4601 $extraFieldLength = $headerData['eflen']; 4602 4603 // Read filename field 4604 $this->fileHeader->file = fread($this->fp, $nameFieldLength); 4605 4606 // Handle file renaming 4607 $isRenamed = false; 4608 if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) 4609 { 4610 if (array_key_exists($this->fileHeader->file, $this->renameFiles)) 4611 { 4612 $this->fileHeader->file = $this->renameFiles[$this->fileHeader->file]; 4613 $isRenamed = true; 4614 } 4615 } 4616 4617 // Handle directory renaming 4618 $isDirRenamed = false; 4619 if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) 4620 { 4621 if (array_key_exists(dirname($this->fileHeader->file), $this->renameDirs)) 4622 { 4623 $file = 4624 rtrim($this->renameDirs[dirname($this->fileHeader->file)], '/') . '/' . basename($this->fileHeader->file); 4625 $isRenamed = true; 4626 $isDirRenamed = true; 4627 } 4628 } 4629 4630 // Read extra field if present 4631 if ($extraFieldLength > 0) 4632 { 4633 $extrafield = fread($this->fp, $extraFieldLength); 4634 } 4635 4636 debugMsg('*' . ftell($this->fp) . ' IS START OF ' . $this->fileHeader->file . ' (' . $this->fileHeader->compressed . ' bytes)'); 4637 4638 4639 // Decide filetype -- Check for directories 4640 $this->fileHeader->type = 'file'; 4641 if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) 4642 { 4643 $this->fileHeader->type = 'dir'; 4644 } 4645 // Decide filetype -- Check for symbolic links 4646 if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) 4647 { 4648 $this->fileHeader->type = 'link'; 4649 } 4650 4651 switch ($headerData['compmethod']) 4652 { 4653 case 0: 4654 $this->fileHeader->compression = 'none'; 4655 break; 4656 case 8: 4657 $this->fileHeader->compression = 'gzip'; 4658 break; 4659 } 4660 4661 // Find hard-coded banned files 4662 if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) 4663 { 4664 $isBannedFile = true; 4665 } 4666 4667 // Also try to find banned files passed in class configuration 4668 if ((count($this->skipFiles) > 0) && (!$isRenamed)) 4669 { 4670 if (in_array($this->fileHeader->file, $this->skipFiles)) 4671 { 4672 $isBannedFile = true; 4673 } 4674 } 4675 4676 // If we have a banned file, let's skip it 4677 if ($isBannedFile) 4678 { 4679 // Advance the file pointer, skipping exactly the size of the compressed data 4680 $seekleft = $this->fileHeader->compressed; 4681 while ($seekleft > 0) 4682 { 4683 // Ensure that we can seek past archive part boundaries 4684 $curSize = @filesize($this->archiveList[$this->currentPartNumber]); 4685 $curPos = @ftell($this->fp); 4686 $canSeek = $curSize - $curPos; 4687 if ($canSeek > $seekleft) 4688 { 4689 $canSeek = $seekleft; 4690 } 4691 @fseek($this->fp, $canSeek, SEEK_CUR); 4692 $seekleft -= $canSeek; 4693 if ($seekleft) 4694 { 4695 $this->nextFile(); 4696 } 4697 } 4698 4699 $this->currentPartOffset = @ftell($this->fp); 4700 $this->runState = AK_STATE_DONE; 4701 4702 return true; 4703 } 4704 4705 // Remove the removePath, if any 4706 $this->fileHeader->file = $this->removePath($this->fileHeader->file); 4707 4708 // Last chance to prepend a path to the filename 4709 if (!empty($this->addPath) && !$isDirRenamed) 4710 { 4711 $this->fileHeader->file = $this->addPath . $this->fileHeader->file; 4712 } 4713 4714 // Get the translated path name 4715 if ($this->fileHeader->type == 'file') 4716 { 4717 $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); 4718 } 4719 elseif ($this->fileHeader->type == 'dir') 4720 { 4721 $this->fileHeader->timestamp = 0; 4722 4723 $dir = $this->fileHeader->file; 4724 4725 $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); 4726 $this->postProcEngine->processFilename(null); 4727 } 4728 else 4729 { 4730 // Symlink; do not post-process 4731 $this->fileHeader->timestamp = 0; 4732 $this->postProcEngine->processFilename(null); 4733 } 4734 4735 $this->createDirectory(); 4736 4737 // Header is read 4738 $this->runState = AK_STATE_HEADER; 4739 4740 return true; 4741 } 4742 4743} 4744 4745/** 4746 * Akeeba Restore 4747 * A JSON-powered JPA, JPS and ZIP archive extraction library 4748 * 4749 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 4750 * @license GNU GPL v2 or - at your option - any later version 4751 * @package akeebabackup 4752 * @subpackage kickstart 4753 */ 4754 4755/** 4756 * JPS archive extraction class 4757 */ 4758class AKUnarchiverJPS extends AKUnarchiverJPA 4759{ 4760 /** 4761 * Header data for the archive 4762 * 4763 * @var array 4764 */ 4765 protected $archiveHeaderData = array(); 4766 4767 /** 4768 * Plaintext password from which the encryption key will be derived with PBKDF2 4769 * 4770 * @var string 4771 */ 4772 protected $password = ''; 4773 4774 /** 4775 * Which hash algorithm should I use for key derivation with PBKDF2. 4776 * 4777 * @var string 4778 */ 4779 private $pbkdf2Algorithm = 'sha1'; 4780 4781 /** 4782 * How many iterations should I use for key derivation with PBKDF2 4783 * 4784 * @var int 4785 */ 4786 private $pbkdf2Iterations = 1000; 4787 4788 /** 4789 * Should I use a static salt for key derivation with PBKDF2? 4790 * 4791 * @var bool 4792 */ 4793 private $pbkdf2UseStaticSalt = 0; 4794 4795 /** 4796 * Static salt for key derivation with PBKDF2 4797 * 4798 * @var string 4799 */ 4800 private $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 4801 4802 /** 4803 * How much compressed data I have read since the last file header read 4804 * 4805 * @var int 4806 */ 4807 private $compressedSizeReadSinceLastFileHeader = 0; 4808 4809 public function __construct() 4810 { 4811 parent::__construct(); 4812 4813 $this->password = AKFactory::get('kickstart.jps.password', ''); 4814 } 4815 4816 public function __wakeup() 4817 { 4818 parent::__wakeup(); 4819 4820 // Make sure the decryption is all set up (required!) 4821 AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); 4822 AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); 4823 AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); 4824 AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); 4825 } 4826 4827 4828 protected function readArchiveHeader() 4829 { 4830 // Initialize header data array 4831 $this->archiveHeaderData = new stdClass(); 4832 4833 // Open the first part 4834 $this->nextFile(); 4835 4836 // Fail for unreadable files 4837 if ($this->fp === false) 4838 { 4839 return false; 4840 } 4841 4842 // Read the signature 4843 $sig = fread($this->fp, 3); 4844 4845 if ($sig != 'JPS') 4846 { 4847 // Not a JPA file 4848 $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); 4849 4850 return false; 4851 } 4852 4853 // Read and parse the known portion of header data (5 bytes) 4854 $bin_data = fread($this->fp, 5); 4855 $header_data = unpack('Cmajor/Cminor/cspanned/vextra', $bin_data); 4856 4857 // Is this a v2 archive? 4858 $versionHumanReadable = $header_data['major'] . '.' . $header_data['minor']; 4859 $isV2Archive = version_compare($versionHumanReadable, '2.0', 'ge'); 4860 4861 // Load any remaining header data 4862 $rest_length = $header_data['extra']; 4863 4864 if ($isV2Archive && $rest_length) 4865 { 4866 // V2 archives only have one kind of extra header 4867 if (!$this->readKeyExpansionExtraHeader()) 4868 { 4869 return false; 4870 } 4871 } 4872 elseif ($rest_length > 0) 4873 { 4874 $junk = fread($this->fp, $rest_length); 4875 } 4876 4877 // Temporary array with all the data we read 4878 $temp = array( 4879 'signature' => $sig, 4880 'major' => $header_data['major'], 4881 'minor' => $header_data['minor'], 4882 'spanned' => $header_data['spanned'] 4883 ); 4884 // Array-to-object conversion 4885 foreach ($temp as $key => $value) 4886 { 4887 $this->archiveHeaderData->{$key} = $value; 4888 } 4889 4890 $this->currentPartOffset = @ftell($this->fp); 4891 4892 $this->dataReadLength = 0; 4893 4894 return true; 4895 } 4896 4897 /** 4898 * Concrete classes must use this method to read the file header 4899 * 4900 * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive 4901 */ 4902 protected function readFileHeader() 4903 { 4904 // If the current part is over, proceed to the next part please 4905 if ($this->isEOF(true)) 4906 { 4907 $this->nextFile(); 4908 } 4909 4910 $this->currentPartOffset = ftell($this->fp); 4911 4912 // Get and decode Entity Description Block 4913 $signature = fread($this->fp, 3); 4914 4915 // Check for end-of-archive siganture 4916 if ($signature == 'JPE') 4917 { 4918 $this->setState('postrun'); 4919 4920 return true; 4921 } 4922 4923 $this->fileHeader = new stdClass(); 4924 $this->fileHeader->timestamp = 0; 4925 4926 // Check signature 4927 if ($signature != 'JPF') 4928 { 4929 if ($this->isEOF(true)) 4930 { 4931 // This file is finished; make sure it's the last one 4932 $this->nextFile(); 4933 if (!$this->isEOF(false)) 4934 { 4935 $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); 4936 4937 return false; 4938 } 4939 4940 // We're just finished 4941 return false; 4942 } 4943 else 4944 { 4945 fseek($this->fp, -6, SEEK_CUR); 4946 $signature = fread($this->fp, 3); 4947 if ($signature == 'JPE') 4948 { 4949 return false; 4950 } 4951 4952 $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); 4953 4954 return false; 4955 } 4956 } 4957 4958 // This a JPS Entity Block. Process the header. 4959 4960 $isBannedFile = false; 4961 4962 // Make sure the decryption is all set up 4963 AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); 4964 AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); 4965 AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); 4966 AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); 4967 4968 // Read and decrypt the header 4969 $edbhData = fread($this->fp, 4); 4970 $edbh = unpack('vencsize/vdecsize', $edbhData); 4971 $bin_data = fread($this->fp, $edbh['encsize']); 4972 4973 // Add the header length to the data read 4974 $this->compressedSizeReadSinceLastFileHeader += $edbh['encsize'] + 4; 4975 4976 // Decrypt and truncate 4977 $bin_data = AKEncryptionAES::AESDecryptCBC($bin_data, $this->password); 4978 $bin_data = substr($bin_data, 0, $edbh['decsize']); 4979 4980 // Read length of EDB and of the Entity Path Data 4981 $length_array = unpack('vpathsize', substr($bin_data, 0, 2)); 4982 // Read the path data 4983 $file = substr($bin_data, 2, $length_array['pathsize']); 4984 4985 // Handle file renaming 4986 $isRenamed = false; 4987 if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) 4988 { 4989 if (array_key_exists($file, $this->renameFiles)) 4990 { 4991 $file = $this->renameFiles[$file]; 4992 $isRenamed = true; 4993 } 4994 } 4995 4996 // Handle directory renaming 4997 $isDirRenamed = false; 4998 if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) 4999 { 5000 if (array_key_exists(dirname($file), $this->renameDirs)) 5001 { 5002 $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); 5003 $isRenamed = true; 5004 $isDirRenamed = true; 5005 } 5006 } 5007 5008 // Read and parse the known data portion 5009 $bin_data = substr($bin_data, 2 + $length_array['pathsize']); 5010 $header_data = unpack('Ctype/Ccompression/Vuncompsize/Vperms/Vfilectime', $bin_data); 5011 5012 $this->fileHeader->timestamp = $header_data['filectime']; 5013 $compressionType = $header_data['compression']; 5014 5015 // Populate the return array 5016 $this->fileHeader->file = $file; 5017 $this->fileHeader->uncompressed = $header_data['uncompsize']; 5018 switch ($header_data['type']) 5019 { 5020 case 0: 5021 $this->fileHeader->type = 'dir'; 5022 break; 5023 5024 case 1: 5025 $this->fileHeader->type = 'file'; 5026 break; 5027 5028 case 2: 5029 $this->fileHeader->type = 'link'; 5030 break; 5031 } 5032 switch ($compressionType) 5033 { 5034 case 0: 5035 $this->fileHeader->compression = 'none'; 5036 break; 5037 case 1: 5038 $this->fileHeader->compression = 'gzip'; 5039 break; 5040 case 2: 5041 $this->fileHeader->compression = 'bzip2'; 5042 break; 5043 } 5044 $this->fileHeader->permissions = $header_data['perms']; 5045 5046 // Find hard-coded banned files 5047 if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) 5048 { 5049 $isBannedFile = true; 5050 } 5051 5052 // Also try to find banned files passed in class configuration 5053 if ((count($this->skipFiles) > 0) && (!$isRenamed)) 5054 { 5055 if (in_array($this->fileHeader->file, $this->skipFiles)) 5056 { 5057 $isBannedFile = true; 5058 } 5059 } 5060 5061 // If we have a banned file, let's skip it 5062 if ($isBannedFile) 5063 { 5064 $done = false; 5065 while (!$done) 5066 { 5067 // Read the Data Chunk Block header 5068 $binMiniHead = fread($this->fp, 8); 5069 if (in_array(substr($binMiniHead, 0, 3), array('JPF', 'JPE'))) 5070 { 5071 // Not a Data Chunk Block header, I am done skipping the file 5072 @fseek($this->fp, -8, SEEK_CUR); // Roll back the file pointer 5073 $done = true; // Mark as done 5074 continue; // Exit loop 5075 } 5076 else 5077 { 5078 // Skip forward by the amount of compressed data 5079 $miniHead = unpack('Vencsize/Vdecsize', $binMiniHead); 5080 @fseek($this->fp, $miniHead['encsize'], SEEK_CUR); 5081 $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHead['encsize']; 5082 } 5083 } 5084 5085 $this->currentPartOffset = @ftell($this->fp); 5086 $this->runState = AK_STATE_DONE; 5087 $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; 5088 $this->compressedSizeReadSinceLastFileHeader = 0; 5089 5090 return true; 5091 } 5092 5093 // Remove the removePath, if any 5094 $this->fileHeader->file = $this->removePath($this->fileHeader->file); 5095 5096 // Last chance to prepend a path to the filename 5097 if (!empty($this->addPath) && !$isDirRenamed) 5098 { 5099 $this->fileHeader->file = $this->addPath . $this->fileHeader->file; 5100 } 5101 5102 // Get the translated path name 5103 $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); 5104 if ($this->fileHeader->type == 'file') 5105 { 5106 // Regular file; ask the postproc engine to process its filename 5107 if ($restorePerms) 5108 { 5109 $this->fileHeader->realFile = 5110 $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); 5111 } 5112 else 5113 { 5114 $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); 5115 } 5116 } 5117 elseif ($this->fileHeader->type == 'dir') 5118 { 5119 $dir = $this->fileHeader->file; 5120 $this->fileHeader->realFile = $dir; 5121 5122 // Directory; just create it 5123 if ($restorePerms) 5124 { 5125 $this->postProcEngine->createDirRecursive($this->fileHeader->file, $this->fileHeader->permissions); 5126 } 5127 else 5128 { 5129 $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); 5130 } 5131 $this->postProcEngine->processFilename(null); 5132 } 5133 else 5134 { 5135 // Symlink; do not post-process 5136 $this->postProcEngine->processFilename(null); 5137 } 5138 5139 $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; 5140 $this->compressedSizeReadSinceLastFileHeader = 0; 5141 5142 $this->createDirectory(); 5143 5144 // Header is read 5145 $this->runState = AK_STATE_HEADER; 5146 5147 $this->dataReadLength = 0; 5148 5149 return true; 5150 } 5151 5152 /** 5153 * Creates the directory this file points to 5154 */ 5155 protected function createDirectory() 5156 { 5157 if (AKFactory::get('kickstart.setup.dryrun', '0')) 5158 { 5159 return true; 5160 } 5161 5162 // Do we need to create a directory? 5163 $lastSlash = strrpos($this->fileHeader->realFile, '/'); 5164 $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); 5165 $perms = 0755; 5166 $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); 5167 5168 if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) 5169 { 5170 $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); 5171 5172 return false; 5173 } 5174 5175 return true; 5176 } 5177 5178 /** 5179 * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when 5180 * it's finished processing the file data. 5181 * 5182 * @return bool True if processing the file data was successful, false if an error occurred 5183 */ 5184 protected function processFileData() 5185 { 5186 switch ($this->fileHeader->type) 5187 { 5188 case 'dir': 5189 return $this->processTypeDir(); 5190 break; 5191 5192 case 'link': 5193 return $this->processTypeLink(); 5194 break; 5195 5196 case 'file': 5197 switch ($this->fileHeader->compression) 5198 { 5199 case 'none': 5200 return $this->processTypeFileUncompressed(); 5201 break; 5202 5203 case 'gzip': 5204 case 'bzip2': 5205 return $this->processTypeFileCompressedSimple(); 5206 break; 5207 5208 } 5209 break; 5210 } 5211 } 5212 5213 /** 5214 * Process the file data of a directory entry 5215 * 5216 * @return bool 5217 */ 5218 private function processTypeDir() 5219 { 5220 // Directory entries in the JPA do not have file data, therefore we're done processing the entry 5221 $this->runState = AK_STATE_DATAREAD; 5222 5223 return true; 5224 } 5225 5226 /** 5227 * Process the file data of a link entry 5228 * 5229 * @return bool 5230 */ 5231 private function processTypeLink() 5232 { 5233 5234 // Does the file have any data, at all? 5235 if ($this->fileHeader->uncompressed == 0) 5236 { 5237 // No file data! 5238 $this->runState = AK_STATE_DATAREAD; 5239 5240 return true; 5241 } 5242 5243 // Read the mini header 5244 $binMiniHeader = fread($this->fp, 8); 5245 $reallyReadBytes = akstringlen($binMiniHeader); 5246 5247 if ($reallyReadBytes < 8) 5248 { 5249 // We read less than requested! Why? Did we hit local EOF? 5250 if ($this->isEOF(true) && !$this->isEOF(false)) 5251 { 5252 // Yeap. Let's go to the next file 5253 $this->nextFile(); 5254 // Retry reading the header 5255 $binMiniHeader = fread($this->fp, 8); 5256 $reallyReadBytes = akstringlen($binMiniHeader); 5257 // Still not enough data? If so, the archive is corrupt or missing parts. 5258 if ($reallyReadBytes < 8) 5259 { 5260 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5261 5262 return false; 5263 } 5264 } 5265 else 5266 { 5267 // Nope. The archive is corrupt 5268 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5269 5270 return false; 5271 } 5272 } 5273 5274 // Read the encrypted data 5275 $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); 5276 $toReadBytes = $miniHeader['encsize']; 5277 $data = $this->fread($this->fp, $toReadBytes); 5278 $reallyReadBytes = akstringlen($data); 5279 $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHeader['encsize']; 5280 5281 if ($reallyReadBytes < $toReadBytes) 5282 { 5283 // We read less than requested! Why? Did we hit local EOF? 5284 if ($this->isEOF(true) && !$this->isEOF(false)) 5285 { 5286 // Yeap. Let's go to the next file 5287 $this->nextFile(); 5288 // Read the rest of the data 5289 $toReadBytes -= $reallyReadBytes; 5290 $restData = $this->fread($this->fp, $toReadBytes); 5291 $reallyReadBytes = akstringlen($data); 5292 if ($reallyReadBytes < $toReadBytes) 5293 { 5294 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5295 5296 return false; 5297 } 5298 $data .= $restData; 5299 } 5300 else 5301 { 5302 // Nope. The archive is corrupt 5303 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5304 5305 return false; 5306 } 5307 } 5308 5309 // Decrypt the data 5310 $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); 5311 5312 // Is the length of the decrypted data less than expected? 5313 $data_length = akstringlen($data); 5314 if ($data_length < $miniHeader['decsize']) 5315 { 5316 $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); 5317 5318 return false; 5319 } 5320 5321 // Trim the data 5322 $data = substr($data, 0, $miniHeader['decsize']); 5323 5324 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5325 { 5326 // Try to remove an existing file or directory by the same name 5327 if (file_exists($this->fileHeader->file)) 5328 { 5329 @unlink($this->fileHeader->file); 5330 @rmdir($this->fileHeader->file); 5331 } 5332 // Remove any trailing slash 5333 if (substr($this->fileHeader->file, -1) == '/') 5334 { 5335 $this->fileHeader->file = substr($this->fileHeader->file, 0, -1); 5336 } 5337 // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( 5338 @symlink($data, $this->fileHeader->file); 5339 } 5340 5341 $this->runState = AK_STATE_DATAREAD; 5342 5343 return true; // No matter if the link was created! 5344 } 5345 5346 private function processTypeFileUncompressed() 5347 { 5348 // Uncompressed files are being processed in small chunks, to avoid timeouts 5349 if (($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun', '0')) 5350 { 5351 // Before processing file data, ensure permissions are adequate 5352 $this->setCorrectPermissions($this->fileHeader->file); 5353 } 5354 5355 // Open the output file 5356 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5357 { 5358 $ignore = 5359 AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); 5360 if ($this->dataReadLength == 0) 5361 { 5362 $outfp = @fopen($this->fileHeader->realFile, 'wb'); 5363 } 5364 else 5365 { 5366 $outfp = @fopen($this->fileHeader->realFile, 'ab'); 5367 } 5368 5369 // Can we write to the file? 5370 if (($outfp === false) && (!$ignore)) 5371 { 5372 // An error occurred 5373 $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); 5374 5375 return false; 5376 } 5377 } 5378 5379 // Does the file have any data, at all? 5380 if ($this->fileHeader->uncompressed == 0) 5381 { 5382 // No file data! 5383 if (!AKFactory::get('kickstart.setup.dryrun', '0') && is_resource($outfp)) 5384 { 5385 @fclose($outfp); 5386 } 5387 $this->runState = AK_STATE_DATAREAD; 5388 5389 return true; 5390 } 5391 5392 $this->setError('An uncompressed file was detected; this is not supported by this archive extraction utility'); 5393 5394 return false; 5395 } 5396 5397 private function processTypeFileCompressedSimple() 5398 { 5399 $timer = AKFactory::getTimer(); 5400 5401 // Files are being processed in small chunks, to avoid timeouts 5402 if (($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun', '0')) 5403 { 5404 // Before processing file data, ensure permissions are adequate 5405 $this->setCorrectPermissions($this->fileHeader->file); 5406 } 5407 5408 // Open the output file 5409 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5410 { 5411 // Open the output file 5412 $outfp = @fopen($this->fileHeader->realFile, 'wb'); 5413 5414 // Can we write to the file? 5415 $ignore = 5416 AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); 5417 if (($outfp === false) && (!$ignore)) 5418 { 5419 // An error occurred 5420 $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); 5421 5422 return false; 5423 } 5424 } 5425 5426 // Does the file have any data, at all? 5427 if ($this->fileHeader->uncompressed == 0) 5428 { 5429 // No file data! 5430 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5431 { 5432 if (is_resource($outfp)) 5433 { 5434 @fclose($outfp); 5435 } 5436 } 5437 $this->runState = AK_STATE_DATAREAD; 5438 5439 return true; 5440 } 5441 5442 $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; 5443 5444 // Loop while there's data to write and enough time to do it 5445 while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) 5446 { 5447 // Read the mini header 5448 $binMiniHeader = fread($this->fp, 8); 5449 $reallyReadBytes = akstringlen($binMiniHeader); 5450 if ($reallyReadBytes < 8) 5451 { 5452 // We read less than requested! Why? Did we hit local EOF? 5453 if ($this->isEOF(true) && !$this->isEOF(false)) 5454 { 5455 // Yeap. Let's go to the next file 5456 $this->nextFile(); 5457 // Retry reading the header 5458 $binMiniHeader = fread($this->fp, 8); 5459 $reallyReadBytes = akstringlen($binMiniHeader); 5460 // Still not enough data? If so, the archive is corrupt or missing parts. 5461 if ($reallyReadBytes < 8) 5462 { 5463 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5464 5465 return false; 5466 } 5467 } 5468 else 5469 { 5470 // Nope. The archive is corrupt 5471 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5472 5473 return false; 5474 } 5475 } 5476 5477 // Read the encrypted data 5478 $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); 5479 $toReadBytes = $miniHeader['encsize']; 5480 $data = $this->fread($this->fp, $toReadBytes); 5481 $reallyReadBytes = akstringlen($data); 5482 5483 $this->compressedSizeReadSinceLastFileHeader += $miniHeader['encsize'] + 8; 5484 5485 if ($reallyReadBytes < $toReadBytes) 5486 { 5487 // We read less than requested! Why? Did we hit local EOF? 5488 if ($this->isEOF(true) && !$this->isEOF(false)) 5489 { 5490 // Yeap. Let's go to the next file 5491 $this->nextFile(); 5492 // Read the rest of the data 5493 $toReadBytes -= $reallyReadBytes; 5494 $restData = $this->fread($this->fp, $toReadBytes); 5495 $reallyReadBytes = akstringlen($restData); 5496 if ($reallyReadBytes < $toReadBytes) 5497 { 5498 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5499 5500 return false; 5501 } 5502 if (akstringlen($data) == 0) 5503 { 5504 $data = $restData; 5505 } 5506 else 5507 { 5508 $data .= $restData; 5509 } 5510 } 5511 else 5512 { 5513 // Nope. The archive is corrupt 5514 $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); 5515 5516 return false; 5517 } 5518 } 5519 5520 // Decrypt the data 5521 $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); 5522 5523 // Is the length of the decrypted data less than expected? 5524 $data_length = akstringlen($data); 5525 if ($data_length < $miniHeader['decsize']) 5526 { 5527 $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); 5528 5529 return false; 5530 } 5531 5532 // Trim the data 5533 $data = substr($data, 0, $miniHeader['decsize']); 5534 5535 // Decompress 5536 $data = gzinflate($data); 5537 $unc_len = akstringlen($data); 5538 5539 // Write the decrypted data 5540 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5541 { 5542 if (is_resource($outfp)) 5543 { 5544 @fwrite($outfp, $data, akstringlen($data)); 5545 } 5546 } 5547 5548 // Update the read length 5549 $this->dataReadLength += $unc_len; 5550 $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; 5551 } 5552 5553 // Close the file pointer 5554 if (!AKFactory::get('kickstart.setup.dryrun', '0')) 5555 { 5556 if (is_resource($outfp)) 5557 { 5558 @fclose($outfp); 5559 } 5560 } 5561 5562 // Was this a pre-timeout bail out? 5563 if ($leftBytes > 0) 5564 { 5565 $this->runState = AK_STATE_DATA; 5566 } 5567 else 5568 { 5569 // Oh! We just finished! 5570 $this->runState = AK_STATE_DATAREAD; 5571 $this->dataReadLength = 0; 5572 } 5573 5574 return true; 5575 } 5576 5577 private function readKeyExpansionExtraHeader() 5578 { 5579 $signature = fread($this->fp, 4); 5580 5581 if ($signature != "JH\x00\x01") 5582 { 5583 // Not a valid JPS file 5584 $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); 5585 5586 return false; 5587 } 5588 5589 $bin_data = fread($this->fp, 8); 5590 $header_data = unpack('vlength/Calgo/Viterations/CuseStaticSalt', $bin_data); 5591 5592 if ($header_data['length'] != 76) 5593 { 5594 // Not a valid JPS file 5595 $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); 5596 5597 return false; 5598 } 5599 5600 switch ($header_data['algo']) 5601 { 5602 case 0: 5603 $algorithm = 'sha1'; 5604 break; 5605 5606 case 1: 5607 $algorithm = 'sha256'; 5608 break; 5609 5610 case 2: 5611 $algorithm = 'sha512'; 5612 break; 5613 5614 default: 5615 // Not a valid JPS file 5616 $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); 5617 5618 return false; 5619 break; 5620 } 5621 5622 $this->pbkdf2Algorithm = $algorithm; 5623 $this->pbkdf2Iterations = $header_data['iterations']; 5624 $this->pbkdf2UseStaticSalt = $header_data['useStaticSalt']; 5625 $this->pbkdf2StaticSalt = fread($this->fp, 64); 5626 5627 return true; 5628 } 5629} 5630 5631/** 5632 * Akeeba Restore 5633 * A JSON-powered JPA, JPS and ZIP archive extraction library 5634 * 5635 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 5636 * @license GNU GPL v2 or - at your option - any later version 5637 * @package akeebabackup 5638 * @subpackage kickstart 5639 */ 5640 5641/** 5642 * Timer class 5643 */ 5644class AKCoreTimer extends AKAbstractObject 5645{ 5646 /** @var int Maximum execution time allowance per step */ 5647 private $max_exec_time = null; 5648 5649 /** @var int Timestamp of execution start */ 5650 private $start_time = null; 5651 5652 /** 5653 * Public constructor, creates the timer object and calculates the execution time limits 5654 * 5655 * @return AECoreTimer 5656 */ 5657 public function __construct() 5658 { 5659 parent::__construct(); 5660 5661 // Initialize start time 5662 $this->start_time = $this->microtime_float(); 5663 5664 // Get configured max time per step and bias 5665 $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14); 5666 $bias = AKFactory::get('kickstart.tuning.run_time_bias', 75) / 100; 5667 5668 // Get PHP's maximum execution time (our upper limit) 5669 if (@function_exists('ini_get')) 5670 { 5671 $php_max_exec_time = @ini_get("maximum_execution_time"); 5672 if ((!is_numeric($php_max_exec_time)) || ($php_max_exec_time == 0)) 5673 { 5674 // If we have no time limit, set a hard limit of about 10 seconds 5675 // (safe for Apache and IIS timeouts, verbose enough for users) 5676 $php_max_exec_time = 14; 5677 } 5678 } 5679 else 5680 { 5681 // If ini_get is not available, use a rough default 5682 $php_max_exec_time = 14; 5683 } 5684 5685 // Apply an arbitrary correction to counter CMS load time 5686 $php_max_exec_time--; 5687 5688 // Apply bias 5689 $php_max_exec_time = $php_max_exec_time * $bias; 5690 $config_max_exec_time = $config_max_exec_time * $bias; 5691 5692 // Use the most appropriate time limit value 5693 if ($config_max_exec_time > $php_max_exec_time) 5694 { 5695 $this->max_exec_time = $php_max_exec_time; 5696 } 5697 else 5698 { 5699 $this->max_exec_time = $config_max_exec_time; 5700 } 5701 } 5702 5703 /** 5704 * Returns the current timestampt in decimal seconds 5705 */ 5706 private function microtime_float() 5707 { 5708 list($usec, $sec) = explode(" ", microtime()); 5709 5710 return ((float) $usec + (float) $sec); 5711 } 5712 5713 /** 5714 * Wake-up function to reset internal timer when we get unserialized 5715 */ 5716 public function __wakeup() 5717 { 5718 // Re-initialize start time on wake-up 5719 $this->start_time = $this->microtime_float(); 5720 } 5721 5722 /** 5723 * Gets the number of seconds left, before we hit the "must break" threshold 5724 * 5725 * @return float 5726 */ 5727 public function getTimeLeft() 5728 { 5729 return $this->max_exec_time - $this->getRunningTime(); 5730 } 5731 5732 /** 5733 * Gets the time elapsed since object creation/unserialization, effectively how 5734 * long Akeeba Engine has been processing data 5735 * 5736 * @return float 5737 */ 5738 public function getRunningTime() 5739 { 5740 return $this->microtime_float() - $this->start_time; 5741 } 5742 5743 /** 5744 * Enforce the minimum execution time 5745 */ 5746 public function enforce_min_exec_time() 5747 { 5748 // Try to get a sane value for PHP's maximum_execution_time INI parameter 5749 if (@function_exists('ini_get')) 5750 { 5751 $php_max_exec = @ini_get("maximum_execution_time"); 5752 } 5753 else 5754 { 5755 $php_max_exec = 10; 5756 } 5757 if (($php_max_exec == "") || ($php_max_exec == 0)) 5758 { 5759 $php_max_exec = 10; 5760 } 5761 // Decrease $php_max_exec time by 500 msec we need (approx.) to tear down 5762 // the application, as well as another 500msec added for rounding 5763 // error purposes. Also make sure this is never gonna be less than 0. 5764 $php_max_exec = max($php_max_exec * 1000 - 1000, 0); 5765 5766 // Get the "minimum execution time per step" Akeeba Backup configuration variable 5767 $minexectime = AKFactory::get('kickstart.tuning.min_exec_time', 0); 5768 if (!is_numeric($minexectime)) 5769 { 5770 $minexectime = 0; 5771 } 5772 5773 // Make sure we are not over PHP's time limit! 5774 if ($minexectime > $php_max_exec) 5775 { 5776 $minexectime = $php_max_exec; 5777 } 5778 5779 // Get current running time 5780 $elapsed_time = $this->getRunningTime() * 1000; 5781 5782 // Only run a sleep delay if we haven't reached the minexectime execution time 5783 if (($minexectime > $elapsed_time) && ($elapsed_time > 0)) 5784 { 5785 $sleep_msec = $minexectime - $elapsed_time; 5786 if (function_exists('usleep')) 5787 { 5788 usleep(1000 * $sleep_msec); 5789 } 5790 elseif (function_exists('time_nanosleep')) 5791 { 5792 $sleep_sec = floor($sleep_msec / 1000); 5793 $sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000)); 5794 time_nanosleep($sleep_sec, $sleep_nsec); 5795 } 5796 elseif (function_exists('time_sleep_until')) 5797 { 5798 $until_timestamp = time() + $sleep_msec / 1000; 5799 time_sleep_until($until_timestamp); 5800 } 5801 elseif (function_exists('sleep')) 5802 { 5803 $sleep_sec = ceil($sleep_msec / 1000); 5804 sleep($sleep_sec); 5805 } 5806 } 5807 elseif ($elapsed_time > 0) 5808 { 5809 // No sleep required, even if user configured us to be able to do so. 5810 } 5811 } 5812 5813 /** 5814 * Reset the timer. It should only be used in CLI mode! 5815 */ 5816 public function resetTime() 5817 { 5818 $this->start_time = $this->microtime_float(); 5819 } 5820 5821 /** 5822 * @param int $max_exec_time 5823 */ 5824 public function setMaxExecTime($max_exec_time) 5825 { 5826 $this->max_exec_time = $max_exec_time; 5827 } 5828} 5829 5830/** 5831 * Akeeba Restore 5832 * A JSON-powered JPA, JPS and ZIP archive extraction library 5833 * 5834 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 5835 * @license GNU GPL v2 or - at your option - any later version 5836 * @package akeebabackup 5837 * @subpackage kickstart 5838 */ 5839 5840/** 5841 * A filesystem scanner which uses opendir() 5842 */ 5843class AKUtilsLister extends AKAbstractObject 5844{ 5845 public function &getFiles($folder, $pattern = '*') 5846 { 5847 // Initialize variables 5848 $arr = array(); 5849 $false = false; 5850 5851 if (!is_dir($folder)) 5852 { 5853 return $false; 5854 } 5855 5856 $handle = @opendir($folder); 5857 // If directory is not accessible, just return FALSE 5858 if ($handle === false) 5859 { 5860 $this->setWarning('Unreadable directory ' . $folder); 5861 5862 return $false; 5863 } 5864 5865 while (($file = @readdir($handle)) !== false) 5866 { 5867 if (!fnmatch($pattern, $file)) 5868 { 5869 continue; 5870 } 5871 5872 if (($file != '.') && ($file != '..')) 5873 { 5874 $ds = 5875 ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? 5876 '' : DIRECTORY_SEPARATOR; 5877 $dir = $folder . $ds . $file; 5878 $isDir = is_dir($dir); 5879 if (!$isDir) 5880 { 5881 $arr[] = $dir; 5882 } 5883 } 5884 } 5885 @closedir($handle); 5886 5887 return $arr; 5888 } 5889 5890 public function &getFolders($folder, $pattern = '*') 5891 { 5892 // Initialize variables 5893 $arr = array(); 5894 $false = false; 5895 5896 if (!is_dir($folder)) 5897 { 5898 return $false; 5899 } 5900 5901 $handle = @opendir($folder); 5902 // If directory is not accessible, just return FALSE 5903 if ($handle === false) 5904 { 5905 $this->setWarning('Unreadable directory ' . $folder); 5906 5907 return $false; 5908 } 5909 5910 while (($file = @readdir($handle)) !== false) 5911 { 5912 if (!fnmatch($pattern, $file)) 5913 { 5914 continue; 5915 } 5916 5917 if (($file != '.') && ($file != '..')) 5918 { 5919 $ds = 5920 ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? 5921 '' : DIRECTORY_SEPARATOR; 5922 $dir = $folder . $ds . $file; 5923 $isDir = is_dir($dir); 5924 if ($isDir) 5925 { 5926 $arr[] = $dir; 5927 } 5928 } 5929 } 5930 @closedir($handle); 5931 5932 return $arr; 5933 } 5934} 5935 5936/** 5937 * Akeeba Restore 5938 * A JSON-powered JPA, JPS and ZIP archive extraction library 5939 * 5940 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 5941 * @license GNU GPL v2 or - at your option - any later version 5942 * @package akeebabackup 5943 * @subpackage kickstart 5944 */ 5945 5946/** 5947 * A simple INI-based i18n engine 5948 */ 5949class AKText extends AKAbstractObject 5950{ 5951 /** 5952 * The default (en_GB) translation used when no other translation is available 5953 * 5954 * @var array 5955 */ 5956 private $default_translation = array( 5957 'AUTOMODEON' => 'Auto-mode enabled', 5958 'ERR_NOT_A_JPA_FILE' => 'The file is not a JPA archive', 5959 'ERR_CORRUPT_ARCHIVE' => 'The archive file is corrupt, truncated or archive parts are missing', 5960 'ERR_INVALID_LOGIN' => 'Invalid login', 5961 'COULDNT_CREATE_DIR' => 'Could not create %s folder', 5962 'COULDNT_WRITE_FILE' => 'Could not open %s for writing.', 5963 'WRONG_FTP_HOST' => 'Wrong FTP host or port', 5964 'WRONG_FTP_USER' => 'Wrong FTP username or password', 5965 'WRONG_FTP_PATH1' => 'Wrong FTP initial directory - the directory doesn\'t exist', 5966 'FTP_CANT_CREATE_DIR' => 'Could not create directory %s', 5967 'FTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', 5968 'SFTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', 5969 'FTP_COULDNT_UPLOAD' => 'Could not upload %s', 5970 'THINGS_HEADER' => 'Things you should know about Akeeba Kickstart', 5971 'THINGS_01' => 'Kickstart is not an installer. It is an archive extraction tool. The actual installer was put inside the archive file at backup time.', 5972 'THINGS_02' => 'Kickstart is not the only way to extract the backup archive. You can use Akeeba eXtract Wizard and upload the extracted files using FTP instead.', 5973 'THINGS_03' => 'Kickstart is bound by your server\'s configuration. As such, it may not work at all.', 5974 'THINGS_04' => 'You should download and upload your archive files using FTP in Binary transfer mode. Any other method could lead to a corrupt backup archive and restoration failure.', 5975 'THINGS_05' => 'Post-restoration site load errors are usually caused by .htaccess or php.ini directives. You should understand that blank pages, 404 and 500 errors can usually be worked around by editing the aforementioned files. It is not our job to mess with your configuration files, because this could be dangerous for your site.', 5976 'THINGS_06' => 'Kickstart overwrites files without a warning. If you are not sure that you are OK with that do not continue.', 5977 'THINGS_07' => 'Trying to restore to the temporary URL of a cPanel host (e.g. http://1.2.3.4/~username) will lead to restoration failure and your site will appear to be not working. This is normal and it\'s just how your server and CMS software work.', 5978 'THINGS_08' => 'You are supposed to read the documentation before using this software. Most issues can be avoided, or easily worked around, by understanding how this software works.', 5979 'THINGS_09' => 'This text does not imply that there is a problem detected. It is standard text displayed every time you launch Kickstart.', 5980 'CLOSE_LIGHTBOX' => 'Click here or press ESC to close this message', 5981 'SELECT_ARCHIVE' => 'Select a backup archive', 5982 'ARCHIVE_FILE' => 'Archive file:', 5983 'SELECT_EXTRACTION' => 'Select an extraction method', 5984 'WRITE_TO_FILES' => 'Write to files:', 5985 'WRITE_HYBRID' => 'Hybrid (use FTP only if needed)', 5986 'WRITE_DIRECTLY' => 'Directly', 5987 'WRITE_FTP' => 'Use FTP for all files', 5988 'WRITE_SFTP' => 'Use SFTP for all files', 5989 'FTP_HOST' => '(S)FTP host name:', 5990 'FTP_PORT' => '(S)FTP port:', 5991 'FTP_FTPS' => 'Use FTP over SSL (FTPS)', 5992 'FTP_PASSIVE' => 'Use FTP Passive Mode', 5993 'FTP_USER' => '(S)FTP user name:', 5994 'FTP_PASS' => '(S)FTP password:', 5995 'FTP_DIR' => '(S)FTP directory:', 5996 'FTP_TEMPDIR' => 'Temporary directory:', 5997 'FTP_CONNECTION_OK' => 'FTP Connection Established', 5998 'SFTP_CONNECTION_OK' => 'SFTP Connection Established', 5999 'FTP_CONNECTION_FAILURE' => 'The FTP Connection Failed', 6000 'SFTP_CONNECTION_FAILURE' => 'The SFTP Connection Failed', 6001 'FTP_TEMPDIR_WRITABLE' => 'The temporary directory is writable.', 6002 'FTP_TEMPDIR_UNWRITABLE' => 'The temporary directory is not writable. Please check the permissions.', 6003 'FTPBROWSER_ERROR_HOSTNAME' => "Invalid FTP host or port", 6004 'FTPBROWSER_ERROR_USERPASS' => "Invalid FTP username or password", 6005 'FTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", 6006 'FTPBROWSER_ERROR_UNSUPPORTED' => "Sorry, your FTP server doesn't support our FTP directory browser.", 6007 'FTPBROWSER_LBL_GOPARENT' => "<up one level>", 6008 'FTPBROWSER_LBL_INSTRUCTIONS' => 'Click on a directory to navigate into it. Click on OK to select that directory, Cancel to abort the procedure.', 6009 'FTPBROWSER_LBL_ERROR' => 'An error occurred', 6010 'SFTP_NO_SSH2' => 'Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', 6011 'SFTP_NO_FTP_SUPPORT' => 'Your SSH server does not allow SFTP connections', 6012 'SFTP_WRONG_USER' => 'Wrong SFTP username or password', 6013 'SFTP_WRONG_STARTING_DIR' => 'You must supply a valid absolute path', 6014 'SFTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", 6015 'SFTP_COULDNT_UPLOAD' => 'Could not upload %s', 6016 'SFTP_CANT_CREATE_DIR' => 'Could not create directory %s', 6017 'UI-ROOT' => '<root>', 6018 'CONFIG_UI_FTPBROWSER_TITLE' => 'FTP Directory Browser', 6019 'FTP_BROWSE' => 'Browse', 6020 'BTN_CHECK' => 'Check', 6021 'BTN_RESET' => 'Reset', 6022 'BTN_TESTFTPCON' => 'Test FTP connection', 6023 'BTN_TESTSFTPCON' => 'Test SFTP connection', 6024 'BTN_GOTOSTART' => 'Start over', 6025 'FINE_TUNE' => 'Fine tune', 6026 'BTN_SHOW_FINE_TUNE' => 'Show advanced options (for experts)', 6027 'MIN_EXEC_TIME' => 'Minimum execution time:', 6028 'MAX_EXEC_TIME' => 'Maximum execution time:', 6029 'SECONDS_PER_STEP' => 'seconds per step', 6030 'EXTRACT_FILES' => 'Extract files', 6031 'BTN_START' => 'Start', 6032 'EXTRACTING' => 'Extracting', 6033 'DO_NOT_CLOSE_EXTRACT' => 'Do not close this window while the extraction is in progress', 6034 'RESTACLEANUP' => 'Restoration and Clean Up', 6035 'BTN_RUNINSTALLER' => 'Run the Installer', 6036 'BTN_CLEANUP' => 'Clean Up', 6037 'BTN_SITEFE' => 'Visit your site\'s frontend', 6038 'BTN_SITEBE' => 'Visit your site\'s backend', 6039 'WARNINGS' => 'Extraction Warnings', 6040 'ERROR_OCCURED' => 'An error occurred', 6041 'STEALTH_MODE' => 'Stealth mode', 6042 'STEALTH_URL' => 'HTML file to show to web visitors', 6043 'ERR_NOT_A_JPS_FILE' => 'The file is not a JPA archive', 6044 'ERR_INVALID_JPS_PASSWORD' => 'The password you gave is wrong or the archive is corrupt', 6045 'JPS_PASSWORD' => 'Archive Password (for JPS files)', 6046 'INVALID_FILE_HEADER' => 'Invalid header in archive file, part %s, offset %s', 6047 'NEEDSOMEHELPKS' => 'Want some help to use this tool? Read this first:', 6048 'QUICKSTART' => 'Quick Start Guide', 6049 'CANTGETITTOWORK' => 'Can\'t get it to work? Click me!', 6050 'NOARCHIVESCLICKHERE' => 'No archives detected. Click here for troubleshooting instructions.', 6051 'POSTRESTORATIONTROUBLESHOOTING' => 'Something not working after the restoration? Click here for troubleshooting instructions.', 6052 'UPDATE_HEADER' => 'An updated version of Akeeba Kickstart (<span id="update-version">unknown</span>) is available!', 6053 'UPDATE_NOTICE' => 'You are advised to always use the latest version of Akeeba Kickstart available. Older versions may be subject to bugs and will not be supported.', 6054 'UPDATE_DLNOW' => 'Download now', 6055 'UPDATE_MOREINFO' => 'More information', 6056 'IGNORE_MOST_ERRORS' => 'Ignore most errors', 6057 'WRONG_FTP_PATH2' => 'Wrong FTP initial directory - the directory doesn\'t correspond to your site\'s web root', 6058 'ARCHIVE_DIRECTORY' => 'Archive directory:', 6059 'RELOAD_ARCHIVES' => 'Reload', 6060 'CONFIG_UI_SFTPBROWSER_TITLE' => 'SFTP Directory Browser', 6061 'ERR_COULD_NOT_OPEN_ARCHIVE_PART' => 'Could not open archive part file %s for reading. Check that the file exists, is readable by the web server and is not in a directory made out of reach by chroot, open_basedir restrictions or any other restriction put in place by your host.', 6062 'RENAME_FILES' => 'Rename server configuration files', 6063 'RESTORE_PERMISSIONS' => 'Restore file permissions', 6064 ); 6065 6066 /** 6067 * The array holding the translation keys 6068 * 6069 * @var array 6070 */ 6071 private $strings; 6072 6073 /** 6074 * The currently detected language (ISO code) 6075 * 6076 * @var string 6077 */ 6078 private $language; 6079 6080 /* 6081 * Initializes the translation engine 6082 * @return AKText 6083 */ 6084 public function __construct() 6085 { 6086 // Start with the default translation 6087 $this->strings = $this->default_translation; 6088 // Try loading the translation file in English, if it exists 6089 $this->loadTranslation('en-GB'); 6090 // Try loading the translation file in the browser's preferred language, if it exists 6091 $this->getBrowserLanguage(); 6092 if (!is_null($this->language)) 6093 { 6094 $this->loadTranslation(); 6095 } 6096 } 6097 6098 private function loadTranslation($lang = null) 6099 { 6100 if (defined('KSLANGDIR')) 6101 { 6102 $dirname = KSLANGDIR; 6103 } 6104 else 6105 { 6106 $dirname = KSROOTDIR; 6107 } 6108 $basename = basename(__FILE__, '.php') . '.ini'; 6109 if (empty($lang)) 6110 { 6111 $lang = $this->language; 6112 } 6113 6114 $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; 6115 if (!@file_exists($translationFilename) && ($basename != 'kickstart.ini')) 6116 { 6117 $basename = 'kickstart.ini'; 6118 $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; 6119 } 6120 if (!@file_exists($translationFilename)) 6121 { 6122 return; 6123 } 6124 $temp = self::parse_ini_file($translationFilename, false); 6125 6126 if (!is_array($this->strings)) 6127 { 6128 $this->strings = array(); 6129 } 6130 if (empty($temp)) 6131 { 6132 $this->strings = array_merge($this->default_translation, $this->strings); 6133 } 6134 else 6135 { 6136 $this->strings = array_merge($this->strings, $temp); 6137 } 6138 } 6139 6140 /** 6141 * A PHP based INI file parser. 6142 * 6143 * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on 6144 * the parse_ini_file page on http://gr.php.net/parse_ini_file 6145 * 6146 * @param string $file Filename to process 6147 * @param bool $process_sections True to also process INI sections 6148 * 6149 * @return array An associative array of sections, keys and values 6150 * @access private 6151 */ 6152 public static function parse_ini_file($file, $process_sections = false, $raw_data = false) 6153 { 6154 $process_sections = ($process_sections !== true) ? false : true; 6155 6156 if (!$raw_data) 6157 { 6158 $ini = @file($file); 6159 } 6160 else 6161 { 6162 $ini = $file; 6163 } 6164 if (count($ini) == 0) 6165 { 6166 return array(); 6167 } 6168 6169 $sections = array(); 6170 $values = array(); 6171 $result = array(); 6172 $globals = array(); 6173 $i = 0; 6174 if (!empty($ini)) 6175 { 6176 foreach ($ini as $line) 6177 { 6178 $line = trim($line); 6179 $line = str_replace("\t", " ", $line); 6180 6181 // Comments 6182 if (!preg_match('/^[a-zA-Z0-9[]/', $line)) 6183 { 6184 continue; 6185 } 6186 6187 // Sections 6188 if ($line[0] == '[') 6189 { 6190 $tmp = explode(']', $line); 6191 $sections[] = trim(substr($tmp[0], 1)); 6192 $i++; 6193 continue; 6194 } 6195 6196 // Key-value pair 6197 list($key, $value) = explode('=', $line, 2); 6198 $key = trim($key); 6199 $value = trim($value); 6200 if (strstr($value, ";")) 6201 { 6202 $tmp = explode(';', $value); 6203 if (count($tmp) == 2) 6204 { 6205 if ((($value[0] != '"') && ($value[0] != "'")) || 6206 preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) || 6207 preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value) 6208 ) 6209 { 6210 $value = $tmp[0]; 6211 } 6212 } 6213 else 6214 { 6215 if ($value[0] == '"') 6216 { 6217 $value = preg_replace('/^"(.*)".*/', '$1', $value); 6218 } 6219 elseif ($value[0] == "'") 6220 { 6221 $value = preg_replace("/^'(.*)'.*/", '$1', $value); 6222 } 6223 else 6224 { 6225 $value = $tmp[0]; 6226 } 6227 } 6228 } 6229 $value = trim($value); 6230 $value = trim($value, "'\""); 6231 6232 if ($i == 0) 6233 { 6234 if (substr($line, -1, 2) == '[]') 6235 { 6236 $globals[$key][] = $value; 6237 } 6238 else 6239 { 6240 $globals[$key] = $value; 6241 } 6242 } 6243 else 6244 { 6245 if (substr($line, -1, 2) == '[]') 6246 { 6247 $values[$i - 1][$key][] = $value; 6248 } 6249 else 6250 { 6251 $values[$i - 1][$key] = $value; 6252 } 6253 } 6254 } 6255 } 6256 6257 for ($j = 0; $j < $i; $j++) 6258 { 6259 if ($process_sections === true) 6260 { 6261 $result[$sections[$j]] = $values[$j]; 6262 } 6263 else 6264 { 6265 $result[] = $values[$j]; 6266 } 6267 } 6268 6269 return $result + $globals; 6270 } 6271 6272 public function getBrowserLanguage() 6273 { 6274 // Detection code from Full Operating system language detection, by Harald Hope 6275 // Retrieved from http://techpatterns.com/downloads/php_language_detection.php 6276 $user_languages = array(); 6277 //check to see if language is set 6278 if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) 6279 { 6280 $languages = strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"]); 6281 // $languages = ' fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3'; 6282 // need to remove spaces from strings to avoid error 6283 $languages = str_replace(' ', '', $languages); 6284 $languages = explode(",", $languages); 6285 6286 foreach ($languages as $language_list) 6287 { 6288 // pull out the language, place languages into array of full and primary 6289 // string structure: 6290 $temp_array = array(); 6291 // slice out the part before ; on first step, the part before - on second, place into array 6292 $temp_array[0] = substr($language_list, 0, strcspn($language_list, ';'));//full language 6293 $temp_array[1] = substr($language_list, 0, 2);// cut out primary language 6294 if ((strlen($temp_array[0]) == 5) && ((substr($temp_array[0], 2, 1) == '-') || (substr($temp_array[0], 2, 1) == '_'))) 6295 { 6296 $langLocation = strtoupper(substr($temp_array[0], 3, 2)); 6297 $temp_array[0] = $temp_array[1] . '-' . $langLocation; 6298 } 6299 //place this array into main $user_languages language array 6300 $user_languages[] = $temp_array; 6301 } 6302 } 6303 else// if no languages found 6304 { 6305 $user_languages[0] = array('', ''); //return blank array. 6306 } 6307 6308 $this->language = null; 6309 $basename = basename(__FILE__, '.php') . '.ini'; 6310 6311 // Try to match main language part of the filename, irrespective of the location, e.g. de_DE will do if de_CH doesn't exist. 6312 if (class_exists('AKUtilsLister')) 6313 { 6314 $fs = new AKUtilsLister(); 6315 $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); 6316 if (empty($iniFiles) && ($basename != 'kickstart.ini')) 6317 { 6318 $basename = 'kickstart.ini'; 6319 $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); 6320 } 6321 } 6322 else 6323 { 6324 $iniFiles = null; 6325 } 6326 6327 if (is_array($iniFiles)) 6328 { 6329 foreach ($user_languages as $languageStruct) 6330 { 6331 if (is_null($this->language)) 6332 { 6333 // Get files matching the main lang part 6334 $iniFiles = $fs->getFiles(KSROOTDIR, $languageStruct[1] . '-??.' . $basename); 6335 if (count($iniFiles) > 0) 6336 { 6337 $filename = $iniFiles[0]; 6338 $filename = substr($filename, strlen(KSROOTDIR) + 1); 6339 $this->language = substr($filename, 0, 5); 6340 } 6341 else 6342 { 6343 $this->language = null; 6344 } 6345 } 6346 } 6347 } 6348 6349 if (is_null($this->language)) 6350 { 6351 // Try to find a full language match 6352 foreach ($user_languages as $languageStruct) 6353 { 6354 if (@file_exists($languageStruct[0] . '.' . $basename) && is_null($this->language)) 6355 { 6356 $this->language = $languageStruct[0]; 6357 } 6358 else 6359 { 6360 6361 } 6362 } 6363 } 6364 else 6365 { 6366 // Do we have an exact match? 6367 foreach ($user_languages as $languageStruct) 6368 { 6369 if (substr($this->language, 0, strlen($languageStruct[1])) == $languageStruct[1]) 6370 { 6371 if (file_exists($languageStruct[0] . '.' . $basename)) 6372 { 6373 $this->language = $languageStruct[0]; 6374 } 6375 } 6376 } 6377 } 6378 6379 // Now, scan for full language based on the partial match 6380 6381 } 6382 6383 public static function sprintf($key) 6384 { 6385 $text = self::getInstance(); 6386 $args = func_get_args(); 6387 if (count($args) > 0) 6388 { 6389 $args[0] = $text->_($args[0]); 6390 6391 return @call_user_func_array('sprintf', $args); 6392 } 6393 6394 return ''; 6395 } 6396 6397 /** 6398 * Singleton pattern for Language 6399 * 6400 * @return AKText The global AKText instance 6401 */ 6402 public static function &getInstance() 6403 { 6404 static $instance; 6405 6406 if (!is_object($instance)) 6407 { 6408 $instance = new AKText(); 6409 } 6410 6411 return $instance; 6412 } 6413 6414 public static function _($string) 6415 { 6416 $text = self::getInstance(); 6417 6418 $key = strtoupper($string); 6419 $key = substr($key, 0, 1) == '_' ? substr($key, 1) : $key; 6420 6421 if (isset ($text->strings[$key])) 6422 { 6423 $string = $text->strings[$key]; 6424 } 6425 else 6426 { 6427 if (defined($string)) 6428 { 6429 $string = constant($string); 6430 } 6431 } 6432 6433 return $string; 6434 } 6435 6436 public function dumpLanguage() 6437 { 6438 $out = ''; 6439 foreach ($this->strings as $key => $value) 6440 { 6441 $out .= "$key=$value\n"; 6442 } 6443 6444 return $out; 6445 } 6446 6447 public function asJavascript() 6448 { 6449 $out = ''; 6450 foreach ($this->strings as $key => $value) 6451 { 6452 $key = addcslashes($key, '\\\'"'); 6453 $value = addcslashes($value, '\\\'"'); 6454 if (!empty($out)) 6455 { 6456 $out .= ",\n"; 6457 } 6458 $out .= "'$key':\t'$value'"; 6459 } 6460 6461 return $out; 6462 } 6463 6464 public function resetTranslation() 6465 { 6466 $this->strings = $this->default_translation; 6467 } 6468 6469 public function addDefaultLanguageStrings($stringList = array()) 6470 { 6471 if (!is_array($stringList)) 6472 { 6473 return; 6474 } 6475 if (empty($stringList)) 6476 { 6477 return; 6478 } 6479 6480 $this->strings = array_merge($stringList, $this->strings); 6481 } 6482} 6483 6484/** 6485 * Akeeba Restore 6486 * A JSON-powered JPA, JPS and ZIP archive extraction library 6487 * 6488 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 6489 * @license GNU GPL v2 or - at your option - any later version 6490 * @package akeebabackup 6491 * @subpackage kickstart 6492 */ 6493 6494/** 6495 * The Akeeba Kickstart Factory class 6496 * This class is reponssible for instanciating all Akeeba Kicsktart classes 6497 */ 6498class AKFactory 6499{ 6500 /** @var array A list of instantiated objects */ 6501 private $objectlist = array(); 6502 6503 /** @var array Simple hash data storage */ 6504 private $varlist = array(); 6505 6506 /** @var self Static instance */ 6507 private static $instance = null; 6508 6509 /** Private constructor makes sure we can't directly instantiate the class */ 6510 private function __construct() 6511 { 6512 } 6513 6514 /** 6515 * Gets a serialized snapshot of the Factory for safekeeping (hibernate) 6516 * 6517 * @return string The serialized snapshot of the Factory 6518 */ 6519 public static function serialize() 6520 { 6521 $engine = self::getUnarchiver(); 6522 $engine->shutdown(); 6523 $serialized = serialize(self::getInstance()); 6524 6525 if (function_exists('base64_encode') && function_exists('base64_decode')) 6526 { 6527 $serialized = base64_encode($serialized); 6528 } 6529 6530 return $serialized; 6531 } 6532 6533 /** 6534 * Gets the unarchiver engine 6535 */ 6536 public static function &getUnarchiver($configOverride = null) 6537 { 6538 static $class_name; 6539 6540 if (!empty($configOverride)) 6541 { 6542 if ($configOverride['reset']) 6543 { 6544 $class_name = null; 6545 } 6546 } 6547 6548 if (empty($class_name)) 6549 { 6550 $filetype = self::get('kickstart.setup.filetype', null); 6551 6552 if (empty($filetype)) 6553 { 6554 $filename = self::get('kickstart.setup.sourcefile', null); 6555 $basename = basename($filename); 6556 $baseextension = strtoupper(substr($basename, -3)); 6557 switch ($baseextension) 6558 { 6559 case 'JPA': 6560 $filetype = 'JPA'; 6561 break; 6562 6563 case 'JPS': 6564 $filetype = 'JPS'; 6565 break; 6566 6567 case 'ZIP': 6568 $filetype = 'ZIP'; 6569 break; 6570 6571 default: 6572 die('Invalid archive type or extension in file ' . $filename); 6573 break; 6574 } 6575 } 6576 6577 $class_name = 'AKUnarchiver' . ucfirst($filetype); 6578 } 6579 6580 $destdir = self::get('kickstart.setup.destdir', null); 6581 if (empty($destdir)) 6582 { 6583 $destdir = KSROOTDIR; 6584 } 6585 6586 $object = self::getClassInstance($class_name); 6587 if ($object->getState() == 'init') 6588 { 6589 $sourcePath = self::get('kickstart.setup.sourcepath', ''); 6590 $sourceFile = self::get('kickstart.setup.sourcefile', ''); 6591 6592 if (!empty($sourcePath)) 6593 { 6594 $sourceFile = rtrim($sourcePath, '/\\') . '/' . $sourceFile; 6595 } 6596 6597 // Initialize the object –– Any change here MUST be reflected to echoHeadJavascript (default values) 6598 $config = array( 6599 'filename' => $sourceFile, 6600 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0), 6601 'post_proc' => self::get('kickstart.procengine', 'direct'), 6602 'add_path' => self::get('kickstart.setup.targetpath', $destdir), 6603 'remove_path' => self::get('kickstart.setup.removepath', ''), 6604 'rename_files' => self::get('kickstart.setup.renamefiles', array( 6605 '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak', 'web.config' => 'web.config.bak', 6606 '.user.ini' => '.user.ini.bak' 6607 )), 6608 'skip_files' => self::get('kickstart.setup.skipfiles', array( 6609 basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', 6610 'cacert.pem' 6611 )), 6612 'ignoredirectories' => self::get('kickstart.setup.ignoredirectories', array( 6613 'tmp', 'log', 'logs' 6614 )), 6615 ); 6616 6617 if (!defined('KICKSTART')) 6618 { 6619 // In restore.php mode we have to exclude the restoration.php files 6620 $moreSkippedFiles = array( 6621 // Akeeba Backup for Joomla! 6622 'administrator/components/com_akeeba/restoration.php', 6623 // Joomla! Update 6624 'administrator/components/com_joomlaupdate/restoration.php', 6625 // Akeeba Backup for WordPress 6626 'wp-content/plugins/akeebabackupwp/app/restoration.php', 6627 'wp-content/plugins/akeebabackupcorewp/app/restoration.php', 6628 'wp-content/plugins/akeebabackup/app/restoration.php', 6629 'wp-content/plugins/akeebabackupwpcore/app/restoration.php', 6630 // Akeeba Solo 6631 'app/restoration.php', 6632 ); 6633 $config['skip_files'] = array_merge($config['skip_files'], $moreSkippedFiles); 6634 } 6635 6636 if (!empty($configOverride)) 6637 { 6638 $config = array_merge($config, $configOverride); 6639 } 6640 6641 $object->setup($config); 6642 } 6643 6644 return $object; 6645 } 6646 6647 // ======================================================================== 6648 // Public factory interface 6649 // ======================================================================== 6650 6651 public static function get($key, $default = null) 6652 { 6653 $self = self::getInstance(); 6654 6655 if (array_key_exists($key, $self->varlist)) 6656 { 6657 return $self->varlist[$key]; 6658 } 6659 else 6660 { 6661 return $default; 6662 } 6663 } 6664 6665 /** 6666 * Gets a single, internally used instance of the Factory 6667 * 6668 * @param string $serialized_data [optional] Serialized data to spawn the instance from 6669 * 6670 * @return AKFactory A reference to the unique Factory object instance 6671 */ 6672 protected static function &getInstance($serialized_data = null) 6673 { 6674 if (!is_object(self::$instance) || !is_null($serialized_data)) 6675 { 6676 if (!is_null($serialized_data)) 6677 { 6678 self::$instance = unserialize($serialized_data); 6679 } 6680 else 6681 { 6682 self::$instance = new self(); 6683 } 6684 } 6685 6686 return self::$instance; 6687 } 6688 6689 /** 6690 * Internal function which instanciates a class named $class_name. 6691 * The autoloader 6692 * 6693 * @param string $class_name 6694 * 6695 * @return object 6696 */ 6697 protected static function &getClassInstance($class_name) 6698 { 6699 $self = self::getInstance(); 6700 6701 if (!isset($self->objectlist[$class_name])) 6702 { 6703 $self->objectlist[$class_name] = new $class_name; 6704 } 6705 6706 return $self->objectlist[$class_name]; 6707 } 6708 6709 // ======================================================================== 6710 // Public hash data storage interface 6711 // ======================================================================== 6712 6713 /** 6714 * Regenerates the full Factory state from a serialized snapshot (resume) 6715 * 6716 * @param string $serialized_data The serialized snapshot to resume from 6717 */ 6718 public static function unserialize($serialized_data) 6719 { 6720 if (function_exists('base64_encode') && function_exists('base64_decode')) 6721 { 6722 $serialized_data = base64_decode($serialized_data); 6723 } 6724 self::getInstance($serialized_data); 6725 } 6726 6727 /** 6728 * Reset the internal factory state, freeing all previously created objects 6729 */ 6730 public static function nuke() 6731 { 6732 self::$instance = null; 6733 } 6734 6735 // ======================================================================== 6736 // Akeeba Kickstart classes 6737 // ======================================================================== 6738 6739 public static function set($key, $value) 6740 { 6741 $self = self::getInstance(); 6742 $self->varlist[$key] = $value; 6743 } 6744 6745 /** 6746 * Gets the post processing engine 6747 * 6748 * @param string $proc_engine 6749 */ 6750 public static function &getPostProc($proc_engine = null) 6751 { 6752 static $class_name; 6753 if (empty($class_name)) 6754 { 6755 if (empty($proc_engine)) 6756 { 6757 $proc_engine = self::get('kickstart.procengine', 'direct'); 6758 } 6759 $class_name = 'AKPostproc' . ucfirst($proc_engine); 6760 } 6761 6762 return self::getClassInstance($class_name); 6763 } 6764 6765 /** 6766 * Get the a reference to the Akeeba Engine's timer 6767 * 6768 * @return AKCoreTimer 6769 */ 6770 public static function &getTimer() 6771 { 6772 return self::getClassInstance('AKCoreTimer'); 6773 } 6774 6775} 6776 6777/** 6778 * Akeeba Restore 6779 * A JSON-powered JPA, JPS and ZIP archive extraction library 6780 * 6781 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 6782 * @license GNU GPL v2 or - at your option - any later version 6783 * @package akeebabackup 6784 * @subpackage kickstart 6785 */ 6786 6787/** 6788 * Interface for AES encryption adapters 6789 */ 6790interface AKEncryptionAESAdapterInterface 6791{ 6792 /** 6793 * Decrypts a string. Returns the raw binary ciphertext, zero-padded. 6794 * 6795 * @param string $plainText The plaintext to encrypt 6796 * @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size) 6797 * 6798 * @return string The raw encrypted binary string. 6799 */ 6800 public function decrypt($plainText, $key); 6801 6802 /** 6803 * Returns the encryption block size in bytes 6804 * 6805 * @return int 6806 */ 6807 public function getBlockSize(); 6808 6809 /** 6810 * Is this adapter supported? 6811 * 6812 * @return bool 6813 */ 6814 public function isSupported(); 6815} 6816 6817/** 6818 * Akeeba Restore 6819 * A JSON-powered JPA, JPS and ZIP archive extraction library 6820 * 6821 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 6822 * @license GNU GPL v2 or - at your option - any later version 6823 * @package akeebabackup 6824 * @subpackage kickstart 6825 */ 6826 6827/** 6828 * Abstract AES encryption class 6829 */ 6830abstract class AKEncryptionAESAdapterAbstract 6831{ 6832 /** 6833 * Trims or zero-pads a key / IV 6834 * 6835 * @param string $key The key or IV to treat 6836 * @param int $size The block size of the currently used algorithm 6837 * 6838 * @return null|string Null if $key is null, treated string of $size byte length otherwise 6839 */ 6840 public function resizeKey($key, $size) 6841 { 6842 if (empty($key)) 6843 { 6844 return null; 6845 } 6846 6847 $keyLength = strlen($key); 6848 6849 if (function_exists('mb_strlen')) 6850 { 6851 $keyLength = mb_strlen($key, 'ASCII'); 6852 } 6853 6854 if ($keyLength == $size) 6855 { 6856 return $key; 6857 } 6858 6859 if ($keyLength > $size) 6860 { 6861 if (function_exists('mb_substr')) 6862 { 6863 return mb_substr($key, 0, $size, 'ASCII'); 6864 } 6865 6866 return substr($key, 0, $size); 6867 } 6868 6869 return $key . str_repeat("\0", ($size - $keyLength)); 6870 } 6871 6872 /** 6873 * Returns null bytes to append to the string so that it's zero padded to the specified block size 6874 * 6875 * @param string $string The binary string which will be zero padded 6876 * @param int $blockSize The block size 6877 * 6878 * @return string The zero bytes to append to the string to zero pad it to $blockSize 6879 */ 6880 protected function getZeroPadding($string, $blockSize) 6881 { 6882 $stringSize = strlen($string); 6883 6884 if (function_exists('mb_strlen')) 6885 { 6886 $stringSize = mb_strlen($string, 'ASCII'); 6887 } 6888 6889 if ($stringSize == $blockSize) 6890 { 6891 return ''; 6892 } 6893 6894 if ($stringSize < $blockSize) 6895 { 6896 return str_repeat("\0", $blockSize - $stringSize); 6897 } 6898 6899 $paddingBytes = $stringSize % $blockSize; 6900 6901 return str_repeat("\0", $blockSize - $paddingBytes); 6902 } 6903} 6904 6905/** 6906 * Akeeba Restore 6907 * A JSON-powered JPA, JPS and ZIP archive extraction library 6908 * 6909 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 6910 * @license GNU GPL v2 or - at your option - any later version 6911 * @package akeebabackup 6912 * @subpackage kickstart 6913 */ 6914 6915class Mcrypt extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface 6916{ 6917 protected $cipherType = MCRYPT_RIJNDAEL_128; 6918 6919 protected $cipherMode = MCRYPT_MODE_CBC; 6920 6921 public function decrypt($cipherText, $key) 6922 { 6923 $iv_size = $this->getBlockSize(); 6924 $key = $this->resizeKey($key, $iv_size); 6925 $iv = substr($cipherText, 0, $iv_size); 6926 $cipherText = substr($cipherText, $iv_size); 6927 $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); 6928 6929 return $plainText; 6930 } 6931 6932 public function isSupported() 6933 { 6934 if (!function_exists('mcrypt_get_key_size')) 6935 { 6936 return false; 6937 } 6938 6939 if (!function_exists('mcrypt_get_iv_size')) 6940 { 6941 return false; 6942 } 6943 6944 if (!function_exists('mcrypt_create_iv')) 6945 { 6946 return false; 6947 } 6948 6949 if (!function_exists('mcrypt_encrypt')) 6950 { 6951 return false; 6952 } 6953 6954 if (!function_exists('mcrypt_decrypt')) 6955 { 6956 return false; 6957 } 6958 6959 if (!function_exists('mcrypt_list_algorithms')) 6960 { 6961 return false; 6962 } 6963 6964 if (!function_exists('hash')) 6965 { 6966 return false; 6967 } 6968 6969 if (!function_exists('hash_algos')) 6970 { 6971 return false; 6972 } 6973 6974 $algorightms = mcrypt_list_algorithms(); 6975 6976 if (!in_array('rijndael-128', $algorightms)) 6977 { 6978 return false; 6979 } 6980 6981 if (!in_array('rijndael-192', $algorightms)) 6982 { 6983 return false; 6984 } 6985 6986 if (!in_array('rijndael-256', $algorightms)) 6987 { 6988 return false; 6989 } 6990 6991 $algorightms = hash_algos(); 6992 6993 if (!in_array('sha256', $algorightms)) 6994 { 6995 return false; 6996 } 6997 6998 return true; 6999 } 7000 7001 public function getBlockSize() 7002 { 7003 return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); 7004 } 7005} 7006 7007/** 7008 * Akeeba Restore 7009 * A JSON-powered JPA, JPS and ZIP archive extraction library 7010 * 7011 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 7012 * @license GNU GPL v2 or - at your option - any later version 7013 * @package akeebabackup 7014 * @subpackage kickstart 7015 */ 7016 7017class OpenSSL extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface 7018{ 7019 /** 7020 * The OpenSSL options for encryption / decryption 7021 * 7022 * @var int 7023 */ 7024 protected $openSSLOptions = 0; 7025 7026 /** 7027 * The encryption method to use 7028 * 7029 * @var string 7030 */ 7031 protected $method = 'aes-128-cbc'; 7032 7033 public function __construct() 7034 { 7035 $this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; 7036 } 7037 7038 public function decrypt($cipherText, $key) 7039 { 7040 $iv_size = $this->getBlockSize(); 7041 $key = $this->resizeKey($key, $iv_size); 7042 $iv = substr($cipherText, 0, $iv_size); 7043 $cipherText = substr($cipherText, $iv_size); 7044 $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); 7045 7046 return $plainText; 7047 } 7048 7049 public function isSupported() 7050 { 7051 if (!function_exists('openssl_get_cipher_methods')) 7052 { 7053 return false; 7054 } 7055 7056 if (!function_exists('openssl_random_pseudo_bytes')) 7057 { 7058 return false; 7059 } 7060 7061 if (!function_exists('openssl_cipher_iv_length')) 7062 { 7063 return false; 7064 } 7065 7066 if (!function_exists('openssl_encrypt')) 7067 { 7068 return false; 7069 } 7070 7071 if (!function_exists('openssl_decrypt')) 7072 { 7073 return false; 7074 } 7075 7076 if (!function_exists('hash')) 7077 { 7078 return false; 7079 } 7080 7081 if (!function_exists('hash_algos')) 7082 { 7083 return false; 7084 } 7085 7086 $algorightms = openssl_get_cipher_methods(); 7087 7088 if (!in_array('aes-128-cbc', $algorightms)) 7089 { 7090 return false; 7091 } 7092 7093 $algorightms = hash_algos(); 7094 7095 if (!in_array('sha256', $algorightms)) 7096 { 7097 return false; 7098 } 7099 7100 return true; 7101 } 7102 7103 /** 7104 * @return int 7105 */ 7106 public function getBlockSize() 7107 { 7108 return openssl_cipher_iv_length($this->method); 7109 } 7110} 7111 7112/** 7113 * Akeeba Restore 7114 * A JSON-powered JPA, JPS and ZIP archive extraction library 7115 * 7116 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 7117 * @license GNU GPL v2 or - at your option - any later version 7118 * @package akeebabackup 7119 * @subpackage kickstart 7120 */ 7121 7122/** 7123 * AES implementation in PHP (c) Chris Veness 2005-2016. 7124 * Right to use and adapt is granted for under a simple creative commons attribution 7125 * licence. No warranty of any form is offered. 7126 * 7127 * Heavily modified for Akeeba Backup by Nicholas K. Dionysopoulos 7128 * Also added AES-128 CBC mode (with mcrypt and OpenSSL) on top of AES CTR 7129 */ 7130class AKEncryptionAES 7131{ 7132 // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1] 7133 protected static $Sbox = 7134 array(0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 7135 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 7136 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 7137 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 7138 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 7139 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 7140 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 7141 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 7142 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 7143 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 7144 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 7145 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 7146 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 7147 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 7148 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 7149 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16); 7150 7151 // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2] 7152 protected static $Rcon = array( 7153 array(0x00, 0x00, 0x00, 0x00), 7154 array(0x01, 0x00, 0x00, 0x00), 7155 array(0x02, 0x00, 0x00, 0x00), 7156 array(0x04, 0x00, 0x00, 0x00), 7157 array(0x08, 0x00, 0x00, 0x00), 7158 array(0x10, 0x00, 0x00, 0x00), 7159 array(0x20, 0x00, 0x00, 0x00), 7160 array(0x40, 0x00, 0x00, 0x00), 7161 array(0x80, 0x00, 0x00, 0x00), 7162 array(0x1b, 0x00, 0x00, 0x00), 7163 array(0x36, 0x00, 0x00, 0x00)); 7164 7165 protected static $passwords = array(); 7166 7167 /** 7168 * The algorithm to use for PBKDF2. Must be a supported hash_hmac algorithm. Default: sha1 7169 * 7170 * @var string 7171 */ 7172 private static $pbkdf2Algorithm = 'sha1'; 7173 7174 /** 7175 * Number of iterations to use for PBKDF2 7176 * 7177 * @var int 7178 */ 7179 private static $pbkdf2Iterations = 1000; 7180 7181 /** 7182 * Should we use a static salt for PBKDF2? 7183 * 7184 * @var int 7185 */ 7186 private static $pbkdf2UseStaticSalt = 0; 7187 7188 /** 7189 * The static salt to use for PBKDF2 7190 * 7191 * @var string 7192 */ 7193 private static $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 7194 7195 /** 7196 * Encrypt a text using AES encryption in Counter mode of operation 7197 * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf 7198 * 7199 * Unicode multi-byte character safe 7200 * 7201 * @param string $plaintext Source text to be encrypted 7202 * @param string $password The password to use to generate a key 7203 * @param int $nBits Number of bits to be used in the key (128, 192, or 256) 7204 * 7205 * @return string Encrypted text 7206 */ 7207 public static function AESEncryptCtr($plaintext, $password, $nBits) 7208 { 7209 $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES 7210 if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) 7211 { 7212 return ''; 7213 } // standard allows 128/192/256 bit keys 7214 // note PHP (5) gives us plaintext and password in UTF8 encoding! 7215 7216 // use AES itself to encrypt password to get cipher key (using plain password as source for 7217 // key expansion) - gives us well encrypted key 7218 $nBytes = $nBits / 8; // no bytes in key 7219 $pwBytes = array(); 7220 for ($i = 0; $i < $nBytes; $i++) 7221 { 7222 $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; 7223 } 7224 $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); 7225 $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long 7226 7227 // initialise counter block (NIST SP800-38A �B.2): millisecond time-stamp for nonce in 7228 // 1st 8 bytes, block counter in 2nd 8 bytes 7229 $counterBlock = array(); 7230 $nonce = floor(microtime(true) * 1000); // timestamp: milliseconds since 1-Jan-1970 7231 $nonceSec = floor($nonce / 1000); 7232 $nonceMs = $nonce % 1000; 7233 // encode nonce with seconds in 1st 4 bytes, and (repeated) ms part filling 2nd 4 bytes 7234 for ($i = 0; $i < 4; $i++) 7235 { 7236 $counterBlock[$i] = self::urs($nonceSec, $i * 8) & 0xff; 7237 } 7238 for ($i = 0; $i < 4; $i++) 7239 { 7240 $counterBlock[$i + 4] = $nonceMs & 0xff; 7241 } 7242 // and convert it to a string to go on the front of the ciphertext 7243 $ctrTxt = ''; 7244 for ($i = 0; $i < 8; $i++) 7245 { 7246 $ctrTxt .= chr($counterBlock[$i]); 7247 } 7248 7249 // generate key schedule - an expansion of the key into distinct Key Rounds for each round 7250 $keySchedule = self::KeyExpansion($key); 7251 7252 $blockCount = ceil(strlen($plaintext) / $blockSize); 7253 $ciphertxt = array(); // ciphertext as array of strings 7254 7255 for ($b = 0; $b < $blockCount; $b++) 7256 { 7257 // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) 7258 // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB) 7259 for ($c = 0; $c < 4; $c++) 7260 { 7261 $counterBlock[15 - $c] = self::urs($b, $c * 8) & 0xff; 7262 } 7263 for ($c = 0; $c < 4; $c++) 7264 { 7265 $counterBlock[15 - $c - 4] = self::urs($b / 0x100000000, $c * 8); 7266 } 7267 7268 $cipherCntr = self::Cipher($counterBlock, $keySchedule); // -- encrypt counter block -- 7269 7270 // block size is reduced on final block 7271 $blockLength = $b < $blockCount - 1 ? $blockSize : (strlen($plaintext) - 1) % $blockSize + 1; 7272 $cipherByte = array(); 7273 7274 for ($i = 0; $i < $blockLength; $i++) 7275 { // -- xor plaintext with ciphered counter byte-by-byte -- 7276 $cipherByte[$i] = $cipherCntr[$i] ^ ord(substr($plaintext, $b * $blockSize + $i, 1)); 7277 $cipherByte[$i] = chr($cipherByte[$i]); 7278 } 7279 $ciphertxt[$b] = implode('', $cipherByte); // escape troublesome characters in ciphertext 7280 } 7281 7282 // implode is more efficient than repeated string concatenation 7283 $ciphertext = $ctrTxt . implode('', $ciphertxt); 7284 $ciphertext = base64_encode($ciphertext); 7285 7286 return $ciphertext; 7287 } 7288 7289 /** 7290 * AES Cipher function: encrypt 'input' with Rijndael algorithm 7291 * 7292 * @param array $input Message as byte-array (16 bytes) 7293 * @param array $w key schedule as 2D byte-array (Nr+1 x Nb bytes) - 7294 * generated from the cipher key by KeyExpansion() 7295 * 7296 * @return string Ciphertext as byte-array (16 bytes) 7297 */ 7298 protected static function Cipher($input, $w) 7299 { // main Cipher function [�5.1] 7300 $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) 7301 $Nr = count($w) / $Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys 7302 7303 $state = array(); // initialise 4xNb byte-array 'state' with input [�3.4] 7304 for ($i = 0; $i < 4 * $Nb; $i++) 7305 { 7306 $state[$i % 4][floor($i / 4)] = $input[$i]; 7307 } 7308 7309 $state = self::AddRoundKey($state, $w, 0, $Nb); 7310 7311 for ($round = 1; $round < $Nr; $round++) 7312 { // apply Nr rounds 7313 $state = self::SubBytes($state, $Nb); 7314 $state = self::ShiftRows($state, $Nb); 7315 $state = self::MixColumns($state); 7316 $state = self::AddRoundKey($state, $w, $round, $Nb); 7317 } 7318 7319 $state = self::SubBytes($state, $Nb); 7320 $state = self::ShiftRows($state, $Nb); 7321 $state = self::AddRoundKey($state, $w, $Nr, $Nb); 7322 7323 $output = array(4 * $Nb); // convert state to 1-d array before returning [�3.4] 7324 for ($i = 0; $i < 4 * $Nb; $i++) 7325 { 7326 $output[$i] = $state[$i % 4][floor($i / 4)]; 7327 } 7328 7329 return $output; 7330 } 7331 7332 protected static function AddRoundKey($state, $w, $rnd, $Nb) 7333 { // xor Round Key into state S [�5.1.4] 7334 for ($r = 0; $r < 4; $r++) 7335 { 7336 for ($c = 0; $c < $Nb; $c++) 7337 { 7338 $state[$r][$c] ^= $w[$rnd * 4 + $c][$r]; 7339 } 7340 } 7341 7342 return $state; 7343 } 7344 7345 protected static function SubBytes($s, $Nb) 7346 { // apply SBox to state S [�5.1.1] 7347 for ($r = 0; $r < 4; $r++) 7348 { 7349 for ($c = 0; $c < $Nb; $c++) 7350 { 7351 $s[$r][$c] = self::$Sbox[$s[$r][$c]]; 7352 } 7353 } 7354 7355 return $s; 7356 } 7357 7358 protected static function ShiftRows($s, $Nb) 7359 { // shift row r of state S left by r bytes [�5.1.2] 7360 $t = array(4); 7361 for ($r = 1; $r < 4; $r++) 7362 { 7363 for ($c = 0; $c < 4; $c++) 7364 { 7365 $t[$c] = $s[$r][($c + $r) % $Nb]; 7366 } // shift into temp copy 7367 for ($c = 0; $c < 4; $c++) 7368 { 7369 $s[$r][$c] = $t[$c]; 7370 } // and copy back 7371 } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES): 7372 return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf 7373 } 7374 7375 protected static function MixColumns($s) 7376 { 7377 // combine bytes of each col of state S [�5.1.3] 7378 for ($c = 0; $c < 4; $c++) 7379 { 7380 $a = array(4); // 'a' is a copy of the current column from 's' 7381 $b = array(4); // 'b' is a�{02} in GF(2^8) 7382 7383 for ($i = 0; $i < 4; $i++) 7384 { 7385 $a[$i] = $s[$i][$c]; 7386 $b[$i] = $s[$i][$c] & 0x80 ? $s[$i][$c] << 1 ^ 0x011b : $s[$i][$c] << 1; 7387 } 7388 7389 // a[n] ^ b[n] is a�{03} in GF(2^8) 7390 $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3 7391 $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3 7392 $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3 7393 $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3 7394 } 7395 7396 return $s; 7397 } 7398 7399 /** 7400 * Key expansion for Rijndael Cipher(): performs key expansion on cipher key 7401 * to generate a key schedule 7402 * 7403 * @param array $key Cipher key byte-array (16 bytes) 7404 * 7405 * @return array Key schedule as 2D byte-array (Nr+1 x Nb bytes) 7406 */ 7407 protected static function KeyExpansion($key) 7408 { 7409 // generate Key Schedule from Cipher Key [�5.2] 7410 7411 // block size (in words): no of columns in state (fixed at 4 for AES) 7412 $Nb = 4; 7413 // key length (in words): 4/6/8 for 128/192/256-bit keys 7414 $Nk = (int) (count($key) / 4); 7415 // no of rounds: 10/12/14 for 128/192/256-bit keys 7416 $Nr = $Nk + 6; 7417 7418 $w = array(); 7419 $temp = array(); 7420 7421 for ($i = 0; $i < $Nk; $i++) 7422 { 7423 $r = array($key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]); 7424 $w[$i] = $r; 7425 } 7426 7427 for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++) 7428 { 7429 $w[$i] = array(); 7430 for ($t = 0; $t < 4; $t++) 7431 { 7432 $temp[$t] = $w[$i - 1][$t]; 7433 } 7434 if ($i % $Nk == 0) 7435 { 7436 $temp = self::SubWord(self::RotWord($temp)); 7437 for ($t = 0; $t < 4; $t++) 7438 { 7439 $rConIndex = (int) ($i / $Nk); 7440 $temp[$t] ^= self::$Rcon[$rConIndex][$t]; 7441 } 7442 } 7443 else if ($Nk > 6 && $i % $Nk == 4) 7444 { 7445 $temp = self::SubWord($temp); 7446 } 7447 for ($t = 0; $t < 4; $t++) 7448 { 7449 $w[$i][$t] = $w[$i - $Nk][$t] ^ $temp[$t]; 7450 } 7451 } 7452 7453 return $w; 7454 } 7455 7456 protected static function SubWord($w) 7457 { // apply SBox to 4-byte word w 7458 for ($i = 0; $i < 4; $i++) 7459 { 7460 $w[$i] = self::$Sbox[$w[$i]]; 7461 } 7462 7463 return $w; 7464 } 7465 7466 /* 7467 * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints 7468 * 7469 * @param a number to be shifted (32-bit integer) 7470 * @param b number of bits to shift a to the right (0..31) 7471 * @return a right-shifted and zero-filled by b bits 7472 */ 7473 7474 protected static function RotWord($w) 7475 { // rotate 4-byte word w left by one byte 7476 $tmp = $w[0]; 7477 for ($i = 0; $i < 3; $i++) 7478 { 7479 $w[$i] = $w[$i + 1]; 7480 } 7481 $w[3] = $tmp; 7482 7483 return $w; 7484 } 7485 7486 protected static function urs($a, $b) 7487 { 7488 $a &= 0xffffffff; 7489 $b &= 0x1f; // (bounds check) 7490 if ($a & 0x80000000 && $b > 0) 7491 { // if left-most bit set 7492 $a = ($a >> 1) & 0x7fffffff; // right-shift one bit & clear left-most bit 7493 $a = $a >> ($b - 1); // remaining right-shifts 7494 } 7495 else 7496 { // otherwise 7497 $a = ($a >> $b); // use normal right-shift 7498 } 7499 7500 return $a; 7501 } 7502 7503 /** 7504 * Decrypt a text encrypted by AES in counter mode of operation 7505 * 7506 * @param string $ciphertext Source text to be decrypted 7507 * @param string $password The password to use to generate a key 7508 * @param int $nBits Number of bits to be used in the key (128, 192, or 256) 7509 * 7510 * @return string Decrypted text 7511 */ 7512 public static function AESDecryptCtr($ciphertext, $password, $nBits) 7513 { 7514 $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES 7515 7516 if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) 7517 { 7518 return ''; 7519 } 7520 7521 // standard allows 128/192/256 bit keys 7522 $ciphertext = base64_decode($ciphertext); 7523 7524 // use AES to encrypt password (mirroring encrypt routine) 7525 $nBytes = $nBits / 8; // no bytes in key 7526 $pwBytes = array(); 7527 7528 for ($i = 0; $i < $nBytes; $i++) 7529 { 7530 $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; 7531 } 7532 7533 $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); 7534 $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long 7535 7536 // recover nonce from 1st element of ciphertext 7537 $counterBlock = array(); 7538 $ctrTxt = substr($ciphertext, 0, 8); 7539 7540 for ($i = 0; $i < 8; $i++) 7541 { 7542 $counterBlock[$i] = ord(substr($ctrTxt, $i, 1)); 7543 } 7544 7545 // generate key schedule 7546 $keySchedule = self::KeyExpansion($key); 7547 7548 // separate ciphertext into blocks (skipping past initial 8 bytes) 7549 $nBlocks = ceil((strlen($ciphertext) - 8) / $blockSize); 7550 $ct = array(); 7551 7552 for ($b = 0; $b < $nBlocks; $b++) 7553 { 7554 $ct[$b] = substr($ciphertext, 8 + $b * $blockSize, 16); 7555 } 7556 7557 $ciphertext = $ct; // ciphertext is now array of block-length strings 7558 7559 // plaintext will get generated block-by-block into array of block-length strings 7560 $plaintxt = array(); 7561 7562 for ($b = 0; $b < $nBlocks; $b++) 7563 { 7564 // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) 7565 for ($c = 0; $c < 4; $c++) 7566 { 7567 $counterBlock[15 - $c] = self::urs($b, $c * 8) & 0xff; 7568 } 7569 7570 for ($c = 0; $c < 4; $c++) 7571 { 7572 $counterBlock[15 - $c - 4] = self::urs(($b + 1) / 0x100000000 - 1, $c * 8) & 0xff; 7573 } 7574 7575 $cipherCntr = self::Cipher($counterBlock, $keySchedule); // encrypt counter block 7576 7577 $plaintxtByte = array(); 7578 7579 for ($i = 0; $i < strlen($ciphertext[$b]); $i++) 7580 { 7581 // -- xor plaintext with ciphered counter byte-by-byte -- 7582 $plaintxtByte[$i] = $cipherCntr[$i] ^ ord(substr($ciphertext[$b], $i, 1)); 7583 $plaintxtByte[$i] = chr($plaintxtByte[$i]); 7584 7585 } 7586 7587 $plaintxt[$b] = implode('', $plaintxtByte); 7588 } 7589 7590 // join array of blocks into single plaintext string 7591 $plaintext = implode('', $plaintxt); 7592 7593 return $plaintext; 7594 } 7595 7596 /** 7597 * AES decryption in CBC mode. This is the standard mode (the CTR methods 7598 * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). 7599 * 7600 * It supports AES-128 only. It assumes that the last 4 bytes 7601 * contain a little-endian unsigned long integer representing the unpadded 7602 * data length. 7603 * 7604 * @since 3.0.1 7605 * @author Nicholas K. Dionysopoulos 7606 * 7607 * @param string $ciphertext The data to encrypt 7608 * @param string $password Encryption password 7609 * 7610 * @return string The plaintext 7611 */ 7612 public static function AESDecryptCBC($ciphertext, $password) 7613 { 7614 $adapter = self::getAdapter(); 7615 7616 if (!$adapter->isSupported()) 7617 { 7618 return false; 7619 } 7620 7621 // Read the data size 7622 $data_size = unpack('V', substr($ciphertext, -4)); 7623 7624 // Do I have a PBKDF2 salt? 7625 $salt = substr($ciphertext, -92, 68); 7626 $rightStringLimit = -4; 7627 7628 $params = self::getKeyDerivationParameters(); 7629 $keySizeBytes = $params['keySize']; 7630 $algorithm = $params['algorithm']; 7631 $iterations = $params['iterations']; 7632 $useStaticSalt = $params['useStaticSalt']; 7633 7634 if (substr($salt, 0, 4) == 'JPST') 7635 { 7636 // We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes 7637 // (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the 7638 // uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the 7639 // format specification). 7640 $salt = substr($salt, 4); 7641 $rightStringLimit -= 68; 7642 7643 $key = self::pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); 7644 } 7645 elseif ($useStaticSalt) 7646 { 7647 // We have a static salt. Use it for PBKDF2. 7648 $key = self::getStaticSaltExpandedKey($password); 7649 } 7650 else 7651 { 7652 // Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD. 7653 $key = self::expandKey($password); 7654 } 7655 7656 // Try to get the IV from the data 7657 $iv = substr($ciphertext, -24, 20); 7658 7659 if (substr($iv, 0, 4) == 'JPIV') 7660 { 7661 // We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes 7662 // (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length) 7663 $iv = substr($iv, 4); 7664 $rightStringLimit -= 20; 7665 } 7666 else 7667 { 7668 // No stored IV. Do it the dumb way. 7669 $iv = self::createTheWrongIV($password); 7670 } 7671 7672 // Decrypt 7673 $plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key); 7674 7675 // Trim padding, if necessary 7676 if (strlen($plaintext) > $data_size) 7677 { 7678 $plaintext = substr($plaintext, 0, $data_size); 7679 } 7680 7681 return $plaintext; 7682 } 7683 7684 /** 7685 * That's the old way of creating an IV that's definitely not cryptographically sound. 7686 * 7687 * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA 7688 * 7689 * @param string $password The raw password from which we create an IV in a super bozo way 7690 * 7691 * @return string A 16-byte IV string 7692 */ 7693 public static function createTheWrongIV($password) 7694 { 7695 static $ivs = array(); 7696 7697 $key = md5($password); 7698 7699 if (!isset($ivs[$key])) 7700 { 7701 $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes 7702 $pwBytes = array(); 7703 for ($i = 0; $i < $nBytes; $i++) 7704 { 7705 $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; 7706 } 7707 $iv = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); 7708 $newIV = ''; 7709 foreach ($iv as $int) 7710 { 7711 $newIV .= chr($int); 7712 } 7713 7714 $ivs[$key] = $newIV; 7715 } 7716 7717 return $ivs[$key]; 7718 } 7719 7720 /** 7721 * Expand the password to an appropriate 128-bit encryption key 7722 * 7723 * @param string $password 7724 * 7725 * @return string 7726 * 7727 * @since 5.2.0 7728 * @author Nicholas K. Dionysopoulos 7729 */ 7730 public static function expandKey($password) 7731 { 7732 // Try to fetch cached key or create it if it doesn't exist 7733 $nBits = 128; 7734 $lookupKey = md5($password . '-' . $nBits); 7735 7736 if (array_key_exists($lookupKey, self::$passwords)) 7737 { 7738 $key = self::$passwords[$lookupKey]; 7739 7740 return $key; 7741 } 7742 7743 // use AES itself to encrypt password to get cipher key (using plain password as source for 7744 // key expansion) - gives us well encrypted key. 7745 $nBytes = $nBits / 8; // Number of bytes in key 7746 $pwBytes = array(); 7747 7748 for ($i = 0; $i < $nBytes; $i++) 7749 { 7750 $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; 7751 } 7752 7753 $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); 7754 $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long 7755 $newKey = ''; 7756 7757 foreach ($key as $int) 7758 { 7759 $newKey .= chr($int); 7760 } 7761 7762 $key = $newKey; 7763 7764 self::$passwords[$lookupKey] = $key; 7765 7766 return $key; 7767 } 7768 7769 /** 7770 * Returns the correct AES-128 CBC encryption adapter 7771 * 7772 * @return AKEncryptionAESAdapterInterface 7773 * 7774 * @since 5.2.0 7775 * @author Nicholas K. Dionysopoulos 7776 */ 7777 public static function getAdapter() 7778 { 7779 static $adapter = null; 7780 7781 if (is_object($adapter) && ($adapter instanceof AKEncryptionAESAdapterInterface)) 7782 { 7783 return $adapter; 7784 } 7785 7786 $adapter = new OpenSSL(); 7787 7788 if (!$adapter->isSupported()) 7789 { 7790 $adapter = new Mcrypt(); 7791 } 7792 7793 return $adapter; 7794 } 7795 7796 /** 7797 * @return string 7798 */ 7799 public static function getPbkdf2Algorithm() 7800 { 7801 return self::$pbkdf2Algorithm; 7802 } 7803 7804 /** 7805 * @param string $pbkdf2Algorithm 7806 * @return void 7807 */ 7808 public static function setPbkdf2Algorithm($pbkdf2Algorithm) 7809 { 7810 self::$pbkdf2Algorithm = $pbkdf2Algorithm; 7811 } 7812 7813 /** 7814 * @return int 7815 */ 7816 public static function getPbkdf2Iterations() 7817 { 7818 return self::$pbkdf2Iterations; 7819 } 7820 7821 /** 7822 * @param int $pbkdf2Iterations 7823 * @return void 7824 */ 7825 public static function setPbkdf2Iterations($pbkdf2Iterations) 7826 { 7827 self::$pbkdf2Iterations = $pbkdf2Iterations; 7828 } 7829 7830 /** 7831 * @return int 7832 */ 7833 public static function getPbkdf2UseStaticSalt() 7834 { 7835 return self::$pbkdf2UseStaticSalt; 7836 } 7837 7838 /** 7839 * @param int $pbkdf2UseStaticSalt 7840 * @return void 7841 */ 7842 public static function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt) 7843 { 7844 self::$pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt; 7845 } 7846 7847 /** 7848 * @return string 7849 */ 7850 public static function getPbkdf2StaticSalt() 7851 { 7852 return self::$pbkdf2StaticSalt; 7853 } 7854 7855 /** 7856 * @param string $pbkdf2StaticSalt 7857 * @return void 7858 */ 7859 public static function setPbkdf2StaticSalt($pbkdf2StaticSalt) 7860 { 7861 self::$pbkdf2StaticSalt = $pbkdf2StaticSalt; 7862 } 7863 7864 /** 7865 * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static 7866 * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block 7867 * to minimize the risk of attacks against the password. 7868 * 7869 * @return array 7870 */ 7871 public static function getKeyDerivationParameters() 7872 { 7873 return array( 7874 'keySize' => 16, 7875 'algorithm' => self::$pbkdf2Algorithm, 7876 'iterations' => self::$pbkdf2Iterations, 7877 'useStaticSalt' => self::$pbkdf2UseStaticSalt, 7878 'staticSalt' => self::$pbkdf2StaticSalt, 7879 ); 7880 } 7881 7882 /** 7883 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt 7884 * 7885 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt 7886 * 7887 * This implementation of PBKDF2 was originally created by https://defuse.ca 7888 * With improvements by http://www.variations-of-shadow.com 7889 * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster) 7890 * 7891 * @param string $password The password. 7892 * @param string $salt A salt that is unique to the password. 7893 * @param string $algorithm The hash algorithm to use. Default is sha1. 7894 * @param int $count Iteration count. Higher is better, but slower. Default: 1000. 7895 * @param int $key_length The length of the derived key in bytes. 7896 * 7897 * @return string A string of $key_length bytes 7898 */ 7899 public static function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16) 7900 { 7901 if (function_exists("hash_pbkdf2")) 7902 { 7903 return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true); 7904 } 7905 7906 $hash_length = akstringlen(hash($algorithm, "", true)); 7907 $block_count = ceil($key_length / $hash_length); 7908 7909 $output = ""; 7910 7911 for ($i = 1; $i <= $block_count; $i++) 7912 { 7913 // $i encoded as 4 bytes, big endian. 7914 $last = $salt . pack("N", $i); 7915 7916 // First iteration 7917 $xorResult = hash_hmac($algorithm, $last, $password, true); 7918 $last = $xorResult; 7919 7920 // Perform the other $count - 1 iterations 7921 for ($j = 1; $j < $count; $j++) 7922 { 7923 $last = hash_hmac($algorithm, $last, $password, true); 7924 $xorResult ^= $last; 7925 } 7926 7927 $output .= $xorResult; 7928 } 7929 7930 return aksubstr($output, 0, $key_length); 7931 } 7932 7933 /** 7934 * Get the expanded key from the user supplied password using a static salt. The results are cached for performance 7935 * reasons. 7936 * 7937 * @param string $password The user-supplied password, UTF-8 encoded. 7938 * 7939 * @return string The expanded key 7940 */ 7941 private static function getStaticSaltExpandedKey($password) 7942 { 7943 $params = self::getKeyDerivationParameters(); 7944 $keySizeBytes = $params['keySize']; 7945 $algorithm = $params['algorithm']; 7946 $iterations = $params['iterations']; 7947 $staticSalt = $params['staticSalt']; 7948 7949 $lookupKey = "PBKDF2-$algorithm-$iterations-" . md5($password . $staticSalt); 7950 7951 if (!array_key_exists($lookupKey, self::$passwords)) 7952 { 7953 self::$passwords[$lookupKey] = self::pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes); 7954 } 7955 7956 return self::$passwords[$lookupKey]; 7957 } 7958 7959} 7960 7961/** 7962 * Akeeba Restore 7963 * A JSON-powered JPA, JPS and ZIP archive extraction library 7964 * 7965 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 7966 * @license GNU GPL v2 or - at your option - any later version 7967 * @package akeebabackup 7968 * @subpackage kickstart 7969 */ 7970 7971/** 7972 * The Master Setup will read the configuration parameters from restoration.php or 7973 * the JSON-encoded "configuration" input variable and return the status. 7974 * 7975 * @return bool True if the master configuration was applied to the Factory object 7976 */ 7977function masterSetup() 7978{ 7979 // ------------------------------------------------------------ 7980 // 1. Import basic setup parameters 7981 // ------------------------------------------------------------ 7982 7983 $ini_data = null; 7984 7985 // In restore.php mode, require restoration.php or fail 7986 if (!defined('KICKSTART')) 7987 { 7988 // This is the standalone mode, used by Akeeba Backup Professional. It looks for a restoration.php 7989 // file to perform its magic. If the file is not there, we will abort. 7990 $setupFile = 'restoration.php'; 7991 7992 if (!file_exists($setupFile)) 7993 { 7994 AKFactory::set('kickstart.enabled', false); 7995 7996 return false; 7997 } 7998 7999 // Load restoration.php. It creates a global variable named $restoration_setup 8000 require_once $setupFile; 8001 8002 $ini_data = $restoration_setup; 8003 8004 if (empty($ini_data)) 8005 { 8006 // No parameters fetched. Darn, how am I supposed to work like that?! 8007 AKFactory::set('kickstart.enabled', false); 8008 8009 return false; 8010 } 8011 8012 AKFactory::set('kickstart.enabled', true); 8013 } 8014 else 8015 { 8016 // Maybe we have $restoration_setup defined in the head of kickstart.php 8017 global $restoration_setup; 8018 8019 if (!empty($restoration_setup) && !is_array($restoration_setup)) 8020 { 8021 $ini_data = AKText::parse_ini_file($restoration_setup, false, true); 8022 } 8023 elseif (is_array($restoration_setup)) 8024 { 8025 $ini_data = $restoration_setup; 8026 } 8027 } 8028 8029 // Import any data from $restoration_setup 8030 if (!empty($ini_data)) 8031 { 8032 foreach ($ini_data as $key => $value) 8033 { 8034 AKFactory::set($key, $value); 8035 } 8036 AKFactory::set('kickstart.enabled', true); 8037 } 8038 8039 // Reinitialize $ini_data 8040 $ini_data = null; 8041 8042 // ------------------------------------------------------------ 8043 // 2. Explode JSON parameters into $_REQUEST scope 8044 // ------------------------------------------------------------ 8045 8046 // Detect a JSON string in the request variable and store it. 8047 $json = getQueryParam('json', null); 8048 8049 // Remove everything from the request, post and get arrays 8050 if (!empty($_REQUEST)) 8051 { 8052 foreach ($_REQUEST as $key => $value) 8053 { 8054 unset($_REQUEST[$key]); 8055 } 8056 } 8057 8058 if (!empty($_POST)) 8059 { 8060 foreach ($_POST as $key => $value) 8061 { 8062 unset($_POST[$key]); 8063 } 8064 } 8065 8066 if (!empty($_GET)) 8067 { 8068 foreach ($_GET as $key => $value) 8069 { 8070 unset($_GET[$key]); 8071 } 8072 } 8073 8074 // Decrypt a possibly encrypted JSON string 8075 $password = AKFactory::get('kickstart.security.password', null); 8076 8077 if (!empty($json)) 8078 { 8079 if (!empty($password)) 8080 { 8081 $json = AKEncryptionAES::AESDecryptCtr($json, $password, 128); 8082 8083 if (empty($json)) 8084 { 8085 die('###{"status":false,"message":"Invalid login"}###'); 8086 } 8087 } 8088 8089 // Get the raw data 8090 $raw = json_decode($json, true); 8091 8092 if (!empty($password) && (empty($raw))) 8093 { 8094 die('###{"status":false,"message":"Invalid login"}###'); 8095 } 8096 8097 // Pass all JSON data to the request array 8098 if (!empty($raw)) 8099 { 8100 foreach ($raw as $key => $value) 8101 { 8102 $_REQUEST[$key] = $value; 8103 } 8104 } 8105 } 8106 elseif (!empty($password)) 8107 { 8108 die('###{"status":false,"message":"Invalid login"}###'); 8109 } 8110 8111 // ------------------------------------------------------------ 8112 // 3. Try the "factory" variable 8113 // ------------------------------------------------------------ 8114 // A "factory" variable will override all other settings. 8115 $serialized = getQueryParam('factory', null); 8116 8117 if (!is_null($serialized)) 8118 { 8119 // Get the serialized factory 8120 AKFactory::unserialize($serialized); 8121 AKFactory::set('kickstart.enabled', true); 8122 8123 return true; 8124 } 8125 8126 // ------------------------------------------------------------ 8127 // 4. Try the configuration variable for Kickstart 8128 // ------------------------------------------------------------ 8129 if (defined('KICKSTART')) 8130 { 8131 $configuration = getQueryParam('configuration'); 8132 8133 if (!is_null($configuration)) 8134 { 8135 // Let's decode the configuration from JSON to array 8136 $ini_data = json_decode($configuration, true); 8137 } 8138 else 8139 { 8140 // Neither exists. Enable Kickstart's interface anyway. 8141 $ini_data = array('kickstart.enabled' => true); 8142 } 8143 8144 // Import any INI data we might have from other sources 8145 if (!empty($ini_data)) 8146 { 8147 foreach ($ini_data as $key => $value) 8148 { 8149 AKFactory::set($key, $value); 8150 } 8151 8152 AKFactory::set('kickstart.enabled', true); 8153 8154 return true; 8155 } 8156 } 8157} 8158 8159/** 8160 * Akeeba Restore 8161 * A JSON-powered JPA, JPS and ZIP archive extraction library 8162 * 8163 * @copyright 2008-2017 Nicholas K. Dionysopoulos / Akeeba Ltd. 8164 * @license GNU GPL v2 or - at your option - any later version 8165 * @package akeebabackup 8166 * @subpackage kickstart 8167 */ 8168 8169// Mini-controller for restore.php 8170if (!defined('KICKSTART')) 8171{ 8172 // The observer class, used to report number of files and bytes processed 8173 class RestorationObserver extends AKAbstractPartObserver 8174 { 8175 public $compressedTotal = 0; 8176 public $uncompressedTotal = 0; 8177 public $filesProcessed = 0; 8178 8179 public function update($object, $message) 8180 { 8181 if (!is_object($message)) 8182 { 8183 return; 8184 } 8185 8186 if (!array_key_exists('type', get_object_vars($message))) 8187 { 8188 return; 8189 } 8190 8191 if ($message->type == 'startfile') 8192 { 8193 $this->filesProcessed++; 8194 $this->compressedTotal += $message->content->compressed; 8195 $this->uncompressedTotal += $message->content->uncompressed; 8196 } 8197 } 8198 8199 public function __toString() 8200 { 8201 return __CLASS__; 8202 } 8203 8204 } 8205 8206 // Import configuration 8207 masterSetup(); 8208 8209 $retArray = array( 8210 'status' => true, 8211 'message' => null 8212 ); 8213 8214 $enabled = AKFactory::get('kickstart.enabled', false); 8215 8216 if ($enabled) 8217 { 8218 $task = getQueryParam('task'); 8219 8220 switch ($task) 8221 { 8222 case 'ping': 8223 // ping task - realy does nothing! 8224 $timer = AKFactory::getTimer(); 8225 $timer->enforce_min_exec_time(); 8226 break; 8227 8228 /** 8229 * There are two separate steps here since we were using an inefficient restoration intialization method in 8230 * the past. Now both startRestore and stepRestore are identical. The difference in behavior depends 8231 * exclusively on the calling Javascript. If no serialized factory was passed in the request then we start a 8232 * new restoration. If a serialized factory was passed in the request then the restoration is resumed. For 8233 * this reason we should NEVER call AKFactory::nuke() in startRestore anymore: that would simply reset the 8234 * extraction engine configuration which was done in masterSetup() leading to an error about the file being 8235 * invalid (since no file is found). 8236 */ 8237 case 'startRestore': 8238 case 'stepRestore': 8239 $engine = AKFactory::getUnarchiver(); // Get the engine 8240 $observer = new RestorationObserver(); // Create a new observer 8241 $engine->attach($observer); // Attach the observer 8242 $engine->tick(); 8243 $ret = $engine->getStatusArray(); 8244 8245 if ($ret['Error'] != '') 8246 { 8247 $retArray['status'] = false; 8248 $retArray['done'] = true; 8249 $retArray['message'] = $ret['Error']; 8250 } 8251 elseif (!$ret['HasRun']) 8252 { 8253 $retArray['files'] = $observer->filesProcessed; 8254 $retArray['bytesIn'] = $observer->compressedTotal; 8255 $retArray['bytesOut'] = $observer->uncompressedTotal; 8256 $retArray['status'] = true; 8257 $retArray['done'] = true; 8258 } 8259 else 8260 { 8261 $retArray['files'] = $observer->filesProcessed; 8262 $retArray['bytesIn'] = $observer->compressedTotal; 8263 $retArray['bytesOut'] = $observer->uncompressedTotal; 8264 $retArray['status'] = true; 8265 $retArray['done'] = false; 8266 $retArray['factory'] = AKFactory::serialize(); 8267 } 8268 break; 8269 8270 case 'finalizeRestore': 8271 $root = AKFactory::get('kickstart.setup.destdir'); 8272 // Remove the installation directory 8273 recursive_remove_directory($root . '/installation'); 8274 8275 $postproc = AKFactory::getPostProc(); 8276 8277 /** 8278 * Should I rename the htaccess.bak and web.config.bak files back to their live filenames...? 8279 */ 8280 $renameFiles = AKFactory::get('kickstart.setup.postrenamefiles', true); 8281 8282 if ($renameFiles) 8283 { 8284 // Rename htaccess.bak to .htaccess 8285 if (file_exists($root . '/htaccess.bak')) 8286 { 8287 if (file_exists($root . '/.htaccess')) 8288 { 8289 $postproc->unlink($root . '/.htaccess'); 8290 } 8291 $postproc->rename($root . '/htaccess.bak', $root . '/.htaccess'); 8292 } 8293 8294 // Rename htaccess.bak to .htaccess 8295 if (file_exists($root . '/web.config.bak')) 8296 { 8297 if (file_exists($root . '/web.config')) 8298 { 8299 $postproc->unlink($root . '/web.config'); 8300 } 8301 $postproc->rename($root . '/web.config.bak', $root . '/web.config'); 8302 } 8303 } 8304 8305 // Remove restoration.php 8306 $basepath = KSROOTDIR; 8307 $basepath = rtrim(str_replace('\\', '/', $basepath), '/'); 8308 if (!empty($basepath)) 8309 { 8310 $basepath .= '/'; 8311 } 8312 $postproc->unlink($basepath . 'restoration.php'); 8313 8314 // Import a custom finalisation file 8315 $filename = dirname(__FILE__) . '/restore_finalisation.php'; 8316 if (file_exists($filename)) 8317 { 8318 // opcode cache busting before including the filename 8319 if (function_exists('opcache_invalidate')) 8320 { 8321 opcache_invalidate($filename); 8322 } 8323 if (function_exists('apc_compile_file')) 8324 { 8325 apc_compile_file($filename); 8326 } 8327 if (function_exists('wincache_refresh_if_changed')) 8328 { 8329 wincache_refresh_if_changed(array($filename)); 8330 } 8331 if (function_exists('xcache_asm')) 8332 { 8333 xcache_asm($filename); 8334 } 8335 include_once $filename; 8336 } 8337 8338 // Run a custom finalisation script 8339 if (function_exists('finalizeRestore')) 8340 { 8341 finalizeRestore($root, $basepath); 8342 } 8343 break; 8344 8345 default: 8346 // Invalid task! 8347 $enabled = false; 8348 break; 8349 } 8350 } 8351 8352 // Maybe we weren't authorized or the task was invalid? 8353 if (!$enabled) 8354 { 8355 // Maybe the user failed to enter any information 8356 $retArray['status'] = false; 8357 $retArray['message'] = AKText::_('ERR_INVALID_LOGIN'); 8358 } 8359 8360 // JSON encode the message 8361 $json = json_encode($retArray); 8362 // Do I have to encrypt? 8363 $password = AKFactory::get('kickstart.security.password', null); 8364 if (!empty($password)) 8365 { 8366 $json = AKEncryptionAES::AESEncryptCtr($json, $password, 128); 8367 } 8368 8369 // Return the message 8370 echo "###$json###"; 8371 8372} 8373 8374// ------------ lixlpixel recursive PHP functions ------------- 8375// recursive_remove_directory( directory to delete, empty ) 8376// expects path to directory and optional TRUE / FALSE to empty 8377// of course PHP has to have the rights to delete the directory 8378// you specify and all files and folders inside the directory 8379// ------------------------------------------------------------ 8380function recursive_remove_directory($directory) 8381{ 8382 // if the path has a slash at the end we remove it here 8383 if (substr($directory, -1) == '/') 8384 { 8385 $directory = substr($directory, 0, -1); 8386 } 8387 // if the path is not valid or is not a directory ... 8388 if (!file_exists($directory) || !is_dir($directory)) 8389 { 8390 // ... we return false and exit the function 8391 return false; 8392 // ... if the path is not readable 8393 } 8394 elseif (!is_readable($directory)) 8395 { 8396 // ... we return false and exit the function 8397 return false; 8398 // ... else if the path is readable 8399 } 8400 else 8401 { 8402 // we open the directory 8403 $handle = opendir($directory); 8404 $postproc = AKFactory::getPostProc(); 8405 // and scan through the items inside 8406 while (false !== ($item = readdir($handle))) 8407 { 8408 // if the filepointer is not the current directory 8409 // or the parent directory 8410 if ($item != '.' && $item != '..') 8411 { 8412 // we build the new path to delete 8413 $path = $directory . '/' . $item; 8414 // if the new path is a directory 8415 if (is_dir($path)) 8416 { 8417 // we call this function with the new path 8418 recursive_remove_directory($path); 8419 // if the new path is a file 8420 } 8421 else 8422 { 8423 // we remove the file 8424 $postproc->unlink($path); 8425 } 8426 } 8427 } 8428 // close the directory 8429 closedir($handle); 8430 // try to delete the now empty directory 8431 if (!$postproc->rmdir($directory)) 8432 { 8433 // return false if not possible 8434 return false; 8435 } 8436 8437 // return success 8438 return true; 8439 } 8440} 8441