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 | 24 | public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) |
|||
67 | { |
||||
68 | 24 | $path = rtrim(strtr($path, '/\\', $ds . $ds), $ds); |
|||
69 | 24 | if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) { |
|||
70 | 23 | 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 | 73 | public static function localize($file, $language = null, $sourceLanguage = null) |
|||
118 | { |
||||
119 | 73 | if ($language === null) { |
|||
120 | 72 | $language = Yii::$app->language; |
|||
121 | } |
||||
122 | 73 | if ($sourceLanguage === null) { |
|||
123 | 72 | $sourceLanguage = Yii::$app->sourceLanguage; |
|||
124 | } |
||||
125 | 73 | if ($language === $sourceLanguage) { |
|||
126 | 73 | 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
![]() |
|||||
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
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
![]() |
|||||
171 | |||||
172 | 32 | if ($info) { |
|||
0 ignored issues
–
show
|
|||||
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
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
![]() |
|||||
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
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
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.');
}
![]() |
|||||
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 | } |
||||
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 | 102 | public static function removeDirectory($dir, $options = []) |
|||
442 | { |
||||
443 | 102 | if (!is_dir($dir)) { |
|||
444 | 4 | return; |
|||
445 | } |
||||
446 | 100 | if (!empty($options['traverseSymlinks']) || !is_link($dir)) { |
|||
447 | 100 | if (!($handle = opendir($dir))) { |
|||
448 | return; |
||||
449 | } |
||||
450 | 100 | while (($file = readdir($handle)) !== false) { |
|||
451 | 100 | if ($file === '.' || $file === '..') { |
|||
452 | 100 | continue; |
|||
453 | } |
||||
454 | 86 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
|||
455 | 86 | if (is_dir($path)) { |
|||
456 | 76 | static::removeDirectory($path, $options); |
|||
457 | } else { |
||||
458 | 64 | static::unlink($path); |
|||
459 | } |
||||
460 | } |
||||
461 | 100 | closedir($handle); |
|||
462 | } |
||||
463 | 100 | if (is_link($dir)) { |
|||
464 | 2 | static::unlink($dir); |
|||
465 | } else { |
||||
466 | 100 | rmdir($dir); |
|||
467 | } |
||||
468 | } |
||||
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 | 65 | public static function unlink($path) |
|||
479 | { |
||||
480 | 65 | $isWindows = DIRECTORY_SEPARATOR === '\\'; |
|||
481 | |||||
482 | 65 | if (!$isWindows) { |
|||
483 | 65 | 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 | 55 | public static function findFiles($dir, $options = []) |
|||
544 | { |
||||
545 | 55 | $dir = self::clearDir($dir); |
|||
546 | 55 | $options = self::setBasePath($dir, $options); |
|||
547 | 55 | $list = []; |
|||
548 | 55 | $handle = self::openDir($dir); |
|||
549 | 55 | while (($file = readdir($handle)) !== false) { |
|||
550 | 55 | if ($file === '.' || $file === '..') { |
|||
551 | 55 | continue; |
|||
552 | } |
||||
553 | 52 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
|||
554 | 52 | if (static::filterPath($path, $options)) { |
|||
555 | 52 | if (is_file($path)) { |
|||
556 | 52 | $list[] = $path; |
|||
557 | 11 | } elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) { |
|||
558 | 10 | $list = array_merge($list, static::findFiles($path, $options)); |
|||
559 | } |
||||
560 | } |
||||
561 | } |
||||
562 | 55 | closedir($handle); |
|||
563 | |||||
564 | 55 | 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 | 56 | private static function setBasePath($dir, $options) |
|||
614 | { |
||||
615 | 56 | if (!isset($options['basePath'])) { |
|||
616 | // this should be done only once |
||||
617 | 56 | $options['basePath'] = realpath($dir); |
|||
618 | 56 | $options = static::normalizeOptions($options); |
|||
619 | } |
||||
620 | |||||
621 | 56 | return $options; |
|||
622 | } |
||||
623 | |||||
624 | /** |
||||
625 | * @param string $dir |
||||
626 | * @return resource |
||||
627 | * @throws InvalidArgumentException if unable to open directory |
||||
628 | */ |
||||
629 | 56 | private static function openDir($dir) |
|||
630 | { |
||||
631 | 56 | $handle = opendir($dir); |
|||
632 | 56 | if ($handle === false) { |
|||
633 | throw new InvalidArgumentException("Unable to open directory: $dir"); |
||||
634 | } |
||||
635 | 56 | return $handle; |
|||
636 | } |
||||
637 | |||||
638 | /** |
||||
639 | * @param string $dir |
||||
640 | * @return string |
||||
641 | * @throws InvalidArgumentException if directory not exists |
||||
642 | */ |
||||
643 | 56 | private static function clearDir($dir) |
|||
644 | { |
||||
645 | 56 | if (!is_dir($dir)) { |
|||
646 | throw new InvalidArgumentException("The dir argument must be a directory: $dir"); |
||||
647 | } |
||||
648 | 56 | 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 | 62 | public static function filterPath($path, $options) |
|||
659 | { |
||||
660 | 62 | 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 | 61 | if (empty($options['except']) && empty($options['only'])) { |
|||
668 | 23 | return true; |
|||
669 | } |
||||
670 | |||||
671 | 47 | $path = str_replace('\\', '/', $path); |
|||
672 | |||||
673 | if ( |
||||
674 | 47 | !empty($options['except']) |
|||
675 | 47 | && ($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null |
|||
676 | ) { |
||||
677 | 3 | return $except['flags'] & self::PATTERN_NEGATIVE; |
|||
0 ignored issues
–
show
|
|||||
678 | } |
||||
679 | |||||
680 | 47 | if (!empty($options['only']) && !is_dir($path)) { |
|||
681 | 45 | return self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only']) !== null; |
|||
682 | } |
||||
683 | |||||
684 | 6 | 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 | 132 | public static function createDirectory($path, $mode = 0775, $recursive = true) |
|||
701 | { |
||||
702 | 132 | if (is_dir($path)) { |
|||
703 | 57 | return true; |
|||
704 | } |
||||
705 | 125 | $parentDir = dirname($path); |
|||
706 | // recurse if parent dir does not exist and we are not at the root of the file system. |
||||
707 | 125 | if ($recursive && !is_dir($parentDir) && $parentDir !== $path) { |
|||
708 | 5 | static::createDirectory($parentDir, $mode, true); |
|||
709 | } |
||||
710 | try { |
||||
711 | 125 | if (!mkdir($path, $mode)) { |
|||
712 | 125 | 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 | 125 | 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 | 46 | private static function matchBasename($baseName, $pattern, $firstWildcard, $flags) |
|||
738 | { |
||||
739 | 46 | if ($firstWildcard === false) { |
|||
740 | 2 | if ($pattern === $baseName) { |
|||
741 | 2 | return true; |
|||
742 | } |
||||
743 | 44 | } elseif ($flags & self::PATTERN_ENDSWITH) { |
|||
744 | /* "*literal" matching against "fooliteral" */ |
||||
745 | 44 | $n = StringHelper::byteLength($pattern); |
|||
746 | 44 | if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) { |
|||
747 | return true; |
||||
748 | } |
||||
749 | } |
||||
750 | |||||
751 | 46 | $matchOptions = []; |
|||
752 | 46 | if ($flags & self::PATTERN_CASE_INSENSITIVE) { |
|||
753 | 1 | $matchOptions['caseSensitive'] = false; |
|||
754 | } |
||||
755 | |||||
756 | 46 | 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 | 41 | 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 | 41 | if (strncmp($pattern, '/', 1) === 0) { |
|||
775 | 40 | $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); |
|||
776 | 40 | if ($firstWildcard !== false && $firstWildcard !== 0) { |
|||
777 | 40 | $firstWildcard--; |
|||
778 | } |
||||
779 | } |
||||
780 | |||||
781 | 41 | $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1); |
|||
782 | 41 | $name = StringHelper::byteSubstr($path, -$namelen, $namelen); |
|||
783 | |||||
784 | 41 | if ($firstWildcard !== 0) { |
|||
785 | 41 | if ($firstWildcard === false) { |
|||
786 | 40 | $firstWildcard = StringHelper::byteLength($pattern); |
|||
787 | } |
||||
788 | // if the non-wildcard part is longer than the remaining pathname, surely it cannot match. |
||||
789 | 41 | if ($firstWildcard > $namelen) { |
|||
790 | 1 | return false; |
|||
791 | } |
||||
792 | |||||
793 | 41 | if (strncmp($pattern, $name, $firstWildcard)) { |
|||
0 ignored issues
–
show
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
![]() |
|||||
794 | 41 | return false; |
|||
795 | } |
||||
796 | 2 | $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern)); |
|||
0 ignored issues
–
show
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
![]() |
|||||
797 | 2 | $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 | 2 | if (empty($pattern) && empty($name)) { |
|||
801 | 1 | return true; |
|||
802 | } |
||||
803 | } |
||||
804 | |||||
805 | 2 | $matchOptions = [ |
|||
806 | 2 | 'filePath' => true |
|||
807 | 2 | ]; |
|||
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 | 47 | private static function lastExcludeMatchingFromList($basePath, $path, $excludes) |
|||
830 | { |
||||
831 | 47 | foreach (array_reverse($excludes) as $exclude) { |
|||
832 | 47 | if (is_string($exclude)) { |
|||
833 | $exclude = self::parseExcludePattern($exclude, false); |
||||
834 | } |
||||
835 | 47 | 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 | 47 | if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) { |
|||
839 | continue; |
||||
840 | } |
||||
841 | |||||
842 | 47 | if ($exclude['flags'] & self::PATTERN_NODIR) { |
|||
843 | 46 | if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { |
|||
844 | 46 | return $exclude; |
|||
845 | } |
||||
846 | 45 | continue; |
|||
847 | } |
||||
848 | |||||
849 | 41 | if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { |
|||
850 | 2 | return $exclude; |
|||
851 | } |
||||
852 | } |
||||
853 | |||||
854 | 46 | 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 | 53 | private static function parseExcludePattern($pattern, $caseSensitive) |
|||
865 | { |
||||
866 | 53 | if (!is_string($pattern)) { |
|||
0 ignored issues
–
show
|
|||||
867 | throw new InvalidArgumentException('Exclude/include pattern must be a string.'); |
||||
868 | } |
||||
869 | |||||
870 | 53 | $result = [ |
|||
871 | 53 | 'pattern' => $pattern, |
|||
872 | 53 | 'flags' => 0, |
|||
873 | 53 | 'firstWildcard' => false, |
|||
874 | 53 | ]; |
|||
875 | |||||
876 | 53 | if (!$caseSensitive) { |
|||
877 | 1 | $result['flags'] |= self::PATTERN_CASE_INSENSITIVE; |
|||
878 | } |
||||
879 | |||||
880 | 53 | if (empty($pattern)) { |
|||
881 | return $result; |
||||
882 | } |
||||
883 | |||||
884 | 53 | if (strncmp($pattern, '!', 1) === 0) { |
|||
885 | 1 | $result['flags'] |= self::PATTERN_NEGATIVE; |
|||
886 | 1 | $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); |
|||
887 | } |
||||
888 | 53 | if (StringHelper::byteLength($pattern) && StringHelper::byteSubstr($pattern, -1, 1) === '/') { |
|||
889 | $pattern = StringHelper::byteSubstr($pattern, 0, -1); |
||||
890 | $result['flags'] |= self::PATTERN_MUSTBEDIR; |
||||
891 | } |
||||
892 | 53 | if (strpos($pattern, '/') === false) { |
|||
893 | 52 | $result['flags'] |= self::PATTERN_NODIR; |
|||
894 | } |
||||
895 | 53 | $result['firstWildcard'] = self::firstWildcardInPattern($pattern); |
|||
896 | 53 | if (strncmp($pattern, '*', 1) === 0 && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { |
|||
897 | 50 | $result['flags'] |= self::PATTERN_ENDSWITH; |
|||
898 | } |
||||
899 | 53 | $result['pattern'] = $pattern; |
|||
900 | |||||
901 | 53 | 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 | 53 | private static function firstWildcardInPattern($pattern) |
|||
910 | { |
||||
911 | 53 | $wildcards = ['*', '?', '[', '\\']; |
|||
912 | 53 | $wildcardSearch = function ($r, $c) use ($pattern) { |
|||
913 | 53 | $p = strpos($pattern, $c); |
|||
914 | |||||
915 | 53 | return $r === false ? $p : ($p === false ? $r : min($r, $p)); |
|||
916 | 53 | }; |
|||
917 | |||||
918 | 53 | 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 | 67 | protected static function normalizeOptions(array $options) |
|||
927 | { |
||||
928 | 67 | if (!array_key_exists('caseSensitive', $options)) { |
|||
929 | 66 | $options['caseSensitive'] = true; |
|||
930 | } |
||||
931 | 67 | if (isset($options['except'])) { |
|||
932 | 48 | foreach ($options['except'] as $key => $value) { |
|||
933 | 48 | if (is_string($value)) { |
|||
934 | 48 | $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']); |
|||
935 | } |
||||
936 | } |
||||
937 | } |
||||
938 | 67 | if (isset($options['only'])) { |
|||
939 | 51 | foreach ($options['only'] as $key => $value) { |
|||
940 | 51 | if (is_string($value)) { |
|||
941 | 51 | $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']); |
|||
942 | } |
||||
943 | } |
||||
944 | } |
||||
945 | |||||
946 | 67 | 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 | 6 | public static function changeOwnership($path, $ownership, $mode = null) |
|||
970 | { |
||||
971 | 6 | if (!file_exists((string)$path)) { |
|||
972 | 1 | throw new InvalidArgumentException('Unable to change ownership, "' . $path . '" is not a file or directory.'); |
|||
973 | } |
||||
974 | |||||
975 | 5 | if (empty($ownership) && $ownership !== 0 && $mode === null) { |
|||
976 | 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
|
|||||
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
|
|||||
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 |