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