1 <?php
2 /*
3 * This code is part of GOsa (https://gosa.gonicus.de)
4 * Copyright (C) 2005 Jan Wenzel
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 and 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;
60 $verbose= true;
62 // Check for empty parameters. If empty, set default.
63 if(strlen($path)==0) {
64 $path= "./";
65 }
66 if(strlen($pattern)==0) {
67 $pattern= "/.+\.(php[34]?$)|(inc$)/";
68 }
69 if(strlen($skip_dirs)==0) {
70 $skip_dirs= ".svn";
71 }
73 // Create file-list
74 $array= read_php_files($path,$pattern,$skip_dirs);
76 // Needed arrays
77 $functions= array();
79 // We need to include our own function here, because PHP seems to lock the current executed function
80 $user_functions= array('test_defined_functions');
82 // Scan files for used defined functions
83 foreach($array as $file) {
84 // Build array of user defined functions
85 $user_functions= array_merge($user_functions, extract_user_functions($file));
86 }
88 // Scan files for called functions
89 foreach($array as $file) {
90 // Build array of called functions
91 array_push($functions, extract_php_functions($file));
92 }
94 // Make Array 'flat'
95 $functions= array_values_recursive($functions);
97 // remove duplicates
98 $functions= array_unique($functions);
100 // remove 'non-real' functions
101 $reduced_functions= array_filter($functions,"filter_meta_functions");
103 // remove user-defined functions
104 $reduced_functions= array_diff($reduced_functions, $user_functions);
106 // Now the array keys are messed up. Build an array with 'normal' keys (1,2,3,...).
107 $functions= array_values($reduced_functions);
109 return $functions;
110 }
112 /**
113 * Scans files in $path and checks for calls of functions that are undefined.
114 * Edit $debug and $verbose to control output. Default is 'false' for both to disallow
115 * console functionality.
116 * @param $path Path to search for files in (default: "./")
117 * @param $pattern RegEx-Pattern for matching PHP-Files (default: "/.+\.(php[34]?$)|(inc$)/")
118 * @param $skip_dirs RegEx-Pattern for directories to ignore (default: ".svn")
119 * @return Array of invalid function-names (Scalars)
120 */
121 function test_functions($path="",$pattern="",$skip_dirs="") {
122 global $debug,$verbose;
124 // Check for empty parameters
125 if(strlen($path)==0) {
126 $path= "./";
127 }
129 if(strlen($pattern)==0) {
130 $pattern= "/.+\.(php[34]?$)|(inc$)/";
131 }
133 if(strlen($skip_dirs)==0) {
134 $skip_dirs= ".svn";
135 }
137 if($debug || $verbose) {
138 print("Scanning Directory...");
139 }
141 // Create file-list
142 $array= read_php_files($path,$pattern,$skip_dirs);
143 if($debug || $verbose) {
144 print("done\n");
145 }
147 $functions= array();
148 $user_functions= array();
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 * Reads $path for files matching $pattern but not in $skip_dirs.
204 * @param $path Path to search for files in (default: "./")
205 * @param $pattern RegEx-Pattern for matching PHP-Files (default: "/.+\.(php[34]?$)|(inc$)/")
206 * @param $skip_dirs RegEx-Pattern for directories to ignore (default: ".svn")
207 * @return array of content from PHP-Files scanned
208 */
209 function read_php_files($path,$pattern,$skip_dirs) {
210 global $debug,$verbose;
211 $result= array();
213 if(is_file($path)) {
214 $file= $path;
216 $php_content="";
218 // Open Filehandle to process
219 $fh= popen("`which php` -w $file", 'r');
220 while(!feof($fh)) {
221 // Read block-wise
222 $php_content.=fread($fh,1024);
223 }
225 // Close Filehandle
226 pclose($fh);
228 array_push($result, $php_content);
229 } else {
230 $files= list_directory($path,$pattern,$skip_dirs);
232 // Walk through files
233 foreach ($files as $key => $file) {
234 $php_content= "";
236 // Open Filehandle to process
237 $fh= popen("`which php` -w $file", 'r');
238 while(!feof($fh)) {
239 // Read block-wise
240 $php_content.=fread($fh,1024);
241 }
243 // Close Filehandle
244 pclose($fh);
246 array_push($result, $php_content);
247 }
248 }
250 return $result;
251 }
253 /** Only used as callback function when doing array_filter().
254 * @return true if $function is not meta-function or keyword, false otherwise
255 */
256 function filter_meta_functions ($function) {
257 $result= true;
258 if(is_meta_function($function)||is_keyword($function)) {
259 $result= false;
260 }
261 return $result;
262 }
264 /**
265 * Returns true if $function is a known php-function, false otherwise.
266 * @return true if $function is a known php-function, false otherwise.
267 */
268 function check_function($function) {
269 $result= false;
270 if(is_keyword($function)) {
271 $result= true;
272 } else if (is_meta_function($function)) {
273 $result= true;
274 } else if (function_exists($function)) {
275 $result= true;
276 }
277 return $result;
278 }
280 /** Returns true if $function is meta-function, false otherwise.
281 * @return true if $function is meta-function, false otherwise.
282 */
283 function is_meta_function($function) {
284 $meta_functions= array(
285 "print","array","isset","exit","unset",
286 "die","list","eval","empty"
287 );
288 return(in_array($function,$meta_functions));
289 }
291 /** Returns true if $function is keyword, false otherwise.
292 * @return true if $function is keyword, false otherwise.
293 */
294 function is_keyword($function) {
295 $keywords= array(
296 "if","else","elseif","while","do","case",
297 "for","foreach","break","continue",
298 "switch","declare","return","require",
299 "include","require_once","include_once",
300 "try","catch"
301 );
303 return(in_array($function,$keywords));
304 }
306 /**
307 * Returns array of called functions.
308 * @param $string PHP-Code
309 * @return array of called functions.
310 */
311 function extract_php_functions($string) {
312 // Function names have to be A-z or _ for the first letter
313 // plus 0-9, +, - for the following letters
314 // Length is 2 minimum
315 $rx_name="[A-Za-z_][A-Za-z0-9_+-]+";
317 // Function calls can be after ',', '.', '(' or spaces
318 // Note: Method calls are not detected - this is wanted
319 $rx_function="[.,(\s]\s*($rx_name)\s*\(";
321 // Get code inside <?php ... (question-mark)>
322 $string= trim(array_pop(get_php_code($string)));
323 $string= remove_strings($string);
324 $result= array();
325 preg_match_all("/$rx_function/",$string,$result);
326 array_shift($result);
329 // We need to check if "function" is actual a class created by new operator,
330 // but negative lookbehind isn't possible with php yet.
331 // So we must scan the array again and remove the found keys later (not while we're walking through).
332 $classes= array();
333 foreach($result[0] as $key => $function) {
334 $match= array();
335 if(preg_match("/new\s+($function)/",$string,$match)) {
336 array_shift($match);
337 array_push($classes,$match);
338 }
339 }
341 // We need to manually unset the keys that we found above
342 foreach($classes as $key => $value) {
343 for($i=0;$i<count($value);$i++) {
344 if(isset($result[0])) {
345 unset($result[0][array_search($value[$i],$result[0])]);
346 }
347 }
348 }
350 return $result;
351 }
353 /**
354 * Extracts function-definitions from php-code
355 * @param $string php-code to extract function definitions from
356 * @return array of functions defined
357 */
358 function extract_user_functions($string) {
359 $rx_name="[A-Za-z_][A-Za-z0-9_+-]*";
360 $rx_user_function="function\s+($rx_name)\s*\(";
362 // Get code inside <?php ... (question-mark)>
363 $string= trim(array_pop(get_php_code($string)));
364 $string= remove_strings($string);
365 $result= array();
366 preg_match_all("/$rx_user_function/s",$string,$result);
367 array_shift($result);
369 return $result= array_pop($result);
370 }
372 /**
373 * Returns php-code without <?php ... markers.
374 * @return php-code without <?php ... markers.
375 */
376 function get_php_code($string) {
377 $array= array();
378 preg_match_all('/\<(\?php|\%)(.*)(\?|\%)\>/sx', $string, $array);
380 // Pop the first two entries as we don't want them
381 $match= array_shift($array);
382 $match= array_shift($array);
384 // Do the same for the last entry
385 $match= array_pop($array);
387 // The array $result only contains regex group (.*) now
388 return array_pop($array);
389 }
391 /**
392 * Removes all double and single quotes strings from sourcecode.
393 * Returns 'print ();' for 'print ("hello world!\n");'
394 * AND:
395 * Returns '$message= sprintf(_()$foo,$bar); for
396 * $message= sprintf(_("Command '%s', specified as POSTREMOVE for plugin '%s' doesn't seem to exist.")$foo,$bar);
397 * (Note the "doesn't")
398 * @param $string code with strings
399 * @return code with strings removed
400 */
401 function remove_strings($string) {
402 $result= "";
404 $inside_string=false;
405 $inside_string_uni=false;
406 $inside_string_double=false;
408 // Walk through $string
409 for($i=0;$i<strlen($string);$i++) {
410 if($string[$i]=="'" && ($i>0 && ($string[$i-1] != "\\"))) {
411 if(!$inside_string_uni) {
412 // We're now inside
413 if(!$inside_string_double) {
414 $inside_string_uni= true;
415 $inside_string= true;
416 }
417 } else {
418 // We're now outside
419 $inside_string_uni= false;
420 if(!$inside_string_double) {
421 $inside_string= false;
422 }
423 }
424 } else if($string[$i]=='"' && ($i>0 && ($string[$i-1] != "\\"))) {
425 if(!$inside_string_double) {
426 // We're now inside
427 if(!$inside_string_uni) {
428 $inside_string_double= true;
429 $inside_string= true;
430 }
431 } else {
432 // We're now outside
433 $inside_string_double= false;
434 if(!$inside_string_uni) {
435 $inside_string= false;
436 }
437 }
438 } else {
439 // Push char to $result if not inside string
440 $result.= (!$inside_string)?$string[$i]:"";
441 }
442 }
444 // Return string
445 return $result;
446 }
448 /**
449 * Scans directory $dir for files (filenames) matching regular expression $pattern
450 * @param $dir Initial Directory to start scan
451 * @param $pattern Regex-Pattern to match on files to scan
452 * @param $skip_dirs Regex-Patten to match on directories to skip
453 * @return file list
454 */
455 function list_directory($dir, $pattern, $skip_dirs) {
456 $file_list= '';
457 $stack[]= $dir;
458 while ($stack) {
459 $current_dir= array_pop($stack);
460 if ($dh= opendir($current_dir)) {
461 while (($file= readdir($dh)) !== false) {
462 if ($file !== '.' && $file !== '..' && preg_match("/$skip_dirs/",$file)==0) {
463 $current_file= "{$current_dir}/{$file}";
464 if (is_file($current_file) && preg_match($pattern, $current_file)) {
465 $file_list[]= "{$current_dir}/{$file}";
466 } elseif (is_dir($current_file)) {
467 $stack[]= $current_file;
468 }
469 }
470 }
471 }
472 }
473 return $file_list;
474 }
476 /**
477 * 'Flats' a multi-dimensional array. Keys are newly (incrementally) created by PHP.
478 */
479 function array_values_recursive($array) {
480 $arrayValues = array();
482 foreach ($array as $value) {
483 if (is_scalar($value) OR is_resource($value)) {
484 $arrayValues[] = $value;
485 } elseif (is_array($value)) {
486 $arrayValues = array_merge($arrayValues, array_values_recursive($value));
487 }
488 }
490 return $arrayValues;
491 }
493 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
494 ?>