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'         => "&lt;up one level&gt;",
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'                         => '&lt;root&gt;',
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