GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 400df7...c8c0ea )
by Robert
24:39 queued 16:15
created

BaseFileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 12.0135

Importance

Changes 0
Metric Value
cc 12
eloc 22
nc 72
nop 5
dl 0
loc 42
ccs 21
cts 22
cp 0.9545
crap 12.0135
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\helpers;
9
10
use Yii;
11
use yii\base\ErrorException;
12
use yii\base\Exception;
13
use yii\base\InvalidArgumentException;
14
use yii\base\InvalidConfigException;
15
16
/**
17
 * BaseFileHelper provides concrete implementation for [[FileHelper]].
18
 *
19
 * Do not use BaseFileHelper. Use [[FileHelper]] instead.
20
 *
21
 * @author Qiang Xue <[email protected]>
22
 * @author Alex Makarov <[email protected]>
23
 * @since 2.0
24
 */
25
class BaseFileHelper
26
{
27
    const PATTERN_NODIR = 1;
28
    const PATTERN_ENDSWITH = 4;
29
    const PATTERN_MUSTBEDIR = 8;
30
    const PATTERN_NEGATIVE = 16;
31
    const PATTERN_CASE_INSENSITIVE = 32;
32
33
    /**
34
     * @var string the path (or alias) of a PHP file containing MIME type information.
35
     */
36
    public static $mimeMagicFile = '@yii/helpers/mimeTypes.php';
37
    /**
38
     * @var string the path (or alias) of a PHP file containing MIME aliases.
39
     * @since 2.0.14
40
     */
41
    public static $mimeAliasesFile = '@yii/helpers/mimeAliases.php';
42
    /**
43
     * @var string the path (or alias) of a PHP file containing extensions per MIME type.
44
     * @since 2.0.48
45
     */
46
    public static $mimeExtensionsFile = '@yii/helpers/mimeExtensions.php';
47
48
49
    /**
50
     * Normalizes a file/directory path.
51
     *
52
     * The normalization does the following work:
53
     *
54
     * - Convert all directory separators into `DIRECTORY_SEPARATOR` (e.g. "\a/b\c" becomes "/a/b/c")
55
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
56
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
57
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
58
     *
59
     * Note: For registered stream wrappers, the consecutive slashes rule
60
     * and ".."/"." translations are skipped.
61
     *
62
     * @param string $path the file/directory path to be normalized
63
     * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
64
     * @return string the normalized file/directory path
65
     */
66 36
    public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
67
    {
68 36
        $path = rtrim(strtr($path, '/\\', $ds . $ds), $ds);
69 36
        if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) {
70 35
            return $path;
71
        }
72
        // fix #17235 stream wrappers
73 1
        foreach (stream_get_wrappers() as $protocol) {
74 1
            if (strpos($path, "{$protocol}://") === 0) {
75 1
                return $path;
76
            }
77
        }
78
        // the path may contain ".", ".." or double slashes, need to clean them up
79 1
        if (strpos($path, "{$ds}{$ds}") === 0 && $ds == '\\') {
80 1
            $parts = [$ds];
81
        } else {
82 1
            $parts = [];
83
        }
84 1
        foreach (explode($ds, $path) as $part) {
85 1
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
86 1
                array_pop($parts);
87 1
            } elseif ($part === '.' || $part === '' && !empty($parts)) {
88 1
                continue;
89
            } else {
90 1
                $parts[] = $part;
91
            }
92
        }
93 1
        $path = implode($ds, $parts);
94 1
        return $path === '' ? '.' : $path;
95
    }
96
97
    /**
98
     * Returns the localized version of a specified file.
99
     *
100
     * The searching is based on the specified language code. In particular,
101
     * a file with the same name will be looked for under the subdirectory
102
     * whose name is the same as the language code. For example, given the file "path/to/view.php"
103
     * and language code "zh-CN", the localized file will be looked for as
104
     * "path/to/zh-CN/view.php". If the file is not found, it will try a fallback with just a language code that is
105
     * "zh" i.e. "path/to/zh/view.php". If it is not found as well the original file will be returned.
106
     *
107
     * If the target and the source language codes are the same, the original file will be returned.
108
     *
109
     * @param string $file the original file
110
     * @param string|null $language the target language that the file should be localized to.
111
     * If not set, the value of [[\yii\base\Application::language]] will be used.
112
     * @param string|null $sourceLanguage the language that the original file is in.
113
     * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used.
114
     * @return string the matching localized file, or the original file if the localized version is not found.
115
     * If the target and the source language codes are the same, the original file will be returned.
116
     */
117 157
    public static function localize($file, $language = null, $sourceLanguage = null)
118
    {
119 157
        if ($language === null) {
120 156
            $language = Yii::$app->language;
121
        }
122 157
        if ($sourceLanguage === null) {
123 156
            $sourceLanguage = Yii::$app->sourceLanguage;
124
        }
125 157
        if ($language === $sourceLanguage) {
126 157
            return $file;
127
        }
128 1
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
129 1
        if (is_file($desiredFile)) {
130 1
            return $desiredFile;
131
        }
132
133
        $language = substr($language, 0, 2);
134
        if ($language === $sourceLanguage) {
135
            return $file;
136
        }
137
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
138
139
        return is_file($desiredFile) ? $desiredFile : $file;
140
    }
141
142
    /**
143
     * Determines the MIME type of the specified file.
144
     * This method will first try to determine the MIME type based on
145
     * [finfo_open](https://www.php.net/manual/en/function.finfo-open.php). If the `fileinfo` extension is not installed,
146
     * it will fall back to [[getMimeTypeByExtension()]] when `$checkExtension` is true.
147
     * @param string $file the file name.
148
     * @param string|null $magicFile name of the optional magic database file (or alias), usually something like `/path/to/magic.mime`.
149
     * This will be passed as the second parameter to [finfo_open()](https://www.php.net/manual/en/function.finfo-open.php)
150
     * when the `fileinfo` extension is installed. If the MIME type is being determined based via [[getMimeTypeByExtension()]]
151
     * and this is null, it will use the file specified by [[mimeMagicFile]].
152
     * @param bool $checkExtension whether to use the file extension to determine the MIME type in case
153
     * `finfo_open()` cannot determine it.
154
     * @return string|null the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined.
155
     * @throws InvalidConfigException when the `fileinfo` PHP extension is not installed and `$checkExtension` is `false`.
156
     */
157 32
    public static function getMimeType($file, $magicFile = null, $checkExtension = true)
158
    {
159 32
        if ($magicFile !== null) {
160
            $magicFile = Yii::getAlias($magicFile);
161
        }
162 32
        if (!extension_loaded('fileinfo')) {
163
            if ($checkExtension) {
164
                return static::getMimeTypeByExtension($file, $magicFile);
0 ignored issues
show
Bug introduced by
It seems like $magicFile can also be of type false; however, parameter $magicFile of yii\helpers\BaseFileHelp...etMimeTypeByExtension() does only seem to accept null|string, 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

164
                return static::getMimeTypeByExtension($file, /** @scrutinizer ignore-type */ $magicFile);
Loading history...
165
            }
166
167
            throw new InvalidConfigException('The fileinfo PHP extension is not installed.');
168
        }
169
170 32
        $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile);
0 ignored issues
show
Bug introduced by
It seems like $magicFile can also be of type false and null; however, parameter $magic_database of finfo_open() does only seem to accept string, 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

170
        $info = finfo_open(FILEINFO_MIME_TYPE, /** @scrutinizer ignore-type */ $magicFile);
Loading history...
171
172 32
        if ($info) {
0 ignored issues
show
introduced by
$info is of type resource, thus it always evaluated to false.
Loading history...
173 32
            $result = finfo_file($info, $file);
174 32
            finfo_close($info);
175
176 32
            if ($result !== false) {
177 32
                return $result;
178
            }
179
        }
180
181
        return $checkExtension ? static::getMimeTypeByExtension($file, $magicFile) : null;
182
    }
183
184
    /**
185
     * Determines the MIME type based on the extension name of the specified file.
186
     * This method will use a local map between extension names and MIME types.
187
     * @param string $file the file name.
188
     * @param string|null $magicFile the path (or alias) of the file that contains all available MIME type information.
189
     * If this is not set, the file specified by [[mimeMagicFile]] will be used.
190
     * @return string|null the MIME type. Null is returned if the MIME type cannot be determined.
191
     */
192 11
    public static function getMimeTypeByExtension($file, $magicFile = null)
193
    {
194 11
        $mimeTypes = static::loadMimeTypes($magicFile);
195
196 11
        if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') {
197 11
            $ext = strtolower($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext can also be of type array; however, parameter $string of strtolower() does only seem to accept string, 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

197
            $ext = strtolower(/** @scrutinizer ignore-type */ $ext);
Loading history...
198 11
            if (isset($mimeTypes[$ext])) {
199 11
                return $mimeTypes[$ext];
200
            }
201
        }
202
203 1
        return null;
204
    }
205
206
    /**
207
     * Determines the extensions by given MIME type.
208
     * This method will use a local map between extension names and MIME types.
209
     * @param string $mimeType file MIME type.
210
     * @param string|null $magicFile the path (or alias) of the file that contains all available MIME type information.
211
     * If this is not set, the file specified by [[mimeMagicFile]] will be used.
212
     * @return array the extensions corresponding to the specified MIME type
213
     */
214 15
    public static function getExtensionsByMimeType($mimeType, $magicFile = null)
215
    {
216 15
        $aliases = static::loadMimeAliases(static::$mimeAliasesFile);
217 15
        if (isset($aliases[$mimeType])) {
218 2
            $mimeType = $aliases[$mimeType];
219
        }
220
221
        // Note: For backwards compatibility the "MimeTypes" file is used.
222 15
        $mimeTypes = static::loadMimeTypes($magicFile);
223 15
        return array_keys($mimeTypes, mb_strtolower($mimeType, 'UTF-8'), true);
224
    }
225
226
    /**
227
     * Determines the most common extension by given MIME type.
228
     * This method will use a local map between MIME types and extension names.
229
     * @param string $mimeType file MIME type.
230
     * @param bool $preferShort return an extension with a maximum of 3 characters.
231
     * @param string|null $magicFile the path (or alias) of the file that contains all available MIME type information.
232
     * If this is not set, the file specified by [[mimeMagicFile]] will be used.
233
     * @return string|null the extensions corresponding to the specified MIME type
234
     * @since 2.0.48
235
     */
236 4
    public static function getExtensionByMimeType($mimeType, $preferShort = false, $magicFile = null)
237
    {
238 4
        $aliases = static::loadMimeAliases(static::$mimeAliasesFile);
239 4
        if (isset($aliases[$mimeType])) {
240
            $mimeType = $aliases[$mimeType];
241
        }
242
243 4
        $mimeExtensions = static::loadMimeExtensions($magicFile);
244
245 4
        if (!array_key_exists($mimeType, $mimeExtensions)) {
246
            return null;
247
        }
248
249 4
        $extensions = $mimeExtensions[$mimeType];
250 4
        if (is_array($extensions)) {
251 2
            if ($preferShort) {
252 1
                foreach ($extensions as $extension) {
253 1
                    if (mb_strlen($extension, 'UTF-8') <= 3) {
254 1
                        return $extension;
255
                    }
256
                }
257
            }
258 1
            return $extensions[0];
259
        } else {
260 2
            return $extensions;
261
        }
262
    }
263
264
    private static $_mimeTypes = [];
265
266
    /**
267
     * Loads MIME types from the specified file.
268
     * @param string|null $magicFile the path (or alias) of the file that contains all available MIME type information.
269
     * If this is not set, the file specified by [[mimeMagicFile]] will be used.
270
     * @return array the mapping from file extensions to MIME types
271
     */
272 26
    protected static function loadMimeTypes($magicFile)
273
    {
274 26
        if ($magicFile === null) {
275 26
            $magicFile = static::$mimeMagicFile;
276
        }
277 26
        $magicFile = Yii::getAlias($magicFile);
278 26
        if (!isset(self::$_mimeTypes[$magicFile])) {
279 1
            self::$_mimeTypes[$magicFile] = require $magicFile;
280
        }
281
282 26
        return self::$_mimeTypes[$magicFile];
283
    }
284
285
    private static $_mimeAliases = [];
286
287
    /**
288
     * Loads MIME aliases from the specified file.
289
     * @param string|null $aliasesFile the path (or alias) of the file that contains MIME type aliases.
290
     * If this is not set, the file specified by [[mimeAliasesFile]] will be used.
291
     * @return array the mapping from file extensions to MIME types
292
     * @since 2.0.14
293
     */
294 19
    protected static function loadMimeAliases($aliasesFile)
295
    {
296 19
        if ($aliasesFile === null) {
297
            $aliasesFile = static::$mimeAliasesFile;
298
        }
299 19
        $aliasesFile = Yii::getAlias($aliasesFile);
300 19
        if (!isset(self::$_mimeAliases[$aliasesFile])) {
301 1
            self::$_mimeAliases[$aliasesFile] = require $aliasesFile;
302
        }
303
304 19
        return self::$_mimeAliases[$aliasesFile];
305
    }
306
307
    private static $_mimeExtensions = [];
308
309
    /**
310
     * Loads MIME extensions from the specified file.
311
     * @param string|null $extensionsFile the path (or alias) of the file that contains MIME type aliases.
312
     * If this is not set, the file specified by [[mimeAliasesFile]] will be used.
313
     * @return array the mapping from file extensions to MIME types
314
     * @since 2.0.48
315
     */
316 4
    protected static function loadMimeExtensions($extensionsFile)
317
    {
318 4
        if ($extensionsFile === null) {
319 4
            $extensionsFile = static::$mimeExtensionsFile;
320
        }
321 4
        $extensionsFile = Yii::getAlias($extensionsFile);
322 4
        if (!isset(self::$_mimeExtensions[$extensionsFile])) {
323 1
            self::$_mimeExtensions[$extensionsFile] = require $extensionsFile;
324
        }
325
326 4
        return self::$_mimeExtensions[$extensionsFile];
327
    }
328
329
    /**
330
     * Copies a whole directory as another one.
331
     * The files and sub-directories will also be copied over.
332
     * @param string $src the source directory
333
     * @param string $dst the destination directory
334
     * @param array $options options for directory copy. Valid options are:
335
     *
336
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
337
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment setting.
338
     * - filter: callback, a PHP callback that is called for each directory or file.
339
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
340
     *   The callback can return one of the following values:
341
     *
342
     *   * true: the directory or file will be copied (the "only" and "except" options will be ignored)
343
     *   * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored)
344
     *   * null: the "only" and "except" options will determine whether the directory or file should be copied
345
     *
346
     * - only: array, list of patterns that the file paths should match if they want to be copied.
347
     *   A path matches a pattern if it contains the pattern string at its end.
348
     *   For example, '.php' matches all file paths ending with '.php'.
349
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths.
350
     *   If a file path matches a pattern in both "only" and "except", it will NOT be copied.
351
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
352
     *   A path matches a pattern if it contains the pattern string at its end.
353
     *   Patterns ending with '/' apply to directory paths only, and patterns not ending with '/'
354
     *   apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b';
355
     *   and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches
356
     *   both '/' and '\' in the paths.
357
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
358
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
359
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
360
     *   If the callback returns false, the copy operation for the sub-directory or file will be cancelled.
361
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
362
     *   file to be copied from, while `$to` is the copy target.
363
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
364
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
365
     *   file copied from, while `$to` is the copy target.
366
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating directories
367
     *   that do not contain files. This affects directories that do not contain files initially as well as directories that
368
     *   do not contain files at the target destination because files have been filtered via `only` or `except`.
369
     *   Defaults to true. This option is available since version 2.0.12. Before 2.0.12 empty directories are always copied.
370
     * @throws InvalidArgumentException if unable to open directory
371
     */
372 17
    public static function copyDirectory($src, $dst, $options = [])
373
    {
374 17
        $src = static::normalizePath($src);
375 17
        $dst = static::normalizePath($dst);
376
377 17
        if ($src === $dst || strpos($dst, $src . DIRECTORY_SEPARATOR) === 0) {
378 2
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
379
        }
380 15
        $dstExists = is_dir($dst);
381 15
        if (!$dstExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
382 6
            static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
383 6
            $dstExists = true;
384
        }
385
386 15
        $handle = opendir($src);
387 15
        if ($handle === false) {
388
            throw new InvalidArgumentException("Unable to open directory: $src");
389
        }
390 15
        if (!isset($options['basePath'])) {
391
            // this should be done only once
392 15
            $options['basePath'] = realpath($src);
393 15
            $options = static::normalizeOptions($options);
394
        }
395 15
        while (($file = readdir($handle)) !== false) {
396 15
            if ($file === '.' || $file === '..') {
397 15
                continue;
398
            }
399 13
            $from = $src . DIRECTORY_SEPARATOR . $file;
400 13
            $to = $dst . DIRECTORY_SEPARATOR . $file;
401 13
            if (static::filterPath($from, $options)) {
402 13
                if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) {
403 2
                    continue;
404
                }
405 11
                if (is_file($from)) {
406 11
                    if (!$dstExists) {
407
                        // delay creation of destination directory until the first file is copied to avoid creating empty directories
408 5
                        static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
409 5
                        $dstExists = true;
410
                    }
411 11
                    copy($from, $to);
412 11
                    if (isset($options['fileMode'])) {
413 11
                        @chmod($to, $options['fileMode']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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

413
                        /** @scrutinizer ignore-unhandled */ @chmod($to, $options['fileMode']);

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...
414
                    }
415
                } else {
416
                    // recursive copy, defaults to true
417 8
                    if (!isset($options['recursive']) || $options['recursive']) {
418 7
                        static::copyDirectory($from, $to, $options);
419
                    }
420
                }
421 11
                if (isset($options['afterCopy'])) {
422
                    call_user_func($options['afterCopy'], $from, $to);
423
                }
424
            }
425
        }
426 15
        closedir($handle);
427 15
    }
428
429
    /**
430
     * Removes a directory (and all its content) recursively.
431
     *
432
     * @param string $dir the directory to be deleted recursively.
433
     * @param array $options options for directory remove. Valid options are:
434
     *
435
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
436
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
437
     *   Only symlink would be removed in that default case.
438
     *
439
     * @throws ErrorException in case of failure
440
     */
441 224
    public static function removeDirectory($dir, $options = [])
442
    {
443 224
        if (!is_dir($dir)) {
444 108
            return;
445
        }
446 222
        if (!empty($options['traverseSymlinks']) || !is_link($dir)) {
447 222
            if (!($handle = opendir($dir))) {
448
                return;
449
            }
450 222
            while (($file = readdir($handle)) !== false) {
451 222
                if ($file === '.' || $file === '..') {
452 222
                    continue;
453
                }
454 199
                $path = $dir . DIRECTORY_SEPARATOR . $file;
455 199
                if (is_dir($path)) {
456 76
                    static::removeDirectory($path, $options);
457
                } else {
458 177
                    static::unlink($path);
459
                }
460
            }
461 222
            closedir($handle);
462
        }
463 222
        if (is_link($dir)) {
464 2
            static::unlink($dir);
465
        } else {
466 222
            rmdir($dir);
467
        }
468 222
    }
469
470
    /**
471
     * Removes a file or symlink in a cross-platform way
472
     *
473
     * @param string $path
474
     * @return bool
475
     *
476
     * @since 2.0.14
477
     */
478 178
    public static function unlink($path)
479
    {
480 178
        $isWindows = DIRECTORY_SEPARATOR === '\\';
481
482 178
        if (!$isWindows) {
483 178
            return unlink($path);
484
        }
485
486
        if (is_link($path) && is_dir($path)) {
487
            return rmdir($path);
488
        }
489
490
        try {
491
            return unlink($path);
492
        } catch (ErrorException $e) {
493
            // last resort measure for Windows
494
            if (is_dir($path) && count(static::findFiles($path)) !== 0) {
495
                return false;
496
            }
497
            if (function_exists('exec') && file_exists($path)) {
498
                exec('DEL /F/Q ' . escapeshellarg($path));
499
500
                return !file_exists($path);
501
            }
502
503
            return false;
504
        }
505
    }
506
507
    /**
508
     * Returns the files found under the specified directory and subdirectories.
509
     * @param string $dir the directory under which the files will be looked for.
510
     * @param array $options options for file searching. Valid options are:
511
     *
512
     * - `filter`: callback, a PHP callback that is called for each directory or file.
513
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
514
     *   The callback can return one of the following values:
515
     *
516
     *   * `true`: the directory or file will be returned (the `only` and `except` options will be ignored)
517
     *   * `false`: the directory or file will NOT be returned (the `only` and `except` options will be ignored)
518
     *   * `null`: the `only` and `except` options will determine whether the directory or file should be returned
519
     *
520
     * - `except`: array, list of patterns excluding from the results matching file or directory paths.
521
     *   Patterns ending with slash ('/') apply to directory paths only, and patterns not ending with '/'
522
     *   apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b';
523
     *   and `.svn/` matches directory paths ending with `.svn`.
524
     *   If the pattern does not contain a slash (`/`), it is treated as a shell glob pattern
525
     *   and checked for a match against the pathname relative to `$dir`.
526
     *   Otherwise, the pattern is treated as a shell glob suitable for consumption by `fnmatch(3)`
527
     *   with the `FNM_PATHNAME` flag: wildcards in the pattern will not match a `/` in the pathname.
528
     *   For example, `views/*.php` matches `views/index.php` but not `views/controller/index.php`.
529
     *   A leading slash matches the beginning of the pathname. For example, `/*.php` matches `index.php` but not `views/start/index.php`.
530
     *   An optional prefix `!` which negates the pattern; any matching file excluded by a previous pattern will become included again.
531
     *   If a negated pattern matches, this will override lower precedence patterns sources. Put a backslash (`\`) in front of the first `!`
532
     *   for patterns that begin with a literal `!`, for example, `\!important!.txt`.
533
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths.
534
     *   You can find more details about the gitignore pattern format [here](https://git-scm.com/docs/gitignore/en#_pattern_format).
535
     * - `only`: array, list of patterns that the file paths should match if they are to be returned. Directory paths
536
     *   are not checked against them. Same pattern matching rules as in the `except` option are used.
537
     *   If a file path matches a pattern in both `only` and `except`, it will NOT be returned.
538
     * - `caseSensitive`: boolean, whether patterns specified at `only` or `except` should be case sensitive. Defaults to `true`.
539
     * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
540
     * @return array files found under the directory, in no particular order. Ordering depends on the files system used.
541
     * @throws InvalidArgumentException if the dir is invalid.
542
     */
543 167
    public static function findFiles($dir, $options = [])
544
    {
545 167
        $dir = self::clearDir($dir);
546 167
        $options = self::setBasePath($dir, $options);
547 167
        $list = [];
548 167
        $handle = self::openDir($dir);
549 167
        while (($file = readdir($handle)) !== false) {
550 167
            if ($file === '.' || $file === '..') {
551 167
                continue;
552
            }
553 164
            $path = $dir . DIRECTORY_SEPARATOR . $file;
554 164
            if (static::filterPath($path, $options)) {
555 164
                if (is_file($path)) {
556 162
                    $list[] = $path;
557 25
                } elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) {
558 24
                    $list = array_merge($list, static::findFiles($path, $options));
559
                }
560
            }
561
        }
562 167
        closedir($handle);
563
564 167
        return $list;
565
    }
566
567
    /**
568
     * Returns the directories found under the specified directory and subdirectories.
569
     * @param string $dir the directory under which the files will be looked for.
570
     * @param array $options options for directory searching. Valid options are:
571
     *
572
     * - `filter`: callback, a PHP callback that is called for each directory or file.
573
     *   The signature of the callback should be: `function (string $path): bool`, where `$path` refers
574
     *   the full path to be filtered. The callback can return one of the following values:
575
     *
576
     *   * `true`: the directory will be returned
577
     *   * `false`: the directory will NOT be returned
578
     *
579
     * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
580
     *   See [[findFiles()]] for more options.
581
     * @return array directories found under the directory, in no particular order. Ordering depends on the files system used.
582
     * @throws InvalidArgumentException if the dir is invalid.
583
     * @since 2.0.14
584
     */
585 1
    public static function findDirectories($dir, $options = [])
586
    {
587 1
        $dir = self::clearDir($dir);
588 1
        $options = self::setBasePath($dir, $options);
589 1
        $list = [];
590 1
        $handle = self::openDir($dir);
591 1
        while (($file = readdir($handle)) !== false) {
592 1
            if ($file === '.' || $file === '..') {
593 1
                continue;
594
            }
595 1
            $path = $dir . DIRECTORY_SEPARATOR . $file;
596 1
            if (is_dir($path) && static::filterPath($path, $options)) {
597 1
                $list[] = $path;
598 1
                if (!isset($options['recursive']) || $options['recursive']) {
599 1
                    $list = array_merge($list, static::findDirectories($path, $options));
600
                }
601
            }
602
        }
603 1
        closedir($handle);
604
605 1
        return $list;
606
    }
607
608
    /**
609
     * @param string $dir
610
     * @param array $options
611
     * @return array
612
     */
613 168
    private static function setBasePath($dir, $options)
614
    {
615 168
        if (!isset($options['basePath'])) {
616
            // this should be done only once
617 168
            $options['basePath'] = realpath($dir);
618 168
            $options = static::normalizeOptions($options);
619
        }
620
621 168
        return $options;
622
    }
623
624
    /**
625
     * @param string $dir
626
     * @return resource
627
     * @throws InvalidArgumentException if unable to open directory
628
     */
629 168
    private static function openDir($dir)
630
    {
631 168
        $handle = opendir($dir);
632 168
        if ($handle === false) {
633
            throw new InvalidArgumentException("Unable to open directory: $dir");
634
        }
635 168
        return $handle;
636
    }
637
638
    /**
639
     * @param string $dir
640
     * @return string
641
     * @throws InvalidArgumentException if directory not exists
642
     */
643 168
    private static function clearDir($dir)
644
    {
645 168
        if (!is_dir($dir)) {
646
            throw new InvalidArgumentException("The dir argument must be a directory: $dir");
647
        }
648 168
        return rtrim($dir, '\/');
649
    }
650
651
    /**
652
     * Checks if the given file path satisfies the filtering options.
653
     * @param string $path the path of the file or directory to be checked
654
     * @param array $options the filtering options. See [[findFiles()]] for explanations of
655
     * the supported options.
656
     * @return bool whether the file or directory satisfies the filtering options.
657
     */
658 174
    public static function filterPath($path, $options)
659
    {
660 174
        if (isset($options['filter'])) {
661 2
            $result = call_user_func($options['filter'], $path);
662 2
            if (is_bool($result)) {
663 2
                return $result;
664
            }
665
        }
666
667 173
        if (empty($options['except']) && empty($options['only'])) {
668 106
            return true;
669
        }
670
671 76
        $path = str_replace('\\', '/', $path);
672
673
        if (
674 76
            !empty($options['except'])
675 76
            && ($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null
676
        ) {
677 3
            return $except['flags'] & self::PATTERN_NEGATIVE;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $except['flags'] & self::PATTERN_NEGATIVE returns the type integer which is incompatible with the documented return type boolean.
Loading history...
678
        }
679
680 76
        if (!empty($options['only']) && !is_dir($path)) {
681 74
            return self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only']) !== null;
682
        }
683
684 20
        return true;
685
    }
686
687
    /**
688
     * Creates a new directory.
689
     *
690
     * This method is similar to the PHP `mkdir()` function except that
691
     * it uses `chmod()` to set the permission of the created directory
692
     * in order to avoid the impact of the `umask` setting.
693
     *
694
     * @param string $path path of the directory to be created.
695
     * @param int $mode the permission to be set for the created directory.
696
     * @param bool $recursive whether to create parent directories if they do not exist.
697
     * @return bool whether the directory is created successfully
698
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
699
     */
700 277
    public static function createDirectory($path, $mode = 0775, $recursive = true)
701
    {
702 277
        if (is_dir($path)) {
703 166
            return true;
704
        }
705 252
        $parentDir = dirname($path);
706
        // recurse if parent dir does not exist and we are not at the root of the file system.
707 252
        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {
708 5
            static::createDirectory($parentDir, $mode, true);
709
        }
710
        try {
711 252
            if (!mkdir($path, $mode)) {
712 252
                return false;
713
            }
714
        } catch (\Exception $e) {
715
            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288
716
                throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
717
            }
718
        }
719
        try {
720 252
            return chmod($path, $mode);
721
        } catch (\Exception $e) {
722
            throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
723
        }
724
    }
725
726
    /**
727
     * Performs a simple comparison of file or directory names.
728
     *
729
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
730
     *
731
     * @param string $baseName file or directory name to compare with the pattern
732
     * @param string $pattern the pattern that $baseName will be compared against
733
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
734
     * @param int $flags pattern flags
735
     * @return bool whether the name matches against pattern
736
     */
737 75
    private static function matchBasename($baseName, $pattern, $firstWildcard, $flags)
738
    {
739 75
        if ($firstWildcard === false) {
740 11
            if ($pattern === $baseName) {
741 11
                return true;
742
            }
743 64
        } elseif ($flags & self::PATTERN_ENDSWITH) {
744
            /* "*literal" matching against "fooliteral" */
745 63
            $n = StringHelper::byteLength($pattern);
746 63
            if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) {
747
                return true;
748
            }
749
        }
750
751 75
        $matchOptions = [];
752 75
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
753 1
            $matchOptions['caseSensitive'] = false;
754
        }
755
756 75
        return StringHelper::matchWildcard($pattern, $baseName, $matchOptions);
757
    }
758
759
    /**
760
     * Compares a path part against a pattern with optional wildcards.
761
     *
762
     * Based on match_pathname() from dir.c of git 1.8.5.3 sources.
763
     *
764
     * @param string $path full path to compare
765
     * @param string $basePath base of path that will not be compared
766
     * @param string $pattern the pattern that path part will be compared against
767
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
768
     * @param int $flags pattern flags
769
     * @return bool whether the path part matches against pattern
770
     */
771 57
    private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags)
772
    {
773
        // match with FNM_PATHNAME; the pattern has base implicitly in front of it.
774 57
        if (strncmp($pattern, '/', 1) === 0) {
775 54
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
776 54
            if ($firstWildcard !== false && $firstWildcard !== 0) {
777 54
                $firstWildcard--;
778
            }
779
        }
780
781 57
        $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
782 57
        $name = StringHelper::byteSubstr($path, -$namelen, $namelen);
783
784 57
        if ($firstWildcard !== 0) {
785 57
            if ($firstWildcard === false) {
786 56
                $firstWildcard = StringHelper::byteLength($pattern);
787
            }
788
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
789 57
            if ($firstWildcard > $namelen) {
790 3
                return false;
791
            }
792
793 57
            if (strncmp($pattern, $name, $firstWildcard)) {
0 ignored issues
show
Bug introduced by
It seems like $firstWildcard can also be of type true; however, parameter $length of strncmp() 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

793
            if (strncmp($pattern, $name, /** @scrutinizer ignore-type */ $firstWildcard)) {
Loading history...
794 57
                return false;
795
            }
796 4
            $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern));
0 ignored issues
show
Bug introduced by
It seems like $firstWildcard can also be of type true; however, parameter $start of yii\helpers\BaseStringHelper::byteSubstr() 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

796
            $pattern = StringHelper::byteSubstr($pattern, /** @scrutinizer ignore-type */ $firstWildcard, StringHelper::byteLength($pattern));
Loading history...
797 4
            $name = StringHelper::byteSubstr($name, $firstWildcard, $namelen);
798
799
            // If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all.
800 4
            if (empty($pattern) && empty($name)) {
801 3
                return true;
802
            }
803
        }
804
805
        $matchOptions = [
806 2
            'filePath' => true
807
        ];
808 2
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
809
            $matchOptions['caseSensitive'] = false;
810
        }
811
812 2
        return StringHelper::matchWildcard($pattern, $name, $matchOptions);
813
    }
814
815
    /**
816
     * Scan the given exclude list in reverse to see whether pathname
817
     * should be ignored.  The first match (i.e. the last on the list), if
818
     * any, determines the fate.  Returns the element which
819
     * matched, or null for undecided.
820
     *
821
     * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
822
     *
823
     * @param string $basePath
824
     * @param string $path
825
     * @param array $excludes list of patterns to match $path against
826
     * @return array|null null or one of $excludes item as an array with keys: 'pattern', 'flags'
827
     * @throws InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern, flags, firstWildcard.
828
     */
829 76
    private static function lastExcludeMatchingFromList($basePath, $path, $excludes)
830
    {
831 76
        foreach (array_reverse($excludes) as $exclude) {
832 76
            if (is_string($exclude)) {
833
                $exclude = self::parseExcludePattern($exclude, false);
834
            }
835 76
            if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) {
836
                throw new InvalidArgumentException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.');
837
            }
838 76
            if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) {
839
                continue;
840
            }
841
842 76
            if ($exclude['flags'] & self::PATTERN_NODIR) {
843 75
                if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
844 73
                    return $exclude;
845
                }
846 74
                continue;
847
            }
848
849 57
            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
850 4
                return $exclude;
851
            }
852
        }
853
854 75
        return null;
855
    }
856
857
    /**
858
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
859
     * @param string $pattern
860
     * @param bool $caseSensitive
861
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
862
     * @throws InvalidArgumentException
863
     */
864 82
    private static function parseExcludePattern($pattern, $caseSensitive)
865
    {
866 82
        if (!is_string($pattern)) {
0 ignored issues
show
introduced by
The condition is_string($pattern) is always true.
Loading history...
867
            throw new InvalidArgumentException('Exclude/include pattern must be a string.');
868
        }
869
870
        $result = [
871 82
            'pattern' => $pattern,
872 82
            'flags' => 0,
873
            'firstWildcard' => false,
874
        ];
875
876 82
        if (!$caseSensitive) {
877 1
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
878
        }
879
880 82
        if (empty($pattern)) {
881
            return $result;
882
        }
883
884 82
        if (strncmp($pattern, '!', 1) === 0) {
885 1
            $result['flags'] |= self::PATTERN_NEGATIVE;
886 1
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
887
        }
888 82
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstr($pattern, -1, 1) === '/') {
889
            $pattern = StringHelper::byteSubstr($pattern, 0, -1);
890
            $result['flags'] |= self::PATTERN_MUSTBEDIR;
891
        }
892 82
        if (strpos($pattern, '/') === false) {
893 81
            $result['flags'] |= self::PATTERN_NODIR;
894
        }
895 82
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
896 82
        if (strncmp($pattern, '*', 1) === 0 && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) {
897 69
            $result['flags'] |= self::PATTERN_ENDSWITH;
898
        }
899 82
        $result['pattern'] = $pattern;
900
901 82
        return $result;
902
    }
903
904
    /**
905
     * Searches for the first wildcard character in the pattern.
906
     * @param string $pattern the pattern to search in
907
     * @return int|bool position of first wildcard character or false if not found
908
     */
909 82
    private static function firstWildcardInPattern($pattern)
910
    {
911 82
        $wildcards = ['*', '?', '[', '\\'];
912 82
        $wildcardSearch = function ($r, $c) use ($pattern) {
913 82
            $p = strpos($pattern, $c);
914
915 82
            return $r === false ? $p : ($p === false ? $r : min($r, $p));
916 82
        };
917
918 82
        return array_reduce($wildcards, $wildcardSearch, false);
919
    }
920
921
    /**
922
     * @param array $options raw options
923
     * @return array normalized options
924
     * @since 2.0.12
925
     */
926 179
    protected static function normalizeOptions(array $options)
927
    {
928 179
        if (!array_key_exists('caseSensitive', $options)) {
929 178
            $options['caseSensitive'] = true;
930
        }
931 179
        if (isset($options['except'])) {
932 62
            foreach ($options['except'] as $key => $value) {
933 62
                if (is_string($value)) {
934 62
                    $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
935
                }
936
            }
937
        }
938 179
        if (isset($options['only'])) {
939 80
            foreach ($options['only'] as $key => $value) {
940 80
                if (is_string($value)) {
941 80
                    $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
942
                }
943
            }
944
        }
945
946 179
        return $options;
947
    }
948
949
    /**
950
     * Changes the Unix user and/or group ownership of a file or directory, and optionally the mode.
951
     * Note: This function will not work on remote files as the file to be examined must be accessible
952
     * via the server's filesystem.
953
     * Note: On Windows, this function fails silently when applied on a regular file.
954
     * @param string $path the path to the file or directory.
955
     * @param string|array|int|null $ownership the user and/or group ownership for the file or directory.
956
     * When $ownership is a string, the format is 'user:group' where both are optional. E.g.
957
     * 'user' or 'user:' will only change the user,
958
     * ':group' will only change the group,
959
     * 'user:group' will change both.
960
     * When $owners is an index array the format is [0 => user, 1 => group], e.g. `[$myUser, $myGroup]`.
961
     * It is also possible to pass an associative array, e.g. ['user' => $myUser, 'group' => $myGroup].
962
     * In case $owners is an integer it will be used as user id.
963
     * If `null`, an empty array or an empty string is passed, the ownership will not be changed.
964
     * @param int|null $mode the permission to be set for the file or directory.
965
     * If `null` is passed, the mode will not be changed.
966
     *
967
     * @since 2.0.43
968
     */
969 90
    public static function changeOwnership($path, $ownership, $mode = null)
970
    {
971 90
        if (!file_exists((string)$path)) {
972 1
            throw new InvalidArgumentException('Unable to change ownership, "' . $path . '" is not a file or directory.');
973
        }
974
975 89
        if (empty($ownership) && $ownership !== 0 && $mode === null) {
976 84
            return;
977
        }
978
979 5
        $user = $group = null;
980 5
        if (!empty($ownership) || $ownership === 0 || $ownership === '0') {
981 4
            if (is_int($ownership)) {
982
                $user = $ownership;
983 4
            } elseif (is_string($ownership)) {
984 1
                $ownerParts = explode(':', $ownership);
985 1
                $user = $ownerParts[0];
986 1
                if (count($ownerParts) > 1) {
987 1
                    $group = $ownerParts[1];
988
                }
989 3
            } elseif (is_array($ownership)) {
0 ignored issues
show
introduced by
The condition is_array($ownership) is always true.
Loading history...
990 2
                $ownershipIsIndexed = ArrayHelper::isIndexed($ownership);
991 2
                $user = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 0 : 'user');
992 2
                $group = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 1 : 'group');
993
            } else {
994 1
                throw new InvalidArgumentException('$ownership must be an integer, string, array, or null.');
995
            }
996
        }
997
998 4
        if ($mode !== null) {
999 1
            if (!is_int($mode)) {
0 ignored issues
show
introduced by
The condition is_int($mode) is always true.
Loading history...
1000 1
                throw new InvalidArgumentException('$mode must be an integer or null.');
1001
            }
1002
            if (!chmod($path, $mode)) {
1003
                throw new Exception('Unable to change mode of "' . $path . '" to "0' . decoct($mode) . '".');
1004
            }
1005
        }
1006 3
        if ($user !== null && $user !== '') {
1007 2
            if (is_numeric($user)) {
1008
                $user = (int) $user;
1009 2
            } elseif (!is_string($user)) {
1010 1
                throw new InvalidArgumentException('The user part of $ownership must be an integer, string, or null.');
1011
            }
1012 1
            if (!chown($path, $user)) {
1013
                throw new Exception('Unable to change user ownership of "' . $path . '" to "' . $user . '".');
1014
            }
1015
        }
1016 1
        if ($group !== null && $group !== '') {
1017 1
            if (is_numeric($group)) {
1018
                $group = (int) $group;
1019 1
            } elseif (!is_string($group)) {
1020 1
                throw new InvalidArgumentException('The group part of $ownership must be an integer, string or null.');
1021
            }
1022
            if (!chgrp($path, $group)) {
1023
                throw new Exception('Unable to change group ownership of "' . $path . '" to "' . $group . '".');
1024
            }
1025
        }
1026
    }
1027
}
1028