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