Issues (50)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/helpers/FileHelper.php (11 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
4
namespace carono\janitor\helpers;
5
6
7
use ErrorException;
8
use InvalidArgumentException;
9
10
class FileHelper
11
{
12
    const PATTERN_NODIR = 1;
13
    const PATTERN_ENDSWITH = 4;
14
    const PATTERN_MUSTBEDIR = 8;
15
    const PATTERN_NEGATIVE = 16;
16
    const PATTERN_CASE_INSENSITIVE = 32;
17
18
    /**
19
     * Normalizes a file/directory path.
20
     *
21
     * The normalization does the following work:
22
     *
23
     * - Convert all directory separators into `DIRECTORY_SEPARATOR` (e.g. "\a/b\c" becomes "/a/b/c")
24
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
25
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
26
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
27
     *
28
     * Note: For registered stream wrappers, the consecutive slashes rule
29
     * and ".."/"." translations are skipped.
30
     *
31
     * @param string $path the file/directory path to be normalized
32
     * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
33
     * @return string the normalized file/directory path
34
     */
35
    public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
36
    {
37
        $path = rtrim(strtr($path, '/\\', $ds . $ds), $ds);
38
        if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) {
39
            return $path;
40
        }
41
        // fix #17235 stream wrappers
42
        foreach (stream_get_wrappers() as $protocol) {
43
            if (strpos($path, "{$protocol}://") === 0) {
44
                return $path;
45
            }
46
        }
47
        // the path may contain ".", ".." or double slashes, need to clean them up
48
        if (strpos($path, "{$ds}{$ds}") === 0 && $ds == '\\') {
49
            $parts = [$ds];
50
        } else {
51
            $parts = [];
52
        }
53
        foreach (explode($ds, $path) as $part) {
54
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
55
                array_pop($parts);
56
            } elseif ($part === '.' || $part === '' && !empty($parts)) {
57
                continue;
58
            } else {
59
                $parts[] = $part;
60
            }
61
        }
62
        $path = implode($ds, $parts);
63
        return $path === '' ? '.' : $path;
64
    }
65
66
    /**
67
     * Copies a whole directory as another one.
68
     * The files and sub-directories will also be copied over.
69
     *
70
     * @param string $src the source directory
71
     * @param string $dst the destination directory
72
     * @param array $options options for directory copy. Valid options are:
73
     *
74
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
75
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment setting.
76
     * - filter: callback, a PHP callback that is called for each directory or file.
77
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
78
     *   The callback can return one of the following values:
79
     *
80
     *   * true: the directory or file will be copied (the "only" and "except" options will be ignored)
81
     *   * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored)
82
     *   * null: the "only" and "except" options will determine whether the directory or file should be copied
83
     *
84
     * - only: array, list of patterns that the file paths should match if they want to be copied.
85
     *   A path matches a pattern if it contains the pattern string at its end.
86
     *   For example, '.php' matches all file paths ending with '.php'.
87
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths.
88
     *   If a file path matches a pattern in both "only" and "except", it will NOT be copied.
89
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
90
     *   A path matches a pattern if it contains the pattern string at its end.
91
     *   Patterns ending with '/' apply to directory paths only, and patterns not ending with '/'
92
     *   apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b';
93
     *   and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches
94
     *   both '/' and '\' in the paths.
95
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
96
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
97
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
98
     *   If the callback returns false, the copy operation for the sub-directory or file will be cancelled.
99
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
100
     *   file to be copied from, while `$to` is the copy target.
101
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
102
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
103
     *   file copied from, while `$to` is the copy target.
104
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating directories
105
     *   that do not contain files. This affects directories that do not contain files initially as well as directories that
106
     *   do not contain files at the target destination because files have been filtered via `only` or `except`.
107
     *   Defaults to true. This option is available since version 2.0.12. Before 2.0.12 empty directories are always copied.
108
     * @throws InvalidArgumentException if unable to open directory
109
     */
110
    public static function copyDirectory($src, $dst, $options = [])
111
    {
112
        $src = static::normalizePath($src);
113
        $dst = static::normalizePath($dst);
114
115
        if ($src === $dst || strpos($dst, $src . DIRECTORY_SEPARATOR) === 0) {
116
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
117
        }
118
        $dstExists = is_dir($dst);
119
        if (!$dstExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
120
            static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
121
            $dstExists = true;
122
        }
123
124
        $handle = opendir($src);
125
        if ($handle === false) {
126
            throw new InvalidArgumentException("Unable to open directory: $src");
127
        }
128 View Code Duplication
        if (!isset($options['basePath'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
            // this should be done only once
130
            $options['basePath'] = realpath($src);
131
            $options = static::normalizeOptions($options);
132
        }
133
        while (($file = readdir($handle)) !== false) {
134
            if ($file === '.' || $file === '..') {
135
                continue;
136
            }
137
            $from = $src . DIRECTORY_SEPARATOR . $file;
138
            $to = $dst . DIRECTORY_SEPARATOR . $file;
139
            if (static::filterPath($from, $options)) {
140
                if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) {
141
                    continue;
142
                }
143
                if (is_file($from)) {
144
                    if (!$dstExists) {
145
                        // delay creation of destination directory until the first file is copied to avoid creating empty directories
146
                        static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
147
                        $dstExists = true;
148
                    }
149
                    copy($from, $to);
150
                    if (isset($options['fileMode'])) {
151
                        @chmod($to, $options['fileMode']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
152
                    }
153
                } else {
154
                    // recursive copy, defaults to true
155
                    if (!isset($options['recursive']) || $options['recursive']) {
156
                        static::copyDirectory($from, $to, $options);
157
                    }
158
                }
159
                if (isset($options['afterCopy'])) {
160
                    call_user_func($options['afterCopy'], $from, $to);
161
                }
162
            }
163
        }
164
        closedir($handle);
165
    }
166
167
    /**
168
     * Removes a directory (and all its content) recursively.
169
     *
170
     * @param string $dir the directory to be deleted recursively.
171
     * @param array $options options for directory remove. Valid options are:
172
     *
173
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
174
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
175
     *   Only symlink would be removed in that default case.
176
     *
177
     * @throws ErrorException in case of failure
178
     */
179
    public static function removeDirectory($dir, $options = [])
180
    {
181
        if (!is_dir($dir)) {
182
            return;
183
        }
184
        if (!empty($options['traverseSymlinks']) || !is_link($dir)) {
185
            if (!($handle = opendir($dir))) {
186
                return;
187
            }
188
            while (($file = readdir($handle)) !== false) {
189
                if ($file === '.' || $file === '..') {
190
                    continue;
191
                }
192
                $path = $dir . DIRECTORY_SEPARATOR . $file;
193
                if (is_dir($path)) {
194
                    static::removeDirectory($path, $options);
195
                } else {
196
                    static::unlink($path);
197
                }
198
            }
199
            closedir($handle);
200
        }
201
        if (is_link($dir)) {
202
            static::unlink($dir);
203
        } else {
204
            rmdir($dir);
205
        }
206
    }
207
208
    /**
209
     * Removes a file or symlink in a cross-platform way
210
     *
211
     * @param string $path
212
     * @return bool
213
     *
214
     * @since 2.0.14
215
     */
216
    public static function unlink($path)
217
    {
218
        $isWindows = DIRECTORY_SEPARATOR === '\\';
219
220
        if (!$isWindows) {
221
            return unlink($path);
222
        }
223
224
        if (is_link($path) && is_dir($path)) {
225
            return rmdir($path);
226
        }
227
228
        try {
229
            return unlink($path);
230
        } catch (ErrorException $e) {
231
            // last resort measure for Windows
232
            if (function_exists('exec') && file_exists($path)) {
233
                exec('DEL /F/Q ' . escapeshellarg($path));
234
235
                return !file_exists($path);
236
            }
237
238
            return false;
239
        }
240
    }
241
242
    /**
243
     * Returns the files found under the specified directory and subdirectories.
244
     *
245
     * @param string $dir the directory under which the files will be looked for.
246
     * @param array $options options for file searching. Valid options are:
247
     *
248
     * - `filter`: callback, a PHP callback that is called for each directory or file.
249
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
250
     *   The callback can return one of the following values:
251
     *
252
     *   * `true`: the directory or file will be returned (the `only` and `except` options will be ignored)
253
     *   * `false`: the directory or file will NOT be returned (the `only` and `except` options will be ignored)
254
     *   * `null`: the `only` and `except` options will determine whether the directory or file should be returned
255
     *
256
     * - `except`: array, list of patterns excluding from the results matching file or directory paths.
257
     *   Patterns ending with slash ('/') apply to directory paths only, and patterns not ending with '/'
258
     *   apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b';
259
     *   and `.svn/` matches directory paths ending with `.svn`.
260
     *   If the pattern does not contain a slash (`/`), it is treated as a shell glob pattern
261
     *   and checked for a match against the pathname relative to `$dir`.
262
     *   Otherwise, the pattern is treated as a shell glob suitable for consumption by `fnmatch(3)`
263
     *   with the `FNM_PATHNAME` flag: wildcards in the pattern will not match a `/` in the pathname.
264
     *   For example, `views/*.php` matches `views/index.php` but not `views/controller/index.php`.
265
     *   A leading slash matches the beginning of the pathname. For example, `/*.php` matches `index.php` but not `views/start/index.php`.
266
     *   An optional prefix `!` which negates the pattern; any matching file excluded by a previous pattern will become included again.
267
     *   If a negated pattern matches, this will override lower precedence patterns sources. Put a backslash (`\`) in front of the first `!`
268
     *   for patterns that begin with a literal `!`, for example, `\!important!.txt`.
269
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths.
270
     * - `only`: array, list of patterns that the file paths should match if they are to be returned. Directory paths
271
     *   are not checked against them. Same pattern matching rules as in the `except` option are used.
272
     *   If a file path matches a pattern in both `only` and `except`, it will NOT be returned.
273
     * - `caseSensitive`: boolean, whether patterns specified at `only` or `except` should be case sensitive. Defaults to `true`.
274
     * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
275
     * @return array files found under the directory, in no particular order. Ordering depends on the files system used.
276
     * @throws InvalidArgumentException if the dir is invalid.
277
     */
278 View Code Duplication
    public static function findFiles($dir, $options = [])
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
    {
280
        $dir = self::clearDir($dir);
281
        $options = self::setBasePath($dir, $options);
282
        $list = [];
283
        $handle = self::openDir($dir);
284
        while (($file = readdir($handle)) !== false) {
285
            if ($file === '.' || $file === '..') {
286
                continue;
287
            }
288
            $path = $dir . DIRECTORY_SEPARATOR . $file;
289
            if (static::filterPath($path, $options)) {
290
                if (is_file($path)) {
291
                    $list[] = $path;
292
                } elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) {
293
                    $list = array_merge($list, static::findFiles($path, $options));
294
                }
295
            }
296
        }
297
        closedir($handle);
298
299
        return $list;
300
    }
301
302
    /**
303
     * Returns the directories found under the specified directory and subdirectories.
304
     *
305
     * @param string $dir the directory under which the files will be looked for.
306
     * @param array $options options for directory searching. Valid options are:
307
     *
308
     * - `filter`: callback, a PHP callback that is called for each directory or file.
309
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
310
     *   The callback can return one of the following values:
311
     *
312
     *   * `true`: the directory will be returned
313
     *   * `false`: the directory will NOT be returned
314
     *
315
     * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
316
     * @return array directories found under the directory, in no particular order. Ordering depends on the files system used.
317
     * @throws InvalidArgumentException if the dir is invalid.
318
     * @since 2.0.14
319
     */
320 View Code Duplication
    public static function findDirectories($dir, $options = [])
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
    {
322
        $dir = self::clearDir($dir);
323
        $options = self::setBasePath($dir, $options);
324
        $list = [];
325
        $handle = self::openDir($dir);
326
        while (($file = readdir($handle)) !== false) {
327
            if ($file === '.' || $file === '..') {
328
                continue;
329
            }
330
            $path = $dir . DIRECTORY_SEPARATOR . $file;
331
            if (is_dir($path) && static::filterPath($path, $options)) {
332
                $list[] = $path;
333
                if (!isset($options['recursive']) || $options['recursive']) {
334
                    $list = array_merge($list, static::findDirectories($path, $options));
335
                }
336
            }
337
        }
338
        closedir($handle);
339
340
        return $list;
341
    }
342
343
    /**
344
     * @param string $dir
345
     */
346
    private static function setBasePath($dir, $options)
347
    {
348 View Code Duplication
        if (!isset($options['basePath'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
349
            // this should be done only once
350
            $options['basePath'] = realpath($dir);
351
            $options = static::normalizeOptions($options);
352
        }
353
354
        return $options;
355
    }
356
357
    /**
358
     * @param string $dir
359
     */
360
    private static function openDir($dir)
361
    {
362
        $handle = opendir($dir);
363
        if ($handle === false) {
364
            throw new InvalidArgumentException("Unable to open directory: $dir");
365
        }
366
        return $handle;
367
    }
368
369
    /**
370
     * @param string $dir
371
     */
372
    private static function clearDir($dir)
373
    {
374
        if (!is_dir($dir)) {
375
            throw new InvalidArgumentException("The dir argument must be a directory: $dir");
376
        }
377
        return rtrim($dir, DIRECTORY_SEPARATOR);
378
    }
379
380
    /**
381
     * Checks if the given file path satisfies the filtering options.
382
     *
383
     * @param string $path the path of the file or directory to be checked
384
     * @param array $options the filtering options. See [[findFiles()]] for explanations of
385
     * the supported options.
386
     * @return bool whether the file or directory satisfies the filtering options.
387
     */
388
    public static function filterPath($path, $options)
389
    {
390
        if (isset($options['filter'])) {
391
            $result = call_user_func($options['filter'], $path);
392
            if (is_bool($result)) {
393
                return $result;
394
            }
395
        }
396
397
        if (empty($options['except']) && empty($options['only'])) {
398
            return true;
399
        }
400
401
        $path = str_replace('\\', '/', $path);
402
403 View Code Duplication
        if (!empty($options['except'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
            if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null) {
405
                return $except['flags'] & self::PATTERN_NEGATIVE;
406
            }
407
        }
408
409
        if (!empty($options['only']) && !is_dir($path)) {
410 View Code Duplication
            if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only'])) !== null) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
411
                // don't check PATTERN_NEGATIVE since those entries are not prefixed with !
412
                return true;
413
            }
414
415
            return false;
416
        }
417
418
        return true;
419
    }
420
421
422
    /**
423
     * @param $name
424
     * @return string
425
     */
426
    public static function prepareFileName($name)
427
    {
428
        $replaceWithLast = [
429
            '720p',
430
            '1080p',
431
            'x264',
432
            'NewStudio',
433
            'Zuich32',
434
            'LostFilm',
435
            'WEBRip',
436
            'WEB-DL',
437
            'WEB',
438
            'BDRip',
439
            'HDRip',
440
            '\bD\b',
441
            '\bP\b',
442
            '\bTS\b',
443
            'Kuraj-Bambey',
444
            '\bRUS\b',
445
            'Jaskier'
446
        ];
447
        $replace = [
448
            '.' => ' ',
449
            '_' => ' '
450
        ];
451
        $name = pathinfo($name, PATHINFO_FILENAME);
452
        $name = trim(strtr($name, $replace));
453
        foreach ($replaceWithLast as $pattern) {
454
            $name = preg_replace("/$pattern.+/ui", '', $name);
455
        }
456
        $name = preg_replace('/\ss\d+/i', ' ', $name);
457
        $name = preg_replace('/e\d+/i', ' ', $name);
458
        return trim($name);
459
    }
460
461
    /**
462
     * Creates a new directory.
463
     *
464
     * This method is similar to the PHP `mkdir()` function except that
465
     * it uses `chmod()` to set the permission of the created directory
466
     * in order to avoid the impact of the `umask` setting.
467
     *
468
     * @param string $path path of the directory to be created.
469
     * @param int $mode the permission to be set for the created directory.
470
     * @param bool $recursive whether to create parent directories if they do not exist.
471
     * @return bool whether the directory is created successfully
472
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
473
     */
474
    public static function createDirectory($path, $mode = 0775, $recursive = true)
475
    {
476
        if (is_dir($path)) {
477
            return true;
478
        }
479
        $parentDir = dirname($path);
480
        // recurse if parent dir does not exist and we are not at the root of the file system.
481
        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {
482
            static::createDirectory($parentDir, $mode, true);
483
        }
484
        try {
485
            if (!mkdir($path, $mode)) {
486
                return false;
487
            }
488
        } catch (\Exception $e) {
489
            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288
490
                throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
491
            }
492
        }
493
        try {
494
            return chmod($path, $mode);
495
        } catch (\Exception $e) {
496
            throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
497
        }
498
    }
499
500
    /**
501
     * Performs a simple comparison of file or directory names.
502
     *
503
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
504
     *
505
     * @param string $baseName file or directory name to compare with the pattern
506
     * @param string $pattern the pattern that $baseName will be compared against
507
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
508
     * @param int $flags pattern flags
509
     * @return bool whether the name matches against pattern
510
     */
511
    private static function matchBasename($baseName, $pattern, $firstWildcard, $flags)
512
    {
513
        if ($firstWildcard === false) {
514
            if ($pattern === $baseName) {
515
                return true;
516
            }
517
        } elseif ($flags & self::PATTERN_ENDSWITH) {
518
            /* "*literal" matching against "fooliteral" */
519
            $n = StringHelper::byteLength($pattern);
520
            if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) {
521
                return true;
522
            }
523
        }
524
525
        $matchOptions = [];
526
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
527
            $matchOptions['caseSensitive'] = false;
528
        }
529
530
        return StringHelper::matchWildcard($pattern, $baseName, $matchOptions);
531
    }
532
533
    /**
534
     * Compares a path part against a pattern with optional wildcards.
535
     *
536
     * Based on match_pathname() from dir.c of git 1.8.5.3 sources.
537
     *
538
     * @param string $path full path to compare
539
     * @param string $basePath base of path that will not be compared
540
     * @param string $pattern the pattern that path part will be compared against
541
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
542
     * @param int $flags pattern flags
543
     * @return bool whether the path part matches against pattern
544
     */
545
    private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags)
546
    {
547
        // match with FNM_PATHNAME; the pattern has base implicitly in front of it.
548
        if (isset($pattern[0]) && $pattern[0] === '/') {
549
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
550
            if ($firstWildcard !== false && $firstWildcard !== 0) {
551
                $firstWildcard--;
552
            }
553
        }
554
555
        $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
556
        $name = StringHelper::byteSubstr($path, -$namelen, $namelen);
557
558
        if ($firstWildcard !== 0) {
559
            if ($firstWildcard === false) {
560
                $firstWildcard = StringHelper::byteLength($pattern);
561
            }
562
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
563
            if ($firstWildcard > $namelen) {
564
                return false;
565
            }
566
567
            if (strncmp($pattern, $name, $firstWildcard)) {
568
                return false;
569
            }
570
            $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern));
0 ignored issues
show
It seems like $firstWildcard can also be of type boolean; however, carono\janitor\helpers\StringHelper::byteSubstr() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
571
            $name = StringHelper::byteSubstr($name, $firstWildcard, $namelen);
0 ignored issues
show
It seems like $firstWildcard can also be of type boolean; however, carono\janitor\helpers\StringHelper::byteSubstr() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
572
573
            // 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.
574
            if (empty($pattern) && empty($name)) {
575
                return true;
576
            }
577
        }
578
579
        $matchOptions = [
580
            'filePath' => true
581
        ];
582
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
583
            $matchOptions['caseSensitive'] = false;
584
        }
585
586
        return StringHelper::matchWildcard($pattern, $name, $matchOptions);
587
    }
588
589
    /**
590
     * Scan the given exclude list in reverse to see whether pathname
591
     * should be ignored.  The first match (i.e. the last on the list), if
592
     * any, determines the fate.  Returns the element which
593
     * matched, or null for undecided.
594
     *
595
     * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
596
     *
597
     * @param string $basePath
598
     * @param string $path
599
     * @param array $excludes list of patterns to match $path against
600
     * @return array|null null or one of $excludes item as an array with keys: 'pattern', 'flags'
601
     * @throws InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern, flags, firstWildcard.
602
     */
603
    private static function lastExcludeMatchingFromList($basePath, $path, $excludes)
604
    {
605
        foreach (array_reverse($excludes) as $exclude) {
606
            if (is_string($exclude)) {
607
                $exclude = self::parseExcludePattern($exclude, false);
608
            }
609
            if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) {
610
                throw new InvalidArgumentException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.');
611
            }
612
            if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) {
613
                continue;
614
            }
615
616
            if ($exclude['flags'] & self::PATTERN_NODIR) {
617
                if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
618
                    return $exclude;
619
                }
620
                continue;
621
            }
622
623
            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
624
                return $exclude;
625
            }
626
        }
627
628
        return null;
629
    }
630
631
    /**
632
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
633
     *
634
     * @param string $pattern
635
     * @param bool $caseSensitive
636
     * @throws InvalidArgumentException
637
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
638
     */
639
    private static function parseExcludePattern($pattern, $caseSensitive)
640
    {
641
        if (!is_string($pattern)) {
642
            throw new InvalidArgumentException('Exclude/include pattern must be a string.');
643
        }
644
645
        $result = [
646
            'pattern' => $pattern,
647
            'flags' => 0,
648
            'firstWildcard' => false,
649
        ];
650
651
        if (!$caseSensitive) {
652
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
653
        }
654
655
        if (!isset($pattern[0])) {
656
            return $result;
657
        }
658
659
        if ($pattern[0] === '!') {
660
            $result['flags'] |= self::PATTERN_NEGATIVE;
661
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
662
        }
663
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstr($pattern, -1, 1) === '/') {
664
            $pattern = StringHelper::byteSubstr($pattern, 0, -1);
665
            $result['flags'] |= self::PATTERN_MUSTBEDIR;
666
        }
667
        if (strpos($pattern, '/') === false) {
668
            $result['flags'] |= self::PATTERN_NODIR;
669
        }
670
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
671
        if ($pattern[0] === '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) {
672
            $result['flags'] |= self::PATTERN_ENDSWITH;
673
        }
674
        $result['pattern'] = $pattern;
675
676
        return $result;
677
    }
678
679
    /**
680
     * Searches for the first wildcard character in the pattern.
681
     *
682
     * @param string $pattern the pattern to search in
683
     * @return int|bool position of first wildcard character or false if not found
684
     */
685
    private static function firstWildcardInPattern($pattern)
686
    {
687
        $wildcards = ['*', '?', '[', '\\'];
688
        $wildcardSearch = function ($r, $c) use ($pattern) {
689
            $p = strpos($pattern, $c);
690
691
            return $r === false ? $p : ($p === false ? $r : min($r, $p));
692
        };
693
694
        return array_reduce($wildcards, $wildcardSearch, false);
695
    }
696
697
    /**
698
     * @param array $options raw options
699
     * @return array normalized options
700
     * @since 2.0.12
701
     */
702
    protected static function normalizeOptions(array $options)
703
    {
704
        if (!array_key_exists('caseSensitive', $options)) {
705
            $options['caseSensitive'] = true;
706
        }
707 View Code Duplication
        if (isset($options['except'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
708
            foreach ($options['except'] as $key => $value) {
709
                if (is_string($value)) {
710
                    $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
711
                }
712
            }
713
        }
714 View Code Duplication
        if (isset($options['only'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
715
            foreach ($options['only'] as $key => $value) {
716
                if (is_string($value)) {
717
                    $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
718
                }
719
            }
720
        }
721
722
        return $options;
723
    }
724
}