ZipExtension.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <?php
  2. /**
  3. * Interface for the zip extension
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use ZipArchive;
  8. use function array_combine;
  9. use function count;
  10. use function crc32;
  11. use function getdate;
  12. use function gzcompress;
  13. use function implode;
  14. use function is_array;
  15. use function is_string;
  16. use function pack;
  17. use function preg_match;
  18. use function sprintf;
  19. use function str_replace;
  20. use function strcmp;
  21. use function strlen;
  22. use function strpos;
  23. use function substr;
  24. /**
  25. * Transformations class
  26. */
  27. class ZipExtension
  28. {
  29. /** @var ZipArchive|null */
  30. private $zip;
  31. public function __construct(?ZipArchive $zip = null)
  32. {
  33. $this->zip = $zip;
  34. }
  35. /**
  36. * Gets zip file contents
  37. *
  38. * @param string $file path to zip file
  39. * @param string $specific_entry regular expression to match a file
  40. *
  41. * @return array ($error_message, $file_data); $error_message
  42. * is empty if no error
  43. */
  44. public function getContents($file, $specific_entry = null)
  45. {
  46. /**
  47. * This function is used to "import" a SQL file which has been exported earlier
  48. * That means that this function works on the assumption that the zip file contains only a single SQL file
  49. * It might also be an ODS file, look below
  50. */
  51. if ($this->zip === null) {
  52. return [
  53. 'error' => sprintf(__('The %s extension is missing. Please check your PHP configuration.'), 'zip'),
  54. 'data' => '',
  55. ];
  56. }
  57. $error_message = '';
  58. $file_data = '';
  59. $res = $this->zip->open($file);
  60. if ($res !== true) {
  61. $error_message = __('Error in ZIP archive:') . ' ' . $this->zip->getStatusString();
  62. $this->zip->close();
  63. return [
  64. 'error' => $error_message,
  65. 'data' => $file_data,
  66. ];
  67. }
  68. if ($this->zip->numFiles === 0) {
  69. $error_message = __('No files found inside ZIP archive!');
  70. $this->zip->close();
  71. return [
  72. 'error' => $error_message,
  73. 'data' => $file_data,
  74. ];
  75. }
  76. /* Is the the zip really an ODS file? */
  77. $ods_mime = 'application/vnd.oasis.opendocument.spreadsheet';
  78. $first_zip_entry = $this->zip->getFromIndex(0);
  79. if (! strcmp($ods_mime, (string) $first_zip_entry)) {
  80. $specific_entry = '/^content\.xml$/';
  81. }
  82. if (! isset($specific_entry)) {
  83. $file_data = $first_zip_entry;
  84. $this->zip->close();
  85. return [
  86. 'error' => $error_message,
  87. 'data' => $file_data,
  88. ];
  89. }
  90. /* Return the correct contents, not just the first entry */
  91. for ($i = 0; $i < $this->zip->numFiles; $i++) {
  92. if (preg_match($specific_entry, (string) $this->zip->getNameIndex($i))) {
  93. $file_data = $this->zip->getFromIndex($i);
  94. break;
  95. }
  96. }
  97. /* Couldn't find any files that matched $specific_entry */
  98. if (empty($file_data)) {
  99. $error_message = __('Error in ZIP archive:')
  100. . ' Could not find "' . $specific_entry . '"';
  101. }
  102. $this->zip->close();
  103. return [
  104. 'error' => $error_message,
  105. 'data' => $file_data,
  106. ];
  107. }
  108. /**
  109. * Returns the filename of the first file that matches the given $file_regexp.
  110. *
  111. * @param string $file path to zip file
  112. * @param string $regex regular expression for the file name to match
  113. *
  114. * @return string|false the file name of the first file that matches the given regular expression
  115. */
  116. public function findFile($file, $regex)
  117. {
  118. if ($this->zip === null) {
  119. return false;
  120. }
  121. $res = $this->zip->open($file);
  122. if ($res === true) {
  123. for ($i = 0; $i < $this->zip->numFiles; $i++) {
  124. if (preg_match($regex, (string) $this->zip->getNameIndex($i))) {
  125. $filename = $this->zip->getNameIndex($i);
  126. $this->zip->close();
  127. return $filename;
  128. }
  129. }
  130. }
  131. return false;
  132. }
  133. /**
  134. * Returns the number of files in the zip archive.
  135. *
  136. * @param string $file path to zip file
  137. *
  138. * @return int the number of files in the zip archive or 0, either if there weren't any files or an error occurred.
  139. */
  140. public function getNumberOfFiles($file)
  141. {
  142. if ($this->zip === null) {
  143. return 0;
  144. }
  145. $num = 0;
  146. $res = $this->zip->open($file);
  147. if ($res === true) {
  148. $num = $this->zip->numFiles;
  149. }
  150. return $num;
  151. }
  152. /**
  153. * Extracts the content of $entry.
  154. *
  155. * @param string $file path to zip file
  156. * @param string $entry file in the archive that should be extracted
  157. *
  158. * @return string|bool data on success, false otherwise
  159. */
  160. public function extract($file, $entry)
  161. {
  162. if ($this->zip === null) {
  163. return false;
  164. }
  165. if ($this->zip->open($file) === true) {
  166. $result = $this->zip->getFromName($entry);
  167. $this->zip->close();
  168. return $result;
  169. }
  170. return false;
  171. }
  172. /**
  173. * Creates a zip file.
  174. * If $data is an array and $name is a string, the filenames will be indexed.
  175. * The function will return false if $data is a string but $name is an array
  176. * or if $data is an array and $name is an array, but they don't have the
  177. * same amount of elements.
  178. *
  179. * @param array|string $data contents of the file/files
  180. * @param array|string $name name of the file/files in the archive
  181. * @param int $time the current timestamp
  182. *
  183. * @return string|bool the ZIP file contents, or false if there was an error.
  184. */
  185. public function createFile($data, $name, $time = 0)
  186. {
  187. $datasec = []; // Array to store compressed data
  188. $ctrl_dir = []; // Central directory
  189. $old_offset = 0; // Last offset position
  190. $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00"; // End of central directory record
  191. if (is_string($data) && is_string($name)) {
  192. $data = [$name => $data];
  193. } elseif (is_array($data) && is_string($name)) {
  194. $ext_pos = (int) strpos($name, '.');
  195. $extension = substr($name, $ext_pos);
  196. $newData = [];
  197. foreach ($data as $key => $value) {
  198. $newName = str_replace(
  199. $extension,
  200. '_' . $key . $extension,
  201. $name
  202. );
  203. $newData[$newName] = $value;
  204. }
  205. $data = $newData;
  206. } elseif (is_array($data) && is_array($name) && count($data) === count($name)) {
  207. $data = array_combine($name, $data);
  208. } else {
  209. return false;
  210. }
  211. foreach ($data as $table => $dump) {
  212. $temp_name = str_replace('\\', '/', $table);
  213. /* Get Local Time */
  214. $timearray = getdate();
  215. if ($timearray['year'] < 1980) {
  216. $timearray['year'] = 1980;
  217. $timearray['mon'] = 1;
  218. $timearray['mday'] = 1;
  219. $timearray['hours'] = 0;
  220. $timearray['minutes'] = 0;
  221. $timearray['seconds'] = 0;
  222. }
  223. $time = $timearray['year'] - 1980 << 25
  224. | ($timearray['mon'] << 21)
  225. | ($timearray['mday'] << 16)
  226. | ($timearray['hours'] << 11)
  227. | ($timearray['minutes'] << 5)
  228. | ($timearray['seconds'] >> 1);
  229. $hexdtime = pack('V', $time);
  230. $unc_len = strlen($dump);
  231. $crc = crc32($dump);
  232. $zdata = (string) gzcompress($dump);
  233. $zdata = substr((string) substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug
  234. $c_len = strlen($zdata);
  235. $fr = "\x50\x4b\x03\x04"
  236. . "\x14\x00" // ver needed to extract
  237. . "\x00\x00" // gen purpose bit flag
  238. . "\x08\x00" // compression method
  239. . $hexdtime // last mod time and date
  240. // "local file header" segment
  241. . pack('V', $crc) // crc32
  242. . pack('V', $c_len) // compressed filesize
  243. . pack('V', $unc_len) // uncompressed filesize
  244. . pack('v', strlen($temp_name)) // length of filename
  245. . pack('v', 0) // extra field length
  246. . $temp_name
  247. // "file data" segment
  248. . $zdata;
  249. $datasec[] = $fr;
  250. // now add to central directory record
  251. $cdrec = "\x50\x4b\x01\x02"
  252. . "\x00\x00" // version made by
  253. . "\x14\x00" // version needed to extract
  254. . "\x00\x00" // gen purpose bit flag
  255. . "\x08\x00" // compression method
  256. . $hexdtime // last mod time & date
  257. . pack('V', $crc) // crc32
  258. . pack('V', $c_len) // compressed filesize
  259. . pack('V', $unc_len) // uncompressed filesize
  260. . pack('v', strlen($temp_name)) // length of filename
  261. . pack('v', 0) // extra field length
  262. . pack('v', 0) // file comment length
  263. . pack('v', 0) // disk number start
  264. . pack('v', 0) // internal file attributes
  265. . pack('V', 32) // external file attributes
  266. // - 'archive' bit set
  267. . pack('V', $old_offset) // relative offset of local header
  268. . $temp_name; // filename
  269. $old_offset += strlen($fr);
  270. // optional extra field, file comment goes here
  271. // save to central directory
  272. $ctrl_dir[] = $cdrec;
  273. }
  274. /* Build string to return */
  275. $temp_ctrldir = implode('', $ctrl_dir);
  276. $header = $temp_ctrldir .
  277. $eof_ctrl_dir .
  278. pack('v', count($ctrl_dir)) . //total #of entries "on this disk"
  279. pack('v', count($ctrl_dir)) . //total #of entries overall
  280. pack('V', strlen($temp_ctrldir)) . //size of central dir
  281. pack('V', $old_offset) . //offset to start of central dir
  282. "\x00\x00"; //.zip file comment length
  283. $data = implode('', $datasec);
  284. return $data . $header;
  285. }
  286. }