2 rcs_id('$Id: cvs.php,v 1.2 2001-10-01 21:55:54 riessen Exp $');
4 * Backend for handling CVS repository.
6 * ASSUMES: that the shell commands 'cvs', 'grep', 'rm', are all located
7 * ASSUMES: in the path of the server calling this script.
9 * Author: Gerrit Riessen, gerrit.riessen@open-source-consultants.de
12 require_once('lib/WikiDB/backend.php');
13 require_once('lib/ErrorManager.php');
16 * Constants used by the CVS backend
18 // these are the parameters defined in db_params
19 define( 'CVS_DOC_DIR', 'doc_dir' );
20 define( 'CVS_REPOSITORY', 'repository' );
21 define( 'CVS_CHECK_FOR_REPOSITORY', 'check_for_repository' );
22 define( 'CVS_DEBUG_FILE', 'debug_file' );
23 define( 'CVS_PAGE_SOURCE', 'pgsrc' );
24 define( 'CVS_MODULE_NAME', 'module_name' );
26 // these are the things that are defined in the page hash
27 // CMD == Cvs Meta Data
28 define( 'CMD_LAST_MODIFIED', 'lastmodified' );
29 define( 'CMD_CONTENT', '%content');
30 define( 'CMD_CREATED', 'created');
31 define( 'CMD_VERSION', 'version');
32 define( 'CMD_AUTHOR', 'author');
35 class WikiDB_backend_cvs
36 extends WikiDB_backend
44 * In the following parameters should be defined in dbparam:
45 * . wiki ==> directory where the pages should be stored
46 * this is not the CVS repository location
47 * . repository ==> local directory where the repository should be
48 * created. This can also be a :pserver: but then
49 * set check_for_repository to false and checkout
50 * the documents beforehand. (This is basically CVSROOT)
51 * . check_for_repository ==> boolean flag to indicate whether the
52 * repository should be created, this only
53 * applies to local directories, for pserver
54 * set this to false and check out the
55 * document base beforehand
56 * . debug_file ==> file name where debug information should be set.
57 * If file doesn't exist then it's created, if this
58 * is empty, then no debugging is done.
59 * . pgsrc ==> directory name where the default wiki pages are stored.
60 * This is only required if the backend is to create a
63 * The class also adds a parameter 'module_name' to indicate the name
64 * of the cvs module that is being used to version the documents. The
65 * module_name is assumed to be the base name of directory given in
66 * wiki, e.g. if wiki == '/some/path/to/documents' then module_name
67 * becomes 'documents' and this module will be created in the CVS
68 * repository or assumed to exist. If on the other hand the parameter
69 * already exists, then it is not overwritten.
71 function WikiDB_backend_cvs( $dbparam )
73 // setup all the instance values.
74 $this->_docDir = $dbparam{CVS_DOC_DIR};
75 $this->_repository = $dbparam{CVS_REPOSITORY};
76 if ( ! $dbparam{CVS_MODULE_NAME} ) {
77 $this->_module_name = basename( $this->_docDir );
78 $dbparam{CVS_MODULE_NAME} = $this->_module_name;
80 $this->_module_name = $dbparam{CVS_MODULE_NAME};
82 $this->_debug_file = $dbparam{CVS_DEBUG_FILE};
84 if ( !($dbparam{CVS_CHECK_FOR_REPOSITORY}
85 && is_dir( $this->_repository )
86 && is_dir( $this->_repository . "/CVSROOT" )
87 && is_dir( $this->_repository . "/" . $this->_module_name ))) {
89 $this->_cvsDebug( "Creating new repository [$this->_repository]" );
91 // doesn't exist, need to create it and the replace the wiki
92 // document directory.
93 $this->_mkdir( $this->_repository, 0775 );
95 // assume that the repository is a local directory, prefix :local:
96 if ( !ereg( "^:local:", $this->_repository ) ) {
97 $this->_repository = ":local:" . $this->_repository;
100 $cmdLine = sprintf( "cvs -d \"%s\" init", $this->_repository);
101 $this->_execCommand( $cmdLine, $cmdOutput, true );
103 $this->_mkdir( $this->_docDir, 0775 );
104 $cmdLine = sprintf("cd %s; cvs -d \"%s\" import -m no_message "
105 ."%s V R", $this->_docDir, $this->_repository,
106 $this->_module_name );
107 $this->_execCommand( $cmdLine, $cmdOutput, true );
109 // remove the wiki directory and check it out from the
111 $cmdLine = sprintf( "rm -fr %s; cd %s; cvs -d \"%s\" co %s",
112 $this->_docDir, dirname($this->_docDir),
113 $this->_repository, $this->_module_name);
114 $this->_execCommand( $cmdLine, $cmdOutput, true );
116 // add the default pages using the update_pagedata
118 $metaData[$AUTHOR] = "PhpWiki -- CVS Backend";
120 if ( is_dir( $dbparam[CVS_PAGE_SOURCE] ) ) {
121 $d = opendir( $dbparam[CVS_PAGE_SOURCE] );
122 while ( $entry = readdir( $d ) ) {
123 $filename = $dbparam[CVS_PAGE_SOURCE] . "/" . $entry;
124 $this->_cvsDebug("Found [$entry] in ["
125 . $dbparam[CVS_PAGE_SOURCE]."]");
127 if ( is_file( $filename ) ) {
128 $metaData[CMD_CONTENT] = join('',file($filename));
129 $this->update_pagedata( $entry, $metaData );
135 // ensure that the results of the is_dir are cleared
141 * Return: metadata about page
143 function get_pagedata($pagename)
145 // the metadata information about a page is stored in the
146 // CVS directory of the document root in serialized form. The
147 // file always has the name _$pagename.
148 $metaFile = $this->_docDir . "/CVS/_" . $pagename;
150 if ( $fd = @fopen( $metaFile, "r" ) ) {
152 $locked = flock( $fd, 1 ); // read lock
155 $this->_cvsError( "Unable to obtain read lock.",__LINE__);
158 $pagehash = unserialize( join( '', file($metaFile) ));
159 // need to strip off the new lines at the end of each line
160 // hmmmm, content expects an array of lines where each line
161 // has it's newline stripped off. Unfortunately because i don't
162 // want to store the files in serialize or wiki specific format,
163 // i need to add newlines when storing but need to strip them
164 // off again when i retrieve the file because file() doesn't remove
166 // $pagehash['content'] = file($filename);
167 // array_walk( $pagehash['content'], '_strip_newlines' );
168 // reset( $pagehash['content'] );
178 * This will create a new page if page being requested does not
181 function update_pagedata($pagename, $newdata)
183 // retrieve the meta data
184 $metaData = $this->get_pagedata( $pagename );
187 $this->_cvsDebug( "update_pagedata: no meta data found" );
188 // this means that the page does not exist, we need to create
192 $metaData[CMD_CREATED] = time();
193 $metaData[CMD_VERSION] = "1";
195 if ( ! isset($newdata[$CONTENT])) {
196 $metaData[CMD_CONTENT] = "";
198 $metaData[CMD_CONTENT] = $newdata[CMD_CONTENT];
201 // create an empty page ...
202 $this->_writePage( $pagename, $metaData[CMD_CONTENT] );
203 $this->_addPage( $pagename );
205 // make sure that the page is written and committed a second time
206 unset( $newdata[CMD_CONTENT] );
207 unset( $metaData[CMD_CONTENT] );
210 // change any meta data information
211 foreach ( $newdata as $key => $value ) {
212 if ( $value == false || empty( $value ) ) {
213 unset( $metaData[$key] );
215 $metaData[$key] = $value;
219 // update the page data, if required
220 if ( isset( $metaData[CMD_CONTENT] ) ) {
221 $this->_writePage( $pagename, $newdata[CMD_CONTENT] );
224 // remove any content from the meta data before storing it
225 unset( $metaData[CMD_CONTENT] );
226 $metaData[CMD_LAST_MODIFIED] = time();
227 $this->_writeMetaInfo( $pagename, $metaData );
229 $this->_commitPage( $pagename, serialize( $metaData ) );
232 function get_latest_version($pagename)
234 $metaData = $this->get_pagedata( $pagename );
236 // the version number is everything about the '1.'
237 return $metaData[CMD_VERSION];
239 $this->_cvsDebug( "get_latest_versioned FAILED for [$pagename]" );
244 function get_previous_version($pagename, $version)
246 // cvs increments the version numbers, so this is real easy ;-)
247 return ($version > 0 ? $version - 1 : 0);
251 * the version parameter is assumed to be everything about the '1.'
252 * in the CVS versioning system.
254 function get_versiondata($pagename, $version, $want_content = false)
256 $this->_cvsDebug( "get_versiondata: [$pagename] [$version] [$want_content]" );
259 if ( $want_content ) {
260 // retrieve the version from the repository
261 $cmdLine = sprintf("cvs -d \"%s\" co -p -r 1.%d %s/%s 2>&1",
262 $this->_repository, $version,
263 $this->_module_name, $pagename );
264 $this->_execCommand( $cmdLine, $filedata, true );
266 // TODO: DEBUG: 5 is a magic number here, depending on the
267 // TODO: DEBUG: version of cvs used here, 5 might have to
268 // TODO: DEBUG: change. Basically find a more reliable way of
269 // TODO: DEBUG: doing this.
270 // the first 5 lines contain various bits of
271 // administrative information that can be ignored.
272 for ( $i = 0; $i < 5; $i++ ) {
273 array_shift( $filedata );
278 * Now obtain the rest of the pagehash information, this is contained
279 * in the log message for the revision in serialized form.
281 $cmdLine = sprintf("cd %s; cvs log -r1.%d %s", $this->_docDir,
282 $version, $pagename );
283 $this->_execCommand( $cmdLine, $logdata, true );
285 // shift log data until we get to the 'revision X.X' line
286 // FIXME: ensure that we don't enter an endless loop here
287 while ( !ereg( "^revision ([^ ]+)$", $logdata[0], $revInfo ) ) {
288 array_shift( $logdata );
291 // serialized hash information now stored in position 2
292 $rVal = unserialize( _unescape( $logdata[2] ) );
294 // version information is incorrect
295 $rVal[CMD_VERSION] = ereg_replace( "^1[.]", "", $revInfo[1] );
296 $rVal[CMD_CONTENT] = $filedata;
298 foreach ( $rVal as $key => $value ) {
299 $this->_cvsDebug( "$key == [$value]" );
306 * This returns false if page was not deleted or could not be deleted
307 * else true is returned.
309 function delete_page($pagename)
311 $this->_cvsDebug( "delete_page [$pagename]" );
312 $filename = $this->_docDir . "/" . $pagename;
313 $metaFile = $this->_docDir . "/CVS/_" . $pagename;
315 // obtain a write block before deleting the file
316 if ( $this->_deleteFile( $filename ) == false ) {
320 $this->_deleteFile( $metaFile );
322 $this->_removePage( $pagename );
324 $this->_cvsDebug( "CvsRemoveOutput [$cmdRemoveOutput]" );
329 function delete_versiondata($pagename, $version)
331 // TODO: Not Implemented.
332 // TODO: This is, for CVS, difficult because it implies removing a
333 // TODO: revision somewhere in the middle of a revision tree, and
334 // TODO: this is basically not possible!
335 trigger_error("delete_versiondata: Not Implemented", E_USER_WARNING);
338 function set_versiondata($pagename, $version, $data)
340 // TODO: Not Implemented.
341 // TODO: requires changing the log(commit) message for a particular
342 // TODO: version and this can't be done??? (You can edit the repository
343 // TODO: file directly but i don't know of a way of doing it via
344 // TODO: the cvs tools).
345 trigger_error("set_versiondata: Not Implemented", E_USER_WARNING);
348 function update_versiondata($pagename, $version, $newdata)
350 // TODO: same problem as set_versiondata
351 trigger_error("set_versiondata: Not Implemented", E_USER_WARNING);
354 function set_links($pagename, $links)
356 // TODO: to be implemented
357 trigger_error("set_links: Not Implemented", E_USER_WARNING);
360 function get_links($pagename, $reversed)
362 // TODO: to be implemented
363 trigger_error("get_links: Not Implemented", E_USER_WARNING);
366 function get_all_revisions($pagename)
368 // TODO: should replace this with something more efficient
369 include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
370 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
373 function get_all_pages($include_defaulted)
375 // FIXME: this ignores the include_defaulted parameter.
376 return new Cvs_Backend_Array_Iterator(
377 $this->_getAllFileNamesInDir( $this->_docDir ));
380 function text_search($search = '', $fullsearch = false)
383 return new Cvs_Backend_Full_Search_Iterator(
384 $this->_getAllFileNamesInDir( $this->_docDir ),
388 return new Cvs_Backend_Title_Search_Iterator(
389 $this->_getAllFileNamesInDir( $this->_docDir ),
394 function most_popular($limit)
398 function most_recent($params)
402 function lock($write_lock = true)
406 function unlock($force = false)
431 // ..-.-..-.-..-.-.. .--..-......-.--. --.-....----.....
432 // The rest are all internal methods, not to be used
434 // ..-.-..-.-..-.-.. .--..-......-.--. --.-....----.....
437 function _writeMetaInfo( $pagename, $hashInfo )
439 $this->_writeFileWithPath( $this->_docDir . "/CVS/_" . $pagename,
440 serialize( $hashInfo ) );
442 function _writePage( $pagename, $content )
444 $this->_writeFileWithPath( $this->_docDir . "/". $pagename, $content );
446 function _removePage( $pagename )
448 $cmdLine = sprintf("cd %s; cvs remove %s 2>&1; cvs commit -m '%s' "
449 ."%s 2>&1", $this->_docDir, $pagename,
450 "remove page", $pagename );
452 $this->_execCommand( $cmdLine, $cmdRemoveOutput, true );
454 function _commitPage( $pagename, $commitMsg = "no_message" )
456 $cmdLine = sprintf( "cd %s; cvs commit -m \"%s\" %s 2>&1",
457 $this->_docDir, escapeshellcmd( $commitMsg ),
459 $this->_execCommand( $cmdLine, $cmdOutput, true );
461 function _addPage( $pagename )
463 // TODO: need to add a check for the mimetype so that binary
464 // TODO: files are added as binary files
465 $cmdLine = sprintf("cd %s; cvs add %s 2>&1", $this->_docDir,
467 $this->_execCommand( $cmdLine, $cmdAddOutput, true );
471 * Returns an array containing all the names of files contained
472 * in a particular directory. The list is sorted according the
473 * string representation of the filenames.
475 function _getAllFileNamesInDir( $dirName )
478 $d = opendir( $dirName );
479 while ( $entry = readdir( $d ) ) {
480 $namelist[] = $entry;
483 sort( $namelist, SORT_STRING );
488 * Recursively create all directories.
490 function _mkdir( $path, $mode )
492 $directoryName = dirname( $path );
493 if ( $directoryName != "/" && $directoryName != "\\"
494 && !is_dir( $directoryName ) && $directoryName != "" ) {
495 $rVal = $this->_mkdir( $directoryName, $mode );
501 return ($rVal && @mkdir( $path, $mode ) );
505 * Recursively create all directories and then the file.
507 function _createFile( $path, $mode )
509 $this->_mkdir( dirname( $path ), $mode );
511 chmod( $path, $mode );
514 * The lord giveth, and the lord taketh.
516 function _deleteFile( $filename )
518 if( $fd = fopen($filename, 'a') ) {
520 $locked = flock($fd,2); // Exclusive blocking lock
523 $this->_cvsError("Unable to delete file, lock was not obtained.",
524 __LINE__, $filename, EM_NOTICE_ERRORS );
527 if ( ($rVal = unlink( $filename )) == 0 ) {
528 /* if successful, then do nothing */
530 $this->_cvsDebug( "[$filename] --> Unlink returned [$rVal]" );
535 $this->_cvsError("deleteFile: Unable to open file",
536 __LINE__, $filename, EM_NOTICE_ERRORS );
542 * Called when something happened that causes the CVS backend to
545 function _cvsError( $msg = "no message",
547 $errfile = "lib/WikiDB/backend/cvs.php",
548 $errno = EM_FATAL_ERRORS)
550 $err = new PhpError( $errno, "[CVS(be)]: " . $msg, $errfile, $errline);
551 // send error to the debug routine
552 $this->_cvsDebug( $err->getDetail() );
553 // send the error to the error manager
554 $GLOBALS['ErrorManager']->handleError( $err );
558 * Debug function specifically for the CVS database functions.
559 * Can be deactived by setting the WikiDB['debug_file'] to ""
561 function _cvsDebug( $msg )
563 if ( $this->_debug_file == "" ) {
567 if ( !file_exists( $this->_debug_file ) ) {
568 $this->_createFile( $this->_debug_file, 0755 );
571 if ( $fdlock = @fopen( $this->_debug_file, 'a' ) ) {
572 $locked = flock( $fdlock, 2 );
578 $fdappend = @fopen( $this->_debug_file, 'a' );
579 fwrite( $fdappend, ($msg . "\n") );
584 // TODO: this should be replaced ...
585 print( "unable to locate/open [$filename], turning debug off\n" );
586 $this->_debug_file = "";
591 * Execute a command and potentially exit if the flag exitOnNonZero is
592 * set to true and the return value was nonZero
594 function _execCommand( $cmdLine, &$cmdOutput, $exitOnNonZero )
596 $this->_cvsDebug( "Preparing to execute [$cmdLine]" );
597 exec( $cmdLine, $cmdOutput, $cmdReturnVal );
598 if ( $exitOnNonZero && ($cmdReturnVal != 0) ) {
599 $this->_cvsError("Command failed [$cmdLine], Return value: $cmdReturnVal",
601 $this->_cvsDebug( "Command failed [$cmdLine], Output: [" .
602 join("\n",$cmdOutput) . "]" );
604 $this->_cvsDebug( "Done execution [" . join("\n", $cmdOutput ) . "]" );
606 return $cmdReturnVal;
610 * Either replace the contents of an existing file or create a
611 * new file in the particular store using the page name as the
614 * Returns true if all went well, else false.
616 // function _WriteFile( $pagename, $storename, $contents )
619 // $filename = $WikiDB[$storename] . "/" . $pagename;
620 // _WriteFileWithPath( $filename, $contents );
623 function _writeFileWithPath( $filename, $contents )
625 if( $fd = fopen($filename, 'a') ) {
626 $locked = flock($fd,2); // Exclusive blocking lock
628 $this->_cvsError("Timeout while obtaining lock.",__LINE__);
631 //Second (actually used) filehandle
632 $fdsafe = fopen($filename, 'w');
633 fwrite($fdsafe, $contents);
637 $this->_cvsError( "Could not open file [$filename]", __LINE__ );
642 * Copy the contents of the source directory to the destination directory.
644 function _copyFilesFromDirectory( $src, $dest )
646 $this->_cvsDebug( "Copying from [$src] to [$dest]" );
648 if ( is_dir( $src ) && is_dir( $dest ) ) {
649 $this->_cvsDebug( "Copying " );
650 $d = opendir( $src );
651 while ( $entry = readdir( $d ) ) {
652 if ( is_file( $src . "/" . $entry )
653 && copy( $src . "/" . $entry, $dest . "/" . $entry ) ) {
654 $this->_cvsDebug( "Copied to [$dest/$entry]" );
656 $this->_cvsDebug( "Failed to copy [$src/$entry]" );
662 $this->_cvsDebug( "Not copying" );
668 * Unescape a string value. Normally this comes from doing an
669 * escapeshellcmd. This converts the following:
675 function _unescape( $val )
677 $val = str_replace( "\\{", "{", $val );
678 $val = str_replace( "\\}", "}", $val );
679 $val = str_replace( "\\;", ";", $val );
680 $val = str_replace( "\\\"", "\"", $val );
686 * Function for removing the newlines from the ends of the
687 * file data returned from file(..). This is used in retrievePage
689 function _strip_newlines( &$item, $key )
691 $item = ereg_replace( "\n$", "", $item );
694 } /* End of WikiDB_backend_cvs class */
697 * Generic iterator for stepping through an array of values.
699 class Cvs_Backend_Array_Iterator
700 extends WikiDB_backend_iterator
704 function Cvs_Backend_Iterator( $arrayValue = Array() )
706 $this->_array = $arrayValue;
711 while ( ($rVal = array_pop( $this->_array )) != NULL ) {
719 unset( $this->_array );
723 class Cvs_Backend_Full_Search_Iterator
724 extends Cvs_Backend_Array_Iterator
726 var $_searchString = '';
729 function Cvs_Backend_Title_Search_Iterator( $arrayValue = Array(),
733 $this->Cvs_Backend_Array_Iterator( $arrayValue );
734 $_searchString = $searchString;
735 $_docDir = $documentDir;
741 $pageName = Cvs_Backend_Array_Iterator::next();
742 } while ( !$this->_searchFile( $_searchString,
743 $_docDir . "/" . $pageName ));
749 * Does nothing more than a grep and search the entire contents
750 * of the given file. Returns TRUE of the searchstring was found,
751 * false if the search string wasn't find or the file was a directory
752 * or could not be read.
754 function _searchFile( $searchString, $fileName )
756 // TODO: using grep here, it might make more sense to use
757 // TODO: some sort of inbuilt/language specific method for
758 // TODO: searching files.
759 $cmdLine = sprintf( "grep -E -i '%s' %s > /dev/null 2>&1",
760 $searchString, $fileName );
762 return ( WikiDB_backend_cvs::_execCommand( $cmdLine, $cmdOutput,
768 * Iterator used for doing a title search.
770 class Cvs_Backend_Title_Search_Iterator
771 extends Cvs_Backend_Array_Iterator
773 var $_searchString = '';
775 function Cvs_Backend_Title_Search_Iterator( $arrayValue = Array(),
778 $this->Cvs_Backend_Array_Iterator( $arrayValue );
779 $_searchString = $searchString;
785 $pageName = Cvs_Backend_Array_Iterator::next();
786 } while ( !eregi( $this->_searchString, $pageName ) );