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 $user_functions= array();
80 // Scan files for used defined functions
81 foreach($array as $file) {
82 // Build array of user defined functions
83 $user_functions= array_merge($user_functions, extract_user_functions($file));
84 }
86 // Scan files for called functions
87 foreach($array as $file) {
88 // Build array of called functions
89 array_push($functions, extract_php_functions($file));
90 }
92 // Make Array 'flat'
93 $functions= array_values_recursive($functions);
95 // remove duplicates
96 $functions= array_unique($functions);
98 // remove 'non-real' functions
99 $reduced_functions= array_filter($functions,"filter_meta_functions");
101 // remove user-defined functions
102 $reduced_functions= array_diff($reduced_functions, $user_functions);
104 // Now the array keys are messed up. Build an array with 'normal' keys (1,2,3,...).
105 $functions= array_values($reduced_functions);
107 return $functions;
108 }
110 /**
111 * Scans files in $path an checks for calls of functions that are undefined.
112 * Edit $debug and $verbose to control output. Default is 'false' for both to disallow
113 * console functionality.
114 * @param $path Path to search for files in (default: "./")
115 * @param $pattern RegEx-Pattern for matching PHP-Files (default: "/.+\.(php[34]?$)|(inc$)/")
116 * @param $skip_dirs RegEx-Pattern for directories to ignore (default: ".svn")
117 * @return Array of invalid function-names (Scalars)
118 */
119 function test_functions($path="",$pattern="",$skip_dirs="") {
120 global $debug,$verbose;
122 // Check for empty parameters
123 if(strlen($path)==0) {
124 $path= "./";
125 }
127 if(strlen($pattern)==0) {
128 $pattern= "/.+\.(php[34]?$)|(inc$)/";
129 }
131 if(strlen($skip_dirs)==0) {
132 $skip_dirs= ".svn";
133 }
135 if($debug || $verbose) {
136 print("Scanning Directory...");
137 }
139 // Create file-list
140 $array= read_php_files($path,$pattern,$skip_dirs);
141 if($debug || $verbose) {
142 print("done\n");
143 }
145 $functions= array();
146 $user_functions= array();
147 $invalid_functions= array();
149 if($debug || $verbose) {
150 print("Importing user functions...");
151 }
153 // Scan files for used defined functions
154 foreach($array as $file) {
155 // Build array of user defined functions
156 $user_functions= array_merge($user_functions, extract_user_functions($file));
157 }
159 if($debug || $verbose) {
160 print("done\n");
161 }
163 if($debug || $verbose) {
164 print("Checking function calls...");
165 }
167 // Scan files for called functions
168 foreach($array as $file) {
169 // Build array of called functions
170 $functions= extract_php_functions($file);
171 foreach($functions as $key => $value) {
172 foreach($value as $fn_key => $function_name) {
173 if(!check_function($function_name) &&
174 // array_search returns key of entry - this can be 0
175 !is_int(array_search($function_name,$user_functions))
176 ) {
177 array_push($invalid_functions, $function_name);
178 }
179 }
180 }
181 }
183 if($debug || $verbose) {
184 print("done\n");
185 }
187 // remove duplicates
188 $invalid_functions= array_unique($invalid_functions);
190 if($debug || $verbose) {
191 foreach($invalid_functions as $function_name) {
192 print("\nFound invalid function ".$function_name);
193 }
194 print("\n");
195 }
197 return $invalid_functions;
198 }
200 /**
201 * @see
202 * @return array of content from PHP-Files scanned
203 */
204 function read_php_files($path,$pattern,$skip_dirs) {
205 $result= array();
207 if(is_file($path)) {
208 $file= $path;
210 $php_content= "";
212 // Open Filehandle to process
213 $fh= popen("`which php` -w $file", 'r');
214 while(!feof($fh)) {
215 // Read block-wise
216 $php_content.=fread($fh,1024);
217 }
219 // Close Filehandle
220 pclose($fh);
222 array_push($result, $php_content);
223 } else {
224 $files= list_directory($path,$pattern,$skip_dirs);
226 // Walk through files
227 foreach ($files as $key => $file) {
228 $php_content= "";
230 // Open Filehandle to process
231 $fh= popen("`which php` -w $file", 'r');
232 while(!feof($fh)) {
233 // Read block-wise
234 $php_content.=fread($fh,1024);
235 }
237 // Close Filehandle
238 pclose($fh);
240 array_push($result, $php_content);
241 }
242 }
244 return $result;
245 }
247 /** Only used as callback function when doing array_filter().
248 * @return true if $function is not meta-function or keyword, false otherwise
249 */
250 function filter_meta_functions ($function) {
251 $result= true;
252 if(is_meta_function($function)||is_keyword($function)) {
253 $result= false;
254 }
255 return $result;
256 }
258 function check_function($function) {
259 $result= false;
260 if(is_keyword($function)) {
261 $result= true;
262 } else if (is_meta_function($function)) {
263 $result= true;
264 } else if (function_exists($function)) {
265 $result= true;
266 }
267 return $result;
268 }
270 /** @return true if $function is meta-function, false otherwise */
271 function is_meta_function($function) {
272 $meta_functions= array(
273 "print","array","isset","exit","unset",
274 "die","list","eval","empty"
275 );
276 return(in_array($function,$meta_functions));
277 }
279 /** @return true if $param is keyword, false otherwise */
280 function is_keyword($function) {
281 $keywords= array(
282 "if","else","elseif","while","do","case",
283 "for","foreach","break","continue",
284 "switch","declare","return","require",
285 "include","require_once","include_once",
286 "try","catch"
287 );
289 return(in_array($function,$keywords));
290 }
292 function extract_php_functions($string) {
293 // Function names have to be A-z or _ for the first letter
294 // plus 0-9, +, - for the following letters
295 // Length is 2 minimum
296 $rx_name="[A-Za-z_][A-Za-z0-9_+-]+";
298 // Function calls can be after ',', '.', '(' or spaces
299 // Note: Method calls are not detected - this is wanted
300 $rx_function="[.,(\s]\s*($rx_name)\s*\(";
302 // Get code inside <?php ... (question-mark)>
303 $string= trim(array_pop(get_php_code($string)));
304 $string= remove_strings($string);
305 $result= array();
306 preg_match_all("/$rx_function/",$string,$result);
307 array_shift($result);
310 // We need to check if "function" is actual a class created by new operator
311 // negative lookbehind isn't possible with php yet
312 $classes= array();
313 foreach($result[0] as $key => $function) {
314 $match= array();
315 if(preg_match("/new\s+($function)/",$string,$match)) {
316 array_shift($match);
317 array_push($classes,$match);
318 }
319 }
321 // We need to manually unset the keys that we found above
322 foreach($classes as $key => $value) {
323 for($i=0;$i<count($value);$i++) {
324 if(isset($result[0])) {
325 unset($result[0][array_search($value[$i],$result[0])]);
326 }
327 }
328 }
330 return $result;
331 }
333 /**
334 * Extracts function-calls in php-code
335 * @param $string php-code to extract function calls from
336 * @return array of functions called
337 */
338 function extract_user_functions($string) {
339 $rx_name="[A-Za-z_][A-Za-z0-9_+-]*";
340 $rx_user_function="function\s+($rx_name)\s*\(";
342 // Get code inside <?php ... (question-mark)>
343 $string= trim(array_pop(get_php_code($string)));
344 $string= remove_strings($string);
345 $result= array();
346 preg_match_all("/$rx_user_function/s",$string,$result);
347 array_shift($result);
349 return $result= array_pop($result);
350 }
352 /**
353 * @return php-code without <?php ... markers
354 */
355 function get_php_code($string) {
356 $array= array();
357 preg_match_all('/\<(\?php|\%)(.*)(\?|\%)\>/sx', $string, $array);
359 // Pop the first two entries as we don't want them
360 $match= array_shift($array);
361 $match= array_shift($array);
363 // Do the same for the last entry
364 $match= array_pop($array);
366 // The array $result only contains regex group (.*) now
367 return array_pop($array);
368 }
370 /**
371 * Returns 'print ();' for 'print ("hello world!\n");'
372 * AND:
373 * Returns '$message= sprintf(_()...); for
374 * $message= sprintf(_("Command '%s', specified as POSTREMOVE for plugin '%s' doesn't seem to exist.")...);
375 * Note the "doesn't"
376 * @param $string code with strings
377 * @return code with strings removed
378 */
379 function remove_strings($string) {
380 $result= "";
382 $inside_string=false;
383 $inside_string_uni=false;
384 $inside_string_double=false;
386 // Walk through $string
387 for($i=0;$i<strlen($string);$i++) {
388 if($string[$i]=="'" && ($i>0 && ($string[$i-1] != "\\"))) {
389 if(!$inside_string_uni) {
390 // We're now inside
391 if(!$inside_string_double) {
392 $inside_string_uni= true;
393 $inside_string= true;
394 }
395 } else {
396 // We're now outside
397 $inside_string_uni= false;
398 if(!$inside_string_double) {
399 $inside_string= false;
400 }
401 }
402 } else if($string[$i]=='"' && ($i>0 && ($string[$i-1] != "\\"))) {
403 if(!$inside_string_double) {
404 // We're now inside
405 if(!$inside_string_uni) {
406 $inside_string_double= true;
407 $inside_string= true;
408 }
409 } else {
410 // We're now outside
411 $inside_string_double= false;
412 if(!$inside_string_uni) {
413 $inside_string= false;
414 }
415 }
416 } else {
417 // Push char to $result if not inside string
418 $result.= (!$inside_string)?$string[$i]:"";
419 }
420 }
422 // Return string
423 return $result;
424 }
426 /**
427 * Iteratively scans directory $dir for files (filenames) matching regular expression $pattern
428 * @param $dir Initial Directory to start scan
429 * @param $pattern Regex-Pattern to match on files to scan
430 * @param $skip_dirs Regex-Patten to match on directories to skip
431 * @return file list
432 */
433 function list_directory($dir, $pattern, $skip_dirs) {
434 $file_list= '';
435 $stack[]= $dir;
436 while ($stack) {
437 $current_dir= array_pop($stack);
438 if ($dh= opendir($current_dir)) {
439 while (($file= readdir($dh)) !== false) {
440 if ($file !== '.' && $file !== '..' && preg_match("/$skip_dirs/",$file)==0) {
441 $current_file= "{$current_dir}/{$file}";
442 if (is_file($current_file) && preg_match($pattern, $current_file)) {
443 $file_list[]= "{$current_dir}/{$file}";
444 } elseif (is_dir($current_file)) {
445 $stack[]= $current_file;
446 }
447 }
448 }
449 }
450 }
451 return $file_list;
452 }
454 function array_values_recursive($array) {
455 $arrayValues = array();
457 foreach ($array as $value) {
458 if (is_scalar($value) OR is_resource($value)) {
459 $arrayValues[] = $value;
460 } elseif (is_array($value)) {
461 $arrayValues = array_merge($arrayValues, array_values_recursive($value));
462 }
463 }
465 return $arrayValues;
466 }
468 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
469 ?>