FilesUtil   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Test Coverage

Coverage 66.46%

Importance

Changes 0
Metric Value
eloc 207
c 0
b 0
f 0
dl 0
loc 433
ccs 105
cts 158
cp 0.6646
rs 2.48
wmc 74

14 Methods

Rating   Name   Duplication   Size   Complexity  
A isEmptyDir() 0 7 2
A normalizeZipPath() 0 8 2
A symlink() 0 7 3
A getMimeTypeFromString() 0 11 2
D convertGlobToRegEx() 0 81 27
A isBadCompressionMimeType() 0 56 2
A isAbsolutePath() 0 9 6
A regexFileSearch() 0 20 4
A fileSearchWithIgnore() 0 27 6
A getMimeTypeFromFile() 0 7 2
A isBadCompressionFile() 0 24 2
A removeDir() 0 13 3
A globFileSearch() 0 17 3
B humanSize() 0 15 10

How to fix   Complexity   

Complex Class

Complex classes like FilesUtil often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FilesUtil, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpZip\Util;
4
5
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
6
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
7
8
/**
9
 * Files util.
10
 *
11
 * @author Ne-Lexa [email protected]
12
 * @license MIT
13
 *
14
 * @internal
15
 */
16
final class FilesUtil
17
{
18
    /**
19
     * Is empty directory.
20
     *
21
     * @param string $dir Directory
22
     *
23
     * @return bool
24
     */
25 31
    public static function isEmptyDir($dir)
26
    {
27 31
        if (!is_readable($dir)) {
28
            return false;
29
        }
30
31 31
        return \count(scandir($dir)) === 2;
0 ignored issues
show
Bug introduced by
It seems like scandir($dir) can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

31
        return \count(/** @scrutinizer ignore-type */ scandir($dir)) === 2;
Loading history...
32
    }
33
34
    /**
35
     * Remove recursive directory.
36
     *
37
     * @param string $dir directory path
38
     */
39 44
    public static function removeDir($dir)
40
    {
41 44
        $files = new \RecursiveIteratorIterator(
42 44
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
43 44
            \RecursiveIteratorIterator::CHILD_FIRST
44
        );
45
46
        /** @var \SplFileInfo $fileInfo */
47 44
        foreach ($files as $fileInfo) {
48 37
            $function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
49 37
            $function($fileInfo->getPathname());
50
        }
51 44
        @rmdir($dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

51
        /** @scrutinizer ignore-unhandled */ @rmdir($dir);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
52 44
    }
53
54
    /**
55
     * Convert glob pattern to regex pattern.
56
     *
57
     * @param string $globPattern
58
     *
59
     * @return string
60
     */
61 2
    public static function convertGlobToRegEx($globPattern)
62
    {
63
        // Remove beginning and ending * globs because they're useless
64 2
        $globPattern = trim($globPattern, '*');
65 2
        $escaping = false;
66 2
        $inCurrent = 0;
67 2
        $chars = str_split($globPattern);
68 2
        $regexPattern = '';
69
70 2
        foreach ($chars as $currentChar) {
71 2
            switch ($currentChar) {
72 2
                case '*':
73
                    $regexPattern .= ($escaping ? '\\*' : '.*');
74
                    $escaping = false;
75
                    break;
76
77 2
                case '?':
78
                    $regexPattern .= ($escaping ? '\\?' : '.');
79
                    $escaping = false;
80
                    break;
81
82 2
                case '.':
83 2
                case '(':
84 2
                case ')':
85 2
                case '+':
86 2
                case '|':
87 2
                case '^':
88 2
                case '$':
89 2
                case '@':
90 2
                case '%':
91 2
                    $regexPattern .= '\\' . $currentChar;
92 2
                    $escaping = false;
93 2
                    break;
94
95 2
                case '\\':
96
                    if ($escaping) {
97
                        $regexPattern .= '\\\\';
98
                        $escaping = false;
99
                    } else {
100
                        $escaping = true;
101
                    }
102
                    break;
103
104 2
                case '{':
105 2
                    if ($escaping) {
106
                        $regexPattern .= '\\{';
107
                    } else {
108 2
                        $regexPattern = '(';
109 2
                        $inCurrent++;
110
                    }
111 2
                    $escaping = false;
112 2
                    break;
113
114 2
                case '}':
115 2
                    if ($inCurrent > 0 && !$escaping) {
116 2
                        $regexPattern .= ')';
117 2
                        $inCurrent--;
118
                    } elseif ($escaping) {
119
                        $regexPattern = '\\}';
120
                    } else {
121
                        $regexPattern = '}';
122
                    }
123 2
                    $escaping = false;
124 2
                    break;
125
126 2
                case ',':
127 2
                    if ($inCurrent > 0 && !$escaping) {
128 2
                        $regexPattern .= '|';
129
                    } elseif ($escaping) {
130
                        $regexPattern .= '\\,';
131
                    } else {
132
                        $regexPattern = ',';
133
                    }
134 2
                    break;
135
                default:
136 2
                    $escaping = false;
137 2
                    $regexPattern .= $currentChar;
138
            }
139
        }
140
141 2
        return $regexPattern;
142
    }
143
144
    /**
145
     * Search files.
146
     *
147
     * @param string $inputDir
148
     * @param bool   $recursive
149
     * @param array  $ignoreFiles
150
     *
151
     * @return array Searched file list
152
     */
153
    public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = [])
154
    {
155
        if ($recursive) {
156
            $directoryIterator = new \RecursiveDirectoryIterator($inputDir);
157
158
            if (!empty($ignoreFiles)) {
159
                $directoryIterator = new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles);
160
            }
161
            $iterator = new \RecursiveIteratorIterator($directoryIterator);
162
        } else {
163
            $directoryIterator = new \DirectoryIterator($inputDir);
164
165
            if (!empty($ignoreFiles)) {
166
                $directoryIterator = new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
167
            }
168
            $iterator = new \IteratorIterator($directoryIterator);
169
        }
170
171
        $fileList = [];
172
173
        foreach ($iterator as $file) {
174
            if ($file instanceof \SplFileInfo) {
175
                $fileList[] = $file->getPathname();
176
            }
177
        }
178
179
        return $fileList;
180
    }
181
182
    /**
183
     * Search files from glob pattern.
184
     *
185
     * @param string $globPattern
186
     * @param int    $flags
187
     * @param bool   $recursive
188
     *
189
     * @return array Searched file list
190
     */
191 4
    public static function globFileSearch($globPattern, $flags = 0, $recursive = true)
192
    {
193 4
        $flags = (int) $flags;
194 4
        $recursive = (bool) $recursive;
195 4
        $files = glob($globPattern, $flags);
196
197 4
        if (!$recursive) {
198 1
            return $files;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $files could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
199
        }
200
201 3
        foreach (glob(\dirname($globPattern) . \DIRECTORY_SEPARATOR . '*', \GLOB_ONLYDIR | \GLOB_NOSORT) as $dir) {
202
            // Unpacking the argument via ... is supported starting from php 5.6 only
203
            /** @noinspection SlowArrayOperationsInLoopInspection */
204 3
            $files = array_merge($files, self::globFileSearch($dir . \DIRECTORY_SEPARATOR . basename($globPattern), $flags, $recursive));
0 ignored issues
show
Bug introduced by
It seems like $files can also be of type false; however, parameter $array1 of array_merge() does only seem to accept array, 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

204
            $files = array_merge(/** @scrutinizer ignore-type */ $files, self::globFileSearch($dir . \DIRECTORY_SEPARATOR . basename($globPattern), $flags, $recursive));
Loading history...
205
        }
206
207 3
        return $files;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $files could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
208
    }
209
210
    /**
211
     * Search files from regex pattern.
212
     *
213
     * @param string $folder
214
     * @param string $pattern
215
     * @param bool   $recursive
216
     *
217
     * @return array Searched file list
218
     */
219 4
    public static function regexFileSearch($folder, $pattern, $recursive = true)
220
    {
221 4
        if ($recursive) {
222 3
            $directoryIterator = new \RecursiveDirectoryIterator($folder);
223 3
            $iterator = new \RecursiveIteratorIterator($directoryIterator);
224
        } else {
225 1
            $directoryIterator = new \DirectoryIterator($folder);
226 1
            $iterator = new \IteratorIterator($directoryIterator);
227
        }
228
229 4
        $regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
230 4
        $fileList = [];
231
232 4
        foreach ($regexIterator as $file) {
233 4
            if ($file instanceof \SplFileInfo) {
234 4
                $fileList[] = $file->getPathname();
235
            }
236
        }
237
238 4
        return $fileList;
239
    }
240
241
    /**
242
     * Convert bytes to human size.
243
     *
244
     * @param int         $size Size bytes
245
     * @param string|null $unit Unit support 'GB', 'MB', 'KB'
246
     *
247
     * @return string
248
     */
249
    public static function humanSize($size, $unit = null)
250
    {
251
        if (($unit === null && $size >= 1 << 30) || $unit === 'GB') {
252
            return number_format($size / (1 << 30), 2) . 'GB';
253
        }
254
255
        if (($unit === null && $size >= 1 << 20) || $unit === 'MB') {
256
            return number_format($size / (1 << 20), 2) . 'MB';
257
        }
258
259
        if (($unit === null && $size >= 1 << 10) || $unit === 'KB') {
260
            return number_format($size / (1 << 10), 2) . 'KB';
261
        }
262
263
        return number_format($size) . ' bytes';
264
    }
265
266
    /**
267
     * Normalizes zip path.
268
     *
269
     * @param string $path Zip path
270
     *
271
     * @return string
272
     */
273 10
    public static function normalizeZipPath($path)
274
    {
275 10
        return implode(
276 10
            \DIRECTORY_SEPARATOR,
277
            array_filter(
278 10
                explode('/', (string) $path),
279 10
                static function ($part) {
280 10
                    return $part !== '.' && $part !== '..';
281 10
                }
282
            )
283
        );
284
    }
285
286
    /**
287
     * Returns whether the file path is an absolute path.
288
     *
289
     * @param string $file A file path
290
     *
291
     * @return bool
292
     *
293
     * @see source symfony filesystem component
294
     */
295
    public static function isAbsolutePath($file)
296
    {
297
        return strspn($file, '/\\', 0, 1)
298
            || (
299
                \strlen($file) > 3 && ctype_alpha($file[0])
300
                && $file[1] === ':'
301
                && strspn($file, '/\\', 2, 1)
302
            )
303
            || parse_url($file, \PHP_URL_SCHEME) !== null;
304
    }
305
306
    /**
307
     * @param string $target
308
     * @param string $path
309
     * @param bool   $allowSymlink
310
     *
311
     * @return bool
312
     */
313 2
    public static function symlink($target, $path, $allowSymlink)
314
    {
315 2
        if (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) {
316 1
            return file_put_contents($path, $target) !== false;
317
        }
318
319 1
        return symlink($target, $path);
320
    }
321
322
    /**
323
     * @param string $file
324
     *
325
     * @return bool
326
     */
327 21
    public static function isBadCompressionFile($file)
328
    {
329
        $badCompressFileExt = [
330 21
            'dic',
331
            'dng',
332
            'f4v',
333
            'flipchart',
334
            'h264',
335
            'lrf',
336
            'mobi',
337
            'mts',
338
            'nef',
339
            'pspimage',
340
        ];
341
342 21
        $ext = strtolower(pathinfo($file, \PATHINFO_EXTENSION));
343
344 21
        if (\in_array($ext, $badCompressFileExt, true)) {
345
            return true;
346
        }
347
348 21
        $mimeType = self::getMimeTypeFromFile($file);
349
350 21
        return self::isBadCompressionMimeType($mimeType);
351
    }
352
353
    /**
354
     * @param string $mimeType
355
     *
356
     * @return bool
357
     */
358 27
    public static function isBadCompressionMimeType($mimeType)
359
    {
360 27
        static $badDeflateCompMimeTypes = [
361
            'application/epub+zip',
362
            'application/gzip',
363
            'application/vnd.debian.binary-package',
364
            'application/vnd.oasis.opendocument.graphics',
365
            'application/vnd.oasis.opendocument.presentation',
366
            'application/vnd.oasis.opendocument.text',
367
            'application/vnd.oasis.opendocument.text-master',
368
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
369
            'application/vnd.rn-realmedia',
370
            'application/x-7z-compressed',
371
            'application/x-arj',
372
            'application/x-bzip2',
373
            'application/x-hwp',
374
            'application/x-lzip',
375
            'application/x-lzma',
376
            'application/x-ms-reader',
377
            'application/x-rar',
378
            'application/x-rpm',
379
            'application/x-stuffit',
380
            'application/x-tar',
381
            'application/x-xz',
382
            'application/zip',
383
            'application/zlib',
384
            'audio/flac',
385
            'audio/mpeg',
386
            'audio/ogg',
387
            'audio/vnd.dolby.dd-raw',
388
            'audio/webm',
389
            'audio/x-ape',
390
            'audio/x-hx-aac-adts',
391
            'audio/x-m4a',
392
            'audio/x-m4a',
393
            'audio/x-wav',
394
            'image/gif',
395
            'image/heic',
396
            'image/jp2',
397
            'image/jpeg',
398
            'image/png',
399
            'image/vnd.djvu',
400
            'image/webp',
401
            'image/x-canon-cr2',
402
            'video/ogg',
403
            'video/webm',
404
            'video/x-matroska',
405
            'video/x-ms-asf',
406
            'x-epoc/x-sisx-app',
407
        ];
408
409 27
        if (\in_array($mimeType, $badDeflateCompMimeTypes, true)) {
410
            return true;
411
        }
412
413 27
        return false;
414
    }
415
416
    /**
417
     * @param string $file
418
     *
419
     * @return string
420
     *
421
     * @noinspection PhpComposerExtensionStubsInspection
422
     */
423 21
    public static function getMimeTypeFromFile($file)
424
    {
425 21
        if (\function_exists('mime_content_type')) {
426 21
            return mime_content_type($file);
427
        }
428
429
        return 'application/octet-stream';
430
    }
431
432
    /**
433
     * @param string $contents
434
     *
435
     * @return string
436
     * @noinspection PhpComposerExtensionStubsInspection
437
     */
438 10
    public static function getMimeTypeFromString($contents)
439
    {
440 10
        $contents = (string) $contents;
441 10
        $finfo = new \finfo(\FILEINFO_MIME);
442 10
        $mimeType = $finfo->buffer($contents);
443
444 10
        if ($mimeType === false) {
445
            $mimeType = 'application/octet-stream';
446
        }
447
448 10
        return explode(';', $mimeType)[0];
449
    }
450
}
451