This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
|
|||
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
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.');
}
![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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. ![]() |
|||
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 | } |
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.