<?php
class page_GetStructure extends Page{
private $structure;
private $project_id;
private $version_id=null;
private $in_comment=false;
private $comment;
private $lines_from_comment;
private $lines_from_declaration;
/**
* Whether or not to collect a description after class declaration
*/
private $desc_class_after = false;
/**
* Whether or not to collect a description after method declaration
*/
private $desc_method_after = false;
/**
* Whether or not to parse functions that are not belonging to any class
*/
private $parse_non_class = false;
function init(){
parent::init();
$this->project_id=$_GET['id'];
$this->desc_class_after=$_REQUEST['desc_class_after']=='Y'||$_GET['desc_class_after']=='Y';
$this->desc_method_after=$_REQUEST['desc_method_after']=='Y'||$_GET['desc_method_after']=='Y';;
$this->parse_non_class=$_REQUEST['parse_non_class']=='Y'||$_GET['parse_non_class']=='Y';;
$this->template->set('projectname', $this->api->getProjectName($this->project_id));
$progress=$this->add('Text', null, 'Content');
$progress->set("<div id=state>Parsing of application source code is in progress." .
"Please be patient.</div><img src=templates/pixel.gif onLoad=\"aasn('progress', '".
$this->api->getDestinationURL(null,array('cut_object'=>$progress->name,
'desc_class_after'=>$_REQUEST['desc_class_after'],
'desc_method_after'=>$_REQUEST['desc_method_after'],
'parse_non_class'=>$_REQUEST['parse_non_class'],
'id'=>$this->project_id,
'do_parse'=>1))."');spinner_on('progress')\">" .
"<div id=progress width=50%>Here should be a progress info</div>")
;
$this->add('Ajax')->useProgressIndicator('progress');
if($_GET['do_parse']){
echo "<h3>Parse report:</h3>";
//parsing all the files recoursively
$this->parseAll($this->api->getProjectPath($this->project_id));
//building class structure
$this->buildClassStructure();
//redirecting to projects
//$this->api->redirect('Projects');
}
}
function defaultTemplate(){
return array('getstructure', '_top');
}
function buildClassStructure(){
/**
* This method changes classes parents depending on their names
*/
$classes=$this->api->db->getAllHash("select * from member where project_id=$this->project_id" .
" and type='class' and version_id=".$this->getVersionId());
foreach($classes as $class){
$parent_id=$this->api->db->getOne
("select id from member where name='".$class['extends']."' and project_id=$this->project_id");
if($parent_id!='')$this->api->db->query("update member set parent_id=$parent_id where id=".$class['id']);
}
}
function parseAll($path){
$omit_dirs=$this->api->getConfig('parsing/omit_dirs', array('.', '..', '.svn'));
//echo "Entering parseAll<br>";
//if($this->parse_non_class)echo "Parsing all the content<br>";
if ($handle=opendir($path)) {
if(substr($path, -1)==DIRECTORY_SEPARATOR)$path=substr($path, 0, -1);
//echo $path;
while(($file=readdir($handle))!==false){
if(array_search($file, $omit_dirs)!==false)continue;
$file=$path.DIRECTORY_SEPARATOR.$file;
if(is_dir($file))$this->parseAll($file);
if(strpos($file, '.php', strlen($file)-5)!==false){
//echo "parsing $file...<br>";
echo "<p>$file</p>";
$this->parseFile($file);
//for testing purposes stopping after the first file
//if($this->structure)return;
}
}
closedir($handle);
}
}
function parseFile($filename){
$lines=file($filename);
$class_id=null;
$func_id=null;
$class_index="";
$prev_item=null;//a pointer just added item; for comment-after-declaration set
$pcount=0; //'{}'-bracket count
foreach($lines as $no=>$orig_line){
$item=array();
//we need to discover the line type...
$line=$this->stripComments($orig_line);
if($this->in_comment){
//collecting comments, they could be useful
}else{
//if there is trigger_error - remainder of the file is obsolete
if($class_id==null&&$func_id==null&&strpos($line, 'trigger_error')!==false)return;
/*
* setting comment that is AFTER member declaration
* such comments are only for classes and functions and are set if new member haven't
* been matched
*/
if($prev_item!=null&&$prev_item['type']!='property'&&$this->comment!=''){
$this->setItemDescription($prev_item);
}
$item['declaration']=$this->stripSyntax($line);
$item['file']=$filename;
$item['line']=$no+1;
//////////////// Class parsing ////////////////
if(strpos($line, 'class ')!==false&&$class_id==null){
$this->lines_from_declaration=0;
//setting a comment if it was collected
if($this->comment!='')$this->setItemDescription($item);
//parsing a class
$first_space=strpos($line, ' ', strpos($line, 'class')+1)+1;
$second_space=strpos($line, ' ', $first_space);
$second_space=$second_space==0?strlen($line):$second_space;
$item['name']=substr($line, $first_space, $second_space-$first_space);
$item['type']='class';
$item['visibility']=$this->getVisibility($line);
$extends=strpos($line, 'extends');
if($extends!==false){
$extends=trim(substr($line, strpos($line, ' ', $extends+1)));
$item['extends']=$this->stripSyntax($extends);
}
$class_index=$item['name'];
$class_id=$this->dbAddMember($item);
$this->structure[$class_index]=$item;
$prev_item=&$this->structure[$class_index];
}
//////////////// Method parsing ////////////////
elseif(strpos($line, 'function')!==false&&$func_id==null&&
($class_id!=null||$this->parse_non_class)){
$this->lines_from_declaration=0;
//setting a comment if it was collected
if($this->comment!='')$this->setItemDescription($item, $this->comment);
//parsing a method
$first_space=strpos($line, ' ', strpos($line, 'function')+1)+1;
$second_space=strpos($line, '(');
if($second_space!==false)$item['name']=substr($line, $first_space, $second_space-$first_space);
$item['type']='method';
$item['visibility']=$this->getVisibility($line);
$func_id=$this->dbAddMember($item, $class_id);
$this->structure[$class_index]['methods'][$item['name']]=$item;
$prev_item=&$this->structure[$class_index]['methods'][$item['name']];
}
//////////////// Property parsing ////////////////
elseif($func_id==null&&(strpos($line, 'private')!==false||strpos($line, 'public')!==false||
strpos($line, 'protected')!==false||strpos($line, 'var')!==false)&&
($class_id!=null||$this->parse_non_class)){
$this->lines_from_declaration=0;
//property's comment is always before it
if($this->comment!='')$this->setItemDescription($item);
$first_space=strpos($line, ' ')+1;
//parsing a property
$item['name']=trim(substr($line, $first_space, strpos($line, ';')-$first_space));
$item['type']='property';
$item['visibility']=$this->getVisibility($line);
$this->dbAddMember($item, $class_id);
$this->structure[$class_index]['properties'][$item['name']]=$item;
$prev_item=&$this->structure[$class_index]['properties'][$item['name']];
}
$this->lines_from_declaration++;
if(strpos($line, '{')!==false){
$pcount++;
}
if(strpos($line, '}')!==false){
//closing opened item
if($pcount>0)$pcount--;
if($pcount==1){
$func_id=null;
//unsetting prev_item if it was a function
if($prev_item['type']=='method')$prev_item=null;
}
if($pcount==0){
$class_id=null;
$class_index=null;
//unsetting prev_item cuase all the comments will be for the next class
$prev_item=null;
}
//else throw new BaseException("Something goes wrong");
}
if($this->lines_from_comment>$this->api->getConfig('parsing/comment_range'))$this->comment='';
else $this->lines_from_comment++;
}
//echo "{$item['name']}: $class_id / $func_id / {$item['description']} / $no : $pcount<br>";
}
}
private function setItemDescription(&$item){
if($this->lines_from_declaration <= $this->api->getConfig('parsing/comment_range')){
if(!isset($item['id'])||(($item['type']=='class'&&$this->desc_class_after)||
($item['type']=='method'&&$this->desc_method_after))){
if($item['description']=='')$item['description']=trim($this->comment);
$this->comment='';
}
//updating only if it is an existing item
if($item['id']!='')$this->dbUpdateMember($item);
}
if($item['id']!='')$item=null;
}
private function setInComment($in_comment){
//erasing comment if reached a new one
if($in_comment)$this->comment="";
else $this->lines_from_comment=0;
$this->in_comment=$in_comment;
}
/**
* Strips brackets, commas, semicolons, etc
*/
private function stripSyntax($line){
$bracket_start=strpos($line, '{');
$bracket_end=strpos($line, '}');
$code=$bracket_start!==false?
substr($line, $bracket_start, $bracket_end===false?strlen($line):$bracket_end):
'';
if($code!='')$line=str_replace($code, '', $line);
$line=str_replace(';', '', $line);
//$line=str_replace(',', '', $line);
return $line;
}
/**
* strips all types of comments from the given line
*/
private function stripComments($line){
$result=trim($line);
//if(strpos($result, '//')!==false)$result=substr($result, 0, strpos($result, '//'));
$comment_start=strpos($result, '//');
$comment_end=false;
if($comment_start===false){
$comment_start=strpos($result, '/*');
//this should be now
if($comment_start!==false)$this->setInComment(true);
if(strpos($result, '*')===0)$comment_start=0;
$comment_end=strpos($result, '*/');
//this should be now
if($comment_end!==false)$this->setInComment(false);
}else $this->lines_from_comment=0;
if($comment_start!==false){
$comment=substr($result, $comment_start, $comment_end===false?strlen($result):$comment_end);
$result=str_replace($comment, '', $result);
$comment=substr($comment, strpos($comment, '//')!==false?2:0);
$comment=substr($comment, strpos($comment, '/**')!==false?3:0);
$comment=substr($comment, strpos($comment, '/*')!==false?2:0);
$comment=substr($comment, strpos($comment, '*')===0?1:0);
$this->comment.=$comment."\n";
}elseif($comment_end!==false){
//we have a comment closure. returning string part after it
$result=substr($result, $comment_end + 2);
}
return trim($result);
}
private function getVisibility($str){
switch(substr($str, 0, strpos($str, ' '))){
case 'private':return 'private';
case 'protected':return 'protected';
default:return 'public';
}
}
private function dbAddClass(&$item){
$this->api->db->query("insert into class values (0, $this->project_id, " .
"'{$item['name']}', null, '{$item['declaration']}', '{$item['description']}', ".
$this->getVersionId().")");
$item['id']=mysql_insert_id();
return $item['id'];
}
private function dbUpdateMember($item){
$this->formatItem($item);
$this->api->db->query("update member set " .
" description='".$item['description']."'".
" where id = ".$item['id']
);
}
private function dbAddMember(&$item, $class_id=null){
if(is_null($class_id))$class_id='null';
if($item['name']=='')throw new BaseException("Member name is empty! Details: " .
"<br>file: ".$item['file'].
"<br>line: ".$item['line']."<br>declaration: ".$item['declaration']);
$this->formatItem($item);
$this->api->db->query("insert into member values (0, $this->project_id, $class_id, " .
"'{$item['name']}', '{$item['extends']}', '{$item['declaration']}', '{$item['description']}', ".
$this->getVersionId().", '{$item['type']}', '{$item['visibility']}', " .
"'".addslashes($item['file'])."', {$item['line']})");
$item['id']=mysql_insert_id();
return $item['id'];
}
private function formatItem(&$item){
foreach($item as $key=>$value){
$item[$key]=str_replace("'", "\"", $value);
}
}
private function getVersionId(){
if(is_null($this->version_id)){
$this->version_id=$this->api->getProjectVersion($this->project_id);
if($this->version_id==0){
$this->api->db->query("insert into version values (0, 'auto add', SYSDATE(), $this->project_id)");
$this->version_id=mysql_insert_id();
}else{
//cleaning data
$this->api->db->query("delete from member where version_id=$this->version_id");
}
}
return $this->version_id;
}
}