]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
Big Template cleanup and refactor.
[SourceForge/phpwiki.git] / lib / WikiDB.php
1 <?php //-*-php-*-
2 rcs_id('$Id: WikiDB.php,v 1.5 2002-01-24 00:45:28 dairiki 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      * Determine whether revision is the latest.
825      *
826      * @access public
827      *
828      * @return bool True iff the revision is the latest (most recent) one.
829      */
830     function isCurrent() {
831         if (!isset($this->_iscurrent)) {
832             $page = $this->getPage();
833             $current = $page->getCurrentRevision();
834             $this->_iscurrent = $this->getVersion() == $current->getVersion();
835         }
836         return $this->_iscurrent;
837     }
838     
839     /**
840      * Get the content as a string.
841      *
842      * @access public
843      *
844      * @return string The page content.
845      * Lines are separated by new-lines.
846      */
847     function getPackedContent() {
848         $data = &$this->_data;
849
850         
851         if (empty($data['%content'])) {
852             // Replace empty content with default value.
853             return sprintf(_("Describe %s here."),
854                            "[". $this->_pagename ."]");
855         }
856
857         // There is (non-default) content.
858         assert($this->_version > 0);
859         
860         if (!is_string($data['%content'])) {
861             // Content was not provided to us at init time.
862             // (This is allowed because for some backends, fetching
863             // the content may be expensive, and often is not wanted
864             // by the user.)
865             //
866             // In any case, now we need to get it.
867             $data['%content'] = $this->_get_content();
868             assert(is_string($data['%content']));
869         }
870         
871         return $data['%content'];
872     }
873
874     function _get_content() {
875         $cache = &$this->_wikidb->_cache;
876         $pagename = $this->_pagename;
877         $version = $this->_version;
878
879         assert($version > 0);
880         
881         $newdata = $cache->get_versiondata($pagename, $version, true);
882         if ($newdata) {
883             assert(is_string($newdata['%content']));
884             return $newdata['%content'];
885         }
886         else {
887             // else revision has been deleted... What to do?
888             return __sprintf("Acck! Revision %s of %s seems to have been deleted!",
889                              $version, $pagename);
890         }
891     }
892
893     /**
894      * Get meta-data for this revision.
895      *
896      *
897      * @access public
898      *
899      * @param $key string Which meta-data to access.
900      *
901      * Some reserved revision meta-data keys are:
902      * <dl>
903      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
904      *        The 'mtime' meta-value is normally set automatically by the database
905      *        backend, but it may be specified explicitly when creating a new revision.
906      * <dt> orig_mtime
907      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
908      *       of a page must be monotonically increasing.  If an attempt is
909      *       made to create a new revision with an mtime less than that of
910      *       the preceeding revision, the new revisions timestamp is force
911      *       to be equal to that of the preceeding revision.  In that case,
912      *       the originally requested mtime is preserved in 'orig_mtime'.
913      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
914      *        This meta-value is <em>always</em> automatically maintained by the database
915      *        backend.  (It is set from the 'mtime' meta-value of the superceding
916      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
917      *
918      * FIXME: this could be refactored:
919      * <dt> author
920      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
921      * <dt> author_id
922      *  <dd> Authenticated author of a page.  This is used to identify
923      *       the distinctness of authors when cleaning old revisions from
924      *       the database.
925      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
926      * <dt> 'summary' <dd> Short change summary entered by page author.
927      * </dl>
928      *
929      * Meta-data keys must be valid C identifers (they have to start with a letter
930      * or underscore, and can contain only alphanumerics and underscores.)
931      *
932      * @return string The requested value, or false if the requested value
933      * is not defined.
934      */
935     function get($key) {
936         if (!$key || $key[0] == '%')
937             return false;
938         $data = &$this->_data;
939         return isset($data[$key]) ? $data[$key] : false;
940     }
941
942     /**
943      * Get all the revision page meta-data as a hash.
944      *
945      * @return hash The revision meta-data.
946      */
947     function getMetaData() {
948         $meta = array();
949         foreach ($this->_data as $key => $val) {
950             if (!empty($val) && $key[0] != '%')
951                 $meta[$key] = $val;
952         }
953         return $meta;
954     }
955     
956             
957     /**
958      * Return a string representation of the revision.
959      *
960      * This is really only for debugging.
961      *
962      * @access public
963      *
964      * @return string Printable representation of the WikiDB_Page.
965      */
966     function asString () {
967         ob_start();
968         printf("[%s:%d\n", get_class($this), $this->get('version'));
969         print_r($this->_data);
970         echo $this->getPackedContent() . "\n]\n";
971         $strval = ob_get_contents();
972         ob_end_clean();
973         return $strval;
974     }
975 };
976
977
978 /**
979  * A class which represents a sequence of WikiDB_Pages.
980  */
981 class WikiDB_PageIterator
982 {
983     function WikiDB_PageIterator(&$wikidb, &$pages) {
984         $this->_pages = $pages;
985         $this->_wikidb = &$wikidb;
986     }
987     
988     /**
989      * Get next WikiDB_Page in sequence.
990      *
991      * @access public
992      *
993      * @return object The next WikiDB_Page in the sequence.
994      */
995     function next () {
996         if ( ! ($next = $this->_pages->next()) )
997             return false;
998
999         $pagename = &$next['pagename'];
1000         if (isset($next['pagedata']))
1001             $this->_wikidb->_cache->cache_data($next);
1002
1003         return new WikiDB_Page($this->_wikidb, $pagename);
1004     }
1005
1006     /**
1007      * Release resources held by this iterator.
1008      *
1009      * The iterator may not be used after free() is called.
1010      *
1011      * There is no need to call free(), if next() has returned false.
1012      * (I.e. if you iterate through all the pages in the sequence,
1013      * you do not need to call free() --- you only need to call it
1014      * if you stop before the end of the iterator is reached.)
1015      *
1016      * @access public
1017      */
1018     function free() {
1019         $this->_pages->free();
1020     }
1021 };
1022
1023 /**
1024  * A class which represents a sequence of WikiDB_PageRevisions.
1025  */
1026 class WikiDB_PageRevisionIterator
1027 {
1028     function WikiDB_PageRevisionIterator(&$wikidb, &$revisions) {
1029         $this->_revisions = $revisions;
1030         $this->_wikidb = &$wikidb;
1031     }
1032     
1033     /**
1034      * Get next WikiDB_PageRevision in sequence.
1035      *
1036      * @access public
1037      *
1038      * @return object The next WikiDB_PageRevision in the sequence.
1039      */
1040     function next () {
1041         if ( ! ($next = $this->_revisions->next()) )
1042             return false;
1043
1044         $this->_wikidb->_cache->cache_data($next);
1045
1046         $pagename = $next['pagename'];
1047         $version = $next['version'];
1048         $versiondata = $next['versiondata'];
1049         assert(!empty($pagename));
1050         assert(is_array($versiondata));
1051         assert($version > 0);
1052
1053         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version, $versiondata);
1054     }
1055
1056     /**
1057      * Release resources held by this iterator.
1058      *
1059      * The iterator may not be used after free() is called.
1060      *
1061      * There is no need to call free(), if next() has returned false.
1062      * (I.e. if you iterate through all the revisions in the sequence,
1063      * you do not need to call free() --- you only need to call it
1064      * if you stop before the end of the iterator is reached.)
1065      *
1066      * @access public
1067      */
1068     function free() { 
1069         $this->_revisions->free();
1070     }
1071 };
1072
1073
1074 /**
1075  * Data cache used by WikiDB.
1076  *
1077  * FIXME: Maybe rename this to caching_backend (or some such).
1078  *
1079  * @access protected
1080  */
1081 class WikiDB_cache 
1082 {
1083     // FIXME: cache (limited) version data, too.
1084
1085     function WikiDB_cache (&$backend) {
1086         $this->_backend = &$backend;
1087
1088         $this->_pagedata_cache = array();
1089     }
1090     
1091     function close() {
1092         $this->_pagedata_cache = false;
1093     }
1094
1095     function get_pagedata($pagename) {
1096         assert(is_string($pagename) && $pagename);
1097         $cache = &$this->_pagedata_cache;
1098
1099         if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
1100             $cache[$pagename] = $this->_backend->get_pagedata($pagename);
1101             if (empty($cache[$pagename]))
1102                 $cache[$pagename] = array();
1103         }
1104
1105         return $cache[$pagename];
1106     }
1107     
1108     function update_pagedata($pagename, $newdata) {
1109         assert(is_string($pagename) && $pagename);
1110
1111         $this->_backend->update_pagedata($pagename, $newdata);
1112
1113         if (is_array($this->_pagedata_cache[$pagename])) {
1114             $cachedata = &$this->_pagedata_cache[$pagename];
1115             foreach($newdata as $key => $val)
1116                 $cachedata[$key] = $val;
1117         }
1118     }
1119
1120     function invalidate_cache($pagename) {
1121         $this->_pagedata_cache[$pagename] = false;
1122     }
1123     
1124     function delete_page($pagename) {
1125         $this->_backend->delete_page($pagename);
1126         $this->_pagedata_cache[$pagename] = false;
1127     }
1128
1129     // FIXME: ugly
1130     function cache_data($data) {
1131         if (isset($data['pagedata']))
1132             $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
1133     }
1134     
1135     function get_versiondata($pagename, $version, $need_content = false) {
1136         $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
1137         // FIXME: ugly
1138         if ($vdata && !empty($vdata['%pagedata']))
1139             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
1140         return $vdata;
1141     }
1142
1143     function set_versiondata($pagename, $version, $data) {
1144         $new = $this->_backend->
1145              set_versiondata($pagename, $version, $data);
1146     }
1147
1148     function update_versiondata($pagename, $version, $data) {
1149         $new = $this->_backend->
1150              update_versiondata($pagename, $version, $data);
1151     }
1152
1153     function delete_versiondata($pagename, $version) {
1154         $new = $this->_backend->
1155              delete_versiondata($pagename, $version);
1156     }
1157 };
1158
1159 // Local Variables:
1160 // mode: php
1161 // tab-width: 8
1162 // c-basic-offset: 4
1163 // c-hanging-comment-ender-p: nil
1164 // indent-tabs-mode: nil
1165 // End:   
1166 ?>