PageRenderTime 77ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/administrator/components/com_akeeba/restore.php

https://bitbucket.org/kraymitchell/saiu
PHP | 5865 lines | 4044 code | 679 blank | 1142 comment | 684 complexity | 1c93f78666664f6f5e8702e6f1b6d316 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, BSD-3-Clause, LGPL-2.1, GPL-3.0
  1. <?php
  2. /**
  3. * Akeeba Restore
  4. * A JSON-powered JPA, JPS and ZIP archive extraction library
  5. *
  6. * @copyright 2010-2012 Nicholas K. Dionysopoulos / AkeebaBackup.com
  7. * @license GNU GPL v2 or - at your option - any later version
  8. * @package akeebabackup
  9. * @subpackage kickstart
  10. */
  11. define('_AKEEBA_RESTORATION', 1);
  12. defined('DS') or define('DS', DIRECTORY_SEPARATOR);
  13. // Unarchiver run states
  14. define('AK_STATE_NOFILE', 0); // File header not read yet
  15. define('AK_STATE_HEADER', 1); // File header read; ready to process data
  16. define('AK_STATE_DATA', 2); // Processing file data
  17. define('AK_STATE_DATAREAD', 3); // Finished processing file data; ready to post-process
  18. define('AK_STATE_POSTPROC', 4); // Post-processing
  19. define('AK_STATE_DONE', 5); // Done with post-processing
  20. /* Windows system detection */
  21. if(!defined('_AKEEBA_IS_WINDOWS'))
  22. {
  23. if (function_exists('php_uname'))
  24. define('_AKEEBA_IS_WINDOWS', stristr(php_uname(), 'windows'));
  25. else
  26. define('_AKEEBA_IS_WINDOWS', DIRECTORY_SEPARATOR == '\\');
  27. }
  28. // Make sure the locale is correct for basename() to work
  29. if(function_exists('setlocale'))
  30. {
  31. @setlocale(LC_ALL, 'en_US.UTF8');
  32. }
  33. // fnmatch not available on non-POSIX systems
  34. // Thanks to soywiz@php.net for this usefull alternative function [http://gr2.php.net/fnmatch]
  35. if (!function_exists('fnmatch')) {
  36. function fnmatch($pattern, $string) {
  37. return @preg_match(
  38. '/^' . strtr(addcslashes($pattern, '/\\.+^$(){}=!<>|'),
  39. array('*' => '.*', '?' => '.?')) . '$/i', $string
  40. );
  41. }
  42. }
  43. // Unicode-safe binary data length function
  44. if(function_exists('mb_strlen')) {
  45. function akstringlen($string) { return mb_strlen($string,'8bit'); }
  46. } else {
  47. function akstringlen($string) { return strlen($string); }
  48. }
  49. /**
  50. * Gets a query parameter from GET or POST data
  51. * @param $key
  52. * @param $default
  53. */
  54. function getQueryParam( $key, $default = null )
  55. {
  56. $value = null;
  57. if(array_key_exists($key, $_REQUEST)) {
  58. $value = $_REQUEST[$key];
  59. } elseif(array_key_exists($key, $_POST)) {
  60. $value = $_POST[$key];
  61. } elseif(array_key_exists($key, $_GET)) {
  62. $value = $_GET[$key];
  63. } else {
  64. return $default;
  65. }
  66. if(get_magic_quotes_gpc() && !is_null($value)) $value=stripslashes($value);
  67. return $value;
  68. }
  69. // Debugging function
  70. function debugMsg($msg)
  71. {
  72. if(!defined('KSDEBUG')) return;
  73. $fp = fopen('debug.txt','at');
  74. fwrite($fp, $msg."\n");
  75. fclose($fp);
  76. }
  77. /**
  78. * Akeeba Backup's JSON compatibility layer
  79. *
  80. * On systems where json_encode and json_decode are not available, Akeeba
  81. * Backup will attempt to use PEAR's Services_JSON library to emulate them.
  82. * A copy of this library is included in this file and will be used if and
  83. * only if it isn't already loaded, e.g. due to PEAR's auto-loading, or a
  84. * 3PD extension loading it for its own purposes.
  85. */
  86. /**
  87. * Converts to and from JSON format.
  88. *
  89. * JSON (JavaScript Object Notation) is a lightweight data-interchange
  90. * format. It is easy for humans to read and write. It is easy for machines
  91. * to parse and generate. It is based on a subset of the JavaScript
  92. * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
  93. * This feature can also be found in Python. JSON is a text format that is
  94. * completely language independent but uses conventions that are familiar
  95. * to programmers of the C-family of languages, including C, C++, C#, Java,
  96. * JavaScript, Perl, TCL, and many others. These properties make JSON an
  97. * ideal data-interchange language.
  98. *
  99. * This package provides a simple encoder and decoder for JSON notation. It
  100. * is intended for use with client-side Javascript applications that make
  101. * use of HTTPRequest to perform server communication functions - data can
  102. * be encoded into JSON notation for use in a client-side javascript, or
  103. * decoded from incoming Javascript requests. JSON format is native to
  104. * Javascript, and can be directly eval()'ed with no further parsing
  105. * overhead
  106. *
  107. * All strings should be in ASCII or UTF-8 format!
  108. *
  109. * LICENSE: Redistribution and use in source and binary forms, with or
  110. * without modification, are permitted provided that the following
  111. * conditions are met: Redistributions of source code must retain the
  112. * above copyright notice, this list of conditions and the following
  113. * disclaimer. Redistributions in binary form must reproduce the above
  114. * copyright notice, this list of conditions and the following disclaimer
  115. * in the documentation and/or other materials provided with the
  116. * distribution.
  117. *
  118. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
  119. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  120. * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
  121. * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  122. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  123. * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  124. * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  125. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
  126. * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
  127. * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
  128. * DAMAGE.
  129. *
  130. * @category
  131. * @package Services_JSON
  132. * @author Michal Migurski <mike-json@teczno.com>
  133. * @author Matt Knapp <mdknapp[at]gmail[dot]com>
  134. * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
  135. * @copyright 2005 Michal Migurski
  136. * @version CVS: $Id: restore.php 612 2011-05-19 08:26:26Z nikosdion $
  137. * @license http://www.opensource.org/licenses/bsd-license.php
  138. * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
  139. */
  140. if(!defined('JSON_FORCE_OBJECT'))
  141. {
  142. define('JSON_FORCE_OBJECT', 1);
  143. }
  144. if(!defined('SERVICES_JSON_SLICE'))
  145. {
  146. /**
  147. * Marker constant for Services_JSON::decode(), used to flag stack state
  148. */
  149. define('SERVICES_JSON_SLICE', 1);
  150. /**
  151. * Marker constant for Services_JSON::decode(), used to flag stack state
  152. */
  153. define('SERVICES_JSON_IN_STR', 2);
  154. /**
  155. * Marker constant for Services_JSON::decode(), used to flag stack state
  156. */
  157. define('SERVICES_JSON_IN_ARR', 3);
  158. /**
  159. * Marker constant for Services_JSON::decode(), used to flag stack state
  160. */
  161. define('SERVICES_JSON_IN_OBJ', 4);
  162. /**
  163. * Marker constant for Services_JSON::decode(), used to flag stack state
  164. */
  165. define('SERVICES_JSON_IN_CMT', 5);
  166. /**
  167. * Behavior switch for Services_JSON::decode()
  168. */
  169. define('SERVICES_JSON_LOOSE_TYPE', 16);
  170. /**
  171. * Behavior switch for Services_JSON::decode()
  172. */
  173. define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
  174. }
  175. /**
  176. * Converts to and from JSON format.
  177. *
  178. * Brief example of use:
  179. *
  180. * <code>
  181. * // create a new instance of Services_JSON
  182. * $json = new Services_JSON();
  183. *
  184. * // convert a complexe value to JSON notation, and send it to the browser
  185. * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
  186. * $output = $json->encode($value);
  187. *
  188. * print($output);
  189. * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
  190. *
  191. * // accept incoming POST data, assumed to be in JSON notation
  192. * $input = file_get_contents('php://input', 1000000);
  193. * $value = $json->decode($input);
  194. * </code>
  195. */
  196. if(!class_exists('Akeeba_Services_JSON'))
  197. {
  198. class Akeeba_Services_JSON
  199. {
  200. /**
  201. * constructs a new JSON instance
  202. *
  203. * @param int $use object behavior flags; combine with boolean-OR
  204. *
  205. * possible values:
  206. * - SERVICES_JSON_LOOSE_TYPE: loose typing.
  207. * "{...}" syntax creates associative arrays
  208. * instead of objects in decode().
  209. * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
  210. * Values which can't be encoded (e.g. resources)
  211. * appear as NULL instead of throwing errors.
  212. * By default, a deeply-nested resource will
  213. * bubble up with an error, so all return values
  214. * from encode() should be checked with isError()
  215. */
  216. function Akeeba_Services_JSON($use = 0)
  217. {
  218. $this->use = $use;
  219. }
  220. /**
  221. * convert a string from one UTF-16 char to one UTF-8 char
  222. *
  223. * Normally should be handled by mb_convert_encoding, but
  224. * provides a slower PHP-only method for installations
  225. * that lack the multibye string extension.
  226. *
  227. * @param string $utf16 UTF-16 character
  228. * @return string UTF-8 character
  229. * @access private
  230. */
  231. function utf162utf8($utf16)
  232. {
  233. // oh please oh please oh please oh please oh please
  234. if(function_exists('mb_convert_encoding')) {
  235. return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
  236. }
  237. $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
  238. switch(true) {
  239. case ((0x7F & $bytes) == $bytes):
  240. // this case should never be reached, because we are in ASCII range
  241. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  242. return chr(0x7F & $bytes);
  243. case (0x07FF & $bytes) == $bytes:
  244. // return a 2-byte UTF-8 character
  245. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  246. return chr(0xC0 | (($bytes >> 6) & 0x1F))
  247. . chr(0x80 | ($bytes & 0x3F));
  248. case (0xFFFF & $bytes) == $bytes:
  249. // return a 3-byte UTF-8 character
  250. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  251. return chr(0xE0 | (($bytes >> 12) & 0x0F))
  252. . chr(0x80 | (($bytes >> 6) & 0x3F))
  253. . chr(0x80 | ($bytes & 0x3F));
  254. }
  255. // ignoring UTF-32 for now, sorry
  256. return '';
  257. }
  258. /**
  259. * convert a string from one UTF-8 char to one UTF-16 char
  260. *
  261. * Normally should be handled by mb_convert_encoding, but
  262. * provides a slower PHP-only method for installations
  263. * that lack the multibye string extension.
  264. *
  265. * @param string $utf8 UTF-8 character
  266. * @return string UTF-16 character
  267. * @access private
  268. */
  269. function utf82utf16($utf8)
  270. {
  271. // oh please oh please oh please oh please oh please
  272. if(function_exists('mb_convert_encoding')) {
  273. return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
  274. }
  275. switch(strlen($utf8)) {
  276. case 1:
  277. // this case should never be reached, because we are in ASCII range
  278. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  279. return $utf8;
  280. case 2:
  281. // return a UTF-16 character from a 2-byte UTF-8 char
  282. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  283. return chr(0x07 & (ord($utf8{0}) >> 2))
  284. . chr((0xC0 & (ord($utf8{0}) << 6))
  285. | (0x3F & ord($utf8{1})));
  286. case 3:
  287. // return a UTF-16 character from a 3-byte UTF-8 char
  288. // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  289. return chr((0xF0 & (ord($utf8{0}) << 4))
  290. | (0x0F & (ord($utf8{1}) >> 2)))
  291. . chr((0xC0 & (ord($utf8{1}) << 6))
  292. | (0x7F & ord($utf8{2})));
  293. }
  294. // ignoring UTF-32 for now, sorry
  295. return '';
  296. }
  297. /**
  298. * encodes an arbitrary variable into JSON format
  299. *
  300. * @param mixed $var any number, boolean, string, array, or object to be encoded.
  301. * see argument 1 to Services_JSON() above for array-parsing behavior.
  302. * if var is a strng, note that encode() always expects it
  303. * to be in ASCII or UTF-8 format!
  304. *
  305. * @return mixed JSON string representation of input var or an error if a problem occurs
  306. * @access public
  307. */
  308. function encode($var)
  309. {
  310. switch (gettype($var)) {
  311. case 'boolean':
  312. return $var ? 'true' : 'false';
  313. case 'NULL':
  314. return 'null';
  315. case 'integer':
  316. return (int) $var;
  317. case 'double':
  318. case 'float':
  319. return (float) $var;
  320. case 'string':
  321. // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
  322. $ascii = '';
  323. $strlen_var = strlen($var);
  324. /*
  325. * Iterate over every character in the string,
  326. * escaping with a slash or encoding to UTF-8 where necessary
  327. */
  328. for ($c = 0; $c < $strlen_var; ++$c) {
  329. $ord_var_c = ord($var{$c});
  330. switch (true) {
  331. case $ord_var_c == 0x08:
  332. $ascii .= '\b';
  333. break;
  334. case $ord_var_c == 0x09:
  335. $ascii .= '\t';
  336. break;
  337. case $ord_var_c == 0x0A:
  338. $ascii .= '\n';
  339. break;
  340. case $ord_var_c == 0x0C:
  341. $ascii .= '\f';
  342. break;
  343. case $ord_var_c == 0x0D:
  344. $ascii .= '\r';
  345. break;
  346. case $ord_var_c == 0x22:
  347. case $ord_var_c == 0x2F:
  348. case $ord_var_c == 0x5C:
  349. // double quote, slash, slosh
  350. $ascii .= '\\'.$var{$c};
  351. break;
  352. case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
  353. // characters U-00000000 - U-0000007F (same as ASCII)
  354. $ascii .= $var{$c};
  355. break;
  356. case (($ord_var_c & 0xE0) == 0xC0):
  357. // characters U-00000080 - U-000007FF, mask 110XXXXX
  358. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  359. $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
  360. $c += 1;
  361. $utf16 = $this->utf82utf16($char);
  362. $ascii .= sprintf('\u%04s', bin2hex($utf16));
  363. break;
  364. case (($ord_var_c & 0xF0) == 0xE0):
  365. // characters U-00000800 - U-0000FFFF, mask 1110XXXX
  366. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  367. $char = pack('C*', $ord_var_c,
  368. ord($var{$c + 1}),
  369. ord($var{$c + 2}));
  370. $c += 2;
  371. $utf16 = $this->utf82utf16($char);
  372. $ascii .= sprintf('\u%04s', bin2hex($utf16));
  373. break;
  374. case (($ord_var_c & 0xF8) == 0xF0):
  375. // characters U-00010000 - U-001FFFFF, mask 11110XXX
  376. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  377. $char = pack('C*', $ord_var_c,
  378. ord($var{$c + 1}),
  379. ord($var{$c + 2}),
  380. ord($var{$c + 3}));
  381. $c += 3;
  382. $utf16 = $this->utf82utf16($char);
  383. $ascii .= sprintf('\u%04s', bin2hex($utf16));
  384. break;
  385. case (($ord_var_c & 0xFC) == 0xF8):
  386. // characters U-00200000 - U-03FFFFFF, mask 111110XX
  387. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  388. $char = pack('C*', $ord_var_c,
  389. ord($var{$c + 1}),
  390. ord($var{$c + 2}),
  391. ord($var{$c + 3}),
  392. ord($var{$c + 4}));
  393. $c += 4;
  394. $utf16 = $this->utf82utf16($char);
  395. $ascii .= sprintf('\u%04s', bin2hex($utf16));
  396. break;
  397. case (($ord_var_c & 0xFE) == 0xFC):
  398. // characters U-04000000 - U-7FFFFFFF, mask 1111110X
  399. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  400. $char = pack('C*', $ord_var_c,
  401. ord($var{$c + 1}),
  402. ord($var{$c + 2}),
  403. ord($var{$c + 3}),
  404. ord($var{$c + 4}),
  405. ord($var{$c + 5}));
  406. $c += 5;
  407. $utf16 = $this->utf82utf16($char);
  408. $ascii .= sprintf('\u%04s', bin2hex($utf16));
  409. break;
  410. }
  411. }
  412. return '"'.$ascii.'"';
  413. case 'array':
  414. /*
  415. * As per JSON spec if any array key is not an integer
  416. * we must treat the the whole array as an object. We
  417. * also try to catch a sparsely populated associative
  418. * array with numeric keys here because some JS engines
  419. * will create an array with empty indexes up to
  420. * max_index which can cause memory issues and because
  421. * the keys, which may be relevant, will be remapped
  422. * otherwise.
  423. *
  424. * As per the ECMA and JSON specification an object may
  425. * have any string as a property. Unfortunately due to
  426. * a hole in the ECMA specification if the key is a
  427. * ECMA reserved word or starts with a digit the
  428. * parameter is only accessible using ECMAScript's
  429. * bracket notation.
  430. */
  431. // treat as a JSON object
  432. if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
  433. $properties = array_map(array($this, 'name_value'),
  434. array_keys($var),
  435. array_values($var));
  436. foreach($properties as $property) {
  437. if(Akeeba_Services_JSON::isError($property)) {
  438. return $property;
  439. }
  440. }
  441. return '{' . join(',', $properties) . '}';
  442. }
  443. // treat it like a regular array
  444. $elements = array_map(array($this, 'encode'), $var);
  445. foreach($elements as $element) {
  446. if(Akeeba_Services_JSON::isError($element)) {
  447. return $element;
  448. }
  449. }
  450. return '[' . join(',', $elements) . ']';
  451. case 'object':
  452. $vars = get_object_vars($var);
  453. $properties = array_map(array($this, 'name_value'),
  454. array_keys($vars),
  455. array_values($vars));
  456. foreach($properties as $property) {
  457. if(Akeeba_Services_JSON::isError($property)) {
  458. return $property;
  459. }
  460. }
  461. return '{' . join(',', $properties) . '}';
  462. default:
  463. return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
  464. ? 'null'
  465. : new Akeeba_Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
  466. }
  467. }
  468. /**
  469. * array-walking function for use in generating JSON-formatted name-value pairs
  470. *
  471. * @param string $name name of key to use
  472. * @param mixed $value reference to an array element to be encoded
  473. *
  474. * @return string JSON-formatted name-value pair, like '"name":value'
  475. * @access private
  476. */
  477. function name_value($name, $value)
  478. {
  479. $encoded_value = $this->encode($value);
  480. if(Akeeba_Services_JSON::isError($encoded_value)) {
  481. return $encoded_value;
  482. }
  483. return $this->encode(strval($name)) . ':' . $encoded_value;
  484. }
  485. /**
  486. * reduce a string by removing leading and trailing comments and whitespace
  487. *
  488. * @param $str string string value to strip of comments and whitespace
  489. *
  490. * @return string string value stripped of comments and whitespace
  491. * @access private
  492. */
  493. function reduce_string($str)
  494. {
  495. $str = preg_replace(array(
  496. // eliminate single line comments in '// ...' form
  497. '#^\s*//(.+)$#m',
  498. // eliminate multi-line comments in '/* ... */' form, at start of string
  499. '#^\s*/\*(.+)\*/#Us',
  500. // eliminate multi-line comments in '/* ... */' form, at end of string
  501. '#/\*(.+)\*/\s*$#Us'
  502. ), '', $str);
  503. // eliminate extraneous space
  504. return trim($str);
  505. }
  506. /**
  507. * decodes a JSON string into appropriate variable
  508. *
  509. * @param string $str JSON-formatted string
  510. *
  511. * @return mixed number, boolean, string, array, or object
  512. * corresponding to given JSON input string.
  513. * See argument 1 to Akeeba_Services_JSON() above for object-output behavior.
  514. * Note that decode() always returns strings
  515. * in ASCII or UTF-8 format!
  516. * @access public
  517. */
  518. function decode($str)
  519. {
  520. $str = $this->reduce_string($str);
  521. switch (strtolower($str)) {
  522. case 'true':
  523. return true;
  524. case 'false':
  525. return false;
  526. case 'null':
  527. return null;
  528. default:
  529. $m = array();
  530. if (is_numeric($str)) {
  531. // Lookie-loo, it's a number
  532. // This would work on its own, but I'm trying to be
  533. // good about returning integers where appropriate:
  534. // return (float)$str;
  535. // Return float or int, as appropriate
  536. return ((float)$str == (integer)$str)
  537. ? (integer)$str
  538. : (float)$str;
  539. } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
  540. // STRINGS RETURNED IN UTF-8 FORMAT
  541. $delim = substr($str, 0, 1);
  542. $chrs = substr($str, 1, -1);
  543. $utf8 = '';
  544. $strlen_chrs = strlen($chrs);
  545. for ($c = 0; $c < $strlen_chrs; ++$c) {
  546. $substr_chrs_c_2 = substr($chrs, $c, 2);
  547. $ord_chrs_c = ord($chrs{$c});
  548. switch (true) {
  549. case $substr_chrs_c_2 == '\b':
  550. $utf8 .= chr(0x08);
  551. ++$c;
  552. break;
  553. case $substr_chrs_c_2 == '\t':
  554. $utf8 .= chr(0x09);
  555. ++$c;
  556. break;
  557. case $substr_chrs_c_2 == '\n':
  558. $utf8 .= chr(0x0A);
  559. ++$c;
  560. break;
  561. case $substr_chrs_c_2 == '\f':
  562. $utf8 .= chr(0x0C);
  563. ++$c;
  564. break;
  565. case $substr_chrs_c_2 == '\r':
  566. $utf8 .= chr(0x0D);
  567. ++$c;
  568. break;
  569. case $substr_chrs_c_2 == '\\"':
  570. case $substr_chrs_c_2 == '\\\'':
  571. case $substr_chrs_c_2 == '\\\\':
  572. case $substr_chrs_c_2 == '\\/':
  573. if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
  574. ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
  575. $utf8 .= $chrs{++$c};
  576. }
  577. break;
  578. case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
  579. // single, escaped unicode character
  580. $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
  581. . chr(hexdec(substr($chrs, ($c + 4), 2)));
  582. $utf8 .= $this->utf162utf8($utf16);
  583. $c += 5;
  584. break;
  585. case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
  586. $utf8 .= $chrs{$c};
  587. break;
  588. case ($ord_chrs_c & 0xE0) == 0xC0:
  589. // characters U-00000080 - U-000007FF, mask 110XXXXX
  590. //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  591. $utf8 .= substr($chrs, $c, 2);
  592. ++$c;
  593. break;
  594. case ($ord_chrs_c & 0xF0) == 0xE0:
  595. // characters U-00000800 - U-0000FFFF, mask 1110XXXX
  596. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  597. $utf8 .= substr($chrs, $c, 3);
  598. $c += 2;
  599. break;
  600. case ($ord_chrs_c & 0xF8) == 0xF0:
  601. // characters U-00010000 - U-001FFFFF, mask 11110XXX
  602. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  603. $utf8 .= substr($chrs, $c, 4);
  604. $c += 3;
  605. break;
  606. case ($ord_chrs_c & 0xFC) == 0xF8:
  607. // characters U-00200000 - U-03FFFFFF, mask 111110XX
  608. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  609. $utf8 .= substr($chrs, $c, 5);
  610. $c += 4;
  611. break;
  612. case ($ord_chrs_c & 0xFE) == 0xFC:
  613. // characters U-04000000 - U-7FFFFFFF, mask 1111110X
  614. // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
  615. $utf8 .= substr($chrs, $c, 6);
  616. $c += 5;
  617. break;
  618. }
  619. }
  620. return $utf8;
  621. } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
  622. // array, or object notation
  623. if ($str{0} == '[') {
  624. $stk = array(SERVICES_JSON_IN_ARR);
  625. $arr = array();
  626. } else {
  627. if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
  628. $stk = array(SERVICES_JSON_IN_OBJ);
  629. $obj = array();
  630. } else {
  631. $stk = array(SERVICES_JSON_IN_OBJ);
  632. $obj = new stdClass();
  633. }
  634. }
  635. array_push($stk, array('what' => SERVICES_JSON_SLICE,
  636. 'where' => 0,
  637. 'delim' => false));
  638. $chrs = substr($str, 1, -1);
  639. $chrs = $this->reduce_string($chrs);
  640. if ($chrs == '') {
  641. if (reset($stk) == SERVICES_JSON_IN_ARR) {
  642. return $arr;
  643. } else {
  644. return $obj;
  645. }
  646. }
  647. //print("\nparsing {$chrs}\n");
  648. $strlen_chrs = strlen($chrs);
  649. for ($c = 0; $c <= $strlen_chrs; ++$c) {
  650. $top = end($stk);
  651. $substr_chrs_c_2 = substr($chrs, $c, 2);
  652. if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
  653. // found a comma that is not inside a string, array, etc.,
  654. // OR we've reached the end of the character list
  655. $slice = substr($chrs, $top['where'], ($c - $top['where']));
  656. array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
  657. //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
  658. if (reset($stk) == SERVICES_JSON_IN_ARR) {
  659. // we are in an array, so just push an element onto the stack
  660. array_push($arr, $this->decode($slice));
  661. } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
  662. // we are in an object, so figure
  663. // out the property name and set an
  664. // element in an associative array,
  665. // for now
  666. $parts = array();
  667. if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
  668. // "name":value pair
  669. $key = $this->decode($parts[1]);
  670. $val = $this->decode($parts[2]);
  671. if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
  672. $obj[$key] = $val;
  673. } else {
  674. $obj->$key = $val;
  675. }
  676. } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
  677. // name:value pair, where name is unquoted
  678. $key = $parts[1];
  679. $val = $this->decode($parts[2]);
  680. if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
  681. $obj[$key] = $val;
  682. } else {
  683. $obj->$key = $val;
  684. }
  685. }
  686. }
  687. } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
  688. // found a quote, and we are not inside a string
  689. array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
  690. //print("Found start of string at {$c}\n");
  691. } elseif (($chrs{$c} == $top['delim']) &&
  692. ($top['what'] == SERVICES_JSON_IN_STR) &&
  693. ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
  694. // found a quote, we're in a string, and it's not escaped
  695. // we know that it's not escaped becase there is _not_ an
  696. // odd number of backslashes at the end of the string so far
  697. array_pop($stk);
  698. //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
  699. } elseif (($chrs{$c} == '[') &&
  700. in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
  701. // found a left-bracket, and we are in an array, object, or slice
  702. array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
  703. //print("Found start of array at {$c}\n");
  704. } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
  705. // found a right-bracket, and we're in an array
  706. array_pop($stk);
  707. //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
  708. } elseif (($chrs{$c} == '{') &&
  709. in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
  710. // found a left-brace, and we are in an array, object, or slice
  711. array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
  712. //print("Found start of object at {$c}\n");
  713. } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
  714. // found a right-brace, and we're in an object
  715. array_pop($stk);
  716. //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
  717. } elseif (($substr_chrs_c_2 == '/*') &&
  718. in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
  719. // found a comment start, and we are in an array, object, or slice
  720. array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
  721. $c++;
  722. //print("Found start of comment at {$c}\n");
  723. } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
  724. // found a comment end, and we're in one now
  725. array_pop($stk);
  726. $c++;
  727. for ($i = $top['where']; $i <= $c; ++$i)
  728. $chrs = substr_replace($chrs, ' ', $i, 1);
  729. //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
  730. }
  731. }
  732. if (reset($stk) == SERVICES_JSON_IN_ARR) {
  733. return $arr;
  734. } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
  735. return $obj;
  736. }
  737. }
  738. }
  739. }
  740. function isError($data, $code = null)
  741. {
  742. if (class_exists('pear')) {
  743. return PEAR::isError($data, $code);
  744. } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
  745. is_subclass_of($data, 'services_json_error'))) {
  746. return true;
  747. }
  748. return false;
  749. }
  750. }
  751. class Akeeba_Services_JSON_Error
  752. {
  753. function Akeeba_Services_JSON_Error($message = 'unknown error', $code = null,
  754. $mode = null, $options = null, $userinfo = null)
  755. {
  756. }
  757. }
  758. }
  759. if(!function_exists('json_encode'))
  760. {
  761. function json_encode($value, $options = 0) {
  762. $flags = SERVICES_JSON_LOOSE_TYPE;
  763. if( $options & JSON_FORCE_OBJECT ) $flags = 0;
  764. $encoder = new Akeeba_Services_JSON($flags);
  765. return $encoder->encode($value);
  766. }
  767. }
  768. if(!function_exists('json_decode'))
  769. {
  770. function json_decode($value, $assoc = false)
  771. {
  772. $flags = 0;
  773. if($assoc) $flags = SERVICES_JSON_LOOSE_TYPE;
  774. $decoder = new Akeeba_Services_JSON($flags);
  775. return $decoder->decode($value);
  776. }
  777. }
  778. /**
  779. * The base class of Akeeba Engine objects. Allows for error and warnings logging
  780. * and propagation. Largely based on the Joomla! 1.5 JObject class.
  781. */
  782. abstract class AKAbstractObject
  783. {
  784. /** @var array An array of errors */
  785. private $_errors = array();
  786. /** @var array The queue size of the $_errors array. Set to 0 for infinite size. */
  787. protected $_errors_queue_size = 0;
  788. /** @var array An array of warnings */
  789. private $_warnings = array();
  790. /** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */
  791. protected $_warnings_queue_size = 0;
  792. /**
  793. * Public constructor, makes sure we are instanciated only by the factory class
  794. */
  795. public function __construct()
  796. {
  797. /*
  798. // Assisted Singleton pattern
  799. if(function_exists('debug_backtrace'))
  800. {
  801. $caller=debug_backtrace();
  802. if(
  803. ($caller[1]['class'] != 'AKFactory') &&
  804. ($caller[2]['class'] != 'AKFactory') &&
  805. ($caller[3]['class'] != 'AKFactory') &&
  806. ($caller[4]['class'] != 'AKFactory')
  807. ) {
  808. var_dump(debug_backtrace());
  809. trigger_error("You can't create direct descendants of ".__CLASS__, E_USER_ERROR);
  810. }
  811. }
  812. */
  813. }
  814. /**
  815. * Get the most recent error message
  816. * @param integer $i Optional error index
  817. * @return string Error message
  818. */
  819. public function getError($i = null)
  820. {
  821. return $this->getItemFromArray($this->_errors, $i);
  822. }
  823. /**
  824. * Return all errors, if any
  825. * @return array Array of error messages
  826. */
  827. public function getErrors()
  828. {
  829. return $this->_errors;
  830. }
  831. /**
  832. * Add an error message
  833. * @param string $error Error message
  834. */
  835. public function setError($error)
  836. {
  837. if($this->_errors_queue_size > 0)
  838. {
  839. if(count($this->_errors) >= $this->_errors_queue_size)
  840. {
  841. array_shift($this->_errors);
  842. }
  843. }
  844. array_push($this->_errors, $error);
  845. }
  846. /**
  847. * Resets all error messages
  848. */
  849. public function resetErrors()
  850. {
  851. $this->_errors = array();
  852. }
  853. /**
  854. * Get the most recent warning message
  855. * @param integer $i Optional warning index
  856. * @return string Error message
  857. */
  858. public function getWarning($i = null)
  859. {
  860. return $this->getItemFromArray($this->_warnings, $i);
  861. }
  862. /**
  863. * Return all warnings, if any
  864. * @return array Array of error messages
  865. */
  866. public function getWarnings()
  867. {
  868. return $this->_warnings;
  869. }
  870. /**
  871. * Add an error message
  872. * @param string $error Error message
  873. */
  874. public function setWarning($warning)
  875. {
  876. if($this->_warnings_queue_size > 0)
  877. {
  878. if(count($this->_warnings) >= $this->_warnings_queue_size)
  879. {
  880. array_shift($this->_warnings);
  881. }
  882. }
  883. array_push($this->_warnings, $warning);
  884. }
  885. /**
  886. * Resets all warning messages
  887. */
  888. public function resetWarnings()
  889. {
  890. $this->_warnings = array();
  891. }
  892. /**
  893. * Propagates errors and warnings to a foreign object. The foreign object SHOULD
  894. * implement the setError() and/or setWarning() methods but DOESN'T HAVE TO be of
  895. * AKAbstractObject type. For example, this can even be used to propagate to a
  896. * JObject instance in Joomla!. Propagated items will be removed from ourself.
  897. * @param object $object The object to propagate errors and warnings to.
  898. */
  899. public function propagateToObject(&$object)
  900. {
  901. // Skip non-objects
  902. if(!is_object($object)) return;
  903. if( method_exists($object,'setError') )
  904. {
  905. if(!empty($this->_errors))
  906. {
  907. foreach($this->_errors as $error)
  908. {
  909. $object->setError($error);
  910. }
  911. $this->_errors = array();
  912. }
  913. }
  914. if( method_exists($object,'setWarning') )
  915. {
  916. if(!empty($this->_warnings))
  917. {
  918. foreach($this->_warnings as $warning)
  919. {
  920. $object->setWarning($warning);
  921. }
  922. $this->_warnings = array();
  923. }
  924. }
  925. }
  926. /**
  927. * Propagates errors and warnings from a foreign object. Each propagated list is
  928. * then cleared on the foreign object, as long as it implements resetErrors() and/or
  929. * resetWarnings() methods.
  930. * @param object $object The object to propagate errors and warnings from
  931. */
  932. public function propagateFromObject(&$object)
  933. {
  934. if( method_exists($object,'getErrors') )
  935. {
  936. $errors = $object->getErrors();
  937. if(!empty($errors))
  938. {
  939. foreach($errors as $error)
  940. {
  941. $this->setError($error);
  942. }
  943. }
  944. if(method_exists($object,'resetErrors'))
  945. {
  946. $object->resetErrors();
  947. }
  948. }
  949. if( method_exists($object,'getWarnings') )
  950. {
  951. $warnings = $object->getWarnings();
  952. if(!empty($warnings))
  953. {
  954. foreach($warnings as $warning)
  955. {
  956. $this->setWarning($warning);
  957. }
  958. }
  959. if(method_exists($object,'resetWarnings'))
  960. {
  961. $object->resetWarnings();
  962. }
  963. }
  964. }
  965. /**
  966. * Sets the size of the error queue (acts like a LIFO buffer)
  967. * @param int $newSize The new queue size. Set to 0 for infinite length.
  968. */
  969. protected function setErrorsQueueSize($newSize = 0)
  970. {
  971. $this->_errors_queue_size = (int)$newSize;
  972. }
  973. /**
  974. * Sets the size of the warnings queue (acts like a LIFO buffer)
  975. * @param int $newSize The new queue size. Set to 0 for infinite length.
  976. */
  977. protected function setWarningsQueueSize($newSize = 0)
  978. {
  979. $this->_warnings_queue_size = (int)$newSize;
  980. }
  981. /**
  982. * Returns the last item of a LIFO string message queue, or a specific item
  983. * if so specified.
  984. * @param array $array An array of strings, holding messages
  985. * @param int $i Optional message index
  986. * @return mixed The message string, or false if the key doesn't exist
  987. */
  988. private function getItemFromArray($array, $i = null)
  989. {
  990. // Find the item
  991. if ( $i === null) {
  992. // Default, return the last item
  993. $item = end($array);
  994. }
  995. else
  996. if ( ! array_key_exists($i, $array) ) {
  997. // If $i has been specified but does not exist, return false
  998. return false;
  999. }
  1000. else
  1001. {
  1002. $item = $array[$i];
  1003. }
  1004. return $item;
  1005. }
  1006. }
  1007. /**
  1008. * File post processor engines base class
  1009. */
  1010. abstract class AKAbstractPostproc extends AKAbstractObject
  1011. {
  1012. /** @var string The current (real) file path we'll have to process */
  1013. protected $filename = null;
  1014. /** @var int The requested permissions */
  1015. protected $perms = 0755;
  1016. /** @var string The temporary file path we gave to the unarchiver engine */
  1017. protected $tempFilename = null;
  1018. /** @var int The UNIX timestamp of the file's desired modification date */
  1019. public $timestamp = 0;
  1020. /**
  1021. * Processes the current file, e.g. moves it from temp to final location by FTP
  1022. */
  1023. abstract public function process();
  1024. /**
  1025. * The unarchiver tells us the path to the filename it wants to extract and we give it
  1026. * a different path instead.
  1027. * @param string $filename The path to the real file
  1028. * @param int $perms The permissions we need the file to have
  1029. * @return string The path to the temporary file
  1030. */
  1031. abstract public function processFilename($filename, $perms = 0755);
  1032. /**
  1033. * Recursively creates a directory if it doesn't exist
  1034. * @param string $dirName The directory to create
  1035. * @param int $perms The permissions to give to that directory
  1036. */
  1037. abstract public function createDirRecursive( $dirName, $perms );
  1038. abstract public function chmod( $file, $perms );
  1039. abstract public function unlink( $file );
  1040. abstract public function rmdir( $directory );
  1041. abstract public function rename( $from, $to );
  1042. }
  1043. /**
  1044. * The base class of unarchiver classes
  1045. */
  1046. abstract class AKAbstractUnarchiver extends AKAbstractPart
  1047. {
  1048. /** @var string Archive filename */
  1049. protected $filename = null;
  1050. /** @var array List of the names of all archive parts */
  1051. public $archiveList = array();
  1052. /** @var int The total size of all archive parts */
  1053. public $totalSize = array();
  1054. /** @var integer Current archive part number */
  1055. protected $currentPartNumber = -1;
  1056. /** @var integer The offset inside the current part */
  1057. protected $currentPartOffset = 0;
  1058. /** @var bool Should I restore permissions? */
  1059. protected $flagRestorePermissions = false;
  1060. /** @var AKAbstractPostproc Post processing class */
  1061. protected $postProcEngine = null;
  1062. /** @var string Absolute path to prepend to extracted files */
  1063. protected $addPath = '';
  1064. /** @var array Which files to rename */
  1065. public $renameFiles = array();
  1066. /** @var array Which directories to rename */
  1067. public $renameDirs = array();
  1068. /** @var array Which files to skip */
  1069. public $skipFiles = array();
  1070. /** @var integer Chunk size for processing */
  1071. protected $chunkSize = 524288;
  1072. /** @var resource File pointer to the current archive part file */
  1073. protected $fp = null;
  1074. /** @var int Run state when processing the current archive file */
  1075. protected $runState = null;
  1076. /** @var stdClass File header data, as read by the readFileHeader() method */
  1077. protected $fileHeader = null;
  1078. /** @var int How much of the uncompressed data we've read so far */
  1079. protected $dataReadLength = 0;
  1080. /**
  1081. * Public constructor
  1082. */
  1083. public function __construct()
  1084. {
  1085. parent::__construct();
  1086. }
  1087. /**
  1088. * Wakeup function, called whenever the class is unserialized
  1089. */
  1090. public function __wakeup()
  1091. {
  1092. if($this->currentPartNumber >= 0)
  1093. {
  1094. $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb');
  1095. if( (is_resource($this->fp)) && ($this->currentPartOffset > 0) )
  1096. {
  1097. @fseek($this->fp, $this->currentPartOffset);
  1098. }
  1099. }
  1100. }
  1101. /**
  1102. * Sleep function, called whenever the class is serialized
  1103. */
  1104. public function shutdown()
  1105. {
  1106. if(is_resource($this->fp))
  1107. {
  1108. $this->currentPartOffset = @ftell($this->fp);
  1109. @fclose($this->fp);
  1110. }
  1111. }
  1112. /**
  1113. * Implements the abstract _prepare() method
  1114. */
  1115. final protected function _prepare()
  1116. {
  1117. parent::__construct();
  1118. if( count($this->_parametersArray) > 0 )
  1119. {
  1120. foreach($this->_parametersArray as $key => $value)
  1121. {
  1122. switch($key)
  1123. {
  1124. case 'filename': // Archive's absolute filename
  1125. $this->filename = $value;
  1126. break;
  1127. case 'restore_permissions': // Should I restore permissions?
  1128. $this->flagRestorePermissions = $value;
  1129. break;
  1130. case 'post_proc': // Should I use FTP?
  1131. $this->postProcEngine = AKFactory::getpostProc($value);
  1132. break;
  1133. case 'add_path': // Path to prepend
  1134. $this->addPath = $value;
  1135. $this->addPath = str_replace('\\','/',$this->addPath);
  1136. $this->addPath = rtrim($this->addPath,'/');
  1137. if(!empty($this->addPath)) $this->addPath .= '/';
  1138. break;
  1139. case 'rename_files': // Which files to rename (hash array)
  1140. $this->renameFiles = $value;
  1141. break;
  1142. case 'rename_dirs': // Which files to rename (hash array)
  1143. $this->renameDirs = $value;
  1144. break;
  1145. case 'skip_files': // Which files to skip (indexed array)
  1146. $this->skipFiles = $value;
  1147. break;
  1148. }
  1149. }
  1150. }
  1151. $this->scanArchives();
  1152. $this->readArchiveHeader();
  1153. $errMessage = $this->getError();
  1154. if(!empty($errMessage))
  1155. {
  1156. $this->setState('error', $errMessage);
  1157. }
  1158. else
  1159. {
  1160. $this->runState = AK_STATE_NOFILE;
  1161. $this->setState('prepared');
  1162. }
  1163. }
  1164. protected function _run()
  1165. {
  1166. if($this->getState() == 'postrun') return;
  1167. $this->setState('running');
  1168. $timer = AKFactory::getTimer();
  1169. $status = true;
  1170. while( $status && ($timer->getTimeLeft() > 0) )
  1171. {
  1172. switch( $this->runState )
  1173. {
  1174. case AK_STATE_NOFILE:
  1175. debugMsg(__CLASS__.'::_run() - Reading file header');
  1176. $status = $this->readFileHeader();
  1177. if($status)
  1178. {
  1179. debugMsg(__CLASS__.'::_run() - Preparing to extract '.$this->fileHeader->realFile);
  1180. // Send start of file notification
  1181. $message = new stdClass;
  1182. $message->type = 'startfile';
  1183. $message->content = new stdClass;
  1184. if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) {
  1185. $message->content->realfile = $this->fileHeader->realFile;
  1186. } else {
  1187. $message->content->realfile = $this->fileHeader->file;
  1188. }
  1189. $message->content->file = $this->fileHeader->file;
  1190. if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) {
  1191. $message->content->compressed = $this->fileHeader->compressed;
  1192. } else {
  1193. $message->content->compressed = 0;
  1194. }
  1195. $message->content->uncompressed = $this->fileHeader->uncompressed;
  1196. $this->notify($message);
  1197. } else {
  1198. debugMsg(__CLASS__.'::_run() - Could not read file header');
  1199. }
  1200. break;
  1201. case AK_STATE_HEADER:
  1202. case AK_STATE_DATA:
  1203. debugMsg(__CLASS__.'::_run() - Processing file data');
  1204. $status = $this->processFileData();
  1205. break;
  1206. case AK_STATE_DATAREAD:
  1207. case AK_STATE_POSTPROC:
  1208. debugMsg(__CLASS__.'::_run() - Calling post-processing class');
  1209. $this->postProcEngine->timestamp = $this->fileHeader->timestamp;
  1210. $status = $this->postProcEngine->process();
  1211. $this->propagateFromObject( $this->postProcEngine );
  1212. $this->runState = AK_STATE_DONE;
  1213. break;
  1214. case AK_STATE_DONE:
  1215. default:
  1216. if($status)
  1217. {
  1218. debugMsg(__CLASS__.'::_run() - Finished extracting file');
  1219. // Send end of file notification
  1220. $message = new stdClass;
  1221. $message->type = 'endfile';
  1222. $message->content = new stdClass;
  1223. if( array_key_exists('realfile', get_object_vars($this->fileHeader)) ) {
  1224. $message->content->realfile = $this->fileHeader->realFile;
  1225. } else {
  1226. $message->content->realfile = $this->fileHeader->file;
  1227. }
  1228. $message->content->file = $this->fileHeader->file;
  1229. if( array_key_exists('compressed', get_object_vars($this->fileHeader)) ) {
  1230. $message->content->compressed = $this->fileHeader->compressed;
  1231. } else {
  1232. $message->content->compressed = 0;
  1233. }
  1234. $message->content->uncompressed = $this->fileHeader->uncompressed;
  1235. $this->notify($message);
  1236. }
  1237. $this->runState = AK_STATE_NOFILE;
  1238. continue;
  1239. }
  1240. }
  1241. $error = $this->getError();
  1242. if( !$status && ($this->runState == AK_STATE_NOFILE) && empty( $error ) )
  1243. {
  1244. debugMsg(__CLASS__.'::_run() - Just finished');
  1245. // We just finished
  1246. $this->setState('postrun');
  1247. }
  1248. elseif( !empty($error) )
  1249. {
  1250. debugMsg(__CLASS__.'::_run() - Halted with an error:');
  1251. debugMsg($error);
  1252. $this->setState( 'error', $error );
  1253. }
  1254. }
  1255. protected function _finalize()
  1256. {
  1257. // Nothing to do
  1258. $this->setState('finished');
  1259. }
  1260. /**
  1261. * Returns the base extension of the file, e.g. '.jpa'
  1262. * @return string
  1263. */
  1264. private function getBaseExtension()
  1265. {
  1266. static $baseextension;
  1267. if(empty($baseextension))
  1268. {
  1269. $basename = basename($this->filename);
  1270. $lastdot = strrpos($basename,'.');
  1271. $baseextension = substr($basename, $lastdot);
  1272. }
  1273. return $baseextension;
  1274. }
  1275. /**
  1276. * Scans for archive parts
  1277. */
  1278. private function scanArchives()
  1279. {
  1280. if(defined('KSDEBUG')) {
  1281. @unlink('debug.txt');
  1282. }
  1283. debugMsg('Preparing to scan archives');
  1284. $privateArchiveList = array();
  1285. // Get the components of the archive filename
  1286. $dirname = dirname($this->filename);
  1287. $base_extension = $this->getBaseExtension();
  1288. $basename = basename($this->filename, $base_extension);
  1289. $this->totalSize = 0;
  1290. // Scan for multiple parts until we don't find any more of them
  1291. $count = 0;
  1292. $found = true;
  1293. $this->archiveList = array();
  1294. while($found)
  1295. {
  1296. ++$count;
  1297. $extension = substr($base_extension, 0, 2).sprintf('%02d', $count);
  1298. $filename = $dirname.DIRECTORY_SEPARATOR.$basename.$extension;
  1299. $found = file_exists($filename);
  1300. if($found)
  1301. {
  1302. debugMsg('- Found archive '.$filename);
  1303. // Add yet another part, with a numeric-appended filename
  1304. $this->archiveList[] = $filename;
  1305. $filesize = @filesize($filename);
  1306. $this->totalSize += $filesize;
  1307. $privateArchiveList[] = array($filename, $filesize);
  1308. }
  1309. else
  1310. {
  1311. debugMsg('- Found archive '.$this->filename);
  1312. // Add the last part, with the regular extension
  1313. $this->archiveList[] = $this->filename;
  1314. $filename = $this->filename;
  1315. $filesize = @filesize($filename);
  1316. $this->totalSize += $filesize;
  1317. $privateArchiveList[] = array($filename, $filesize);
  1318. }
  1319. }
  1320. debugMsg('Total archive parts: '.$count);
  1321. $this->currentPartNumber = -1;
  1322. $this->currentPartOffset = 0;
  1323. $this->runState = AK_STATE_NOFILE;
  1324. // Send start of file notification
  1325. $message = new stdClass;
  1326. $message->type = 'totalsize';
  1327. $message->content = new stdClass;
  1328. $message->content->totalsize = $this->totalSize;
  1329. $message->content->filelist = $privateArchiveList;
  1330. $this->notify($message);
  1331. }
  1332. /**
  1333. * Opens the next part file for reading
  1334. */
  1335. protected function nextFile()
  1336. {
  1337. debugMsg('Current part is '.$this->currentPartNumber.'; opening the next part');
  1338. ++$this->currentPartNumber;
  1339. if( $this->currentPartNumber > (count($this->archiveList) - 1) )
  1340. {
  1341. $this->setState('postrun');
  1342. return false;
  1343. }
  1344. else
  1345. {
  1346. if( is_resource($this->fp) ) @fclose($this->fp);
  1347. debugMsg('Opening file '.$this->archiveList[$this->currentPartNumber]);
  1348. $this->fp = @fopen( $this->archiveList[$this->currentPartNumber], 'rb' );
  1349. if($this->fp === false) {
  1350. debugMsg('Could not open file - crash imminent');
  1351. }
  1352. fseek($this->fp, 0);
  1353. $this->currentPartOffset = 0;
  1354. return true;
  1355. }
  1356. }
  1357. /**
  1358. * Returns true if we have reached the end of file
  1359. * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of the archive set
  1360. * @return bool True if we have reached End Of File
  1361. */
  1362. protected function isEOF($local = false)
  1363. {
  1364. $eof = @feof($this->fp);
  1365. if(!$eof)
  1366. {
  1367. // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why
  1368. // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report
  1369. // true. Incredible! :(
  1370. $position = @ftell($this->fp);
  1371. $filesize = @filesize( $this->archiveList[$this->currentPartNumber] );
  1372. if($filesize <= 0) {
  1373. // 2Gb or more files on a 32 bit version of PHP tend to get screwed up. Meh.
  1374. $eof = false;
  1375. } elseif( $position >= $filesize ) {
  1376. $eof = true;
  1377. }
  1378. }
  1379. if($local)
  1380. {
  1381. return $eof;
  1382. }
  1383. else
  1384. {
  1385. return $eof && ($this->currentPartNumber >= (count($this->archiveList)-1) );
  1386. }
  1387. }
  1388. /**
  1389. * Tries to make a directory user-writable so that we can write a file to it
  1390. * @param $path string A path to a file
  1391. */
  1392. protected function setCorrectPermissions($path)
  1393. {
  1394. static $rootDir = null;
  1395. if(is_null($rootDir)) {
  1396. $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir',''),'/\\');
  1397. }
  1398. $directory = rtrim(dirname($path),'/\\');
  1399. if($directory != $rootDir) {
  1400. // Is this an unwritable directory?
  1401. if(!is_writeable($directory)) {
  1402. $this->postProcEngine->chmod( $directory, 0755 );
  1403. }
  1404. }
  1405. $this->postProcEngine->chmod( $path, 0644 );
  1406. }
  1407. /**
  1408. * Concrete classes are supposed to use this method in order to read the archive's header and
  1409. * prepare themselves to the point of being ready to extract the first file.
  1410. */
  1411. protected abstract function readArchiveHeader();
  1412. /**
  1413. * Concrete classes must use this method to read the file header
  1414. * @return bool True if reading the file was successful, false if an error occured or we reached end of archive
  1415. */
  1416. protected abstract function readFileHeader();
  1417. /**
  1418. * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when
  1419. * it's finished processing the file data.
  1420. * @return bool True if processing the file data was successful, false if an error occured
  1421. */
  1422. protected abstract function processFileData();
  1423. /**
  1424. * Reads data from the archive and notifies the observer with the 'reading' message
  1425. * @param $fp
  1426. * @param $length
  1427. */
  1428. protected function fread($fp, $length = null)
  1429. {
  1430. if(is_numeric($length))
  1431. {
  1432. if($length > 0) {
  1433. $data = fread($fp, $length);
  1434. } else {
  1435. $data = fread($fp);
  1436. }
  1437. }
  1438. else
  1439. {
  1440. $data = fread($fp);
  1441. }
  1442. if($data === false) $data = '';
  1443. // Send start of file notification
  1444. $message = new stdClass;
  1445. $message->type = 'reading';
  1446. $message->content = new stdClass;
  1447. $message->content->length = strlen($data);
  1448. $this->notify($message);
  1449. return $data;
  1450. }
  1451. }
  1452. /**
  1453. * The superclass of all Akeeba Kickstart parts. The "parts" are intelligent stateful
  1454. * classes which perform a single procedure and have preparation, running and
  1455. * finalization phases. The transition between phases is handled automatically by
  1456. * this superclass' tick() final public method, which should be the ONLY public API
  1457. * exposed to the rest of the Akeeba Engine.
  1458. */
  1459. abstract class AKAbstractPart extends AKAbstractObject
  1460. {
  1461. /**
  1462. * Indicates whether this part has finished its initialisation cycle
  1463. * @var boolean
  1464. */
  1465. protected $isPrepared = false;
  1466. /**
  1467. * Indicates whether this part has more work to do (it's in running state)
  1468. * @var boolean
  1469. */
  1470. protected $isRunning = false;
  1471. /**
  1472. * Indicates whether this part has finished its finalization cycle
  1473. * @var boolean
  1474. */
  1475. protected $isFinished = false;
  1476. /**
  1477. * Indicates whether this part has finished its run cycle
  1478. * @var boolean
  1479. */
  1480. protected $hasRan = false;
  1481. /**
  1482. * The name of the engine part (a.k.a. Domain), used in return table
  1483. * generation.
  1484. * @var string
  1485. */
  1486. protected $active_domain = "";
  1487. /**
  1488. * The step this engine part is in. Used verbatim in return table and
  1489. * should be set by the code in the _run() method.
  1490. * @var string
  1491. */
  1492. protected $active_step = "";
  1493. /**
  1494. * A more detailed description of the step this engine part is in. Used
  1495. * verbatim in return table and should be set by the code in the _run()
  1496. * method.
  1497. * @var string
  1498. */
  1499. protected $active_substep = "";
  1500. /**
  1501. * Any configuration variables, in the form of an array.
  1502. * @var array
  1503. */
  1504. protected $_parametersArray = array();
  1505. /** @var string The database root key */
  1506. protected $databaseRoot = array();
  1507. /** @var int Last reported warnings's position in array */
  1508. private $warnings_pointer = -1;
  1509. /** @var array An array of observers */
  1510. protected $observers = array();
  1511. /**
  1512. * Runs the preparation for this part. Should set _isPrepared
  1513. * to true
  1514. */
  1515. abstract protected function _prepare();
  1516. /**
  1517. * Runs the finalisation process for this part. Should set
  1518. * _isFinished to true.
  1519. */
  1520. abstract protected function _finalize();
  1521. /**
  1522. * Runs the main functionality loop for this part. Upon calling,
  1523. * should set the _isRunning to true. When it finished, should set
  1524. * the _hasRan to true. If an error is encountered, setError should
  1525. * be used.
  1526. */
  1527. abstract protected function _run();
  1528. /**
  1529. * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately,
  1530. * in fear of timing out.
  1531. */
  1532. protected function setBreakFlag()
  1533. {
  1534. AKFactory::set('volatile.breakflag', true);
  1535. }
  1536. /**
  1537. * Sets the engine part's internal state, in an easy to use manner
  1538. *
  1539. * @param string $state One of init, prepared, running, postrun, finished, error
  1540. * @param string $errorMessage The reported error message, should the state be set to error
  1541. */
  1542. protected function setState($state = 'init', $errorMessage='Invalid setState argument')
  1543. {
  1544. switch($state)
  1545. {
  1546. case 'init':
  1547. $this->isPrepared = false;
  1548. $this->isRunning = false;
  1549. $this->isFinished = false;
  1550. $this->hasRun = false;
  1551. break;
  1552. case 'prepared':
  1553. $this->isPrepared = true;
  1554. $this->isRunning = false;
  1555. $this->isFinished = false;
  1556. $this->hasRun = false;
  1557. break;
  1558. case 'running':
  1559. $this->isPrepared = true;
  1560. $this->isRunning = true;
  1561. $this->isFinished = false;
  1562. $this->hasRun = false;
  1563. break;
  1564. case 'postrun':
  1565. $this->isPrepared = true;
  1566. $this->isRunning = false;
  1567. $this->isFinished = false;
  1568. $this->hasRun = true;
  1569. break;
  1570. case 'finished':
  1571. $this->isPrepared = true;
  1572. $this->isRunning = false;
  1573. $this->isFinished = true;
  1574. $this->hasRun = false;
  1575. break;
  1576. case 'error':
  1577. default:
  1578. $this->setError($errorMessage);
  1579. break;
  1580. }
  1581. }
  1582. /**
  1583. * The public interface to an engine part. This method takes care for
  1584. * calling the correct method in order to perform the initialisation -
  1585. * run - finalisation cycle of operation and return a proper reponse array.
  1586. * @return array A Reponse Array
  1587. */
  1588. final public function tick()
  1589. {
  1590. // Call the right action method, depending on engine part state
  1591. switch( $this->getState() )
  1592. {
  1593. case "init":
  1594. $this->_prepare();
  1595. break;
  1596. case "prepared":
  1597. $this->_run();
  1598. break;
  1599. case "running":
  1600. $this->_run();
  1601. break;
  1602. case "postrun":
  1603. $this->_finalize();
  1604. break;
  1605. }
  1606. // Send a Return Table back to the caller
  1607. $out = $this->_makeReturnTable();
  1608. return $out;
  1609. }
  1610. /**
  1611. * Returns a copy of the class's status array
  1612. * @return array
  1613. */
  1614. public function getStatusArray()
  1615. {
  1616. return $this->_makeReturnTable();
  1617. }
  1618. /**
  1619. * Sends any kind of setup information to the engine part. Using this,
  1620. * we avoid passing parameters to the constructor of the class. These
  1621. * parameters should be passed as an indexed array and should be taken
  1622. * into account during the preparation process only. This function will
  1623. * set the error flag if it's called after the engine part is prepared.
  1624. *
  1625. * @param array $parametersArray The parameters to be passed to the
  1626. * engine part.
  1627. */
  1628. final public function setup( $parametersArray )
  1629. {
  1630. if( $this->isPrepared )
  1631. {
  1632. $this->setState('error', "Can't modify configuration after the preparation of " . $this->active_domain);
  1633. }
  1634. else
  1635. {
  1636. $this->_parametersArray = $parametersArray;
  1637. if(array_key_exists('root', $parametersArray))
  1638. {
  1639. $this->databaseRoot = $parametersArray['root'];
  1640. }
  1641. }
  1642. }
  1643. /**
  1644. * Returns the state of this engine part.
  1645. *
  1646. * @return string The state of this engine part. It can be one of
  1647. * error, init, prepared, running, postrun, finished.
  1648. */
  1649. final public function getState()
  1650. {
  1651. if( $this->getError() )
  1652. {
  1653. return "error";
  1654. }
  1655. if( !($this->isPrepared) )
  1656. {
  1657. return "init";
  1658. }
  1659. if( !($this->isFinished) && !($this->isRunning) && !( $this->hasRun ) && ($this->isPrepared) )
  1660. {
  1661. return "prepared";
  1662. }
  1663. if ( !($this->isFinished) && $this->isRunning && !( $this->hasRun ) )
  1664. {
  1665. return "running";
  1666. }
  1667. if ( !($this->isFinished) && !($this->isRunning) && $this->hasRun )
  1668. {
  1669. return "postrun";
  1670. }
  1671. if ( $this->isFinished )
  1672. {
  1673. return "finished";
  1674. }
  1675. }
  1676. /**
  1677. * Constructs a Response Array based on the engine part's state.
  1678. * @return array The Response Array for the current state
  1679. */
  1680. final protected function _makeReturnTable()
  1681. {
  1682. // Get a list of warnings
  1683. $warnings = $this->getWarnings();
  1684. // Report only new warnings if there is no warnings queue size
  1685. if( $this->_warnings_queue_size == 0 )
  1686. {
  1687. if( ($this->warnings_pointer > 0) && ($this->warnings_pointer < (count($warnings)) ) )
  1688. {
  1689. $warnings = array_slice($warnings, $this->warnings_pointer + 1);
  1690. $this->warnings_pointer += count($warnings);
  1691. }
  1692. else
  1693. {
  1694. $this->warnings_pointer = count($warnings);
  1695. }
  1696. }
  1697. $out = array(
  1698. 'HasRun' => (!($this->isFinished)),
  1699. 'Domain' => $this->active_domain,
  1700. 'Step' => $this->active_step,
  1701. 'Substep' => $this->active_substep,
  1702. 'Error' => $this->getError(),
  1703. 'Warnings' => $warnings
  1704. );
  1705. return $out;
  1706. }
  1707. final protected function setDomain($new_domain)
  1708. {
  1709. $this->active_domain = $new_domain;
  1710. }
  1711. final public function getDomain()
  1712. {
  1713. return $this->active_domain;
  1714. }
  1715. final protected function setStep($new_step)
  1716. {
  1717. $this->active_step = $new_step;
  1718. }
  1719. final public function getStep()
  1720. {
  1721. return $this->active_step;
  1722. }
  1723. final protected function setSubstep($new_substep)
  1724. {
  1725. $this->active_substep = $new_substep;
  1726. }
  1727. final public function getSubstep()
  1728. {
  1729. return $this->active_substep;
  1730. }
  1731. /**
  1732. * Attaches an observer object
  1733. * @param AKAbstractPartObserver $obs
  1734. */
  1735. function attach(AKAbstractPartObserver $obs) {
  1736. $this->observers["$obs"] = $obs;
  1737. }
  1738. /**
  1739. * Dettaches an observer object
  1740. * @param AKAbstractPartObserver $obs
  1741. */
  1742. function detach(AKAbstractPartObserver $obs) {
  1743. delete($this->observers["$obs"]);
  1744. }
  1745. /**
  1746. * Notifies observers each time something interesting happened to the part
  1747. * @param mixed $message The event object
  1748. */
  1749. protected function notify($message) {
  1750. foreach ($this->observers as $obs) {
  1751. $obs->update($this, $message);
  1752. }
  1753. }
  1754. }
  1755. /**
  1756. * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify)
  1757. * @author Nicholas
  1758. *
  1759. */
  1760. abstract class AKAbstractPartObserver
  1761. {
  1762. abstract public function update($object, $message);
  1763. }
  1764. /**
  1765. * Direct file writer
  1766. */
  1767. class AKPostprocDirect extends AKAbstractPostproc
  1768. {
  1769. public function process()
  1770. {
  1771. $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false);
  1772. if($restorePerms)
  1773. {
  1774. @chmod($this->filename, $this->perms);
  1775. }
  1776. else
  1777. {
  1778. if(@is_file($this->filename))
  1779. {
  1780. @chmod($this->filename, 0644);
  1781. }
  1782. else
  1783. {
  1784. @chmod($this->filename, 0755);
  1785. }
  1786. }
  1787. if($this->timestamp > 0)
  1788. {
  1789. @touch($this->filename, $this->timestamp);
  1790. }
  1791. return true;
  1792. }
  1793. public function processFilename($filename, $perms = 0755)
  1794. {
  1795. $this->perms = $perms;
  1796. $this->filename = $filename;
  1797. return $filename;
  1798. }
  1799. public function createDirRecursive( $dirName, $perms )
  1800. {
  1801. if( AKFactory::get('kickstart.setup.dryrun','0') ) return true;
  1802. if (@mkdir($dirName, 0755, true)) {
  1803. @chmod($dirName, 0755);
  1804. return true;
  1805. }
  1806. $root = AKFactory::get('kickstart.setup.destdir');
  1807. $root = rtrim(str_replace('\\','/',$root),'/');
  1808. $dir = rtrim(str_replace('\\','/',$dirName),'/');
  1809. if(strpos($dir, $root) === 0) {
  1810. $dir = ltrim(substr($dir, strlen($root)), '/');
  1811. $root .= '/';
  1812. } else {
  1813. $root = '';
  1814. }
  1815. if(empty($dir)) return true;
  1816. $dirArray = explode('/', $dir);
  1817. $path = '';
  1818. foreach( $dirArray as $dir )
  1819. {
  1820. $path .= $dir . '/';
  1821. $ret = is_dir($root.$path) ? true : @mkdir($root.$path);
  1822. if( !$ret ) {
  1823. // Is this a file instead of a directory?
  1824. if(is_file($root.$path) )
  1825. {
  1826. @unlink($root.$path);
  1827. $ret = @mkdir($root.$path);
  1828. }
  1829. if( !$ret ) {
  1830. $this->setError( AKText::sprintf('COULDNT_CREATE_DIR',$path) );
  1831. return false;
  1832. }
  1833. }
  1834. // Try to set new directory permissions to 0755
  1835. @chmod($root.$path, $perms);
  1836. }
  1837. return true;
  1838. }
  1839. public function chmod( $file, $perms )
  1840. {
  1841. if( AKFactory::get('kickstart.setup.dryrun','0') ) return true;
  1842. return @chmod( $file, $perms );
  1843. }
  1844. public function unlink( $file )
  1845. {
  1846. return @unlink( $file );
  1847. }
  1848. public function rmdir( $directory )
  1849. {
  1850. return @rmdir( $directory );
  1851. }
  1852. public function rename( $from, $to )
  1853. {
  1854. return @rename($from, $to);
  1855. }
  1856. }
  1857. /**
  1858. * FTP file writer
  1859. */
  1860. class AKPostprocFTP extends AKAbstractPostproc
  1861. {
  1862. /** @var bool Should I use FTP over implicit SSL? */
  1863. public $useSSL = false;
  1864. /** @var bool use Passive mode? */
  1865. public $passive = true;
  1866. /** @var string FTP host name */
  1867. public $host = '';
  1868. /** @var int FTP port */
  1869. public $port = 21;
  1870. /** @var string FTP user name */
  1871. public $user = '';
  1872. /** @var string FTP password */
  1873. public $pass = '';
  1874. /** @var string FTP initial directory */
  1875. public $dir = '';
  1876. /** @var resource The FTP handle */
  1877. private $handle = null;
  1878. /** @var string The temporary directory where the data will be stored */
  1879. private $tempDir = '';
  1880. public function __construct()
  1881. {
  1882. parent::__construct();
  1883. $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false);
  1884. $this->passive = AKFactory::get('kickstart.ftp.passive', true);
  1885. $this->host = AKFactory::get('kickstart.ftp.host', '');
  1886. $this->port = AKFactory::get('kickstart.ftp.port', 21);
  1887. if(trim($this->port) == '') $this->port = 21;
  1888. $this->user = AKFactory::get('kickstart.ftp.user', '');
  1889. $this->pass = AKFactory::get('kickstart.ftp.pass', '');
  1890. $this->dir = AKFactory::get('kickstart.ftp.dir', '');
  1891. $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', '');
  1892. $connected = $this->connect();
  1893. if($connected)
  1894. {
  1895. if(!empty($this->tempDir))
  1896. {
  1897. $tempDir = rtrim($this->tempDir, '/\\').'/';
  1898. $writable = $this->isDirWritable($tempDir);
  1899. }
  1900. else
  1901. {
  1902. $tempDir = '';
  1903. $writable = false;
  1904. }
  1905. if(!$writable) {
  1906. // Default temporary directory is the current root
  1907. $tempDir = function_exists('getcwd') ? getcwd() : dirname(__FILE__);
  1908. if(empty($tempDir))
  1909. {
  1910. // Oh, we have no directory reported!
  1911. $tempDir = '.';
  1912. }
  1913. $absoluteDirToHere = $tempDir;
  1914. $tempDir = rtrim(str_replace('\\','/',$tempDir),'/');
  1915. if(!empty($tempDir)) $tempDir .= '/';
  1916. $this->tempDir = $tempDir;
  1917. // Is this directory writable?
  1918. $writable = $this->isDirWritable($tempDir);
  1919. }
  1920. if(!$writable)
  1921. {
  1922. // Nope. Let's try creating a temporary directory in the site's root.
  1923. $tempDir = $absoluteDirToHere.'/kicktemp';
  1924. $this->createDirRecursive($tempDir, 0777);
  1925. // Try making it writable...
  1926. $this->fixPermissions($tempDir);
  1927. $writable = $this->isDirWritable($tempDir);
  1928. }
  1929. // Was the new directory writable?
  1930. if(!$writable)
  1931. {
  1932. // Let's see if the user has specified one
  1933. $userdir = AKFactory::get('kickstart.ftp.tempdir', '');
  1934. if(!empty($userdir))
  1935. {
  1936. // Is it an absolute or a relative directory?
  1937. $absolute = false;
  1938. $absolute = $absolute || ( substr($userdir,0,1) == '/' );
  1939. $absolute = $absolute || ( substr($userdir,1,1) == ':' );
  1940. $absolute = $absolute || ( substr($userdir,2,1) == ':' );
  1941. if(!$absolute)
  1942. {
  1943. // Make absolute
  1944. $tempDir = $absoluteDirToHere.$userdir;
  1945. }
  1946. else
  1947. {
  1948. // it's already absolute
  1949. $tempDir = $userdir;
  1950. }
  1951. // Does the directory exist?
  1952. if( is_dir($tempDir) )
  1953. {
  1954. // Yeah. Is it writable?
  1955. $writable = $this->isDirWritable($tempDir);
  1956. }
  1957. }
  1958. }
  1959. $this->tempDir = $tempDir;
  1960. if(!$writable)
  1961. {
  1962. // No writable directory found!!!
  1963. $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE'));
  1964. }
  1965. else
  1966. {
  1967. AKFactory::set('kickstart.ftp.tempdir', $tempDir);
  1968. $this->tempDir = $tempDir;
  1969. }
  1970. }
  1971. }
  1972. function __wakeup()
  1973. {
  1974. $this->connect();
  1975. }
  1976. public function connect()
  1977. {
  1978. // Connect to server, using SSL if so required
  1979. if($this->useSSL) {
  1980. $this->handle = @ftp_ssl_connect($this->host, $this->port);
  1981. } else {
  1982. $this->handle = @ftp_connect($this->host, $this->port);
  1983. }
  1984. if($this->handle === false)
  1985. {
  1986. $this->setError(AKText::_('WRONG_FTP_HOST'));
  1987. return false;
  1988. }
  1989. // Login
  1990. if(! @ftp_login($this->handle, $this->user, $this->pass))
  1991. {
  1992. $this->setError(AKText::_('WRONG_FTP_USER'));
  1993. @ftp_close($this->handle);
  1994. return false;
  1995. }
  1996. // Change to initial directory
  1997. if(! @ftp_chdir($this->handle, $this->dir))
  1998. {
  1999. $this->setError(AKText::_('WRONG_FTP_PATH1'));
  2000. @ftp_close($this->handle);
  2001. return false;
  2002. }
  2003. // Enable passive mode if the user requested it
  2004. if( $this->passive )
  2005. {
  2006. @ftp_pasv($this->handle, true);
  2007. }
  2008. else
  2009. {
  2010. @ftp_pasv($this->handle, false);
  2011. }
  2012. return true;
  2013. }
  2014. public function process()
  2015. {
  2016. if( is_null($this->tempFilename) )
  2017. {
  2018. // If an empty filename is passed, it means that we shouldn't do any post processing, i.e.
  2019. // the entity was a directory or symlink
  2020. return true;
  2021. }
  2022. $remotePath = dirname($this->filename);
  2023. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2024. if(!empty($removePath))
  2025. {
  2026. $removePath = ltrim($removePath, "/");
  2027. $remotePath = ltrim($remotePath, "/");
  2028. $left = substr($remotePath, 0, strlen($removePath));
  2029. if($left == $removePath)
  2030. {
  2031. $remotePath = substr($remotePath, strlen($removePath));
  2032. }
  2033. }
  2034. $absoluteFSPath = dirname($this->filename);
  2035. $relativeFTPPath = trim($remotePath, '/');
  2036. $absoluteFTPPath = '/'.trim( $this->dir, '/' ).'/'.trim($remotePath, '/');
  2037. $onlyFilename = basename($this->filename);
  2038. $remoteName = $absoluteFTPPath.'/'.$onlyFilename;
  2039. $ret = @ftp_chdir($this->handle, $absoluteFTPPath);
  2040. if($ret === false)
  2041. {
  2042. $ret = $this->createDirRecursive( $absoluteFSPath, 0755);
  2043. if($ret === false) {
  2044. $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename));
  2045. return false;
  2046. }
  2047. $ret = @ftp_chdir($this->handle, $absoluteFTPPath);
  2048. if($ret === false) {
  2049. $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename));
  2050. return false;
  2051. }
  2052. }
  2053. $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY);
  2054. if($ret === false)
  2055. {
  2056. // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry!
  2057. $this->fixPermissions($this->filename);
  2058. $this->unlink($this->filename);
  2059. $fp = @fopen($this->tempFilename);
  2060. if($fp !== false)
  2061. {
  2062. $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY);
  2063. @fclose($fp);
  2064. }
  2065. else
  2066. {
  2067. $ret = false;
  2068. }
  2069. }
  2070. @unlink($this->tempFilename);
  2071. if($ret === false)
  2072. {
  2073. $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename));
  2074. return false;
  2075. }
  2076. $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false);
  2077. if($restorePerms)
  2078. {
  2079. @ftp_chmod($this->_handle, $perms, $remoteName);
  2080. }
  2081. else
  2082. {
  2083. @ftp_chmod($this->_handle, 0644, $remoteName);
  2084. }
  2085. return true;
  2086. }
  2087. public function processFilename($filename, $perms = 0755)
  2088. {
  2089. // Catch some error conditions...
  2090. if($this->getError())
  2091. {
  2092. return false;
  2093. }
  2094. // If a null filename is passed, it means that we shouldn't do any post processing, i.e.
  2095. // the entity was a directory or symlink
  2096. if(is_null($filename))
  2097. {
  2098. $this->filename = null;
  2099. $this->tempFilename = null;
  2100. return null;
  2101. }
  2102. // Strip absolute filesystem path to website's root
  2103. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2104. if(!empty($removePath))
  2105. {
  2106. $left = substr($filename, 0, strlen($removePath));
  2107. if($left == $removePath)
  2108. {
  2109. $filename = substr($filename, strlen($removePath));
  2110. }
  2111. }
  2112. // Trim slash on the left
  2113. $filename = ltrim($filename, '/');
  2114. $this->filename = $filename;
  2115. $this->tempFilename = tempnam($this->tempDir, 'kickstart-');
  2116. $this->perms = $perms;
  2117. if( empty($this->tempFilename) )
  2118. {
  2119. // Oops! Let's try something different
  2120. $this->tempFilename = $this->tempDir.'/kickstart-'.time().'.dat';
  2121. }
  2122. return $this->tempFilename;
  2123. }
  2124. private function isDirWritable($dir)
  2125. {
  2126. $fp = @fopen($dir.'/kickstart.dat', 'wb');
  2127. if($fp === false)
  2128. {
  2129. return false;
  2130. }
  2131. else
  2132. {
  2133. @fclose($fp);
  2134. unlink($dir.'/kickstart.dat');
  2135. return true;
  2136. }
  2137. }
  2138. public function createDirRecursive( $dirName, $perms )
  2139. {
  2140. // Strip absolute filesystem path to website's root
  2141. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2142. if(!empty($removePath))
  2143. {
  2144. // UNIXize the paths
  2145. $removePath = str_replace('\\','/',$removePath);
  2146. $dirName = str_replace('\\','/',$dirName);
  2147. // Make sure they both end in a slash
  2148. $removePath = rtrim($removePath,'/\\').'/';
  2149. $dirName = rtrim($dirName,'/\\').'/';
  2150. // Process the path removal
  2151. $left = substr($dirName, 0, strlen($removePath));
  2152. if($left == $removePath)
  2153. {
  2154. $dirName = substr($dirName, strlen($removePath));
  2155. }
  2156. }
  2157. if(empty($dirName)) $dirName = ''; // 'cause the substr() above may return FALSE.
  2158. $check = '/'.trim($this->dir,'/').'/'.trim($dirName, '/');
  2159. if($this->is_dir($check)) return true;
  2160. $alldirs = explode('/', $dirName);
  2161. $previousDir = '/'.trim($this->dir);
  2162. foreach($alldirs as $curdir)
  2163. {
  2164. $check = $previousDir.'/'.$curdir;
  2165. if(!$this->is_dir($check))
  2166. {
  2167. // Proactively try to delete a file by the same name
  2168. @ftp_delete($this->handle, $check);
  2169. if(@ftp_mkdir($this->handle, $check) === false)
  2170. {
  2171. // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry!
  2172. $this->fixPermissions($removePath.$check);
  2173. if(@ftp_mkdir($this->handle, $check) === false)
  2174. {
  2175. // Can we fall back to pure PHP mode, sire?
  2176. if(!@mkdir($check))
  2177. {
  2178. $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR',$dir));
  2179. return false;
  2180. }
  2181. else
  2182. {
  2183. // Since the directory was built by PHP, change its permissions
  2184. @chmod($check, "0777");
  2185. return true;
  2186. }
  2187. }
  2188. }
  2189. @ftp_chmod($this->handle, $perms, $check);
  2190. }
  2191. $previousDir = $check;
  2192. }
  2193. return true;
  2194. }
  2195. public function close()
  2196. {
  2197. @ftp_close($this->handle);
  2198. }
  2199. /*
  2200. * Tries to fix directory/file permissions in the PHP level, so that
  2201. * the FTP operation doesn't fail.
  2202. * @param $path string The full path to a directory or file
  2203. */
  2204. private function fixPermissions( $path )
  2205. {
  2206. // Turn off error reporting
  2207. if(!defined('KSDEBUG')) {
  2208. $oldErrorReporting = @error_reporting(E_NONE);
  2209. }
  2210. // Get UNIX style paths
  2211. $relPath = str_replace('\\','/',$path);
  2212. $basePath = rtrim(str_replace('\\','/',dirname(__FILE__)),'/');
  2213. $basePath = rtrim($basePath,'/');
  2214. if(!empty($basePath)) $basePath .= '/';
  2215. // Remove the leading relative root
  2216. if( substr($relPath,0,strlen($basePath)) == $basePath )
  2217. $relPath = substr($relPath,strlen($basePath));
  2218. $dirArray = explode('/', $relPath);
  2219. $pathBuilt = rtrim($basePath,'/');
  2220. foreach( $dirArray as $dir )
  2221. {
  2222. if(empty($dir)) continue;
  2223. $oldPath = $pathBuilt;
  2224. $pathBuilt .= '/'.$dir;
  2225. if(is_dir($oldPath.$dir))
  2226. {
  2227. @chmod($oldPath.$dir, 0777);
  2228. }
  2229. else
  2230. {
  2231. if(@chmod($oldPath.$dir, 0777) === false)
  2232. {
  2233. @unlink($oldPath.$dir);
  2234. }
  2235. }
  2236. }
  2237. // Restore error reporting
  2238. if(!defined('KSDEBUG')) {
  2239. @error_reporting($oldErrorReporting);
  2240. }
  2241. }
  2242. public function chmod( $file, $perms )
  2243. {
  2244. return @ftp_chmod($this->handle, $perms, $path);
  2245. }
  2246. private function is_dir( $dir )
  2247. {
  2248. return @ftp_chdir( $this->handle, $dir );
  2249. }
  2250. public function unlink( $file )
  2251. {
  2252. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2253. if(!empty($removePath))
  2254. {
  2255. $left = substr($file, 0, strlen($removePath));
  2256. if($left == $removePath)
  2257. {
  2258. $file = substr($file, strlen($removePath));
  2259. }
  2260. }
  2261. $check = '/'.trim($this->dir,'/').'/'.trim($file, '/');
  2262. return @ftp_delete( $this->handle, $check );
  2263. }
  2264. public function rmdir( $directory )
  2265. {
  2266. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2267. if(!empty($removePath))
  2268. {
  2269. $left = substr($directory, 0, strlen($removePath));
  2270. if($left == $removePath)
  2271. {
  2272. $directory = substr($directory, strlen($removePath));
  2273. }
  2274. }
  2275. $check = '/'.trim($this->dir,'/').'/'.trim($directory, '/');
  2276. return @ftp_rmdir( $this->handle, $check );
  2277. }
  2278. public function rename( $from, $to )
  2279. {
  2280. $originalFrom = $from;
  2281. $originalTo = $to;
  2282. $removePath = AKFactory::get('kickstart.setup.destdir','');
  2283. if(!empty($removePath))
  2284. {
  2285. $left = substr($from, 0, strlen($removePath));
  2286. if($left == $removePath)
  2287. {
  2288. $from = substr($from, strlen($removePath));
  2289. }
  2290. }
  2291. $from = '/'.trim($this->dir,'/').'/'.trim($from, '/');
  2292. if(!empty($removePath))
  2293. {
  2294. $left = substr($to, 0, strlen($removePath));
  2295. if($left == $removePath)
  2296. {
  2297. $to = substr($to, strlen($removePath));
  2298. }
  2299. }
  2300. $to = '/'.trim($this->dir,'/').'/'.trim($to, '/');
  2301. $result = @ftp_rename( $this->handle, $from, $to );
  2302. if($result !== true)
  2303. {
  2304. return @rename($from, $to);
  2305. }
  2306. else
  2307. {
  2308. return true;
  2309. }
  2310. }
  2311. }
  2312. /**
  2313. * JPA archive extraction class
  2314. */
  2315. class AKUnarchiverJPA extends AKAbstractUnarchiver
  2316. {
  2317. private $archiveHeaderData = array();
  2318. protected function readArchiveHeader()
  2319. {
  2320. debugMsg('Preparing to read archive header');
  2321. // Initialize header data array
  2322. $this->archiveHeaderData = new stdClass();
  2323. // Open the first part
  2324. debugMsg('Opening the first part');
  2325. $this->nextFile();
  2326. // Fail for unreadable files
  2327. if( $this->fp === false ) {
  2328. debugMsg('Could not open the first part');
  2329. return false;
  2330. }
  2331. // Read the signature
  2332. $sig = fread( $this->fp, 3 );
  2333. if ($sig != 'JPA')
  2334. {
  2335. // Not a JPA file
  2336. debugMsg('Invalid archive signature');
  2337. $this->setError( AKText::_('ERR_NOT_A_JPA_FILE') );
  2338. return false;
  2339. }
  2340. // Read and parse header length
  2341. $header_length_array = unpack( 'v', fread( $this->fp, 2 ) );
  2342. $header_length = $header_length_array[1];
  2343. // Read and parse the known portion of header data (14 bytes)
  2344. $bin_data = fread($this->fp, 14);
  2345. $header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data);
  2346. // Load any remaining header data (forward compatibility)
  2347. $rest_length = $header_length - 19;
  2348. if( $rest_length > 0 )
  2349. $junk = fread($this->fp, $rest_length);
  2350. else
  2351. $junk = '';
  2352. // Temporary array with all the data we read
  2353. $temp = array(
  2354. 'signature' => $sig,
  2355. 'length' => $header_length,
  2356. 'major' => $header_data['major'],
  2357. 'minor' => $header_data['minor'],
  2358. 'filecount' => $header_data['count'],
  2359. 'uncompressedsize' => $header_data['uncsize'],
  2360. 'compressedsize' => $header_data['csize'],
  2361. 'unknowndata' => $junk
  2362. );
  2363. // Array-to-object conversion
  2364. foreach($temp as $key => $value)
  2365. {
  2366. $this->archiveHeaderData->{$key} = $value;
  2367. }
  2368. debugMsg('Header data:');
  2369. debugMsg('Length : '.$header_length);
  2370. debugMsg('Major : '.$header_data['major']);
  2371. debugMsg('Minor : '.$header_data['minor']);
  2372. debugMsg('File count : '.$header_data['count']);
  2373. debugMsg('Uncompressed size : '.$header_data['uncsize']);
  2374. debugMsg('Compressed size : '.$header_data['csize']);
  2375. $this->currentPartOffset = @ftell($this->fp);
  2376. $this->dataReadLength = 0;
  2377. return true;
  2378. }
  2379. /**
  2380. * Concrete classes must use this method to read the file header
  2381. * @return bool True if reading the file was successful, false if an error occured or we reached end of archive
  2382. */
  2383. protected function readFileHeader()
  2384. {
  2385. // If the current part is over, proceed to the next part please
  2386. if( $this->isEOF(true) ) {
  2387. debugMsg('Archive part EOF; moving to next file');
  2388. $this->nextFile();
  2389. }
  2390. debugMsg('Reading file signature');
  2391. // Get and decode Entity Description Block
  2392. $signature = fread($this->fp, 3);
  2393. $this->fileHeader = new stdClass();
  2394. $this->fileHeader->timestamp = 0;
  2395. // Check signature
  2396. if( $signature != 'JPF' )
  2397. {
  2398. if($this->isEOF(true))
  2399. {
  2400. // This file is finished; make sure it's the last one
  2401. $this->nextFile();
  2402. if(!$this->isEOF(false))
  2403. {
  2404. debugMsg('Invalid file signature before end of archive encountered');
  2405. $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset));
  2406. return false;
  2407. }
  2408. // We're just finished
  2409. return false;
  2410. }
  2411. else
  2412. {
  2413. $screwed = true;
  2414. if(AKFactory::get('kickstart.setup.ignoreerrors', false)) {
  2415. debugMsg('Invalid file block signature; launching heuristic file block signature scanner');
  2416. $screwed = !$this->heuristicFileHeaderLocator();
  2417. if(!$screwed) {
  2418. $signature = 'JPF';
  2419. } else {
  2420. debugMsg('Heuristics failed. Brace yourself for the imminent crash.');
  2421. }
  2422. }
  2423. if($screwed) {
  2424. debugMsg('Invalid file block signature');
  2425. // This is not a file block! The archive is corrupt.
  2426. $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset));
  2427. return false;
  2428. }
  2429. }
  2430. }
  2431. // This a JPA Entity Block. Process the header.
  2432. $isBannedFile = false;
  2433. // Read length of EDB and of the Entity Path Data
  2434. $length_array = unpack('vblocksize/vpathsize', fread($this->fp, 4));
  2435. // Read the path data
  2436. if($length_array['pathsize'] > 0) {
  2437. $file = fread( $this->fp, $length_array['pathsize'] );
  2438. } else {
  2439. $file = '';
  2440. }
  2441. // Handle file renaming
  2442. $isRenamed = false;
  2443. if(is_array($this->renameFiles) && (count($this->renameFiles) > 0) )
  2444. {
  2445. if(array_key_exists($file, $this->renameFiles))
  2446. {
  2447. $file = $this->renameFiles[$file];
  2448. $isRenamed = true;
  2449. }
  2450. }
  2451. // Handle directory renaming
  2452. $isDirRenamed = false;
  2453. if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) {
  2454. if(array_key_exists(dirname($file), $this->renameDirs)) {
  2455. $file = rtrim($this->renameDirs[dirname($file)],'/').'/'.basename($file);
  2456. $isRenamed = true;
  2457. $isDirRenamed = true;
  2458. }
  2459. }
  2460. // Read and parse the known data portion
  2461. $bin_data = fread( $this->fp, 14 );
  2462. $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data);
  2463. // Read any unknown data
  2464. $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']);
  2465. if( $restBytes > 0 )
  2466. {
  2467. // Start reading the extra fields
  2468. while($restBytes >= 4)
  2469. {
  2470. $extra_header_data = fread($this->fp, 4);
  2471. $extra_header = unpack('vsignature/vlength', $extra_header_data);
  2472. $restBytes -= 4;
  2473. $extra_header['length'] -= 4;
  2474. switch($extra_header['signature'])
  2475. {
  2476. case 256:
  2477. // File modified timestamp
  2478. if($extra_header['length'] > 0)
  2479. {
  2480. $bindata = fread($this->fp, $extra_header['length']);
  2481. $restBytes -= $extra_header['length'];
  2482. $timestamps = unpack('Vmodified', substr($bindata,0,4));
  2483. $filectime = $timestamps['modified'];
  2484. $this->fileHeader->timestamp = $filectime;
  2485. }
  2486. break;
  2487. default:
  2488. // Unknown field
  2489. if($extra_header['length']>0) {
  2490. $junk = fread($this->fp, $extra_header['length']);
  2491. $restBytes -= $extra_header['length'];
  2492. }
  2493. break;
  2494. }
  2495. }
  2496. if($restBytes > 0) $junk = fread($this->fp, $restBytes);
  2497. }
  2498. $compressionType = $header_data['compression'];
  2499. // Populate the return array
  2500. $this->fileHeader->file = $file;
  2501. $this->fileHeader->compressed = $header_data['compsize'];
  2502. $this->fileHeader->uncompressed = $header_data['uncompsize'];
  2503. switch($header_data['type'])
  2504. {
  2505. case 0:
  2506. $this->fileHeader->type = 'dir';
  2507. break;
  2508. case 1:
  2509. $this->fileHeader->type = 'file';
  2510. break;
  2511. case 2:
  2512. $this->fileHeader->type = 'link';
  2513. break;
  2514. }
  2515. switch( $compressionType )
  2516. {
  2517. case 0:
  2518. $this->fileHeader->compression = 'none';
  2519. break;
  2520. case 1:
  2521. $this->fileHeader->compression = 'gzip';
  2522. break;
  2523. case 2:
  2524. $this->fileHeader->compression = 'bzip2';
  2525. break;
  2526. }
  2527. $this->fileHeader->permissions = $header_data['perms'];
  2528. // Find hard-coded banned files
  2529. if( (basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..") )
  2530. {
  2531. $isBannedFile = true;
  2532. }
  2533. // Also try to find banned files passed in class configuration
  2534. if((count($this->skipFiles) > 0) && (!$isRenamed) )
  2535. {
  2536. if(in_array($this->fileHeader->file, $this->skipFiles))
  2537. {
  2538. $isBannedFile = true;
  2539. }
  2540. }
  2541. // If we have a banned file, let's skip it
  2542. if($isBannedFile)
  2543. {
  2544. debugMsg('Skipping file '.$this->fileHeader->file);
  2545. // Advance the file pointer, skipping exactly the size of the compressed data
  2546. $seekleft = $this->fileHeader->compressed;
  2547. while($seekleft > 0)
  2548. {
  2549. // Ensure that we can seek past archive part boundaries
  2550. $curSize = @filesize($this->archiveList[$this->currentPartNumber]);
  2551. $curPos = @ftell($this->fp);
  2552. $canSeek = $curSize - $curPos;
  2553. if($canSeek > $seekleft) $canSeek = $seekleft;
  2554. @fseek( $this->fp, $canSeek, SEEK_CUR );
  2555. $seekleft -= $canSeek;
  2556. if($seekleft) $this->nextFile();
  2557. }
  2558. $this->currentPartOffset = @ftell($this->fp);
  2559. $this->runState = AK_STATE_DONE;
  2560. return true;
  2561. }
  2562. // Last chance to prepend a path to the filename
  2563. if(!empty($this->addPath) && !$isDirRenamed)
  2564. {
  2565. $this->fileHeader->file = $this->addPath.$this->fileHeader->file;
  2566. }
  2567. // Get the translated path name
  2568. $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false);
  2569. if($this->fileHeader->type == 'file')
  2570. {
  2571. // Regular file; ask the postproc engine to process its filename
  2572. if($restorePerms)
  2573. {
  2574. $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file, $this->fileHeader->permissions );
  2575. }
  2576. else
  2577. {
  2578. $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file );
  2579. }
  2580. }
  2581. elseif($this->fileHeader->type == 'dir')
  2582. {
  2583. $dir = $this->fileHeader->file;
  2584. // Directory; just create it
  2585. if($restorePerms)
  2586. {
  2587. $this->postProcEngine->createDirRecursive( $this->fileHeader->file, $this->fileHeader->permissions );
  2588. }
  2589. else
  2590. {
  2591. $this->postProcEngine->createDirRecursive( $this->fileHeader->file, 0755 );
  2592. }
  2593. $this->postProcEngine->processFilename(null);
  2594. }
  2595. else
  2596. {
  2597. // Symlink; do not post-process
  2598. $this->postProcEngine->processFilename(null);
  2599. }
  2600. $this->createDirectory();
  2601. // Header is read
  2602. $this->runState = AK_STATE_HEADER;
  2603. $this->dataReadLength = 0;
  2604. return true;
  2605. }
  2606. /**
  2607. * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when
  2608. * it's finished processing the file data.
  2609. * @return bool True if processing the file data was successful, false if an error occured
  2610. */
  2611. protected function processFileData()
  2612. {
  2613. switch( $this->fileHeader->type )
  2614. {
  2615. case 'dir':
  2616. return $this->processTypeDir();
  2617. break;
  2618. case 'link':
  2619. return $this->processTypeLink();
  2620. break;
  2621. case 'file':
  2622. switch($this->fileHeader->compression)
  2623. {
  2624. case 'none':
  2625. return $this->processTypeFileUncompressed();
  2626. break;
  2627. case 'gzip':
  2628. case 'bzip2':
  2629. return $this->processTypeFileCompressedSimple();
  2630. break;
  2631. }
  2632. break;
  2633. default:
  2634. debugMsg('Unknown file type '.$this->fileHeader->type);
  2635. break;
  2636. }
  2637. }
  2638. private function processTypeFileUncompressed()
  2639. {
  2640. // Uncompressed files are being processed in small chunks, to avoid timeouts
  2641. if( ($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun','0') )
  2642. {
  2643. // Before processing file data, ensure permissions are adequate
  2644. $this->setCorrectPermissions( $this->fileHeader->file );
  2645. }
  2646. // Open the output file
  2647. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2648. {
  2649. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  2650. if ($this->dataReadLength == 0) {
  2651. $outfp = @fopen( $this->fileHeader->realFile, 'wb' );
  2652. } else {
  2653. $outfp = @fopen( $this->fileHeader->realFile, 'ab' );
  2654. }
  2655. // Can we write to the file?
  2656. if( ($outfp === false) && (!$ignore) ) {
  2657. // An error occured
  2658. debugMsg('Could not write to output file');
  2659. $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) );
  2660. return false;
  2661. }
  2662. }
  2663. // Does the file have any data, at all?
  2664. if( $this->fileHeader->compressed == 0 )
  2665. {
  2666. // No file data!
  2667. if( !AKFactory::get('kickstart.setup.dryrun','0') && is_resource($outfp) ) @fclose($outfp);
  2668. $this->runState = AK_STATE_DATAREAD;
  2669. return true;
  2670. }
  2671. // Reference to the global timer
  2672. $timer = AKFactory::getTimer();
  2673. $toReadBytes = 0;
  2674. $leftBytes = $this->fileHeader->compressed - $this->dataReadLength;
  2675. // Loop while there's data to read and enough time to do it
  2676. while( ($leftBytes > 0) && ($timer->getTimeLeft() > 0) )
  2677. {
  2678. $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes;
  2679. $data = $this->fread( $this->fp, $toReadBytes );
  2680. $reallyReadBytes = akstringlen($data);
  2681. $leftBytes -= $reallyReadBytes;
  2682. $this->dataReadLength += $reallyReadBytes;
  2683. if($reallyReadBytes < $toReadBytes)
  2684. {
  2685. // We read less than requested! Why? Did we hit local EOF?
  2686. if( $this->isEOF(true) && !$this->isEOF(false) )
  2687. {
  2688. // Yeap. Let's go to the next file
  2689. $this->nextFile();
  2690. }
  2691. else
  2692. {
  2693. // Nope. The archive is corrupt
  2694. debugMsg('Not enough data in file. The archive is truncated or corrupt.');
  2695. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  2696. return false;
  2697. }
  2698. }
  2699. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2700. if(is_resource($outfp)) @fwrite( $outfp, $data );
  2701. }
  2702. // Close the file pointer
  2703. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2704. if(is_resource($outfp)) @fclose($outfp);
  2705. // Was this a pre-timeout bail out?
  2706. if( $leftBytes > 0 )
  2707. {
  2708. $this->runState = AK_STATE_DATA;
  2709. }
  2710. else
  2711. {
  2712. // Oh! We just finished!
  2713. $this->runState = AK_STATE_DATAREAD;
  2714. $this->dataReadLength = 0;
  2715. }
  2716. return true;
  2717. }
  2718. private function processTypeFileCompressedSimple()
  2719. {
  2720. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2721. {
  2722. // Before processing file data, ensure permissions are adequate
  2723. $this->setCorrectPermissions( $this->fileHeader->file );
  2724. // Open the output file
  2725. $outfp = @fopen( $this->fileHeader->realFile, 'wb' );
  2726. // Can we write to the file?
  2727. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  2728. if( ($outfp === false) && (!$ignore) ) {
  2729. // An error occured
  2730. debugMsg('Could not write to output file');
  2731. $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) );
  2732. return false;
  2733. }
  2734. }
  2735. // Does the file have any data, at all?
  2736. if( $this->fileHeader->compressed == 0 )
  2737. {
  2738. // No file data!
  2739. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2740. if(is_resource($outfp)) @fclose($outfp);
  2741. $this->runState = AK_STATE_DATAREAD;
  2742. return true;
  2743. }
  2744. // Simple compressed files are processed as a whole; we can't do chunk processing
  2745. $zipData = $this->fread( $this->fp, $this->fileHeader->compressed );
  2746. while( akstringlen($zipData) < $this->fileHeader->compressed )
  2747. {
  2748. // End of local file before reading all data, but have more archive parts?
  2749. if($this->isEOF(true) && !$this->isEOF(false))
  2750. {
  2751. // Yeap. Read from the next file
  2752. $this->nextFile();
  2753. $bytes_left = $this->fileHeader->compressed - akstringlen($zipData);
  2754. $zipData .= $this->fread( $this->fp, $bytes_left );
  2755. }
  2756. else
  2757. {
  2758. debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.');
  2759. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  2760. return false;
  2761. }
  2762. }
  2763. if($this->fileHeader->compression == 'gzip')
  2764. {
  2765. $unzipData = gzinflate( $zipData );
  2766. }
  2767. elseif($this->fileHeader->compression == 'bzip2')
  2768. {
  2769. $unzipData = bzdecompress( $zipData );
  2770. }
  2771. unset($zipData);
  2772. // Write to the file.
  2773. if( !AKFactory::get('kickstart.setup.dryrun','0') && is_resource($outfp) )
  2774. {
  2775. @fwrite( $outfp, $unzipData, $this->fileHeader->uncompressed );
  2776. @fclose( $outfp );
  2777. }
  2778. unset($unzipData);
  2779. $this->runState = AK_STATE_DATAREAD;
  2780. return true;
  2781. }
  2782. /**
  2783. * Process the file data of a link entry
  2784. * @return bool
  2785. */
  2786. private function processTypeLink()
  2787. {
  2788. $readBytes = 0;
  2789. $toReadBytes = 0;
  2790. $leftBytes = $this->fileHeader->compressed;
  2791. $data = '';
  2792. while( $leftBytes > 0)
  2793. {
  2794. $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes;
  2795. $mydata = $this->fread( $this->fp, $toReadBytes );
  2796. $reallyReadBytes = akstringlen($mydata);
  2797. $data .= $mydata;
  2798. $leftBytes -= $reallyReadBytes;
  2799. if($reallyReadBytes < $toReadBytes)
  2800. {
  2801. // We read less than requested! Why? Did we hit local EOF?
  2802. if( $this->isEOF(true) && !$this->isEOF(false) )
  2803. {
  2804. // Yeap. Let's go to the next file
  2805. $this->nextFile();
  2806. }
  2807. else
  2808. {
  2809. debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.');
  2810. // Nope. The archive is corrupt
  2811. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  2812. return false;
  2813. }
  2814. }
  2815. }
  2816. // Try to remove an existing file or directory by the same name
  2817. if(file_exists($this->fileHeader->realFile)) { @unlink($this->fileHeader->realFile); @rmdir($this->fileHeader->realFile); }
  2818. // Remove any trailing slash
  2819. if(substr($this->fileHeader->realFile, -1) == '/') $this->fileHeader->realFile = substr($this->fileHeader->realFile, 0, -1);
  2820. // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :(
  2821. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  2822. @symlink($data, $this->fileHeader->realFile);
  2823. $this->runState = AK_STATE_DATAREAD;
  2824. return true; // No matter if the link was created!
  2825. }
  2826. /**
  2827. * Process the file data of a directory entry
  2828. * @return bool
  2829. */
  2830. private function processTypeDir()
  2831. {
  2832. // Directory entries in the JPA do not have file data, therefore we're done processing the entry
  2833. $this->runState = AK_STATE_DATAREAD;
  2834. return true;
  2835. }
  2836. /**
  2837. * Creates the directory this file points to
  2838. */
  2839. protected function createDirectory()
  2840. {
  2841. if( AKFactory::get('kickstart.setup.dryrun','0') ) return true;
  2842. // Do we need to create a directory?
  2843. if(empty($this->fileHeader->realFile)) $this->fileHeader->realFile = $this->fileHeader->file;
  2844. $lastSlash = strrpos($this->fileHeader->realFile, '/');
  2845. $dirName = substr( $this->fileHeader->realFile, 0, $lastSlash);
  2846. $perms = $this->flagRestorePermissions ? $retArray['permissions'] : 0755;
  2847. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  2848. if( ($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore) ) {
  2849. $this->setError( AKText::sprintf('COULDNT_CREATE_DIR', $dirName) );
  2850. return false;
  2851. }
  2852. else
  2853. {
  2854. return true;
  2855. }
  2856. }
  2857. protected function heuristicFileHeaderLocator()
  2858. {
  2859. $ret = false;
  2860. $fullEOF = false;
  2861. while(!$ret && !$fullEOF) {
  2862. $this->currentPartOffset = @ftell($this->fp);
  2863. if($this->isEOF(true)) {
  2864. die("PAPARIA");
  2865. $this->nextFile();
  2866. }
  2867. if($this->isEOF(false)) {
  2868. $fullEOF = true;
  2869. continue;
  2870. }
  2871. // Read 512Kb
  2872. $chunk = fread($this->fp, 524288);
  2873. $size_read = mb_strlen($string,'8bit');
  2874. //$pos = strpos($chunk, 'JPF');
  2875. $pos = mb_strpos($chunk, 'JPF', 0, '8bit');
  2876. if($pos !== false) {
  2877. // We found it!
  2878. $this->currentPartOffset += $pos + 3;
  2879. @fseek($this->fp, $this->currentPartOffset, SEEK_SET);
  2880. $ret = true;
  2881. } else {
  2882. // Not yet found :(
  2883. $this->currentPartOffset = @ftell($this->fp);
  2884. }
  2885. }
  2886. return $ret;
  2887. }
  2888. }
  2889. /**
  2890. * ZIP archive extraction class
  2891. *
  2892. * Since the file data portion of ZIP and JPA are similarly structured (it's empty for dirs,
  2893. * linked node name for symlinks, dumped binary data for no compressions and dumped gzipped
  2894. * binary data for gzip compression) we just have to subclass AKUnarchiverJPA and change the
  2895. * header reading bits. Reusable code ;)
  2896. */
  2897. class AKUnarchiverZIP extends AKUnarchiverJPA
  2898. {
  2899. var $expectDataDescriptor = false;
  2900. protected function readArchiveHeader()
  2901. {
  2902. debugMsg('Preparing to read archive header');
  2903. // Initialize header data array
  2904. $this->archiveHeaderData = new stdClass();
  2905. // Open the first part
  2906. debugMsg('Opening the first part');
  2907. $this->nextFile();
  2908. // Fail for unreadable files
  2909. if( $this->fp === false ) {
  2910. debugMsg('The first part is not readable');
  2911. return false;
  2912. }
  2913. // Read a possible multipart signature
  2914. $sigBinary = fread( $this->fp, 4 );
  2915. $headerData = unpack('Vsig', $sigBinary);
  2916. // Roll back if it's not a multipart archive
  2917. if( $headerData['sig'] == 0x04034b50 ) {
  2918. debugMsg('The archive is not multipart');
  2919. fseek($this->fp, -4, SEEK_CUR);
  2920. } else {
  2921. debugMsg('The archive is multipart');
  2922. }
  2923. $multiPartSigs = array(
  2924. 0x08074b50, // Multi-part ZIP
  2925. 0x30304b50, // Multi-part ZIP (alternate)
  2926. 0x04034b50 // Single file
  2927. );
  2928. if( !in_array($headerData['sig'], $multiPartSigs) )
  2929. {
  2930. debugMsg('Invalid header signature '.dechex($headerData['sig']));
  2931. $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE'));
  2932. return false;
  2933. }
  2934. $this->currentPartOffset = @ftell($this->fp);
  2935. debugMsg('Current part offset after reading header: '.$this->currentPartOffset);
  2936. $this->dataReadLength = 0;
  2937. return true;
  2938. }
  2939. /**
  2940. * Concrete classes must use this method to read the file header
  2941. * @return bool True if reading the file was successful, false if an error occured or we reached end of archive
  2942. */
  2943. protected function readFileHeader()
  2944. {
  2945. // If the current part is over, proceed to the next part please
  2946. if( $this->isEOF(true) ) {
  2947. debugMsg('Opening next archive part');
  2948. $this->nextFile();
  2949. }
  2950. if($this->expectDataDescriptor)
  2951. {
  2952. // The last file had bit 3 of the general purpose bit flag set. This means that we have a
  2953. // 12 byte data descriptor we need to skip. To make things worse, there might also be a 4
  2954. // byte optional data descriptor header (0x08074b50).
  2955. $junk = @fread($this->fp, 4);
  2956. $junk = unpack('Vsig', $junk);
  2957. if($junk['sig'] == 0x08074b50) {
  2958. // Yes, there was a signature
  2959. $junk = @fread($this->fp, 12);
  2960. debugMsg('Data descriptor (w/ header) skipped at '.(ftell($this->fp)-12));
  2961. } else {
  2962. // No, there was no signature, just read another 8 bytes
  2963. $junk = @fread($this->fp, 8);
  2964. debugMsg('Data descriptor (w/out header) skipped at '.(ftell($this->fp)-8));
  2965. }
  2966. // And check for EOF, too
  2967. if( $this->isEOF(true) ) {
  2968. debugMsg('EOF before reading header');
  2969. $this->nextFile();
  2970. }
  2971. }
  2972. // Get and decode Local File Header
  2973. $headerBinary = fread($this->fp, 30);
  2974. $headerData = unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary);
  2975. // Check signature
  2976. if(!( $headerData['sig'] == 0x04034b50 ))
  2977. {
  2978. debugMsg('Not a file signature at '.(ftell($this->fp)-4));
  2979. // The signature is not the one used for files. Is this a central directory record (i.e. we're done)?
  2980. if($headerData['sig'] == 0x02014b50)
  2981. {
  2982. debugMsg('EOCD signature at '.(ftell($this->fp)-4));
  2983. // End of ZIP file detected. We'll just skip to the end of file...
  2984. while( $this->nextFile() ) {};
  2985. @fseek($this->fp, 0, SEEK_END); // Go to EOF
  2986. return false;
  2987. }
  2988. else
  2989. {
  2990. debugMsg( 'Invalid signature ' . dechex($headerData['sig']) . ' at '.ftell($this->fp) );
  2991. $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE'));
  2992. return false;
  2993. }
  2994. }
  2995. // If bit 3 of the bitflag is set, expectDataDescriptor is true
  2996. $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4;
  2997. $this->fileHeader = new stdClass();
  2998. $this->fileHeader->timestamp = 0;
  2999. // Read the last modified data and time
  3000. $lastmodtime = $headerData['lastmodtime'];
  3001. $lastmoddate = $headerData['lastmoddate'];
  3002. if($lastmoddate && $lastmodtime)
  3003. {
  3004. // ----- Extract time
  3005. $v_hour = ($lastmodtime & 0xF800) >> 11;
  3006. $v_minute = ($lastmodtime & 0x07E0) >> 5;
  3007. $v_seconde = ($lastmodtime & 0x001F)*2;
  3008. // ----- Extract date
  3009. $v_year = (($lastmoddate & 0xFE00) >> 9) + 1980;
  3010. $v_month = ($lastmoddate & 0x01E0) >> 5;
  3011. $v_day = $lastmoddate & 0x001F;
  3012. // ----- Get UNIX date format
  3013. $this->fileHeader->timestamp = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
  3014. }
  3015. $isBannedFile = false;
  3016. $this->fileHeader->compressed = $headerData['compsize'];
  3017. $this->fileHeader->uncompressed = $headerData['uncomp'];
  3018. $nameFieldLength = $headerData['fnamelen'];
  3019. $extraFieldLength = $headerData['eflen'];
  3020. // Read filename field
  3021. $this->fileHeader->file = fread( $this->fp, $nameFieldLength );
  3022. // Handle file renaming
  3023. $isRenamed = false;
  3024. if(is_array($this->renameFiles) && (count($this->renameFiles) > 0) )
  3025. {
  3026. if(array_key_exists($this->fileHeader->file, $this->renameFiles))
  3027. {
  3028. $this->fileHeader->file = $this->renameFiles[$this->fileHeader->file];
  3029. $isRenamed = true;
  3030. }
  3031. }
  3032. // Handle directory renaming
  3033. $isDirRenamed = false;
  3034. if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) {
  3035. if(array_key_exists(dirname($file), $this->renameDirs)) {
  3036. $file = rtrim($this->renameDirs[dirname($file)],'/').'/'.basename($file);
  3037. $isRenamed = true;
  3038. $isDirRenamed = true;
  3039. }
  3040. }
  3041. // Read extra field if present
  3042. if($extraFieldLength > 0) {
  3043. $extrafield = fread( $this->fp, $extraFieldLength );
  3044. }
  3045. debugMsg( '*'.ftell($this->fp).' IS START OF '.$this->fileHeader->file. ' ('.$this->fileHeader->compressed.' bytes)' );
  3046. // Decide filetype -- Check for directories
  3047. $this->fileHeader->type = 'file';
  3048. if( strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1 ) $this->fileHeader->type = 'dir';
  3049. // Decide filetype -- Check for symbolic links
  3050. if( ($headerData['ver1'] == 10) && ($headerData['ver2'] == 3) )$this->fileHeader->type = 'link';
  3051. switch( $headerData['compmethod'] )
  3052. {
  3053. case 0:
  3054. $this->fileHeader->compression = 'none';
  3055. break;
  3056. case 8:
  3057. $this->fileHeader->compression = 'gzip';
  3058. break;
  3059. }
  3060. // Find hard-coded banned files
  3061. if( (basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..") )
  3062. {
  3063. $isBannedFile = true;
  3064. }
  3065. // Also try to find banned files passed in class configuration
  3066. if((count($this->skipFiles) > 0) && (!$isRenamed))
  3067. {
  3068. if(in_array($this->fileHeader->file, $this->skipFiles))
  3069. {
  3070. $isBannedFile = true;
  3071. }
  3072. }
  3073. // If we have a banned file, let's skip it
  3074. if($isBannedFile)
  3075. {
  3076. // Advance the file pointer, skipping exactly the size of the compressed data
  3077. $seekleft = $this->fileHeader->compressed;
  3078. while($seekleft > 0)
  3079. {
  3080. // Ensure that we can seek past archive part boundaries
  3081. $curSize = @filesize($this->archiveList[$this->currentPartNumber]);
  3082. $curPos = @ftell($this->fp);
  3083. $canSeek = $curSize - $curPos;
  3084. if($canSeek > $seekleft) $canSeek = $seekleft;
  3085. @fseek( $this->fp, $canSeek, SEEK_CUR );
  3086. $seekleft -= $canSeek;
  3087. if($seekleft) $this->nextFile();
  3088. }
  3089. $this->currentPartOffset = @ftell($this->fp);
  3090. $this->runState = AK_STATE_DONE;
  3091. return true;
  3092. }
  3093. // Last chance to prepend a path to the filename
  3094. if(!empty($this->addPath) && !$isDirRenamed)
  3095. {
  3096. $this->fileHeader->file = $this->addPath.$this->fileHeader->file;
  3097. }
  3098. // Get the translated path name
  3099. if($this->fileHeader->type == 'file')
  3100. {
  3101. $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file );
  3102. }
  3103. elseif($this->fileHeader->type == 'dir')
  3104. {
  3105. $this->fileHeader->timestamp = 0;
  3106. $dir = $this->fileHeader->file;
  3107. $this->postProcEngine->createDirRecursive( $this->fileHeader->file, 0755 );
  3108. $this->postProcEngine->processFilename(null);
  3109. }
  3110. else
  3111. {
  3112. // Symlink; do not post-process
  3113. $this->fileHeader->timestamp = 0;
  3114. $this->postProcEngine->processFilename(null);
  3115. }
  3116. $this->createDirectory();
  3117. // Header is read
  3118. $this->runState = AK_STATE_HEADER;
  3119. return true;
  3120. }
  3121. }
  3122. /**
  3123. * Timer class
  3124. */
  3125. class AKCoreTimer extends AKAbstractObject
  3126. {
  3127. /** @var int Maximum execution time allowance per step */
  3128. private $max_exec_time = null;
  3129. /** @var int Timestamp of execution start */
  3130. private $start_time = null;
  3131. /**
  3132. * Public constructor, creates the timer object and calculates the execution time limits
  3133. * @return AECoreTimer
  3134. */
  3135. public function __construct()
  3136. {
  3137. parent::__construct();
  3138. // Initialize start time
  3139. $this->start_time = $this->microtime_float();
  3140. // Get configured max time per step and bias
  3141. $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14);
  3142. $bias = AKFactory::get('kickstart.tuning.run_time_bias', 75)/100;
  3143. // Get PHP's maximum execution time (our upper limit)
  3144. if(@function_exists('ini_get'))
  3145. {
  3146. $php_max_exec_time = @ini_get("maximum_execution_time");
  3147. if ( (!is_numeric($php_max_exec_time)) || ($php_max_exec_time == 0) ) {
  3148. // If we have no time limit, set a hard limit of about 10 seconds
  3149. // (safe for Apache and IIS timeouts, verbose enough for users)
  3150. $php_max_exec_time = 14;
  3151. }
  3152. }
  3153. else
  3154. {
  3155. // If ini_get is not available, use a rough default
  3156. $php_max_exec_time = 14;
  3157. }
  3158. // Apply an arbitrary correction to counter CMS load time
  3159. $php_max_exec_time--;
  3160. // Apply bias
  3161. $php_max_exec_time = $php_max_exec_time * $bias;
  3162. $config_max_exec_time = $config_max_exec_time * $bias;
  3163. // Use the most appropriate time limit value
  3164. if( $config_max_exec_time > $php_max_exec_time )
  3165. {
  3166. $this->max_exec_time = $php_max_exec_time;
  3167. }
  3168. else
  3169. {
  3170. $this->max_exec_time = $config_max_exec_time;
  3171. }
  3172. }
  3173. /**
  3174. * Wake-up function to reset internal timer when we get unserialized
  3175. */
  3176. public function __wakeup()
  3177. {
  3178. // Re-initialize start time on wake-up
  3179. $this->start_time = $this->microtime_float();
  3180. }
  3181. /**
  3182. * Gets the number of seconds left, before we hit the "must break" threshold
  3183. * @return float
  3184. */
  3185. public function getTimeLeft()
  3186. {
  3187. return $this->max_exec_time - $this->getRunningTime();
  3188. }
  3189. /**
  3190. * Gets the time elapsed since object creation/unserialization, effectively how
  3191. * long Akeeba Engine has been processing data
  3192. * @return float
  3193. */
  3194. public function getRunningTime()
  3195. {
  3196. return $this->microtime_float() - $this->start_time;
  3197. }
  3198. /**
  3199. * Returns the current timestampt in decimal seconds
  3200. */
  3201. private function microtime_float()
  3202. {
  3203. list($usec, $sec) = explode(" ", microtime());
  3204. return ((float)$usec + (float)$sec);
  3205. }
  3206. /**
  3207. * Enforce the minimum execution time
  3208. */
  3209. public function enforce_min_exec_time()
  3210. {
  3211. // Try to get a sane value for PHP's maximum_execution_time INI parameter
  3212. if(@function_exists('ini_get'))
  3213. {
  3214. $php_max_exec = @ini_get("maximum_execution_time");
  3215. }
  3216. else
  3217. {
  3218. $php_max_exec = 10;
  3219. }
  3220. if ( ($php_max_exec == "") || ($php_max_exec == 0) ) {
  3221. $php_max_exec = 10;
  3222. }
  3223. // Decrease $php_max_exec time by 500 msec we need (approx.) to tear down
  3224. // the application, as well as another 500msec added for rounding
  3225. // error purposes. Also make sure this is never gonna be less than 0.
  3226. $php_max_exec = max($php_max_exec * 1000 - 1000, 0);
  3227. // Get the "minimum execution time per step" Akeeba Backup configuration variable
  3228. $minexectime = AKFactory::get('kickstart.tuning.min_exec_time',0);
  3229. if(!is_numeric($minexectime)) $minexectime = 0;
  3230. // Make sure we are not over PHP's time limit!
  3231. if($minexectime > $php_max_exec) $minexectime = $php_max_exec;
  3232. // Get current running time
  3233. $elapsed_time = $this->getRunningTime() * 1000;
  3234. // Only run a sleep delay if we haven't reached the minexectime execution time
  3235. if( ($minexectime > $elapsed_time) && ($elapsed_time > 0) )
  3236. {
  3237. $sleep_msec = $minexectime - $elapsed_time;
  3238. if(function_exists('usleep'))
  3239. {
  3240. usleep(1000 * $sleep_msec);
  3241. }
  3242. elseif(function_exists('time_nanosleep'))
  3243. {
  3244. $sleep_sec = floor($sleep_msec / 1000);
  3245. $sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000));
  3246. time_nanosleep($sleep_sec, $sleep_nsec);
  3247. }
  3248. elseif(function_exists('time_sleep_until'))
  3249. {
  3250. $until_timestamp = time() + $sleep_msec / 1000;
  3251. time_sleep_until($until_timestamp);
  3252. }
  3253. elseif(function_exists('sleep'))
  3254. {
  3255. $sleep_sec = ceil($sleep_msec/1000);
  3256. sleep( $sleep_sec );
  3257. }
  3258. }
  3259. elseif( $elapsed_time > 0 )
  3260. {
  3261. // No sleep required, even if user configured us to be able to do so.
  3262. }
  3263. }
  3264. /**
  3265. * Reset the timer. It should only be used in CLI mode!
  3266. */
  3267. public function resetTime()
  3268. {
  3269. $this->start_time = $this->microtime_float();
  3270. }
  3271. }
  3272. /**
  3273. * JPS archive extraction class
  3274. */
  3275. class AKUnarchiverJPS extends AKUnarchiverJPA
  3276. {
  3277. private $archiveHeaderData = array();
  3278. private $password = '';
  3279. public function __construct()
  3280. {
  3281. parent::__construct();
  3282. $this->password = AKFactory::get('kickstart.jps.password','');
  3283. }
  3284. protected function readArchiveHeader()
  3285. {
  3286. // Initialize header data array
  3287. $this->archiveHeaderData = new stdClass();
  3288. // Open the first part
  3289. $this->nextFile();
  3290. // Fail for unreadable files
  3291. if( $this->fp === false ) return false;
  3292. // Read the signature
  3293. $sig = fread( $this->fp, 3 );
  3294. if ($sig != 'JPS')
  3295. {
  3296. // Not a JPA file
  3297. $this->setError( AKText::_('ERR_NOT_A_JPS_FILE') );
  3298. return false;
  3299. }
  3300. // Read and parse the known portion of header data (5 bytes)
  3301. $bin_data = fread($this->fp, 5);
  3302. $header_data = unpack('Cmajor/Cminor/cspanned/vextra', $bin_data);
  3303. // Load any remaining header data (forward compatibility)
  3304. $rest_length = $header_data['extra'];
  3305. if( $rest_length > 0 )
  3306. $junk = fread($this->fp, $rest_length);
  3307. else
  3308. $junk = '';
  3309. // Temporary array with all the data we read
  3310. $temp = array(
  3311. 'signature' => $sig,
  3312. 'major' => $header_data['major'],
  3313. 'minor' => $header_data['minor'],
  3314. 'spanned' => $header_data['spanned']
  3315. );
  3316. // Array-to-object conversion
  3317. foreach($temp as $key => $value)
  3318. {
  3319. $this->archiveHeaderData->{$key} = $value;
  3320. }
  3321. $this->currentPartOffset = @ftell($this->fp);
  3322. $this->dataReadLength = 0;
  3323. return true;
  3324. }
  3325. /**
  3326. * Concrete classes must use this method to read the file header
  3327. * @return bool True if reading the file was successful, false if an error occured or we reached end of archive
  3328. */
  3329. protected function readFileHeader()
  3330. {
  3331. // If the current part is over, proceed to the next part please
  3332. if( $this->isEOF(true) ) {
  3333. $this->nextFile();
  3334. }
  3335. // Get and decode Entity Description Block
  3336. $signature = fread($this->fp, 3);
  3337. // Check for end-of-archive siganture
  3338. if($signature == 'JPE')
  3339. {
  3340. $this->setState('postrun');
  3341. return true;
  3342. }
  3343. $this->fileHeader = new stdClass();
  3344. $this->fileHeader->timestamp = 0;
  3345. // Check signature
  3346. if( $signature != 'JPF' )
  3347. {
  3348. if($this->isEOF(true))
  3349. {
  3350. // This file is finished; make sure it's the last one
  3351. $this->nextFile();
  3352. if(!$this->isEOF(false))
  3353. {
  3354. $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset));
  3355. return false;
  3356. }
  3357. // We're just finished
  3358. return false;
  3359. }
  3360. else
  3361. {
  3362. fseek($this->fp, -6, SEEK_CUR);
  3363. $signature = fread($this->fp, 3);
  3364. if($signature == 'JPE')
  3365. {
  3366. return false;
  3367. }
  3368. $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset));
  3369. return false;
  3370. }
  3371. }
  3372. // This a JPA Entity Block. Process the header.
  3373. $isBannedFile = false;
  3374. // Read and decrypt the header
  3375. $edbhData = fread($this->fp, 4);
  3376. $edbh = unpack('vencsize/vdecsize', $edbhData);
  3377. $bin_data = fread($this->fp, $edbh['encsize']);
  3378. // Decrypt and truncate
  3379. $bin_data = AKEncryptionAES::AESDecryptCBC($bin_data, $this->password, 128);
  3380. $bin_data = substr($bin_data,0,$edbh['decsize']);
  3381. // Read length of EDB and of the Entity Path Data
  3382. $length_array = unpack('vpathsize', substr($bin_data,0,2) );
  3383. // Read the path data
  3384. $file = substr($bin_data,2,$length_array['pathsize']);
  3385. // Handle file renaming
  3386. $isRenamed = false;
  3387. if(is_array($this->renameFiles) && (count($this->renameFiles) > 0) )
  3388. {
  3389. if(array_key_exists($file, $this->renameFiles))
  3390. {
  3391. $file = $this->renameFiles[$file];
  3392. $isRenamed = true;
  3393. }
  3394. }
  3395. // Handle directory renaming
  3396. $isDirRenamed = false;
  3397. if(is_array($this->renameDirs) && (count($this->renameDirs) > 0)) {
  3398. if(array_key_exists(dirname($file), $this->renameDirs)) {
  3399. $file = rtrim($this->renameDirs[dirname($file)],'/').'/'.basename($file);
  3400. $isRenamed = true;
  3401. $isDirRenamed = true;
  3402. }
  3403. }
  3404. // Read and parse the known data portion
  3405. $bin_data = substr($bin_data, 2 + $length_array['pathsize']);
  3406. $header_data = unpack('Ctype/Ccompression/Vuncompsize/Vperms/Vfilectime', $bin_data);
  3407. $this->fileHeader->timestamp = $header_data['filectime'];
  3408. $compressionType = $header_data['compression'];
  3409. // Populate the return array
  3410. $this->fileHeader->file = $file;
  3411. $this->fileHeader->uncompressed = $header_data['uncompsize'];
  3412. switch($header_data['type'])
  3413. {
  3414. case 0:
  3415. $this->fileHeader->type = 'dir';
  3416. break;
  3417. case 1:
  3418. $this->fileHeader->type = 'file';
  3419. break;
  3420. case 2:
  3421. $this->fileHeader->type = 'link';
  3422. break;
  3423. }
  3424. switch( $compressionType )
  3425. {
  3426. case 0:
  3427. $this->fileHeader->compression = 'none';
  3428. break;
  3429. case 1:
  3430. $this->fileHeader->compression = 'gzip';
  3431. break;
  3432. case 2:
  3433. $this->fileHeader->compression = 'bzip2';
  3434. break;
  3435. }
  3436. $this->fileHeader->permissions = $header_data['perms'];
  3437. // Find hard-coded banned files
  3438. if( (basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..") )
  3439. {
  3440. $isBannedFile = true;
  3441. }
  3442. // Also try to find banned files passed in class configuration
  3443. if((count($this->skipFiles) > 0) && (!$isRenamed) )
  3444. {
  3445. if(in_array($this->fileHeader->file, $this->skipFiles))
  3446. {
  3447. $isBannedFile = true;
  3448. }
  3449. }
  3450. // If we have a banned file, let's skip it
  3451. if($isBannedFile)
  3452. {
  3453. $done = false;
  3454. while(!$done)
  3455. {
  3456. // Read the Data Chunk Block header
  3457. $binMiniHead = fread($this->fp, 8);
  3458. if( in_array( substr($binMiniHead,0,3), array('JPF','JPE') ) )
  3459. {
  3460. // Not a Data Chunk Block header, I am done skipping the file
  3461. @fseek($this->fp,-8,SEEK_CUR); // Roll back the file pointer
  3462. $done = true; // Mark as done
  3463. continue; // Exit loop
  3464. }
  3465. else
  3466. {
  3467. // Skip forward by the amount of compressed data
  3468. $miniHead = unpack('Vencsize/Vdecsize');
  3469. @fseek($this->fp, $miniHead['encsize'], SEEK_CUR);
  3470. }
  3471. }
  3472. $this->currentPartOffset = @ftell($this->fp);
  3473. $this->runState = AK_STATE_DONE;
  3474. return true;
  3475. }
  3476. // Last chance to prepend a path to the filename
  3477. if(!empty($this->addPath) && !$isDirRenamed)
  3478. {
  3479. $this->fileHeader->file = $this->addPath.$this->fileHeader->file;
  3480. }
  3481. // Get the translated path name
  3482. $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false);
  3483. if($this->fileHeader->type == 'file')
  3484. {
  3485. // Regular file; ask the postproc engine to process its filename
  3486. if($restorePerms)
  3487. {
  3488. $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file, $this->fileHeader->permissions );
  3489. }
  3490. else
  3491. {
  3492. $this->fileHeader->realFile = $this->postProcEngine->processFilename( $this->fileHeader->file );
  3493. }
  3494. }
  3495. elseif($this->fileHeader->type == 'dir')
  3496. {
  3497. $dir = $this->fileHeader->file;
  3498. $this->fileHeader->realFile = $dir;
  3499. // Directory; just create it
  3500. if($restorePerms)
  3501. {
  3502. $this->postProcEngine->createDirRecursive( $this->fileHeader->file, $this->fileHeader->permissions );
  3503. }
  3504. else
  3505. {
  3506. $this->postProcEngine->createDirRecursive( $this->fileHeader->file, 0755 );
  3507. }
  3508. $this->postProcEngine->processFilename(null);
  3509. }
  3510. else
  3511. {
  3512. // Symlink; do not post-process
  3513. $this->postProcEngine->processFilename(null);
  3514. }
  3515. $this->createDirectory();
  3516. // Header is read
  3517. $this->runState = AK_STATE_HEADER;
  3518. $this->dataReadLength = 0;
  3519. return true;
  3520. }
  3521. /**
  3522. * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when
  3523. * it's finished processing the file data.
  3524. * @return bool True if processing the file data was successful, false if an error occured
  3525. */
  3526. protected function processFileData()
  3527. {
  3528. switch( $this->fileHeader->type )
  3529. {
  3530. case 'dir':
  3531. return $this->processTypeDir();
  3532. break;
  3533. case 'link':
  3534. return $this->processTypeLink();
  3535. break;
  3536. case 'file':
  3537. switch($this->fileHeader->compression)
  3538. {
  3539. case 'none':
  3540. return $this->processTypeFileUncompressed();
  3541. break;
  3542. case 'gzip':
  3543. case 'bzip2':
  3544. return $this->processTypeFileCompressedSimple();
  3545. break;
  3546. }
  3547. break;
  3548. }
  3549. }
  3550. private function processTypeFileUncompressed()
  3551. {
  3552. // Uncompressed files are being processed in small chunks, to avoid timeouts
  3553. if( ($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun','0') )
  3554. {
  3555. // Before processing file data, ensure permissions are adequate
  3556. $this->setCorrectPermissions( $this->fileHeader->file );
  3557. }
  3558. // Open the output file
  3559. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3560. {
  3561. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  3562. if ($this->dataReadLength == 0) {
  3563. $outfp = @fopen( $this->fileHeader->realFile, 'wb' );
  3564. } else {
  3565. $outfp = @fopen( $this->fileHeader->realFile, 'ab' );
  3566. }
  3567. // Can we write to the file?
  3568. if( ($outfp === false) && (!$ignore) ) {
  3569. // An error occured
  3570. $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) );
  3571. return false;
  3572. }
  3573. }
  3574. // Does the file have any data, at all?
  3575. if( $this->fileHeader->uncompressed == 0 )
  3576. {
  3577. // No file data!
  3578. if( !AKFactory::get('kickstart.setup.dryrun','0') && is_resource($outfp) ) @fclose($outfp);
  3579. $this->runState = AK_STATE_DATAREAD;
  3580. return true;
  3581. }
  3582. else
  3583. {
  3584. $this->setError('An uncompressed file was detected; this is not supported by this archive extraction utility');
  3585. return false;
  3586. }
  3587. return true;
  3588. }
  3589. private function processTypeFileCompressedSimple()
  3590. {
  3591. $timer = AKFactory::getTimer();
  3592. // Files are being processed in small chunks, to avoid timeouts
  3593. if( ($this->dataReadLength == 0) && !AKFactory::get('kickstart.setup.dryrun','0') )
  3594. {
  3595. // Before processing file data, ensure permissions are adequate
  3596. $this->setCorrectPermissions( $this->fileHeader->file );
  3597. }
  3598. // Open the output file
  3599. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3600. {
  3601. // Open the output file
  3602. $outfp = @fopen( $this->fileHeader->realFile, 'wb' );
  3603. // Can we write to the file?
  3604. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  3605. if( ($outfp === false) && (!$ignore) ) {
  3606. // An error occured
  3607. $this->setError( AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile) );
  3608. return false;
  3609. }
  3610. }
  3611. // Does the file have any data, at all?
  3612. if( $this->fileHeader->uncompressed == 0 )
  3613. {
  3614. // No file data!
  3615. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3616. if(is_resource($outfp)) @fclose($outfp);
  3617. $this->runState = AK_STATE_DATAREAD;
  3618. return true;
  3619. }
  3620. $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength;
  3621. // Loop while there's data to write and enough time to do it
  3622. while( ($leftBytes > 0) && ($timer->getTimeLeft() > 0) )
  3623. {
  3624. // Read the mini header
  3625. $binMiniHeader = fread($this->fp, 8);
  3626. $reallyReadBytes = akstringlen($binMiniHeader);
  3627. if($reallyReadBytes < 8)
  3628. {
  3629. // We read less than requested! Why? Did we hit local EOF?
  3630. if( $this->isEOF(true) && !$this->isEOF(false) )
  3631. {
  3632. // Yeap. Let's go to the next file
  3633. $this->nextFile();
  3634. // Retry reading the header
  3635. $binMiniHeader = fread($this->fp, 8);
  3636. $reallyReadBytes = akstringlen($binMiniHeader);
  3637. // Still not enough data? If so, the archive is corrupt or missing parts.
  3638. if($reallyReadBytes < 8) {
  3639. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3640. return false;
  3641. }
  3642. }
  3643. else
  3644. {
  3645. // Nope. The archive is corrupt
  3646. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3647. return false;
  3648. }
  3649. }
  3650. // Read the encrypted data
  3651. $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader);
  3652. $toReadBytes = $miniHeader['encsize'];
  3653. $data = $this->fread( $this->fp, $toReadBytes );
  3654. $reallyReadBytes = akstringlen($data);
  3655. if($reallyReadBytes < $toReadBytes)
  3656. {
  3657. // We read less than requested! Why? Did we hit local EOF?
  3658. if( $this->isEOF(true) && !$this->isEOF(false) )
  3659. {
  3660. // Yeap. Let's go to the next file
  3661. $this->nextFile();
  3662. // Read the rest of the data
  3663. $toReadBytes -= $reallyReadBytes;
  3664. $restData = $this->fread( $this->fp, $toReadBytes );
  3665. $reallyReadBytes = akstringlen($restData);
  3666. if($reallyReadBytes < $toReadBytes) {
  3667. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3668. return false;
  3669. }
  3670. if(akstringlen($data) == 0) {
  3671. $data = $restData;
  3672. } else {
  3673. $data .= $restData;
  3674. }
  3675. }
  3676. else
  3677. {
  3678. // Nope. The archive is corrupt
  3679. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3680. return false;
  3681. }
  3682. }
  3683. // Decrypt the data
  3684. $data = AKEncryptionAES::AESDecryptCBC($data, $this->password, 128);
  3685. // Is the length of the decrypted data less than expected?
  3686. $data_length = akstringlen($data);
  3687. if($data_length < $miniHeader['decsize']) {
  3688. $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD'));
  3689. return false;
  3690. }
  3691. // Trim the data
  3692. $data = substr($data,0,$miniHeader['decsize']);
  3693. // Decompress
  3694. $data = gzinflate($data);
  3695. $unc_len = akstringlen($data);
  3696. // Write the decrypted data
  3697. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3698. if(is_resource($outfp)) @fwrite( $outfp, $data, akstringlen($data) );
  3699. // Update the read length
  3700. $this->dataReadLength += $unc_len;
  3701. $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength;
  3702. }
  3703. // Close the file pointer
  3704. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3705. if(is_resource($outfp)) @fclose($outfp);
  3706. // Was this a pre-timeout bail out?
  3707. if( $leftBytes > 0 )
  3708. {
  3709. $this->runState = AK_STATE_DATA;
  3710. }
  3711. else
  3712. {
  3713. // Oh! We just finished!
  3714. $this->runState = AK_STATE_DATAREAD;
  3715. $this->dataReadLength = 0;
  3716. }
  3717. return true;
  3718. }
  3719. /**
  3720. * Process the file data of a link entry
  3721. * @return bool
  3722. */
  3723. private function processTypeLink()
  3724. {
  3725. // Does the file have any data, at all?
  3726. if( $this->fileHeader->uncompressed == 0 )
  3727. {
  3728. // No file data!
  3729. $this->runState = AK_STATE_DATAREAD;
  3730. return true;
  3731. }
  3732. // Read the mini header
  3733. $binMiniHeader = fread($this->fp, 8);
  3734. $reallyReadBytes = akstringlen($binMiniHeader);
  3735. if($reallyReadBytes < 8)
  3736. {
  3737. // We read less than requested! Why? Did we hit local EOF?
  3738. if( $this->isEOF(true) && !$this->isEOF(false) )
  3739. {
  3740. // Yeap. Let's go to the next file
  3741. $this->nextFile();
  3742. // Retry reading the header
  3743. $binMiniHeader = fread($this->fp, 8);
  3744. $reallyReadBytes = akstringlen($binMiniHeader);
  3745. // Still not enough data? If so, the archive is corrupt or missing parts.
  3746. if($reallyReadBytes < 8) {
  3747. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3748. return false;
  3749. }
  3750. }
  3751. else
  3752. {
  3753. // Nope. The archive is corrupt
  3754. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3755. return false;
  3756. }
  3757. }
  3758. // Read the encrypted data
  3759. $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader);
  3760. $toReadBytes = $miniHeader['encsize'];
  3761. $data = $this->fread( $this->fp, $toReadBytes );
  3762. $reallyReadBytes = akstringlen($data);
  3763. if($reallyReadBytes < $toReadBytes)
  3764. {
  3765. // We read less than requested! Why? Did we hit local EOF?
  3766. if( $this->isEOF(true) && !$this->isEOF(false) )
  3767. {
  3768. // Yeap. Let's go to the next file
  3769. $this->nextFile();
  3770. // Read the rest of the data
  3771. $toReadBytes -= $reallyReadBytes;
  3772. $restData = $this->fread( $this->fp, $toReadBytes );
  3773. $reallyReadBytes = akstringlen($data);
  3774. if($reallyReadBytes < $toReadBytes) {
  3775. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3776. return false;
  3777. }
  3778. $data .= $restData;
  3779. }
  3780. else
  3781. {
  3782. // Nope. The archive is corrupt
  3783. $this->setError( AKText::_('ERR_CORRUPT_ARCHIVE') );
  3784. return false;
  3785. }
  3786. }
  3787. // Decrypt the data
  3788. $data = AKEncryptionAES::AESDecryptCBC($data, $this->password, 128);
  3789. // Is the length of the decrypted data less than expected?
  3790. $data_length = akstringlen($data);
  3791. if($data_length < $miniHeader['decsize']) {
  3792. $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD'));
  3793. return false;
  3794. }
  3795. // Trim the data
  3796. $data = substr($data,0,$miniHeader['decsize']);
  3797. // Try to remove an existing file or directory by the same name
  3798. if(file_exists($this->fileHeader->realFile)) { @unlink($this->fileHeader->realFile); @rmdir($this->fileHeader->realFile); }
  3799. // Remove any trailing slash
  3800. if(substr($this->fileHeader->realFile, -1) == '/') $this->fileHeader->realFile = substr($this->fileHeader->realFile, 0, -1);
  3801. // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :(
  3802. if( !AKFactory::get('kickstart.setup.dryrun','0') )
  3803. @symlink($data, $this->fileHeader->realFile);
  3804. $this->runState = AK_STATE_DATAREAD;
  3805. return true; // No matter if the link was created!
  3806. }
  3807. /**
  3808. * Process the file data of a directory entry
  3809. * @return bool
  3810. */
  3811. private function processTypeDir()
  3812. {
  3813. // Directory entries in the JPA do not have file data, therefore we're done processing the entry
  3814. $this->runState = AK_STATE_DATAREAD;
  3815. return true;
  3816. }
  3817. /**
  3818. * Creates the directory this file points to
  3819. */
  3820. protected function createDirectory()
  3821. {
  3822. if( AKFactory::get('kickstart.setup.dryrun','0') ) return true;
  3823. // Do we need to create a directory?
  3824. $lastSlash = strrpos($this->fileHeader->realFile, '/');
  3825. $dirName = substr( $this->fileHeader->realFile, 0, $lastSlash);
  3826. $perms = $this->flagRestorePermissions ? $retArray['permissions'] : 0755;
  3827. $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false);
  3828. if( ($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore) ) {
  3829. $this->setError( AKText::sprintf('COULDNT_CREATE_DIR', $dirName) );
  3830. return false;
  3831. }
  3832. else
  3833. {
  3834. return true;
  3835. }
  3836. }
  3837. }
  3838. /**
  3839. * A filesystem scanner which uses opendir()
  3840. */
  3841. class AKUtilsLister extends AKAbstractObject
  3842. {
  3843. public function &getFiles($folder, $pattern = '*')
  3844. {
  3845. // Initialize variables
  3846. $arr = array();
  3847. $false = false;
  3848. if(!is_dir($folder)) return $false;
  3849. $handle = @opendir($folder);
  3850. // If directory is not accessible, just return FALSE
  3851. if ($handle === FALSE) {
  3852. $this->setWarning( 'Unreadable directory '.$folder);
  3853. return $false;
  3854. }
  3855. while (($file = @readdir($handle)) !== false)
  3856. {
  3857. if( !fnmatch($pattern, $file) ) continue;
  3858. if (($file != '.') && ($file != '..'))
  3859. {
  3860. $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
  3861. $dir = $folder . $ds . $file;
  3862. $isDir = is_dir($dir);
  3863. if (!$isDir) {
  3864. $arr[] = $dir;
  3865. }
  3866. }
  3867. }
  3868. @closedir($handle);
  3869. return $arr;
  3870. }
  3871. public function &getFolders($folder, $pattern = '*')
  3872. {
  3873. // Initialize variables
  3874. $arr = array();
  3875. $false = false;
  3876. if(!is_dir($folder)) return $false;
  3877. $handle = @opendir($folder);
  3878. // If directory is not accessible, just return FALSE
  3879. if ($handle === FALSE) {
  3880. $this->setWarning( 'Unreadable directory '.$folder);
  3881. return $false;
  3882. }
  3883. while (($file = @readdir($handle)) !== false)
  3884. {
  3885. if( !fnmatch($pattern, $file) ) continue;
  3886. if (($file != '.') && ($file != '..'))
  3887. {
  3888. $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
  3889. $dir = $folder . $ds . $file;
  3890. $isDir = is_dir($dir);
  3891. if ($isDir) {
  3892. $arr[] = $dir;
  3893. }
  3894. }
  3895. }
  3896. @closedir($handle);
  3897. return $arr;
  3898. }
  3899. }
  3900. /**
  3901. * A simple INI-based i18n engine
  3902. */
  3903. class AKText extends AKAbstractObject
  3904. {
  3905. /**
  3906. * The default (en_GB) translation used when no other translation is available
  3907. * @var array
  3908. */
  3909. private $default_translation = array(
  3910. 'AUTOMODEON' => 'Auto-mode enabled',
  3911. 'ERR_NOT_A_JPA_FILE' => 'The file is not a JPA archive',
  3912. 'ERR_CORRUPT_ARCHIVE' => 'The archive file is corrupt, truncated or archive parts are missing',
  3913. 'ERR_INVALID_LOGIN' => 'Invalid login',
  3914. 'COULDNT_CREATE_DIR' => 'Could not create %s folder',
  3915. 'COULDNT_WRITE_FILE' => 'Could not open %s for writing.',
  3916. 'WRONG_FTP_HOST' => 'Wrong FTP host or port',
  3917. 'WRONG_FTP_USER' => 'Wrong FTP username or password',
  3918. 'WRONG_FTP_PATH1' => 'Wrong FTP initial directory - the directory doesn\'t exist',
  3919. 'FTP_CANT_CREATE_DIR' => 'Could not create directory %s',
  3920. 'FTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory',
  3921. 'FTP_COULDNT_UPLOAD' => 'Could not upload %s',
  3922. 'THINGS_HEADER' => 'Things you should know about Akeeba Kickstart',
  3923. '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.',
  3924. '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.',
  3925. 'THINGS_03' => 'Kickstart is bound by your server\'s configuration. As such, it may not work at all.',
  3926. '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.',
  3927. '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.',
  3928. 'THINGS_06' => 'Kickstart overwrites files without a warning. If you are not sure that you are OK with that do not continue.',
  3929. '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.',
  3930. '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.',
  3931. 'THINGS_09' => 'This text does not imply that there is a problem detected. It is standard text displayed every time you launch Kickstart.',
  3932. 'CLOSE_LIGHTBOX' => 'Click here or press ESC to close this message',
  3933. 'SELECT_ARCHIVE' => 'Select a backup archive',
  3934. 'ARCHIVE_FILE' => 'Archive file:',
  3935. 'SELECT_EXTRACTION' => 'Select an extraction method',
  3936. 'WRITE_TO_FILES' => 'Write to files:',
  3937. 'WRITE_DIRECTLY' => 'Directly',
  3938. 'WRITE_FTP' => 'Use FTP',
  3939. 'FTP_HOST' => 'FTP host name:',
  3940. 'FTP_PORT' => 'FTP port:',
  3941. 'FTP_FTPS' => 'Use FTP over SSL (FTPS)',
  3942. 'FTP_PASSIVE' => 'Use FTP Passive Mode',
  3943. 'FTP_USER' => 'FTP user name:',
  3944. 'FTP_PASS' => 'FTP password:',
  3945. 'FTP_DIR' => 'FTP directory:',
  3946. 'FTP_TEMPDIR' => 'Temporary directory:',
  3947. 'FTP_CONNECTION_OK' => 'FTP Connection Established',
  3948. 'FTP_CONNECTION_FAILURE' => 'The FTP Connection Failed',
  3949. 'FTP_TEMPDIR_WRITABLE' => 'The temporary directory is writable.',
  3950. 'FTP_TEMPDIR_UNWRITABLE' => 'The temporary directory is not writable. Please check the permissions.',
  3951. 'BTN_CHECK' => 'Check',
  3952. 'BTN_RESET' => 'Reset',
  3953. 'BTN_TESTFTPCON' => 'Test FTP connection',
  3954. 'BTN_GOTOSTART' => 'Start over',
  3955. 'FINE_TUNE' => 'Fine tune',
  3956. 'MIN_EXEC_TIME' => 'Minimum execution time:',
  3957. 'MAX_EXEC_TIME' => 'Maximum execution time:',
  3958. 'SECONDS_PER_STEP' => 'seconds per step',
  3959. 'EXTRACT_FILES' => 'Extract files',
  3960. 'BTN_START' => 'Start',
  3961. 'EXTRACTING' => 'Extracting',
  3962. 'DO_NOT_CLOSE_EXTRACT' => 'Do not close this window while the extraction is in progress',
  3963. 'RESTACLEANUP' => 'Restoration and Clean Up',
  3964. 'BTN_RUNINSTALLER' => 'Run the Installer',
  3965. 'BTN_CLEANUP' => 'Clean Up',
  3966. 'BTN_SITEFE' => 'Visit your site\'s front-end',
  3967. 'BTN_SITEBE' => 'Visit your site\'s back-end',
  3968. 'WARNINGS' => 'Extraction Warnings',
  3969. 'ERROR_OCCURED' => 'An error occured',
  3970. 'STEALTH_MODE' => 'Stealth mode',
  3971. 'STEALTH_URL' => 'HTML file to show to web visitors',
  3972. 'ERR_NOT_A_JPS_FILE' => 'The file is not a JPA archive',
  3973. 'ERR_INVALID_JPS_PASSWORD' => 'The password you gave is wrong or the archive is corrupt',
  3974. 'JPS_PASSWORD' => 'Archive Password (for JPS files)',
  3975. 'INVALID_FILE_HEADER' => 'Invalid header in archive file, part %s, offset %s',
  3976. 'NEEDSOMEHELPKS' => 'Want some help to use this tool? Read this first:',
  3977. 'QUICKSTART' => 'Quick Start Guide',
  3978. 'CANTGETITTOWORK' => 'Can\'t get it to work? Click me!',
  3979. 'NOARCHIVESCLICKHERE' => 'No archives detected. Click here for troubleshooting instructions.',
  3980. 'POSTRESTORATIONTROUBLESHOOTING' => 'Something not working after the restoration? Click here for troubleshooting instructions.',
  3981. 'UPDATE_HEADER' => 'An updated version of Akeeba Kickstart (<span id="update-version">unknown</span>) is available!',
  3982. '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.',
  3983. 'UPDATE_DLNOW' => 'Download now',
  3984. 'UPDATE_MOREINFO' => 'More information',
  3985. 'IGNORE_MOST_ERRORS' => 'Ignore most errors'
  3986. );
  3987. /**
  3988. * The array holding the translation keys
  3989. * @var array
  3990. */
  3991. private $strings;
  3992. /**
  3993. * The currently detected language (ISO code)
  3994. * @var string
  3995. */
  3996. private $language;
  3997. /*
  3998. * Initializes the translation engine
  3999. * @return AKText
  4000. */
  4001. public function __construct()
  4002. {
  4003. // Start with the default translation
  4004. $this->strings = $this->default_translation;
  4005. // Try loading the translation file in English, if it exists
  4006. $this->loadTranslation('en-GB');
  4007. // Try loading the translation file in the browser's preferred language, if it exists
  4008. $this->getBrowserLanguage();
  4009. if(!is_null($this->language))
  4010. {
  4011. $this->loadTranslation();
  4012. }
  4013. }
  4014. /**
  4015. * Singleton pattern for Language
  4016. * @return Language The global Language instance
  4017. */
  4018. public static function &getInstance()
  4019. {
  4020. static $instance;
  4021. if(!is_object($instance))
  4022. {
  4023. $instance = new AKText();
  4024. }
  4025. return $instance;
  4026. }
  4027. public static function _($string)
  4028. {
  4029. $text = self::getInstance();
  4030. $key = strtoupper($string);
  4031. $key = substr($key, 0, 1) == '_' ? substr($key, 1) : $key;
  4032. if (isset ($text->strings[$key]))
  4033. {
  4034. $string = $text->strings[$key];
  4035. }
  4036. else
  4037. {
  4038. if (defined($string))
  4039. {
  4040. $string = constant($string);
  4041. }
  4042. }
  4043. return $string;
  4044. }
  4045. public static function sprintf($key)
  4046. {
  4047. $text = self::getInstance();
  4048. $args = func_get_args();
  4049. if (count($args) > 0) {
  4050. $args[0] = $text->_($args[0]);
  4051. return @call_user_func_array('sprintf', $args);
  4052. }
  4053. return '';
  4054. }
  4055. public function dumpLanguage()
  4056. {
  4057. $out = '';
  4058. foreach($this->strings as $key => $value)
  4059. {
  4060. $out .= "$key=$value\n";
  4061. }
  4062. return $out;
  4063. }
  4064. public function asJavascript()
  4065. {
  4066. $out = '';
  4067. foreach($this->strings as $key => $value)
  4068. {
  4069. $key = addcslashes($key, '\\\'"');
  4070. $value = addcslashes($value, '\\\'"');
  4071. if(!empty($out)) $out .= ",\n";
  4072. $out .= "'$key':\t'$value'";
  4073. }
  4074. return $out;
  4075. }
  4076. public function resetTranslation()
  4077. {
  4078. $this->strings = $this->default_translation;
  4079. }
  4080. public function getBrowserLanguage()
  4081. {
  4082. // Detection code from Full Operating system language detection, by Harald Hope
  4083. // Retrieved from http://techpatterns.com/downloads/php_language_detection.php
  4084. $user_languages = array();
  4085. //check to see if language is set
  4086. if ( isset( $_SERVER["HTTP_ACCEPT_LANGUAGE"] ) )
  4087. {
  4088. $languages = strtolower( $_SERVER["HTTP_ACCEPT_LANGUAGE"] );
  4089. // $languages = ' fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3';
  4090. // need to remove spaces from strings to avoid error
  4091. $languages = str_replace( ' ', '', $languages );
  4092. $languages = explode( ",", $languages );
  4093. foreach ( $languages as $language_list )
  4094. {
  4095. // pull out the language, place languages into array of full and primary
  4096. // string structure:
  4097. $temp_array = array();
  4098. // slice out the part before ; on first step, the part before - on second, place into array
  4099. $temp_array[0] = substr( $language_list, 0, strcspn( $language_list, ';' ) );//full language
  4100. $temp_array[1] = substr( $language_list, 0, 2 );// cut out primary language
  4101. if( (strlen($temp_array[0]) == 5) && ( (substr($temp_array[0],2,1) == '-') || (substr($temp_array[0],2,1) == '_') ) )
  4102. {
  4103. $langLocation = strtoupper(substr($temp_array[0],3,2));
  4104. $temp_array[0] = $temp_array[1].'-'.$langLocation;
  4105. }
  4106. //place this array into main $user_languages language array
  4107. $user_languages[] = $temp_array;
  4108. }
  4109. }
  4110. else// if no languages found
  4111. {
  4112. $user_languages[0] = array( '','' ); //return blank array.
  4113. }
  4114. $this->language = null;
  4115. $basename=basename(__FILE__, '.php') . '.ini';
  4116. // 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.
  4117. $fs = new AKUtilsLister();
  4118. $iniFiles = $fs->getFiles( dirname(__FILE__), '*.'.$basename );
  4119. if(empty($iniFiles) && ($basename != 'kickstart.ini')) {
  4120. $basename = 'kickstart.ini';
  4121. $iniFiles = $fs->getFiles( dirname(__FILE__), '*.'.$basename );
  4122. }
  4123. if (is_array($iniFiles)) {
  4124. foreach($user_languages as $languageStruct)
  4125. {
  4126. if(is_null($this->language))
  4127. {
  4128. // Get files matching the main lang part
  4129. $iniFiles = $fs->getFiles( dirname(__FILE__), $languageStruct[1].'-??.'.$basename );
  4130. if (count($iniFiles) > 0) {
  4131. $filename = $iniFiles[0];
  4132. $filename = substr($filename, strlen(dirname(__FILE__))+1);
  4133. $this->language = substr($filename, 0, 5);
  4134. } else {
  4135. $this->language = null;
  4136. }
  4137. }
  4138. }
  4139. }
  4140. if(is_null($this->language)) {
  4141. // Try to find a full language match
  4142. foreach($user_languages as $languageStruct)
  4143. {
  4144. if (@file_exists($languageStruct[0].'.'.$basename) && is_null($this->language)) {
  4145. $this->language = $languageStruct[0];
  4146. } else {
  4147. }
  4148. }
  4149. } else {
  4150. // Do we have an exact match?
  4151. foreach($user_languages as $languageStruct)
  4152. {
  4153. if(substr($this->language,0,strlen($languageStruct[1])) == $languageStruct[1]) {
  4154. if(file_exists($languageStruct[0].'.'.$basename)) {
  4155. $this->language = $languageStruct[0];
  4156. }
  4157. }
  4158. }
  4159. }
  4160. // Now, scan for full language based on the partial match
  4161. }
  4162. private function loadTranslation( $lang = null )
  4163. {
  4164. $dirname = function_exists('getcwd') ? getcwd() : dirname(__FILE__);
  4165. $basename=basename(__FILE__, '.php') . '.ini';
  4166. if( empty($lang) ) $lang = $this->language;
  4167. $translationFilename = $dirname.DIRECTORY_SEPARATOR.$lang.'.'.$basename;
  4168. if(!@file_exists($translationFilename) && ($basename != 'kickstart.ini')) {
  4169. $basename = 'kickstart.ini';
  4170. $translationFilename = $dirname.DIRECTORY_SEPARATOR.$lang.'.'.$basename;
  4171. }
  4172. if(!@file_exists($translationFilename)) return;
  4173. $temp = self::parse_ini_file($translationFilename, false);
  4174. if(!is_array($this->strings)) $this->strings = array();
  4175. if(empty($temp)) {
  4176. $this->strings = array_merge($this->default_translation, $this->strings);
  4177. } else {
  4178. $this->strings = array_merge($this->strings, $temp);
  4179. }
  4180. }
  4181. public function addDefaultLanguageStrings($stringList = array())
  4182. {
  4183. if(!is_array($stringList)) return;
  4184. if(empty($stringList)) return;
  4185. $this->strings = array_merge($stringList, $this->strings);
  4186. }
  4187. /**
  4188. * A PHP based INI file parser.
  4189. *
  4190. * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on
  4191. * the parse_ini_file page on http://gr.php.net/parse_ini_file
  4192. *
  4193. * @param string $file Filename to process
  4194. * @param bool $process_sections True to also process INI sections
  4195. * @return array An associative array of sections, keys and values
  4196. * @access private
  4197. */
  4198. public static function parse_ini_file($file, $process_sections = false, $raw_data = false)
  4199. {
  4200. $process_sections = ($process_sections !== true) ? false : true;
  4201. if(!$raw_data)
  4202. {
  4203. $ini = @file($file);
  4204. }
  4205. else
  4206. {
  4207. $ini = $file;
  4208. }
  4209. if (count($ini) == 0) {return array();}
  4210. $sections = array();
  4211. $values = array();
  4212. $result = array();
  4213. $globals = array();
  4214. $i = 0;
  4215. if(!empty($ini)) foreach ($ini as $line) {
  4216. $line = trim($line);
  4217. $line = str_replace("\t", " ", $line);
  4218. // Comments
  4219. if (!preg_match('/^[a-zA-Z0-9[]/', $line)) {continue;}
  4220. // Sections
  4221. if ($line{0} == '[') {
  4222. $tmp = explode(']', $line);
  4223. $sections[] = trim(substr($tmp[0], 1));
  4224. $i++;
  4225. continue;
  4226. }
  4227. // Key-value pair
  4228. list($key, $value) = explode('=', $line, 2);
  4229. $key = trim($key);
  4230. $value = trim($value);
  4231. if (strstr($value, ";")) {
  4232. $tmp = explode(';', $value);
  4233. if (count($tmp) == 2) {
  4234. if ((($value{0} != '"') && ($value{0} != "'")) ||
  4235. preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) ||
  4236. preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value) ){
  4237. $value = $tmp[0];
  4238. }
  4239. } else {
  4240. if ($value{0} == '"') {
  4241. $value = preg_replace('/^"(.*)".*/', '$1', $value);
  4242. } elseif ($value{0} == "'") {
  4243. $value = preg_replace("/^'(.*)'.*/", '$1', $value);
  4244. } else {
  4245. $value = $tmp[0];
  4246. }
  4247. }
  4248. }
  4249. $value = trim($value);
  4250. $value = trim($value, "'\"");
  4251. if ($i == 0) {
  4252. if (substr($line, -1, 2) == '[]') {
  4253. $globals[$key][] = $value;
  4254. } else {
  4255. $globals[$key] = $value;
  4256. }
  4257. } else {
  4258. if (substr($line, -1, 2) == '[]') {
  4259. $values[$i-1][$key][] = $value;
  4260. } else {
  4261. $values[$i-1][$key] = $value;
  4262. }
  4263. }
  4264. }
  4265. for($j = 0; $j < $i; $j++) {
  4266. if ($process_sections === true) {
  4267. $result[$sections[$j]] = $values[$j];
  4268. } else {
  4269. $result[] = $values[$j];
  4270. }
  4271. }
  4272. return $result + $globals;
  4273. }
  4274. }
  4275. /**
  4276. * The Akeeba Kickstart Factory class
  4277. * This class is reponssible for instanciating all Akeeba Kicsktart classes
  4278. */
  4279. class AKFactory {
  4280. /** @var array A list of instanciated objects */
  4281. private $objectlist = array();
  4282. /** @var array Simple hash data storage */
  4283. private $varlist = array();
  4284. /** Private constructor makes sure we can't directly instanciate the class */
  4285. private function __construct() {}
  4286. /**
  4287. * Gets a single, internally used instance of the Factory
  4288. * @param string $serialized_data [optional] Serialized data to spawn the instance from
  4289. * @return AKFactory A reference to the unique Factory object instance
  4290. */
  4291. protected static function &getInstance( $serialized_data = null ) {
  4292. static $myInstance;
  4293. if(!is_object($myInstance) || !is_null($serialized_data))
  4294. if(!is_null($serialized_data))
  4295. {
  4296. $myInstance = unserialize($serialized_data);
  4297. }
  4298. else
  4299. {
  4300. $myInstance = new self();
  4301. }
  4302. return $myInstance;
  4303. }
  4304. /**
  4305. * Internal function which instanciates a class named $class_name.
  4306. * The autoloader
  4307. * @param object $class_name
  4308. * @return
  4309. */
  4310. protected static function &getClassInstance($class_name) {
  4311. $self = self::getInstance();
  4312. if(!isset($self->objectlist[$class_name]))
  4313. {
  4314. $self->objectlist[$class_name] = new $class_name;
  4315. }
  4316. return $self->objectlist[$class_name];
  4317. }
  4318. // ========================================================================
  4319. // Public factory interface
  4320. // ========================================================================
  4321. /**
  4322. * Gets a serialized snapshot of the Factory for safekeeping (hibernate)
  4323. * @return string The serialized snapshot of the Factory
  4324. */
  4325. public static function serialize() {
  4326. $engine = self::getUnarchiver();
  4327. $engine->shutdown();
  4328. $serialized = serialize(self::getInstance());
  4329. if(function_exists('base64_encode') && function_exists('base64_decode'))
  4330. {
  4331. $serialized = base64_encode($serialized);
  4332. }
  4333. return $serialized;
  4334. }
  4335. /**
  4336. * Regenerates the full Factory state from a serialized snapshot (resume)
  4337. * @param string $serialized_data The serialized snapshot to resume from
  4338. */
  4339. public static function unserialize($serialized_data) {
  4340. if(function_exists('base64_encode') && function_exists('base64_decode'))
  4341. {
  4342. $serialized_data = base64_decode($serialized_data);
  4343. }
  4344. self::getInstance($serialized_data);
  4345. }
  4346. /**
  4347. * Reset the internal factory state, freeing all previously created objects
  4348. */
  4349. public static function nuke()
  4350. {
  4351. $self = self::getInstance();
  4352. foreach($self->objectlist as $key => $object)
  4353. {
  4354. $self->objectlist[$key] = null;
  4355. }
  4356. $self->objectlist = array();
  4357. }
  4358. // ========================================================================
  4359. // Public hash data storage interface
  4360. // ========================================================================
  4361. public static function set($key, $value)
  4362. {
  4363. $self = self::getInstance();
  4364. $self->varlist[$key] = $value;
  4365. }
  4366. public static function get($key, $default = null)
  4367. {
  4368. $self = self::getInstance();
  4369. if( array_key_exists($key, $self->varlist) )
  4370. {
  4371. return $self->varlist[$key];
  4372. }
  4373. else
  4374. {
  4375. return $default;
  4376. }
  4377. }
  4378. // ========================================================================
  4379. // Akeeba Kickstart classes
  4380. // ========================================================================
  4381. /**
  4382. * Gets the post processing engine
  4383. * @param string $proc_engine
  4384. */
  4385. public static function &getPostProc($proc_engine = null)
  4386. {
  4387. static $class_name;
  4388. if( empty($class_name) )
  4389. {
  4390. if(empty($proc_engine))
  4391. {
  4392. $proc_engine = self::get('kickstart.procengine','direct');
  4393. }
  4394. $class_name = 'AKPostproc'.ucfirst($proc_engine);
  4395. }
  4396. return self::getClassInstance($class_name);
  4397. }
  4398. /**
  4399. * Gets the unarchiver engine
  4400. */
  4401. public static function &getUnarchiver( $configOverride = null )
  4402. {
  4403. static $class_name;
  4404. if(!empty($configOverride))
  4405. {
  4406. if($configOverride['reset']) {
  4407. $class_name = null;
  4408. }
  4409. }
  4410. if( empty($class_name) )
  4411. {
  4412. $filetype = self::get('kickstart.setup.filetype', null);
  4413. if(empty($filetype))
  4414. {
  4415. $filename = self::get('kickstart.setup.sourcefile', null);
  4416. $basename = basename($filename);
  4417. $baseextension = strtoupper(substr($basename,-3));
  4418. switch($baseextension)
  4419. {
  4420. case 'JPA':
  4421. $filetype = 'JPA';
  4422. break;
  4423. case 'JPS':
  4424. $filetype = 'JPS';
  4425. break;
  4426. case 'ZIP':
  4427. $filetype = 'ZIP';
  4428. break;
  4429. default:
  4430. die('Invalid archive type or extension in file '.$filename);
  4431. break;
  4432. }
  4433. }
  4434. $class_name = 'AKUnarchiver'.ucfirst($filetype);
  4435. }
  4436. $destdir = self::get('kickstart.setup.destdir', null);
  4437. if(empty($destdir))
  4438. {
  4439. $destdir = function_exists('getcwd') ? getcwd() : dirname(__FILE__);
  4440. }
  4441. $object = self::getClassInstance($class_name);
  4442. if( $object->getState() == 'init')
  4443. {
  4444. // Initialize the object
  4445. $config = array(
  4446. 'filename' => self::get('kickstart.setup.sourcefile', ''),
  4447. 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0),
  4448. 'post_proc' => self::get('kickstart.procengine', 'direct'),
  4449. 'add_path' => self::get('kickstart.setup.targetpath', $destdir),
  4450. 'rename_files' => array( '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak' ),
  4451. 'skip_files' => array( basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', 'cacert.pem' )
  4452. );
  4453. if(!defined('KICKSTART'))
  4454. {
  4455. // In restore.php mode we have to exclude some more files
  4456. $config['skip_files'][] = 'administrator/components/com_akeeba/restore.php';
  4457. $config['skip_files'][] = 'administrator/components/com_akeeba/restoration.php';
  4458. }
  4459. if(!empty($configOverride))
  4460. {
  4461. foreach($configOverride as $key => $value)
  4462. {
  4463. $config[$key] = $value;
  4464. }
  4465. }
  4466. $object->setup($config);
  4467. }
  4468. return $object;
  4469. }
  4470. /**
  4471. * Get the a reference to the Akeeba Engine's timer
  4472. * @return AKCoreTimer
  4473. */
  4474. public static function &getTimer()
  4475. {
  4476. return self::getClassInstance('AKCoreTimer');
  4477. }
  4478. }
  4479. /**
  4480. * AES implementation in PHP (c) Chris Veness 2005-2011.
  4481. * Right to use and adapt is granted for under a simple creative commons attribution
  4482. * licence. No warranty of any form is offered.
  4483. *
  4484. * Modified for Akeeba Backup by Nicholas K. Dionysopoulos
  4485. */
  4486. class AKEncryptionAES
  4487. {
  4488. // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [?5.1.1]
  4489. protected static $Sbox =
  4490. array(0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
  4491. 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
  4492. 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
  4493. 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
  4494. 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
  4495. 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
  4496. 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
  4497. 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
  4498. 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
  4499. 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
  4500. 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
  4501. 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
  4502. 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
  4503. 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
  4504. 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
  4505. 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16);
  4506. // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [?5.2]
  4507. protected static $Rcon = array(
  4508. array(0x00, 0x00, 0x00, 0x00),
  4509. array(0x01, 0x00, 0x00, 0x00),
  4510. array(0x02, 0x00, 0x00, 0x00),
  4511. array(0x04, 0x00, 0x00, 0x00),
  4512. array(0x08, 0x00, 0x00, 0x00),
  4513. array(0x10, 0x00, 0x00, 0x00),
  4514. array(0x20, 0x00, 0x00, 0x00),
  4515. array(0x40, 0x00, 0x00, 0x00),
  4516. array(0x80, 0x00, 0x00, 0x00),
  4517. array(0x1b, 0x00, 0x00, 0x00),
  4518. array(0x36, 0x00, 0x00, 0x00) );
  4519. protected static $passwords = array();
  4520. /**
  4521. * AES Cipher function: encrypt 'input' with Rijndael algorithm
  4522. *
  4523. * @param input message as byte-array (16 bytes)
  4524. * @param w key schedule as 2D byte-array (Nr+1 x Nb bytes) -
  4525. * generated from the cipher key by KeyExpansion()
  4526. * @return ciphertext as byte-array (16 bytes)
  4527. */
  4528. protected static function Cipher($input, $w) { // main Cipher function [?5.1]
  4529. $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
  4530. $Nr = count($w)/$Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys
  4531. $state = array(); // initialise 4xNb byte-array 'state' with input [?3.4]
  4532. for ($i=0; $i<4*$Nb; $i++) $state[$i%4][floor($i/4)] = $input[$i];
  4533. $state = self::AddRoundKey($state, $w, 0, $Nb);
  4534. for ($round=1; $round<$Nr; $round++) { // apply Nr rounds
  4535. $state = self::SubBytes($state, $Nb);
  4536. $state = self::ShiftRows($state, $Nb);
  4537. $state = self::MixColumns($state, $Nb);
  4538. $state = self::AddRoundKey($state, $w, $round, $Nb);
  4539. }
  4540. $state = self::SubBytes($state, $Nb);
  4541. $state = self::ShiftRows($state, $Nb);
  4542. $state = self::AddRoundKey($state, $w, $Nr, $Nb);
  4543. $output = array(4*$Nb); // convert state to 1-d array before returning [?3.4]
  4544. for ($i=0; $i<4*$Nb; $i++) $output[$i] = $state[$i%4][floor($i/4)];
  4545. return $output;
  4546. }
  4547. protected static function AddRoundKey($state, $w, $rnd, $Nb) { // xor Round Key into state S [?5.1.4]
  4548. for ($r=0; $r<4; $r++) {
  4549. for ($c=0; $c<$Nb; $c++) $state[$r][$c] ^= $w[$rnd*4+$c][$r];
  4550. }
  4551. return $state;
  4552. }
  4553. protected static function SubBytes($s, $Nb) { // apply SBox to state S [?5.1.1]
  4554. for ($r=0; $r<4; $r++) {
  4555. for ($c=0; $c<$Nb; $c++) $s[$r][$c] = self::$Sbox[$s[$r][$c]];
  4556. }
  4557. return $s;
  4558. }
  4559. protected static function ShiftRows($s, $Nb) { // shift row r of state S left by r bytes [?5.1.2]
  4560. $t = array(4);
  4561. for ($r=1; $r<4; $r++) {
  4562. for ($c=0; $c<4; $c++) $t[$c] = $s[$r][($c+$r)%$Nb]; // shift into temp copy
  4563. for ($c=0; $c<4; $c++) $s[$r][$c] = $t[$c]; // and copy back
  4564. } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):
  4565. return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf
  4566. }
  4567. protected static function MixColumns($s, $Nb) { // combine bytes of each col of state S [?5.1.3]
  4568. for ($c=0; $c<4; $c++) {
  4569. $a = array(4); // 'a' is a copy of the current column from 's'
  4570. $b = array(4); // 'b' is a?{02} in GF(2^8)
  4571. for ($i=0; $i<4; $i++) {
  4572. $a[$i] = $s[$i][$c];
  4573. $b[$i] = $s[$i][$c]&0x80 ? $s[$i][$c]<<1 ^ 0x011b : $s[$i][$c]<<1;
  4574. }
  4575. // a[n] ^ b[n] is a?{03} in GF(2^8)
  4576. $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3
  4577. $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3
  4578. $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3
  4579. $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3
  4580. }
  4581. return $s;
  4582. }
  4583. /**
  4584. * Key expansion for Rijndael Cipher(): performs key expansion on cipher key
  4585. * to generate a key schedule
  4586. *
  4587. * @param key cipher key byte-array (16 bytes)
  4588. * @return key schedule as 2D byte-array (Nr+1 x Nb bytes)
  4589. */
  4590. protected static function KeyExpansion($key) { // generate Key Schedule from Cipher Key [?5.2]
  4591. $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
  4592. $Nk = count($key)/4; // key length (in words): 4/6/8 for 128/192/256-bit keys
  4593. $Nr = $Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys
  4594. $w = array();
  4595. $temp = array();
  4596. for ($i=0; $i<$Nk; $i++) {
  4597. $r = array($key[4*$i], $key[4*$i+1], $key[4*$i+2], $key[4*$i+3]);
  4598. $w[$i] = $r;
  4599. }
  4600. for ($i=$Nk; $i<($Nb*($Nr+1)); $i++) {
  4601. $w[$i] = array();
  4602. for ($t=0; $t<4; $t++) $temp[$t] = $w[$i-1][$t];
  4603. if ($i % $Nk == 0) {
  4604. $temp = self::SubWord(self::RotWord($temp));
  4605. for ($t=0; $t<4; $t++) $temp[$t] ^= self::$Rcon[$i/$Nk][$t];
  4606. } else if ($Nk > 6 && $i%$Nk == 4) {
  4607. $temp = self::SubWord($temp);
  4608. }
  4609. for ($t=0; $t<4; $t++) $w[$i][$t] = $w[$i-$Nk][$t] ^ $temp[$t];
  4610. }
  4611. return $w;
  4612. }
  4613. protected static function SubWord($w) { // apply SBox to 4-byte word w
  4614. for ($i=0; $i<4; $i++) $w[$i] = self::$Sbox[$w[$i]];
  4615. return $w;
  4616. }
  4617. protected static function RotWord($w) { // rotate 4-byte word w left by one byte
  4618. $tmp = $w[0];
  4619. for ($i=0; $i<3; $i++) $w[$i] = $w[$i+1];
  4620. $w[3] = $tmp;
  4621. return $w;
  4622. }
  4623. /*
  4624. * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints
  4625. *
  4626. * @param a number to be shifted (32-bit integer)
  4627. * @param b number of bits to shift a to the right (0..31)
  4628. * @return a right-shifted and zero-filled by b bits
  4629. */
  4630. protected static function urs($a, $b) {
  4631. $a &= 0xffffffff; $b &= 0x1f; // (bounds check)
  4632. if ($a&0x80000000 && $b>0) { // if left-most bit set
  4633. $a = ($a>>1) & 0x7fffffff; // right-shift one bit & clear left-most bit
  4634. $a = $a >> ($b-1); // remaining right-shifts
  4635. } else { // otherwise
  4636. $a = ($a>>$b); // use normal right-shift
  4637. }
  4638. return $a;
  4639. }
  4640. /**
  4641. * Encrypt a text using AES encryption in Counter mode of operation
  4642. * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
  4643. *
  4644. * Unicode multi-byte character safe
  4645. *
  4646. * @param plaintext source text to be encrypted
  4647. * @param password the password to use to generate a key
  4648. * @param nBits number of bits to be used in the key (128, 192, or 256)
  4649. * @return encrypted text
  4650. */
  4651. public static function AESEncryptCtr($plaintext, $password, $nBits) {
  4652. $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
  4653. if (!($nBits==128 || $nBits==192 || $nBits==256)) return ''; // standard allows 128/192/256 bit keys
  4654. // note PHP (5) gives us plaintext and password in UTF8 encoding!
  4655. // use AES itself to encrypt password to get cipher key (using plain password as source for
  4656. // key expansion) - gives us well encrypted key
  4657. $nBytes = $nBits/8; // no bytes in key
  4658. $pwBytes = array();
  4659. for ($i=0; $i<$nBytes; $i++) $pwBytes[$i] = ord(substr($password,$i,1)) & 0xff;
  4660. $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes));
  4661. $key = array_merge($key, array_slice($key, 0, $nBytes-16)); // expand key to 16/24/32 bytes long
  4662. // initialise counter block (NIST SP800-38A ?B.2): millisecond time-stamp for nonce in
  4663. // 1st 8 bytes, block counter in 2nd 8 bytes
  4664. $counterBlock = array();
  4665. $nonce = floor(microtime(true)*1000); // timestamp: milliseconds since 1-Jan-1970
  4666. $nonceSec = floor($nonce/1000);
  4667. $nonceMs = $nonce%1000;
  4668. // encode nonce with seconds in 1st 4 bytes, and (repeated) ms part filling 2nd 4 bytes
  4669. for ($i=0; $i<4; $i++) $counterBlock[$i] = self::urs($nonceSec, $i*8) & 0xff;
  4670. for ($i=0; $i<4; $i++) $counterBlock[$i+4] = $nonceMs & 0xff;
  4671. // and convert it to a string to go on the front of the ciphertext
  4672. $ctrTxt = '';
  4673. for ($i=0; $i<8; $i++) $ctrTxt .= chr($counterBlock[$i]);
  4674. // generate key schedule - an expansion of the key into distinct Key Rounds for each round
  4675. $keySchedule = self::KeyExpansion($key);
  4676. $blockCount = ceil(strlen($plaintext)/$blockSize);
  4677. $ciphertxt = array(); // ciphertext as array of strings
  4678. for ($b=0; $b<$blockCount; $b++) {
  4679. // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
  4680. // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
  4681. for ($c=0; $c<4; $c++) $counterBlock[15-$c] = self::urs($b, $c*8) & 0xff;
  4682. for ($c=0; $c<4; $c++) $counterBlock[15-$c-4] = self::urs($b/0x100000000, $c*8);
  4683. $cipherCntr = self::Cipher($counterBlock, $keySchedule); // -- encrypt counter block --
  4684. // block size is reduced on final block
  4685. $blockLength = $b<$blockCount-1 ? $blockSize : (strlen($plaintext)-1)%$blockSize+1;
  4686. $cipherByte = array();
  4687. for ($i=0; $i<$blockLength; $i++) { // -- xor plaintext with ciphered counter byte-by-byte --
  4688. $cipherByte[$i] = $cipherCntr[$i] ^ ord(substr($plaintext, $b*$blockSize+$i, 1));
  4689. $cipherByte[$i] = chr($cipherByte[$i]);
  4690. }
  4691. $ciphertxt[$b] = implode('', $cipherByte); // escape troublesome characters in ciphertext
  4692. }
  4693. // implode is more efficient than repeated string concatenation
  4694. $ciphertext = $ctrTxt . implode('', $ciphertxt);
  4695. $ciphertext = base64_encode($ciphertext);
  4696. return $ciphertext;
  4697. }
  4698. /**
  4699. * Decrypt a text encrypted by AES in counter mode of operation
  4700. *
  4701. * @param ciphertext source text to be decrypted
  4702. * @param password the password to use to generate a key
  4703. * @param nBits number of bits to be used in the key (128, 192, or 256)
  4704. * @return decrypted text
  4705. */
  4706. public static function AESDecryptCtr($ciphertext, $password, $nBits) {
  4707. $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
  4708. if (!($nBits==128 || $nBits==192 || $nBits==256)) return ''; // standard allows 128/192/256 bit keys
  4709. $ciphertext = base64_decode($ciphertext);
  4710. // use AES to encrypt password (mirroring encrypt routine)
  4711. $nBytes = $nBits/8; // no bytes in key
  4712. $pwBytes = array();
  4713. for ($i=0; $i<$nBytes; $i++) $pwBytes[$i] = ord(substr($password,$i,1)) & 0xff;
  4714. $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes));
  4715. $key = array_merge($key, array_slice($key, 0, $nBytes-16)); // expand key to 16/24/32 bytes long
  4716. // recover nonce from 1st element of ciphertext
  4717. $counterBlock = array();
  4718. $ctrTxt = substr($ciphertext, 0, 8);
  4719. for ($i=0; $i<8; $i++) $counterBlock[$i] = ord(substr($ctrTxt,$i,1));
  4720. // generate key schedule
  4721. $keySchedule = self::KeyExpansion($key);
  4722. // separate ciphertext into blocks (skipping past initial 8 bytes)
  4723. $nBlocks = ceil((strlen($ciphertext)-8) / $blockSize);
  4724. $ct = array();
  4725. for ($b=0; $b<$nBlocks; $b++) $ct[$b] = substr($ciphertext, 8+$b*$blockSize, 16);
  4726. $ciphertext = $ct; // ciphertext is now array of block-length strings
  4727. // plaintext will get generated block-by-block into array of block-length strings
  4728. $plaintxt = array();
  4729. for ($b=0; $b<$nBlocks; $b++) {
  4730. // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
  4731. for ($c=0; $c<4; $c++) $counterBlock[15-$c] = self::urs($b, $c*8) & 0xff;
  4732. for ($c=0; $c<4; $c++) $counterBlock[15-$c-4] = self::urs(($b+1)/0x100000000-1, $c*8) & 0xff;
  4733. $cipherCntr = self::Cipher($counterBlock, $keySchedule); // encrypt counter block
  4734. $plaintxtByte = array();
  4735. for ($i=0; $i<strlen($ciphertext[$b]); $i++) {
  4736. // -- xor plaintext with ciphered counter byte-by-byte --
  4737. $plaintxtByte[$i] = $cipherCntr[$i] ^ ord(substr($ciphertext[$b],$i,1));
  4738. $plaintxtByte[$i] = chr($plaintxtByte[$i]);
  4739. }
  4740. $plaintxt[$b] = implode('', $plaintxtByte);
  4741. }
  4742. // join array of blocks into single plaintext string
  4743. $plaintext = implode('',$plaintxt);
  4744. return $plaintext;
  4745. }
  4746. /**
  4747. * AES decryption in CBC mode. This is the standard mode (the CTR methods
  4748. * actually use Rijndael-128 in CTR mode, which - technically - isn't AES).
  4749. *
  4750. * Supports AES-128, AES-192 and AES-256. It supposes that the last 4 bytes
  4751. * contained a little-endian unsigned long integer representing the unpadded
  4752. * data length.
  4753. *
  4754. * @since 3.0.1
  4755. * @author Nicholas K. Dionysopoulos
  4756. *
  4757. * @param string $ciphertext The data to encrypt
  4758. * @param string $password Encryption password
  4759. * @param int $nBits Encryption key size. Can be 128, 192 or 256
  4760. * @return string The plaintext
  4761. */
  4762. public static function AESDecryptCBC($ciphertext, $password, $nBits = 128)
  4763. {
  4764. if (!($nBits==128 || $nBits==192 || $nBits==256)) return false; // standard allows 128/192/256 bit keys
  4765. if(!function_exists('mcrypt_module_open')) return false;
  4766. // Try to fetch cached key/iv or create them if they do not exist
  4767. $lookupKey = $password.'-'.$nBits;
  4768. if(array_key_exists($lookupKey, self::$passwords))
  4769. {
  4770. $key = self::$passwords[$lookupKey]['key'];
  4771. $iv = self::$passwords[$lookupKey]['iv'];
  4772. }
  4773. else
  4774. {
  4775. // use AES itself to encrypt password to get cipher key (using plain password as source for
  4776. // key expansion) - gives us well encrypted key
  4777. $nBytes = $nBits/8; // no bytes in key
  4778. $pwBytes = array();
  4779. for ($i=0; $i<$nBytes; $i++) $pwBytes[$i] = ord(substr($password,$i,1)) & 0xff;
  4780. $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes));
  4781. $key = array_merge($key, array_slice($key, 0, $nBytes-16)); // expand key to 16/24/32 bytes long
  4782. $newKey = '';
  4783. foreach($key as $int) { $newKey .= chr($int); }
  4784. $key = $newKey;
  4785. // Create an Initialization Vector (IV) based on the password, using the same technique as for the key
  4786. $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes
  4787. $pwBytes = array();
  4788. for ($i=0; $i<$nBytes; $i++) $pwBytes[$i] = ord(substr($password,$i,1)) & 0xff;
  4789. $iv = self::Cipher($pwBytes, self::KeyExpansion($pwBytes));
  4790. $newIV = '';
  4791. foreach($iv as $int) { $newIV .= chr($int); }
  4792. $iv = $newIV;
  4793. self::$passwords[$lookupKey]['key'] = $key;
  4794. self::$passwords[$lookupKey]['iv'] = $iv;
  4795. }
  4796. // Read the data size
  4797. $data_size = unpack('V', substr($ciphertext,-4) );
  4798. // Decrypt
  4799. $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
  4800. mcrypt_generic_init($td, $key, $iv);
  4801. $plaintext = mdecrypt_generic($td, substr($ciphertext,0,-4));
  4802. mcrypt_generic_deinit($td);
  4803. // Trim padding, if necessary
  4804. if(strlen($plaintext) > $data_size)
  4805. {
  4806. $plaintext = substr($plaintext, 0, $data_size);
  4807. }
  4808. return $plaintext;
  4809. }
  4810. }
  4811. /**
  4812. * The Master Setup will read the configuration parameters from restoration.php, abiautomation.ini, or
  4813. * the JSON-encoded "configuration" input variable and return the status.
  4814. * @return bool True if the master configuration was applied to the Factory object
  4815. */
  4816. function masterSetup()
  4817. {
  4818. // ------------------------------------------------------------
  4819. // 1. Import basic setup parameters
  4820. // ------------------------------------------------------------
  4821. $ini_data = null;
  4822. // In restore.php mode, require restoration.php or fail
  4823. if(!defined('KICKSTART'))
  4824. {
  4825. // This is the standalone mode, used by Akeeba Backup Professional. It looks for a restoration.php
  4826. // file to perform its magic. If the file is not there, we will abort.
  4827. $setupFile = 'restoration.php';
  4828. if( !file_exists($setupFile) )
  4829. {
  4830. // Uh oh... Somebody tried to pooh on our back yard. Lock the gates! Don't let the traitor inside!
  4831. AKFactory::set('kickstart.enabled', false);
  4832. return false;
  4833. }
  4834. // Load restoration.php. It creates a global variable named $restoration_setup
  4835. require_once $setupFile;
  4836. $ini_data = $restoration_setup;
  4837. if(empty($ini_data))
  4838. {
  4839. // No parameters fetched. Darn, how am I supposed to work like that?!
  4840. AKFactory::set('kickstart.enabled', false);
  4841. return false;
  4842. }
  4843. AKFactory::set('kickstart.enabled', true);
  4844. }
  4845. else
  4846. {
  4847. // Maybe we have $restoration_setup defined in the head of kickstart.php
  4848. global $restoration_setup;
  4849. if(!empty($restoration_setup) && !is_array($restoration_setup)) {
  4850. $ini_data = AKText::parse_ini_file($restoration_setup, false, true);
  4851. } elseif(is_array($restoration_setup)) {
  4852. $ini_data = $restoration_setup;
  4853. }
  4854. }
  4855. // Import any data from $restoration_setup
  4856. if(!empty($ini_data))
  4857. {
  4858. foreach($ini_data as $key => $value)
  4859. {
  4860. AKFactory::set($key, $value);
  4861. }
  4862. AKFactory::set('kickstart.enabled', true);
  4863. }
  4864. // Reinitialize $ini_data
  4865. $ini_data = null;
  4866. // ------------------------------------------------------------
  4867. // 2. Explode JSON parameters into $_REQUEST scope
  4868. // ------------------------------------------------------------
  4869. // Detect a JSON string in the request variable and store it.
  4870. $json = getQueryParam('json', null);
  4871. // Remove everything from the request array
  4872. if(!empty($_REQUEST))
  4873. {
  4874. foreach($_REQUEST as $key => $value)
  4875. {
  4876. unset($_REQUEST[$key]);
  4877. }
  4878. }
  4879. // Decrypt a possibly encrypted JSON string
  4880. if(!empty($json))
  4881. {
  4882. $password = AKFactory::get('kickstart.security.password', null);
  4883. if(!empty($password))
  4884. {
  4885. $json = AKEncryptionAES::AESDecryptCtr($json, $password, 128);
  4886. }
  4887. // Get the raw data
  4888. $raw = json_decode( $json, true );
  4889. // Pass all JSON data to the request array
  4890. if(!empty($raw))
  4891. {
  4892. foreach($raw as $key => $value)
  4893. {
  4894. $_REQUEST[$key] = $value;
  4895. }
  4896. }
  4897. }
  4898. // ------------------------------------------------------------
  4899. // 3. Try the "factory" variable
  4900. // ------------------------------------------------------------
  4901. // A "factory" variable will override all other settings.
  4902. $serialized = getQueryParam('factory', null);
  4903. if( !is_null($serialized) )
  4904. {
  4905. // Get the serialized factory
  4906. AKFactory::unserialize($serialized);
  4907. AKFactory::set('kickstart.enabled', true);
  4908. return true;
  4909. }
  4910. // ------------------------------------------------------------
  4911. // 4. Try abiautomation.ini and the configuration variable for Kickstart
  4912. // ------------------------------------------------------------
  4913. if(defined('KICKSTART'))
  4914. {
  4915. // We are in Kickstart mode. abiautomation.ini has precedence.
  4916. $setupFile = 'abiautomation.ini';
  4917. if( file_exists($setupFile) )
  4918. {
  4919. // abiautomation.ini was found
  4920. $ini_data = AKText::parse_ini_file('restoration.ini', false);
  4921. }
  4922. else
  4923. {
  4924. // abiautomation.ini was not found. Let's try input parameters.
  4925. $configuration = getQueryParam('configuration');
  4926. if( !is_null($configuration) )
  4927. {
  4928. // Let's decode the configuration from JSON to array
  4929. $ini_data = json_decode($configuration, true);
  4930. }
  4931. else
  4932. {
  4933. // Neither exists. Enable Kickstart's interface anyway.
  4934. $ini_data = array('kickstart.enabled'=>true);
  4935. }
  4936. }
  4937. // Import any INI data we might have from other sources
  4938. if(!empty($ini_data))
  4939. {
  4940. foreach($ini_data as $key => $value)
  4941. {
  4942. AKFactory::set($key, $value);
  4943. }
  4944. AKFactory::set('kickstart.enabled', true);
  4945. return true;
  4946. }
  4947. }
  4948. }
  4949. // Mini-controller for restore.php
  4950. if(!defined('KICKSTART'))
  4951. {
  4952. // The observer class, used to report number of files and bytes processed
  4953. class RestorationObserver extends AKAbstractPartObserver
  4954. {
  4955. public $compressedTotal = 0;
  4956. public $uncompressedTotal = 0;
  4957. public $filesProcessed = 0;
  4958. public function update($object, $message)
  4959. {
  4960. if(!is_object($message)) return;
  4961. if( !array_key_exists('type', get_object_vars($message)) ) return;
  4962. if( $message->type == 'startfile' )
  4963. {
  4964. $this->filesProcessed++;
  4965. $this->compressedTotal += $message->content->compressed;
  4966. $this->uncompressedTotal += $message->content->uncompressed;
  4967. }
  4968. }
  4969. public function __toString()
  4970. {
  4971. return __CLASS__;
  4972. }
  4973. }
  4974. // Import configuration
  4975. masterSetup();
  4976. $retArray = array(
  4977. 'status' => true,
  4978. 'message' => null
  4979. );
  4980. $enabled = AKFactory::get('kickstart.enabled', false);
  4981. if($enabled)
  4982. {
  4983. $task = getQueryParam('task');
  4984. switch($task)
  4985. {
  4986. case 'ping':
  4987. // ping task - realy does nothing!
  4988. $timer = AKFactory::getTimer();
  4989. $timer->enforce_min_exec_time();
  4990. break;
  4991. case 'startRestore':
  4992. AKFactory::nuke(); // Reset the factory
  4993. // Let the control flow to the next step (the rest of the code is common!!)
  4994. case 'stepRestore':
  4995. $engine = AKFactory::getUnarchiver(); // Get the engine
  4996. $observer = new RestorationObserver(); // Create a new observer
  4997. $engine->attach($observer); // Attach the observer
  4998. $engine->tick();
  4999. $ret = $engine->getStatusArray();
  5000. if( $ret['Error'] != '' )
  5001. {
  5002. $retArray['status'] = false;
  5003. $retArray['done'] = true;
  5004. $retArray['message'] = $ret['Error'];
  5005. }
  5006. elseif( !$ret['HasRun'] )
  5007. {
  5008. $retArray['files'] = $observer->filesProcessed;
  5009. $retArray['bytesIn'] = $observer->compressedTotal;
  5010. $retArray['bytesOut'] = $observer->uncompressedTotal;
  5011. $retArray['status'] = true;
  5012. $retArray['done'] = true;
  5013. }
  5014. else
  5015. {
  5016. $retArray['files'] = $observer->filesProcessed;
  5017. $retArray['bytesIn'] = $observer->compressedTotal;
  5018. $retArray['bytesOut'] = $observer->uncompressedTotal;
  5019. $retArray['status'] = true;
  5020. $retArray['done'] = false;
  5021. $retArray['factory'] = AKFactory::serialize();
  5022. }
  5023. break;
  5024. case 'finalizeRestore':
  5025. $root = AKFactory::get('kickstart.setup.destdir');
  5026. // Remove the installation directory
  5027. recursive_remove_directory( $root.'/installation' );
  5028. $postproc = AKFactory::getPostProc();
  5029. // Rename htaccess.bak to .htaccess
  5030. if(file_exists($root.'/htaccess.bak'))
  5031. {
  5032. if( file_exists($root.'/.htaccess') )
  5033. {
  5034. $postproc->unlink($root.'/.htaccess');
  5035. }
  5036. $postproc->rename( $root.'/htaccess.bak', $root.'/.htaccess' );
  5037. }
  5038. // Remove restoration.php
  5039. $basepath = dirname(__FILE__);
  5040. $basepath = rtrim( str_replace('\\','/',$basepath), '/' );
  5041. if(!empty($basepath)) $basepath .= '/';
  5042. $postproc->unlink( $basepath.'restoration.php' );
  5043. break;
  5044. default:
  5045. // Invalid task!
  5046. $enabled = false;
  5047. break;
  5048. }
  5049. }
  5050. // Maybe we weren't authorized or the task was invalid?
  5051. if(!$enabled)
  5052. {
  5053. // Maybe the user failed to enter any information
  5054. $retArray['status'] = false;
  5055. $retArray['message'] = AKText::_('ERR_INVALID_LOGIN');
  5056. }
  5057. // JSON encode the message
  5058. $json = json_encode($retArray);
  5059. // Do I have to encrypt?
  5060. $password = AKFactory::get('kickstart.security.password', null);
  5061. if(!empty($password))
  5062. {
  5063. $json = AKEncryptionAES::AESEncryptCtr($json, $password, 128);
  5064. }
  5065. // Return the message
  5066. echo "###$json###";
  5067. }
  5068. // ------------ lixlpixel recursive PHP functions -------------
  5069. // recursive_remove_directory( directory to delete, empty )
  5070. // expects path to directory and optional TRUE / FALSE to empty
  5071. // of course PHP has to have the rights to delete the directory
  5072. // you specify and all files and folders inside the directory
  5073. // ------------------------------------------------------------
  5074. function recursive_remove_directory($directory)
  5075. {
  5076. // if the path has a slash at the end we remove it here
  5077. if(substr($directory,-1) == '/')
  5078. {
  5079. $directory = substr($directory,0,-1);
  5080. }
  5081. // if the path is not valid or is not a directory ...
  5082. if(!file_exists($directory) || !is_dir($directory))
  5083. {
  5084. // ... we return false and exit the function
  5085. return FALSE;
  5086. // ... if the path is not readable
  5087. }elseif(!is_readable($directory))
  5088. {
  5089. // ... we return false and exit the function
  5090. return FALSE;
  5091. // ... else if the path is readable
  5092. }else{
  5093. // we open the directory
  5094. $handle = opendir($directory);
  5095. $postproc = AKFactory::getPostProc();
  5096. // and scan through the items inside
  5097. while (FALSE !== ($item = readdir($handle)))
  5098. {
  5099. // if the filepointer is not the current directory
  5100. // or the parent directory
  5101. if($item != '.' && $item != '..')
  5102. {
  5103. // we build the new path to delete
  5104. $path = $directory.'/'.$item;
  5105. // if the new path is a directory
  5106. if(is_dir($path))
  5107. {
  5108. // we call this function with the new path
  5109. recursive_remove_directory($path);
  5110. // if the new path is a file
  5111. }else{
  5112. // we remove the file
  5113. $postproc->unlink($path);
  5114. }
  5115. }
  5116. }
  5117. // close the directory
  5118. closedir($handle);
  5119. // try to delete the now empty directory
  5120. if(!$postproc->rmdir($directory))
  5121. {
  5122. // return false if not possible
  5123. return FALSE;
  5124. }
  5125. // return success
  5126. return TRUE;
  5127. }
  5128. }
  5129. ?>