3 * Interface for the zip extension
6 declare(strict_types
=1);
13 use function array_combine
;
17 use function gzcompress
;
19 use function is_array
;
20 use function is_string
;
22 use function preg_match
;
24 use function str_replace
;
30 * Transformations class
34 public function __construct(private ZipArchive|
null $zip = null)
39 * Gets zip file contents
41 * @param string $file path to zip file
42 * @param string|null $specificEntry regular expression to match a file
44 * @return array<string, string>
45 * @psalm-return array{error: string, data: string}
47 public function getContents(string $file, string|
null $specificEntry = null): array
50 * This function is used to "import" a SQL file which has been exported earlier
51 * That means that this function works on the assumption that the zip file contains only a single SQL file
52 * It might also be an ODS file, look below
55 if ($this->zip
=== null) {
57 'error' => sprintf(__('The %s extension is missing. Please check your PHP configuration.'), 'zip'),
65 $res = $this->zip
->open($file);
68 $errorMessage = __('Error in ZIP archive:') . ' ' . $this->zip
->getStatusString();
71 return ['error' => $errorMessage, 'data' => $fileData];
74 if ($this->zip
->numFiles
=== 0) {
75 $errorMessage = __('No files found inside ZIP archive!');
78 return ['error' => $errorMessage, 'data' => $fileData];
81 /* Is the the zip really an ODS file? */
82 $odsMediaType = 'application/vnd.oasis.opendocument.spreadsheet';
83 $firstZipEntry = $this->zip
->getFromIndex(0);
84 if ($odsMediaType === (string) $firstZipEntry) {
85 $specificEntry = '/^content\.xml$/';
88 if ($specificEntry === null) {
89 $fileData = $firstZipEntry;
92 return ['error' => $errorMessage, 'data' => $fileData];
95 /* Return the correct contents, not just the first entry */
96 for ($i = 0; $i < $this->zip
->numFiles
; $i++
) {
97 if (preg_match($specificEntry, (string) $this->zip
->getNameIndex($i))) {
98 $fileData = $this->zip
->getFromIndex($i);
103 /* Couldn't find any files that matched $specific_entry */
104 if ($fileData === '' ||
$fileData === false) {
105 $errorMessage = __('Error in ZIP archive:')
106 . ' Could not find "' . $specificEntry . '"';
111 return ['error' => $errorMessage, 'data' => $fileData];
115 * Returns the filename of the first file that matches the given $file_regexp.
117 * @param string $file path to zip file
118 * @param string $regex regular expression for the file name to match
120 * @return string|false the file name of the first file that matches the given regular expression
122 public function findFile(string $file, string $regex): string|
false
124 if ($this->zip
=== null) {
128 $res = $this->zip
->open($file);
131 for ($i = 0; $i < $this->zip
->numFiles
; $i++
) {
132 if (preg_match($regex, (string) $this->zip
->getNameIndex($i))) {
133 $filename = $this->zip
->getNameIndex($i);
145 * Returns the number of files in the zip archive.
147 * @param string $file path to zip file
149 * @return int the number of files in the zip archive or 0, either if there weren't any files or an error occurred.
151 public function getNumberOfFiles(string $file): int
153 if ($this->zip
=== null) {
157 $res = $this->zip
->open($file);
160 return $this->zip
->numFiles
;
167 * Extracts the content of $entry.
169 * @param string $file path to zip file
170 * @param string $entry file in the archive that should be extracted
172 * @return string|false data on success, false otherwise
174 public function extract(string $file, string $entry): string|
false
176 if ($this->zip
=== null) {
180 if ($this->zip
->open($file) === true) {
181 $result = $this->zip
->getFromName($entry);
191 * Creates a zip file.
192 * If $data is an array and $name is a string, the filenames will be indexed.
193 * The function will return false if $data is a string but $name is an array
194 * or if $data is an array and $name is an array, but they don't have the
195 * same amount of elements.
197 * @param mixed[]|string $data contents of the file/files
198 * @param mixed[]|string $name name of the file/files in the archive
200 * @return string|false the ZIP file contents, or false if there was an error.
202 public function createFile(array|
string $data, array|
string $name): string|
false
204 $datasec = []; // Array to store compressed data
205 $ctrlDir = []; // Central directory
206 $oldOffset = 0; // Last offset position
207 $eofCtrlDir = "\x50\x4b\x05\x06\x00\x00\x00\x00"; // End of central directory record
209 if (is_string($data) && is_string($name)) {
210 $data = [$name => $data];
211 } elseif (is_array($data) && is_string($name)) {
212 $extPos = (int) strpos($name, '.');
213 $extension = substr($name, $extPos);
215 foreach ($data as $key => $value) {
216 $newName = str_replace($extension, '_' . $key . $extension, $name);
217 $newData[$newName] = $value;
221 } elseif (is_array($data) && is_array($name) && count($data) === count($name)) {
222 $data = array_combine($name, $data);
227 foreach ($data as $table => $dump) {
228 $tempName = str_replace('\\', '/', $table);
231 $timearray = getdate();
233 if ($timearray['year'] < 1980) {
234 $timearray['year'] = 1980;
235 $timearray['mon'] = 1;
236 $timearray['mday'] = 1;
237 $timearray['hours'] = 0;
238 $timearray['minutes'] = 0;
239 $timearray['seconds'] = 0;
242 $time = $timearray['year'] - 1980 << 25
243 |
($timearray['mon'] << 21)
244 |
($timearray['mday'] << 16)
245 |
($timearray['hours'] << 11)
246 |
($timearray['minutes'] << 5)
247 |
($timearray['seconds'] >> 1);
249 $hexdtime = pack('V', $time);
251 $uncLen = strlen($dump);
253 $zdata = (string) gzcompress($dump);
254 $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug
255 $cLen = strlen($zdata);
256 $fr = "\x50\x4b\x03\x04"
257 . "\x14\x00" // ver needed to extract
258 . "\x00\x00" // gen purpose bit flag
259 . "\x08\x00" // compression method
260 . $hexdtime // last mod time and date
262 // "local file header" segment
263 . pack('V', $crc) // crc32
264 . pack('V', $cLen) // compressed filesize
265 . pack('V', $uncLen) // uncompressed filesize
266 . pack('v', strlen($tempName)) // length of filename
267 . pack('v', 0) // extra field length
270 // "file data" segment
275 // now add to central directory record
276 $cdrec = "\x50\x4b\x01\x02"
277 . "\x00\x00" // version made by
278 . "\x14\x00" // version needed to extract
279 . "\x00\x00" // gen purpose bit flag
280 . "\x08\x00" // compression method
281 . $hexdtime // last mod time & date
282 . pack('V', $crc) // crc32
283 . pack('V', $cLen) // compressed filesize
284 . pack('V', $uncLen) // uncompressed filesize
285 . pack('v', strlen($tempName)) // length of filename
286 . pack('v', 0) // extra field length
287 . pack('v', 0) // file comment length
288 . pack('v', 0) // disk number start
289 . pack('v', 0) // internal file attributes
290 . pack('V', 32) // external file attributes
291 // - 'archive' bit set
292 . pack('V', $oldOffset) // relative offset of local header
293 . $tempName; // filename
294 $oldOffset +
= strlen($fr);
295 // optional extra field, file comment goes here
296 // save to central directory
300 /* Build string to return */
301 $tempCtrlDir = implode('', $ctrlDir);
302 $header = $tempCtrlDir . $eofCtrlDir
303 . pack('v', count($ctrlDir)) //total #of entries "on this disk"
304 . pack('v', count($ctrlDir)) //total #of entries overall
305 . pack('V', strlen($tempCtrlDir)) //size of central dir
306 . pack('V', $oldOffset) //offset to start of central dir
307 . "\x00\x00"; //.zip file comment length
309 $data = implode('', $datasec);
311 return $data . $header;