]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/file.php
include [all] Include and file path should be devided with single space. File path...
[SourceForge/phpwiki.git] / lib / WikiDB / backend / file.php
1 <?php // -*-php-*-
2
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 along
20  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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     // common file load / save functions:
87     function _pagename2filename($type, $pagename, $version) {
88          if ($version == 0)
89              return $this->_dir_names[$type].'/'.urlencode($pagename);
90          else
91              return $this->_dir_names[$type].'/'.urlencode($pagename).'--'.$version;
92     }
93
94     function _loadPage($type, $pagename, $version, $set_pagename = true) {
95       $filename = $this->_pagename2filename($type, $pagename, $version);
96       if (!file_exists($filename)) return NULL;
97       if (!filesize($filename)) return array();
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            rewind($fd);
129            ftruncate($fd, 0);
130            $pagedata = serialize($data);
131            $len = strlen($pagedata);
132            $num = fwrite($fd, $pagedata, $len);
133            assert($num == $len);
134            fclose($fd);
135         } else {
136            ExitWiki("Error while writing page '$pagename'");
137         }
138     }
139
140     function _removePage($type, $pagename, $version) {
141         $filename = $this->_pagename2filename($type, $pagename, $version);
142         if (!file_exists($filename)) return NULL;
143         $f = @unlink($filename);
144         if ($f == false)
145             trigger_error("delete file failed: ".$filename." ver: ".$version, E_USER_WARNING);
146     }
147
148     // *********************************************************************
149
150     // *********************************************************************
151     // Load/Save Version-Data
152     function _loadVersionData($pagename, $version) {
153         if ($this->_page_version_data != NULL) {
154             if ( ($this->_page_version_data['pagename'] == $pagename) &&
155                 ($this->_page_version_data['version'] == $version) ) {
156                 return $this->_page_version_data;
157              }
158         }
159         $vd = $this->_loadPage('ver_data', $pagename, $version);
160         if ($vd != NULL) {
161             $this->_page_version_data = $vd;
162             if ( ($this->_page_version_data['pagename'] == $pagename) &&
163                 ($this->_page_version_data['version'] == $version) ) {
164                 return $this->_page_version_data;
165              }
166         }
167         return NULL;
168     }
169
170     function _saveVersionData($pagename, $version, $data) {
171         $this->_savePage('ver_data', $pagename, $version, $data);
172
173         // check if this is a newer version:
174         if ($this->_getLatestVersion($pagename) < $version) {
175             // write new latest-version-info
176             $this->_setLatestVersion($pagename, $version);
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     // Load/Save Page-Links
246     function _loadPageLinks($pagename) {
247         $pd = $this->_loadPage('links', $pagename, 0, false);
248         if ($pd != NULL)
249             return $pd;;
250         return array();  // no values found
251     }
252
253     function _savePageLinks($pagename, $links) {
254         $this->_savePage('links', $pagename, 0, $links);
255     }
256
257     /**
258      * Get page meta-data from database.
259      *
260      * @param $pagename string Page name.
261      * @return hash
262      * Returns a hash containing the page meta-data.
263      * Returns an empty array if there is no meta-data for the requested page.
264      * Keys which might be present in the hash are:
265      * <dl>
266      *  <dt> locked  <dd> If the page is locked.
267      *  <dt> hits    <dd> The page hit count.
268      *  <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
269      *                    don't think we need this...)
270      * </dl>
271      */
272     function get_pagedata($pagename) {
273         return $this->_loadPageData($pagename);
274     }
275
276     /**
277      * Update the page meta-data.
278      *
279      * Set page meta-data.
280      *
281      * Only meta-data whose keys are preset in $newdata is affected.
282      *
283      * For example:
284      * <pre>
285      *   $backend->update_pagedata($pagename, array('locked' => 1));
286      * </pre>
287      * will set the value of 'locked' to 1 for the specified page, but it
288      * will not affect the value of 'hits' (or whatever other meta-data
289      * may have been stored for the page.)
290      *
291      * To delete a particular piece of meta-data, set it's value to false.
292      * <pre>
293      *   $backend->update_pagedata($pagename, array('locked' => false));
294      * </pre>
295      *
296      * @param $pagename string Page name.
297      * @param $newdata hash New meta-data.
298      */
299     /**
300      * This will create a new page if page being requested does not
301      * exist.
302      */
303     function update_pagedata($pagename, $newdata) {
304         $data = $this->get_pagedata($pagename);
305         if (count($data) == 0) {
306             $this->_savePageData($pagename, $newdata);  // create a new pagedata-file
307             return;
308         }
309
310         foreach ($newdata as $key => $val) {
311             if (empty($val))
312                 unset($data[$key]);
313             else
314                 $data[$key] = $val;
315         }
316         $this->_savePageData($pagename, $data);  // write new pagedata-file
317     }
318
319     /**
320      * Get the current version number for a page.
321      *
322      * @param $pagename string Page name.
323      * @return int The latest version number for the page.  Returns zero if
324      *  no versions of a page exist.
325      */
326     function get_latest_version($pagename) {
327         return $this->_getLatestVersion($pagename);
328     }
329
330     /**
331      * Get preceding version number.
332      *
333      * @param $pagename string Page name.
334      * @param $version int Find version before this one.
335      * @return int The version number of the version in the database which
336      *  immediately preceeds $version.
337      *
338      * FIXED: Check if this version really exists!
339      */
340     function get_previous_version($pagename, $version) {
341         $prev = ($version > 0 ? $version - 1 : 0);
342         while ($prev and !file_exists($this->_pagename2filename('ver_data', $pagename, $prev))) {
343             $prev--;
344         }
345         return $prev;
346     }
347
348     /**
349      * Get revision meta-data and content.
350      *
351      * @param $pagename string Page name.
352      * @param $version integer Which version to get.
353      * @param $want_content boolean
354      *  Indicates the caller really wants the page content.  If this
355      *  flag is not set, the backend is free to skip fetching of the
356      *  page content (as that may be expensive).  If the backend omits
357      *  the content, the backend might still want to set the value of
358      *  '%content' to the empty string if it knows there's no content.
359      *
360      * @return hash The version data, or false if specified version does not
361      *    exist.
362      *
363      * Some keys which might be present in the $versiondata hash are:
364      * <dl>
365      * <dt> %content
366      *  <dd> This is a pseudo-meta-data element (since it's actually
367      *       the page data, get it?) containing the page content.
368      *       If the content was not fetched, this key may not be present.
369      * </dl>
370      * For description of other version meta-data see WikiDB_PageRevision::get().
371      * @see WikiDB_PageRevision::get
372      */
373     function get_versiondata($pagename, $version, $want_content = false) {
374     $vd = $this->_loadVersionData($pagename, $version);
375         if ($vd == NULL)
376             return false;
377         return $vd;
378     }
379
380     /**
381      * Rename all files for this page
382      *
383      * @access protected   Via WikiDB
384      */
385     function rename_page($pagename, $to) {
386         $version = _getLatestVersion($pagename);
387         foreach ($this->_dir_names as $type => $path) {
388             if (is_dir($path)) {
389                 $filename = $this->_pagename2filename($type, $pagename, $version);
390                 $new = $this->_pagename2filename($type, $to, $version);
391                 @rename($filename,$new);
392             }
393         }
394         $this->update_pagedata($pagename, array('pagename' => $to));
395         return true;
396     }
397
398     /**
399      * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
400      */
401     function delete_page($pagename) {
402         $this->purge_page($pagename);
403     }
404
405     /**
406      * Delete page from the database.
407      *
408      * Delete page (and all it's revisions) from the database.
409      *
410      * @param $pagename string Page name.
411      */
412     function purge_page($pagename) {
413         $ver = $this->get_latest_version($pagename);
414         while ($ver > 0) {
415             $this->_removePage('ver_data', $pagename, $ver);
416             $ver = $this->get_previous_version($pagename, $ver);
417         }
418         $this->_removePage('page_data', $pagename, 0);
419         $this->_removePage('links', $pagename, 0);
420         // remove page from latest_version...
421         $this->_setLatestVersion($pagename, 0);
422     }
423
424     /**
425      * Delete an old revision of a page.
426      *
427      * Note that one is never allowed to delete the most recent version,
428      * but that this requirement is enforced by WikiDB not by the backend.
429      *
430      * In fact, to be safe, backends should probably allow the deletion of
431      * the most recent version.
432      *
433      * @param $pagename string Page name.
434      * @param $version integer Version to delete.
435      */
436     function delete_versiondata($pagename, $version) {
437         if ($this->get_latest_version($pagename) == $version) {
438             // try to delete the latest version!
439             // so check if an older version exist:
440             if ($this->get_versiondata($pagename,
441                                        $this->get_previous_version($pagename, $version),
442                                        false) == false) {
443               // there is no older version....
444               // so the completely page will be removed:
445               $this->delete_page($pagename);
446               return;
447             }
448         }
449         $this->_removePage('ver_data', $pagename, $version);
450     }
451
452     /**
453      * Create a new page revision.
454      *
455      * If the given ($pagename,$version) is already in the database,
456      * this method completely overwrites any stored data for that version.
457      *
458      * @param $pagename string Page name.
459      * @param $version int New revisions content.
460      * @param $data hash New revision metadata.
461      *
462      * @see get_versiondata
463      */
464     function set_versiondata($pagename, $version, $data) {
465         $this->_saveVersionData($pagename, $version, $data);
466     }
467
468     /**
469      * Update page version meta-data.
470      *
471      * If the given ($pagename,$version) is already in the database,
472      * this method only changes those meta-data values whose keys are
473      * explicity listed in $newdata.
474      *
475      * @param $pagename string Page name.
476      * @param $version int New revisions content.
477      * @param $newdata hash New revision metadata.
478      * @see set_versiondata, get_versiondata
479      */
480     function update_versiondata($pagename, $version, $newdata) {
481         $data = $this->get_versiondata($pagename, $version, true);
482         if (!$data) {
483             assert($data);
484             return;
485         }
486         foreach ($newdata as $key => $val) {
487             if (empty($val))
488                 unset($data[$key]);
489             else
490                 $data[$key] = $val;
491         }
492         $this->set_versiondata($pagename, $version, $data);
493     }
494
495     /**
496      * Set links for page.
497      *
498      * @param $pagename string Page name.
499      *
500      * @param $links array List of page(names) which page links to.
501      */
502     function set_links($pagename, $links) {
503         $this->_savePageLinks($pagename, $links);
504     }
505
506     /**
507      * Find pages which link to or are linked from a page.
508      *
509      * @param $pagename string Page name.
510      * @param $reversed boolean True to get backlinks.
511      *
512      * FIXME: array or iterator?
513      * @return object A WikiDB_backend_iterator.
514      */
515     function get_links($pagename, $reversed=true, $include_empty=false,
516                        $sortby='', $limit='', $exclude='',
517                        $want_relations=false)
518     {
519         if ($reversed == false)
520             return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
521
522         $this->_loadLatestVersions();
523         $pagenames = $this->_latest_versions;  // now we have an array with the key is the pagename of all pages
524
525         $out = array();  // create empty out array
526
527         foreach ($pagenames as $key => $val) {
528             $links = $this->_loadPageLinks($key);
529         foreach ($links as $key2 => $val2) {
530                 if ($val2['linkto'] == $pagename)
531                     array_push($out, $key);
532             }
533         }
534         return new WikiDB_backend_file_iter($this, $out);
535     }
536
537     /**
538      * Get all revisions of a page.
539      *
540      * @param $pagename string The page name.
541      * @return object A WikiDB_backend_iterator.
542      */
543     /*
544     function get_all_revisions($pagename) {
545         include_once 'lib/WikiDB/backend/dumb/AllRevisionsIter.php';
546         return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
547     }
548     */
549
550     /**
551      * Get all pages in the database.
552      *
553      * Pages should be returned in alphabetical order if that is
554      * feasable.
555      *
556      * @access protected
557      *
558      * @param $include_defaulted boolean
559      * If set, even pages with no content will be returned
560      * --- but still only if they have at least one revision (not
561      * counting the default revision 0) entered in the database.
562      *
563      * Normally pages whose current revision has empty content
564      * are not returned as these pages are considered to be
565      * non-existing.
566      *
567      * @return object A WikiDB_backend_iterator.
568      */
569     function get_all_pages($include_empty=false, $sortby='', $limit='', $exclude='') {
570         require_once 'lib/PageList.php';
571         $this->_loadLatestVersions();
572         $a = array_keys($this->_latest_versions);
573         if (empty($a))
574             return new WikiDB_backend_file_iter($this, $a);
575         $sortby = $this->sortby($sortby, 'db', $this->sortable_columns());
576         switch ($sortby) {
577         case '': break;
578         case 'pagename ASC':  sort($a); break;
579         case 'pagename DESC': rsort($a); break;
580         }
581         return new WikiDB_backend_file_iter($this, $a);
582     }
583
584     function sortable_columns() {
585         return array('pagename');
586     }
587
588     function numPages($filter=false, $exclude='') {
589         $this->_loadLatestVersions();
590         return count($this->_latest_versions);
591     }
592
593     /**
594      * Lock backend database.
595      *
596      * Calls may be nested.
597      *
598      * @param $write_lock boolean Unless this is set to false, a write lock
599      *     is acquired, otherwise a read lock.  If the backend doesn't support
600      *     read locking, then it should make a write lock no matter which type
601      *     of lock was requested.
602      *
603      *     All backends <em>should</em> support write locking.
604      */
605     function lock($write_lock = true) {
606         //trigger_error("lock: Not Implemented", E_USER_WARNING);
607     }
608
609     /**
610      * Unlock backend database.
611      *
612      * @param $force boolean Normally, the database is not unlocked until
613      *  unlock() is called as many times as lock() has been.  If $force is
614      *  set to true, the the database is unconditionally unlocked.
615      */
616     function unlock($force = false) {
617         //trigger_error("unlock: Not Implemented", E_USER_WARNING);
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         return 0;//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, $options=array()) {
692         $this->_backend = &$backend;
693         $this->_result = $query_result;
694         $this->_options = $options;
695
696         if (count($this->_result) > 0)
697             reset($this->_result);
698     }
699
700     function next() {
701         if (!$this->_result)
702             return false;
703         if (count($this->_result) <= 0)
704             return false;
705
706         $e = each($this->_result);
707         if ($e == false) {
708             return false;
709         }
710
711         $pn = $e[1];
712         if (is_array($pn) and isset($pn['linkto'])) { // support relation link iterator
713             $pn = $pn['linkto'];
714         }
715         $pagedata = $this->_backend->get_pagedata($pn);
716         // don't pass _cached_html via iterators
717         if (isset($pagedata['_cached_html']))
718             unset($pagedata['_cached_html']);
719         unset($pagedata['pagename']);
720         $rec = array('pagename' => $pn,
721                      'pagedata' => $pagedata);
722         if (is_array($e[1])) {
723             $rec['linkrelation'] = $e[1]['relation'];
724         }
725         //$rec['version'] = $backend->get_latest_version($pn);
726         //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
727         return $rec;
728     }
729     function asArray() {
730         reset($this->_result);
731         return $this->_result;
732     }
733     function count() {
734         return count($this->_result);
735     }
736     function free () {
737         $this->_result = array();
738     }
739 }
740
741 // Local Variables:
742 // mode: php
743 // tab-width: 8
744 // c-basic-offset: 4
745 // c-hanging-comment-ender-p: nil
746 // indent-tabs-mode: nil
747 // End: