Zip::getFileStream()   A
last analyzed

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

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

373
    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...
374
    {
375
        return method_exists('\ZipArchive', 'setEncryptionName');
376
    }
377
}
378