#!/usr/bin/php array(), 'output' => new OutputModule() ); $stderr = fopen( 'php://stderr', 'w' ); $config = array( 'svn' => false, 'modules' => array(), 'output_format' => 'text', 'output_file' => 'php://stdout', 'quiet' => false, ); $help = "Usage: {$argv[0]} [options] file|path [file|path ...] {$argv[0]} [options] -b base_path -r revision[,revision ...] Options: -b path Set the base path for the scan. Useful if you want --base-path path to scan individual files in a code base that don't live in the project's base directory. --curses Enables the ncurses GUI -f format Select the output format. Defaults to text. --format format -h Display this usage information --help -m modulename Loads the requested scanning module. If this --module modulename parameter is not specified, all available modules will be loaded. -o filename Write output to a file instead of stdout --output filename -q Suppresses all progress output --quiet -r Get files from a comma separated list of SVN revisions. You must supply a base path first for this! --svn Enables SVN integration "; $faults = array(); class ScannerModule { var $faults; var $blame; var $file; function ScannerModule() { $this->faults = array(); $this->blame = array(); err( "Initializing " . get_class( $this ) . "...\n" ); } function fault( $object, $level, $reason = '', $force = false ) { global $config, $revisions, $faults; if( !$force && $this->file['revision'] > 0 && $this->blame[$object['line']]['revision'] != $this->file['revision'] ) { /* If files have been added using SVN revisions, filter out any faulty changes that aren't a part of the requested changeset(s). */ return false; } $object['file'] = filename( $object['file'] ); $faults[] = $this->faults[] = array( 'module' => get_class( $this ), 'object' => $object, 'file' => $object['file'], 'line' => $object['line'], 'level' => $level, 'reason' => $reason, 'author' => ( $config['svn'] === true ) ? $this->blame[$object['line']]['author'] : '', 'revision' => ( $config['svn'] === true ) ? $this->blame[$object['line']]['revision'] : '' ); } function parserCallback( $object ) { } function preScan( $file ) { global $config, $svn_root, $svn_base; $this->file = $file; if( $config['svn'] === true ) { $this->blame = array(); $output = array(); if( $file['revision'] > 0 ) { exec( "svn blame -r {$file['revision']} {$svn_root}{$svn_base}/{$file['filename']} 2>/dev/null", $output, $result ); } else { exec( "svn blame '{$file['filename']}' 2>/dev/null", $output, $result ); } if( $result == 0 ) { foreach( $output as $line => $text ) { $matches = array(); preg_match( '/^\s*(\d+)\s+([^\s]+)/', $text, $matches ); $this->blame[$line + 1] = array( 'author' => $matches[2], 'revision' => $matches[1] ); } } } } function postScan( $filename ) { } } class OutputModule { function display() { $this->write( 'php://output' ); } function write( $filename ) { } } function _callback( $object ) { global $modules, $files, $current_file; $object['file'] = $files[$current_file - 1]['filename']; foreach( $modules['scanner'] as $module ) { $module->parserCallback( $object ); } } function addModule( $module_instance ) { global $modules; if( $module_instance instanceof ScannerModule ) { $modules['scanner'][] = $module_instance; } elseif( $module_instance instanceof OutputModule ) { $modules['output'] = $module_instance; } } function filename( $filename ) { global $base_path; $filename = realpath( $filename ); if( strpos( $filename, $base_path ) === 0 ) { $filename = substr( $filename, strlen( $base_path ) ); } return $filename; } function err( $string ) { global $stderr, $config, $curses; if( $config['quiet'] === false && $curses === false ) { fputs( $stderr, $string ); } } function scan_progress( $pos, $max, $title ) { global $curses, $nc_progress, $nc_progress_bar; $title = !empty( $title ) ? $title : 'Scanning [%d/%d]'; $title = @sprintf( $title, $pos, $max ); if( $curses ) { $nc_progress->write_fixed( 0, 1, $title ); $nc_progress_bar->max( $max ); $nc_progress_bar->pos( $pos ); } } // Handle application arguments $revisions = array(); $files = array(); $base_path = false; $curses = false; for( $i = 1; $i < $argc; $i++ ) { switch( $argv[$i] ) { case '-b': case '--base-path': $new_base = $argv[++$i]; if( is_dir( $new_base ) ) { $base_path = realpath( $new_base ) . '/'; } break; case '--curses': $curses = function_exists( 'ncurses_init' ); break; case '-f': case '--format': $config['output_format'] = $argv[++$i]; break; case '-h': case '--help': die( $help ); break; case '-m': case '--module': $config['modules'][] = $argv[++$i]; break; case '-o': case '--output': $config['output_file'] = $argv[++$i]; break; case '-q': case '--quiet': $config['quiet'] = true; break; case '-r': $revs = explode( ',', $argv[++$i] ); if( $base_path === false ) { die( "Set a base path before supplying SVN revisions\n" ); } // First, find out what the full path is so we can trim it down to the relative one. $xml = shell_exec( "svn info --xml $base_path" ); $parser = xml_parser_create(); xml_parser_set_option( $parser, XML_OPTION_SKIP_WHITE, 1 ); xml_parse_into_struct( $parser, $xml, $values, $index ); xml_parser_free( $parser ); foreach( $values as $value ) { switch( $value['tag'] ) { case 'URL': $svn_url = $value['value']; break; case 'ROOT': $svn_root = $value['value']; } } $svn_base = substr( $svn_url, strlen( $svn_root ) ); foreach( $revs as $rev ) { $revisions[] = $rev = intval( $rev ); $xml = shell_exec( "svn log -v --xml -r $rev $base_path 2>/dev/null" ); $parser = xml_parser_create(); xml_parser_set_option( $parser, XML_OPTION_SKIP_WHITE, 1 ); xml_parse_into_struct( $parser, $xml, $values, $index ); xml_parser_free( $parser ); foreach( $values as $value ) { if( $value['tag'] == 'PATH' && isset( $value['attributes']['ACTION'] ) && in_array( $value['attributes']['ACTION'], array( 'A', 'M' ) ) && strtolower( substr( $value['value'], -4 ) ) == '.php' ) { $file = substr( $value['value'], strlen( $svn_base ) + 1 ); $files[] = array( 'filename' => $file, 'revision' => $rev, 'contents' => shell_exec( "svn cat -r {$rev} {$svn_root}{$value['value']} 2>/dev/null" ) ); } } } case '--svn': $config['svn'] = true; break; default: if( file_exists( $argv[$i] ) && strtolower( substr( $argv[$i], -4 ) ) == '.php' ) { $base_path = ( $base_path === false ) ? realpath( dirname( $argv[$i] ) ) . '/' : $base_path; $files[] = array( 'filename' => $argv[$i], 'revision' => 0 ); } else if( is_dir( $argv[$i] ) ) { $base_path = ( $base_path === false ) ? realpath( $argv[$i] ) . '/' : $base_path; exec( "find {$argv[$i]} -iname '*.php' 2>/dev/null", $output, $result ); foreach( $output as $file ) { $files[] = array( 'filename' => $file, 'revision' => 0 ); } } } } if( count( $files ) == 0 ) { if( count( $revisions ) > 0 ) { die( "Revisions invalid or contained no files from the supplied path\n" ); } else { die( $help ); } } if( $curses ) { ncurses_init(); ncurses_noecho(); ncurses_curs_set( false ); $nc_screen = ncurses_newwin( 0, 0, 0, 0 ); ncurses_refresh(); $nc_main = new NcWindow( $nc_screen, 0, 0, 0, 0 ); $nc_main->title( 'Code Scanner' ); $nc_progress = new NcWindow( $nc_main, 4, 0, 0, 0 ); $nc_progress->title( 'Scanning Progress' ); $nc_progress_bar = new NcProgressBar( $nc_progress, 0, 1, 0 ); $nc_faults_columns = array( 'module' => 'Module', 'level' => 'Level', 'file' => 'File', 'line' => 'Line', 'author' => 'Author', 'revision' => 'Revision', 'reason' => 'Reason', ); $nc_faults = new NcTableView( $nc_main, -5, 0, 4, 0, array( 'columns' => $nc_faults_columns ) ); $nc_faults->title( 'Faults' ); $nc_main->write( -1, 0, '[Q] Quit | [S] Save Output file | [Return] Fault Info' ); } // Dig into the modules folder and load up what we find $module_files = scandir( APP_PATH . '/modules' ); foreach( $module_files as $module_file ) { if( strtolower( substr( $module_file, -4 ) ) == '.php' ) { $module = substr( $module_file, 0, strlen( $module_file ) - 4 ); list( $type, $module ) = split( '_', $module ); switch( $type ) { case 'output': if( $module == $config['output_format'] ) { require_once( APP_PATH . "/modules/{$module_file}" ); } break; case 'scanner': if( count( $config['modules'] ) == 0 || in_array( $module, $config['modules'] ) ) { require_once( APP_PATH . "/modules/{$module_file}" ); } break; } } } $parser = new PHPParser(); $parser->registerCallback( '_callback' ); err( "Parsing files...\n" ); $current_file = 0; $total = count( $files ); foreach( $files as $file ) { $current_file++; scan_progress( $current_file, $total, 'Scanning %d of %d source files' ); if( $curses ) { // Do this to avoid getting fouled up during scanning... // Apparently, system calls (like exec(), used while scanning) break curses keyboard input somehow ncurses_def_prog_mode(); } foreach( $modules['scanner'] as $module ) { $module->preScan( $file ); } $file_contents = ( $file['revision'] > 0 ? shell_exec( "svn cat -r {$rev} {$svn_root}{$svn_base}/{$file['filename']} 2>/dev/null" ) : file_get_contents( $file['filename'] ) ); $parser->parse( $file_contents ); if( $curses ) { ncurses_reset_prog_mode(); $nc_faults->title( sprintf( 'Faults [%d found]', count( $faults ) ) ); } } err( "\n" ); if( $curses ) { $nc_faults->load( $faults ); $done = false; while( !$done ) { $key = $nc_main->get_char(); switch( $key ) { case NCURSES_KEY_UP: $nc_faults->update( $nc_faults->selected - 1 ); break; case NCURSES_KEY_DOWN: $nc_faults->update( $nc_faults->selected + 1 ); break; case NCURSES_KEY_PPAGE: $nc_faults->update( $nc_faults->selected - $nc_faults->height() - 1 ); break; case NCURSES_KEY_NPAGE: $nc_faults->update( $nc_faults->selected + $nc_faults->height() - 1 ); break; case 10: case 13: if( count( $nc_faults->records ) > 0 ) { $nc_fault_info = new NcWindow( $nc_main, -10, -10, 5, 5 ); $fault = &$faults[$nc_faults->selected]; $nc_fault_info->attribute_on( NCURSES_A_BOLD ); $nc_fault_info->write_centered( 0, 'Fault Information' ); $nc_fault_info->write( 3, 0, "Module:\nLevel:\nFilename:\nLine Number:\nAuthor:\nRevision:\nReason:\n\nContext:" ); $nc_fault_info->attribute_off( NCURSES_A_BOLD ); $fault['object']['context'] = trim( $fault['object']['context'] ); $nc_fault_info->write( 3, 20, "{$fault['module']}\n{$fault['level']}\n{$fault['file']}\n{$fault['line']}\n{$fault['author']}\n{$fault['revision']}\n{$fault['reason']}\n\n{$fault['object']['context']}" ); $nc_fault_info->get_char(); $nc_fault_info->hide(); unset( $nc_fault_info ); } break; case ord( 's' ): case ord( 'S' ); $nc_save = new NcWindow( $nc_main, -10, -10, 5, 5 ); $nc_save->attribute_on( NCURSES_A_BOLD ); $nc_save->write_centered( 0, 'Save Faults To File' ); $nc_save->attribute_off( NCURSES_A_BOLD ); $nc_save->write( 2, 0, 'File to save to:' ); $nc_save_file_input = new NcTextInput( $nc_save, -4, 3, 2 ); $filename_selected = false; while( !$filename_selected ) { $filename = $nc_save_file_input->get(); $nc_save_confirm = new NcWindow( $nc_save, -10, -10, 5, 5 ); $nc_save_confirm->title( get_class( $modules['output'] ) ); $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ) - 2, "Save results to '{$filename}'?" ); $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ), '[Y]es [N]o [C]ancel' ); $valid_key = false; while( !$valid_key ) { switch( $nc_save_confirm->get_char() ) { case ord( 'y' ): case ord( 'Y' ): $save_file = true; case ord( 'c' ): case ord( 'C' ): $filename_selected = true; case ord( 'n' ): case ord( 'N' ): $valid_key = true; } } $nc_save_confirm->hide(); } if( $save_file ) { $result = $modules['output']->write( $filename ); $save_message = ( $result === false ) ? 'Save Failed' : 'Saved successfully'; $nc_save_confirm->erase(); $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ) - 2, $save_message ); $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ), 'Press any key to continue' ); $nc_save_confirm->show(); $nc_save_confirm->get_char(); } $nc_save->hide(); unset( $nc_save ); break; case ord( 'q' ): case ord( 'Q' ): $done = true; break; } } ncurses_clear(); ncurses_refresh(); ncurses_end(); } else { $modules['output']->write( $config['output_file'] ); sleep( 1 ); err( sprintf( "Found %d faults in %d files.\n", count( $faults ), count( $files ) ) ); } ?>