Passed
Push — master ( e9611b...a3681d )
by f
13:07
created

Zip::isInstalled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

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 0
cts 0
cp 0
crap 2
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\Exceptions\ArchiveCreationException;
8
use wapmorgan\UnifiedArchive\Exceptions\ArchiveExtractionException;
9
use wapmorgan\UnifiedArchive\Exceptions\ArchiveModificationException;
10
use wapmorgan\UnifiedArchive\Exceptions\UnsupportedOperationException;
11
use wapmorgan\UnifiedArchive\Formats;
12
use ZipArchive;
13
14
/**
15
 * Class Zip
16
 *
17
 * @package wapmorgan\UnifiedArchive\Formats
18
 * @requires ext-zip
19
 */
20
class Zip extends BasicDriver
21
{
22
    const TYPE = self::TYPE_EXTENSION;
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
    public static function isInstalled()
35
    {
36
        return extension_loaded('zip');
37
    }
38
39
    public static function getInstallationInstruction()
40
    {
41 1
        return 'install [zip] php extension';
42
    }
43
44 1
    /**
45 1
     * @return array
46
     */
47
    public static function getSupportedFormats()
48
    {
49
        return [
50
            Formats::ZIP
51
        ];
52
    }
53
54
    /**
55
     * @param $format
56
     * @return array
57
     */
58
    public static function checkFormatSupport($format)
59
    {
60
        if (!static::isInstalled()) {
61
            return [];
62
        }
63
        $abilities = [
64 4
            BasicDriver::OPEN,
65
            BasicDriver::OPEN_ENCRYPTED,
66 4
            BasicDriver::GET_COMMENT,
67 4
            BasicDriver::EXTRACT_CONTENT,
68
            BasicDriver::STREAM_CONTENT,
69 4
            BasicDriver::APPEND,
70
            BasicDriver::DELETE,
71
            BasicDriver::SET_COMMENT,
72
            BasicDriver::CREATE,
73
        ];
74
75 4
        if (static::canEncrypt($format)) {
76
            $abilities[] = BasicDriver::CREATE_ENCRYPTED;
77 4
        }
78 4
79 4
        return $abilities;
80
    }
81
82 4
    /**
83
     * @inheritDoc
84
     */
85
    public function __construct($archiveFileName, $format, $password = null)
86
    {
87 4
        $this->open($archiveFileName);
88
        if ($password !== null)
89 4
            $this->zip->setPassword($password);
90 4
    }
91
92
    /**
93
     * @param string $archiveFileName
94
     * @throws UnsupportedOperationException
95 4
     */
96
    protected function open($archiveFileName)
97 4
    {
98 4
        $this->zip = new ZipArchive();
99 4
        $open_result = $this->zip->open($archiveFileName);
100
        if ($open_result !== true) {
101 4
            throw new UnsupportedOperationException('Could not open Zip archive: '.$open_result);
102 4
        }
103 4
    }
104 4
105 4
    /**
106
     * Zip format destructor
107 4
     */
108
    public function __destruct()
109
    {
110
        unset($this->zip);
111
    }
112
113
    /**
114
     * @return ArchiveInformation
115
     */
116
    public function getArchiveInformation()
117
    {
118
        $information = new ArchiveInformation();
119
        $this->pureFilesNumber = 0;
120
        for ($i = 0; $i < $this->zip->numFiles; $i++) {
121
            $file = $this->zip->statIndex($i);
122
            // skip directories
123
            if (in_array(substr($file['name'], -1), ['/', '\\'], true))
124
                continue;
125
            $this->pureFilesNumber++;
126
            $information->files[$i] = $file['name'];
127
            $information->compressedFilesSize += $file['comp_size'];
128
            $information->uncompressedFilesSize += $file['size'];
129
        }
130
        return $information;
131
    }
132
133
    /**
134
     * @return false|string|null
135
     */
136
    public function getComment()
137
    {
138
        return $this->zip->getArchiveComment();
139
    }
140
141
    /**
142
     * @param string|null $comment
143
     * @return bool|null
144
     */
145
    public function setComment($comment)
146
    {
147
        return $this->zip->setArchiveComment($comment);
148
    }
149
150
    /**
151
     * @return array
152
     */
153
    public function getFileNames()
154
    {
155
        $files = [];
156
        for ($i = 0; $i < $this->zip->numFiles; $i++) {
157
            $file_name = $this->zip->getNameIndex($i);
158 1
            // skip directories
159
            if (in_array(substr($file_name, -1), ['/', '\\'], true))
160 1
                continue;
161 1
            $files[] = $file_name;
162 1
        }
163
        return $files;
164
    }
165
166
    /**
167
     * @param string $fileName
168
     *
169
     * @return bool
170
     */
171 2
    public function isFileExists($fileName)
172
    {
173 2
        return $this->zip->statName($fileName) !== false;
174 2
    }
175
176 2
    /**
177
     * @param string $fileName
178
     *
179
     * @return ArchiveEntry
180
     */
181
    public function getFileData($fileName)
182
    {
183 1
        $stat = $this->zip->statName($fileName);
184
185 1
        return new ArchiveEntry(
186
            $fileName,
187
            $stat['comp_size'],
188
            $stat['size'],
189
            $stat['mtime'],
190
            $stat['comp_method'] != 0,
191
            $this->zip->getCommentName($fileName),
192
            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

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

380
    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...
381
    {
382
        return method_exists('\ZipArchive', 'setEncryptionName');
383
    }
384
}
385