Joomla Platform  13.1
Documentation des API du framework Joomla Platform
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
zip.php
Aller à la documentation de ce fichier.
1 <?php
2 /**
3  * @package Joomla.Platform
4  * @subpackage Archive
5  *
6  * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
7  * @license GNU General Public License version 2 or later; see LICENSE
8  */
9 
10 defined('JPATH_PLATFORM') or die;
11 
12 jimport('joomla.filesystem.file');
13 jimport('joomla.filesystem.folder');
14 
15 /**
16  * ZIP format adapter for the JArchive class
17  *
18  * The ZIP compression code is partially based on code from:
19  * Eric Mueller <eric@themepark.com>
20  * http://www.zend.com/codex.php?id=535&single=1
21  *
22  * Deins125 <webmaster@atlant.ru>
23  * http://www.zend.com/codex.php?id=470&single=1
24  *
25  * The ZIP compression date code is partially based on code from
26  * Peter Listiak <mlady@users.sourceforge.net>
27  *
28  * This class is inspired from and draws heavily in code and concept from the Compress package of
29  * The Horde Project <http://www.horde.org>
30  *
31  * @contributor Chuck Hagenbuch <chuck@horde.org>
32  * @contributor Michael Slusarz <slusarz@horde.org>
33  * @contributor Michael Cochrane <mike@graftonhall.co.nz>
34  *
35  * @package Joomla.Platform
36  * @subpackage Archive
37  * @since 11.1
38  */
40 {
41  /**
42  * ZIP compression methods.
43  *
44  * @var array
45  * @since 11.1
46  */
47  private $_methods = array(0x0 => 'None', 0x1 => 'Shrunk', 0x2 => 'Super Fast', 0x3 => 'Fast', 0x4 => 'Normal', 0x5 => 'Maximum', 0x6 => 'Imploded',
48  0x8 => 'Deflated');
49 
50  /**
51  * Beginning of central directory record.
52  *
53  * @var string
54  * @since 11.1
55  */
56  private $_ctrlDirHeader = "\x50\x4b\x01\x02";
57 
58  /**
59  * End of central directory record.
60  *
61  * @var string
62  * @since 11.1
63  */
64  private $_ctrlDirEnd = "\x50\x4b\x05\x06\x00\x00\x00\x00";
65 
66  /**
67  * Beginning of file contents.
68  *
69  * @var string
70  * @since 11.1
71  */
72  private $_fileHeader = "\x50\x4b\x03\x04";
73 
74  /**
75  * ZIP file data buffer
76  *
77  * @var string
78  * @since 11.1
79  */
80  private $_data = null;
81 
82  /**
83  * ZIP file metadata array
84  *
85  * @var array
86  * @since 11.1
87  */
88  private $_metadata = null;
89 
90  /**
91  * Create a ZIP compressed file from an array of file data.
92  *
93  * @param string $archive Path to save archive.
94  * @param array $files Array of files to add to archive.
95  *
96  * @return boolean True if successful.
97  *
98  * @since 11.1
99  *
100  * @todo Finish Implementation
101  */
102  public function create($archive, $files)
103  {
104  $contents = array();
105  $ctrldir = array();
106 
107  foreach ($files as $file)
108  {
109  $this->_addToZIPFile($file, $contents, $ctrldir);
110  }
111 
112  return $this->_createZIPFile($contents, $ctrldir, $archive);
113  }
114 
115  /**
116  * Extract a ZIP compressed file to a given path
117  *
118  * @param string $archive Path to ZIP archive to extract
119  * @param string $destination Path to extract archive into
120  * @param array $options Extraction options [unused]
121  *
122  * @return boolean True if successful
123  *
124  * @since 11.1
125  * @throws RuntimeException
126  */
127  public function extract($archive, $destination, array $options = array())
128  {
129  if (!is_file($archive))
130  {
131  if (class_exists('JError'))
132  {
133  return JError::raiseWarning(100, 'Archive does not exist');
134  }
135  else
136  {
137  throw new RuntimeException('Archive does not exist');
138  }
139  }
140 
141  if ($this->hasNativeSupport())
142  {
143  return $this->extractNative($archive, $destination);
144  }
145  else
146  {
147  return $this->extractCustom($archive, $destination);
148  }
149  }
150 
151  /**
152  * Tests whether this adapter can unpack files on this computer.
153  *
154  * @return boolean True if supported
155  *
156  * @since 11.3
157  */
158  public static function isSupported()
159  {
160  return (self::hasNativeSupport() || extension_loaded('zlib'));
161  }
162 
163  /**
164  * Method to determine if the server has native zip support for faster handling
165  *
166  * @return boolean True if php has native ZIP support
167  *
168  * @since 11.1
169  */
170  public static function hasNativeSupport()
171  {
172  return (function_exists('zip_open') && function_exists('zip_read'));
173  }
174 
175  /**
176  * Checks to see if the data is a valid ZIP file.
177  *
178  * @param string &$data ZIP archive data buffer.
179  *
180  * @return boolean True if valid, false if invalid.
181  *
182  * @since 11.1
183  */
184  public function checkZipData(&$data)
185  {
186  if (strpos($data, $this->_fileHeader) === false)
187  {
188  return false;
189  }
190  else
191  {
192  return true;
193  }
194  }
195 
196  /**
197  * Extract a ZIP compressed file to a given path using a php based algorithm that only requires zlib support
198  *
199  * @param string $archive Path to ZIP archive to extract.
200  * @param string $destination Path to extract archive into.
201  *
202  * @return mixed True if successful
203  *
204  * @since 11.1
205  * @throws RuntimeException
206  */
207  protected function extractCustom($archive, $destination)
208  {
209  $this->_data = null;
210  $this->_metadata = null;
211 
212  if (!extension_loaded('zlib'))
213  {
214  if (class_exists('JError'))
215  {
216  return JError::raiseWarning(100, 'Zlib not supported');
217  }
218  else
219  {
220  throw new RuntimeException('Zlib not supported');
221  }
222  }
223 
224  $this->_data = file_get_contents($archive);
225 
226  if (!$this->_data)
227  {
228  if (class_exists('JError'))
229  {
230  return JError::raiseWarning(100, 'Unable to read archive (zip)');
231  }
232  else
233  {
234  throw new RuntimeException('Unable to read archive (zip)');
235  }
236  }
237 
238  if (!$this->_readZipInfo($this->_data))
239  {
240  if (class_exists('JError'))
241  {
242  return JError::raiseWarning(100, 'Get ZIP Information failed');
243  }
244  else
245  {
246  throw new RuntimeException('Get ZIP Information failed');
247  }
248  }
249 
250  for ($i = 0, $n = count($this->_metadata); $i < $n; $i++)
251  {
252  $lastPathCharacter = substr($this->_metadata[$i]['name'], -1, 1);
253 
254  if ($lastPathCharacter !== '/' && $lastPathCharacter !== '\\')
255  {
256  $buffer = $this->_getFileData($i);
257  $path = JPath::clean($destination . '/' . $this->_metadata[$i]['name']);
258 
259  // Make sure the destination folder exists
260  if (!JFolder::create(dirname($path)))
261  {
262  if (class_exists('JError'))
263  {
264  return JError::raiseWarning(100, 'Unable to create destination');
265  }
266  else
267  {
268  throw new RuntimeException('Unable to create destination');
269  }
270  }
271 
272  if (JFile::write($path, $buffer) === false)
273  {
274  if (class_exists('JError'))
275  {
276  return JError::raiseWarning(100, 'Unable to write entry');
277  }
278  else
279  {
280  throw new RuntimeException('Unable to write entry');
281  }
282  }
283  }
284  }
285 
286  return true;
287  }
288 
289  /**
290  * Extract a ZIP compressed file to a given path using native php api calls for speed
291  *
292  * @param string $archive Path to ZIP archive to extract
293  * @param string $destination Path to extract archive into
294  *
295  * @return boolean True on success
296  *
297  * @since 11.1
298  * @throws RuntimeException
299  */
300  protected function extractNative($archive, $destination)
301  {
302  $zip = zip_open($archive);
303 
304  if (is_resource($zip))
305  {
306  // Make sure the destination folder exists
307  if (!JFolder::create($destination))
308  {
309  if (class_exists('JError'))
310  {
311  return JError::raiseWarning(100, 'Unable to create destination');
312  }
313  else
314  {
315  throw new RuntimeException('Unable to create destination');
316  }
317  }
318 
319  // Read files in the archive
320  while ($file = @zip_read($zip))
321  {
322  if (zip_entry_open($zip, $file, "r"))
323  {
324  if (substr(zip_entry_name($file), strlen(zip_entry_name($file)) - 1) != "/")
325  {
326  $buffer = zip_entry_read($file, zip_entry_filesize($file));
327 
328  if (JFile::write($destination . '/' . zip_entry_name($file), $buffer) === false)
329  {
330  if (class_exists('JError'))
331  {
332  return JError::raiseWarning(100, 'Unable to write entry');
333  }
334  else
335  {
336  throw new RuntimeException('Unable to write entry');
337  }
338  }
339 
340  zip_entry_close($file);
341  }
342  }
343  else
344  {
345  if (class_exists('JError'))
346  {
347  return JError::raiseWarning(100, 'Unable to read entry');
348  }
349  else
350  {
351  throw new RuntimeException('Unable to read entry');
352  }
353  }
354  }
355 
356  @zip_close($zip);
357  }
358  else
359  {
360  if (class_exists('JError'))
361  {
362  return JError::raiseWarning(100, 'Unable to open archive');
363  }
364  else
365  {
366  throw new RuntimeException('Unable to open archive');
367  }
368  }
369 
370  return true;
371  }
372 
373  /**
374  * Get the list of files/data from a ZIP archive buffer.
375  *
376  * <pre>
377  * KEY: Position in zipfile
378  * VALUES: 'attr' -- File attributes
379  * 'crc' -- CRC checksum
380  * 'csize' -- Compressed file size
381  * 'date' -- File modification time
382  * 'name' -- Filename
383  * 'method'-- Compression method
384  * 'size' -- Original file size
385  * 'type' -- File type
386  * </pre>
387  *
388  * @param string &$data The ZIP archive buffer.
389  *
390  * @return boolean True on success
391  *
392  * @since 11.1
393  * @throws RuntimeException
394  */
395  private function _readZipInfo(&$data)
396  {
397  $entries = array();
398 
399  // Find the last central directory header entry
400  $fhLast = strpos($data, $this->_ctrlDirEnd);
401 
402  do
403  {
404  $last = $fhLast;
405  }
406  while (($fhLast = strpos($data, $this->_ctrlDirEnd, $fhLast + 1)) !== false);
407 
408  // Find the central directory offset
409  $offset = 0;
410 
411  if ($last)
412  {
413  $endOfCentralDirectory = unpack(
414  'vNumberOfDisk/vNoOfDiskWithStartOfCentralDirectory/vNoOfCentralDirectoryEntriesOnDisk/' .
415  'vTotalCentralDirectoryEntries/VSizeOfCentralDirectory/VCentralDirectoryOffset/vCommentLength',
416  substr($data, $last + 4)
417  );
418  $offset = $endOfCentralDirectory['CentralDirectoryOffset'];
419  }
420 
421  // Get details from central directory structure.
422  $fhStart = strpos($data, $this->_ctrlDirHeader, $offset);
423  $dataLength = strlen($data);
424 
425  do
426  {
427  if ($dataLength < $fhStart + 31)
428  {
429  if (class_exists('JError'))
430  {
431  return JError::raiseWarning(100, 'Invalid Zip Data');
432  }
433  else
434  {
435  throw new RuntimeException('Invalid Zip Data');
436  }
437  }
438 
439  $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength', substr($data, $fhStart + 10, 20));
440  $name = substr($data, $fhStart + 46, $info['Length']);
441 
442  $entries[$name] = array(
443  'attr' => null,
444  'crc' => sprintf("%08s", dechex($info['CRC32'])),
445  'csize' => $info['Compressed'],
446  'date' => null,
447  '_dataStart' => null,
448  'name' => $name,
449  'method' => $this->_methods[$info['Method']],
450  '_method' => $info['Method'],
451  'size' => $info['Uncompressed'],
452  'type' => null
453  );
454 
455  $entries[$name]['date'] = mktime(
456  (($info['Time'] >> 11) & 0x1f),
457  (($info['Time'] >> 5) & 0x3f),
458  (($info['Time'] << 1) & 0x3e),
459  (($info['Time'] >> 21) & 0x07),
460  (($info['Time'] >> 16) & 0x1f),
461  ((($info['Time'] >> 25) & 0x7f) + 1980)
462  );
463 
464  if ($dataLength < $fhStart + 43)
465  {
466  if (class_exists('JError'))
467  {
468  return JError::raiseWarning(100, 'Invalid ZIP data');
469  }
470  else
471  {
472  throw new RuntimeException('Invalid ZIP data');
473  }
474  }
475 
476  $info = unpack('vInternal/VExternal/VOffset', substr($data, $fhStart + 36, 10));
477 
478  $entries[$name]['type'] = ($info['Internal'] & 0x01) ? 'text' : 'binary';
479  $entries[$name]['attr'] = (($info['External'] & 0x10) ? 'D' : '-') . (($info['External'] & 0x20) ? 'A' : '-')
480  . (($info['External'] & 0x03) ? 'S' : '-') . (($info['External'] & 0x02) ? 'H' : '-') . (($info['External'] & 0x01) ? 'R' : '-');
481  $entries[$name]['offset'] = $info['Offset'];
482 
483  // Get details from local file header since we have the offset
484  $lfhStart = strpos($data, $this->_fileHeader, $entries[$name]['offset']);
485 
486  if ($dataLength < $lfhStart + 34)
487  {
488  if (class_exists('JError'))
489  {
490  return JError::raiseWarning(100, 'Invalid Zip Data');
491  }
492  else
493  {
494  throw new RuntimeException('Invalid Zip Data');
495  }
496  }
497 
498  $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength/vExtraLength', substr($data, $lfhStart + 8, 25));
499  $name = substr($data, $lfhStart + 30, $info['Length']);
500  $entries[$name]['_dataStart'] = $lfhStart + 30 + $info['Length'] + $info['ExtraLength'];
501 
502  // Bump the max execution time because not using the built in php zip libs makes this process slow.
503  @set_time_limit(ini_get('max_execution_time'));
504  }
505  while ((($fhStart = strpos($data, $this->_ctrlDirHeader, $fhStart + 46)) !== false));
506 
507  $this->_metadata = array_values($entries);
508 
509  return true;
510  }
511 
512  /**
513  * Returns the file data for a file by offsest in the ZIP archive
514  *
515  * @param integer $key The position of the file in the archive.
516  *
517  * @return string Uncompressed file data buffer.
518  *
519  * @since 11.1
520  */
521  private function _getFileData($key)
522  {
523  if ($this->_metadata[$key]['_method'] == 0x8)
524  {
525  return gzinflate(substr($this->_data, $this->_metadata[$key]['_dataStart'], $this->_metadata[$key]['csize']));
526  }
527  elseif ($this->_metadata[$key]['_method'] == 0x0)
528  {
529  /* Files that aren't compressed. */
530  return substr($this->_data, $this->_metadata[$key]['_dataStart'], $this->_metadata[$key]['csize']);
531  }
532  elseif ($this->_metadata[$key]['_method'] == 0x12)
533  {
534  // If bz2 extension is loaded use it
535  if (extension_loaded('bz2'))
536  {
537  return bzdecompress(substr($this->_data, $this->_metadata[$key]['_dataStart'], $this->_metadata[$key]['csize']));
538  }
539  }
540 
541  return '';
542  }
543 
544  /**
545  * Converts a UNIX timestamp to a 4-byte DOS date and time format
546  * (date in high 2-bytes, time in low 2-bytes allowing magnitude
547  * comparison).
548  *
549  * @param int $unixtime The current UNIX timestamp.
550  *
551  * @return int The current date in a 4-byte DOS format.
552  *
553  * @since 11.1
554  */
555  protected function _unix2DOSTime($unixtime = null)
556  {
557  $timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
558 
559  if ($timearray['year'] < 1980)
560  {
561  $timearray['year'] = 1980;
562  $timearray['mon'] = 1;
563  $timearray['mday'] = 1;
564  $timearray['hours'] = 0;
565  $timearray['minutes'] = 0;
566  $timearray['seconds'] = 0;
567  }
568 
569  return (($timearray['year'] - 1980) << 25) | ($timearray['mon'] << 21) | ($timearray['mday'] << 16) | ($timearray['hours'] << 11) |
570  ($timearray['minutes'] << 5) | ($timearray['seconds'] >> 1);
571  }
572 
573  /**
574  * Adds a "file" to the ZIP archive.
575  *
576  * @param array &$file File data array to add
577  * @param array &$contents An array of existing zipped files.
578  * @param array &$ctrldir An array of central directory information.
579  *
580  * @return void
581  *
582  * @since 11.1
583  *
584  * @todo Review and finish implementation
585  */
586  private function _addToZIPFile(array &$file, array &$contents, array &$ctrldir)
587  {
588  $data = &$file['data'];
589  $name = str_replace('\\', '/', $file['name']);
590 
591  /* See if time/date information has been provided. */
592  $ftime = null;
593 
594  if (isset($file['time']))
595  {
596  $ftime = $file['time'];
597  }
598 
599  // Get the hex time.
600  $dtime = dechex($this->_unix2DosTime($ftime));
601  $hexdtime = chr(hexdec($dtime[6] . $dtime[7])) . chr(hexdec($dtime[4] . $dtime[5])) . chr(hexdec($dtime[2] . $dtime[3]))
602  . chr(hexdec($dtime[0] . $dtime[1]));
603 
604  /* Begin creating the ZIP data. */
605  $fr = $this->_fileHeader;
606  /* Version needed to extract. */
607  $fr .= "\x14\x00";
608  /* General purpose bit flag. */
609  $fr .= "\x00\x00";
610  /* Compression method. */
611  $fr .= "\x08\x00";
612  /* Last modification time/date. */
613  $fr .= $hexdtime;
614 
615  /* "Local file header" segment. */
616  $unc_len = strlen($data);
617  $crc = crc32($data);
618  $zdata = gzcompress($data);
619  $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2);
620  $c_len = strlen($zdata);
621 
622  /* CRC 32 information. */
623  $fr .= pack('V', $crc);
624  /* Compressed filesize. */
625  $fr .= pack('V', $c_len);
626  /* Uncompressed filesize. */
627  $fr .= pack('V', $unc_len);
628  /* Length of filename. */
629  $fr .= pack('v', strlen($name));
630  /* Extra field length. */
631  $fr .= pack('v', 0);
632  /* File name. */
633  $fr .= $name;
634 
635  /* "File data" segment. */
636  $fr .= $zdata;
637 
638  /* Add this entry to array. */
639  $old_offset = strlen(implode('', $contents));
640  $contents[] = &$fr;
641 
642  /* Add to central directory record. */
643  $cdrec = $this->_ctrlDirHeader;
644  /* Version made by. */
645  $cdrec .= "\x00\x00";
646  /* Version needed to extract */
647  $cdrec .= "\x14\x00";
648  /* General purpose bit flag */
649  $cdrec .= "\x00\x00";
650  /* Compression method */
651  $cdrec .= "\x08\x00";
652  /* Last mod time/date. */
653  $cdrec .= $hexdtime;
654  /* CRC 32 information. */
655  $cdrec .= pack('V', $crc);
656  /* Compressed filesize. */
657  $cdrec .= pack('V', $c_len);
658  /* Uncompressed filesize. */
659  $cdrec .= pack('V', $unc_len);
660  /* Length of filename. */
661  $cdrec .= pack('v', strlen($name));
662  /* Extra field length. */
663  $cdrec .= pack('v', 0);
664  /* File comment length. */
665  $cdrec .= pack('v', 0);
666  /* Disk number start. */
667  $cdrec .= pack('v', 0);
668  /* Internal file attributes. */
669  $cdrec .= pack('v', 0);
670  /* External file attributes -'archive' bit set. */
671  $cdrec .= pack('V', 32);
672  /* Relative offset of local header. */
673  $cdrec .= pack('V', $old_offset);
674  /* File name. */
675  $cdrec .= $name;
676  /* Optional extra field, file comment goes here. */
677 
678  /* Save to central directory array. */
679  $ctrldir[] = &$cdrec;
680  }
681 
682  /**
683  * Creates the ZIP file.
684  *
685  * Official ZIP file format: http://www.pkware.com/appnote.txt
686  *
687  * @param array &$contents An array of existing zipped files.
688  * @param array &$ctrlDir An array of central directory information.
689  * @param string $path The path to store the archive.
690  *
691  * @return boolean True if successful
692  *
693  * @since 11.1
694  *
695  * @todo Review and finish implementation
696  */
697  private function _createZIPFile(array &$contents, array &$ctrlDir, $path)
698  {
699  $data = implode('', $contents);
700  $dir = implode('', $ctrlDir);
701 
702  $buffer = $data . $dir . $this->_ctrlDirEnd . /* Total # of entries "on this disk". */
703  pack('v', count($ctrlDir)) . /* Total # of entries overall. */
704  pack('v', count($ctrlDir)) . /* Size of central directory. */
705  pack('V', strlen($dir)) . /* Offset to start of central dir. */
706  pack('V', strlen($data)) . /* ZIP file comment length. */
707  "\x00\x00";
708 
709  if (JFile::write($path, $buffer) === false)
710  {
711  return false;
712  }
713  else
714  {
715  return true;
716  }
717  }
718 }