You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

532 lines
13 KiB

7 years ago
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 1.0.0
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * Zip Compression Class
  41. *
  42. * This class is based on a library I found at Zend:
  43. * http://www.zend.com/codex.php?id=696&single=1
  44. *
  45. * The original library is a little rough around the edges so I
  46. * refactored it and added several additional methods -- Rick Ellis
  47. *
  48. * @package CodeIgniter
  49. * @subpackage Libraries
  50. * @category Encryption
  51. * @author EllisLab Dev Team
  52. * @link https://codeigniter.com/user_guide/libraries/zip.html
  53. */
  54. class CI_Zip {
  55. /**
  56. * Zip data in string form
  57. *
  58. * @var string
  59. */
  60. public $zipdata = '';
  61. /**
  62. * Zip data for a directory in string form
  63. *
  64. * @var string
  65. */
  66. public $directory = '';
  67. /**
  68. * Number of files/folder in zip file
  69. *
  70. * @var int
  71. */
  72. public $entries = 0;
  73. /**
  74. * Number of files in zip
  75. *
  76. * @var int
  77. */
  78. public $file_num = 0;
  79. /**
  80. * relative offset of local header
  81. *
  82. * @var int
  83. */
  84. public $offset = 0;
  85. /**
  86. * Reference to time at init
  87. *
  88. * @var int
  89. */
  90. public $now;
  91. /**
  92. * The level of compression
  93. *
  94. * Ranges from 0 to 9, with 9 being the highest level.
  95. *
  96. * @var int
  97. */
  98. public $compression_level = 2;
  99. /**
  100. * mbstring.func_overload flag
  101. *
  102. * @var bool
  103. */
  104. protected static $func_overload;
  105. /**
  106. * Initialize zip compression class
  107. *
  108. * @return void
  109. */
  110. public function __construct()
  111. {
  112. isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload'));
  113. $this->now = time();
  114. log_message('info', 'Zip Compression Class Initialized');
  115. }
  116. // --------------------------------------------------------------------
  117. /**
  118. * Add Directory
  119. *
  120. * Lets you add a virtual directory into which you can place files.
  121. *
  122. * @param mixed $directory the directory name. Can be string or array
  123. * @return void
  124. */
  125. public function add_dir($directory)
  126. {
  127. foreach ((array) $directory as $dir)
  128. {
  129. if ( ! preg_match('|.+/$|', $dir))
  130. {
  131. $dir .= '/';
  132. }
  133. $dir_time = $this->_get_mod_time($dir);
  134. $this->_add_dir($dir, $dir_time['file_mtime'], $dir_time['file_mdate']);
  135. }
  136. }
  137. // --------------------------------------------------------------------
  138. /**
  139. * Get file/directory modification time
  140. *
  141. * If this is a newly created file/dir, we will set the time to 'now'
  142. *
  143. * @param string $dir path to file
  144. * @return array filemtime/filemdate
  145. */
  146. protected function _get_mod_time($dir)
  147. {
  148. // filemtime() may return false, but raises an error for non-existing files
  149. $date = file_exists($dir) ? getdate(filemtime($dir)) : getdate($this->now);
  150. return array(
  151. 'file_mtime' => ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2,
  152. 'file_mdate' => (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday']
  153. );
  154. }
  155. // --------------------------------------------------------------------
  156. /**
  157. * Add Directory
  158. *
  159. * @param string $dir the directory name
  160. * @param int $file_mtime
  161. * @param int $file_mdate
  162. * @return void
  163. */
  164. protected function _add_dir($dir, $file_mtime, $file_mdate)
  165. {
  166. $dir = str_replace('\\', '/', $dir);
  167. $this->zipdata .=
  168. "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00"
  169. .pack('v', $file_mtime)
  170. .pack('v', $file_mdate)
  171. .pack('V', 0) // crc32
  172. .pack('V', 0) // compressed filesize
  173. .pack('V', 0) // uncompressed filesize
  174. .pack('v', self::strlen($dir)) // length of pathname
  175. .pack('v', 0) // extra field length
  176. .$dir
  177. // below is "data descriptor" segment
  178. .pack('V', 0) // crc32
  179. .pack('V', 0) // compressed filesize
  180. .pack('V', 0); // uncompressed filesize
  181. $this->directory .=
  182. "\x50\x4b\x01\x02\x00\x00\x0a\x00\x00\x00\x00\x00"
  183. .pack('v', $file_mtime)
  184. .pack('v', $file_mdate)
  185. .pack('V',0) // crc32
  186. .pack('V',0) // compressed filesize
  187. .pack('V',0) // uncompressed filesize
  188. .pack('v', self::strlen($dir)) // length of pathname
  189. .pack('v', 0) // extra field length
  190. .pack('v', 0) // file comment length
  191. .pack('v', 0) // disk number start
  192. .pack('v', 0) // internal file attributes
  193. .pack('V', 16) // external file attributes - 'directory' bit set
  194. .pack('V', $this->offset) // relative offset of local header
  195. .$dir;
  196. $this->offset = self::strlen($this->zipdata);
  197. $this->entries++;
  198. }
  199. // --------------------------------------------------------------------
  200. /**
  201. * Add Data to Zip
  202. *
  203. * Lets you add files to the archive. If the path is included
  204. * in the filename it will be placed within a directory. Make
  205. * sure you use add_dir() first to create the folder.
  206. *
  207. * @param mixed $filepath A single filepath or an array of file => data pairs
  208. * @param string $data Single file contents
  209. * @return void
  210. */
  211. public function add_data($filepath, $data = NULL)
  212. {
  213. if (is_array($filepath))
  214. {
  215. foreach ($filepath as $path => $data)
  216. {
  217. $file_data = $this->_get_mod_time($path);
  218. $this->_add_data($path, $data, $file_data['file_mtime'], $file_data['file_mdate']);
  219. }
  220. }
  221. else
  222. {
  223. $file_data = $this->_get_mod_time($filepath);
  224. $this->_add_data($filepath, $data, $file_data['file_mtime'], $file_data['file_mdate']);
  225. }
  226. }
  227. // --------------------------------------------------------------------
  228. /**
  229. * Add Data to Zip
  230. *
  231. * @param string $filepath the file name/path
  232. * @param string $data the data to be encoded
  233. * @param int $file_mtime
  234. * @param int $file_mdate
  235. * @return void
  236. */
  237. protected function _add_data($filepath, $data, $file_mtime, $file_mdate)
  238. {
  239. $filepath = str_replace('\\', '/', $filepath);
  240. $uncompressed_size = self::strlen($data);
  241. $crc32 = crc32($data);
  242. $gzdata = self::substr(gzcompress($data, $this->compression_level), 2, -4);
  243. $compressed_size = self::strlen($gzdata);
  244. $this->zipdata .=
  245. "\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00"
  246. .pack('v', $file_mtime)
  247. .pack('v', $file_mdate)
  248. .pack('V', $crc32)
  249. .pack('V', $compressed_size)
  250. .pack('V', $uncompressed_size)
  251. .pack('v', self::strlen($filepath)) // length of filename
  252. .pack('v', 0) // extra field length
  253. .$filepath
  254. .$gzdata; // "file data" segment
  255. $this->directory .=
  256. "\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00"
  257. .pack('v', $file_mtime)
  258. .pack('v', $file_mdate)
  259. .pack('V', $crc32)
  260. .pack('V', $compressed_size)
  261. .pack('V', $uncompressed_size)
  262. .pack('v', self::strlen($filepath)) // length of filename
  263. .pack('v', 0) // extra field length
  264. .pack('v', 0) // file comment length
  265. .pack('v', 0) // disk number start
  266. .pack('v', 0) // internal file attributes
  267. .pack('V', 32) // external file attributes - 'archive' bit set
  268. .pack('V', $this->offset) // relative offset of local header
  269. .$filepath;
  270. $this->offset = self::strlen($this->zipdata);
  271. $this->entries++;
  272. $this->file_num++;
  273. }
  274. // --------------------------------------------------------------------
  275. /**
  276. * Read the contents of a file and add it to the zip
  277. *
  278. * @param string $path
  279. * @param bool $archive_filepath
  280. * @return bool
  281. */
  282. public function read_file($path, $archive_filepath = FALSE)
  283. {
  284. if (file_exists($path) && FALSE !== ($data = file_get_contents($path)))
  285. {
  286. if (is_string($archive_filepath))
  287. {
  288. $name = str_replace('\\', '/', $archive_filepath);
  289. }
  290. else
  291. {
  292. $name = str_replace('\\', '/', $path);
  293. if ($archive_filepath === FALSE)
  294. {
  295. $name = preg_replace('|.*/(.+)|', '\\1', $name);
  296. }
  297. }
  298. $this->add_data($name, $data);
  299. return TRUE;
  300. }
  301. return FALSE;
  302. }
  303. // ------------------------------------------------------------------------
  304. /**
  305. * Read a directory and add it to the zip.
  306. *
  307. * This function recursively reads a folder and everything it contains (including
  308. * sub-folders) and creates a zip based on it. Whatever directory structure
  309. * is in the original file path will be recreated in the zip file.
  310. *
  311. * @param string $path path to source directory
  312. * @param bool $preserve_filepath
  313. * @param string $root_path
  314. * @return bool
  315. */
  316. public function read_dir($path, $preserve_filepath = TRUE, $root_path = NULL)
  317. {
  318. $path = rtrim($path, '/\\').DIRECTORY_SEPARATOR;
  319. if ( ! $fp = @opendir($path))
  320. {
  321. return FALSE;
  322. }
  323. // Set the original directory root for child dir's to use as relative
  324. if ($root_path === NULL)
  325. {
  326. $root_path = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, dirname($path)).DIRECTORY_SEPARATOR;
  327. }
  328. while (FALSE !== ($file = readdir($fp)))
  329. {
  330. if ($file[0] === '.')
  331. {
  332. continue;
  333. }
  334. if (is_dir($path.$file))
  335. {
  336. $this->read_dir($path.$file.DIRECTORY_SEPARATOR, $preserve_filepath, $root_path);
  337. }
  338. elseif (FALSE !== ($data = file_get_contents($path.$file)))
  339. {
  340. $name = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $path);
  341. if ($preserve_filepath === FALSE)
  342. {
  343. $name = str_replace($root_path, '', $name);
  344. }
  345. $this->add_data($name.$file, $data);
  346. }
  347. }
  348. closedir($fp);
  349. return TRUE;
  350. }
  351. // --------------------------------------------------------------------
  352. /**
  353. * Get the Zip file
  354. *
  355. * @return string (binary encoded)
  356. */
  357. public function get_zip()
  358. {
  359. // Is there any data to return?
  360. if ($this->entries === 0)
  361. {
  362. return FALSE;
  363. }
  364. return $this->zipdata
  365. .$this->directory."\x50\x4b\x05\x06\x00\x00\x00\x00"
  366. .pack('v', $this->entries) // total # of entries "on this disk"
  367. .pack('v', $this->entries) // total # of entries overall
  368. .pack('V', self::strlen($this->directory)) // size of central dir
  369. .pack('V', self::strlen($this->zipdata)) // offset to start of central dir
  370. ."\x00\x00"; // .zip file comment length
  371. }
  372. // --------------------------------------------------------------------
  373. /**
  374. * Write File to the specified directory
  375. *
  376. * Lets you write a file
  377. *
  378. * @param string $filepath the file name
  379. * @return bool
  380. */
  381. public function archive($filepath)
  382. {
  383. if ( ! ($fp = @fopen($filepath, 'w+b')))
  384. {
  385. return FALSE;
  386. }
  387. flock($fp, LOCK_EX);
  388. for ($result = $written = 0, $data = $this->get_zip(), $length = self::strlen($data); $written < $length; $written += $result)
  389. {
  390. if (($result = fwrite($fp, self::substr($data, $written))) === FALSE)
  391. {
  392. break;
  393. }
  394. }
  395. flock($fp, LOCK_UN);
  396. fclose($fp);
  397. return is_int($result);
  398. }
  399. // --------------------------------------------------------------------
  400. /**
  401. * Download
  402. *
  403. * @param string $filename the file name
  404. * @return void
  405. */
  406. public function download($filename = 'backup.zip')
  407. {
  408. if ( ! preg_match('|.+?\.zip$|', $filename))
  409. {
  410. $filename .= '.zip';
  411. }
  412. get_instance()->load->helper('download');
  413. $get_zip = $this->get_zip();
  414. $zip_content =& $get_zip;
  415. force_download($filename, $zip_content);
  416. }
  417. // --------------------------------------------------------------------
  418. /**
  419. * Initialize Data
  420. *
  421. * Lets you clear current zip data. Useful if you need to create
  422. * multiple zips with different data.
  423. *
  424. * @return CI_Zip
  425. */
  426. public function clear_data()
  427. {
  428. $this->zipdata = '';
  429. $this->directory = '';
  430. $this->entries = 0;
  431. $this->file_num = 0;
  432. $this->offset = 0;
  433. return $this;
  434. }
  435. // --------------------------------------------------------------------
  436. /**
  437. * Byte-safe strlen()
  438. *
  439. * @param string $str
  440. * @return int
  441. */
  442. protected static function strlen($str)
  443. {
  444. return (self::$func_overload)
  445. ? mb_strlen($str, '8bit')
  446. : strlen($str);
  447. }
  448. // --------------------------------------------------------------------
  449. /**
  450. * Byte-safe substr()
  451. *
  452. * @param string $str
  453. * @param int $start
  454. * @param int $length
  455. * @return string
  456. */
  457. protected static function substr($str, $start, $length = NULL)
  458. {
  459. if (self::$func_overload)
  460. {
  461. // mb_substr($str, $start, null, '8bit') returns an empty
  462. // string on PHP 5.3
  463. isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start);
  464. return mb_substr($str, $start, $length, '8bit');
  465. }
  466. return isset($length)
  467. ? substr($str, $start, $length)
  468. : substr($str, $start);
  469. }
  470. }