]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/cvs.php
Clarify API: sortby,limit and exclude are strings.
[SourceForge/phpwiki.git] / lib / WikiDB / backend / cvs.php
1 <?php
2 rcs_id('$Id: cvs.php,v 1.27 2007-01-04 16:45:49 rurban Exp $');
3 /**
4  * Backend for handling CVS repository. 
5  *
6  * ASSUMES: that the shell commands 'cvs', 'grep', 'rm', are all located 
7  * ASSUMES: in the path of the server calling this script.
8  *
9  * Author: Gerrit Riessen, gerrit.riessen@open-source-consultants.de
10  */
11
12 require_once('lib/WikiDB/backend.php');
13 require_once('lib/ErrorManager.php');
14
15 /** 
16  * Constants used by the CVS backend 
17  **/
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' );
25
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');
33 define( 'CMD_LINK_ATT',      '_links_' );
34
35 // file names used to store specific information
36 define( 'CVS_MP_FILE',              '.most_popular' );
37 define( 'CVS_MR_FILE',              '.most_recent' );
38
39 class WikiDB_backend_cvs
40 extends WikiDB_backend
41 {
42     var $_docDir;
43     var $_repository;
44     var $_module_name;
45     var $_debug_file;
46
47     /**
48      * In the following parameters should be defined in dbparam:
49      *   . wiki ==> directory where the pages should be stored
50      *              this is not the CVS repository location
51      *   . repository ==> local directory where the repository should be 
52      *                    created. This can also be a :pserver: but then
53      *                    set check_for_repository to false and checkout
54      *                    the documents beforehand. (This is basically CVSROOT)
55      *   . check_for_repository ==> boolean flag to indicate whether the 
56      *                              repository should be created, this only
57      *                              applies to local directories, for pserver
58      *                              set this to false and check out the 
59      *                              document base beforehand
60      *   . debug_file ==> file name where debug information should be sent.
61      *                    If file doesn't exist then it's created, if this
62      *                    is empty, then debugging is turned off.
63      *   . pgsrc ==> directory name where the default wiki pages are stored.
64      *               This is only required if the backend is to create a
65      *               new CVS repository.
66      *
67      * The class also adds a parameter 'module_name' to indicate the name
68      * of the cvs module that is being used to version the documents. The
69      * module_name is assumed to be the base name of directory given in
70      * wiki, e.g. if wiki == '/some/path/to/documents' then module_name 
71      * becomes 'documents' and this module will be created in the CVS 
72      * repository or assumed to exist. If on the other hand the parameter
73      * already exists, then it is not overwritten.
74      */
75     function WikiDB_backend_cvs( $dbparam ) 
76     {
77         // setup all the instance values.
78         $this->_docDir = $dbparam{CVS_DOC_DIR};
79         $this->_repository = $dbparam{CVS_REPOSITORY};
80         if ( ! $dbparam{CVS_MODULE_NAME} ) {
81             $this->_module_name = basename( $this->_docDir );
82             $dbparam{CVS_MODULE_NAME} = $this->_module_name;
83         } else {
84             $this->_module_name = $dbparam{CVS_MODULE_NAME};
85         }
86         $this->_debug_file = $dbparam{CVS_DEBUG_FILE};
87
88         if ( $dbparam{CVS_CHECK_FOR_REPOSITORY}
89              && !( is_dir( $this->_repository )
90                    && is_dir( $this->_repository . "/CVSROOT" )
91                    && is_dir( $this->_repository . "/" . $this->_module_name ))) {
92
93             $this->_cvsDebug( sprintf("Creating new repository [%s]", $this->_repository) );
94
95             // doesn't exist, need to create it and the replace the wiki 
96             // document directory.
97             $this->_mkdir( $this->_repository, 0775 );
98     
99             // assume that the repository is a local directory, prefix :local:
100             if ( !ereg( "^:local:", $this->_repository ) ) {
101                 $this->_repository = ":local:" . $this->_repository;
102             }
103             
104             $cmdLine = sprintf( "cvs -d \"%s\" init", $this->_repository);
105             $this->_execCommand( $cmdLine, $cmdOutput, true );
106
107             $this->_mkdir( $this->_docDir, 0775 );
108             $cmdLine = sprintf("cd %s; cvs -d \"%s\" import -m no_message "
109                                ."%s V R", $this->_docDir, $this->_repository,
110                                $this->_module_name );
111             $this->_execCommand( $cmdLine, $cmdOutput, true );
112             
113             // remove the wiki directory and check it out from the 
114             // CVS repository
115             $cmdLine = sprintf( "rm -fr %s; cd %s; cvs -d \"%s\" co %s",
116                                 $this->_docDir, dirname($this->_docDir), 
117                                 $this->_repository, $this->_module_name);
118             $this->_execCommand( $cmdLine, $cmdOutput, true );
119             
120             // add the default pages using the update_pagedata
121             $metaData = array();
122             $metaData[$AUTHOR] = "PhpWiki -- CVS Backend";
123
124             if ( is_dir( $dbparam[CVS_PAGE_SOURCE] ) ) {
125                 $d = opendir( $dbparam[CVS_PAGE_SOURCE] );
126                 while ( $entry = readdir( $d ) ) {
127                     $filename = $dbparam[CVS_PAGE_SOURCE] . "/" . $entry;
128                     $this->_cvsDebug( sprintf("Found [%s] in [%s]", $entry, $dbparam[CVS_PAGE_SOURCE]) );
129                     
130                     if ( is_file( $filename ) ) {
131                         $metaData[CMD_CONTENT] = join('',file($filename));
132                         $this->update_pagedata( $entry, $metaData );
133                     }
134                 }
135                 closedir( $d );
136             }
137             
138             // ensure that the results of the is_dir are cleared
139             clearstatcache();
140         }
141     }
142
143     /**
144      * Return: metadata about page
145      */
146     function get_pagedata($pagename) 
147     {
148         // the metadata information about a page is stored in the 
149         // CVS directory of the document root in serialized form. The
150         // file always has the name, i.e. '_$pagename'.
151         $metaFile = $this->_docDir . "/CVS/_" . $pagename;
152
153         if ( file_exists( $metaFile ) ) {
154             
155             $megaHash = 
156                  unserialize(join( '',$this->_readFileWithPath($metaFile)));
157
158             $filename = $this->_docDir . "/" . $pagename;
159             if ( file_exists( $filename ) ) {
160                 $megaHash[CMD_CONTENT] = $this->_readFileWithPath( $filename );
161             } else {
162                 $megaHash[CMD_CONTENT] = "";
163             }
164
165             $this->_updateMostRecent( $pagename );
166             $this->_updateMostPopular( $pagename );
167
168             return $megaHash;
169         } else {
170             return false;
171         }
172     }
173
174     /**
175      * This will create a new page if page being requested does not
176      * exist.
177      */
178     function update_pagedata($pagename, $newdata = array() ) 
179     {
180         // check argument
181         if ( ! is_array( $newdata ) ) {
182             trigger_error("update_pagedata: Argument 'newdata' was not array", 
183                           E_USER_WARNING);
184         }
185
186         // retrieve the meta data
187         $metaData = $this->get_pagedata( $pagename );
188
189         if ( ! $metaData ) {
190             $this->_cvsDebug("update_pagedata: no meta data found");
191             // this means that the page does not exist, we need to create
192             // it.
193             $metaData = array();
194
195             $metaData[CMD_CREATED] = time();
196             $metaData[CMD_VERSION] = "1";
197
198             if ( ! isset($newdata[CMD_CONTENT])) {
199                 $metaData[CMD_CONTENT] = "";
200             } else {
201                 $metaData[CMD_CONTENT] = $newdata[CMD_CONTENT];
202             }
203
204             // create an empty page ...
205             $this->_writePage( $pagename, $metaData[CMD_CONTENT] );
206             $this->_addPage( $pagename );
207
208             // make sure that the page is written and committed a second time
209             unset( $newdata[CMD_CONTENT] );
210             unset( $metaData[CMD_CONTENT] );
211         }
212
213         // change any meta data information
214         foreach ( $newdata as $key => $value ) {
215             if ( $value == false || empty( $value ) ) {
216                 unset( $metaData[$key] );
217             } else {
218                 $metaData[$key] = $value;
219             }
220         }
221
222         // update the page data, if required. Use newdata because it could
223         // be empty and thus unset($metaData[CMD_CONTENT]).
224         if ( isset( $newdata[CMD_CONTENT] ) ) {
225             $this->_writePage( $pagename, $newdata[CMD_CONTENT] );
226         }
227
228         // remove any content from the meta data before storing it
229         unset( $metaData[CMD_CONTENT] );
230         $metaData[CMD_LAST_MODIFIED] = time();
231
232         $metaData[CMD_VERSION] = $this->_commitPage( $pagename, $metaData );
233         $this->_writeMetaInfo( $pagename, $metaData );
234     }
235
236     function get_latest_version($pagename) 
237     {
238         $metaData = $this->get_pagedata( $pagename );
239         if ( $metaData ) {
240             // the version number is everything after the '1.'
241             return $metaData[CMD_VERSION];
242         } else {
243             $this->_cvsDebug(sprintf("get_latest_versioned FAILED for [%s]", $pagename));
244             return 0;
245         }
246     }
247
248     function get_previous_version($pagename, $version) 
249     {
250         // cvs increments the version numbers, so this is real easy ;-)
251         return ($version > 0 ? $version - 1 : 0);
252     }
253
254     /**
255      * the version parameter is assumed to be everything after the '1.'
256      * in the CVS versioning system.
257      */
258     function get_versiondata($pagename, $version, $want_content = false) 
259     {
260         $this->_cvsDebug( "get_versiondata: [$pagename] [$version] [$want_content]" );
261       
262         $filedata = "";
263         if ( $want_content ) {
264             // retrieve the version from the repository
265             $cmdLine = sprintf("cvs -d \"%s\" co -p -r 1.%d %s/%s 2>&1", 
266                                $this->_repository, $version, 
267                                $this->_module_name, $pagename );
268             $this->_execCommand( $cmdLine, $filedata, true );
269         
270             // TODO: DEBUG: 5 is a magic number here, depending on the
271             // TODO: DEBUG: version of cvs used here, 5 might have to
272             // TODO: DEBUG: change. Basically find a more reliable way of
273             // TODO: DEBUG: doing this.
274             // the first 5 lines contain various bits of 
275             // administrative information that can be ignored.
276             for ( $i = 0; $i < 5; $i++ ) {
277                 array_shift( $filedata );
278             }
279         }
280
281         /**
282          * Now obtain the rest of the pagehash information, this is contained
283          * in the log message for the revision in serialized form.
284          */
285         $cmdLine = sprintf("cd %s; cvs log -r1.%d %s", $this->_docDir,
286                            $version, $pagename );
287         $this->_execCommand( $cmdLine, $logdata, true );
288
289         // shift log data until we get to the 'revision X.X' line
290         // FIXME: ensure that we don't enter an endless loop here
291         while ( !ereg( "^revision 1.([0-9]+)$", $logdata[0], $revInfo ) ) {
292             array_shift( $logdata );
293         }
294
295         // serialized hash information now stored in position 2
296         $rVal = unserialize( _unescape( $logdata[2] ) );
297
298         // version information is incorrect
299         $rVal[CMD_VERSION] = $revInfo[1];
300         $rVal[CMD_CONTENT] = $filedata;
301
302         foreach ( $rVal as $key => $value ) {
303             $this->_cvsDebug( "$key == [$value]" );
304         }
305       
306         return $rVal;
307     }
308
309     /**
310      * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
311      * See backend.php
312      */
313     //function delete_page($pagename) { $this->purge_page($pagename); }
314
315     /**
316      * This returns false if page was not deleted or could not be deleted
317      * else return true.
318      */
319     function purge_page($pagename) 
320     {
321         $this->_cvsDebug( "delete_page [$pagename]") ;
322         $filename = $this->_docDir . "/" . $pagename;
323         $metaFile = $this->_docDir . "/CVS/_" . $pagename;
324         
325         // obtain a write block before deleting the file
326         if ( $this->_deleteFile( $filename ) == false ) {
327             return false;
328         }
329         
330         $this->_deleteFile( $metaFile );
331         
332         $this->_removePage( $pagename );
333
334         return true;
335     }
336
337     /**
338      * For now delete and create a new one.
339      *
340      * This returns false if page was not renamed,
341      * else return true.
342      */
343     function rename_page($pagename, $to) 
344     {
345         $this->_cvsDebug( "rename_page [$pagename,$to]") ;
346         $data = get_pagedata($pagename);
347         if (isset($data['pagename']))
348           $data['pagename'] = $to;
349         //$version = $this->get_latest_version($pagename);
350         //$vdata = get_versiondata($pagename, $version, 1);
351         //$data[CMD_CONTENT] = $vdata[CMD_CONTENT];
352         $this->delete_page($pagename);
353         $this->update_pagedata($to, $data);
354         return true;
355     }
356
357     function delete_versiondata($pagename, $version) 
358     {
359         // TODO: Not Implemented.
360         // TODO: This is, for CVS, difficult because it implies removing a
361         // TODO: revision somewhere in the middle of a revision tree, and
362         // TODO: this is basically not possible!
363         trigger_error("delete_versiondata: Not Implemented", E_USER_WARNING);
364     }
365
366     function set_versiondata($pagename, $version, $data) 
367     {
368         // TODO: Not Implemented.
369         // TODO: requires changing the log(commit) message for a particular
370         // TODO: version and this can't be done??? (You can edit the repository
371         // TODO: file directly but i don't know of a way of doing it via
372         // TODO: the cvs tools).
373         trigger_error("set_versiondata: Not Implemented", E_USER_WARNING);
374     }
375
376     function update_versiondata($pagename, $version, $newdata) 
377     {
378         // TODO: same problem as set_versiondata
379         trigger_error("set_versiondata: Not Implemented", E_USER_WARNING);
380     }
381
382     function set_links($pagename, $links) 
383     {
384         // TODO: needs to be tested ....
385         $megaHash = get_pagedata( $pagename );
386         $megaHash[CMD_LINK_ATT] = $links;
387         $this->_writeMetaInfo( $pagename, $megaHash );
388     }
389
390     function get_links($pagename, $reversed=true, $include_empty=false,
391                        $sortby='', $limit='', $exclude='')
392     {
393         // TODO: ignores the $reversed argument and returns
394         // TODO: the value of _links_ attribute of the meta information
395         // TODO: to implement a reversed version, i guess, we going to
396         // TODO: need to do a grep on all files for the pagename in 
397         // TODO: in question and return all those page names that contained
398         // TODO: the required pagename!
399         $megaHash = get_pagedata( $pagename );
400         return $megaHash[CMD_LINK_ATT];
401     }
402
403     /* function get_all_revisions($pagename) {
404         // TODO: should replace this with something more efficient
405         include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
406         return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
407     } */
408
409     function get_all_pages($include_empty=false, $sortby='', $limit='') 
410     {
411         // FIXME: this ignores the parameters.
412         return new Cvs_Backend_Array_Iterator(
413                               $this->_getAllFileNamesInDir( $this->_docDir ));
414     }
415
416     function text_search($search, $fullsearch = false, $orderby=false, $limit='', $exclude='') 
417     {
418         if ( $fullsearch ) {
419             $iter = new Cvs_Backend_Full_Search_Iterator(
420                                $this->_getAllFileNamesInDir( $this->_docDir ), 
421                                $search, 
422                                $this->_docDir );
423             $iter->stoplisted =& $search->stoplisted;
424             return $iter;
425         } else {
426             return new Cvs_Backend_Title_Search_Iterator(
427                                $this->_getAllFileNamesInDir( $this->_docDir ),
428                                $search);
429         }
430     }
431
432     function most_popular($limit, $sortby='') {
433         // TODO: needs to be tested ...
434         $mp = $this->_getMostPopular();
435         if ($limit < 0){
436             asort ($mp, SORT_NUMERIC);
437             $limit = -$limit;
438         } else {
439             arsort( $mp, SORT_NUMERIC );
440         }
441         $returnVal = array();
442         
443         while ( (list($key, $val) = each($a)) && $limit > 0 ) {
444             $returnVal[] = $key;
445             $limit--;
446         }
447         return $returnVal;
448     }
449
450     /**
451      * This only accepts the 'since' and 'limit' attributes, everything
452      * else is ignored.
453      */
454     function most_recent($params) 
455     {
456         // TODO: needs to be tested ...
457         // most recent are those pages with the highest time value ...
458         $mr = $this->_getMostRecent();
459         $rev = false;
460         $returnVal = array();
461         if ( isset( $params['limit'] ) ) {
462             $limit = $params['limit'];
463             $rev = $limit < 0;
464         }
465         if ($rev){
466             arsort( $mr, SORT_NUMERIC );
467         } else {
468             asort( $mr, SORT_NUMERIC );
469         }
470         if ( isset( $limit ) ) {
471             while ( (list($key, $val) = each($a)) && $limit > 0 ) {
472                 $returnVal[] = $key;
473                 $limit--;
474             }
475         } else if ( isset( $params['since'] ) ) {
476             while ( (list($key, $val) = each($a)) ) {
477                 
478                 if ( $val > $params['since'] ) {
479                     $returnVal[] = $key;
480                 }
481             }
482         }
483
484         return new Cvs_Backend_Array_Iterator( $returnVal );
485     }
486
487     function lock($write_lock = true) 
488     {
489         // TODO: to be implemented
490         trigger_error("lock: Not Implemented", E_USER_WARNING);
491     }
492
493     function unlock($force = false) 
494     {
495         // TODO: to be implemented
496         trigger_error("unlock: Not Implemented", E_USER_WARNING);
497     }
498
499     function close () 
500     {
501     }
502
503     function sync() 
504     {
505     }
506
507     function optimize() 
508     {
509     }
510
511     /**
512      * What we do here is take a listing of the documents directory and
513      * check that each page has metadata file. If not, then a metadata
514      * file is created for the page.
515      *
516      * This can happen if rebuild() was called and someone has added
517      * files to the CVS repository not via PhpWiki. These files are 
518      * added to the document directory but without any metadata files.
519      */
520     function check() 
521     {
522         // TODO:
523         // TODO: test this .... i.e. add test to unit test file.
524         // TODO:
525         $page_names = $this->_getAllFileNamesInDir($this->_docDir);
526         $meta_names = $this->_getAllFileNamesInDir($this->_docDir . "/CVS");
527
528         array_walk( $meta_names, '_strip_leading_underscore' );
529         reset( $meta_names );
530         $no_meta_files = array_diff( $page_names, $meta_names );
531
532         array_walk( $no_meta_files, '_create_meta_file', $this );
533
534         return true;
535     }
536
537     /**
538      * Do an update of the CVS repository 
539      */
540     function rebuild() 
541     {
542         // TODO:
543         // TODO: test this .... i.e. add test to unit test file.
544         // TODO:
545         $cmdLine = sprintf( "cd %s; cvs update -d 2>&1", $this->_docDir );
546         $this->_execCommand( $cmdLine, $cmdOutput, true );
547         return true;
548     }
549     
550     // 
551     // ..-.-..-.-..-.-.. .--..-......-.--. --.-....----.....
552     // The rest are all internal methods, not to be used 
553     // directly.
554     // ..-.-..-.-..-.-.. .--..-......-.--. --.-....----.....
555     //
556     function _create_meta_file( $page_name, $key, &$backend )
557     {
558         // this is used as part of an array walk and therefore takes
559         // the backend argument
560         $backend->_cvsDebug(sprintf("Creating meta file for [%s]", $page_name));
561         $backend->update_pagedata( $page_name, array() );
562     }
563
564     function _strip_leading_underscore( &$item ) 
565     {
566         $item = ereg_replace( "^_", "", $item );
567     }
568
569     /**
570      * update the most popular information by incrementing the count
571      * for the following page. If the page was not defined, it is entered
572      * with a value of 1.
573      */
574     function _updateMostPopular( $pagename )
575     {
576         $mp = $this->_getMostPopular();
577         if ( isset( $mp[$pagename] ) ) {
578             $mp[$pagename]++;
579         } else {
580             $mp[$pagename] = 1;
581         }
582         $this->_writeFileWithPath( $this->_docDir . "/CVS/" . CVS_MP_FILE, 
583                                    serialize( $mp ) );
584     }
585
586
587     /**
588      * Returns an array containing the most popular information. This
589      * creates the most popular file if it does not exist.
590      */
591     function _getMostPopular()
592     {
593         $mostPopular = $this->_docDir . "/CVS/" . CVS_MP_FILE;
594         if ( !file_exists( $mostPopular ) ) {
595             $this->_writeFileWithPath( $mostPopular, serialize( array() ) );
596         }
597         return unserialize(join( '',$this->_readFileWithPath($mostPopular)));
598     }
599
600     function _getMostRecent()
601     {
602         $mostRecent = $this->_docDir . "/CVS/" . CVS_MR_FILE;
603         if ( !file_exists( $mostRecent ) ) {
604             $this->_writeFileWithPath( $mostRecent, serialize( array() ) );
605         }
606         return unserialize(join( '',$this->_readFileWithPath($mostRecent)));
607     }
608
609     function _updateMostRecent( $pagename )
610     {
611         $mr = $this->_getMostRecent();
612         $mr[$pagename] = time();
613         $this->_writeFileWithPath( $this->_docDir . "/CVS/" . CVS_MR_FILE, 
614                                    serialize( $mr ) );
615     }
616
617     function _writeMetaInfo( $pagename, $hashInfo )
618     {
619         $this->_writeFileWithPath( $this->_docDir . "/CVS/_" . $pagename, 
620                                    serialize( $hashInfo ) );
621     }
622     function _writePage( $pagename, $content )
623     {
624         $this->_writeFileWithPath( $this->_docDir . "/". $pagename, $content );
625     }
626     function _removePage( $pagename )
627     {
628         $cmdLine = sprintf("cd %s; cvs remove %s 2>&1; cvs commit -m '%s' "
629                            ."%s 2>&1", $this->_docDir, $pagename, 
630                            "remove page", $pagename );
631         
632         $this->_execCommand( $cmdLine, $cmdRemoveOutput, true );
633     }
634
635     /**
636      * this returns the new version number of the file.
637      */
638     function _commitPage( $pagename, &$meta_data )
639     {
640         $cmdLine = sprintf( "cd %s; cvs commit -m \"%s\" %s 2>&1", 
641                             $this->_docDir, 
642                             escapeshellcmd( serialize( $meta_data ) ),
643                             $pagename );
644         $this->_execCommand( $cmdLine, $cmdOutput, true );
645
646         $cmdOutput = implode( "\n", $cmdOutput );
647         $revInfo = array();
648         ereg( "\nnew revision: 1[.]([0-9]+); previous revision: ", $cmdOutput,
649               $revInfo );
650
651         $this->_cvsDebug( "CP: revInfo 0: $revInfo[0]" );
652         $this->_cvsDebug( "CP: $cmdOutput" );
653         if ( isset( $revInfo[1] ) ) {
654             $this->_cvsDebug( "CP: got revision information" );
655             return $revInfo[1];
656         } else {
657             ereg( "\ninitial revision: 1[.]([0-9]+)", $cmdOutput, $revInfo );
658             if ( isset( $revInfo[1] ) ) {
659                 $this->_cvsDebug( "CP: is initial release" );
660                 return 1;
661             }
662             $this->_cvsDebug( "CP: returning old version" );
663             return $meta_data[CMD_VERSION];
664         }
665     }
666     function _addPage( $pagename )
667     {
668         // TODO: need to add a check for the mimetype so that binary
669         // TODO: files are added as binary files
670         $cmdLine = sprintf("cd %s; cvs add %s 2>&1", $this->_docDir, 
671                            $pagename );
672         $this->_execCommand( $cmdLine, $cmdAddOutput, true );
673     }
674
675     /**
676      * Returns an array containing all the names of files contained
677      * in a particular directory. The list is sorted according the 
678      * string representation of the filenames.
679      */
680     function _getAllFileNamesInDir( $dirName ) 
681     {
682         $namelist = array();
683         $d = opendir( $dirName );
684         while ( $entry = readdir( $d ) ) {
685             $namelist[] = $entry;
686         }
687         closedir( $d );
688         sort( $namelist, SORT_STRING );
689         return $namelist;
690     }
691
692     /**
693      * Recursively create all directories.
694      */
695     function _mkdir( $path, $mode ) 
696     {
697         $directoryName = dirname( $path );
698         if ( $directoryName != "/" && $directoryName != "\\"  
699              && !is_dir( $directoryName ) && $directoryName != "" ) {
700             $rVal = $this->_mkdir( $directoryName, $mode );
701         }
702         else {
703             $rVal = true;
704         }
705       
706         return ($rVal && @mkdir( $path, $mode ) );
707     }
708
709     /**
710      * Recursively create all directories and then the file.
711      */
712     function _createFile( $path, $mode ) 
713     {
714         $this->_mkdir( dirname( $path ), $mode );
715         touch( $path );
716         chmod( $path, $mode );
717     }
718
719     /**
720      * The lord giveth, and the lord taketh.
721      */
722     function _deleteFile( $filename )
723     {
724         if( $fd = fopen($filename, 'a') ) { 
725             
726             $locked = flock($fd,2);  // Exclusive blocking lock 
727
728             if (!$locked) { 
729                 $this->_cvsError("Unable to delete file, lock was not obtained.",
730                                  __LINE__, $filename, EM_NOTICE_ERRORS );
731             } 
732
733             if ( ($rVal = unlink( $filename )) != 0 ) {
734                 $this->_cvsDebug( "[$filename] --> Unlink returned [$rVal]" );
735             }
736
737             return $rVal;
738         } else {
739             $this->_cvsError( "deleteFile: Unable to open file",
740                       __LINE__, $filename, EM_NOTICE_ERRORS );
741             return false;
742         }
743     }
744
745     /**
746      * Called when something happened that causes the CVS backend to 
747      * fail.
748      */
749     function _cvsError( $msg     = "no message", 
750                         $errline = 0, 
751                         $errfile = "lib/WikiDB/backend/cvs.php",
752                         $errno   = EM_FATAL_ERRORS)
753     {
754         $err = new PhpError( $errno, "[CVS(be)]: " . $msg, $errfile, $errline);
755         // send error to the debug routine
756         $this->_cvsDebug( $err->asXML() );
757         // send the error to the error manager
758         $GLOBALS['ErrorManager']->handleError( $err );
759     }
760
761     /**
762      * Debug function specifically for the CVS database functions.
763      * Can be deactived by setting the WikiDB['debug_file'] to ""
764      */
765     function _cvsDebug( $msg )  
766     {
767         if ( $this->_debug_file == "" ) {
768             return;
769         }
770         
771         if ( !file_exists( $this->_debug_file  ) ) {
772             $this->_createFile( $this->_debug_file, 0755 );
773         }
774
775         if ( $fdlock = @fopen( $this->_debug_file, 'a' ) ) {
776             $locked = flock( $fdlock, 2 );
777             if ( !$locked ) {
778                 fclose( $fdlock );
779                 return;
780             }
781             
782             $fdappend = @fopen( $this->_debug_file, 'a' );
783             fwrite( $fdappend, ($msg . "\n") );
784             fclose( $fdappend );
785             fclose( $fdlock );
786         }
787         else {
788             // TODO: this should be replaced ...
789             printf("unable to locate/open [%s], turning debug off\n", $filename);
790             $this->_debug_file = "";
791         }
792     }
793
794     /**
795      * Execute a command and potentially exit if the flag exitOnNonZero is 
796      * set to true and the return value was nonZero
797      */
798     function _execCommand( $cmdLine, &$cmdOutput, $exitOnNonZero )
799     {
800         $this->_cvsDebug( sprintf("Preparing to execute [%s]", $cmdLine) );
801         exec( $cmdLine, $cmdOutput, $cmdReturnVal );
802         if ( $exitOnNonZero && ($cmdReturnVal != 0) ) {
803             $this->_cvsDebug( sprintf("Command failed [%s], Output: ", $cmdLine) ."[". 
804                               join("\n",$cmdOutput) . "]" );
805             $this->_cvsError( sprintf("Command failed [%s], Return value: %s", $cmdLine, $cmdReturnVal),
806                             __LINE__ );
807         }
808         $this->_cvsDebug( "Done execution [" . join("\n", $cmdOutput) . "]" );
809
810         return $cmdReturnVal;
811     }
812
813     /**
814      * Read locks a file, reads it, and returns it contents
815      */
816     function _readFileWithPath( $filename ) 
817     {
818         if ( $fd = @fopen( $filename, "r" ) )  {
819             $locked = flock( $fd, 1 ); // read lock
820             if ( !$locked ) {
821                 fclose( $fd );
822                 $this->_cvsError( "Unable to obtain read lock.", __LINE__ );
823             }
824
825             $content = file( $filename );
826             fclose( $fd );
827             return $content;
828         } else {
829             $this->_cvsError( sprintf("Unable to open file '%s' for reading", $filename),
830                               __LINE__ );
831             return false;
832         }
833     }
834
835     /**
836      * Either replace the contents of an existing file or create a 
837      * new file in the particular store using the page name as the
838      * file name.
839      * 
840      * Nothing is returned, might be useful to return something ;-)
841      */
842     function _writeFileWithPath( $filename, $contents )
843     { 
844         // TODO: $contents should probably be a reference parameter ...
845         if( $fd = fopen($filename, 'a') ) { 
846             $locked = flock($fd,2);  // Exclusive blocking lock 
847             if (!$locked) { 
848                 $this->_cvsError( "Timeout while obtaining lock.", __LINE__ );
849             } 
850
851             // Second filehandle -- we use this to write the contents
852             $fdsafe = fopen($filename, 'w'); 
853             fwrite($fdsafe, $contents); 
854             fclose($fdsafe); 
855             fclose($fd);
856         } else {
857             $this->_cvsError( sprintf("Could not open file '%s' for writing", $filename), 
858                               __LINE__ );
859         }
860     }
861
862    /**
863     * Copy the contents of the source directory to the destination directory.
864     */
865     function _copyFilesFromDirectory( $src, $dest )
866     {
867         $this->_cvsDebug( sprintf("Copying from [%s] to [%s]", $src, $dest) );
868
869         if ( is_dir( $src ) && is_dir( $dest ) ) {
870             $this->_cvsDebug( "Copying " );
871             $d = opendir( $src );
872             while ( $entry = readdir( $d ) ) {
873                 if ( is_file( $src . "/" . $entry )
874                      && copy( $src . "/" . $entry, $dest . "/" . $entry ) ) {
875                     $this->_cvsDebug( sprintf("Copied to [%s]", "$dest/$entry") );
876                 } else {
877                     $this->_cvsDebug( sprintf("Failed to copy [%s]", "$src/$entry") );
878                 }
879             }
880             closedir( $d );
881             return true;
882         } else {
883             $this->_cvsDebug( "Not copying" );
884             return false;
885         }
886     }
887
888     /**
889      * Unescape a string value. Normally this comes from doing an 
890      * escapeshellcmd. This converts the following:
891      *    \{ --> {
892      *    \} --> }
893      *    \; --> ;
894      *    \" --> "
895      */
896     function _unescape( $val )
897     {
898         $val = str_replace( "\\{", "{", $val );
899         $val = str_replace( "\\}", "}", $val );
900         $val = str_replace( "\\;", ";", $val );
901         $val = str_replace( "\\\"", "\"", $val );
902         
903         return $val;
904     }
905
906     /**
907      * Function for removing the newlines from the ends of the
908      * file data returned from file(..). This is used in retrievePage
909      */
910     function _strip_newlines( &$item, $key )
911     {
912         $item = ereg_replace( "\n$", "", $item );
913     }
914
915 } /* End of WikiDB_backend_cvs class */
916
917 /**
918  * Generic iterator for stepping through an array of values.
919  */
920 class Cvs_Backend_Array_Iterator
921 extends WikiDB_backend_iterator
922 {
923     var $_array;
924
925     function Cvs_Backend_Iterator( $arrayValue = Array() )
926     {
927         $this->_array = $arrayValue;
928     }
929
930     function next() 
931     {
932         while ( ($rVal = array_pop( $this->_array )) != NULL ) {
933             return $rVal;
934         }
935         return false;
936     }
937
938     function count() {
939         return count($this->_array);
940     }
941
942     function free()
943     {
944         unset( $this->_array );
945     }
946 }
947
948 class Cvs_Backend_Full_Search_Iterator
949 extends Cvs_Backend_Array_Iterator
950 {
951     var $_searchString = '';
952     var $_docDir = "";
953
954     function Cvs_Backend_Title_Search_Iterator( $arrayValue = Array(),
955                                                 $searchString  = "",
956                                                 $documentDir = ".")
957     {
958         $this->Cvs_Backend_Array_Iterator( $arrayValue );
959         $_searchString = $searchString;
960         $_docDir = $documentDir;
961     }
962
963     function next()
964     {
965         do {
966             $pageName = Cvs_Backend_Array_Iterator::next();
967         } while ( !$this->_searchFile( $_searchString, 
968                                        $_docDir . "/" . $pageName ));
969
970         return $pageName;
971     }
972
973     /**
974      * Does nothing more than a grep and search the entire contents
975      * of the given file. Returns TRUE of the searchstring was found, 
976      * false if the search string wasn't find or the file was a directory
977      * or could not be read.
978      */
979     function _searchFile( $searchString, $fileName )
980     {
981         // TODO: using grep here, it might make more sense to use
982         // TODO: some sort of inbuilt/language specific method for
983         // TODO: searching files.
984         $cmdLine = sprintf( "grep -E -i '%s' %s > /dev/null 2>&1", 
985                             $searchString, $fileName );
986         
987         return ( WikiDB_backend_cvs::_execCommand( $cmdLine, $cmdOutput, 
988                                                    false ) == 0 );
989     }
990 }
991
992 /**
993  * Iterator used for doing a title search.
994  */
995 class Cvs_Backend_Title_Search_Iterator
996 extends Cvs_Backend_Array_Iterator
997 {
998     var $_searchString = '';
999
1000     function Cvs_Backend_Title_Search_Iterator( $arrayValue = Array(),
1001                                                 $searchString  = "")
1002     {
1003         $this->Cvs_Backend_Array_Iterator( $arrayValue );
1004         $_searchString = $searchString;
1005     }
1006
1007     function next()
1008     {
1009         do {
1010             $pageName = Cvs_Backend_Array_Iterator::next();
1011         } while ( !eregi( $this->_searchString, $pageName ) );
1012
1013         return $pageName;
1014     }
1015 }
1016
1017 // (c-file-style: "gnu")
1018 // Local Variables:
1019 // mode: php
1020 // tab-width: 8
1021 // c-basic-offset: 4
1022 // c-hanging-comment-ender-p: nil
1023 // indent-tabs-mode: nil
1024 // End:   
1025 ?>