1 | <?php |
||
2 | /** |
||
3 | * @link http://www.yiiframework.com/ |
||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||
5 | * @license http://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 | |||
44 | /** |
||
45 | * Normalizes a file/directory path. |
||
46 | * |
||
47 | * The normalization does the following work: |
||
48 | * |
||
49 | * - Convert all directory separators into `DIRECTORY_SEPARATOR` (e.g. "\a/b\c" becomes "/a/b/c") |
||
50 | * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c") |
||
51 | * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c") |
||
52 | * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c") |
||
53 | * |
||
54 | * Note: For registered stream wrappers, the consecutive slashes rule |
||
55 | * and ".."/"." translations are skipped. |
||
56 | * |
||
57 | * @param string $path the file/directory path to be normalized |
||
58 | * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`. |
||
59 | * @return string the normalized file/directory path |
||
60 | 36 | */ |
|
61 | public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) |
||
62 | 36 | { |
|
63 | 36 | $path = rtrim(strtr($path, '/\\', $ds . $ds), $ds); |
|
64 | 35 | if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) { |
|
65 | return $path; |
||
66 | } |
||
67 | 1 | // fix #17235 stream wrappers |
|
68 | 1 | foreach (stream_get_wrappers() as $protocol) { |
|
69 | 1 | if (strpos($path, "{$protocol}://") === 0) { |
|
70 | return $path; |
||
71 | } |
||
72 | } |
||
73 | 1 | // the path may contain ".", ".." or double slashes, need to clean them up |
|
74 | 1 | if (strpos($path, "{$ds}{$ds}") === 0 && $ds == '\\') { |
|
75 | $parts = [$ds]; |
||
76 | 1 | } else { |
|
77 | $parts = []; |
||
78 | 1 | } |
|
79 | 1 | foreach (explode($ds, $path) as $part) { |
|
80 | 1 | if ($part === '..' && !empty($parts) && end($parts) !== '..') { |
|
81 | 1 | array_pop($parts); |
|
82 | 1 | } elseif ($part === '.' || $part === '' && !empty($parts)) { |
|
83 | continue; |
||
84 | 1 | } else { |
|
85 | $parts[] = $part; |
||
86 | } |
||
87 | 1 | } |
|
88 | 1 | $path = implode($ds, $parts); |
|
89 | return $path === '' ? '.' : $path; |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Returns the localized version of a specified file. |
||
94 | * |
||
95 | * The searching is based on the specified language code. In particular, |
||
96 | * a file with the same name will be looked for under the subdirectory |
||
97 | * whose name is the same as the language code. For example, given the file "path/to/view.php" |
||
98 | * and language code "zh-CN", the localized file will be looked for as |
||
99 | * "path/to/zh-CN/view.php". If the file is not found, it will try a fallback with just a language code that is |
||
100 | * "zh" i.e. "path/to/zh/view.php". If it is not found as well the original file will be returned. |
||
101 | * |
||
102 | * If the target and the source language codes are the same, |
||
103 | * the original file will be returned. |
||
104 | * |
||
105 | * @param string $file the original file |
||
106 | * @param string $language the target language that the file should be localized to. |
||
107 | * If not set, the value of [[\yii\base\Application::language]] will be used. |
||
108 | * @param string $sourceLanguage the language that the original file is in. |
||
109 | * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. |
||
110 | * @return string the matching localized file, or the original file if the localized version is not found. |
||
111 | * If the target and the source language codes are the same, the original file will be returned. |
||
112 | 155 | */ |
|
113 | public static function localize($file, $language = null, $sourceLanguage = null) |
||
114 | 155 | { |
|
115 | 154 | if ($language === null) { |
|
116 | $language = Yii::$app->language; |
||
117 | 155 | } |
|
118 | 154 | if ($sourceLanguage === null) { |
|
119 | $sourceLanguage = Yii::$app->sourceLanguage; |
||
120 | 155 | } |
|
121 | 155 | if ($language === $sourceLanguage) { |
|
122 | return $file; |
||
123 | 1 | } |
|
124 | 1 | $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); |
|
125 | 1 | if (is_file($desiredFile)) { |
|
126 | return $desiredFile; |
||
127 | } |
||
128 | |||
129 | $language = substr($language, 0, 2); |
||
130 | if ($language === $sourceLanguage) { |
||
131 | return $file; |
||
132 | } |
||
133 | $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); |
||
134 | |||
135 | return is_file($desiredFile) ? $desiredFile : $file; |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * Determines the MIME type of the specified file. |
||
140 | * This method will first try to determine the MIME type based on |
||
141 | * [finfo_open](https://secure.php.net/manual/en/function.finfo-open.php). If the `fileinfo` extension is not installed, |
||
142 | * it will fall back to [[getMimeTypeByExtension()]] when `$checkExtension` is true. |
||
143 | * @param string $file the file name. |
||
144 | * @param string $magicFile name of the optional magic database file (or alias), usually something like `/path/to/magic.mime`. |
||
145 | * This will be passed as the second parameter to [finfo_open()](https://secure.php.net/manual/en/function.finfo-open.php) |
||
146 | * when the `fileinfo` extension is installed. If the MIME type is being determined based via [[getMimeTypeByExtension()]] |
||
147 | * and this is null, it will use the file specified by [[mimeMagicFile]]. |
||
148 | * @param bool $checkExtension whether to use the file extension to determine the MIME type in case |
||
149 | * `finfo_open()` cannot determine it. |
||
150 | * @return string|null the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. |
||
151 | * @throws InvalidConfigException when the `fileinfo` PHP extension is not installed and `$checkExtension` is `false`. |
||
152 | 31 | */ |
|
153 | public static function getMimeType($file, $magicFile = null, $checkExtension = true) |
||
154 | 31 | { |
|
155 | if ($magicFile !== null) { |
||
156 | $magicFile = Yii::getAlias($magicFile); |
||
157 | 31 | } |
|
158 | if (!extension_loaded('fileinfo')) { |
||
159 | if ($checkExtension) { |
||
160 | return static::getMimeTypeByExtension($file, $magicFile); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
161 | } |
||
162 | |||
163 | throw new InvalidConfigException('The fileinfo PHP extension is not installed.'); |
||
164 | 31 | } |
|
165 | $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); |
||
166 | 31 | ||
167 | 31 | if ($info) { |
|
168 | 31 | $result = finfo_file($info, $file); |
|
169 | finfo_close($info); |
||
170 | 31 | ||
171 | 31 | if ($result !== false) { |
|
172 | return $result; |
||
173 | } |
||
174 | } |
||
175 | |||
176 | return $checkExtension ? static::getMimeTypeByExtension($file, $magicFile) : null; |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * Determines the MIME type based on the extension name of the specified file. |
||
181 | * This method will use a local map between extension names and MIME types. |
||
182 | * @param string $file the file name. |
||
183 | * @param string $magicFile the path (or alias) of the file that contains all available MIME type information. |
||
184 | * If this is not set, the file specified by [[mimeMagicFile]] will be used. |
||
185 | * @return string|null the MIME type. Null is returned if the MIME type cannot be determined. |
||
186 | 11 | */ |
|
187 | public static function getMimeTypeByExtension($file, $magicFile = null) |
||
188 | 11 | { |
|
189 | $mimeTypes = static::loadMimeTypes($magicFile); |
||
190 | 11 | ||
191 | 11 | if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { |
|
192 | 11 | $ext = strtolower($ext); |
|
193 | 11 | if (isset($mimeTypes[$ext])) { |
|
194 | return $mimeTypes[$ext]; |
||
195 | } |
||
196 | } |
||
197 | 1 | ||
198 | return null; |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Determines the extensions by given MIME type. |
||
203 | * This method will use a local map between extension names and MIME types. |
||
204 | * @param string $mimeType file MIME type. |
||
205 | * @param string $magicFile the path (or alias) of the file that contains all available MIME type information. |
||
206 | * If this is not set, the file specified by [[mimeMagicFile]] will be used. |
||
207 | * @return array the extensions corresponding to the specified MIME type |
||
208 | 13 | */ |
|
209 | public static function getExtensionsByMimeType($mimeType, $magicFile = null) |
||
210 | 13 | { |
|
211 | 13 | $aliases = static::loadMimeAliases(static::$mimeAliasesFile); |
|
212 | if (isset($aliases[$mimeType])) { |
||
213 | $mimeType = $aliases[$mimeType]; |
||
214 | } |
||
215 | 13 | ||
216 | 13 | $mimeTypes = static::loadMimeTypes($magicFile); |
|
217 | return array_keys($mimeTypes, mb_strtolower($mimeType, 'UTF-8'), true); |
||
218 | } |
||
219 | |||
220 | private static $_mimeTypes = []; |
||
221 | |||
222 | /** |
||
223 | * Loads MIME types from the specified file. |
||
224 | * @param string $magicFile the path (or alias) of the file that contains all available MIME type information. |
||
225 | * If this is not set, the file specified by [[mimeMagicFile]] will be used. |
||
226 | * @return array the mapping from file extensions to MIME types |
||
227 | 24 | */ |
|
228 | protected static function loadMimeTypes($magicFile) |
||
229 | 24 | { |
|
230 | 24 | if ($magicFile === null) { |
|
231 | $magicFile = static::$mimeMagicFile; |
||
232 | 24 | } |
|
233 | 24 | $magicFile = Yii::getAlias($magicFile); |
|
234 | 1 | if (!isset(self::$_mimeTypes[$magicFile])) { |
|
235 | self::$_mimeTypes[$magicFile] = require $magicFile; |
||
236 | } |
||
237 | 24 | ||
238 | return self::$_mimeTypes[$magicFile]; |
||
239 | } |
||
240 | |||
241 | private static $_mimeAliases = []; |
||
242 | |||
243 | /** |
||
244 | * Loads MIME aliases from the specified file. |
||
245 | * @param string $aliasesFile the path (or alias) of the file that contains MIME type aliases. |
||
246 | * If this is not set, the file specified by [[mimeAliasesFile]] will be used. |
||
247 | * @return array the mapping from file extensions to MIME types |
||
248 | * @since 2.0.14 |
||
249 | 13 | */ |
|
250 | protected static function loadMimeAliases($aliasesFile) |
||
251 | 13 | { |
|
252 | if ($aliasesFile === null) { |
||
253 | $aliasesFile = static::$mimeAliasesFile; |
||
254 | 13 | } |
|
255 | 13 | $aliasesFile = Yii::getAlias($aliasesFile); |
|
256 | 1 | if (!isset(self::$_mimeAliases[$aliasesFile])) { |
|
257 | self::$_mimeAliases[$aliasesFile] = require $aliasesFile; |
||
258 | } |
||
259 | 13 | ||
260 | return self::$_mimeAliases[$aliasesFile]; |
||
261 | } |
||
262 | |||
263 | /** |
||
264 | * Copies a whole directory as another one. |
||
265 | * The files and sub-directories will also be copied over. |
||
266 | * @param string $src the source directory |
||
267 | * @param string $dst the destination directory |
||
268 | * @param array $options options for directory copy. Valid options are: |
||
269 | * |
||
270 | * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775. |
||
271 | * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. |
||
272 | * - filter: callback, a PHP callback that is called for each directory or file. |
||
273 | * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. |
||
274 | * The callback can return one of the following values: |
||
275 | * |
||
276 | * * true: the directory or file will be copied (the "only" and "except" options will be ignored) |
||
277 | * * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored) |
||
278 | * * null: the "only" and "except" options will determine whether the directory or file should be copied |
||
279 | * |
||
280 | * - only: array, list of patterns that the file paths should match if they want to be copied. |
||
281 | * A path matches a pattern if it contains the pattern string at its end. |
||
282 | * For example, '.php' matches all file paths ending with '.php'. |
||
283 | * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. |
||
284 | * If a file path matches a pattern in both "only" and "except", it will NOT be copied. |
||
285 | * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied. |
||
286 | * A path matches a pattern if it contains the pattern string at its end. |
||
287 | * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' |
||
288 | * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; |
||
289 | * and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches |
||
290 | * both '/' and '\' in the paths. |
||
291 | * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true. |
||
292 | * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true. |
||
293 | * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. |
||
294 | * If the callback returns false, the copy operation for the sub-directory or file will be cancelled. |
||
295 | * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or |
||
296 | * file to be copied from, while `$to` is the copy target. |
||
297 | * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied. |
||
298 | * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or |
||
299 | * file copied from, while `$to` is the copy target. |
||
300 | * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating directories |
||
301 | * that do not contain files. This affects directories that do not contain files initially as well as directories that |
||
302 | * do not contain files at the target destination because files have been filtered via `only` or `except`. |
||
303 | * Defaults to true. This option is available since version 2.0.12. Before 2.0.12 empty directories are always copied. |
||
304 | * @throws InvalidArgumentException if unable to open directory |
||
305 | 17 | */ |
|
306 | public static function copyDirectory($src, $dst, $options = []) |
||
307 | 17 | { |
|
308 | 17 | $src = static::normalizePath($src); |
|
309 | $dst = static::normalizePath($dst); |
||
310 | 17 | ||
311 | 2 | if ($src === $dst || strpos($dst, $src . DIRECTORY_SEPARATOR) === 0) { |
|
312 | throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.'); |
||
313 | 15 | } |
|
314 | 15 | $dstExists = is_dir($dst); |
|
315 | 6 | if (!$dstExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) { |
|
316 | 6 | static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true); |
|
317 | $dstExists = true; |
||
318 | } |
||
319 | 15 | ||
320 | 15 | $handle = opendir($src); |
|
321 | if ($handle === false) { |
||
322 | throw new InvalidArgumentException("Unable to open directory: $src"); |
||
323 | 15 | } |
|
324 | if (!isset($options['basePath'])) { |
||
325 | 15 | // this should be done only once |
|
326 | 15 | $options['basePath'] = realpath($src); |
|
327 | $options = static::normalizeOptions($options); |
||
328 | 15 | } |
|
329 | 15 | while (($file = readdir($handle)) !== false) { |
|
330 | 15 | if ($file === '.' || $file === '..') { |
|
331 | continue; |
||
332 | 13 | } |
|
333 | 13 | $from = $src . DIRECTORY_SEPARATOR . $file; |
|
334 | 13 | $to = $dst . DIRECTORY_SEPARATOR . $file; |
|
335 | 13 | if (static::filterPath($from, $options)) { |
|
336 | 2 | if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) { |
|
337 | continue; |
||
338 | 11 | } |
|
339 | 11 | if (is_file($from)) { |
|
340 | if (!$dstExists) { |
||
341 | 5 | // delay creation of destination directory until the first file is copied to avoid creating empty directories |
|
342 | 5 | static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true); |
|
343 | $dstExists = true; |
||
344 | 11 | } |
|
345 | 11 | copy($from, $to); |
|
346 | 11 | if (isset($options['fileMode'])) { |
|
347 | @chmod($to, $options['fileMode']); |
||
348 | } |
||
349 | } else { |
||
350 | 8 | // recursive copy, defaults to true |
|
351 | 7 | if (!isset($options['recursive']) || $options['recursive']) { |
|
352 | static::copyDirectory($from, $to, $options); |
||
353 | } |
||
354 | 11 | } |
|
355 | if (isset($options['afterCopy'])) { |
||
356 | call_user_func($options['afterCopy'], $from, $to); |
||
357 | } |
||
358 | } |
||
359 | 15 | } |
|
360 | 15 | closedir($handle); |
|
361 | } |
||
362 | |||
363 | /** |
||
364 | * Removes a directory (and all its content) recursively. |
||
365 | * |
||
366 | * @param string $dir the directory to be deleted recursively. |
||
367 | * @param array $options options for directory remove. Valid options are: |
||
368 | * |
||
369 | * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too. |
||
370 | * Defaults to `false`, meaning the content of the symlinked directory would not be deleted. |
||
371 | * Only symlink would be removed in that default case. |
||
372 | * |
||
373 | * @throws ErrorException in case of failure |
||
374 | 222 | */ |
|
375 | public static function removeDirectory($dir, $options = []) |
||
376 | 222 | { |
|
377 | 107 | if (!is_dir($dir)) { |
|
378 | return; |
||
379 | 220 | } |
|
380 | 220 | if (!empty($options['traverseSymlinks']) || !is_link($dir)) { |
|
381 | if (!($handle = opendir($dir))) { |
||
382 | return; |
||
383 | 220 | } |
|
384 | 220 | while (($file = readdir($handle)) !== false) { |
|
385 | 220 | if ($file === '.' || $file === '..') { |
|
386 | continue; |
||
387 | 198 | } |
|
388 | 198 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
|
389 | 76 | if (is_dir($path)) { |
|
390 | static::removeDirectory($path, $options); |
||
391 | 176 | } else { |
|
392 | static::unlink($path); |
||
393 | } |
||
394 | 220 | } |
|
395 | closedir($handle); |
||
396 | 220 | } |
|
397 | 2 | if (is_link($dir)) { |
|
398 | static::unlink($dir); |
||
399 | 220 | } else { |
|
400 | rmdir($dir); |
||
401 | 220 | } |
|
402 | } |
||
403 | |||
404 | /** |
||
405 | * Removes a file or symlink in a cross-platform way |
||
406 | * |
||
407 | * @param string $path |
||
408 | * @return bool |
||
409 | * |
||
410 | * @since 2.0.14 |
||
411 | 177 | */ |
|
412 | public static function unlink($path) |
||
413 | 177 | { |
|
414 | $isWindows = DIRECTORY_SEPARATOR === '\\'; |
||
415 | 177 | ||
416 | 177 | if (!$isWindows) { |
|
417 | return unlink($path); |
||
418 | } |
||
419 | |||
420 | if (is_link($path) && is_dir($path)) { |
||
421 | return rmdir($path); |
||
422 | } |
||
423 | |||
424 | try { |
||
425 | return unlink($path); |
||
426 | } catch (ErrorException $e) { |
||
427 | // last resort measure for Windows |
||
428 | if (is_dir($path) && count(static::findFiles($path)) !== 0) { |
||
429 | return false; |
||
430 | } |
||
431 | if (function_exists('exec') && file_exists($path)) { |
||
432 | exec('DEL /F/Q ' . escapeshellarg($path)); |
||
433 | |||
434 | return !file_exists($path); |
||
435 | } |
||
436 | |||
437 | return false; |
||
438 | } |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * Returns the files found under the specified directory and subdirectories. |
||
443 | * @param string $dir the directory under which the files will be looked for. |
||
444 | * @param array $options options for file searching. Valid options are: |
||
445 | * |
||
446 | * - `filter`: callback, a PHP callback that is called for each directory or file. |
||
447 | * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. |
||
448 | * The callback can return one of the following values: |
||
449 | * |
||
450 | * * `true`: the directory or file will be returned (the `only` and `except` options will be ignored) |
||
451 | * * `false`: the directory or file will NOT be returned (the `only` and `except` options will be ignored) |
||
452 | * * `null`: the `only` and `except` options will determine whether the directory or file should be returned |
||
453 | * |
||
454 | * - `except`: array, list of patterns excluding from the results matching file or directory paths. |
||
455 | * Patterns ending with slash ('/') apply to directory paths only, and patterns not ending with '/' |
||
456 | * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; |
||
457 | * and `.svn/` matches directory paths ending with `.svn`. |
||
458 | * If the pattern does not contain a slash (`/`), it is treated as a shell glob pattern |
||
459 | * and checked for a match against the pathname relative to `$dir`. |
||
460 | * Otherwise, the pattern is treated as a shell glob suitable for consumption by `fnmatch(3)` |
||
461 | * with the `FNM_PATHNAME` flag: wildcards in the pattern will not match a `/` in the pathname. |
||
462 | * For example, `views/*.php` matches `views/index.php` but not `views/controller/index.php`. |
||
463 | * A leading slash matches the beginning of the pathname. For example, `/*.php` matches `index.php` but not `views/start/index.php`. |
||
464 | * An optional prefix `!` which negates the pattern; any matching file excluded by a previous pattern will become included again. |
||
465 | * If a negated pattern matches, this will override lower precedence patterns sources. Put a backslash (`\`) in front of the first `!` |
||
466 | * for patterns that begin with a literal `!`, for example, `\!important!.txt`. |
||
467 | * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. |
||
468 | * - `only`: array, list of patterns that the file paths should match if they are to be returned. Directory paths |
||
469 | * are not checked against them. Same pattern matching rules as in the `except` option are used. |
||
470 | * If a file path matches a pattern in both `only` and `except`, it will NOT be returned. |
||
471 | * - `caseSensitive`: boolean, whether patterns specified at `only` or `except` should be case sensitive. Defaults to `true`. |
||
472 | * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`. |
||
473 | * @return array files found under the directory, in no particular order. Ordering depends on the files system used. |
||
474 | * @throws InvalidArgumentException if the dir is invalid. |
||
475 | 166 | */ |
|
476 | public static function findFiles($dir, $options = []) |
||
477 | 166 | { |
|
478 | 166 | $dir = self::clearDir($dir); |
|
479 | 166 | $options = self::setBasePath($dir, $options); |
|
480 | 166 | $list = []; |
|
481 | 166 | $handle = self::openDir($dir); |
|
482 | 166 | while (($file = readdir($handle)) !== false) { |
|
483 | 166 | if ($file === '.' || $file === '..') { |
|
484 | continue; |
||
485 | 163 | } |
|
486 | 163 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
|
487 | 163 | if (static::filterPath($path, $options)) { |
|
488 | 161 | if (is_file($path)) { |
|
489 | 25 | $list[] = $path; |
|
490 | 24 | } elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) { |
|
491 | $list = array_merge($list, static::findFiles($path, $options)); |
||
492 | } |
||
493 | } |
||
494 | 166 | } |
|
495 | closedir($handle); |
||
496 | 166 | ||
497 | return $list; |
||
498 | } |
||
499 | |||
500 | /** |
||
501 | * Returns the directories found under the specified directory and subdirectories. |
||
502 | * @param string $dir the directory under which the files will be looked for. |
||
503 | * @param array $options options for directory searching. Valid options are: |
||
504 | * |
||
505 | * - `filter`: callback, a PHP callback that is called for each directory or file. |
||
506 | * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. |
||
507 | * The callback can return one of the following values: |
||
508 | * |
||
509 | * * `true`: the directory will be returned |
||
510 | * * `false`: the directory will NOT be returned |
||
511 | * |
||
512 | * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`. |
||
513 | * @return array directories found under the directory, in no particular order. Ordering depends on the files system used. |
||
514 | * @throws InvalidArgumentException if the dir is invalid. |
||
515 | * @since 2.0.14 |
||
516 | 1 | */ |
|
517 | public static function findDirectories($dir, $options = []) |
||
518 | 1 | { |
|
519 | 1 | $dir = self::clearDir($dir); |
|
520 | 1 | $options = self::setBasePath($dir, $options); |
|
521 | 1 | $list = []; |
|
522 | 1 | $handle = self::openDir($dir); |
|
523 | 1 | while (($file = readdir($handle)) !== false) { |
|
524 | 1 | if ($file === '.' || $file === '..') { |
|
525 | continue; |
||
526 | 1 | } |
|
527 | 1 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
|
528 | 1 | if (is_dir($path) && static::filterPath($path, $options)) { |
|
529 | 1 | $list[] = $path; |
|
530 | 1 | if (!isset($options['recursive']) || $options['recursive']) { |
|
531 | $list = array_merge($list, static::findDirectories($path, $options)); |
||
532 | } |
||
533 | } |
||
534 | 1 | } |
|
535 | closedir($handle); |
||
536 | 1 | ||
537 | return $list; |
||
538 | } |
||
539 | |||
540 | /** |
||
541 | * @param string $dir |
||
542 | 167 | */ |
|
543 | private static function setBasePath($dir, $options) |
||
544 | 167 | { |
|
545 | if (!isset($options['basePath'])) { |
||
546 | 167 | // this should be done only once |
|
547 | 167 | $options['basePath'] = realpath($dir); |
|
548 | $options = static::normalizeOptions($options); |
||
549 | } |
||
550 | 167 | ||
551 | return $options; |
||
552 | } |
||
553 | |||
554 | /** |
||
555 | * @param string $dir |
||
556 | 167 | */ |
|
557 | private static function openDir($dir) |
||
558 | 167 | { |
|
559 | 167 | $handle = opendir($dir); |
|
560 | if ($handle === false) { |
||
561 | throw new InvalidArgumentException("Unable to open directory: $dir"); |
||
562 | 167 | } |
|
563 | return $handle; |
||
564 | } |
||
565 | |||
566 | /** |
||
567 | * @param string $dir |
||
568 | 167 | */ |
|
569 | private static function clearDir($dir) |
||
570 | 167 | { |
|
571 | if (!is_dir($dir)) { |
||
572 | throw new InvalidArgumentException("The dir argument must be a directory: $dir"); |
||
573 | 167 | } |
|
574 | return rtrim($dir, DIRECTORY_SEPARATOR); |
||
575 | } |
||
576 | |||
577 | /** |
||
578 | * Checks if the given file path satisfies the filtering options. |
||
579 | * @param string $path the path of the file or directory to be checked |
||
580 | * @param array $options the filtering options. See [[findFiles()]] for explanations of |
||
581 | * the supported options. |
||
582 | * @return bool whether the file or directory satisfies the filtering options. |
||
583 | 173 | */ |
|
584 | public static function filterPath($path, $options) |
||
585 | 173 | { |
|
586 | 2 | if (isset($options['filter'])) { |
|
587 | 2 | $result = call_user_func($options['filter'], $path); |
|
588 | 2 | if (is_bool($result)) { |
|
589 | return $result; |
||
590 | } |
||
591 | } |
||
592 | 172 | ||
593 | 105 | if (empty($options['except']) && empty($options['only'])) { |
|
594 | return true; |
||
595 | } |
||
596 | 76 | ||
597 | $path = str_replace('\\', '/', $path); |
||
598 | 76 | ||
599 | 56 | if ( |
|
600 | 3 | !empty($options['except']) |
|
601 | && ($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null |
||
602 | ) { |
||
603 | return $except['flags'] & self::PATTERN_NEGATIVE; |
||
604 | 76 | } |
|
605 | 74 | ||
606 | if (!empty($options['only']) && !is_dir($path)) { |
||
607 | 72 | return self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only']) !== null; |
|
608 | } |
||
609 | |||
610 | 20 | return true; |
|
611 | } |
||
612 | |||
613 | 20 | /** |
|
614 | * Creates a new directory. |
||
615 | * |
||
616 | * This method is similar to the PHP `mkdir()` function except that |
||
617 | * it uses `chmod()` to set the permission of the created directory |
||
618 | * in order to avoid the impact of the `umask` setting. |
||
619 | * |
||
620 | * @param string $path path of the directory to be created. |
||
621 | * @param int $mode the permission to be set for the created directory. |
||
622 | * @param bool $recursive whether to create parent directories if they do not exist. |
||
623 | * @return bool whether the directory is created successfully |
||
624 | * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes) |
||
625 | */ |
||
626 | public static function createDirectory($path, $mode = 0775, $recursive = true) |
||
627 | { |
||
628 | if (is_dir($path)) { |
||
629 | 271 | return true; |
|
630 | } |
||
631 | 271 | $parentDir = dirname($path); |
|
632 | 160 | // recurse if parent dir does not exist and we are not at the root of the file system. |
|
633 | if ($recursive && !is_dir($parentDir) && $parentDir !== $path) { |
||
634 | 251 | static::createDirectory($parentDir, $mode, true); |
|
635 | } |
||
636 | 251 | try { |
|
637 | 5 | if (!mkdir($path, $mode)) { |
|
638 | return false; |
||
639 | } |
||
640 | 251 | } catch (\Exception $e) { |
|
641 | 251 | if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288 |
|
642 | throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e); |
||
643 | } |
||
644 | } |
||
645 | try { |
||
646 | return chmod($path, $mode); |
||
647 | } catch (\Exception $e) { |
||
648 | throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e); |
||
649 | 251 | } |
|
650 | } |
||
651 | |||
652 | /** |
||
653 | * Performs a simple comparison of file or directory names. |
||
654 | * |
||
655 | * Based on match_basename() from dir.c of git 1.8.5.3 sources. |
||
656 | * |
||
657 | * @param string $baseName file or directory name to compare with the pattern |
||
658 | * @param string $pattern the pattern that $baseName will be compared against |
||
659 | * @param int|bool $firstWildcard location of first wildcard character in the $pattern |
||
660 | * @param int $flags pattern flags |
||
661 | * @return bool whether the name matches against pattern |
||
662 | */ |
||
663 | private static function matchBasename($baseName, $pattern, $firstWildcard, $flags) |
||
664 | { |
||
665 | if ($firstWildcard === false) { |
||
666 | 75 | if ($pattern === $baseName) { |
|
667 | return true; |
||
668 | 75 | } |
|
669 | 64 | } elseif ($flags & self::PATTERN_ENDSWITH) { |
|
670 | 64 | /* "*literal" matching against "fooliteral" */ |
|
671 | $n = StringHelper::byteLength($pattern); |
||
672 | 64 | if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) { |
|
673 | return true; |
||
674 | 63 | } |
|
675 | 63 | } |
|
676 | |||
677 | $matchOptions = []; |
||
678 | if ($flags & self::PATTERN_CASE_INSENSITIVE) { |
||
679 | $matchOptions['caseSensitive'] = false; |
||
680 | 75 | } |
|
681 | 75 | ||
682 | 1 | return StringHelper::matchWildcard($pattern, $baseName, $matchOptions); |
|
683 | } |
||
684 | |||
685 | 75 | /** |
|
686 | * Compares a path part against a pattern with optional wildcards. |
||
687 | * |
||
688 | * Based on match_pathname() from dir.c of git 1.8.5.3 sources. |
||
689 | * |
||
690 | * @param string $path full path to compare |
||
691 | * @param string $basePath base of path that will not be compared |
||
692 | * @param string $pattern the pattern that path part will be compared against |
||
693 | * @param int|bool $firstWildcard location of first wildcard character in the $pattern |
||
694 | * @param int $flags pattern flags |
||
695 | * @return bool whether the path part matches against pattern |
||
696 | */ |
||
697 | private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags) |
||
698 | { |
||
699 | // match with FNM_PATHNAME; the pattern has base implicitly in front of it. |
||
700 | 57 | if (strpos($pattern, '/') === 0) { |
|
701 | $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); |
||
702 | if ($firstWildcard !== false && $firstWildcard !== 0) { |
||
703 | 57 | $firstWildcard--; |
|
704 | 54 | } |
|
705 | 54 | } |
|
706 | |||
707 | $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1); |
||
708 | $name = StringHelper::byteSubstr($path, -$namelen, $namelen); |
||
709 | |||
710 | 57 | if ($firstWildcard !== 0) { |
|
711 | 57 | if ($firstWildcard === false) { |
|
712 | $firstWildcard = StringHelper::byteLength($pattern); |
||
713 | 57 | } |
|
714 | 57 | // if the non-wildcard part is longer than the remaining pathname, surely it cannot match. |
|
715 | 56 | if ($firstWildcard > $namelen) { |
|
716 | return false; |
||
717 | } |
||
718 | 57 | ||
719 | 2 | if (strncmp($pattern, $name, $firstWildcard)) { |
|
720 | return false; |
||
721 | } |
||
722 | 57 | $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern)); |
|
723 | 57 | $name = StringHelper::byteSubstr($name, $firstWildcard, $namelen); |
|
724 | |||
725 | 4 | // 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. |
|
726 | 4 | if (empty($pattern) && empty($name)) { |
|
727 | return true; |
||
728 | } |
||
729 | 4 | } |
|
730 | 3 | ||
731 | $matchOptions = [ |
||
732 | 'filePath' => true |
||
733 | ]; |
||
734 | if ($flags & self::PATTERN_CASE_INSENSITIVE) { |
||
735 | 2 | $matchOptions['caseSensitive'] = false; |
|
736 | } |
||
737 | 2 | ||
738 | return StringHelper::matchWildcard($pattern, $name, $matchOptions); |
||
739 | } |
||
740 | |||
741 | 2 | /** |
|
742 | * Scan the given exclude list in reverse to see whether pathname |
||
743 | * should be ignored. The first match (i.e. the last on the list), if |
||
744 | * any, determines the fate. Returns the element which |
||
745 | * matched, or null for undecided. |
||
746 | * |
||
747 | * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources. |
||
748 | * |
||
749 | * @param string $basePath |
||
750 | * @param string $path |
||
751 | * @param array $excludes list of patterns to match $path against |
||
752 | * @return array|null null or one of $excludes item as an array with keys: 'pattern', 'flags' |
||
753 | * @throws InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern, flags, firstWildcard. |
||
754 | */ |
||
755 | private static function lastExcludeMatchingFromList($basePath, $path, $excludes) |
||
756 | { |
||
757 | foreach (array_reverse($excludes) as $exclude) { |
||
758 | 76 | if (is_string($exclude)) { |
|
759 | $exclude = self::parseExcludePattern($exclude, false); |
||
760 | 76 | } |
|
761 | 76 | if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) { |
|
762 | throw new InvalidArgumentException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'); |
||
763 | } |
||
764 | 76 | if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) { |
|
765 | continue; |
||
766 | } |
||
767 | 76 | ||
768 | if ($exclude['flags'] & self::PATTERN_NODIR) { |
||
769 | if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { |
||
770 | return $exclude; |
||
771 | 76 | } |
|
772 | 75 | continue; |
|
773 | 73 | } |
|
774 | |||
775 | 74 | if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { |
|
776 | return $exclude; |
||
777 | } |
||
778 | 57 | } |
|
779 | 57 | ||
780 | return null; |
||
781 | } |
||
782 | |||
783 | 75 | /** |
|
784 | * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead. |
||
785 | * @param string $pattern |
||
786 | * @param bool $caseSensitive |
||
787 | * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard |
||
788 | * @throws InvalidArgumentException |
||
789 | */ |
||
790 | private static function parseExcludePattern($pattern, $caseSensitive) |
||
791 | { |
||
792 | if (!is_string($pattern)) { |
||
793 | 82 | throw new InvalidArgumentException('Exclude/include pattern must be a string.'); |
|
794 | } |
||
795 | 82 | ||
796 | $result = [ |
||
797 | 'pattern' => $pattern, |
||
798 | 'flags' => 0, |
||
799 | 'firstWildcard' => false, |
||
800 | 82 | ]; |
|
801 | 82 | ||
802 | if (!$caseSensitive) { |
||
803 | $result['flags'] |= self::PATTERN_CASE_INSENSITIVE; |
||
804 | } |
||
805 | 82 | ||
806 | 1 | if (empty($pattern)) { |
|
807 | return $result; |
||
808 | } |
||
809 | 82 | ||
810 | if (strpos($pattern, '!') === 0) { |
||
811 | $result['flags'] |= self::PATTERN_NEGATIVE; |
||
812 | $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); |
||
813 | 82 | } |
|
814 | if (StringHelper::byteLength($pattern) && StringHelper::byteSubstr($pattern, -1, 1) === '/') { |
||
815 | $pattern = StringHelper::byteSubstr($pattern, 0, -1); |
||
816 | $result['flags'] |= self::PATTERN_MUSTBEDIR; |
||
817 | 82 | } |
|
818 | if (strpos($pattern, '/') === false) { |
||
819 | $result['flags'] |= self::PATTERN_NODIR; |
||
820 | } |
||
821 | 82 | $result['firstWildcard'] = self::firstWildcardInPattern($pattern); |
|
822 | 81 | if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { |
|
823 | $result['flags'] |= self::PATTERN_ENDSWITH; |
||
824 | 82 | } |
|
825 | 82 | $result['pattern'] = $pattern; |
|
826 | 69 | ||
827 | return $result; |
||
828 | 82 | } |
|
829 | |||
830 | 82 | /** |
|
831 | * Searches for the first wildcard character in the pattern. |
||
832 | * @param string $pattern the pattern to search in |
||
833 | * @return int|bool position of first wildcard character or false if not found |
||
834 | */ |
||
835 | private static function firstWildcardInPattern($pattern) |
||
836 | { |
||
837 | $wildcards = ['*', '?', '[', '\\']; |
||
838 | 82 | $wildcardSearch = function ($r, $c) use ($pattern) { |
|
839 | $p = strpos($pattern, $c); |
||
840 | 82 | ||
841 | 82 | return $r === false ? $p : ($p === false ? $r : min($r, $p)); |
|
842 | 82 | }; |
|
843 | |||
844 | 82 | return array_reduce($wildcards, $wildcardSearch, false); |
|
845 | 82 | } |
|
846 | |||
847 | 82 | /** |
|
848 | * @param array $options raw options |
||
849 | * @return array normalized options |
||
850 | * @since 2.0.12 |
||
851 | */ |
||
852 | protected static function normalizeOptions(array $options) |
||
853 | { |
||
854 | if (!array_key_exists('caseSensitive', $options)) { |
||
855 | 178 | $options['caseSensitive'] = true; |
|
856 | } |
||
857 | 178 | if (isset($options['except'])) { |
|
858 | 177 | foreach ($options['except'] as $key => $value) { |
|
859 | if (is_string($value)) { |
||
860 | 178 | $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']); |
|
861 | 62 | } |
|
862 | 62 | } |
|
863 | 62 | } |
|
864 | if (isset($options['only'])) { |
||
865 | foreach ($options['only'] as $key => $value) { |
||
866 | if (is_string($value)) { |
||
867 | 178 | $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']); |
|
868 | 80 | } |
|
869 | 80 | } |
|
870 | 80 | } |
|
871 | |||
872 | return $options; |
||
873 | } |
||
874 | |||
875 | 178 | /** |
|
876 | * Changes the Unix user and/or group ownership of a file or directory, and optionally the mode. |
||
877 | * Note: This function will not work on remote files as the file to be examined must be accessible |
||
878 | * via the server's filesystem. |
||
879 | * Note: On Windows, this function fails silently when applied on a regular file. |
||
880 | * @param string $path the path to the file or directory. |
||
881 | * @param string|array|int|null $ownership the user and/or group ownership for the file or directory. |
||
882 | * When $ownership is a string, the format is 'user:group' where both are optional. E.g. |
||
883 | * 'user' or 'user:' will only change the user, |
||
884 | * ':group' will only change the group, |
||
885 | * 'user:group' will change both. |
||
886 | * When $owners is an index array the format is [0 => user, 1 => group], e.g. `[$myUser, $myGroup]`. |
||
887 | * It is also possible to pass an associative array, e.g. ['user' => $myUser, 'group' => $myGroup]. |
||
888 | * In case $owners is an integer it will be used as user id. |
||
889 | * If `null`, an empty array or an empty string is passed, the ownership will not be changed. |
||
890 | * @param int|null $mode the permission to be set for the file or directory. |
||
891 | * If `null` is passed, the mode will not be changed. |
||
892 | * |
||
893 | * @since 2.0.43 |
||
894 | */ |
||
895 | public static function changeOwnership($path, $ownership, $mode = null) |
||
896 | { |
||
897 | if (!file_exists($path)) { |
||
898 | throw new InvalidArgumentException('Unable to change ownerhip, "' . $path . '" is not a file or directory.'); |
||
899 | } |
||
900 | |||
901 | if (empty($ownership) && $ownership !== 0 && $mode === null) { |
||
902 | return; |
||
903 | } |
||
904 | |||
905 | $user = $group = null; |
||
906 | if (!empty($ownership) || $ownership === 0 || $ownership === '0') { |
||
907 | if (is_int($ownership)) { |
||
908 | $user = $ownership; |
||
909 | } elseif (is_string($ownership)) { |
||
910 | $ownerParts = explode(':', $ownership); |
||
911 | $user = $ownerParts[0]; |
||
912 | if (count($ownerParts) > 1) { |
||
913 | $group = $ownerParts[1]; |
||
914 | } |
||
915 | } elseif (is_array($ownership)) { |
||
916 | $ownershipIsIndexed = ArrayHelper::isIndexed($ownership); |
||
917 | $user = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 0 : 'user'); |
||
918 | $group = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 1 : 'group'); |
||
919 | } else { |
||
920 | throw new InvalidArgumentException('$ownership must be an integer, string, array, or null.'); |
||
921 | } |
||
922 | } |
||
923 | |||
924 | if ($mode !== null) { |
||
925 | if (!is_int($mode)) { |
||
926 | throw new InvalidArgumentException('$mode must be an integer or null.'); |
||
927 | } |
||
928 | if (!chmod($path, $mode)) { |
||
929 | throw new Exception('Unable to change mode of "' . $path . '" to "0' . decoct($mode) . '".'); |
||
930 | } |
||
931 | } |
||
932 | if ($user !== null && $user !== '') { |
||
933 | if (is_numeric($user)) { |
||
934 | $user = (int) $user; |
||
935 | } elseif (!is_string($user)) { |
||
936 | throw new InvalidArgumentException('The user part of $ownership must be an integer, string, or null.'); |
||
937 | } |
||
938 | if (!chown($path, $user)) { |
||
939 | throw new Exception('Unable to change user ownership of "' . $path . '" to "' . $user . '".'); |
||
940 | } |
||
941 | } |
||
942 | if ($group !== null && $group !== '') { |
||
943 | if (is_numeric($group)) { |
||
944 | $group = (int) $group; |
||
945 | } elseif (!is_string($group)) { |
||
946 | throw new InvalidArgumentException('The group part of $ownership must be an integer, string or null.'); |
||
947 | } |
||
948 | if (!chgrp($path, $group)) { |
||
949 | throw new Exception('Unable to change group ownership of "' . $path . '" to "' . $group . '".'); |
||
950 | } |
||
951 | } |
||
952 | } |
||
953 | } |
||
954 |