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