Passed
Push — master ( 0d8a9b...48a836 )
by f
12:20
created

Zip::getInstallationInstruction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
ccs 1
cts 1
cp 1
crap 1
1
<?php
2
namespace wapmorgan\UnifiedArchive\Drivers;
3
4
use Exception;
5
use wapmorgan\UnifiedArchive\ArchiveEntry;
6
use wapmorgan\UnifiedArchive\ArchiveInformation;
7
use wapmorgan\UnifiedArchive\Drivers\Basic\BasicDriver;
8
use wapmorgan\UnifiedArchive\Drivers\Basic\BasicExtensionDriver;
9
use wapmorgan\UnifiedArchive\Exceptions\ArchiveCreationException;
10
use wapmorgan\UnifiedArchive\Exceptions\ArchiveExtractionException;
11
use wapmorgan\UnifiedArchive\Exceptions\ArchiveModificationException;
12
use wapmorgan\UnifiedArchive\Exceptions\UnsupportedOperationException;
13
use wapmorgan\UnifiedArchive\Formats;
14
use ZipArchive;
15
16
/**
17
 * Class Zip
18
 *
19
 * @package wapmorgan\UnifiedArchive\Formats
20
 * @requires ext-zip
21
 */
22
class Zip extends BasicExtensionDriver
23
{
24
    /** @var ZipArchive */
25
    protected $zip;
26
27
    protected $pureFilesNumber;
28
29
    public static function getDescription()
30 1
    {
31
        return 'adapter for ext-zip'.(extension_loaded('zip') && defined('\ZipArchive::LIBZIP_VERSION') ? ' ('. ZipArchive::LIBZIP_VERSION.')' : null);
32
    }
33 1
34
    /**
35
     * @return array
36
     */
37
    public static function getSupportedFormats()
38
    {
39
        return [
40
            Formats::ZIP
41 1
        ];
42
    }
43
44 1
    /**
45 1
     * @param $format
46
     * @return array
47
     */
48
    public static function checkFormatSupport($format)
49
    {
50
        if (!static::isInstalled()) {
51
            return [];
52
        }
53
        $abilities = [
54
            BasicDriver::OPEN,
55
            BasicDriver::OPEN_ENCRYPTED,
56
            BasicDriver::GET_COMMENT,
57
            BasicDriver::EXTRACT_CONTENT,
58
            BasicDriver::STREAM_CONTENT,
59
            BasicDriver::APPEND,
60
            BasicDriver::DELETE,
61
            BasicDriver::SET_COMMENT,
62
            BasicDriver::CREATE,
63
        ];
64 4
65
        if (static::canEncrypt($format)) {
66 4
            $abilities[] = BasicDriver::CREATE_ENCRYPTED;
67 4
        }
68
69 4
        return $abilities;
70
    }
71
72
    /**
73
     * @inheritDoc
74
     */
75 4
    public function __construct($archiveFileName, $format, $password = null)
76
    {
77 4
        parent::__construct($archiveFileName, $format);
78 4
        $this->open($archiveFileName);
79 4
        if ($password !== null)
80
            $this->zip->setPassword($password);
81
    }
82 4
83
    /**
84
     * @param string $archiveFileName
85
     * @throws UnsupportedOperationException
86
     */
87 4
    protected function open($archiveFileName)
88
    {
89 4
        $this->zip = new ZipArchive();
90 4
        $open_result = $this->zip->open($archiveFileName);
91
        if ($open_result !== true) {
92
            throw new UnsupportedOperationException('Could not open Zip archive: '.$open_result);
93
        }
94
    }
95 4
96
    /**
97 4
     * Zip format destructor
98 4
     */
99 4
    public function __destruct()
100
    {
101 4
        unset($this->zip);
102 4
    }
103 4
104 4
    /**
105 4
     * @return ArchiveInformation
106
     */
107 4
    public function getArchiveInformation()
108
    {
109
        $information = new ArchiveInformation();
110
        $this->pureFilesNumber = 0;
111
        for ($i = 0; $i < $this->zip->numFiles; $i++) {
112
            $file = $this->zip->statIndex($i);
113
            // skip directories
114
            if (in_array(substr($file['name'], -1), ['/', '\\'], true))
115
                continue;
116
            $this->pureFilesNumber++;
117
            $information->files[$i] = $file['name'];
118
            $information->compressedFilesSize += $file['comp_size'];
119
            $information->uncompressedFilesSize += $file['size'];
120
        }
121
        return $information;
122
    }
123
124
    /**
125
     * @return false|string|null
126
     */
127
    public function getComment()
128
    {
129
        return $this->zip->getArchiveComment();
130
    }
131
132
    /**
133
     * @param string|null $comment
134
     * @return bool|null
135
     */
136
    public function setComment($comment)
137
    {
138
        return $this->zip->setArchiveComment($comment);
139
    }
140
141
    /**
142
     * @return array
143
     */
144
    public function getFileNames()
145
    {
146
        $files = [];
147
        for ($i = 0; $i < $this->zip->numFiles; $i++) {
148
            $file_name = $this->zip->getNameIndex($i);
149
            // skip directories
150
            if (in_array(substr($file_name, -1), ['/', '\\'], true))
151
                continue;
152
            $files[] = $file_name;
153
        }
154
        return $files;
155
    }
156
157
    /**
158 1
     * @param string $fileName
159
     *
160 1
     * @return bool
161 1
     */
162 1
    public function isFileExists($fileName)
163
    {
164
        return $this->zip->statName($fileName) !== false;
165
    }
166
167
    /**
168
     * @param string $fileName
169
     *
170
     * @return ArchiveEntry
171 2
     */
172
    public function getFileData($fileName)
173 2
    {
174 2
        $stat = $this->zip->statName($fileName);
175
176 2
        return new ArchiveEntry(
177
            $fileName,
178
            $stat['comp_size'],
179
            $stat['size'],
180
            $stat['mtime'],
181
            $stat['comp_method'] != 0,
182
            $this->zip->getCommentName($fileName),
183 1
            strtoupper(dechex($stat['crc'] < 0 ? sprintf('%u', $stat['crc']) : $stat['crc']))
0 ignored issues
show
Bug introduced by
It seems like $stat['crc'] < 0 ? sprin...['crc']) : $stat['crc'] can also be of type string; however, parameter $num of dechex() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

183
            strtoupper(dechex(/** @scrutinizer ignore-type */ $stat['crc'] < 0 ? sprintf('%u', $stat['crc']) : $stat['crc']))
Loading history...
184
        );
185 1
    }
186
187
    /**
188
     * @param string $fileName
189
     *
190
     * @return string|false
191
     * @throws \Exception
192
     */
193
    public function getFileContent($fileName)
194
    {
195
        $result = $this->zip->getFromName($fileName);
196
        if ($result === false)
197
            throw new Exception('Could not get file information: '.$result.'. May use password?');
198
        return $result;
199
    }
200
201
    /**
202
     * @param $fileName
203
     * @return false|resource
204
     */
205
    public function getFileStream($fileName)
206
    {
207
        return $this->zip->getStream($fileName);
208
    }
209
210
    /**
211
     * @param string $outputFolder
212
     * @param array $files
213
     * @return int Number of extracted files
214
     * @throws ArchiveExtractionException
215
     */
216
    public function extractFiles($outputFolder, array $files)
217
    {
218
        if ($this->zip->extractTo($outputFolder, $files) === false)
219
            throw new ArchiveExtractionException($this->zip->getStatusString(), $this->zip->status);
220
221 1
        return count($files);
222
    }
223 1
224 1
    /**
225 1
     * @param string $outputFolder
226
     * @return int Number of extracted files
227 1
     *@throws ArchiveExtractionException
228
     */
229
    public function extractArchive($outputFolder)
230
    {
231 1
        if ($this->zip->extractTo($outputFolder) === false)
232 1
            throw new ArchiveExtractionException($this->zip->getStatusString(), $this->zip->status);
233 1
234
        return $this->pureFilesNumber;
235 1
    }
236
237
    /**
238
     * @param array $files
239
     * @return int
240
     * @throws ArchiveModificationException
241
     * @throws UnsupportedOperationException
242
     */
243
    public function deleteFiles(array $files)
244 1
    {
245
        $count = 0;
246 1
        foreach ($files as $file) {
247 1
            if ($this->zip->deleteName($file) === false)
248 1
                throw new ArchiveModificationException($this->zip->getStatusString(), $this->zip->status);
249
            $count++;
250
        }
251
252 1
        // reopen archive to save changes
253
        $archive_filename = $this->zip->filename;
254 1
        $this->zip->close();
255
        $this->open($archive_filename);
256
257
        return $count;
258
    }
259 1
260 1
    /**
261 1
     * @param array $files
262
     * @return int
263 1
     * @throws ArchiveModificationException
264
     * @throws UnsupportedOperationException
265
     */
266
    public function addFiles(array $files)
267
    {
268
        $added_files = 0;
269
        foreach ($files as $localName => $fileName) {
270
            if (is_null($fileName)) {
271
                if ($this->zip->addEmptyDir($localName) === false)
272
                    throw new ArchiveModificationException($this->zip->getStatusString(), $this->zip->status);
273
            } else {
274
                if ($this->zip->addFile($fileName, $localName) === false)
275
                    throw new ArchiveModificationException($this->zip->getStatusString(), $this->zip->status);
276
                $added_files++;
277
            }
278
        }
279
280
        // reopen archive to save changes
281
        $archive_filename = $this->zip->filename;
282
        $this->zip->close();
283
        $this->open($archive_filename);
284
285
        return $added_files;
286 1
    }
287
288 1
    /**
289
     * @param string $inArchiveName
290
     * @param string $content
291
     * @return bool
292
     */
293
    public function addFileFromString($inArchiveName, $content)
294
    {
295
        return $this->zip->addFromString($inArchiveName, $content);
296 1
    }
297 1
298
    /**
299 1
     * @param array $files
300
     * @param string $archiveFileName
301
     * @param int $archiveFormat
302 1
     * @param int $compressionLevel
303 1
     * @param null $password
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $password is correct as it would always require null to be passed?
Loading history...
304
     * @param $fileProgressCallable
305 1
     * @return int
306
     * @throws ArchiveCreationException
307
     * @throws UnsupportedOperationException
308
     */
309 1
    public static function createArchive(
310 1
        array $files,
311 1
        $archiveFileName,
312 1
        $archiveFormat,
313
        $compressionLevel = self::COMPRESSION_AVERAGE,
314 1
        $password = null,
315
        $fileProgressCallable = null
316 1
    ) {
317 1
        static $compressionLevelMap = [
318
            self::COMPRESSION_NONE => ZipArchive::CM_STORE,
319 1
            self::COMPRESSION_WEAK => ZipArchive::CM_DEFLATE,
320 1
            self::COMPRESSION_AVERAGE => ZipArchive::CM_DEFLATE,
321
            self::COMPRESSION_STRONG => ZipArchive::CM_DEFLATE,
322
            self::COMPRESSION_MAXIMUM => ZipArchive::CM_DEFLATE64,
323
        ];
324 1
325
        $zip = new ZipArchive();
326 1
        $result = $zip->open($archiveFileName, ZipArchive::CREATE);
327
328
        if ($result !== true)
329
            throw new ArchiveCreationException('ZipArchive error: '.$result);
330
331
        $can_set_compression_level = method_exists($zip, 'setCompressionName');
332 1
        $can_encrypt = static::canEncrypt(Formats::ZIP);
333
334 1
        if ($password !== null && !$can_encrypt) {
0 ignored issues
show
introduced by
The condition $password !== null is always false.
Loading history...
335
            throw new ArchiveCreationException('Encryption is not supported on current platform');
336
        }
337
338
        if ($fileProgressCallable !== null && !is_callable($fileProgressCallable)) {
339
            throw new ArchiveCreationException('File progress callable is not callable');
340 1
        }
341
342 1
        $current_file = 0;
343
        $total_files = count($files);
344
345
        foreach ($files as $localName => $fileName) {
346
            if ($fileName === null) {
347
                if ($zip->addEmptyDir($localName) === false)
348 1
                    throw new ArchiveCreationException('Could not archive directory "'.$localName.'": '.$zip->getStatusString(), $zip->status);
349
            } else {
350 1
                if ($zip->addFile($fileName, $localName) === false)
351
                    throw new ArchiveCreationException('Could not archive file "'.$fileName.'": '.$zip->getStatusString(), $zip->status);
352
                if ($can_set_compression_level) {
353
                    $zip->setCompressionName($localName, $compressionLevelMap[$compressionLevel]);
354
                }
355
                if ($password !== null && $can_encrypt) {
356 1
                    $zip->setEncryptionName($localName, ZipArchive::EM_AES_256, $password);
357
                }
358 1
            }
359
            if ($fileProgressCallable !== null) {
360
                call_user_func_array($fileProgressCallable, [$current_file++, $total_files, $fileName, $localName]);
361
            }
362
        }
363
        $zip->close();
364
365
        return count($files);
366
    }
367
368
    /**
369
     * @inheritDoc
370
     */
371
    public static function canEncrypt($format)
0 ignored issues
show
Unused Code introduced by
The parameter $format is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

371
    public static function canEncrypt(/** @scrutinizer ignore-unused */ $format)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
372
    {
373
        return method_exists('\ZipArchive', 'setEncryptionName');
374
    }
375
}
376