@@ -26,41 +26,34 @@ public function collectFiles(
2626 array $ paths ,
2727 ?DetectorInterface $ detector = null ,
2828 ?array $ excludePatterns = null ,
29+ bool $ recursive = false ,
2930 ): array {
3031 $ allFiles = [];
3132 foreach ($ paths as $ path ) {
3233 if (!(new Filesystem ())->exists ($ path )) {
33- $ this ->logger ->error ('The provided path " ' .$ path .'" is not a valid directory. ' );
34+ $ this ->logger ? ->error('The provided path " ' .$ path .'" is not a valid directory. ' );
3435 continue ;
3536 }
3637
3738 foreach (ParserRegistry::getAvailableParsers () as $ parserClass ) {
38- $ globFiles = glob ($ path .'/* ' );
39- if (false === $ globFiles ) {
39+ $ files = $ this ->findFiles ($ path , $ parserClass ::getSupportedFileExtensions (), $ recursive );
40+ if (empty ($ files )) {
41+ $ this ->logger ?->debug('No files found for parser class " ' .$ parserClass .'" in path " ' .$ path .'". ' );
4042 continue ;
4143 }
4244
43- $ files = array_filter (
44- $ globFiles ,
45- static fn ($ file ) => in_array (
46- pathinfo ($ file , PATHINFO_EXTENSION ),
47- $ parserClass ::getSupportedFileExtensions (),
48- true
49- )
50- );
51-
5245 if ($ excludePatterns ) {
5346 $ files = array_filter (
5447 $ files ,
5548 static fn ($ file ) => !array_filter (
5649 $ excludePatterns ,
57- static fn ($ pattern ) => fnmatch ($ pattern , basename ($ file ))
50+ static fn ($ pattern ) => fnmatch ($ pattern , basename (( string ) $ file ))
5851 )
5952 );
6053 }
6154
6255 if (empty ($ files )) {
63- $ this ->logger ->debug ('No files found for parser class " ' .$ parserClass .'" in path " ' .$ path .'". ' );
56+ $ this ->logger ? ->debug('No files found for parser class " ' .$ parserClass .'" in path " ' .$ path .'". ' );
6457 continue ;
6558 }
6659
@@ -80,4 +73,92 @@ public function collectFiles(
8073
8174 return $ allFiles ;
8275 }
76+
77+ /**
78+ * Find files in a directory, optionally recursively.
79+ *
80+ * @param string[] $supportedExtensions
81+ *
82+ * @return string[]
83+ */
84+ private function findFiles (string $ path , array $ supportedExtensions , bool $ recursive ): array
85+ {
86+ if (!$ recursive ) {
87+ $ globFiles = glob ($ path .'/* ' );
88+ if (false === $ globFiles ) {
89+ $ this ->logger ?->warning('Failed to glob files in path: ' .$ path );
90+
91+ return [];
92+ }
93+
94+ return array_filter (
95+ $ globFiles ,
96+ static fn ($ file ) => in_array (
97+ pathinfo ($ file , PATHINFO_EXTENSION ),
98+ $ supportedExtensions ,
99+ true
100+ )
101+ );
102+ }
103+
104+ $ normalizedPath = $ this ->normalizePath ($ path );
105+ if (!$ this ->isPathSafe ($ normalizedPath )) {
106+ $ this ->logger ?->warning('Skipping potentially unsafe path: ' .$ path );
107+
108+ return [];
109+ }
110+
111+ $ files = [];
112+
113+ try {
114+ $ iterator = new \RecursiveIteratorIterator (
115+ new \RecursiveDirectoryIterator ($ normalizedPath , \RecursiveDirectoryIterator::SKIP_DOTS ),
116+ \RecursiveIteratorIterator::LEAVES_ONLY
117+ );
118+
119+ foreach ($ iterator as $ file ) {
120+ $ filePath = $ file ->getPathname ();
121+ $ extension = pathinfo ((string ) $ filePath , PATHINFO_EXTENSION );
122+
123+ if (in_array ($ extension , $ supportedExtensions , true ) && is_file ($ filePath )) {
124+ $ files [] = $ filePath ;
125+ }
126+ }
127+ } catch (\Exception $ e ) {
128+ $ this ->logger ?->error('Error during recursive file search: ' .$ e ->getMessage ());
129+
130+ return [];
131+ }
132+
133+ return $ files ;
134+ }
135+
136+ /**
137+ * Normalize a file path to prevent path traversal attacks.
138+ */
139+ private function normalizePath (string $ path ): string
140+ {
141+ $ resolved = realpath ($ path );
142+ if (false !== $ resolved ) {
143+ return $ resolved ;
144+ }
145+
146+ return rtrim ($ path , '/ \\' );
147+ }
148+
149+ /**
150+ * Basic path safety check to prevent obvious security issues.
151+ */
152+ private function isPathSafe (string $ path ): bool
153+ {
154+ $ dangerousPaths = ['/etc ' , '/usr ' , '/bin ' , '/sbin ' , '/proc ' , '/sys ' , '/private/etc ' ];
155+
156+ foreach ($ dangerousPaths as $ dangerousPath ) {
157+ if (str_starts_with ($ path , $ dangerousPath )) {
158+ return false ;
159+ }
160+ }
161+
162+ return substr_count ($ path , '/ ' ) + substr_count ($ path , '\\' ) <= 20 ;
163+ }
83164}
0 commit comments