Passed
Push — master ( b1d4f6...7824cd )
by f
10:25
created

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

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