]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
Found more strings needing gettext _() and sprintf. Error strings containing function...
[SourceForge/phpwiki.git] / lib / WikiDB.php
1 <?php //-*-php-*-
2 rcs_id('$Id: WikiDB.php,v 1.3 2001-12-28 09:53:29 carstenklapp Exp $');
3
4 //FIXME: arg on get*Revision to hint that content is wanted.
5
6 define('WIKIDB_FORCE_CREATE', -1);
7
8 /** 
9  * Abstract base class for the database used by PhpWiki.
10  *
11  * A <tt>WikiDB</tt> is a container for <tt>WikiDB_Page</tt>s
12  * which in turn contain <tt>WikiDB_PageRevision</tt>s.
13  *
14  * Conceptually a <tt>WikiDB</tt> contains all possible <tt>WikiDB_Page</tt>s,
15  * whether they have been initialized or not.  Since all possible pages are already
16  * contained in a WikiDB, a call to WikiDB::getPage() will never fail
17  * (barring bugs and e.g. filesystem or SQL database problems.)
18  *
19  * Also each <tt>WikiDB_Page</tt> always contains at least one <tt>WikiDB_PageRevision</tt>:
20  * the default content (e.g. "Describe [PageName] here.").  This default content
21  * has a version number of zero.
22  *
23  * <tt>WikiDB_PageRevision</tt>s have read-only semantics.  One can only create new
24  * revisions or delete old ones --- one can not modify an existing revision.
25  */
26 class WikiDB {
27     /**
28      * Open a WikiDB database.
29      *
30      * This is a static member function.   This function inspects its
31      * arguments to determine the proper subclass of WikiDB to instantiate,
32      * and then it instantiates it.
33      *
34      * @access public
35      *
36      * @param $dbparams hash Database configuration parameters.
37      * Some pertinent paramters are:
38      * <dl>
39      * <dt> dbtype
40      * <dd> The back-end type.  Current supported types are:
41      *   <dl>
42      *   <dt> SQL
43      *   <dd> Generic SQL backend based on the PEAR/DB database abstraction
44      *       library.
45      *   <dt> dba
46      *   <dd> Dba based backend.
47      *   </dl>
48      *
49      * <dt> dsn
50      * <dd> (Used by the SQL backend.)
51      *      The DSN specifying which database to connect to.
52      *
53      * <dt> prefix
54      * <dd> Prefix to be prepended to database table (and file names).
55      *
56      * <dt> directory
57      * <dd> (Used by the dba backend.)
58      *      Which directory db files reside in.
59      *
60      * <dt> timeout
61      * <dd> (Used by the dba backend.)
62      *      Timeout in seconds for opening (and obtaining lock) on the db files.
63      *
64      * <dt> dba_handler
65      * <dd> (Used by the dba backend.)
66      *      Which dba handler to use.  Good choices are probably either 'gdbm'
67      *      or 'db2'.
68      * </dl>
69      *
70      * @return object A WikiDB object.
71      **/
72     function open ($dbparams) {
73         $dbtype = $dbparams{'dbtype'};
74         include_once("lib/WikiDB/$dbtype.php");
75         $class = 'WikiDB_' . $dbtype;
76         return new $class ($dbparams);
77     }
78
79
80     /**
81      * Constructor
82      * @access protected
83      */
84     function WikiDB ($backend, $dbparams) {
85         $this->_backend = &$backend;
86         $this->_cache = new WikiDB_cache($backend);
87
88         //FIXME: devel checking.
89         //$this->_backend->check();
90     }
91     
92     /**
93      * Get any user-level warnings about this WikiDB.
94      *
95      * Some back-ends, e.g. by default create there data files
96      * in the global /tmp directory.  We would like to warn the user
97      * when this happens (since /tmp files tend to get wiped
98      * periodically.)   Warnings such as these may be communicated
99      * from specific back-ends through this method.
100      *
101      * @access public
102      *
103      * @return string A warning message (or <tt>false</tt> if there is none.)
104      */
105     function genericWarnings() {
106         return false;
107     }
108      
109     /**
110      * Close database connection.
111      *
112      * The database may no longer be used after it is closed.
113      *
114      * Closing a WikiDB invalidates all <tt>WikiDB_Page</tt>s,
115      * <tt>WikiDB_PageRevision</tt>s and <tt>WikiDB_PageIterator</tt>s which
116      * have been obtained from it.
117      *
118      * @access public
119      */
120     function close () {
121         $this->_backend->close();
122         $this->_cache->close();
123     }
124     
125     /**
126      * Get a WikiDB_Page from a WikiDB.
127      *
128      * A WikiDB consists of the (infinite) set of all possible pages,
129      * therefore this method never fails.
130      *
131      * @access public
132      * @param $pagename string Which page to get.
133      * @return object The requested WikiDB_Page.
134      */
135     function getPage($pagename) {
136         assert(is_string($pagename) && $pagename);
137         return new WikiDB_Page($this, $pagename);
138     }
139
140         
141     // Do we need this?
142     //function nPages() { 
143     //}
144
145
146     /**
147      * Determine whether page exists (in non-default form).
148      *
149      * <pre>
150      *   $is_page = $dbi->isWikiPage($pagename);
151      * </pre>
152      * is equivalent to
153      * <pre>
154      *   $page = $dbi->getPage($pagename);
155      *   $current = $page->getCurrentRevision();
156      *   $is_page = ! $current->hasDefaultContents();
157      * </pre>
158      * however isWikiPage may be implemented in a more efficient
159      * manner in certain back-ends.
160      *
161      * @access public
162      *
163      * @param $pagename string Which page to check.
164      *
165      * @return boolean True if the page actually exists with non-default contents
166      * in the WikiDataBase.
167      */
168     function isWikiPage ($pagename) {
169         $page = $this->getPage($pagename);
170         $current = $page->getCurrentRevision();
171         return ! $current->hasDefaultContents();
172     }
173
174     /**
175      * Delete page from the WikiDB. 
176      *
177      * Deletes all revisions of the page from the WikiDB.
178      * Also resets all page meta-data to the default values.
179      *
180      * @access public
181      *
182      * @param $pagename string Name of page to delete.
183      */
184     function deletePage($pagename) {
185         $this->_cache->delete_page($pagename);
186         $this->_backend->set_links($pagename, false);
187     }
188
189     /**
190      * Retrieve all pages.
191      *
192      * Gets the set of all pages with non-default contents.
193      *
194      * FIXME: do we need this?  I think so.  The simple searches
195      *        need this stuff.
196      *
197      * @access public
198      *
199      * @param $include_defaulted boolean Normally pages whose most recent
200      * revision has empty content are considered to be non-existant.
201      * Unless $include_defaulted is set to true, those pages will
202      * not be returned.
203      *
204      * @return object A WikiDB_PageIterator which contains all pages
205      *     in the WikiDB which have non-default contents.
206      */
207     function getAllPages($include_defaulted = false) {
208         $result = $this->_backend->get_all_pages($include_defaulted);
209         return new WikiDB_PageIterator($this, $result);
210     }
211
212     /**
213      * Title search.
214      *
215      * Search for pages containing (or not containing) certain words in their
216      * names.
217      *
218      * Pages are returned in alphabetical order whenever it is practical
219      * to do so.
220      *
221      * FIXME: should titleSearch and fullSearch be combined?  I think so.
222      *
223      * @access public
224      * @param $search object A TextSearchQuery
225      * @return object A WikiDB_PageIterator containing the matching pages.
226      * @see TextSearchQuery
227      */
228     function titleSearch($search) {
229         $result = $this->_backend->text_search($search);
230         return new WikiDB_PageIterator($this, $result);
231     }
232
233     /**
234      * Full text search.
235      *
236      * Search for pages containing (or not containing) certain words in their
237      * entire text (this includes the page content and the page name).
238      *
239      * Pages are returned in alphabetical order whenever it is practical
240      * to do so.
241      *
242      * @access public
243      *
244      * @param $search object A TextSearchQuery object.
245      * @return object A WikiDB_PageIterator containing the matching pages.
246      * @see TextSearchQuery
247      */
248     function fullSearch($search) {
249         $result = $this->_backend->text_search($search, 'full_text');
250         return new WikiDB_PageIterator($this, $result);
251     }
252
253     /**
254      * Find the pages with the greatest hit counts.
255      *
256      * Pages are returned in reverse order by hit count.
257      *
258      * @access public
259      *
260      * @param $limit unsigned The maximum number of pages to return.
261      * Set $limit to zero to return all pages.
262      *
263      * @return object A WikiDB_PageIterator containing the matching pages.
264      */
265     function mostPopular($limit = 20) {
266         $result = $this->_backend->most_popular($limit);
267         return new WikiDB_PageIterator($this, $result);
268     }
269
270     /**
271      * Find recent page revisions.
272      *
273      * Revisions are returned in reverse order by creation time.
274      *
275      * @access public
276      *
277      * @param $params hash This hash is used to specify various optional
278      *   parameters:
279      * <dl>
280      * <dt> limit 
281      *    <dd> (integer) At most this many revisions will be returned.
282      * <dt> since
283      *    <dd> (integer) Only revisions since this time (unix-timestamp) will be returned. 
284      * <dt> include_minor_revisions
285      *    <dd> (boolean) Also include minor revisions.  (Default is not to.)
286      * <dt> exclude_major_revisions
287      *    <dd> (boolean) Don't include non-minor revisions.
288      *         (Exclude_major_revisions implies include_minor_revisions.)
289      * <dt> include_all_revisions
290      *    <dd> (boolean) Return all matching revisions for each page.
291      *         Normally only the most recent matching revision is returned
292      *         for each page.
293      * </dl>
294      *
295      * @return object A WikiDB_PageRevisionIterator containing the matching revisions.
296      */
297     function mostRecent($params = false) {
298         $result = $this->_backend->most_recent($params);
299         return new WikiDB_PageRevisionIterator($this, $result);
300     }
301 };
302
303
304 /**
305  * An abstract base class which representing a wiki-page within a WikiDB.
306  *
307  * A WikiDB_Page contains a number (at least one) of WikiDB_PageRevisions.
308  */
309 class WikiDB_Page 
310 {
311     function WikiDB_Page(&$wikidb, $pagename) {
312         $this->_wikidb = &$wikidb;
313         $this->_pagename = $pagename;
314         assert(!empty($this->_pagename));
315     }
316
317     /**
318      * Get the name of the wiki page.
319      *
320      * @access public
321      *
322      * @return string The page name.
323      */
324     function getName() {
325         return $this->_pagename;
326     }
327
328
329     /**
330      * Delete an old revision of a WikiDB_Page. 
331      *
332      * Deletes the specified revision of the page.
333      * It is a fatal error to attempt to delete the current revision.
334      *
335      * @access public
336      *
337      * @param $version integer Which revision to delete.  (You can also
338      *  use a WikiDB_PageRevision object here.)
339      */
340     function deleteRevision($version) {
341         $backend = &$this->_wikidb->_backend;
342         $cache = &$this->_wikidb->_cache;
343         $pagename = &$this->_pagename;
344
345         $version = $this->_coerce_to_version($version);
346         if ($version == 0)
347             return;
348
349         $backend->lock();
350         $latestversion = $backend->get_latest_version($pagename);
351         if ($latestversion && $version == $latestversion) {
352             $backend->unlock();
353             trigger_error(sprintf(_("Attempt to delete most recent revision of '%s'"),$pagename),
354                           E_USER_ERROR);
355             return;
356         }
357
358         $cache->delete_versiondata($pagename, $version);
359         $backend->unlock();
360     }
361
362     /*
363      * Delete a revision, or possibly merge it with a previous
364      * revision.
365      *
366      * The idea is this:
367      * Suppose an author make a (major) edit to a page.  Shortly
368      * after that the same author makes a minor edit (e.g. to fix
369      * spelling mistakes he just made.)
370      *
371      * Now some time later, where cleaning out old saved revisions,
372      * and would like to delete his minor revision (since there's really
373      * no point in keeping minor revisions around for a long time.)
374      *
375      * Note that the text after the minor revision probably represents
376      * what the author intended to write better than the text after the
377      * preceding major edit.
378      *
379      * So what we really want to do is merge the minor edit with the
380      * preceding edit.
381      *
382      * We will only do this when:
383      * <ul>
384      * <li>The revision being deleted is a minor one, and
385      * <li>It has the same author as the immediately preceding revision.
386      * </ul>
387      */
388     function mergeRevision($version) {
389         $backend = &$this->_wikidb->_backend;
390         $cache = &$this->_wikidb->_cache;
391         $pagename = &$this->_pagename;
392
393         $version = $this->_coerce_to_version($version);
394         if ($version == 0)
395             return;
396
397         $backend->lock();
398         $latestversion = $backend->get_latest_version($pagename);
399         if ($latestversion && $version == $latestversion) {
400             $backend->unlock();
401             trigger_error(sprintf(_("Attempt to merge most recent revision of '%s'"),$pagename),
402                           E_USER_ERROR);
403             return;
404         }
405
406         $versiondata = $cache->get_versiondata($pagename, $version, true);
407         if (!$versiondata) {
408             // Not there? ... we're done!
409             $backend->unlock();
410             return;
411         }
412
413         if ($versiondata['is_minor_edit']) {
414             $previous = $backend->get_previous_version($pagename, $version);
415             if ($previous) {
416                 $prevdata = $cache->get_versiondata($pagename, $previous);
417                 if ($prevdata['author_id'] == $versiondata['author_id']) {
418                     // This is a minor revision, previous version is by the
419                     // same author.  We will merge the revisions.
420                     $cache->update_versiondata($pagename, $previous,
421                                                array('%content' => $versiondata['%content'],
422                                                      '_supplanted' => $versiondata['_supplanted']));
423                 }
424             }
425         }
426
427         $cache->delete_versiondata($pagename, $version);
428         $backend->unlock();
429     }
430
431     
432     /**
433      * Create a new revision of a WikiDB_Page.
434      *
435      * @access public
436      *
437      * @param $content string Contents of new revision.
438      *
439      * @param $metadata hash Metadata for new revision.
440      * All values in the hash should be scalars (strings or integers).
441      *
442      *
443      * @param $version int Version number for new revision.  
444      * To ensure proper serialization of edits, $version must be
445      * exactly one higher than the current latest version.
446      * (You can defeat this check by setting $version to
447      * WIKIDB_FORCE_CREATE --- not usually recommended.)
448      *
449      * @param $links array List of pagenames which this page links to.
450      *
451      * @return object Returns the new WikiDB_PageRevision object.  If $version was incorrect,
452      * returns false
453      */
454     function createRevision($version, &$content, $metadata, $links) {
455         $backend = &$this->_wikidb->_backend;
456         $cache = &$this->_wikidb->_cache;
457         $pagename = &$this->_pagename;
458                 
459         $backend->lock();
460
461         $latestversion = $backend->get_latest_version($pagename);
462         $newversion = $latestversion + 1;
463         assert($newversion >= 1);
464
465         if ($version != WIKIDB_FORCE_CREATE && $version != $newversion) {
466             $backend->unlock();
467             return false;
468         }
469
470         $data = $metadata;
471         
472         foreach ($data as $key => $val) {
473             if (empty($val) || $key[0] == '_' || $key[0] == '%')
474                 unset($data[$key]);
475         }
476                         
477         assert(!empty($data['author_id']));
478         if (empty($data['author_id']))
479             @$data['author_id'] = $data['author'];
480                 
481         if (empty($data['mtime']))
482             $data['mtime'] = time();
483
484         if ($latestversion) {
485             // Ensure mtimes are monotonic.
486             $pdata = $cache->get_versiondata($pagename, $latestversion);
487             if ($data['mtime'] < $pdata['mtime']) {
488                 trigger_error(sprintf(_("%s: Date of new revision is %s"),$pagename,"'non-monotonic'"),
489                               E_USER_NOTICE);
490                 $data['orig_mtime'] = $data['mtime'];
491                 $data['mtime'] = $pdata['mtime'];
492             }
493             
494             // FIXME: use (possibly user specified) 'mtime' time or time()?
495             $cache->update_versiondata($pagename, $latestversion,
496                                        array('_supplanted' => $data['mtime']));
497         }
498
499         $data['%content'] = &$content;
500
501         $cache->set_versiondata($pagename, $newversion, $data);
502
503         //$cache->update_pagedata($pagename, array(':latestversion' => $newversion,
504         //':deleted' => empty($content)));
505         
506         $backend->set_links($pagename, $links);
507
508         $backend->unlock();
509
510         // FIXME: probably should have some global state information in the backend
511         // to control when to optimize.
512         if (time() % 50 == 0) {
513             trigger_error(sprintf(_("Optimizing %s"),'backend'), E_USER_NOTICE);
514             $backend->optimize();
515         }
516
517         return new WikiDB_PageRevision($this->_wikidb, $pagename, $newversion, $data);
518     }
519
520     /**
521      * Get the most recent revision of a page.
522      *
523      * @access public
524      *
525      * @return object The current WikiDB_PageRevision object. 
526      */
527     function getCurrentRevision() {
528         $backend = &$this->_wikidb->_backend;
529         $cache = &$this->_wikidb->_cache;
530         $pagename = &$this->_pagename;
531
532         $backend->lock();
533         $version = $backend->get_latest_version($pagename);
534         $revision = $this->getRevision($version);
535         $backend->unlock();
536         assert($revision);
537         return $revision;
538     }
539
540     /**
541      * Get a specific revision of a WikiDB_Page.
542      *
543      * @access public
544      *
545      * @param $version integer Which revision to get.
546      *
547      * @return object The requested WikiDB_PageRevision object, or false if the
548      * requested revision does not exist in the WikiDB.  Note that
549      * version zero of any page always exists.
550      */
551     function getRevision($version) {
552         $cache = &$this->_wikidb->_cache;
553         $pagename = &$this->_pagename;
554         
555         if ($version == 0)
556             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
557
558         assert($version > 0);
559         $vdata = $cache->get_versiondata($pagename, $version);
560         if (!$vdata)
561             return false;
562         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version, $vdata);
563     }
564
565     /**
566      * Get previous page revision.
567      *
568      * This method find the most recent revision before a specified version.
569      *
570      * @access public
571      *
572      * @param $version integer Find most recent revision before this version.
573      *  You can also use a WikiDB_PageRevision object to specify the $version.
574      *
575      * @return object The requested WikiDB_PageRevision object, or false if the
576      * requested revision does not exist in the WikiDB.  Note that
577      * unless $version is greater than zero, a revision (perhaps version zero,
578      * the default revision) will always be found.
579      */
580     function getRevisionBefore($version) {
581         $backend = &$this->_wikidb->_backend;
582         $pagename = &$this->_pagename;
583
584         $version = $this->_coerce_to_version($version);
585
586         if ($version == 0)
587             return false;
588         $backend->lock();
589         $previous = $backend->get_previous_version($pagename, $version);
590         $revision = $this->getRevision($previous);
591         $backend->unlock();
592         assert($revision);
593         return $revision;
594     }
595
596     /**
597      * Get all revisions of the WikiDB_Page.
598      *
599      * This does not include the version zero (default) revision in the
600      * returned revision set.
601      *
602      * @return object a WikiDB_PageRevisionIterator containing all revisions of
603      * this WikiDB_Page in reverse order by version number.
604      */
605     function getAllRevisions() {
606         $backend = &$this->_wikidb->_backend;
607         $revs = $backend->get_all_revisions($this->_pagename);
608         return new WikiDB_PageRevisionIterator($this->_wikidb, $revs);
609     }
610     
611     /**
612      * Find pages which link to or are linked from a page.
613      *
614      * @access public
615      *
616      * @param $reversed enum Which links to find: true for backlinks (default).
617      *
618      * @return object A WikiDB_PageIterator containing all matching pages.
619      */
620     function getLinks($reversed = true) {
621         $backend = &$this->_wikidb->_backend;
622         $result =  $backend->get_links($this->_pagename, $reversed);
623         return new WikiDB_PageIterator($this->_wikidb, $result);
624     }
625             
626     /**
627      * Access WikiDB_Page meta-data.
628      *
629      * @access public
630      *
631      * @param $key string Which meta data to get.
632      * Some reserved meta-data keys are:
633      * <dl>
634      * <dt>'locked'<dd> Is page locked?
635      * <dt>'hits'  <dd> Page hit counter.
636      * <dt>'score  <dd> Page score (not yet implement, do we need?)
637      * </dl>
638      *
639      * @return scalar The requested value, or false if the requested data
640      * is not set.
641      */
642     function get($key) {
643         $cache = &$this->_wikidb->_cache;
644         if (!$key || $key[0] == '%')
645             return false;
646         $data = $cache->get_pagedata($this->_pagename);
647         return isset($data[$key]) ? $data[$key] : false;
648     }
649
650     /**
651      * Get all the page meta-data as a hash.
652      *
653      * @return hash The page meta-data.
654      */
655     function getMetaData() {
656         $cache = &$this->_wikidb->_cache;
657         $data = $cache->get_pagedata($this->_pagename);
658         $meta = array();
659         foreach ($data as $key => $val) {
660             if (!empty($val) && $key[0] != '%')
661                 $meta[$key] = $val;
662         }
663         return $meta;
664     }
665
666     /**
667      * Set page meta-data.
668      *
669      * @see get
670      * @access public
671      *
672      * @param $key string Meta-data key to set.
673      * @param $newval string New value.
674      */
675     function set($key, $newval) {
676         $cache = &$this->_wikidb->_cache;
677         $pagename = &$this->_pagename;
678         
679         assert($key && $key[0] != '%');
680
681         $data = $cache->get_pagedata($pagename);
682
683         if (!empty($newval)) {
684             if (!empty($data[$key]) && $data[$key] == $newval)
685                 return;         // values identical, skip update.
686         }
687         else {
688             if (empty($data[$key]))
689                 return;         // values identical, skip update.
690         }
691
692         $cache->update_pagedata($pagename, array($key => $newval));
693     }
694
695     /**
696      * Increase page hit count.
697      *
698      * FIXME: IS this needed?  Probably not.
699      *
700      * This is a convenience function.
701      * <pre> $page->increaseHitCount(); </pre>
702      * is functionally identical to
703      * <pre> $page->set('hits',$page->get('hits')+1); </pre>
704      *
705      * Note that this method may be implemented in more efficient ways
706      * in certain backends.
707      *
708      * @access public
709      */
710     function increaseHitCount() {
711         @$newhits = $this->get('hits') + 1;
712         $this->set('hits', $newhits);
713     }
714
715     /**
716      * Return a string representation of the WikiDB_Page
717      *
718      * This is really only for debugging.
719      *
720      * @access public
721      *
722      * @return string Printable representation of the WikiDB_Page.
723      */
724     function asString () {
725         ob_start();
726         printf("[%s:%s\n", get_class($this), $this->getName());
727         print_r($this->getMetaData());
728         echo "]\n";
729         $strval = ob_get_contents();
730         ob_end_clean();
731         return $strval;
732     }
733
734
735     /**
736      * @access private
737      * @param $version_or_pagerevision int or object
738      * Takes either the version number (and int) or a WikiDB_PageRevision
739      * object.
740      * @return int The version number.
741      */
742     function _coerce_to_version($version_or_pagerevision) {
743         if (method_exists($version_or_pagerevision, "getContent"))
744             $version = $version_or_pagerevision->getVersion();
745         else
746             $version = (int) $version_or_pagerevision;
747
748         assert($version >= 0);
749         return $version;
750     }
751 };
752
753 /**
754  * This class represents a specific revision of a WikiDB_Page within
755  * a WikiDB.
756  *
757  * A WikiDB_PageRevision has read-only semantics.  You may only
758  * create new revisions (and delete old ones) --- you cannot
759  * modify existing revisions.
760  */
761 class WikiDB_PageRevision
762 {
763     function WikiDB_PageRevision(&$wikidb, $pagename, $version, $versiondata = false) {
764         $this->_wikidb = &$wikidb;
765         $this->_pagename = $pagename;
766         $this->_version = $version;
767         $this->_data = $versiondata ? $versiondata : array();
768     }
769     
770     /**
771      * Get the WikiDB_Page which this revision belongs to.
772      *
773      * @access public
774      *
775      * @return object The WikiDB_Page which this revision belongs to.
776      */
777     function getPage() {
778         return new WikiDB_Page($this->_wikidb, $this->_pagename);
779     }
780
781     /**
782      * Get the version number of this revision.
783      *
784      * @access public
785      *
786      * @return int The version number of this revision.
787      */
788     function getVersion() {
789         return $this->_version;
790     }
791     
792     /**
793      * Determine whether this revision has defaulted content.
794      *
795      * The default revision (version 0) of each page, as well as
796      * any pages which are created with empty content
797      * have their content defaulted to something like:
798      * <pre>
799      *   Describe [ThisPage] here.
800      * </pre>
801      *
802      * @access public
803      *
804      * @return boolean Returns true if the page has default content.
805      */
806     function hasDefaultContents() {
807         $data = &$this->_data;
808         return empty($data['%content']);
809     }
810
811     /**
812      * Get the content as an array of lines.
813      *
814      * @access public
815      *
816      * @return array An array of lines.
817      * The lines should contain no trailing white space.
818      */
819     function getContent() {
820         return explode("\n", $this->getPackedContent());
821     }
822
823     /**
824      * Get the content as a string.
825      *
826      * @access public
827      *
828      * @return string The page content.
829      * Lines are separated by new-lines.
830      */
831     function getPackedContent() {
832         $data = &$this->_data;
833
834         
835         if (empty($data['%content'])) {
836             // Replace empty content with default value.
837             return sprintf(_("Describe %s here."),
838                            "[". $this->_pagename ."]");
839         }
840
841         // There is (non-default) content.
842         assert($this->_version > 0);
843         
844         if (!is_string($data['%content'])) {
845             // Content was not provided to us at init time.
846             // (This is allowed because for some backends, fetching
847             // the content may be expensive, and often is not wanted
848             // by the user.)
849             //
850             // In any case, now we need to get it.
851             $data['%content'] = $this->_get_content();
852             assert(is_string($data['%content']));
853         }
854         
855         return $data['%content'];
856     }
857
858     function _get_content() {
859         $cache = &$this->_wikidb->_cache;
860         $pagename = $this->_pagename;
861         $version = $this->_version;
862
863         assert($version > 0);
864         
865         $newdata = $cache->get_versiondata($pagename, $version, true);
866         if ($newdata) {
867             assert(is_string($newdata['%content']));
868             return $newdata['%content'];
869         }
870         else {
871             // else revision has been deleted... What to do?
872             return "Acck! Revision $version of $pagename seems to have been deleted!";
873         }
874     }
875
876     /**
877      * Get meta-data for this revision.
878      *
879      *
880      * @access public
881      *
882      * @param $key string Which meta-data to access.
883      *
884      * Some reserved revision meta-data keys are:
885      * <dl>
886      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
887      *        The 'mtime' meta-value is normally set automatically by the database
888      *        backend, but it may be specified explicitly when creating a new revision.
889      * <dt> orig_mtime
890      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
891      *       of a page must be monotonically increasing.  If an attempt is
892      *       made to create a new revision with an mtime less than that of
893      *       the preceeding revision, the new revisions timestamp is force
894      *       to be equal to that of the preceeding revision.  In that case,
895      *       the originally requested mtime is preserved in 'orig_mtime'.
896      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
897      *        This meta-value is <i>always</i> automatically maintained by the database
898      *        backend.  (It is set from the 'mtime' meta-value of the superceding
899      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
900      *
901      * FIXME: this could be refactored:
902      * <dt> author
903      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
904      * <dt> author_id
905      *  <dd> Authenticated author of a page.  This is used to identify
906      *       the distinctness of authors when cleaning old revisions from
907      *       the database.
908      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
909      * <dt> 'summary' <dd> Short change summary entered by page author.
910      * </dl>
911      *
912      * Meta-data keys must be valid C identifers (they have to start with a letter
913      * or underscore, and can contain only alphanumerics and underscores.)
914      *
915      * @return string The requested value, or false if the requested value
916      * is not defined.
917      */
918     function get($key) {
919         if (!$key || $key[0] == '%')
920             return false;
921         $data = &$this->_data;
922         return isset($data[$key]) ? $data[$key] : false;
923     }
924
925     /**
926      * Get all the revision page meta-data as a hash.
927      *
928      * @return hash The revision meta-data.
929      */
930     function getMetaData() {
931         $meta = array();
932         foreach ($this->_data as $key => $val) {
933             if (!empty($val) && $key[0] != '%')
934                 $meta[$key] = $val;
935         }
936         return $meta;
937     }
938     
939             
940     /**
941      * Return a string representation of the revision.
942      *
943      * This is really only for debugging.
944      *
945      * @access public
946      *
947      * @return string Printable representation of the WikiDB_Page.
948      */
949     function asString () {
950         ob_start();
951         printf("[%s:%d\n", get_class($this), $this->get('version'));
952         print_r($this->_data);
953         echo $this->getPackedContent() . "\n]\n";
954         $strval = ob_get_contents();
955         ob_end_clean();
956         return $strval;
957     }
958 };
959
960
961 /**
962  * A class which represents a sequence of WikiDB_Pages.
963  */
964 class WikiDB_PageIterator
965 {
966     function WikiDB_PageIterator(&$wikidb, &$pages) {
967         $this->_pages = $pages;
968         $this->_wikidb = &$wikidb;
969     }
970     
971     /**
972      * Get next WikiDB_Page in sequence.
973      *
974      * @access public
975      *
976      * @return object The next WikiDB_Page in the sequence.
977      */
978     function next () {
979         if ( ! ($next = $this->_pages->next()) )
980             return false;
981
982         $pagename = &$next['pagename'];
983         if (isset($next['pagedata']))
984             $this->_wikidb->_cache->cache_data($next);
985
986         return new WikiDB_Page($this->_wikidb, $pagename);
987     }
988
989     /**
990      * Release resources held by this iterator.
991      *
992      * The iterator may not be used after free() is called.
993      *
994      * There is no need to call free(), if next() has returned false.
995      * (I.e. if you iterate through all the pages in the sequence,
996      * you do not need to call free() --- you only need to call it
997      * if you stop before the end of the iterator is reached.)
998      *
999      * @access public
1000      */
1001     function free() {
1002         $this->_pages->free();
1003     }
1004 };
1005
1006 /**
1007  * A class which represents a sequence of WikiDB_PageRevisions.
1008  */
1009 class WikiDB_PageRevisionIterator
1010 {
1011     function WikiDB_PageRevisionIterator(&$wikidb, &$revisions) {
1012         $this->_revisions = $revisions;
1013         $this->_wikidb = &$wikidb;
1014     }
1015     
1016     /**
1017      * Get next WikiDB_PageRevision in sequence.
1018      *
1019      * @access public
1020      *
1021      * @return object The next WikiDB_PageRevision in the sequence.
1022      */
1023     function next () {
1024         if ( ! ($next = $this->_revisions->next()) )
1025             return false;
1026
1027         $this->_wikidb->_cache->cache_data($next);
1028
1029         $pagename = $next['pagename'];
1030         $version = $next['version'];
1031         $versiondata = $next['versiondata'];
1032         assert(!empty($pagename));
1033         assert(is_array($versiondata));
1034         assert($version > 0);
1035
1036         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version, $versiondata);
1037     }
1038
1039     /**
1040      * Release resources held by this iterator.
1041      *
1042      * The iterator may not be used after free() is called.
1043      *
1044      * There is no need to call free(), if next() has returned false.
1045      * (I.e. if you iterate through all the revisions in the sequence,
1046      * you do not need to call free() --- you only need to call it
1047      * if you stop before the end of the iterator is reached.)
1048      *
1049      * @access public
1050      */
1051     function free() { 
1052         $this->_revisions->free();
1053     }
1054 };
1055
1056
1057 /**
1058  * Data cache used by WikiDB.
1059  *
1060  * FIXME: Maybe rename this to caching_backend (or some such).
1061  *
1062  * @access protected
1063  */
1064 class WikiDB_cache 
1065 {
1066     // FIXME: cache (limited) version data, too.
1067
1068     function WikiDB_cache (&$backend) {
1069         $this->_backend = &$backend;
1070
1071         $this->_pagedata_cache = array();
1072     }
1073     
1074     function close() {
1075         $this->_pagedata_cache = false;
1076     }
1077
1078     function get_pagedata($pagename) {
1079         assert(is_string($pagename) && $pagename);
1080         $cache = &$this->_pagedata_cache;
1081
1082         if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
1083             $cache[$pagename] = $this->_backend->get_pagedata($pagename);
1084             if (empty($cache[$pagename]))
1085                 $cache[$pagename] = array();
1086         }
1087
1088         return $cache[$pagename];
1089     }
1090     
1091     function update_pagedata($pagename, $newdata) {
1092         assert(is_string($pagename) && $pagename);
1093
1094         $this->_backend->update_pagedata($pagename, $newdata);
1095
1096         if (is_array($this->_pagedata_cache[$pagename])) {
1097             $cachedata = &$this->_pagedata_cache[$pagename];
1098             foreach($newdata as $key => $val)
1099                 $cachedata[$key] = $val;
1100         }
1101     }
1102
1103     function invalidate_cache($pagename) {
1104         $this->_pagedata_cache[$pagename] = false;
1105     }
1106     
1107     function delete_page($pagename) {
1108         $this->_backend->delete_page($pagename);
1109         $this->_pagedata_cache[$pagename] = false;
1110     }
1111
1112     // FIXME: ugly
1113     function cache_data($data) {
1114         if (isset($data['pagedata']))
1115             $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
1116     }
1117     
1118     function get_versiondata($pagename, $version, $need_content = false) {
1119         $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
1120         // FIXME: ugly
1121         if ($vdata && !empty($vdata['%pagedata']))
1122             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
1123         return $vdata;
1124     }
1125
1126     function set_versiondata($pagename, $version, $data) {
1127         $new = $this->_backend->
1128              set_versiondata($pagename, $version, $data);
1129     }
1130
1131     function update_versiondata($pagename, $version, $data) {
1132         $new = $this->_backend->
1133              update_versiondata($pagename, $version, $data);
1134     }
1135
1136     function delete_versiondata($pagename, $version) {
1137         $new = $this->_backend->
1138              delete_versiondata($pagename, $version);
1139     }
1140 };
1141
1142 // Local Variables:
1143 // mode: php
1144 // tab-width: 8
1145 // c-basic-offset: 4
1146 // c-hanging-comment-ender-p: nil
1147 // indent-tabs-mode: nil
1148 // End:   
1149 ?>