]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/file.php
added log tag, converted file to unix format
[SourceForge/phpwiki.git] / lib / WikiDB / backend / file.php
1 <?php // -*-php-*-
2 rcs_id('$Id: file.php,v 1.2 2003-01-04 03:30:34 wainstead Exp $');
3
4 /**
5  * Backend for handling file storage. 
6  *
7  * Author: Jochen Kalmbach, Jochen@kalmbachnet.de
8  */
9
10 /*
11  * TODO: 
12  * - Implement "optimize" / "sync" / "check" / "rebuild"
13  * - Optimize "get_previous_version"
14  * - Optimize "get_links" (reversed = true)
15  * - Optimize "get_all_revisions"
16  * - Optimize "most_popular" (separate file for "hitcount", 
17  *   which contains all pages)
18  * - Optimize "most_recent"
19  * - What should be done in "lock"/"unlock"/"close" ?
20  * - "WikiDB_backend_file_iter": Do I need to return 'version' and 'versiondata' ?
21  *
22  */
23
24 require_once('lib/WikiDB/backend.php');
25 require_once('lib/ErrorManager.php');
26
27
28
29 class WikiDB_backend_file
30 extends WikiDB_backend
31 {
32     var $data_dir;
33     var $_dir_names;
34
35     var $_page_data;  // temorarily stores the pagedata (via _loadPageData)
36     var $_page_version_data;  // temorarily stores the versiondata (via _loadVersionData)
37     var $_latest_versions;  // temorarily stores the latest version-numbers (for every pagename)  (via _loadLatestVersions)
38     
39
40     function WikiDB_backend_file( $dbparam )
41     {
42         $this->data_dir = $dbparam['directory'];
43         if (is_dir($this->data_dir) == false) {
44             mkdir($this->data_dir, 0755);
45         }
46
47         $this->_dir_names
48             = array('ver_data'     => $this->data_dir.'/'.'ver_data',
49                     'page_data'    => $this->data_dir.'/'.'page_data',
50                     'latest_ver'   => $this->data_dir.'/'.'latest_ver',
51                     'links'        => $this->data_dir.'/'.'links' );
52
53         foreach ($this->_dir_names as $key => $val) {
54             if (is_dir($val) == false)
55                 mkdir($val, 0755);
56         }
57
58         $this->_page_data = NULL;
59         $this->_page_version_data = NULL;
60         $this->_latest_versions = NULL;
61
62
63     }
64
65     // *********************************************************************
66     // common file load / save functions:
67     function _pagename2filename($type, $pagename, $version) {
68          if ($version == 0)
69              return $this->_dir_names[$type].'/'.urlencode($pagename);
70          else
71              return $this->_dir_names[$type].'/'.urlencode($pagename).'--'.$version;
72     }
73
74     function _loadPage($type, $pagename, $version, $set_pagename = true) {
75       $filename = $this->_pagename2filename($type, $pagename, $version);
76       if ($fd = @fopen($filename, "rb")) {
77          $locked = flock($fd, 1); # Read lock
78          if (!$locked) { 
79             ExitWiki("Timeout while obtaining lock. Please try again"); 
80          }
81          if ($data = fread($fd, filesize($filename))) {
82             $pd = unserialize($data);
83             if ($set_pagename == true)
84                 $pd['pagename'] = $pagename;
85             if ($version != 0)
86                 $pd['version'] = $version;
87             if (!is_array($pd))
88                 ExitWiki(sprintf(gettext("'%s': corrupt file"),
89                                  htmlspecialchars($filename)));
90             else
91               return $pd;
92          }      
93          fclose($fd);
94       }
95       return NULL;
96     }
97
98     function _savePage($type, $pagename, $version, $data) {
99         $filename = $this->_pagename2filename($type, $pagename, $version);
100         if($fd = fopen($filename, 'a+b')) { 
101            $locked = flock($fd,2); #Exclusive blocking lock 
102            if (!$locked) { 
103               ExitWiki("Timeout while obtaining lock. Please try again"); 
104            }
105          
106
107            rewind($fd);
108            ftruncate($fd, 0);
109            $pagedata = serialize($data);
110            fwrite($fd, $pagedata); 
111            fclose($fd);
112         } else {
113            ExitWiki("Error while writing page '$pagename'");
114         }
115     }
116
117     function _removePage($type, $pagename, $version) {
118         $filename = $this->_pagename2filename($type, $pagename, $version);
119         $f = @unlink($filename);
120         if ($f == false)
121                   trigger_error("delete file failed: ".$filename." ver: ".$version, E_USER_WARNING);
122         
123     }
124     // *********************************************************************
125
126
127     // *********************************************************************
128     // Load/Save Version-Data
129     function _loadVersionData($pagename, $version) {
130         if ($this->_page_version_data != NULL) {
131             if ( ($this->_page_version_data['pagename'] == $pagename) && 
132                 ($this->_page_version_data['version'] == $version) ) {
133                 return $this->_page_version_data;
134              }
135         }
136         $vd = $this->_loadPage('ver_data', $pagename, $version);
137         if ($vd != NULL) {
138             $this->_page_version_data = $vd;
139             if ( ($this->_page_version_data['pagename'] == $pagename) && 
140                 ($this->_page_version_data['version'] == $version) ) {
141                 return $this->_page_version_data;
142              }
143         }
144         return NULL;
145     }
146
147     function _saveVersionData($pagename, $version, $data) {
148         $this->_savePage('ver_data', $pagename, $version, $data);
149
150         // check if this is a newer version:
151         if ($this->_getLatestVersion($pagename) < $version) {
152             // write new latest-version-info
153             $this->_setLatestVersion($pagename, $version);
154         }
155     }
156
157
158     // *********************************************************************
159     // Load/Save Page-Data
160     function _loadPageData($pagename) {
161         if ($this->_page_data != NULL) {
162             if ($this->_page_data['pagename'] == $pagename) {
163                 return $this->_page_data;
164              }
165         }
166         $pd = $this->_loadPage('page_data', $pagename, 0);
167         if ($pd != NULL)
168             $this->_page_data = $pd;
169         if ($this->_page_data != NULL) {
170             if ($this->_page_data['pagename'] == $pagename) {
171                 return $this->_page_data;
172              }
173         }
174         return array();  // no values found
175     }
176
177     function _savePageData($pagename, $data) {
178         $this->_savePage('page_data', $pagename, 0, $data);
179     }
180
181     // *********************************************************************
182     // Load/Save Latest-Version
183     function _saveLatestVersions() {
184         $data = $this->_latest_versions;
185         if ($data == NULL)
186             $data = array();
187         $this->_savePage('latest_ver', 'latest_versions', 0, $data);
188     }
189
190     function _setLatestVersion($pagename, $version) {
191         // make sure the page version list is loaded:
192         $this->_getLatestVersion($pagename);
193         if ($version > 0) {
194             $this->_getLatestVersion($pagename);
195             $this->_latest_versions[$pagename] = $version;
196         }
197         else {
198             // Remove this page from the Latest-Version-List:
199             unset($this->_latest_versions[$pagename]);
200         }
201         $this->_saveLatestVersions();
202     }
203
204     function _loadLatestVersions() {
205         if ($this->_latest_versions != NULL)
206             return;
207
208         $pd = $this->_loadPage('latest_ver', 'latest_versions', 0, false);
209         if ($pd != NULL)
210             $this->_latest_versions = $pd;
211         else
212             $this->_latest_versions = array(); // empty array
213     }
214
215     function _getLatestVersion($pagename) {
216        $this->_loadLatestVersions();
217        if (array_key_exists($pagename, $this->_latest_versions) == false)
218            return 0; // do version exists
219        return $this->_latest_versions[$pagename];
220     }
221
222
223     // *********************************************************************
224     // Load/Save Page-Links
225     function _loadPageLinks($pagename) {
226         $pd = $this->_loadPage('links', $pagename, 0, false);
227         if ($pd != NULL)
228             return $pd;;
229         return array();  // no values found
230     }
231
232     function _savePageLinks($pagename, $links) {
233         $this->_savePage('links', $pagename, 0, $links);
234     }
235
236
237
238     /**
239      * Get page meta-data from database.
240      *
241      * @param $pagename string Page name.
242      * @return hash
243      * Returns a hash containing the page meta-data.
244      * Returns an empty array if there is no meta-data for the requested page.
245      * Keys which might be present in the hash are:
246      * <dl>
247      *  <dt> locked  <dd> If the page is locked.
248      *  <dt> hits    <dd> The page hit count.
249      *  <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
250      *                    don't think we need this...) 
251      * </dl>
252      */
253     function get_pagedata($pagename) {
254         return $this->_loadPageData($pagename);
255     }
256
257     /**
258      * Update the page meta-data.
259      *
260      * Set page meta-data.
261      *
262      * Only meta-data whose keys are preset in $newdata is affected.
263      *
264      * For example:
265      * <pre>
266      *   $backend->update_pagedata($pagename, array('locked' => 1)); 
267      * </pre>
268      * will set the value of 'locked' to 1 for the specified page, but it
269      * will not affect the value of 'hits' (or whatever other meta-data
270      * may have been stored for the page.)
271      *
272      * To delete a particular piece of meta-data, set it's value to false.
273      * <pre>
274      *   $backend->update_pagedata($pagename, array('locked' => false)); 
275      * </pre>
276      *
277      * @param $pagename string Page name.
278      * @param $newdata hash New meta-data.
279      */
280     /**
281      * This will create a new page if page being requested does not
282      * exist.
283      */
284     function update_pagedata($pagename, $newdata) {
285         $data = $this->get_pagedata($pagename);
286         if (count($data) == 0) {
287             $this->_savePageData($pagename, $newdata);  // create a new pagedata-file
288             return;
289         }
290         
291         foreach ($newdata as $key => $val) {
292             if (empty($val))
293                 unset($data[$key]);
294             else
295                 $data[$key] = $val;
296         }
297         $this->_savePageData($pagename, $data);  // write new pagedata-file
298     }
299     
300
301     /**
302      * Get the current version number for a page.
303      *
304      * @param $pagename string Page name.
305      * @return int The latest version number for the page.  Returns zero if
306      *  no versions of a page exist.
307      */
308     function get_latest_version($pagename) {
309         return $this->_getLatestVersion($pagename);
310     }
311     
312     /**
313      * Get preceding version number.
314      *
315      * @param $pagename string Page name.
316      * @param $version int Find version before this one.
317      * @return int The version number of the version in the database which
318      *  immediately preceeds $version.
319      */
320     function get_previous_version($pagename, $version) {
321         return ($version > 0 ? $version - 1 : 0);
322     }
323     
324     /**
325      * Get revision meta-data and content.
326      *
327      * @param $pagename string Page name.
328      * @param $version integer Which version to get.
329      * @param $want_content boolean
330      *  Indicates the caller really wants the page content.  If this
331      *  flag is not set, the backend is free to skip fetching of the
332      *  page content (as that may be expensive).  If the backend omits
333      *  the content, the backend might still want to set the value of
334      *  '%content' to the empty string if it knows there's no content.
335      *
336      * @return hash The version data, or false if specified version does not
337      *    exist.
338      *
339      * Some keys which might be present in the $versiondata hash are:
340      * <dl>
341      * <dt> %content
342      *  <dd> This is a pseudo-meta-data element (since it's actually
343      *       the page data, get it?) containing the page content.
344      *       If the content was not fetched, this key may not be present.
345      * </dl>
346      * For description of other version meta-data see WikiDB_PageRevision::get().
347      * @see WikiDB_PageRevision::get
348      */
349     function get_versiondata($pagename, $version, $want_content = false) {
350         $vd = $this->_loadVersionData($pagename, $version);
351         if ($vd == NULL)
352             return false;
353         return $vd;
354     }
355
356     /**
357      * Delete page from the database.
358      *
359      * Delete page (and all it's revisions) from the database.
360      *
361      * @param $pagename string Page name.
362      */
363     function delete_page($pagename) {
364         $ver = $this->get_latest_version($pagename);
365         while($ver > 0) {
366             $this->_removePage('ver_data', $pagename, $ver);
367             $ver = $this->get_previous_version($pagename, $ver);
368         }
369                 $this->_removePage('page_data', $pagename, 0);
370                 $this->_removePage('links', $pagename, 0);
371                 // remove page from latest_version...
372                 $this->_setLatestVersion($pagename, 0);
373     }
374             
375     /**
376      * Delete an old revision of a page.
377      *
378      * Note that one is never allowed to delete the most recent version,
379      * but that this requirement is enforced by WikiDB not by the backend.
380      *
381      * In fact, to be safe, backends should probably allow the deletion of
382      * the most recent version.
383      *
384      * @param $pagename string Page name.
385      * @param $version integer Version to delete.
386      */
387     function delete_versiondata($pagename, $version) {
388         if ($this->get_latest_version($pagename) == $version) {
389             // try to delete the latest version!
390             // so check if an older version exist:
391             if ($this->get_versiondata($pagename, $this->get_previous_version($pagename, $version), false) == false) {
392               // there is no older version....
393               // so the completely page will be removed:
394               $this->delete_page($pagename);
395               return;
396             }
397         }
398         $this->_removePage('ver_data', $pagename, $version);
399     }                           
400
401     /**
402      * Create a new page revision.
403      *
404      * If the given ($pagename,$version) is already in the database,
405      * this method completely overwrites any stored data for that version.
406      *
407      * @param $pagename string Page name.
408      * @param $version int New revisions content.
409      * @param $data hash New revision metadata.
410      *
411      * @see get_versiondata
412      */
413     function set_versiondata($pagename, $version, $data) {
414         $this->_saveVersionData($pagename, $version, $data);
415     }
416
417     /**
418      * Update page version meta-data.
419      *
420      * If the given ($pagename,$version) is already in the database,
421      * this method only changes those meta-data values whose keys are
422      * explicity listed in $newdata.
423      *
424      * @param $pagename string Page name.
425      * @param $version int New revisions content.
426      * @param $newdata hash New revision metadata.
427      * @see set_versiondata, get_versiondata
428      */
429     function update_versiondata($pagename, $version, $newdata) {
430         $data = $this->get_versiondata($pagename, $version, true);
431         if (!$data) {
432             assert($data);
433             return;
434         }
435         foreach ($newdata as $key => $val) {
436             if (empty($val))
437                 unset($data[$key]);
438             else
439                 $data[$key] = $val;
440         }
441         $this->set_versiondata($pagename, $version, $data);
442     }
443     
444     /**
445      * Set links for page.
446      *
447      * @param $pagename string Page name.
448      *
449      * @param $links array List of page(names) which page links to.
450      */
451     function set_links($pagename, $links) {
452         $this->_savePageLinks($pagename, $links);
453     }
454         
455     /**
456      * Find pages which link to or are linked from a page.
457      *
458      * @param $pagename string Page name.
459      * @param $reversed boolean True to get backlinks.
460      *
461      * FIXME: array or iterator?
462      * @return object A WikiDB_backend_iterator.
463      */
464     function get_links($pagename, $reversed) {
465         if ($reversed == false)
466             return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
467
468         $this->_loadLatestVersions();
469         $pagenames = $this->_latest_versions;  // now we have an array with the key is the pagename of all pages
470
471         $out = array();  // create empty out array
472
473         foreach ($pagenames as $key => $val) {
474             $links = $this->_loadPageLinks($key);
475             foreach ($links as $key2 => $val2) {
476                 if ($val2 == $pagename)
477                     array_push($out, $key);
478             }
479         }
480         return new WikiDB_backend_file_iter($this, $out);
481     }
482
483     /**
484      * Get all revisions of a page.
485      *
486      * @param $pagename string The page name.
487      * @return object A WikiDB_backend_iterator.
488      */
489     function get_all_revisions($pagename) {
490         include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
491         return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
492     }
493     
494     /**
495      * Get all pages in the database.
496      *
497      * Pages should be returned in alphabetical order if that is
498      * feasable.
499      *
500      * @access protected
501      *
502      * @param $include_defaulted boolean
503      * If set, even pages with no content will be returned
504      * --- but still only if they have at least one revision (not
505      * counting the default revision 0) entered in the database.
506      *
507      * Normally pages whose current revision has empty content
508      * are not returned as these pages are considered to be
509      * non-existing.
510      *
511      * @return object A WikiDB_backend_iterator.
512      */
513     function get_all_pages($include_deleted) {
514         $this->_loadLatestVersions();
515         $a = array_keys($this->_latest_versions);
516
517         return new WikiDB_backend_file_iter($this, $a);
518     }
519         
520     /**
521      * Title or full text search.
522      *
523      * Pages should be returned in alphabetical order if that is
524      * feasable.
525      *
526      * @access protected
527      *
528      * @param $search object A TextSearchQuery object describing what pages
529      * are to be searched for.
530      *
531      * @param $fullsearch boolean If true, a full text search is performed,
532      *  otherwise a title search is performed.
533      *
534      * @return object A WikiDB_backend_iterator.
535      *
536      * @see WikiDB::titleSearch
537      */
538     function text_search($search = '', $fullsearch = false) {
539         // This is method implements a simple linear search
540         // through all the pages in the database.
541         //
542         // It is expected that most backends will overload
543         // method with something more efficient.
544         include_once('lib/WikiDB/backend/dumb/TextSearchIter.php');
545         $pages = $this->get_all_pages(false);
546         return new WikiDB_backend_dumb_TextSearchIter($this, $pages, $search, $fullsearch);
547     }
548
549     /**
550      * Find pages with highest hit counts.
551      *
552      * Find the pages with the highest hit counts.  The pages should
553      * be returned in reverse order by hit count.
554      *
555      * @access protected
556      * @param $limit integer  No more than this many pages
557      * @return object A WikiDB_backend_iterator.
558      */
559     function most_popular($limit) {
560         // This is method fetches all pages, then
561         // sorts them by hit count.
562         // (Not very efficient.)
563         //
564         // It is expected that most backends will overload
565         // method with something more efficient.
566         include_once('lib/WikiDB/backend/dumb/MostPopularIter.php');
567         $pages = $this->get_all_pages(false);
568         
569         return new WikiDB_backend_dumb_MostPopularIter($this, $pages, $limit);
570     }
571
572     /**
573      * Find recent changes.
574      *
575      * @access protected
576      * @param $params hash See WikiDB::mostRecent for a description
577      *  of parameters which can be included in this hash.
578      * @return object A WikiDB_backend_iterator.
579      * @see WikiDB::mostRecent
580      */
581     function most_recent($params) {
582         // This method is very inefficient and searches through
583         // all pages for the most recent changes.
584         //
585         // It is expected that most backends will overload
586         // method with something more efficient.
587         include_once('lib/WikiDB/backend/dumb/MostRecentIter.php');
588         $pages = $this->get_all_pages(true);
589         return new WikiDB_backend_dumb_MostRecentIter($this, $pages, $params);
590     }
591
592     /**
593      * Lock backend database.
594      *
595      * Calls may be nested.
596      *
597      * @param $write_lock boolean Unless this is set to false, a write lock
598      *     is acquired, otherwise a read lock.  If the backend doesn't support
599      *     read locking, then it should make a write lock no matter which type
600      *     of lock was requested.
601      *
602      *     All backends <em>should</em> support write locking.
603      */
604     function lock($write_lock = true) {
605         //trigger_error("lock: Not Implemented", E_USER_WARNING);
606     }
607
608     /**
609      * Unlock backend database.
610      *
611      * @param $force boolean Normally, the database is not unlocked until
612      *  unlock() is called as many times as lock() has been.  If $force is
613      *  set to true, the the database is unconditionally unlocked.
614      */
615     function unlock($force = false) {
616         //trigger_error("unlock: Not Implemented", E_USER_WARNING);
617     }
618
619
620     /**
621      * Close database.
622      */
623     function close () {
624         //trigger_error("close: Not Implemented", E_USER_WARNING);
625     }
626
627     /**
628      * Synchronize with filesystem.
629      *
630      * This should flush all unwritten data to the filesystem.
631      */
632     function sync() {
633         //trigger_error("sync: Not Implemented", E_USER_WARNING);
634     }
635
636     /**
637      * Optimize the database.
638      */
639     function optimize() {
640         //trigger_error("optimize: Not Implemented", E_USER_WARNING);
641     }
642
643     /**
644      * Check database integrity.
645      *
646      * This should check the validity of the internal structure of the database.
647      * Errors should be reported via:
648      * <pre>
649      *   trigger_error("Message goes here.", E_USER_WARNING);
650      * </pre>
651      *
652      * @return boolean True iff database is in a consistent state.
653      */
654     function check() {
655         //trigger_error("check: Not Implemented", E_USER_WARNING);
656     }
657
658     /**
659      * Put the database into a consistent state.
660      *
661      * This should put the database into a consistent state.
662      * (I.e. rebuild indexes, etc...)
663      *
664      * @return boolean True iff successful.
665      */
666     function rebuild() {
667         //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
668     }
669
670     function _parse_searchwords($search) {
671         $search = strtolower(trim($search));
672         if (!$search)
673             return array(array(),array());
674         
675         $words = preg_split('/\s+/', $search);
676         $exclude = array();
677         foreach ($words as $key => $word) {
678             if ($word[0] == '-' && $word != '-') {
679                 $word = substr($word, 1);
680                 $exclude[] = preg_quote($word);
681                 unset($words[$key]);
682             }
683         }
684         return array($words, $exclude);
685     }
686        
687 };
688
689 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
690 {
691     function WikiDB_backend_file_iter(&$backend, &$query_result) {
692         $this->_backend = &$backend;
693         $this->_result = $query_result;
694
695         if (count($this->_result) > 0)
696             reset($this->_result);
697     }
698     
699     function next() {
700         $backend = &$this->_backend;
701
702         if (!$this->_result)
703             return false;
704
705         if (count($this->_result) <= 0)
706             return false;
707
708         $e = each($this->_result);
709         if ($e == false)
710             return false;
711
712         $pn = urldecode($e[1]);
713
714         $pagedata = $backend->get_pagedata($pn);
715         $rec = array('pagename' => $pn,
716                      'pagedata' => $pagedata);
717
718         //$rec['version'] = $backend->get_latest_version($pn);
719         //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
720
721         return $rec;
722     }
723
724     function free () {
725     }
726 }
727
728 // $Log: not supported by cvs2svn $
729
730 // For emacs users
731 // Local Variables:
732 // mode: php
733 // tab-width: 8
734 // c-basic-offset: 4
735 // c-hanging-comment-ender-p: nil
736 // indent-tabs-mode: nil
737 // End:
738
739 ?>