Main Page   Class Hierarchy   File List  

cgiClass.cpp

00001 /*:
00002   cgiClass.C
00003 
00004   This file contains the implementation of
00005   the cgi object methods
00006 
00007   copyright (c) 1996 - 2002
00008 
00009   David McCombs davidmc@newcottage.com
00010 
00011   Open Core Class Library
00012 
00013 */
00014 
00015 #include "cgiClass.h"
00016 #include <cstdlib> // for getenv()
00017 #include "ocString.h"
00018 
00019 // #define DO_OPEN_LOGGING
00020 #include "openLogger.h"
00021 
00022 // helper class to parse multipart data
00023 enum multipart_state
00024 {
00025   init = 0,
00026   newData,  // past a boundary
00027   dataSep,  // separator between attributes and data
00028   fileType, // expect a content-type marker for file content
00029   fileSep,  // separator between attributes and file data
00030   readData, // on data line
00031   readFile, // in file buffer
00032   fileRead, // after file buffer
00033   finished, // finished with data (or file) and on a boundary
00034   eof       // end of parse found by closing boundary
00035 };
00036 class multipart
00037 {
00038   string boundary;
00039   size_t boundaryLen;
00040   string fileBoundary;
00041   string endboundary;
00042   size_t fileBoundaryLen;
00043   string clrf;
00044   ocString testline;
00045   string name;
00046   string filename;
00047   string value;
00048   string type;
00049   char * testdata;
00050   multipart_state state;
00051   queryStringMap & rMap;
00052   ocFiles        & rFileMap;
00053   string path;
00054 public:
00055   multipart( string inBoundary, queryStringMap & inVars, ocFiles  & fileMap, string iPath )
00056   :testdata(NULL),boundaryLen(0),state(init),rMap(inVars),rFileMap(fileMap),path(iPath)
00057   {
00058     clrf = "\r\n";
00059     fileBoundary = clrf;
00060     boundary = inBoundary;
00061     writelog2( "boundary: ", boundary );
00062     fileBoundary += inBoundary;
00063     endboundary = boundary;
00064     endboundary += "--";
00065     writelog2( "endboundary: ", endboundary );
00066     boundaryLen = boundary.length();
00067     fileBoundaryLen = fileBoundary.length();
00068     testdata = new char[fileBoundaryLen+1]; // for terminating null
00069     memset(testdata, 0, fileBoundaryLen+1); // initially zero length string
00070   }
00071   ~multipart()
00072   {
00073     delete testdata;
00074   }
00075   bool getline(  istream & argstream )
00076   {
00077     char term;
00078     if( argstream.rdstate() == ios::goodbit &&
00079         state != readFile ) // don't want to steal first line from inline file
00080     {
00081       // try this:      
00082       std::getline(argstream, testline);
00083       
00084       // remove the CR of the CRLF pair
00085       if( testline.length() ) testline.resize( testline.length() - 1 );
00086       // prepare testline for parsing
00087       testline.parseInit();
00088       // to the log if loggin enabled
00089       writelog2( "getline got: ", testline );
00090     }
00091     return true;
00092   }
00093   void addFile( void )
00094   {
00095     ocFile tempFile;
00096     tempFile.name=value;
00097     tempFile.path=filename;
00098     tempFile.type=type;
00099     rFileMap[value] = tempFile;
00100   }
00101   void addDataItem( void )
00102   {
00103     // add element to (or modify element of) the collection
00104     queryStringMap::iterator pos = rMap.find(name);
00105     aString tmpVal(value);
00106     if( pos == rMap.end() )
00107     {
00108       rMap.insert(make_pair(name,tmpVal));
00109     }
00110     else
00111     {
00112       rMap[name] += "|";
00113       rMap[name] += tmpVal;
00114     }
00115   }
00116   void fixupFilename( string temp )
00117   {
00118     value = "";
00119     filename = "";
00120     writelog2("fixupFilename: checking length of",temp);
00121     if(temp.length())
00122     {
00123       // now for stupid IE browsers that give us the WHOLE MSDOSLIKE FILE PATH!            
00124       string::size_type idx = temp.find_last_of("\\");
00125       if( idx != string::npos )
00126       {
00127         temp = temp.substr(idx+1);
00128       }       
00129       // finally set the filename
00130       writelog("fixupFilename adding path");
00131       filename = path;
00132       writelog("fixupFilename adding filename")
00133       filename += temp;
00134       value = temp;
00135     }    
00136     writelog2("fixupFilename  Done!", value);
00137   }
00138   // data consumption is line oriented until we get to a file attachment
00139   bool consume( istream & argstream )
00140   {
00141     bool bret = true;
00142     ocString test;
00143     while(  argstream.rdstate() == ios::goodbit &&
00144             state != eof &&
00145             getline( argstream ) )
00146     {
00147       writelog2("Consumed",testline);
00148       switch( state )
00149       {
00150       case init:
00151         // check to see if we just consumed the boundary
00152         if( boundary != testline )
00153         {
00154           state = eof;
00155         }
00156         else
00157         {
00158           state = newData;
00159         }
00160         break;
00161       case newData:
00162         // should be reading the content disposition line
00163         test = testline.parse(": ");
00164         transform(test.begin(),test.end(),test.begin(),::tolower);
00165         writelog2("Testing",test);
00166         if( test == "content-disposition" )
00167         {
00168           // good - see what the data is
00169           test = testline.parse("; ");          
00170           // Added :: prefix to tolower so SGI recognizes global scope C function
00171           transform(test.begin(),test.end(),test.begin(),::tolower);          
00172           // expect it to be form-data
00173           writelog2("Content Testing",test);
00174           if( test == "form-data" )
00175           {
00176             // parse any remaining parameters
00177             while( testline.length() && !testline.endOfParse() )
00178             {
00179               ocString test = testline.parse("; ");
00180               writelog2("Param Testing",test);
00181               if( test.length() )
00182               {
00183                 string paramname=test.parse("=\"");
00184                 // Added :: prefix to tolower so SGI recognizes global scope C function
00185                 transform(paramname.begin(),paramname.end(),paramname.begin(),::tolower);
00186                 // could be name or filename
00187                 if( paramname == "name" )
00188                 {
00189                   // set the name
00190                   name = test.parse("\"");
00191                   state = dataSep;
00192                 }
00193                 else if( paramname == "filename" )
00194                 {
00195                   writelog2("Fix Filename",test);
00196                   fixupFilename(test.parse("\""));
00197                   writelog("Filename Fixed");
00198                   state = fileType;
00199                 }
00200               }
00201             }
00202           }
00203         }
00204         else
00205         {
00206           // unexpected place so abort method call
00207           state = eof;
00208         }
00209         break;
00210       case fileType:
00211         // expect Content-Type: image/jpeg
00212         test = testline.parse(": ");
00213         // Added :: prefix to tolower so SGI recognizes global scope C function
00214         transform(test.begin(),test.end(),test.begin(),::tolower);
00215         if( test == "content-type" )
00216         {
00217           type = testline.remainder();
00218           state = fileSep;
00219         }
00220         else
00221         {
00222           state = eof;
00223         }
00224         break;
00225       case fileSep:
00226         if(testline.length() == 0)
00227         {
00228           state = readFile;
00229         }
00230         else
00231         {
00232           state = eof;
00233         }
00234         break;
00235       case dataSep:
00236         // expect an empty line
00237         if(testline.length() == 0)
00238         {
00239           state = readData;
00240         }
00241         else
00242         {
00243           state = eof;
00244         }
00245         break;
00246       case readData:
00247         // expect the data
00248         dataConsume(argstream);
00249         break;
00250       case readFile:
00251         fileConsume(argstream);
00252         addFile();
00253         addDataItem();
00254         // fileConsume also consumes the boundary (up to the (potential) -- closing)
00255         state = fileRead;
00256         break;
00257       case fileRead:
00258         if( testline == "--" )
00259         {
00260           state = eof;
00261         }
00262         state = newData;
00263         break;
00264       case finished:
00265         // see if there is another boundary:
00266         if( boundary == testline )
00267         {
00268           state = newData; // note that the terminating boundary will have -- at the end
00269         }
00270         else
00271         {
00272           state = eof;
00273         }
00274         break;
00275       default:
00276         state = eof;
00277         break;
00278       } // end switch
00279     }  // end while
00280     return bret;
00281   }
00282   // Data consumption
00283   bool dataConsume( istream & argstream )
00284   {
00285     bool bRet = false;
00286     if( argstream.rdstate() == ios::goodbit )
00287     {
00288       value = testline;
00289       while( argstream.rdstate() == ios::goodbit &&
00290              getline( argstream ) &&
00291              testline != boundary &&
00292              testline != endboundary )
00293       {
00294         value +="\n";
00295         value += testline;
00296       }
00297 
00298       // set the proper state
00299       if(testline == boundary) state = newData;
00300       else if(testline == endboundary) state = eof;
00301       // add the whole item
00302       addDataItem();
00303     }
00304     else
00305     {
00306       state = eof;
00307     }
00308     return bRet;
00309   }
00310 
00311   // File consumption
00312   bool fileConsume( istream & argstream )
00313   {
00314     char c = '\0';
00315     size_t pos = 0;
00316     ofstream ofile;
00317     bool haveFile = filename.length() > 0;
00318     writelog2( "Consuming and saving file: ", filename );
00319     if( haveFile )
00320     {
00321       ofile.open( filename.c_str(), ios::out | ios::trunc | ios::binary );
00322     }
00323     writelog2( "fileBoundaryLen: ", fileBoundaryLen );
00324     // fill the buffer
00325     while( argstream.rdstate() == ios::goodbit &&
00326            pos < fileBoundaryLen &&
00327            argstream.get(c) )
00328     {
00329       testdata[pos++] = c;
00330     }
00331     writelog2( "final file testdata pos: ", pos-1 )
00332     // scan the buffer
00333     while( argstream.rdstate() == ios::goodbit &&
00334            fileBoundary != testdata &&
00335            argstream.get(c) )
00336     {
00337       if( haveFile )  ofile.put(testdata[0]);
00338       memmove( testdata, testdata + 1, fileBoundaryLen );
00339       testdata[fileBoundaryLen-1]=c;
00340       writelog2( "file data: ", testdata );
00341     }
00342     writelog2( "Closing file: ", boundary );
00343     if( haveFile ) ofile.close();
00344     return true;
00345   }
00346 
00347   // file dump of cgi input
00348   bool fileDump( istream & argstream )
00349   {
00350     char c = '\0';
00351     size_t pos = 0;
00352     ofstream ofile;
00353     filename = "cgiDump.log";    
00354     writelog2( "Dumping file: ", filename );
00355     ofile.open( filename.c_str(), ios::out | ios::trunc | ios::binary );    
00356     while( argstream.rdstate() == ios::goodbit &&
00357            argstream.get(c) )
00358     {
00359       ofile.put(c);
00360     }    
00361     ofile.close();
00362     return true;
00363   }
00364 };
00365 
00366 //
00367 // method implementations for cgiInput class.
00368 //
00369 
00370 cgiInput::cgiInput()
00371 {
00372   ;
00373 }
00374 
00375 cgiInput::~cgiInput()
00376 {;}
00377 void cgiInput::setMultipart( aString boundary )
00378 {
00379   string path;
00380   if( uploadPath.length() )
00381   {
00382     path = uploadPath.str();
00383     int pathlength =  path.length();
00384     if( pathlength  && path[pathlength-1] != '/' )
00385     {
00386       path += "/";
00387     }
00388   }
00389   multipart mp(boundary.str(),theMap,fileMap,path);
00390   mp.consume( cin );
00391   
00392   // mp.fileDump( cin ); // for testing
00393   writelog2("cgiInput::setMultipart mp.consume( cin ) called",boundary);
00394 }
00395 
00396 void cgiInput::set( const char * queryString, size_t size )
00397 {
00398   if(  queryString && strlen(queryString) > 0 )
00399   {
00400     if( size )
00401     {
00402       safe.setSize( size+1 );
00403       char * buf = (char*) safe;
00404       if( buf )
00405       {
00406         memcpy( buf, queryString, size );
00407         buf[size] = '\0';
00408       }
00409     }
00410     else
00411     {
00412       safe = queryString;
00413     }
00414     
00415     const char * pchTok = safe.token( "&" );
00416 
00417     while ( pchTok && strlen( pchTok ) )
00418     {
00419       // a string we can modify
00420       aString subToken = pchTok;
00421       subToken.deHexify('%');
00422       subToken.replaceFoundWith("+"," ");
00423       subToken.replaceFoundWith("+"," ");
00424 
00425       // get the name first.
00426       string tmpName = subToken.token( "=" );
00427 
00428       // get the variable(s)
00429       aString tmpVal = subToken.remainder();
00430 
00431       // add element to (or modify element of) the collection
00432       queryStringMap::iterator pos = theMap.find(tmpName);
00433 
00434       if( pos == theMap.end() )
00435       {
00436         theMap.insert(make_pair(tmpName,tmpVal));
00437       }
00438       else
00439       {
00440         theMap[tmpName] += "|";
00441         theMap[tmpName] += tmpVal;
00442       }
00443       pchTok = safe.token( "&" );
00444     }
00445   }
00446 }
00447 
00448 aString & cgiInput::Safe(void)
00449 {
00450   return safe;
00451 }
00452 queryStringMap & cgiInput::TheMap(void)
00453 {
00454   return theMap;
00455 }
00456 ocFiles & cgiInput::FileMap(void)
00457 {
00458   return fileMap;
00459 }
00460 int  cgiInput::count( const char * key )
00461 {
00462   int retVal = 0;
00463   queryStringMap::iterator pos;
00464   pos = theMap.find(key);
00465   if( pos != theMap.end() )
00466   {
00467     aString & whole = theMap[key];
00468     while( whole.token("|") )
00469     {
00470       retVal++;
00471     }
00472   }
00473   return retVal;
00474 }
00475 aString & cgiInput::operator [] ( const char * key )
00476 {
00477   aString & returnValue = theMap[key];
00478   return returnValue;
00479 }
00480 
00481 
00482 //
00483 // method implementations for cgiEnvironment class.
00484 //
00485 cgiEnvironment::cgiEnvironment(const char * uploadPath) : contentSize(0)
00486 {
00487   contentLength =     getenv("CONTENT_LENGTH");
00488   contentType =       getenv("CONTENT_TYPE");
00489   if( uploadPath ) clientArguments.uploadPath = uploadPath;
00490   aString aTemp = contentType.token(";");
00491   if( contentType.remainderPosition() != -1 )
00492   {
00493     contentType.token("=");
00494     /* rfc1521:
00495        7.2.3. The Multipart/alternative subtype
00496        Need 2 dashes + boundary for matching
00497     */
00498     contentBoundary = "--";
00499     contentBoundary += contentType.remainder();
00500     contentType = aTemp;
00501     contentType = contentType.lower();
00502   }
00503   
00504   gatewayInterface =  getenv("GATEWAY_INTERFACE");
00505   httpAccept =        getenv("HTTP_ACCEPT");
00506   httpUserAgent =     getenv("HTTP_USER_AGENT");
00507   pathInfo =          getenv("PATH_INFO");
00508   pathTranslated =    getenv("PATH_TRANSLATED");
00509   queryString =       getenv("QUERY_STRING");
00510   remoteAddr =        getenv("REMOTE_ADDR");
00511   remoteHost =        getenv("REMOTE_HOST");
00512   remoteIdent =       getenv("REMOTE_IDENT");
00513   requestMethod =     getenv("REQUEST_METHOD");
00514   remoteUser =        getenv("REMOTE_USER");
00515   scriptName =        getenv("SCRIPT_NAME");
00516   serverName =        getenv("SERVER_NAME");
00517   serverPort =        getenv("SERVER_PORT");
00518   serverProtocol =    getenv("SERVER_PROTOCOL");
00519   serverSoftware =    getenv("SERVER_SOFTWARE");
00520 
00521   contentSize = atoi( contentLength.str() );
00522 
00523   if( requestMethod.match("GET") )
00524   {
00525     clientArguments.set( queryString.str() );
00526   }
00527   else if( requestMethod.match("POST") )
00528   {
00529     if( contentType == "multipart/form-data" )
00530     {
00531       // client arguments come as boundary delimited values
00532       clientArguments.setMultipart( contentBoundary );
00533       writelog("cgiEnvironment::cgiEnvironment back from clientArguments.setMultipart()");
00534     }
00535     else
00536     {
00537       char * tempBuf = new char[ contentSize + 1 ];
00538       cin.read( tempBuf, contentSize );
00539       tempBuf[contentSize]='\0';
00540       clientArguments.set( tempBuf, contentSize );
00541       delete [] tempBuf;
00542 
00543     }
00544 
00545     // In addition - get any values on the query string
00546     if( queryString.length() )
00547     {
00548       writelog2("cgiEnvironment::cgiEnvironment found additional data in query string: ", queryString );
00549       clientArguments.set( queryString.str() );
00550     }
00551   } // end else POST method
00552   writelog("cgiEnvironment::cgiEnvironment contructor done" );
00553 }
00554 cgiEnvironment::~cgiEnvironment()
00555 {
00556   ;
00557 }
00558 // contains the total number of characters in the user input
00559 aString & cgiEnvironment::ContentLength(void)
00560 {
00561   return contentLength;
00562 }
00563 aString & cgiEnvironment::ContentType(void)
00564 {
00565   return contentType;
00566 }
00567 aString & cgiEnvironment::GatewayInterface(void)
00568 {
00569   return gatewayInterface;
00570 }
00571 aString & cgiEnvironment::HttpAccept(void)
00572 {
00573   return httpAccept;
00574 }
00575 aString & cgiEnvironment::HttpUserAgent(void)
00576 {
00577   return httpUserAgent;
00578 }
00579 aString & cgiEnvironment::PathInfo(void)
00580 {
00581   return pathInfo;
00582 }
00583 aString & cgiEnvironment::PathTranslated(void)
00584 {
00585   return pathTranslated;
00586 }
00587 aString & cgiEnvironment::QueryString(void)
00588 {
00589   return queryString;
00590 }
00591 aString & cgiEnvironment::RemoteAddr(void)
00592 {
00593   return remoteAddr;
00594 }
00595 aString & cgiEnvironment::RemoteHost(void)
00596 {
00597   return remoteHost;
00598 }
00599 aString & cgiEnvironment::RemoteIdent(void)
00600 {
00601   return remoteIdent;
00602 }
00603 aString & cgiEnvironment::RequestMethod(void)
00604 {
00605   return requestMethod;
00606 }
00607 aString & cgiEnvironment::RemoteUser(void)
00608 {
00609   return remoteUser;
00610 }
00611 aString & cgiEnvironment::ScriptName(void)
00612 {
00613   return scriptName;
00614 }
00615 aString & cgiEnvironment::ServerName(void)
00616 {
00617   return serverName;
00618 }
00619 aString & cgiEnvironment::ServerPort(void)
00620 {
00621   return serverPort;
00622 }
00623 aString & cgiEnvironment::ServerProtocol(void)
00624 {
00625   return serverProtocol;
00626 }
00627 aString & cgiEnvironment::ServerSoftware(void)
00628 {
00629   return serverSoftware;
00630 }
00631 cgiInput & cgiEnvironment::ClientArguments(void)
00632 {
00633   return clientArguments;
00634 }
00635 aString & cgiEnvironment::ContentBoundary(void)
00636 {
00637   return contentBoundary;
00638 }
00639 // cgi Base Class methods
00640 cgiBase::cgiBase():ostream(cout.rdbuf()),endLine("\r\n"),id(cgi)
00641 {
00642   ;
00643 }
00644 cgiBase::cgiBase(cgiBase& input):ostream(cout.rdbuf()),endLine("\r\n"),id(input.id)
00645 {
00646   ;
00647 }
00648 const char * cgiBase::tag( void )
00649 {
00650   return opening.str();
00651 }
00652 cgiBase::~cgiBase()
00653 {  
00654 }
00655 
00656 
00657 cgiScript & cgiScript::DebugString( void )
00658 {
00659    *this << "<pre>" << endl;
00660    *this << "mime type: " << mimeType << endl;
00661    if( RequestMethod().length() > 0 )
00662    {
00663      *this << "ContentLength: [[" << ContentLength() << "]]"<< endl;
00664      *this << "ContentType: [[" << ContentType() << "]]"<< endl;
00665      *this << "ContentBoundary: [[" << ContentBoundary()  << "]]"<< endl;
00666      *this << "GatewayInterface: [[" << GatewayInterface() << "]]"<< endl;
00667      *this << "HttpAccept: [[" << HttpAccept() << "]]"<< endl;
00668      *this << "HttpUserAgent: [[" << HttpUserAgent() << "]]"<< endl;
00669      *this << "PathInfo: " << PathInfo() << "]]"<< endl;
00670      *this << "PathTranslated: [[" << PathTranslated() << "]]"<< endl;
00671      *this << "QueryString: [[" << QueryString() << "]]"<< endl;
00672      *this << "RemoteAddr: [[" << RemoteAddr() << "]]"<< endl;
00673      *this << "RemoteHost: [[" << RemoteHost() << "]]"<< endl;
00674      *this << "RemoteIdent: [[" << RemoteIdent() << "]]"<< endl;
00675      *this << "RequestMethod: [[" << RequestMethod() << "]]"<< endl;
00676      *this << "RemoteUse: [[" << RemoteUser() << "]]"<< endl;
00677      *this << "ScriptName: [[" << ScriptName() << "]]"<< endl;
00678      *this << "ServerName: [[" << ServerName() << "]]"<< endl;
00679      *this << "ServerPort: [[" << ServerPort() << "]]"<< endl;
00680      *this << "ServerProtocol: [[" << ServerProtocol() << "]]"<< endl;
00681      *this << "ServerSoftware: [[" << ServerSoftware() << "]]"<< endl;
00682      *this << "Saved Query String: [[" << ClientArguments().Safe().str() << "]]"<< endl;
00683    }
00684    else
00685    {
00686      *this << "Invalid call to cgi program - invalid environment variables." << endl;
00687    }
00688   *this << endl <<"Client Arguments:" << endl;
00689    cgiInput & cgiInput = ClientArguments();
00690    queryStringMap::iterator pos;
00691 
00692    for( pos = cgiInput.TheMap().begin();
00693         pos != cgiInput.TheMap().end();
00694         ++pos )
00695    {
00696      *this << pos->first.c_str() << ": [[";
00697      *this << pos->second << "]]" << endl;
00698    }
00699 
00700 
00701    *this << "</pre>" << endl;
00702    *this << ends;
00703    return *this;
00704 }
00705 
00706 // methods for cgiScriptLite...
00707 cgiScriptLite::cgiScriptLite( const char * mimeString, bool bCloseHeader )
00708 {
00709   id = cgi;
00710   mimeType=mimeString;
00711   opening="Content-type: ";
00712   opening+=mimeType;
00713   opening+=endLine;
00714   close=endLine;
00715   *this << opening;
00716   if (bCloseHeader ) closeHeader();
00717   
00718 }
00719 
00720 void cgiScriptLite::closeHeader( void )
00721 {
00722   *this  << endLine;  
00723 }
00724 
00725 cgiScriptLite::~cgiScriptLite()
00726 {
00727   *this << close << endLine;
00728 }
00729 
00730 void cgiScriptLite::Redirect( const char * location )
00731 {
00732   *this << "Location: " << location << endLine << endLine;
00733 }
00734 
00735 
00736 /*
00737   The cgi script class methods
00738 */
00739 
00740 cgiScript::cgiScript( const char * mimeString, 
00741                       bool bCloseHeader, const char * uploadPath)
00742 :cgiScriptLite(mimeString,bCloseHeader),cgiEnvironment(uploadPath)
00743 {
00744   ;
00745 }
00746 
00747 cgiScript::~cgiScript()
00748 {
00749   ;
00750 }
00751 
00752 
00753 // The html tag container methods
00754 
00755 cgiHtml::cgiHtml( char * attr)
00756 {
00757   id = html;
00758   opening="<html";
00759   opening += attr;
00760   opening += ">";
00761   close="</html>";
00762   *this << opening << endl;
00763 }
00764 cgiHtml::~cgiHtml()
00765 {
00766   *this << close << endl;
00767 }
00768 
00769 
00770 
00771 
00772 // the head tag container methods
00773 
00774 cgiHead::cgiHead( char * attr)
00775 {
00776   id = head;
00777   opening="<head";
00778   opening += attr;
00779   opening += ">";
00780   close="</head>";
00781   *this << opening << endl;
00782 }
00783 cgiHead::~cgiHead()
00784 {
00785   *this << close << endl;
00786 }
00787 
00788 
00789 // the body tag container methods
00790 
00791 cgiBody::cgiBody(char * attr)
00792 {
00793   id = body;
00794   opening="<body";
00795   opening += attr;
00796   opening += ">";
00797   close="</body>";
00798   *this << opening << endl;
00799 }
00800 cgiBody::~cgiBody()
00801 {
00802   *this << close << endl;
00803 }
00804 
00805 
00806 

Generated on Tue Jan 20 09:03:27 2004 for OpenTools by doxygen1.2.18