]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
Fix for API change in WikiUser constructor.
[SourceForge/phpwiki.git] / lib / WikiDB.php
1 <?php //-*-php-*-
2 rcs_id('$Id: WikiDB.php,v 1.18 2003-02-15 02:24:23 dairiki Exp $');
3
4 //FIXME: arg on get*Revision to hint that content is wanted.
5
6 /**
7  * The classes in the file define the interface to the
8  * page database.
9  *
10  * @package WikiDB
11  * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
12  */
13
14 /**
15  * Force the creation of a new revision.
16  * @see WikiDB_Page::createRevision()
17  */
18 define('WIKIDB_FORCE_CREATE', -1);
19
20 // FIXME:  used for debugging only.  Comment out if cache does not work
21 define('USECACHE', 1);
22
23 /** 
24  * Abstract base class for the database used by PhpWiki.
25  *
26  * A <tt>WikiDB</tt> is a container for <tt>WikiDB_Page</tt>s which in
27  * turn contain <tt>WikiDB_PageRevision</tt>s.
28  *
29  * Conceptually a <tt>WikiDB</tt> contains all possible
30  * <tt>WikiDB_Page</tt>s, whether they have been initialized or not.
31  * Since all possible pages are already contained in a WikiDB, a call
32  * to WikiDB::getPage() will never fail (barring bugs and
33  * e.g. filesystem or SQL database problems.)
34  *
35  * Also each <tt>WikiDB_Page</tt> always contains at least one
36  * <tt>WikiDB_PageRevision</tt>: the default content (e.g. "Describe
37  * [PageName] here.").  This default content has a version number of
38  * zero.
39  *
40  * <tt>WikiDB_PageRevision</tt>s have read-only semantics. One can
41  * only create new revisions or delete old ones --- one can not modify
42  * an existing revision.
43  */
44 class WikiDB {
45     /**
46      * Open a WikiDB database.
47      *
48      * This is a static member function. This function inspects its
49      * arguments to determine the proper subclass of WikiDB to
50      * instantiate, and then it instantiates it.
51      *
52      * @access public
53      *
54      * @param hash $dbparams Database configuration parameters.
55      * Some pertinent paramters are:
56      * <dl>
57      * <dt> dbtype
58      * <dd> The back-end type.  Current supported types are:
59      *   <dl>
60      *   <dt> SQL
61      *   <dd> Generic SQL backend based on the PEAR/DB database abstraction
62      *       library.
63      *   <dt> dba
64      *   <dd> Dba based backend.
65      *   </dl>
66      *
67      * <dt> dsn
68      * <dd> (Used by the SQL backend.)
69      *      The DSN specifying which database to connect to.
70      *
71      * <dt> prefix
72      * <dd> Prefix to be prepended to database table (and file names).
73      *
74      * <dt> directory
75      * <dd> (Used by the dba backend.)
76      *      Which directory db files reside in.
77      *
78      * <dt> timeout
79      * <dd> (Used by the dba backend.)
80      *      Timeout in seconds for opening (and obtaining lock) on the
81      *      db files.
82      *
83      * <dt> dba_handler
84      * <dd> (Used by the dba backend.)
85      *
86      *      Which dba handler to use. Good choices are probably either
87      *      'gdbm' or 'db2'.
88      * </dl>
89      *
90      * @return WikiDB A WikiDB object.
91      **/
92     function open ($dbparams) {
93         $dbtype = $dbparams{'dbtype'};
94         include_once("lib/WikiDB/$dbtype.php");
95                                 
96         $class = 'WikiDB_' . $dbtype;
97         return new $class ($dbparams);
98     }
99
100
101     /**
102      * Constructor.
103      *
104      * @access private
105      * @see open()
106      */
107     function WikiDB ($backend, $dbparams) {
108         $this->_backend = &$backend;
109         $this->_cache = new WikiDB_cache($backend);
110
111         //FIXME: devel checking.
112         //$this->_backend->check();
113     }
114     
115     /**
116      * Get any user-level warnings about this WikiDB.
117      *
118      * Some back-ends, e.g. by default create there data files in the
119      * global /tmp directory. We would like to warn the user when this
120      * happens (since /tmp files tend to get wiped periodically.)
121      * Warnings such as these may be communicated from specific
122      * back-ends through this method.
123      *
124      * @access public
125      *
126      * @return string A warning message (or <tt>false</tt> if there is
127      * none.)
128      */
129     function genericWarnings() {
130         return false;
131     }
132      
133     /**
134      * Close database connection.
135      *
136      * The database may no longer be used after it is closed.
137      *
138      * Closing a WikiDB invalidates all <tt>WikiDB_Page</tt>s,
139      * <tt>WikiDB_PageRevision</tt>s and <tt>WikiDB_PageIterator</tt>s
140      * which have been obtained from it.
141      *
142      * @access public
143      */
144     function close () {
145         $this->_backend->close();
146         $this->_cache->close();
147     }
148     
149     /**
150      * Get a WikiDB_Page from a WikiDB.
151      *
152      * A {@link WikiDB} consists of the (infinite) set of all possible pages,
153      * therefore this method never fails.
154      *
155      * @access public
156      * @param string $pagename Which page to get.
157      * @return WikiDB_Page The requested WikiDB_Page.
158      */
159     function getPage($pagename) {
160         assert(is_string($pagename) && $pagename);
161         return new WikiDB_Page($this, $pagename);
162     }
163
164         
165     // Do we need this?
166     //function nPages() { 
167     //}
168
169
170     /**
171      * Determine whether page exists (in non-default form).
172      *
173      * <pre>
174      *   $is_page = $dbi->isWikiPage($pagename);
175      * </pre>
176      * is equivalent to
177      * <pre>
178      *   $page = $dbi->getPage($pagename);
179      *   $current = $page->getCurrentRevision();
180      *   $is_page = ! $current->hasDefaultContents();
181      * </pre>
182      * however isWikiPage may be implemented in a more efficient
183      * manner in certain back-ends.
184      *
185      * @access public
186      *
187      * @param string $pagename string Which page to check.
188      *
189      * @return boolean True if the page actually exists with
190      * non-default contents in the WikiDataBase.
191      */
192     function isWikiPage ($pagename) {
193         $page = $this->getPage($pagename);
194         $current = $page->getCurrentRevision();
195         return ! $current->hasDefaultContents();
196     }
197
198     /**
199      * Delete page from the WikiDB. 
200      *
201      * Deletes all revisions of the page from the WikiDB. Also resets
202      * all page meta-data to the default values.
203      *
204      * @access public
205      *
206      * @param string $pagename Name of page to delete.
207      */
208     function deletePage($pagename) {
209         $this->_cache->delete_page($pagename);
210         $this->_backend->set_links($pagename, false);
211     }
212
213     /**
214      * Retrieve all pages.
215      *
216      * Gets the set of all pages with non-default contents.
217      *
218      * FIXME: do we need this?  I think so.  The simple searches
219      *        need this stuff.
220      *
221      * @access public
222      *
223      * @param boolean $include_defaulted Normally pages whose most
224      * recent revision has empty content are considered to be
225      * non-existant. Unless $include_defaulted is set to true, those
226      * pages will not be returned.
227      *
228      * @return WikiDB_PageIterator A WikiDB_PageIterator which contains all pages
229      *     in the WikiDB which have non-default contents.
230      */
231     function getAllPages($include_defaulted = false) {
232         $result = $this->_backend->get_all_pages($include_defaulted);
233         return new WikiDB_PageIterator($this, $result);
234     }
235
236     /**
237      * Title search.
238      *
239      * Search for pages containing (or not containing) certain words
240      * in their names.
241      *
242      * Pages are returned in alphabetical order whenever it is
243      * practical to do so.
244      *
245      * FIXME: should titleSearch and fullSearch be combined?  I think so.
246      *
247      * @access public
248      * @param TextSearchQuery $search A TextSearchQuery object
249      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
250      * @see TextSearchQuery
251      */
252     function titleSearch($search) {
253         $result = $this->_backend->text_search($search);
254         return new WikiDB_PageIterator($this, $result);
255     }
256
257     /**
258      * Full text search.
259      *
260      * Search for pages containing (or not containing) certain words
261      * in their entire text (this includes the page content and the
262      * page name).
263      *
264      * Pages are returned in alphabetical order whenever it is
265      * practical to do so.
266      *
267      * @access public
268      *
269      * @param TextSearchQuery $search A TextSearchQuery object.
270      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
271      * @see TextSearchQuery
272      */
273     function fullSearch($search) {
274         $result = $this->_backend->text_search($search, 'full_text');
275         return new WikiDB_PageIterator($this, $result);
276     }
277
278     /**
279      * Find the pages with the greatest hit counts.
280      *
281      * Pages are returned in reverse order by hit count.
282      *
283      * @access public
284      *
285      * @param integer $limit The maximum number of pages to return.
286      * Set $limit to zero to return all pages.  If $limit < 0, pages will
287      * be sorted in decreasing order of popularity.
288      *
289      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching
290      * pages.
291      */
292     function mostPopular($limit = 20) {
293         $result = $this->_backend->most_popular($limit);
294         return new WikiDB_PageIterator($this, $result);
295     }
296
297     /**
298      * Find recent page revisions.
299      *
300      * Revisions are returned in reverse order by creation time.
301      *
302      * @access public
303      *
304      * @param hash $params This hash is used to specify various optional
305      *   parameters:
306      * <dl>
307      * <dt> limit 
308      *    <dd> (integer) At most this many revisions will be returned.
309      * <dt> since
310      *    <dd> (integer) Only revisions since this time (unix-timestamp) will be returned. 
311      * <dt> include_minor_revisions
312      *    <dd> (boolean) Also include minor revisions.  (Default is not to.)
313      * <dt> exclude_major_revisions
314      *    <dd> (boolean) Don't include non-minor revisions.
315      *         (Exclude_major_revisions implies include_minor_revisions.)
316      * <dt> include_all_revisions
317      *    <dd> (boolean) Return all matching revisions for each page.
318      *         Normally only the most recent matching revision is returned
319      *         for each page.
320      * </dl>
321      *
322      * @return WikiDB_PageRevisionIterator A WikiDB_PageRevisionIterator containing the
323      * matching revisions.
324      */
325     function mostRecent($params = false) {
326         $result = $this->_backend->most_recent($params);
327         return new WikiDB_PageRevisionIterator($this, $result);
328     }
329
330    /**
331      * Blog search. (experimental)
332      *
333      * Search for blog entries related to a certain page.
334      *
335      * FIXME: with pagetype support and perhaps a RegexpSearchQuery
336      * we can make sure we are returning *ONLY* blog pages to the
337      * main routine.  Currently, we just use titleSearch which requires
338      * some furher checking in lib/plugin/WikiBlog.php (BAD).
339      *
340      * @access public
341      *
342      * @param string $order  'normal' (chronological) or 'reverse'
343      * @param string $page   Find blog entries related to this page.
344      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the relevant pages.
345      */
346     function blogSearch($page, $order) {
347       //FIXME: implement ordering
348
349       require_once('lib/TextSearchQuery.php');
350       $query = new TextSearchQuery ($page . SUBPAGE_SEPARATOR);
351
352       return $this->titleSearch($query);
353     }
354
355 };
356
357
358 /**
359  * An abstract base class which representing a wiki-page within a
360  * WikiDB.
361  *
362  * A WikiDB_Page contains a number (at least one) of
363  * WikiDB_PageRevisions.
364  */
365 class WikiDB_Page 
366 {
367     function WikiDB_Page(&$wikidb, $pagename) {
368         $this->_wikidb = &$wikidb;
369         $this->_pagename = $pagename;
370         assert(!empty($this->_pagename));
371     }
372
373     /**
374      * Get the name of the wiki page.
375      *
376      * @access public
377      *
378      * @return string The page name.
379      */
380     function getName() {
381         return $this->_pagename;
382     }
383
384
385     /**
386      * Delete an old revision of a WikiDB_Page.
387      *
388      * Deletes the specified revision of the page.
389      * It is a fatal error to attempt to delete the current revision.
390      *
391      * @access public
392      *
393      * @param integer $version Which revision to delete.  (You can also
394      *  use a WikiDB_PageRevision object here.)
395      */
396     function deleteRevision($version) {
397         $backend = &$this->_wikidb->_backend;
398         $cache = &$this->_wikidb->_cache;
399         $pagename = &$this->_pagename;
400
401         $version = $this->_coerce_to_version($version);
402         if ($version == 0)
403             return;
404
405         $backend->lock();
406         $latestversion = $cache->get_latest_version($pagename);
407         if ($latestversion && $version == $latestversion) {
408             $backend->unlock();
409             trigger_error(sprintf("Attempt to delete most recent revision of '%s'",
410                                   $pagename), E_USER_ERROR);
411             return;
412         }
413
414         $cache->delete_versiondata($pagename, $version);
415                 
416         $backend->unlock();
417     }
418
419     /*
420      * Delete a revision, or possibly merge it with a previous
421      * revision.
422      *
423      * The idea is this:
424      * Suppose an author make a (major) edit to a page.  Shortly
425      * after that the same author makes a minor edit (e.g. to fix
426      * spelling mistakes he just made.)
427      *
428      * Now some time later, where cleaning out old saved revisions,
429      * and would like to delete his minor revision (since there's
430      * really no point in keeping minor revisions around for a long
431      * time.)
432      *
433      * Note that the text after the minor revision probably represents
434      * what the author intended to write better than the text after
435      * the preceding major edit.
436      *
437      * So what we really want to do is merge the minor edit with the
438      * preceding edit.
439      *
440      * We will only do this when:
441      * <ul>
442      * <li>The revision being deleted is a minor one, and
443      * <li>It has the same author as the immediately preceding revision.
444      * </ul>
445      */
446     function mergeRevision($version) {
447         $backend = &$this->_wikidb->_backend;
448         $cache = &$this->_wikidb->_cache;
449         $pagename = &$this->_pagename;
450
451         $version = $this->_coerce_to_version($version);
452         if ($version == 0)
453             return;
454
455         $backend->lock();
456         $latestversion = $backend->get_latest_version($pagename);
457         if ($latestversion && $version == $latestversion) {
458             $backend->unlock();
459             trigger_error(sprintf("Attempt to merge most recent revision of '%s'",
460                                   $pagename), E_USER_ERROR);
461             return;
462         }
463
464         $versiondata = $cache->get_versiondata($pagename, $version, true);
465         if (!$versiondata) {
466             // Not there? ... we're done!
467             $backend->unlock();
468             return;
469         }
470
471         if ($versiondata['is_minor_edit']) {
472             $previous = $backend->get_previous_version($pagename, $version);
473             if ($previous) {
474                 $prevdata = $cache->get_versiondata($pagename, $previous);
475                 if ($prevdata['author_id'] == $versiondata['author_id']) {
476                     // This is a minor revision, previous version is
477                     // by the same author. We will merge the
478                     // revisions.
479                     $cache->update_versiondata($pagename, $previous,
480                                                array('%content' => $versiondata['%content'],
481                                                      '_supplanted' => $versiondata['_supplanted']));
482                 }
483             }
484         }
485
486         $cache->delete_versiondata($pagename, $version);
487         $backend->unlock();
488     }
489
490     
491     /**
492      * Create a new revision of a {@link WikiDB_Page}.
493      *
494      * @access public
495      *
496      * @param int $version Version number for new revision.  
497      * To ensure proper serialization of edits, $version must be
498      * exactly one higher than the current latest version.
499      * (You can defeat this check by setting $version to
500      * {@link WIKIDB_FORCE_CREATE} --- not usually recommended.)
501      *
502      * @param string $content Contents of new revision.
503      *
504      * @param hash $metadata Metadata for new revision.
505      * All values in the hash should be scalars (strings or integers).
506      *
507      * @param array $links List of pagenames which this page links to.
508      *
509      * @return WikiDB_PageRevision  Returns the new WikiDB_PageRevision object. If
510      * $version was incorrect, returns false
511      */
512     function createRevision($version, &$content, $metadata, $links) {
513         $backend = &$this->_wikidb->_backend;
514         $cache = &$this->_wikidb->_cache;
515         $pagename = &$this->_pagename;
516                 
517         $backend->lock();
518
519         $latestversion = $backend->get_latest_version($pagename);
520         $newversion = $latestversion + 1;
521         assert($newversion >= 1);
522
523         if ($version != WIKIDB_FORCE_CREATE && $version != $newversion) {
524             $backend->unlock();
525             return false;
526         }
527
528         $data = $metadata;
529         
530         foreach ($data as $key => $val) {
531             if (empty($val) || $key[0] == '_' || $key[0] == '%')
532                 unset($data[$key]);
533         }
534                         
535         assert(!empty($data['author_id']));
536         if (empty($data['author_id']))
537             @$data['author_id'] = $data['author'];
538                 
539         if (empty($data['mtime']))
540             $data['mtime'] = time();
541
542         if ($latestversion) {
543             // Ensure mtimes are monotonic.
544             $pdata = $cache->get_versiondata($pagename, $latestversion);
545             if ($data['mtime'] < $pdata['mtime']) {
546                 trigger_error(sprintf(_("%s: Date of new revision is %s"),
547                                       $pagename,"'non-monotonic'"),
548                               E_USER_NOTICE);
549                 $data['orig_mtime'] = $data['mtime'];
550                 $data['mtime'] = $pdata['mtime'];
551             }
552             
553             // FIXME: use (possibly user specified) 'mtime' time or
554             // time()?
555             $cache->update_versiondata($pagename, $latestversion,
556                                        array('_supplanted' => $data['mtime']));
557         }
558
559         $data['%content'] = &$content;
560
561         $cache->set_versiondata($pagename, $newversion, $data);
562
563         //$cache->update_pagedata($pagename, array(':latestversion' => $newversion,
564         //':deleted' => empty($content)));
565         
566         $backend->set_links($pagename, $links);
567
568         $backend->unlock();
569
570         // FIXME: probably should have some global state information
571         // in the backend to control when to optimize.
572         if (time() % 50 == 0) {
573             trigger_error(sprintf(_("Optimizing %s"),'backend'), E_USER_NOTICE);
574             $backend->optimize();
575         }
576
577         return new WikiDB_PageRevision($this->_wikidb, $pagename, $newversion,
578                                        $data);
579     }
580
581     /**
582      * Get the most recent revision of a page.
583      *
584      * @access public
585      *
586      * @return WikiDB_PageRevision The current WikiDB_PageRevision object. 
587      */
588     function getCurrentRevision() {
589         $backend = &$this->_wikidb->_backend;
590         $cache = &$this->_wikidb->_cache;
591         $pagename = &$this->_pagename;
592
593         $backend->lock();
594         $version = $cache->get_latest_version($pagename);
595         $revision = $this->getRevision($version);
596         $backend->unlock();
597         assert($revision);
598         return $revision;
599     }
600
601     /**
602      * Get a specific revision of a WikiDB_Page.
603      *
604      * @access public
605      *
606      * @param integer $version  Which revision to get.
607      *
608      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or
609      * false if the requested revision does not exist in the {@link WikiDB}.
610      * Note that version zero of any page always exists.
611      */
612     function getRevision($version) {
613         $cache = &$this->_wikidb->_cache;
614         $pagename = &$this->_pagename;
615         
616         if ($version == 0)
617             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
618
619         assert($version > 0);
620         $vdata = $cache->get_versiondata($pagename, $version);
621         if (!$vdata)
622             return false;
623         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
624                                        $vdata);
625     }
626
627     /**
628      * Get previous page revision.
629      *
630      * This method find the most recent revision before a specified
631      * version.
632      *
633      * @access public
634      *
635      * @param integer $version  Find most recent revision before this version.
636      *  You can also use a WikiDB_PageRevision object to specify the $version.
637      *
638      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or false if the
639      * requested revision does not exist in the {@link WikiDB}.  Note that
640      * unless $version is greater than zero, a revision (perhaps version zero,
641      * the default revision) will always be found.
642      */
643     function getRevisionBefore($version) {
644         $backend = &$this->_wikidb->_backend;
645         $pagename = &$this->_pagename;
646
647         $version = $this->_coerce_to_version($version);
648
649         if ($version == 0)
650             return false;
651         $backend->lock();
652         $previous = $backend->get_previous_version($pagename, $version);
653         $revision = $this->getRevision($previous);
654         $backend->unlock();
655         assert($revision);
656         return $revision;
657     }
658
659     /**
660      * Get all revisions of the WikiDB_Page.
661      *
662      * This does not include the version zero (default) revision in the
663      * returned revision set.
664      *
665      * @return WikiDB_PageRevisionIterator A
666      * WikiDB_PageRevisionIterator containing all revisions of this
667      * WikiDB_Page in reverse order by version number.
668      */
669     function getAllRevisions() {
670         $backend = &$this->_wikidb->_backend;
671         $revs = $backend->get_all_revisions($this->_pagename);
672         return new WikiDB_PageRevisionIterator($this->_wikidb, $revs);
673     }
674     
675     /**
676      * Find pages which link to or are linked from a page.
677      *
678      * @access public
679      *
680      * @param boolean $reversed Which links to find: true for backlinks (default).
681      *
682      * @return WikiDB_PageIterator A WikiDB_PageIterator containing
683      * all matching pages.
684      */
685     function getLinks($reversed = true) {
686         $backend = &$this->_wikidb->_backend;
687         $result =  $backend->get_links($this->_pagename, $reversed);
688         return new WikiDB_PageIterator($this->_wikidb, $result);
689     }
690             
691     /**
692      * Access WikiDB_Page meta-data.
693      *
694      * @access public
695      *
696      * @param string $key Which meta data to get.
697      * Some reserved meta-data keys are:
698      * <dl>
699      * <dt>'locked'<dd> Is page locked?
700      * <dt>'hits'  <dd> Page hit counter.
701      * <dt>'pref'  <dd> Users preferences, stored in homepages.
702      * <dt>'owner' <dd> Default: first author_id. We might add a group with a dot here:
703      *                  E.g. "owner.users"
704      * <dt>'perm'  <dd> Permission flag to authorize read/write/execution of 
705      *                  page-headers and content.
706      * <dt>'score' <dd> Page score (not yet implement, do we need?)
707      * </dl>
708      *
709      * @return scalar The requested value, or false if the requested data
710      * is not set.
711      */
712     function get($key) {
713         $cache = &$this->_wikidb->_cache;
714         if (!$key || $key[0] == '%')
715             return false;
716         $data = $cache->get_pagedata($this->_pagename);
717         return isset($data[$key]) ? $data[$key] : false;
718     }
719
720     /**
721      * Get all the page meta-data as a hash.
722      *
723      * @return hash The page meta-data.
724      */
725     function getMetaData() {
726         $cache = &$this->_wikidb->_cache;
727         $data = $cache->get_pagedata($this->_pagename);
728         $meta = array();
729         foreach ($data as $key => $val) {
730             if (!empty($val) && $key[0] != '%')
731                 $meta[$key] = $val;
732         }
733         return $meta;
734     }
735
736     /**
737      * Set page meta-data.
738      *
739      * @see get
740      * @access public
741      *
742      * @param string $key  Meta-data key to set.
743      * @param string $newval  New value.
744      */
745     function set($key, $newval) {
746         $cache = &$this->_wikidb->_cache;
747         $pagename = &$this->_pagename;
748         
749         assert($key && $key[0] != '%');
750
751         $data = $cache->get_pagedata($pagename);
752
753         if (!empty($newval)) {
754             if (!empty($data[$key]) && $data[$key] == $newval)
755                 return;         // values identical, skip update.
756         }
757         else {
758             if (empty($data[$key]))
759                 return;         // values identical, skip update.
760         }
761
762         // special handling of sensitive pref data or upgrades from older versions:
763         if ($key == 'pref') {
764             if (!empty($newval['userid'])) unset($newval['userid']);
765             //if ($GLOBALS['user']->isAdmin()) unset($newval['passwd']);
766         }
767         $cache->update_pagedata($pagename, array($key => $newval));
768     }
769
770     /**
771      * Increase page hit count.
772      *
773      * FIXME: IS this needed?  Probably not.
774      *
775      * This is a convenience function.
776      * <pre> $page->increaseHitCount(); </pre>
777      * is functionally identical to
778      * <pre> $page->set('hits',$page->get('hits')+1); </pre>
779      *
780      * Note that this method may be implemented in more efficient ways
781      * in certain backends.
782      *
783      * @access public
784      */
785     function increaseHitCount() {
786         @$newhits = $this->get('hits') + 1;
787         $this->set('hits', $newhits);
788     }
789
790     /**
791      * Return a string representation of the WikiDB_Page
792      *
793      * This is really only for debugging.
794      *
795      * @access public
796      *
797      * @return string Printable representation of the WikiDB_Page.
798      */
799     function asString () {
800         ob_start();
801         printf("[%s:%s\n", get_class($this), $this->getName());
802         print_r($this->getMetaData());
803         echo "]\n";
804         $strval = ob_get_contents();
805         ob_end_clean();
806         return $strval;
807     }
808
809
810     /**
811      * @access private
812      * @param integer_or_object $version_or_pagerevision
813      * Takes either the version number (and int) or a WikiDB_PageRevision
814      * object.
815      * @return integer The version number.
816      */
817     function _coerce_to_version($version_or_pagerevision) {
818         if (method_exists($version_or_pagerevision, "getContent"))
819             $version = $version_or_pagerevision->getVersion();
820         else
821             $version = (int) $version_or_pagerevision;
822
823         assert($version >= 0);
824         return $version;
825     }
826
827     function isUserPage ($include_empty = true) {
828         return $this->get('pref') ? true : false;
829         if ($include_empty)
830             return true;
831         $current = $this->getCurrentRevision();
832         return ! $current->hasDefaultContents();
833     }
834
835 };
836
837 /**
838  * This class represents a specific revision of a WikiDB_Page within
839  * a WikiDB.
840  *
841  * A WikiDB_PageRevision has read-only semantics. You may only create
842  * new revisions (and delete old ones) --- you cannot modify existing
843  * revisions.
844  */
845 class WikiDB_PageRevision
846 {
847     function WikiDB_PageRevision(&$wikidb, $pagename, $version,
848                                  $versiondata = false)
849         {
850             $this->_wikidb = &$wikidb;
851             $this->_pagename = $pagename;
852             $this->_version = $version;
853             $this->_data = $versiondata ? $versiondata : array();
854         }
855     
856     /**
857      * Get the WikiDB_Page which this revision belongs to.
858      *
859      * @access public
860      *
861      * @return WikiDB_Page The WikiDB_Page which this revision belongs to.
862      */
863     function getPage() {
864         return new WikiDB_Page($this->_wikidb, $this->_pagename);
865     }
866
867     /**
868      * Get the version number of this revision.
869      *
870      * @access public
871      *
872      * @return integer The version number of this revision.
873      */
874     function getVersion() {
875         return $this->_version;
876     }
877     
878     /**
879      * Determine whether this revision has defaulted content.
880      *
881      * The default revision (version 0) of each page, as well as any
882      * pages which are created with empty content have their content
883      * defaulted to something like:
884      * <pre>
885      *   Describe [ThisPage] here.
886      * </pre>
887      *
888      * @access public
889      *
890      * @return boolean Returns true if the page has default content.
891      */
892     function hasDefaultContents() {
893         $data = &$this->_data;
894         return empty($data['%content']);
895     }
896
897     /**
898      * Get the content as an array of lines.
899      *
900      * @access public
901      *
902      * @return array An array of lines.
903      * The lines should contain no trailing white space.
904      */
905     function getContent() {
906         return explode("\n", $this->getPackedContent());
907     }
908         
909         /**
910      * Get the pagename of the revision.
911      *
912      * @access public
913      *
914      * @return string pagename.
915      */
916     function getPageName() {
917         return $this->_pagename;
918     }
919
920     /**
921      * Determine whether revision is the latest.
922      *
923      * @access public
924      *
925      * @return boolean True iff the revision is the latest (most recent) one.
926      */
927     function isCurrent() {
928         if (!isset($this->_iscurrent)) {
929             $page = $this->getPage();
930             $current = $page->getCurrentRevision();
931             $this->_iscurrent = $this->getVersion() == $current->getVersion();
932         }
933         return $this->_iscurrent;
934     }
935     
936     /**
937      * Get the content as a string.
938      *
939      * @access public
940      *
941      * @return string The page content.
942      * Lines are separated by new-lines.
943      */
944     function getPackedContent() {
945         $data = &$this->_data;
946
947         
948         if (empty($data['%content'])) {
949             // Replace empty content with default value.
950             return sprintf(_("Describe %s here."),
951                            "[". $this->_pagename ."]");
952         }
953
954         // There is (non-default) content.
955         assert($this->_version > 0);
956         
957         if (!is_string($data['%content'])) {
958             // Content was not provided to us at init time.
959             // (This is allowed because for some backends, fetching
960             // the content may be expensive, and often is not wanted
961             // by the user.)
962             //
963             // In any case, now we need to get it.
964             $data['%content'] = $this->_get_content();
965             assert(is_string($data['%content']));
966         }
967         
968         return $data['%content'];
969     }
970
971     function _get_content() {
972         $cache = &$this->_wikidb->_cache;
973         $pagename = $this->_pagename;
974         $version = $this->_version;
975
976         assert($version > 0);
977         
978         $newdata = $cache->get_versiondata($pagename, $version, true);
979         if ($newdata) {
980             assert(is_string($newdata['%content']));
981             return $newdata['%content'];
982         }
983         else {
984             // else revision has been deleted... What to do?
985             return __sprintf("Acck! Revision %s of %s seems to have been deleted!",
986                              $version, $pagename);
987         }
988     }
989
990     /**
991      * Get meta-data for this revision.
992      *
993      *
994      * @access public
995      *
996      * @param string $key Which meta-data to access.
997      *
998      * Some reserved revision meta-data keys are:
999      * <dl>
1000      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
1001      *        The 'mtime' meta-value is normally set automatically by the database
1002      *        backend, but it may be specified explicitly when creating a new revision.
1003      * <dt> orig_mtime
1004      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
1005      *       of a page must be monotonically increasing.  If an attempt is
1006      *       made to create a new revision with an mtime less than that of
1007      *       the preceeding revision, the new revisions timestamp is force
1008      *       to be equal to that of the preceeding revision.  In that case,
1009      *       the originally requested mtime is preserved in 'orig_mtime'.
1010      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
1011      *        This meta-value is <em>always</em> automatically maintained by the database
1012      *        backend.  (It is set from the 'mtime' meta-value of the superceding
1013      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
1014      *
1015      * FIXME: this could be refactored:
1016      * <dt> author
1017      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
1018      * <dt> author_id
1019      *  <dd> Authenticated author of a page.  This is used to identify
1020      *       the distinctness of authors when cleaning old revisions from
1021      *       the database.
1022      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
1023      * <dt> 'summary' <dd> Short change summary entered by page author.
1024      * </dl>
1025      *
1026      * Meta-data keys must be valid C identifers (they have to start with a letter
1027      * or underscore, and can contain only alphanumerics and underscores.)
1028      *
1029      * @return string The requested value, or false if the requested value
1030      * is not defined.
1031      */
1032     function get($key) {
1033         if (!$key || $key[0] == '%')
1034             return false;
1035         $data = &$this->_data;
1036         return isset($data[$key]) ? $data[$key] : false;
1037     }
1038
1039     /**
1040      * Get all the revision page meta-data as a hash.
1041      *
1042      * @return hash The revision meta-data.
1043      */
1044     function getMetaData() {
1045         $meta = array();
1046         foreach ($this->_data as $key => $val) {
1047             if (!empty($val) && $key[0] != '%')
1048                 $meta[$key] = $val;
1049         }
1050         return $meta;
1051     }
1052     
1053             
1054     /**
1055      * Return a string representation of the revision.
1056      *
1057      * This is really only for debugging.
1058      *
1059      * @access public
1060      *
1061      * @return string Printable representation of the WikiDB_Page.
1062      */
1063     function asString () {
1064         ob_start();
1065         printf("[%s:%d\n", get_class($this), $this->get('version'));
1066         print_r($this->_data);
1067         echo $this->getPackedContent() . "\n]\n";
1068         $strval = ob_get_contents();
1069         ob_end_clean();
1070         return $strval;
1071     }
1072 };
1073
1074
1075 /**
1076  * A class which represents a sequence of WikiDB_Pages.
1077  */
1078 class WikiDB_PageIterator
1079 {
1080     function WikiDB_PageIterator(&$wikidb, &$pages) {
1081         $this->_pages = $pages;
1082         $this->_wikidb = &$wikidb;
1083     }
1084     
1085     /**
1086      * Get next WikiDB_Page in sequence.
1087      *
1088      * @access public
1089      *
1090      * @return WikiDB_Page The next WikiDB_Page in the sequence.
1091      */
1092     function next () {
1093         if ( ! ($next = $this->_pages->next()) )
1094             return false;
1095
1096         $pagename = &$next['pagename'];
1097         if (isset($next['pagedata']))
1098             $this->_wikidb->_cache->cache_data($next);
1099
1100         return new WikiDB_Page($this->_wikidb, $pagename);
1101     }
1102
1103     /**
1104      * Release resources held by this iterator.
1105      *
1106      * The iterator may not be used after free() is called.
1107      *
1108      * There is no need to call free(), if next() has returned false.
1109      * (I.e. if you iterate through all the pages in the sequence,
1110      * you do not need to call free() --- you only need to call it
1111      * if you stop before the end of the iterator is reached.)
1112      *
1113      * @access public
1114      */
1115     function free() {
1116         $this->_pages->free();
1117     }
1118
1119     // Not yet used.
1120     function setSortby ($arg = false) {
1121         if (!$arg) {
1122             $arg = @$_GET['sortby'];
1123             if ($arg) {
1124                 $sortby = substr($arg,1);
1125                 $order  = substr($arg,0,1)=='+' ? 'ASC' : 'DESC';
1126             }
1127         }
1128         if (is_array($arg)) { // array('mtime' => 'desc')
1129             $sortby = $arg[0];
1130             $order = $arg[1];
1131         } else {
1132             $sortby = $arg;
1133             $order  = 'ASC';
1134         }
1135         // available column types to sort by:
1136         // todo: we must provide access methods for the generic dumb/iterator
1137         $this->_types = explode(',','pagename,mtime,hits,version,author,locked,minor,markup');
1138         if (in_array($sortby,$this->_types))
1139             $this->_options['sortby'] = $sortby;
1140         else
1141             trigger_error(fmt("Argument %s '%s' ignored",'sortby',$sortby), E_USER_WARNING);
1142         if (in_array(strtoupper($order),'ASC','DESC')) 
1143             $this->_options['order'] = strtoupper($order);
1144         else
1145             trigger_error(fmt("Argument %s '%s' ignored",'order',$order), E_USER_WARNING);
1146     }
1147
1148 };
1149
1150 /**
1151  * A class which represents a sequence of WikiDB_PageRevisions.
1152  */
1153 class WikiDB_PageRevisionIterator
1154 {
1155     function WikiDB_PageRevisionIterator(&$wikidb, &$revisions) {
1156         $this->_revisions = $revisions;
1157         $this->_wikidb = &$wikidb;
1158     }
1159     
1160     /**
1161      * Get next WikiDB_PageRevision in sequence.
1162      *
1163      * @access public
1164      *
1165      * @return WikiDB_PageRevision
1166      * The next WikiDB_PageRevision in the sequence.
1167      */
1168     function next () {
1169         if ( ! ($next = $this->_revisions->next()) )
1170             return false;
1171
1172         $this->_wikidb->_cache->cache_data($next);
1173
1174         $pagename = $next['pagename'];
1175         $version = $next['version'];
1176         $versiondata = $next['versiondata'];
1177         assert(!empty($pagename));
1178         assert(is_array($versiondata));
1179         assert($version > 0);
1180
1181         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
1182                                        $versiondata);
1183     }
1184
1185     /**
1186      * Release resources held by this iterator.
1187      *
1188      * The iterator may not be used after free() is called.
1189      *
1190      * There is no need to call free(), if next() has returned false.
1191      * (I.e. if you iterate through all the revisions in the sequence,
1192      * you do not need to call free() --- you only need to call it
1193      * if you stop before the end of the iterator is reached.)
1194      *
1195      * @access public
1196      */
1197     function free() { 
1198         $this->_revisions->free();
1199     }
1200 };
1201
1202
1203 /**
1204  * Data cache used by WikiDB.
1205  *
1206  * FIXME: Maybe rename this to caching_backend (or some such).
1207  *
1208  * @access private
1209  */
1210 class WikiDB_cache 
1211 {
1212     // FIXME: beautify versiondata cache.  Cache only limited data?
1213
1214     function WikiDB_cache (&$backend) {
1215         $this->_backend = &$backend;
1216
1217         $this->_pagedata_cache = array();
1218         $this->_versiondata_cache = array();
1219         array_push ($this->_versiondata_cache, array());
1220         $this->_glv_cache = array();
1221     }
1222     
1223     function close() {
1224         $this->_pagedata_cache = false;
1225                 $this->_versiondata_cache = false;
1226                 $this->_glv_cache = false;
1227     }
1228
1229     function get_pagedata($pagename) {
1230         assert(is_string($pagename) && $pagename);
1231         $cache = &$this->_pagedata_cache;
1232
1233         if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
1234             $cache[$pagename] = $this->_backend->get_pagedata($pagename);
1235             if (empty($cache[$pagename]))
1236                 $cache[$pagename] = array();
1237         }
1238
1239         return $cache[$pagename];
1240     }
1241     
1242     function update_pagedata($pagename, $newdata) {
1243         assert(is_string($pagename) && $pagename);
1244
1245         $this->_backend->update_pagedata($pagename, $newdata);
1246
1247         if (is_array($this->_pagedata_cache[$pagename])) {
1248             $cachedata = &$this->_pagedata_cache[$pagename];
1249             foreach($newdata as $key => $val)
1250                 $cachedata[$key] = $val;
1251         }
1252     }
1253
1254     function invalidate_cache($pagename) {
1255         unset ($this->_pagedata_cache[$pagename]);
1256                 unset ($this->_versiondata_cache[$pagename]);
1257                 unset ($this->_glv_cache[$pagename]);
1258     }
1259     
1260     function delete_page($pagename) {
1261         $this->_backend->delete_page($pagename);
1262         unset ($this->_pagedata_cache[$pagename]);
1263                 unset ($this->_glv_cache[$pagename]);
1264     }
1265
1266     // FIXME: ugly
1267     function cache_data($data) {
1268         if (isset($data['pagedata']))
1269             $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
1270     }
1271     
1272     function get_versiondata($pagename, $version, $need_content = false) {
1273                 //  FIXME: Seriously ugly hackage
1274         if (defined ('USECACHE')){   //temporary - for debugging
1275         assert(is_string($pagename) && $pagename);
1276                 // there is a bug here somewhere which results in an assertion failure at line 105
1277                 // of ArchiveCleaner.php  It goes away if we use the next line.
1278                 $need_content = true;
1279                 $nc = $need_content ? '1':'0';
1280         $cache = &$this->_versiondata_cache;
1281         if (!isset($cache[$pagename][$version][$nc])||
1282                                 !(is_array ($cache[$pagename])) || !(is_array ($cache[$pagename][$version]))) {
1283             $cache[$pagename][$version][$nc] = 
1284                                 $this->_backend->get_versiondata($pagename,$version, $need_content);
1285                         // If we have retrieved all data, we may as well set the cache for $need_content = false
1286                         if($need_content){
1287                                 $cache[$pagename][$version]['0'] = $cache[$pagename][$version]['1'];
1288                         }
1289                 }
1290         $vdata = $cache[$pagename][$version][$nc];
1291         }
1292         else
1293         {
1294     $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
1295         }
1296         // FIXME: ugly
1297         if ($vdata && !empty($vdata['%pagedata']))
1298             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
1299         return $vdata;
1300     }
1301
1302     function set_versiondata($pagename, $version, $data) {
1303         $new = $this->_backend->
1304              set_versiondata($pagename, $version, $data);
1305                 // Update the cache
1306                 $this->_versiondata_cache[$pagename][$version]['1'] = $data;
1307                 // FIXME: hack
1308                 $this->_versiondata_cache[$pagename][$version]['0'] = $data;
1309                 // Is this necessary?
1310                 unset($this->_glv_cache[$pagename]);
1311                 
1312     }
1313
1314     function update_versiondata($pagename, $version, $data) {
1315         $new = $this->_backend->
1316              update_versiondata($pagename, $version, $data);
1317                 // Update the cache
1318                 $this->_versiondata_cache[$pagename][$version]['1'] = $data;
1319                 // FIXME: hack
1320                 $this->_versiondata_cache[$pagename][$version]['0'] = $data;
1321                 // Is this necessary?
1322                 unset($this->_glv_cache[$pagename]);
1323
1324     }
1325
1326     function delete_versiondata($pagename, $version) {
1327         $new = $this->_backend->
1328             delete_versiondata($pagename, $version);
1329         unset ($this->_versiondata_cache[$pagename][$version]['1']);
1330         unset ($this->_versiondata_cache[$pagename][$version]['0']);
1331         unset ($this->_glv_cache[$pagename]);
1332     }
1333         
1334     function get_latest_version($pagename)  {
1335         if(defined('USECACHE')){
1336             assert (is_string($pagename) && $pagename);
1337             $cache = &$this->_glv_cache;        
1338             if (!isset($cache[$pagename])) {
1339                 $cache[$pagename] = $this->_backend->get_latest_version($pagename);
1340                 if (empty($cache[$pagename]))
1341                     $cache[$pagename] = 0;
1342             } 
1343             return $cache[$pagename];}
1344         else {
1345             return $this->_backend->get_latest_version($pagename); 
1346         }
1347     }
1348
1349 };
1350
1351 /**
1352  * FIXME! Class for externally authenticated users.
1353  *
1354  * We might have read-only access to the password and/or group membership,
1355  * or we might even be able to update the entries.
1356  *
1357  * FIXME: This was written before we stored prefs as %pagedata, so
1358  *
1359  * FIXME: I believe this is not currently used.
1360  */
1361 //  class WikiDB_User
1362 //  extends WikiUser
1363 //  {
1364 //      var $_authdb;
1365
1366 //      function WikiDB_User($userid, $authlevel = false) {
1367 //          global $request;
1368 //          $this->_authdb = new WikiAuthDB($GLOBALS['DBAuthParams']);
1369 //          $this->_authmethod = 'AuthDB';
1370 //          WikiUser::WikiUser($request, $userid, $authlevel);
1371 //      }
1372
1373 //      /*
1374 //      function getPreferences() {
1375 //          // external prefs override internal ones?
1376 //          if (! $this->_authdb->getPrefs() )
1377 //              if ($pref = WikiUser::getPreferences())
1378 //                  return $prefs;
1379 //          return false;
1380 //      }
1381
1382 //      function setPreferences($prefs) {
1383 //          if (! $this->_authdb->setPrefs($prefs) )
1384 //              return WikiUser::setPreferences();
1385 //      }
1386 //      */
1387
1388 //      function exists() {
1389 //          return $this->_authdb->exists($this->_userid);
1390 //      }
1391
1392 //      // create user and default user homepage
1393 //      function createUser ($pref) {
1394 //          if ($this->exists()) return;
1395 //          if (! $this->_authdb->createUser($pref)) {
1396 //              // external auth doesn't allow this.
1397 //              // do our policies allow local users instead?
1398 //              return WikiUser::createUser($pref);
1399 //          }
1400 //      }
1401
1402 //      function checkPassword($passwd) {
1403 //          return $this->_authdb->pwcheck($this->userid, $passwd);
1404 //      }
1405
1406 //      function changePassword($passwd) {
1407 //          if (! $this->mayChangePassword() ) {
1408 //              trigger_error(sprintf("Attempt to change an external password for '%s'",
1409 //                                    $this->_userid), E_USER_ERROR);
1410 //              return;
1411 //          }
1412 //          return $this->_authdb->changePass($this->userid, $passwd);
1413 //      }
1414
1415 //      function mayChangePassword() {
1416 //          return $this->_authdb->auth_update;
1417 //      }
1418 //  }
1419
1420 /*
1421  * FIXME: I believe this is not currently used.
1422  */
1423 //  class WikiAuthDB
1424 //  extends WikiDB
1425 //  {
1426 //      var $auth_dsn = false, $auth_check = false;
1427 //      var $auth_crypt_method = 'crypt', $auth_update = false;
1428 //      var $group_members = false, $user_groups = false;
1429 //      var $pref_update = false, $pref_select = false;
1430 //      var $_dbh;
1431
1432 //      function WikiAuthDB($DBAuthParams) {
1433 //          foreach ($DBAuthParams as $key => $value) {
1434 //              $this->$key = $value;
1435 //          }
1436 //          if (!$this->auth_dsn) {
1437 //              trigger_error(_("no \$DBAuthParams['dsn'] provided"), E_USER_ERROR);
1438 //              return false;
1439 //          }
1440 //          // compare auth DB to the existing page DB. reuse if it's on the same database.
1441 //          if (isa($this->_backend, 'WikiDB_backend_PearDB') and 
1442 //              $this->_backend->_dsn == $this->auth_dsn) {
1443 //              $this->_dbh = &$this->_backend->_dbh;
1444 //              return $this->_backend;
1445 //          }
1446 //          include_once("lib/WikiDB/SQL.php");
1447 //          return new WikiDB_SQL($DBAuthParams);
1448 //      }
1449
1450 //      function param_missing ($param) {
1451 //          trigger_error(sprintf(_("No \$DBAuthParams['%s'] provided."), $param), E_USER_ERROR);
1452 //          return;
1453 //      }
1454
1455 //      function getPrefs($prefs) {
1456 //          if ($this->pref_select) {
1457 //              $statement = $this->_backend->Prepare($this->pref_select);
1458 //              return unserialize($this->_backend->Execute($statement, 
1459 //                                                          $prefs->get('userid')));
1460 //          } else {
1461 //              param_missing('pref_select');
1462 //              return false;
1463 //          }
1464 //      }
1465
1466 //      function setPrefs($prefs) {
1467 //          if ($this->pref_write) {
1468 //              $statement = $this->_backend->Prepare($this->pref_write);
1469 //              return $this->_backend->Execute($statement, 
1470 //                                              $prefs->get('userid'), serialize($prefs->_prefs));
1471 //          } else {
1472 //              param_missing('pref_write');
1473 //              return false;
1474 //          }
1475 //      }
1476
1477 //      function createUser ($pref) {
1478 //          if ($this->user_create) {
1479 //              $statement = $this->_backend->Prepare($this->user_create);
1480 //              return $this->_backend->Execute($statement, 
1481 //                                          $prefs->get('userid'), serialize($prefs->_prefs));
1482 //          } else {
1483 //              param_missing('user_create');
1484 //              return false;
1485 //          }
1486 //      }
1487
1488 //      function exists($userid) {
1489 //          if ($this->user_check) {
1490 //              $statement = $this->_backend->Prepare($this->user_check);
1491 //              return $this->_backend->Execute($statement, $prefs->get('userid'));
1492 //          } else {
1493 //              param_missing('user_check');
1494 //              return false;
1495 //          }
1496 //      }
1497
1498 //      function pwcheck($userid, $pass) {
1499 //          if ($this->auth_check) {
1500 //              $statement = $this->_backend->Prepare($this->auth_check);
1501 //              return $this->_backend->Execute($statement, $userid, $pass);
1502 //          } else {
1503 //              param_missing('auth_check');
1504 //              return false;
1505 //          }
1506 //      }
1507 //  }
1508
1509 // Local Variables:
1510 // mode: php
1511 // tab-width: 8
1512 // c-basic-offset: 4
1513 // c-hanging-comment-ender-p: nil
1514 // indent-tabs-mode: nil
1515 // End:   
1516 ?>