23141788d8b64f0795daf4826b395968060f0cf3
1 <?
2 /*
3 * This code is part of GOsa (https://gosa.gonicus.de)
4 * Copyright (C) 2003 Cajus Pollmeier
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
21 /* This file builds a list of used php_functions in all files matching $pattern
22 * (walks recursively through current directory but skips dirs matching $skip_dirs).
23 *
24 * 1. builds an array of filenames
25 * 2. get contents from files
26 * 2. fetches user defined functions using content
27 * 3. fetches functions calls using content
28 * 4. checks function calls for matching known functions
29 */
31 /**
32 * Print status messages if set to true (useful for command-line use).
33 * default: false */
34 $verbose= false;
36 /**
37 * Print debug messages if set to true.
38 * default: false
39 */
40 $debug= false;
42 /**
43 * Used by command-line client.
44 */
45 function cmd_test_functions($path) {
46 global $verbose;
47 $verbose= true;
48 test_functions($path);
49 }
51 /**
52 * Scans files in $path an checks for calls of functions.
53 * @param $path Path to search for files in (default: "./")
54 * @param $pattern RegEx-Pattern for matching PHP-Files (default: "/.+\.(php[34]?$)|(inc$)/")
55 * @param $skip_dirs RegEx-Pattern for directories to ignore (default: ".svn")
56 * @return Array of used function-names
57 */
58 function test_defined_functions($path="",$pattern="",$skip_dirs="") {
59 global $debug,$verbose;
61 // Check for empty parameters
62 if(strlen($path)==0) {
63 $path= "./";
64 }
66 if(strlen($pattern)==0) {
67 $pattern= "/.+\.(php[34]?$)|(inc$)/";
68 }
70 if(strlen($skip_dirs)==0) {
71 $skip_dirs= ".svn";
72 }
74 // Create file-list
75 $array= read_php_files($path,$pattern,$skip_dirs);
77 $functions= array();
78 // We need to add our own method here, cause PHP doesn't scan 'itself' :-(
79 $user_functions= array('test_defined_functions');
81 // Scan files for used defined functions
82 foreach($array as $file) {
83 // Build array of user defined functions
84 $user_functions= array_merge($user_functions, extract_user_functions($file));
85 }
87 // Scan files for called functions
88 foreach($array as $file) {
89 // Build array of called functions
90 array_push($functions, extract_php_functions($file));
91 }
93 // Make Array 'flat'
94 $functions= array_values_recursive($functions);
96 // remove duplicates
97 $functions= array_unique($functions);
99 // remove 'non-real' functions
100 $reduced_functions= array_filter($functions,"filter_meta_functions");
102 // remove user-defined functions
103 $reduced_functions= array_diff($reduced_functions, $user_functions);
105 // Now the array keys are messed up. Build an array with 'normal' keys (1,2,3,...).
106 $functions= array_values($reduced_functions);
108 return $functions;
109 }
111 /**
112 * Scans files in $path an checks for calls of functions that are undefined.
113 * Edit $debug and $verbose to control output. Default is 'false' for both to disallow
114 * console functionality.
115 * @param $path Path to search for files in (default: "./")
116 * @param $pattern RegEx-Pattern for matching PHP-Files (default: "/.+\.(php[34]?$)|(inc$)/")
117 * @param $skip_dirs RegEx-Pattern for directories to ignore (default: ".svn")
118 * @return Array of invalid function-names (Scalars)
119 */
120 function test_functions($path="",$pattern="",$skip_dirs="") {
121 global $debug,$verbose;
123 // Check for empty parameters
124 if(strlen($path)==0) {
125 $path= "./";
126 }
128 if(strlen($pattern)==0) {
129 $pattern= "/.+\.(php[34]?$)|(inc$)/";
130 }
132 if(strlen($skip_dirs)==0) {
133 $skip_dirs= ".svn";
134 }
136 if($debug || $verbose) {
137 print("Scanning Directory...");
138 }
140 // Create file-list
141 $array= read_php_files($path,$pattern,$skip_dirs);
142 if($debug || $verbose) {
143 print("done\n");
144 }
146 $functions= array();
147 // We need to add our own method here, cause PHP doesn't scan 'itself' :-(
148 $user_functions= array('test_functions');
149 $invalid_functions= array();
151 if($debug || $verbose) {
152 print("Importing user functions...");
153 }
155 // Scan files for used defined functions
156 foreach($array as $file) {
157 // Build array of user defined functions
158 $user_functions= array_merge($user_functions, extract_user_functions($file));
159 }
161 if($debug || $verbose) {
162 print("done\n");
163 }
165 if($debug || $verbose) {
166 print("Checking function calls...");
167 }
169 // Scan files for called functions
170 foreach($array as $file) {
171 // Build array of called functions
172 $functions= extract_php_functions($file);
173 foreach($functions as $key => $value) {
174 foreach($value as $fn_key => $function_name) {
175 if(!check_function($function_name) &&
176 // array_search returns key of entry - this can be 0
177 !is_int(array_search($function_name,$user_functions))
178 ) {
179 array_push($invalid_functions, $function_name);
180 }
181 }
182 }
183 }
185 if($debug || $verbose) {
186 print("done\n");
187 }
189 // remove duplicates
190 $invalid_functions= array_unique($invalid_functions);
192 if($debug || $verbose) {
193 foreach($invalid_functions as $function_name) {
194 print("\nFound invalid function ".$function_name);
195 }
196 print("\n");
197 }
199 return $invalid_functions;
200 }
202 /**
203 * @return array of content from PHP-Files scanned
204 */
205 function read_php_files($path,$pattern,$skip_dirs) {
206 $result= array();
208 if(is_file($path)) {
209 $file= $path;
211 $php_content= "";
213 // Open Filehandle to process
214 $fh= popen("`which php` -w $file", 'r');
215 while(!feof($fh)) {
216 // Read block-wise
217 $php_content.=fread($fh,1024);
218 }
220 // Close Filehandle
221 pclose($fh);
223 array_push($result, $php_content);
224 } else {
225 $files= list_directory($path,$pattern,$skip_dirs);
227 // Walk through files
228 foreach ($files as $key => $file) {
229 $php_content= "";
231 // Open Filehandle to process
232 $fh= popen("`which php` -w $file", 'r');
233 while(!feof($fh)) {
234 // Read block-wise
235 $php_content.=fread($fh,1024);
236 }
238 // Close Filehandle
239 pclose($fh);
241 array_push($result, $php_content);
242 }
243 }
245 return $result;
246 }
248 /** Only used as callback function when doing array_filter().
249 * @return true if $function is not meta-function or keyword, false otherwise
250 */
251 function filter_meta_functions ($function) {
252 $result= true;
253 if(is_meta_function($function)||is_keyword($function)) {
254 $result= false;
255 }
256 return $result;
257 }
259 function check_function($function) {
260 $result= false;
261 if(is_keyword($function)) {
262 $result= true;
263 } else if (is_meta_function($function)) {
264 $result= true;
265 } else if (function_exists($function)) {
266 $result= true;
267 }
268 return $result;
269 }
271 /** @return true if $function is meta-function, false otherwise */
272 function is_meta_function($function) {
273 $meta_functions= array(
274 "print","array","isset","exit","unset",
275 "die","list","eval","empty"
276 );
277 return(in_array($function,$meta_functions));
278 }
280 /** @return true if $param is keyword, false otherwise */
281 function is_keyword($function) {
282 $keywords= array(
283 "if","else","elseif","while","do","case",
284 "for","foreach","break","continue",
285 "switch","declare","return","require",
286 "include","require_once","include_once",
287 "try","catch"
288 );
290 return(in_array($function,$keywords));
291 }
293 function extract_php_functions($string) {
294 // Function names have to be A-z or _ for the first letter
295 // plus 0-9, +, - for the following letters
296 // Length is 2 minimum
297 $rx_name="[A-Za-z_][A-Za-z0-9_+-]+";
299 // Function calls can be after ',', '.', '(' or spaces
300 // Note: Method calls are not detected - this is wanted
301 $rx_function="[.,(\s]\s*($rx_name)\s*\(";
303 // Get code inside <?php ... (question-mark)>
304 $string= trim(array_pop(get_php_code($string)));
305 $string= remove_strings($string);
306 $result= array();
307 preg_match_all("/$rx_function/",$string,$result);
308 array_shift($result);
311 // We need to check if "function" is actual a class created by new operator
312 // negative lookbehind isn't possible with php yet
313 $classes= array();
314 foreach($result[0] as $key => $function) {
315 $match= array();
316 if(preg_match("/new\s+($function)/",$string,$match)) {
317 array_shift($match);
318 array_push($classes,$match);
319 }
320 }
322 // We need to manually unset the keys that we found above
323 foreach($classes as $key => $value) {
324 for($i=0;$i<count($value);$i++) {
325 if(isset($result[0])) {
326 unset($result[0][array_search($value[$i],$result[0])]);
327 }
328 }
329 }
331 return $result;
332 }
334 /**
335 * Extracts function-calls in php-code
336 * @param $string php-code to extract function calls from
337 * @return array of functions called
338 */
339 function extract_user_functions($string) {
340 $rx_name="[A-Za-z_][A-Za-z0-9_+-]*";
341 $rx_user_function="function\s+($rx_name)\s*\(";
343 // Get code inside <?php ... (question-mark)>
344 $string= trim(array_pop(get_php_code($string)));
345 $string= remove_strings($string);
346 $result= array();
347 preg_match_all("/$rx_user_function/s",$string,$result);
348 array_shift($result);
350 return $result= array_pop($result);
351 }
353 /**
354 * @return php-code without <?php ... markers
355 */
356 function get_php_code($string) {
357 $array= array();
358 preg_match_all('/\<(\?php|\%)(.*)(\?|\%)\>/sx', $string, $array);
360 // Pop the first two entries as we don't want them
361 $match= array_shift($array);
362 $match= array_shift($array);
364 // Do the same for the last entry
365 $match= array_pop($array);
367 // The array $result only contains regex group (.*) now
368 return array_pop($array);
369 }
371 /**
372 * Returns 'print ();' for 'print ("hello world!\n");'
373 * AND:
374 * Returns '$message= sprintf(_()...); for
375 * $message= sprintf(_("Command '%s', specified as POSTREMOVE for plugin '%s' doesn't seem to exist.")...);
376 * Note the "doesn't"
377 * @param $string code with strings
378 * @return code with strings removed
379 */
380 function remove_strings($string) {
381 $result= "";
383 $inside_string=false;
384 $inside_string_uni=false;
385 $inside_string_double=false;
387 // Walk through $string
388 for($i=0;$i<strlen($string);$i++) {
389 if($string[$i]=="'" && ($i>0 && ($string[$i-1] != "\\"))) {
390 if(!$inside_string_uni) {
391 // We're now inside
392 if(!$inside_string_double) {
393 $inside_string_uni= true;
394 $inside_string= true;
395 }
396 } else {
397 // We're now outside
398 $inside_string_uni= false;
399 if(!$inside_string_double) {
400 $inside_string= false;
401 }
402 }
403 } else if($string[$i]=='"' && ($i>0 && ($string[$i-1] != "\\"))) {
404 if(!$inside_string_double) {
405 // We're now inside
406 if(!$inside_string_uni) {
407 $inside_string_double= true;
408 $inside_string= true;
409 }
410 } else {
411 // We're now outside
412 $inside_string_double= false;
413 if(!$inside_string_uni) {
414 $inside_string= false;
415 }
416 }
417 } else {
418 // Push char to $result if not inside string
419 $result.= (!$inside_string)?$string[$i]:"";
420 }
421 }
423 // Return string
424 return $result;
425 }
427 /**
428 * Iteratively scans directory $dir for files (filenames) matching regular expression $pattern
429 * @param $dir Initial Directory to start scan
430 * @param $pattern Regex-Pattern to match on files to scan
431 * @param $skip_dirs Regex-Patten to match on directories to skip
432 * @return file list
433 */
434 function list_directory($dir, $pattern, $skip_dirs) {
435 $file_list= '';
436 $stack[]= $dir;
437 while ($stack) {
438 $current_dir= array_pop($stack);
439 if ($dh= opendir($current_dir)) {
440 while (($file= readdir($dh)) !== false) {
441 if ($file !== '.' && $file !== '..' && preg_match("/$skip_dirs/",$file)==0) {
442 $current_file= "{$current_dir}/{$file}";
443 if (is_file($current_file) && preg_match($pattern, $current_file)) {
444 $file_list[]= "{$current_dir}/{$file}";
445 } elseif (is_dir($current_file)) {
446 $stack[]= $current_file;
447 }
448 }
449 }
450 }
451 }
452 return $file_list;
453 }
455 function array_values_recursive($array) {
456 $arrayValues = array();
458 foreach ($array as $value) {
459 if (is_scalar($value) OR is_resource($value)) {
460 $arrayValues[] = $value;
461 } elseif (is_array($value)) {
462 $arrayValues = array_merge($arrayValues, array_values_recursive($value));
463 }
464 }
466 return $arrayValues;
467 }
469 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
470 ?>