]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/file.php
var --> public
[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 WikiDB_backend_file($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      * @access protected   Via WikiDB
404      */
405     function rename_page($pagename, $to)
406     {
407         $version = _getLatestVersion($pagename);
408         foreach ($this->_dir_names as $type => $path) {
409             if (is_dir($path)) {
410                 $filename = $this->_pagename2filename($type, $pagename, $version);
411                 $new = $this->_pagename2filename($type, $to, $version);
412                 @rename($filename, $new);
413             }
414         }
415         $this->update_pagedata($pagename, array('pagename' => $to));
416         return true;
417     }
418
419     /**
420      * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
421      */
422     function delete_page($pagename)
423     {
424         $this->purge_page($pagename);
425     }
426
427     /**
428      * Delete page from the database.
429      *
430      * Delete page (and all it's revisions) from the database.
431      *
432      * @param $pagename string Page name.
433      */
434     function purge_page($pagename)
435     {
436         $ver = $this->get_latest_version($pagename);
437         while ($ver > 0) {
438             $this->_removePage('ver_data', $pagename, $ver);
439             $ver = $this->get_previous_version($pagename, $ver);
440         }
441         $this->_removePage('page_data', $pagename, 0);
442         $this->_removePage('links', $pagename, 0);
443         // remove page from latest_version...
444         $this->_setLatestVersion($pagename, 0);
445     }
446
447     /**
448      * Delete an old revision of a page.
449      *
450      * Note that one is never allowed to delete the most recent version,
451      * but that this requirement is enforced by WikiDB not by the backend.
452      *
453      * In fact, to be safe, backends should probably allow the deletion of
454      * the most recent version.
455      *
456      * @param $pagename string Page name.
457      * @param $version integer Version to delete.
458      */
459     function delete_versiondata($pagename, $version)
460     {
461         if ($this->get_latest_version($pagename) == $version) {
462             // try to delete the latest version!
463             // so check if an older version exist:
464             if ($this->get_versiondata($pagename,
465                 $this->get_previous_version($pagename, $version),
466                 false) == false
467             ) {
468                 // there is no older version....
469                 // so the completely page will be removed:
470                 $this->delete_page($pagename);
471                 return;
472             }
473         }
474         $this->_removePage('ver_data', $pagename, $version);
475     }
476
477     /**
478      * Create a new page revision.
479      *
480      * If the given ($pagename,$version) is already in the database,
481      * this method completely overwrites any stored data for that version.
482      *
483      * @param $pagename string Page name.
484      * @param $version int New revisions content.
485      * @param $data hash New revision metadata.
486      *
487      * @see get_versiondata
488      */
489     function set_versiondata($pagename, $version, $data)
490     {
491         $this->_saveVersionData($pagename, $version, $data);
492     }
493
494     /**
495      * Update page version meta-data.
496      *
497      * If the given ($pagename,$version) is already in the database,
498      * this method only changes those meta-data values whose keys are
499      * explicity listed in $newdata.
500      *
501      * @param $pagename string Page name.
502      * @param $version int New revisions content.
503      * @param $newdata hash New revision metadata.
504      * @see set_versiondata, get_versiondata
505      */
506     function update_versiondata($pagename, $version, $newdata)
507     {
508         $data = $this->get_versiondata($pagename, $version, true);
509         if (!$data) {
510             assert($data);
511             return;
512         }
513         foreach ($newdata as $key => $val) {
514             if (empty($val))
515                 unset($data[$key]);
516             else
517                 $data[$key] = $val;
518         }
519         $this->set_versiondata($pagename, $version, $data);
520     }
521
522     /**
523      * Set links for page.
524      *
525      * @param $pagename string Page name.
526      *
527      * @param $links array List of page(names) which page links to.
528      */
529     function set_links($pagename, $links)
530     {
531         $this->_savePageLinks($pagename, $links);
532     }
533
534     /**
535      * Find pages which link to or are linked from a page.
536      *
537      * @param $pagename string Page name.
538      * @param $reversed boolean True to get backlinks.
539      *
540      * FIXME: array or iterator?
541      * @return object A WikiDB_backend_iterator.
542      */
543     function get_links($pagename, $reversed = true, $include_empty = false,
544                        $sortby = '', $limit = '', $exclude = '',
545                        $want_relations = false)
546     {
547         if ($reversed == false)
548             return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
549
550         $this->_loadLatestVersions();
551         $pagenames = $this->_latest_versions; // now we have an array with the key is the pagename of all pages
552
553         $out = array(); // create empty out array
554
555         foreach ($pagenames as $key => $val) {
556             $links = $this->_loadPageLinks($key);
557             foreach ($links as $key2 => $val2) {
558                 if ($val2['linkto'] == $pagename)
559                     array_push($out, $key);
560             }
561         }
562         return new WikiDB_backend_file_iter($this, $out);
563     }
564
565     /**
566      * Get all revisions of a page.
567      *
568      * @param $pagename string The page name.
569      * @return object A WikiDB_backend_iterator.
570      */
571     /*
572     function get_all_revisions($pagename) {
573         include_once 'lib/WikiDB/backend/dumb/AllRevisionsIter.php';
574         return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
575     }
576     */
577
578     /**
579      * Get all pages in the database.
580      *
581      * Pages should be returned in alphabetical order if that is
582      * feasable.
583      *
584      * @access protected
585      *
586      * @param $include_defaulted boolean
587      * If set, even pages with no content will be returned
588      * --- but still only if they have at least one revision (not
589      * counting the default revision 0) entered in the database.
590      *
591      * Normally pages whose current revision has empty content
592      * are not returned as these pages are considered to be
593      * non-existing.
594      *
595      * @return object A WikiDB_backend_iterator.
596      */
597     function get_all_pages($include_empty = false, $sortby = '', $limit = '', $exclude = '')
598     {
599         require_once 'lib/PageList.php';
600         $this->_loadLatestVersions();
601         $a = array_keys($this->_latest_versions);
602         if (empty($a))
603             return new WikiDB_backend_file_iter($this, $a);
604         $sortby = $this->sortby($sortby, 'db', $this->sortable_columns());
605         switch ($sortby) {
606             case '':
607                 break;
608             case 'pagename ASC':
609                 sort($a);
610                 break;
611             case 'pagename DESC':
612                 rsort($a);
613                 break;
614         }
615         return new WikiDB_backend_file_iter($this, $a);
616     }
617
618     function sortable_columns()
619     {
620         return array('pagename');
621     }
622
623     function numPages($filter = false, $exclude = '')
624     {
625         $this->_loadLatestVersions();
626         return count($this->_latest_versions);
627     }
628
629     /**
630      * Lock backend database.
631      *
632      * Calls may be nested.
633      *
634      * @param $write_lock boolean Unless this is set to false, a write lock
635      *     is acquired, otherwise a read lock.  If the backend doesn't support
636      *     read locking, then it should make a write lock no matter which type
637      *     of lock was requested.
638      *
639      *     All backends <em>should</em> support write locking.
640      */
641     function lock($write_lock = true)
642     {
643         //trigger_error("lock: Not Implemented", E_USER_WARNING);
644     }
645
646     /**
647      * Unlock backend database.
648      *
649      * @param $force boolean Normally, the database is not unlocked until
650      *  unlock() is called as many times as lock() has been.  If $force is
651      *  set to true, the the database is unconditionally unlocked.
652      */
653     function unlock($force = false)
654     {
655         //trigger_error("unlock: Not Implemented", E_USER_WARNING);
656     }
657
658     /**
659      * Close database.
660      */
661     function close()
662     {
663         //trigger_error("close: Not Implemented", E_USER_WARNING);
664     }
665
666     /**
667      * Synchronize with filesystem.
668      *
669      * This should flush all unwritten data to the filesystem.
670      */
671     function sync()
672     {
673         //trigger_error("sync: Not Implemented", E_USER_WARNING);
674     }
675
676     /**
677      * Optimize the database.
678      */
679     function optimize()
680     {
681         return 0; //trigger_error("optimize: Not Implemented", E_USER_WARNING);
682     }
683
684     /**
685      * Check database integrity.
686      *
687      * This should check the validity of the internal structure of the database.
688      * Errors should be reported via:
689      * <pre>
690      *   trigger_error("Message goes here.", E_USER_WARNING);
691      * </pre>
692      *
693      * @return boolean True iff database is in a consistent state.
694      */
695     function check()
696     {
697         //trigger_error("check: Not Implemented", E_USER_WARNING);
698     }
699
700     /**
701      * Put the database into a consistent state.
702      *
703      * This should put the database into a consistent state.
704      * (I.e. rebuild indexes, etc...)
705      *
706      * @return boolean True iff successful.
707      */
708     function rebuild()
709     {
710         //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
711     }
712
713     function _parse_searchwords($search)
714     {
715         $search = strtolower(trim($search));
716         if (!$search)
717             return array(array(), array());
718
719         $words = preg_split('/\s+/', $search);
720         $exclude = array();
721         foreach ($words as $key => $word) {
722             if ($word[0] == '-' && $word != '-') {
723                 $word = substr($word, 1);
724                 $exclude[] = preg_quote($word);
725                 unset($words[$key]);
726             }
727         }
728         return array($words, $exclude);
729     }
730
731 }
732
733 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
734 {
735     function WikiDB_backend_file_iter(&$backend, &$query_result, $options = array())
736     {
737         $this->_backend = &$backend;
738         $this->_result = $query_result;
739         $this->_options = $options;
740
741         if (count($this->_result) > 0)
742             reset($this->_result);
743     }
744
745     function next()
746     {
747         if (!$this->_result)
748             return false;
749         if (count($this->_result) <= 0)
750             return false;
751
752         $e = each($this->_result);
753         if ($e == false) {
754             return false;
755         }
756
757         $pn = $e[1];
758         if (is_array($pn) and isset($pn['linkto'])) { // support relation link iterator
759             $pn = $pn['linkto'];
760         }
761         $pagedata = $this->_backend->get_pagedata($pn);
762         // don't pass _cached_html via iterators
763         if (isset($pagedata['_cached_html']))
764             unset($pagedata['_cached_html']);
765         unset($pagedata['pagename']);
766         $rec = array('pagename' => $pn,
767             'pagedata' => $pagedata);
768         if (is_array($e[1])) {
769             $rec['linkrelation'] = $e[1]['relation'];
770         }
771         //$rec['version'] = $backend->get_latest_version($pn);
772         //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
773         return $rec;
774     }
775
776     function asArray()
777     {
778         reset($this->_result);
779         return $this->_result;
780     }
781
782     function count()
783     {
784         return count($this->_result);
785     }
786
787     function free()
788     {
789         $this->_result = array();
790     }
791 }
792
793 // Local Variables:
794 // mode: php
795 // tab-width: 8
796 // c-basic-offset: 4
797 // c-hanging-comment-ender-p: nil
798 // indent-tabs-mode: nil
799 // End: