]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
improve stability, trying to find the InlineParser endless loop on sf.net
[SourceForge/phpwiki.git] / lib / WikiDB.php
1 <?php //-*-php-*-
2 rcs_id('$Id: WikiDB.php,v 1.52 2004-05-06 19:26:16 rurban Exp $');
3
4 require_once('lib/stdlib.php');
5 require_once('lib/PageType.php');
6
7 //FIXME: arg on get*Revision to hint that content is wanted.
8
9 /**
10  * The classes in the file define the interface to the
11  * page database.
12  *
13  * @package WikiDB
14  * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
15  */
16
17 /**
18  * Force the creation of a new revision.
19  * @see WikiDB_Page::createRevision()
20  */
21 define('WIKIDB_FORCE_CREATE', -1);
22
23 // FIXME:  used for debugging only.  Comment out if cache does not work
24 define('USECACHE', 1);
25
26 /** 
27  * Abstract base class for the database used by PhpWiki.
28  *
29  * A <tt>WikiDB</tt> is a container for <tt>WikiDB_Page</tt>s which in
30  * turn contain <tt>WikiDB_PageRevision</tt>s.
31  *
32  * Conceptually a <tt>WikiDB</tt> contains all possible
33  * <tt>WikiDB_Page</tt>s, whether they have been initialized or not.
34  * Since all possible pages are already contained in a WikiDB, a call
35  * to WikiDB::getPage() will never fail (barring bugs and
36  * e.g. filesystem or SQL database problems.)
37  *
38  * Also each <tt>WikiDB_Page</tt> always contains at least one
39  * <tt>WikiDB_PageRevision</tt>: the default content (e.g. "Describe
40  * [PageName] here.").  This default content has a version number of
41  * zero.
42  *
43  * <tt>WikiDB_PageRevision</tt>s have read-only semantics. One can
44  * only create new revisions or delete old ones --- one can not modify
45  * an existing revision.
46  */
47 class WikiDB {
48     /**
49      * Open a WikiDB database.
50      *
51      * This is a static member function. This function inspects its
52      * arguments to determine the proper subclass of WikiDB to
53      * instantiate, and then it instantiates it.
54      *
55      * @access public
56      *
57      * @param hash $dbparams Database configuration parameters.
58      * Some pertinent paramters are:
59      * <dl>
60      * <dt> dbtype
61      * <dd> The back-end type.  Current supported types are:
62      *   <dl>
63      *   <dt> SQL
64      *   <dd> Generic SQL backend based on the PEAR/DB database abstraction
65      *       library.
66      *   <dt> dba
67      *   <dd> Dba based backend.
68      *   </dl>
69      *
70      * <dt> dsn
71      * <dd> (Used by the SQL backend.)
72      *      The DSN specifying which database to connect to.
73      *
74      * <dt> prefix
75      * <dd> Prefix to be prepended to database table (and file names).
76      *
77      * <dt> directory
78      * <dd> (Used by the dba backend.)
79      *      Which directory db files reside in.
80      *
81      * <dt> timeout
82      * <dd> (Used by the dba backend.)
83      *      Timeout in seconds for opening (and obtaining lock) on the
84      *      db files.
85      *
86      * <dt> dba_handler
87      * <dd> (Used by the dba backend.)
88      *
89      *      Which dba handler to use. Good choices are probably either
90      *      'gdbm' or 'db2'.
91      * </dl>
92      *
93      * @return WikiDB A WikiDB object.
94      **/
95     function open ($dbparams) {
96         $dbtype = $dbparams{'dbtype'};
97         include_once("lib/WikiDB/$dbtype.php");
98                                 
99         $class = 'WikiDB_' . $dbtype;
100         return new $class ($dbparams);
101     }
102
103
104     /**
105      * Constructor.
106      *
107      * @access private
108      * @see open()
109      */
110     function WikiDB (&$backend, $dbparams) {
111         $this->_backend = &$backend;
112         $this->_cache = new WikiDB_cache($backend);
113
114         // If the database doesn't yet have a timestamp, initialize it now.
115         if ($this->get('_timestamp') === false)
116             $this->touch();
117         
118         //FIXME: devel checking.
119         //$this->_backend->check();
120     }
121     
122     /**
123      * Get any user-level warnings about this WikiDB.
124      *
125      * Some back-ends, e.g. by default create there data files in the
126      * global /tmp directory. We would like to warn the user when this
127      * happens (since /tmp files tend to get wiped periodically.)
128      * Warnings such as these may be communicated from specific
129      * back-ends through this method.
130      *
131      * @access public
132      *
133      * @return string A warning message (or <tt>false</tt> if there is
134      * none.)
135      */
136     function genericWarnings() {
137         return false;
138     }
139      
140     /**
141      * Close database connection.
142      *
143      * The database may no longer be used after it is closed.
144      *
145      * Closing a WikiDB invalidates all <tt>WikiDB_Page</tt>s,
146      * <tt>WikiDB_PageRevision</tt>s and <tt>WikiDB_PageIterator</tt>s
147      * which have been obtained from it.
148      *
149      * @access public
150      */
151     function close () {
152         $this->_backend->close();
153         $this->_cache->close();
154     }
155     
156     /**
157      * Get a WikiDB_Page from a WikiDB.
158      *
159      * A {@link WikiDB} consists of the (infinite) set of all possible pages,
160      * therefore this method never fails.
161      *
162      * @access public
163      * @param string $pagename Which page to get.
164      * @return WikiDB_Page The requested WikiDB_Page.
165      */
166     function getPage($pagename) {
167         static $error_displayed = false;
168         if (DEBUG) {
169             if (!(is_string($pagename) and $pagename != '')) {
170                 if ($error_displayed) return false;
171                 $error_displayed = true;
172                 trigger_error("empty pagename",E_USER_WARNING);
173                 return false;
174             }
175         } else 
176             assert(is_string($pagename) and $pagename != '');
177         return new WikiDB_Page($this, $pagename);
178     }
179
180     /**
181      * Determine whether page exists (in non-default form).
182      *
183      * <pre>
184      *   $is_page = $dbi->isWikiPage($pagename);
185      * </pre>
186      * is equivalent to
187      * <pre>
188      *   $page = $dbi->getPage($pagename);
189      *   $current = $page->getCurrentRevision();
190      *   $is_page = ! $current->hasDefaultContents();
191      * </pre>
192      * however isWikiPage may be implemented in a more efficient
193      * manner in certain back-ends.
194      *
195      * @access public
196      *
197      * @param string $pagename string Which page to check.
198      *
199      * @return boolean True if the page actually exists with
200      * non-default contents in the WikiDataBase.
201      */
202     function isWikiPage ($pagename) {
203         $page = $this->getPage($pagename);
204         $current = $page->getCurrentRevision();
205         return ! $current->hasDefaultContents();
206     }
207
208     /**
209      * Delete page from the WikiDB. 
210      *
211      * Deletes all revisions of the page from the WikiDB. Also resets
212      * all page meta-data to the default values.
213      *
214      * @access public
215      *
216      * @param string $pagename Name of page to delete.
217      */
218     function deletePage($pagename) {
219         $this->_cache->delete_page($pagename);
220
221         //How to create a RecentChanges entry with explaining summary?
222         /*
223         $page = $this->getPage($pagename);
224         $current = $page->getCurrentRevision();
225         $meta = $current->_data;
226         $version = $current->getVersion();
227         $meta['summary'] = _("removed");
228         $page->save($current->getPackedContent(), $version + 1, $meta);
229         */
230     }
231
232     /**
233      * Retrieve all pages.
234      *
235      * Gets the set of all pages with non-default contents.
236      *
237      * FIXME: do we need this?  I think so.  The simple searches
238      *        need this stuff.
239      *
240      * @access public
241      *
242      * @param boolean $include_defaulted Normally pages whose most
243      * recent revision has empty content are considered to be
244      * non-existant. Unless $include_defaulted is set to true, those
245      * pages will not be returned.
246      *
247      * @return WikiDB_PageIterator A WikiDB_PageIterator which contains all pages
248      *     in the WikiDB which have non-default contents.
249      */
250     function getAllPages($include_defaulted=false, $sortby=false, $limit=false) {
251         $result = $this->_backend->get_all_pages($include_defaulted,$sortby,$limit);
252         return new WikiDB_PageIterator($this, $result);
253     }
254
255     // Do we need this?
256     //function nPages() { 
257     //}
258     // Yes, for paging. Renamed.
259     function numPages($filter=false, $exclude='') {
260         if (method_exists($this->_backend,'numPages'))
261             $count = $this->_backend->numPages($filter,$exclude);
262         else {
263             $iter = $this->getAllPages();
264             $count = $iter->count();
265         }
266         return (int)$count;
267     }
268     
269     /**
270      * Title search.
271      *
272      * Search for pages containing (or not containing) certain words
273      * in their names.
274      *
275      * Pages are returned in alphabetical order whenever it is
276      * practical to do so.
277      *
278      * FIXME: should titleSearch and fullSearch be combined?  I think so.
279      *
280      * @access public
281      * @param TextSearchQuery $search A TextSearchQuery object
282      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
283      * @see TextSearchQuery
284      */
285     function titleSearch($search) {
286         $result = $this->_backend->text_search($search);
287         return new WikiDB_PageIterator($this, $result);
288     }
289
290     /**
291      * Full text search.
292      *
293      * Search for pages containing (or not containing) certain words
294      * in their entire text (this includes the page content and the
295      * page name).
296      *
297      * Pages are returned in alphabetical order whenever it is
298      * practical to do so.
299      *
300      * @access public
301      *
302      * @param TextSearchQuery $search A TextSearchQuery object.
303      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
304      * @see TextSearchQuery
305      */
306     function fullSearch($search) {
307         $result = $this->_backend->text_search($search, 'full_text');
308         return new WikiDB_PageIterator($this, $result);
309     }
310
311     /**
312      * Find the pages with the greatest hit counts.
313      *
314      * Pages are returned in reverse order by hit count.
315      *
316      * @access public
317      *
318      * @param integer $limit The maximum number of pages to return.
319      * Set $limit to zero to return all pages.  If $limit < 0, pages will
320      * be sorted in decreasing order of popularity.
321      *
322      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching
323      * pages.
324      */
325     function mostPopular($limit = 20, $sortby = '') {
326         // we don't support sortby=mtime here
327         if (strstr($sortby,'mtime'))
328             $sortby = '';
329         $result = $this->_backend->most_popular($limit, $sortby);
330         return new WikiDB_PageIterator($this, $result);
331     }
332
333     /**
334      * Find recent page revisions.
335      *
336      * Revisions are returned in reverse order by creation time.
337      *
338      * @access public
339      *
340      * @param hash $params This hash is used to specify various optional
341      *   parameters:
342      * <dl>
343      * <dt> limit 
344      *    <dd> (integer) At most this many revisions will be returned.
345      * <dt> since
346      *    <dd> (integer) Only revisions since this time (unix-timestamp) will be returned. 
347      * <dt> include_minor_revisions
348      *    <dd> (boolean) Also include minor revisions.  (Default is not to.)
349      * <dt> exclude_major_revisions
350      *    <dd> (boolean) Don't include non-minor revisions.
351      *         (Exclude_major_revisions implies include_minor_revisions.)
352      * <dt> include_all_revisions
353      *    <dd> (boolean) Return all matching revisions for each page.
354      *         Normally only the most recent matching revision is returned
355      *         for each page.
356      * </dl>
357      *
358      * @return WikiDB_PageRevisionIterator A WikiDB_PageRevisionIterator containing the
359      * matching revisions.
360      */
361     function mostRecent($params = false) {
362         $result = $this->_backend->most_recent($params);
363         return new WikiDB_PageRevisionIterator($this, $result);
364     }
365
366     /**
367      * Call the appropriate backend method.
368      *
369      * @access public
370      * @param string $from Page to rename
371      * @param string $to   New name
372      * @param boolean $updateWikiLinks If the text in all pages should be replaced.
373      * @return boolean     true or false
374      */
375     function renamePage($from, $to, $updateWikiLinks = false) {
376         assert(is_string($from) && $from != '');
377         assert(is_string($to) && $to != '');
378         $result = false;
379         if (method_exists($this->_backend,'rename_page')) {
380             $oldpage = $this->getPage($from);
381             $newpage = $this->getPage($to);
382             if ($oldpage->exists() and ! $newpage->exists()) {
383                 if ($result = $this->_backend->rename_page($from, $to)) {
384                     //update all WikiLinks in existing pages
385                     if ($updateWikiLinks) {
386                         //trigger_error(_("WikiDB::renamePage(..,..,updateWikiLinks) not yet implemented"),E_USER_WARNING);
387                         require_once('lib/plugin/WikiAdminSearchReplace.php');
388                         $links = $oldpage->getLinks();
389                         while ($linked_page = $links->next()) {
390                             WikiPlugin_WikiAdminSearchReplace::replaceHelper($this,$linked_page->getName(),$from,$to);
391                         }
392                         $links = $newpage->getLinks();
393                         while ($linked_page = $links->next()) {
394                             WikiPlugin_WikiAdminSearchReplace::replaceHelper($this,$linked_page->getName(),$from,$to);
395                         }
396                     }
397                     //create a RecentChanges entry with explaining summary
398                     $page = $this->getPage($to);
399                     $current = $page->getCurrentRevision();
400                     $meta = $current->_data;
401                     $version = $current->getVersion();
402                     $meta['summary'] = sprintf(_("renamed from %s"),$from);
403                     $page->save($current->getPackedContent(), $version + 1, $meta);
404                 }
405             }
406         } else {
407             trigger_error(_("WikiDB::renamePage() not yet implemented for this backend"),E_USER_WARNING);
408         }
409         return $result;
410     }
411
412     /** Get timestamp when database was last modified.
413      *
414      * @return string A string consisting of two integers,
415      * separated by a space.  The first is the time in
416      * unix timestamp format, the second is a modification
417      * count for the database.
418      *
419      * The idea is that you can cast the return value to an
420      * int to get a timestamp, or you can use the string value
421      * as a good hash for the entire database.
422      */
423     function getTimestamp() {
424         $ts = $this->get('_timestamp');
425         return sprintf("%d %d", $ts[0], $ts[1]);
426     }
427     
428     /**
429      * Update the database timestamp.
430      *
431      */
432     function touch() {
433         $ts = $this->get('_timestamp');
434         $this->set('_timestamp', array(time(), $ts[1] + 1));
435     }
436
437         
438     /**
439      * Access WikiDB global meta-data.
440      *
441      * NOTE: this is currently implemented in a hackish and
442      * not very efficient manner.
443      *
444      * @access public
445      *
446      * @param string $key Which meta data to get.
447      * Some reserved meta-data keys are:
448      * <dl>
449      * <dt>'_timestamp' <dd> Data used by getTimestamp().
450      * </dl>
451      *
452      * @return scalar The requested value, or false if the requested data
453      * is not set.
454      */
455     function get($key) {
456         if (!$key || $key[0] == '%')
457             return false;
458         /*
459          * Hack Alert: We can use any page (existing or not) to store
460          * this data (as long as we always use the same one.)
461          */
462         $gd = $this->getPage('global_data');
463         $data = $gd->get('__global');
464
465         if ($data && isset($data[$key]))
466             return $data[$key];
467         else
468             return false;
469     }
470
471     /**
472      * Set global meta-data.
473      *
474      * NOTE: this is currently implemented in a hackish and
475      * not very efficient manner.
476      *
477      * @see get
478      * @access public
479      *
480      * @param string $key  Meta-data key to set.
481      * @param string $newval  New value.
482      */
483     function set($key, $newval) {
484         if (!$key || $key[0] == '%')
485             return;
486         
487         $gd = $this->getPage('global_data');
488         
489         $data = $gd->get('__global');
490         if ($data === false)
491             $data = array();
492
493         if (empty($newval))
494             unset($data[$key]);
495         else
496             $data[$key] = $newval;
497
498         $gd->set('__global', $data);
499     }
500
501     // simple select or create/update queries which do trigger_error
502     function simpleQuery($sql) {
503         global $DBParams;
504         if ($DBParams['dbtype'] == 'SQL') {
505             $result = $this->_backend->_dbh->query($sql);
506             if (DB::isError($result)) {
507                 $msg = $result->getMessage();
508                 trigger_error("SQL Error: ".DB::errorMessage($result),E_USER_WARNING);
509                 return false;
510             } else {
511                 return $result;
512             }
513         } elseif ($DBParams['dbtype'] == 'ADODB') {
514             if (!($result = $this->_backend->_dbh->Execute($sql))) {
515                 trigger_error("SQL Error: ".$this->_backend->_dbh->ErrorMsg(),E_USER_WARNING);
516                 return false;
517             } else {
518                 return $result;
519             }
520         }
521     }
522
523 };
524
525
526 /**
527  * An abstract base class which representing a wiki-page within a
528  * WikiDB.
529  *
530  * A WikiDB_Page contains a number (at least one) of
531  * WikiDB_PageRevisions.
532  */
533 class WikiDB_Page 
534 {
535     function WikiDB_Page(&$wikidb, $pagename) {
536         $this->_wikidb = &$wikidb;
537         $this->_pagename = $pagename;
538         if (DEBUG) {
539             if (!(is_string($pagename) and $pagename != '')) {
540                 trigger_error("empty pagename",E_USER_WARNING);
541                 return false;
542             }
543         } else assert(is_string($pagename) and $pagename != '');
544     }
545
546     /**
547      * Get the name of the wiki page.
548      *
549      * @access public
550      *
551      * @return string The page name.
552      */
553     function getName() {
554         return $this->_pagename;
555     }
556
557     function exists() {
558         $current = $this->getCurrentRevision();
559         return ! $current->hasDefaultContents();
560     }
561
562     /**
563      * Delete an old revision of a WikiDB_Page.
564      *
565      * Deletes the specified revision of the page.
566      * It is a fatal error to attempt to delete the current revision.
567      *
568      * @access public
569      *
570      * @param integer $version Which revision to delete.  (You can also
571      *  use a WikiDB_PageRevision object here.)
572      */
573     function deleteRevision($version) {
574         $backend = &$this->_wikidb->_backend;
575         $cache = &$this->_wikidb->_cache;
576         $pagename = &$this->_pagename;
577
578         $version = $this->_coerce_to_version($version);
579         if ($version == 0)
580             return;
581
582         $backend->lock(array('page','version'));
583         $latestversion = $cache->get_latest_version($pagename);
584         if ($latestversion && $version == $latestversion) {
585             $backend->unlock(array('page','version'));
586             trigger_error(sprintf("Attempt to delete most recent revision of '%s'",
587                                   $pagename), E_USER_ERROR);
588             return;
589         }
590
591         $cache->delete_versiondata($pagename, $version);
592         $backend->unlock(array('page','version'));
593     }
594
595     /*
596      * Delete a revision, or possibly merge it with a previous
597      * revision.
598      *
599      * The idea is this:
600      * Suppose an author make a (major) edit to a page.  Shortly
601      * after that the same author makes a minor edit (e.g. to fix
602      * spelling mistakes he just made.)
603      *
604      * Now some time later, where cleaning out old saved revisions,
605      * and would like to delete his minor revision (since there's
606      * really no point in keeping minor revisions around for a long
607      * time.)
608      *
609      * Note that the text after the minor revision probably represents
610      * what the author intended to write better than the text after
611      * the preceding major edit.
612      *
613      * So what we really want to do is merge the minor edit with the
614      * preceding edit.
615      *
616      * We will only do this when:
617      * <ul>
618      * <li>The revision being deleted is a minor one, and
619      * <li>It has the same author as the immediately preceding revision.
620      * </ul>
621      */
622     function mergeRevision($version) {
623         $backend = &$this->_wikidb->_backend;
624         $cache = &$this->_wikidb->_cache;
625         $pagename = &$this->_pagename;
626
627         $version = $this->_coerce_to_version($version);
628         if ($version == 0)
629             return;
630
631         $backend->lock(array('version'));
632         $latestversion = $backend->get_latest_version($pagename);
633         if ($latestversion && $version == $latestversion) {
634             $backend->unlock(array('version'));
635             trigger_error(sprintf("Attempt to merge most recent revision of '%s'",
636                                   $pagename), E_USER_ERROR);
637             return;
638         }
639
640         $versiondata = $cache->get_versiondata($pagename, $version, true);
641         if (!$versiondata) {
642             // Not there? ... we're done!
643             $backend->unlock(array('version'));
644             return;
645         }
646
647         if ($versiondata['is_minor_edit']) {
648             $previous = $backend->get_previous_version($pagename, $version);
649             if ($previous) {
650                 $prevdata = $cache->get_versiondata($pagename, $previous);
651                 if ($prevdata['author_id'] == $versiondata['author_id']) {
652                     // This is a minor revision, previous version is
653                     // by the same author. We will merge the
654                     // revisions.
655                     $cache->update_versiondata($pagename, $previous,
656                                                array('%content' => $versiondata['%content'],
657                                                      '_supplanted' => $versiondata['_supplanted']));
658                 }
659             }
660         }
661
662         $cache->delete_versiondata($pagename, $version);
663         $backend->unlock(array('version'));
664     }
665
666     
667     /**
668      * Create a new revision of a {@link WikiDB_Page}.
669      *
670      * @access public
671      *
672      * @param int $version Version number for new revision.  
673      * To ensure proper serialization of edits, $version must be
674      * exactly one higher than the current latest version.
675      * (You can defeat this check by setting $version to
676      * {@link WIKIDB_FORCE_CREATE} --- not usually recommended.)
677      *
678      * @param string $content Contents of new revision.
679      *
680      * @param hash $metadata Metadata for new revision.
681      * All values in the hash should be scalars (strings or integers).
682      *
683      * @param array $links List of pagenames which this page links to.
684      *
685      * @return WikiDB_PageRevision  Returns the new WikiDB_PageRevision object. If
686      * $version was incorrect, returns false
687      */
688     function createRevision($version, &$content, $metadata, $links) {
689         $backend = &$this->_wikidb->_backend;
690         $cache = &$this->_wikidb->_cache;
691         $pagename = &$this->_pagename;
692                 
693         $backend->lock(array('version','page','recent','links','nonempty'));
694
695         $latestversion = $backend->get_latest_version($pagename);
696         $newversion = $latestversion + 1;
697         assert($newversion >= 1);
698
699         if ($version != WIKIDB_FORCE_CREATE && $version != $newversion) {
700             $backend->unlock(array('version','page','recent','links'));
701             return false;
702         }
703
704         $data = $metadata;
705         
706         foreach ($data as $key => $val) {
707             if (empty($val) || $key[0] == '_' || $key[0] == '%')
708                 unset($data[$key]);
709         }
710                         
711         assert(!empty($data['author']));
712         if (empty($data['author_id']))
713             @$data['author_id'] = $data['author'];
714                 
715         if (empty($data['mtime']))
716             $data['mtime'] = time();
717
718         if ($latestversion) {
719             // Ensure mtimes are monotonic.
720             $pdata = $cache->get_versiondata($pagename, $latestversion);
721             if ($data['mtime'] < $pdata['mtime']) {
722                 trigger_error(sprintf(_("%s: Date of new revision is %s"),
723                                       $pagename,"'non-monotonic'"),
724                               E_USER_NOTICE);
725                 $data['orig_mtime'] = $data['mtime'];
726                 $data['mtime'] = $pdata['mtime'];
727             }
728             
729             // FIXME: use (possibly user specified) 'mtime' time or
730             // time()?
731             $cache->update_versiondata($pagename, $latestversion,
732                                        array('_supplanted' => $data['mtime']));
733         }
734
735         $data['%content'] = &$content;
736
737         $cache->set_versiondata($pagename, $newversion, $data);
738
739         //$cache->update_pagedata($pagename, array(':latestversion' => $newversion,
740         //':deleted' => empty($content)));
741         
742         $backend->set_links($pagename, $links);
743
744         $backend->unlock(array('version','page','recent','links','nonempty'));
745
746         return new WikiDB_PageRevision($this->_wikidb, $pagename, $newversion,
747                                        $data);
748     }
749
750     /** A higher-level interface to createRevision.
751      *
752      * This takes care of computing the links, and storing
753      * a cached version of the transformed wiki-text.
754      *
755      * @param string $wikitext  The page content.
756      *
757      * @param int $version Version number for new revision.  
758      * To ensure proper serialization of edits, $version must be
759      * exactly one higher than the current latest version.
760      * (You can defeat this check by setting $version to
761      * {@link WIKIDB_FORCE_CREATE} --- not usually recommended.)
762      *
763      * @param hash $meta  Meta-data for new revision.
764      */
765     function save($wikitext, $version, $meta) {
766         $formatted = new TransformedText($this, $wikitext, $meta);
767         $type = $formatted->getType();
768         $meta['pagetype'] = $type->getName();
769         $links = $formatted->getWikiPageLinks();
770
771         $backend = &$this->_wikidb->_backend;
772         $newrevision = $this->createRevision($version, $wikitext, $meta, $links);
773         if ($newrevision)
774             if (!defined('WIKIDB_NOCACHE_MARKUP') or !WIKIDB_NOCACHE_MARKUP)
775                 $this->set('_cached_html', $formatted->pack());
776
777         // FIXME: probably should have some global state information
778         // in the backend to control when to optimize.
779         //
780         // We're doing this here rather than in createRevision because
781         // postgres can't optimize while locked.
782         if (time() % 50 == 0) {
783             if ($backend->optimize())
784                 trigger_error(sprintf(_("Optimizing %s"),'backend'), E_USER_NOTICE);
785         }
786
787         /* Generate notification emails? */
788         if (isa($newrevision, 'wikidb_pagerevision')) {
789             // Save didn't fail because of concurrent updates.
790             $notify = $this->_wikidb->get('notify');
791             if (!empty($notify) and is_array($notify)) {
792                 list($emails,$userids) = $this->getPageChangeEmails($notify);
793                 if (!empty($emails))
794                     $this->sendPageChangeNotification($wikitext, $version, $meta, $emails, $userids);
795             }
796         }
797
798         $newrevision->_transformedContent = $formatted;
799         return $newrevision;
800     }
801
802     function getPageChangeEmails($notify) {
803         $emails = array(); $userids = array();
804         foreach ($notify as $page => $users) {
805             if (glob_match($page,$this->_pagename)) {
806                 foreach ($users as $userid => $user) {
807                     if (!empty($user['verified']) and !empty($user['email'])) {
808                         $emails[]  = $user['email'];
809                         $userids[] = $userid;
810                     } elseif (!empty($user['email'])) {
811                         global $request;
812                         // do a dynamic emailVerified check update
813                         $u = $request->getUser();
814                         if ($u->UserName() == $userid) {
815                             if ($request->_prefs->get('emailVerified')) {
816                                 $emails[] = $user['email'];
817                                 $userids[] = $userid;
818                                 $notify[$page][$userid]['verified'] = 1;
819                                 $request->_dbi->set('notify',$notify);
820                             }
821                         } else {
822                             $u = WikiUser($userid);
823                             if ($u->_prefs->get('emailVerified')) {
824                                 $emails[] = $user['email'];
825                                 $userids[] = $userid;
826                                 $notify[$page][$userid]['verified'] = 1;
827                                 $request->_dbi->set('notify',$notify);
828                             }
829                         }
830                         // ignore verification
831                         /*
832                         if (DEBUG) {
833                             if (!in_array($user['email'],$emails))
834                                 $emails[] = $user['email'];
835                         }
836                         */
837                     }
838                 }
839             }
840         }
841         $emails = array_unique($emails);
842         $userids = array_unique($userids);
843         return array($emails,$userids);
844     }
845
846     function sendPageChangeNotification(&$wikitext, $version, $meta, $emails, $userids) {
847         $backend = &$this->_wikidb->_backend;
848         $subject = _("Page change").' '.$this->_pagename;
849         $previous = $backend->get_previous_version($this->_pagename, $version);
850         if (!isset($meta['mtime'])) $meta['mtime'] = time();
851         if ($previous) {
852             $difflink = WikiURL($this->_pagename,array('action'=>'diff'),true);
853             $cache = &$this->_wikidb->_cache;
854             $this_content = explode("\n", $wikitext);
855             $prevdata = $cache->get_versiondata($this->_pagename, $previous, true);
856             if (empty($prevdata['%content']))
857                 $prevdata = $backend->get_versiondata($this->_pagename, $previous, true);
858             $other_content = explode("\n", $prevdata['%content']);
859             
860             include_once("lib/diff.php");
861             $diff2 = new Diff($other_content, $this_content);
862             $context_lines = max(4, count($other_content) + 1,
863                                  count($this_content) + 1);
864             $fmt = new UnifiedDiffFormatter($context_lines);
865             $content  = $this->_pagename . " " . $previous . " " . Iso8601DateTime($prevdata['mtime']) . "\n";
866             $content .= $this->_pagename . " " . $version . " " .  Iso8601DateTime($meta['mtime']) . "\n";
867             $content .= $fmt->format($diff2);
868             
869         } else {
870             $difflink = WikiURL($this->_pagename,array(),true);
871             $content = $this->_pagename . " " . $version . " " .  Iso8601DateTime($meta['mtime']) . "\n";
872             $content .= _("New Page");
873         }
874         $editedby = sprintf(_("Edited by: %s"), $meta['author']);
875         $emails = join(',',$emails);
876         if (mail($emails,"[".WIKI_NAME."] ".$subject, 
877                  $subject."\n".
878                  $editedby."\n".
879                  $difflink."\n\n".
880                  $content))
881             trigger_error(sprintf(_("PageChange Notification of %s sent to %s"),
882                                   $this->_pagename, join(',',$userids)), E_USER_NOTICE);
883         else
884             trigger_error(sprintf(_("PageChange Notification Error: Couldn't send %s to %s"),
885                                   $this->_pagename, join(',',$userids)), E_USER_WARNING);
886     }
887
888     /**
889      * Get the most recent revision of a page.
890      *
891      * @access public
892      *
893      * @return WikiDB_PageRevision The current WikiDB_PageRevision object. 
894      */
895     function getCurrentRevision() {
896         $backend = &$this->_wikidb->_backend;
897         $cache = &$this->_wikidb->_cache;
898         $pagename = &$this->_pagename;
899         
900         // Prevent deadlock in case of memory exhausted errors
901         // Pure selection doesn't really need locking here.
902         //   sf.net bug#927395
903         // I know it would be better, but with lots of pages this deadlock is more 
904         // severe than occasionally get not the latest revision.
905         //$backend->lock();
906         $version = $cache->get_latest_version($pagename);
907         $revision = $this->getRevision($version);
908         //$backend->unlock();
909         assert($revision);
910         return $revision;
911     }
912
913     /**
914      * Get a specific revision of a WikiDB_Page.
915      *
916      * @access public
917      *
918      * @param integer $version  Which revision to get.
919      *
920      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or
921      * false if the requested revision does not exist in the {@link WikiDB}.
922      * Note that version zero of any page always exists.
923      */
924     function getRevision($version) {
925         $cache = &$this->_wikidb->_cache;
926         $pagename = &$this->_pagename;
927         
928         if ($version == 0)
929             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
930
931         assert($version > 0);
932         $vdata = $cache->get_versiondata($pagename, $version);
933         if (!$vdata)
934             return false;
935         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
936                                        $vdata);
937     }
938
939     /**
940      * Get previous page revision.
941      *
942      * This method find the most recent revision before a specified
943      * version.
944      *
945      * @access public
946      *
947      * @param integer $version  Find most recent revision before this version.
948      *  You can also use a WikiDB_PageRevision object to specify the $version.
949      *
950      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or false if the
951      * requested revision does not exist in the {@link WikiDB}.  Note that
952      * unless $version is greater than zero, a revision (perhaps version zero,
953      * the default revision) will always be found.
954      */
955     function getRevisionBefore($version) {
956         $backend = &$this->_wikidb->_backend;
957         $pagename = &$this->_pagename;
958
959         $version = $this->_coerce_to_version($version);
960
961         if ($version == 0)
962             return false;
963         //$backend->lock();
964         $previous = $backend->get_previous_version($pagename, $version);
965         $revision = $this->getRevision($previous);
966         //$backend->unlock();
967         assert($revision);
968         return $revision;
969     }
970
971     /**
972      * Get all revisions of the WikiDB_Page.
973      *
974      * This does not include the version zero (default) revision in the
975      * returned revision set.
976      *
977      * @return WikiDB_PageRevisionIterator A
978      * WikiDB_PageRevisionIterator containing all revisions of this
979      * WikiDB_Page in reverse order by version number.
980      */
981     function getAllRevisions() {
982         $backend = &$this->_wikidb->_backend;
983         $revs = $backend->get_all_revisions($this->_pagename);
984         return new WikiDB_PageRevisionIterator($this->_wikidb, $revs);
985     }
986     
987     /**
988      * Find pages which link to or are linked from a page.
989      *
990      * @access public
991      *
992      * @param boolean $reversed Which links to find: true for backlinks (default).
993      *
994      * @return WikiDB_PageIterator A WikiDB_PageIterator containing
995      * all matching pages.
996      */
997     function getLinks($reversed = true) {
998         $backend = &$this->_wikidb->_backend;
999         $result =  $backend->get_links($this->_pagename, $reversed);
1000         return new WikiDB_PageIterator($this->_wikidb, $result);
1001     }
1002             
1003     /**
1004      * Access WikiDB_Page meta-data.
1005      *
1006      * @access public
1007      *
1008      * @param string $key Which meta data to get.
1009      * Some reserved meta-data keys are:
1010      * <dl>
1011      * <dt>'locked'<dd> Is page locked?
1012      * <dt>'hits'  <dd> Page hit counter.
1013      * <dt>'pref'  <dd> Users preferences, stored in homepages.
1014      * <dt>'owner' <dd> Default: first author_id. We might add a group with a dot here:
1015      *                  E.g. "owner.users"
1016      * <dt>'perm'  <dd> Permission flag to authorize read/write/execution of 
1017      *                  page-headers and content.
1018      * <dt>'score' <dd> Page score (not yet implement, do we need?)
1019      * </dl>
1020      *
1021      * @return scalar The requested value, or false if the requested data
1022      * is not set.
1023      */
1024     function get($key) {
1025         $cache = &$this->_wikidb->_cache;
1026         if (!$key || $key[0] == '%')
1027             return false;
1028         $data = $cache->get_pagedata($this->_pagename);
1029         return isset($data[$key]) ? $data[$key] : false;
1030     }
1031
1032     /**
1033      * Get all the page meta-data as a hash.
1034      *
1035      * @return hash The page meta-data.
1036      */
1037     function getMetaData() {
1038         $cache = &$this->_wikidb->_cache;
1039         $data = $cache->get_pagedata($this->_pagename);
1040         $meta = array();
1041         foreach ($data as $key => $val) {
1042             if (/*!empty($val) &&*/ $key[0] != '%')
1043                 $meta[$key] = $val;
1044         }
1045         return $meta;
1046     }
1047
1048     /**
1049      * Set page meta-data.
1050      *
1051      * @see get
1052      * @access public
1053      *
1054      * @param string $key  Meta-data key to set.
1055      * @param string $newval  New value.
1056      */
1057     function set($key, $newval) {
1058         $cache = &$this->_wikidb->_cache;
1059         $pagename = &$this->_pagename;
1060         
1061         assert($key && $key[0] != '%');
1062
1063         $data = $cache->get_pagedata($pagename);
1064
1065         if (!empty($newval)) {
1066             if (!empty($data[$key]) && $data[$key] == $newval)
1067                 return;         // values identical, skip update.
1068         }
1069         else {
1070             if (empty($data[$key]))
1071                 return;         // values identical, skip update.
1072         }
1073
1074         $cache->update_pagedata($pagename, array($key => $newval));
1075     }
1076
1077     /**
1078      * Increase page hit count.
1079      *
1080      * FIXME: IS this needed?  Probably not.
1081      *
1082      * This is a convenience function.
1083      * <pre> $page->increaseHitCount(); </pre>
1084      * is functionally identical to
1085      * <pre> $page->set('hits',$page->get('hits')+1); </pre>
1086      *
1087      * Note that this method may be implemented in more efficient ways
1088      * in certain backends.
1089      *
1090      * @access public
1091      */
1092     function increaseHitCount() {
1093         @$newhits = $this->get('hits') + 1;
1094         $this->set('hits', $newhits);
1095     }
1096
1097     /**
1098      * Return a string representation of the WikiDB_Page
1099      *
1100      * This is really only for debugging.
1101      *
1102      * @access public
1103      *
1104      * @return string Printable representation of the WikiDB_Page.
1105      */
1106     function asString () {
1107         ob_start();
1108         printf("[%s:%s\n", get_class($this), $this->getName());
1109         print_r($this->getMetaData());
1110         echo "]\n";
1111         $strval = ob_get_contents();
1112         ob_end_clean();
1113         return $strval;
1114     }
1115
1116
1117     /**
1118      * @access private
1119      * @param integer_or_object $version_or_pagerevision
1120      * Takes either the version number (and int) or a WikiDB_PageRevision
1121      * object.
1122      * @return integer The version number.
1123      */
1124     function _coerce_to_version($version_or_pagerevision) {
1125         if (method_exists($version_or_pagerevision, "getContent"))
1126             $version = $version_or_pagerevision->getVersion();
1127         else
1128             $version = (int) $version_or_pagerevision;
1129
1130         assert($version >= 0);
1131         return $version;
1132     }
1133
1134     function isUserPage ($include_empty = true) {
1135         if ($include_empty) {
1136             $current = $this->getCurrentRevision();
1137             if ($current->hasDefaultContents()) {
1138                 return false;
1139             }
1140         }
1141         return $this->get('pref') ? true : false;
1142     }
1143
1144 };
1145
1146 /**
1147  * This class represents a specific revision of a WikiDB_Page within
1148  * a WikiDB.
1149  *
1150  * A WikiDB_PageRevision has read-only semantics. You may only create
1151  * new revisions (and delete old ones) --- you cannot modify existing
1152  * revisions.
1153  */
1154 class WikiDB_PageRevision
1155 {
1156     var $_transformedContent = false; // set by WikiDB_Page::save()
1157     
1158     function WikiDB_PageRevision(&$wikidb, $pagename, $version,
1159                                  $versiondata = false)
1160         {
1161             $this->_wikidb = &$wikidb;
1162             $this->_pagename = $pagename;
1163             $this->_version = $version;
1164             $this->_data = $versiondata ? $versiondata : array();
1165         }
1166     
1167     /**
1168      * Get the WikiDB_Page which this revision belongs to.
1169      *
1170      * @access public
1171      *
1172      * @return WikiDB_Page The WikiDB_Page which this revision belongs to.
1173      */
1174     function getPage() {
1175         return new WikiDB_Page($this->_wikidb, $this->_pagename);
1176     }
1177
1178     /**
1179      * Get the version number of this revision.
1180      *
1181      * @access public
1182      *
1183      * @return integer The version number of this revision.
1184      */
1185     function getVersion() {
1186         return $this->_version;
1187     }
1188     
1189     /**
1190      * Determine whether this revision has defaulted content.
1191      *
1192      * The default revision (version 0) of each page, as well as any
1193      * pages which are created with empty content have their content
1194      * defaulted to something like:
1195      * <pre>
1196      *   Describe [ThisPage] here.
1197      * </pre>
1198      *
1199      * @access public
1200      *
1201      * @return boolean Returns true if the page has default content.
1202      */
1203     function hasDefaultContents() {
1204         $data = &$this->_data;
1205         return empty($data['%content']);
1206     }
1207
1208     /**
1209      * Get the content as an array of lines.
1210      *
1211      * @access public
1212      *
1213      * @return array An array of lines.
1214      * The lines should contain no trailing white space.
1215      */
1216     function getContent() {
1217         return explode("\n", $this->getPackedContent());
1218     }
1219         
1220         /**
1221      * Get the pagename of the revision.
1222      *
1223      * @access public
1224      *
1225      * @return string pagename.
1226      */
1227     function getPageName() {
1228         return $this->_pagename;
1229     }
1230
1231     /**
1232      * Determine whether revision is the latest.
1233      *
1234      * @access public
1235      *
1236      * @return boolean True iff the revision is the latest (most recent) one.
1237      */
1238     function isCurrent() {
1239         if (!isset($this->_iscurrent)) {
1240             $page = $this->getPage();
1241             $current = $page->getCurrentRevision();
1242             $this->_iscurrent = $this->getVersion() == $current->getVersion();
1243         }
1244         return $this->_iscurrent;
1245     }
1246
1247     /**
1248      * Get the transformed content of a page.
1249      *
1250      * @param string $pagetype  Override the page-type of the revision.
1251      *
1252      * @return object An XmlContent-like object containing the page transformed
1253      * contents.
1254      */
1255     function getTransformedContent($pagetype_override=false) {
1256         $backend = &$this->_wikidb->_backend;
1257         
1258         if ($pagetype_override) {
1259             // Figure out the normal page-type for this page.
1260             $type = PageType::GetPageType($this->get('pagetype'));
1261             if ($type->getName() == $pagetype_override)
1262                 $pagetype_override = false; // Not really an override...
1263         }
1264
1265         if ($pagetype_override) {
1266             // Overriden page type, don't cache (or check cache).
1267             return new TransformedText($this->getPage(),
1268                                        $this->getPackedContent(),
1269                                        $this->getMetaData(),
1270                                        $pagetype_override);
1271         }
1272
1273         $possibly_cache_results = true;
1274
1275         if (defined('WIKIDB_NOCACHE_MARKUP') and WIKIDB_NOCACHE_MARKUP) {
1276             if (WIKIDB_NOCACHE_MARKUP == 'purge') {
1277                 // flush cache for this page.
1278                 $page = $this->getPage();
1279                 $page->set('_cached_html', false);
1280             }
1281             $possibly_cache_results = false;
1282         }
1283         elseif (!$this->_transformedContent) {
1284             //$backend->lock();
1285             if ($this->isCurrent()) {
1286                 $page = $this->getPage();
1287                 $this->_transformedContent = TransformedText::unpack($page->get('_cached_html'));
1288             }
1289             else {
1290                 $possibly_cache_results = false;
1291             }
1292             //$backend->unlock();
1293         }
1294         
1295         if (!$this->_transformedContent) {
1296             $this->_transformedContent
1297                 = new TransformedText($this->getPage(),
1298                                       $this->getPackedContent(),
1299                                       $this->getMetaData());
1300             
1301             if ($possibly_cache_results) {
1302                 // If we're still the current version, cache the transfomed page.
1303                 //$backend->lock();
1304                 if ($this->isCurrent()) {
1305                     $page->set('_cached_html', $this->_transformedContent->pack());
1306                 }
1307                 //$backend->unlock();
1308             }
1309         }
1310
1311         return $this->_transformedContent;
1312     }
1313
1314     /**
1315      * Get the content as a string.
1316      *
1317      * @access public
1318      *
1319      * @return string The page content.
1320      * Lines are separated by new-lines.
1321      */
1322     function getPackedContent() {
1323         $data = &$this->_data;
1324
1325         
1326         if (empty($data['%content'])) {
1327             include_once('lib/InlineParser.php');
1328             // Replace empty content with default value.
1329             return sprintf(_("Describe %s here."), 
1330                            "[" . WikiEscape($this->_pagename) . "]");
1331         }
1332
1333         // There is (non-default) content.
1334         assert($this->_version > 0);
1335         
1336         if (!is_string($data['%content'])) {
1337             // Content was not provided to us at init time.
1338             // (This is allowed because for some backends, fetching
1339             // the content may be expensive, and often is not wanted
1340             // by the user.)
1341             //
1342             // In any case, now we need to get it.
1343             $data['%content'] = $this->_get_content();
1344             assert(is_string($data['%content']));
1345         }
1346         
1347         return $data['%content'];
1348     }
1349
1350     function _get_content() {
1351         $cache = &$this->_wikidb->_cache;
1352         $pagename = $this->_pagename;
1353         $version = $this->_version;
1354
1355         assert($version > 0);
1356         
1357         $newdata = $cache->get_versiondata($pagename, $version, true);
1358         if ($newdata) {
1359             assert(is_string($newdata['%content']));
1360             return $newdata['%content'];
1361         }
1362         else {
1363             // else revision has been deleted... What to do?
1364             return __sprintf("Oops! Revision %s of %s seems to have been deleted!",
1365                              $version, $pagename);
1366         }
1367     }
1368
1369     /**
1370      * Get meta-data for this revision.
1371      *
1372      *
1373      * @access public
1374      *
1375      * @param string $key Which meta-data to access.
1376      *
1377      * Some reserved revision meta-data keys are:
1378      * <dl>
1379      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
1380      *        The 'mtime' meta-value is normally set automatically by the database
1381      *        backend, but it may be specified explicitly when creating a new revision.
1382      * <dt> orig_mtime
1383      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
1384      *       of a page must be monotonically increasing.  If an attempt is
1385      *       made to create a new revision with an mtime less than that of
1386      *       the preceeding revision, the new revisions timestamp is force
1387      *       to be equal to that of the preceeding revision.  In that case,
1388      *       the originally requested mtime is preserved in 'orig_mtime'.
1389      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
1390      *        This meta-value is <em>always</em> automatically maintained by the database
1391      *        backend.  (It is set from the 'mtime' meta-value of the superceding
1392      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
1393      *
1394      * FIXME: this could be refactored:
1395      * <dt> author
1396      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
1397      * <dt> author_id
1398      *  <dd> Authenticated author of a page.  This is used to identify
1399      *       the distinctness of authors when cleaning old revisions from
1400      *       the database.
1401      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
1402      * <dt> 'summary' <dd> Short change summary entered by page author.
1403      * </dl>
1404      *
1405      * Meta-data keys must be valid C identifers (they have to start with a letter
1406      * or underscore, and can contain only alphanumerics and underscores.)
1407      *
1408      * @return string The requested value, or false if the requested value
1409      * is not defined.
1410      */
1411     function get($key) {
1412         if (!$key || $key[0] == '%')
1413             return false;
1414         $data = &$this->_data;
1415         return isset($data[$key]) ? $data[$key] : false;
1416     }
1417
1418     /**
1419      * Get all the revision page meta-data as a hash.
1420      *
1421      * @return hash The revision meta-data.
1422      */
1423     function getMetaData() {
1424         $meta = array();
1425         foreach ($this->_data as $key => $val) {
1426             if (!empty($val) && $key[0] != '%')
1427                 $meta[$key] = $val;
1428         }
1429         return $meta;
1430     }
1431     
1432             
1433     /**
1434      * Return a string representation of the revision.
1435      *
1436      * This is really only for debugging.
1437      *
1438      * @access public
1439      *
1440      * @return string Printable representation of the WikiDB_Page.
1441      */
1442     function asString () {
1443         ob_start();
1444         printf("[%s:%d\n", get_class($this), $this->get('version'));
1445         print_r($this->_data);
1446         echo $this->getPackedContent() . "\n]\n";
1447         $strval = ob_get_contents();
1448         ob_end_clean();
1449         return $strval;
1450     }
1451 };
1452
1453
1454 /**
1455  * A class which represents a sequence of WikiDB_Pages.
1456  */
1457 class WikiDB_PageIterator
1458 {
1459     function WikiDB_PageIterator(&$wikidb, &$pages) {
1460         $this->_pages = $pages;
1461         $this->_wikidb = &$wikidb;
1462     }
1463     
1464     function count () {
1465         return $this->_pages->count();
1466     }
1467
1468     /**
1469      * Get next WikiDB_Page in sequence.
1470      *
1471      * @access public
1472      *
1473      * @return WikiDB_Page The next WikiDB_Page in the sequence.
1474      */
1475     function next () {
1476         if ( ! ($next = $this->_pages->next()) )
1477             return false;
1478
1479         $pagename = &$next['pagename'];
1480         if (isset($next['pagedata']))
1481             $this->_wikidb->_cache->cache_data($next);
1482
1483         return new WikiDB_Page($this->_wikidb, $pagename);
1484     }
1485
1486     /**
1487      * Release resources held by this iterator.
1488      *
1489      * The iterator may not be used after free() is called.
1490      *
1491      * There is no need to call free(), if next() has returned false.
1492      * (I.e. if you iterate through all the pages in the sequence,
1493      * you do not need to call free() --- you only need to call it
1494      * if you stop before the end of the iterator is reached.)
1495      *
1496      * @access public
1497      */
1498     function free() {
1499         $this->_pages->free();
1500     }
1501
1502     
1503     function asArray() {
1504         $result = array();
1505         while ($page = $this->next())
1506             $result[] = $page;
1507         $this->free();
1508         return $result;
1509     }
1510     
1511     // Not yet used and problematic. Order should be set in the query, not afterwards.
1512     // See PageList::sortby
1513     function setSortby ($arg = false) {
1514         if (!$arg) {
1515             $arg = @$_GET['sortby'];
1516             if ($arg) {
1517                 $sortby = substr($arg,1);
1518                 $order  = substr($arg,0,1)=='+' ? 'ASC' : 'DESC';
1519             }
1520         }
1521         if (is_array($arg)) { // array('mtime' => 'desc')
1522             $sortby = $arg[0];
1523             $order = $arg[1];
1524         } else {
1525             $sortby = $arg;
1526             $order  = 'ASC';
1527         }
1528         // available column types to sort by:
1529         // todo: we must provide access methods for the generic dumb/iterator
1530         $this->_types = explode(',','pagename,mtime,hits,version,author,locked,minor,markup');
1531         if (in_array($sortby,$this->_types))
1532             $this->_options['sortby'] = $sortby;
1533         else
1534             trigger_error(sprintf("Argument %s '%s' ignored",'sortby',$sortby), E_USER_WARNING);
1535         if (in_array(strtoupper($order),'ASC','DESC')) 
1536             $this->_options['order'] = strtoupper($order);
1537         else
1538             trigger_error(sprintf("Argument %s '%s' ignored",'order',$order), E_USER_WARNING);
1539     }
1540
1541 };
1542
1543 /**
1544  * A class which represents a sequence of WikiDB_PageRevisions.
1545  */
1546 class WikiDB_PageRevisionIterator
1547 {
1548     function WikiDB_PageRevisionIterator(&$wikidb, &$revisions) {
1549         $this->_revisions = $revisions;
1550         $this->_wikidb = &$wikidb;
1551     }
1552     
1553     function count () {
1554         return $this->_revisions->count();
1555     }
1556
1557     /**
1558      * Get next WikiDB_PageRevision in sequence.
1559      *
1560      * @access public
1561      *
1562      * @return WikiDB_PageRevision
1563      * The next WikiDB_PageRevision in the sequence.
1564      */
1565     function next () {
1566         if ( ! ($next = $this->_revisions->next()) )
1567             return false;
1568
1569         $this->_wikidb->_cache->cache_data($next);
1570
1571         $pagename = $next['pagename'];
1572         $version = $next['version'];
1573         $versiondata = $next['versiondata'];
1574         if (DEBUG) {
1575             if (!(is_string($pagename) and $pagename != '')) {
1576                 trigger_error("empty pagename",E_USER_WARNING);
1577                 return false;
1578             }
1579         } else assert(is_string($pagename) and $pagename != '');
1580         if (DEBUG) {
1581             if (!is_array($versiondata)) {
1582                 trigger_error("empty versiondata",E_USER_WARNING);
1583                 return false;
1584             }
1585         } else assert(is_array($versiondata));
1586         if (DEBUG) {
1587             if (!($version > 0)) {
1588                 trigger_error("invalid version",E_USER_WARNING);
1589                 return false;
1590             }
1591         } else assert($version > 0);
1592
1593         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
1594                                        $versiondata);
1595     }
1596
1597     /**
1598      * Release resources held by this iterator.
1599      *
1600      * The iterator may not be used after free() is called.
1601      *
1602      * There is no need to call free(), if next() has returned false.
1603      * (I.e. if you iterate through all the revisions in the sequence,
1604      * you do not need to call free() --- you only need to call it
1605      * if you stop before the end of the iterator is reached.)
1606      *
1607      * @access public
1608      */
1609     function free() { 
1610         $this->_revisions->free();
1611     }
1612 };
1613
1614
1615 /**
1616  * Data cache used by WikiDB.
1617  *
1618  * FIXME: Maybe rename this to caching_backend (or some such).
1619  *
1620  * @access private
1621  */
1622 class WikiDB_cache 
1623 {
1624     // FIXME: beautify versiondata cache.  Cache only limited data?
1625
1626     function WikiDB_cache (&$backend) {
1627         $this->_backend = &$backend;
1628
1629         $this->_pagedata_cache = array();
1630         $this->_versiondata_cache = array();
1631         array_push ($this->_versiondata_cache, array());
1632         $this->_glv_cache = array();
1633     }
1634     
1635     function close() {
1636         $this->_pagedata_cache = false;
1637         $this->_versiondata_cache = false;
1638         $this->_glv_cache = false;
1639     }
1640
1641     function get_pagedata($pagename) {
1642         assert(is_string($pagename) && $pagename != '');
1643         $cache = &$this->_pagedata_cache;
1644
1645         if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
1646             $cache[$pagename] = $this->_backend->get_pagedata($pagename);
1647             if (empty($cache[$pagename]))
1648                 $cache[$pagename] = array();
1649         }
1650
1651         return $cache[$pagename];
1652     }
1653     
1654     function update_pagedata($pagename, $newdata) {
1655         assert(is_string($pagename) && $pagename != '');
1656
1657         $this->_backend->update_pagedata($pagename, $newdata);
1658
1659         if (is_array($this->_pagedata_cache[$pagename])) {
1660             $cachedata = &$this->_pagedata_cache[$pagename];
1661             foreach($newdata as $key => $val)
1662                 $cachedata[$key] = $val;
1663         }
1664     }
1665
1666     function invalidate_cache($pagename) {
1667         unset ($this->_pagedata_cache[$pagename]);
1668         unset ($this->_versiondata_cache[$pagename]);
1669         unset ($this->_glv_cache[$pagename]);
1670     }
1671     
1672     function delete_page($pagename) {
1673         $this->_backend->delete_page($pagename);
1674         unset ($this->_pagedata_cache[$pagename]);
1675         unset ($this->_glv_cache[$pagename]);
1676     }
1677
1678     // FIXME: ugly
1679     function cache_data($data) {
1680         if (isset($data['pagedata']))
1681             $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
1682     }
1683     
1684     function get_versiondata($pagename, $version, $need_content = false) {
1685         //  FIXME: Seriously ugly hackage
1686         if (defined ('USECACHE')){   //temporary - for debugging
1687             assert(is_string($pagename) && $pagename != '');
1688             // there is a bug here somewhere which results in an assertion failure at line 105
1689             // of ArchiveCleaner.php  It goes away if we use the next line.
1690             $need_content = true;
1691             $nc = $need_content ? '1':'0';
1692             $cache = &$this->_versiondata_cache;
1693             if (!isset($cache[$pagename][$version][$nc])||
1694                 !(is_array ($cache[$pagename])) || !(is_array ($cache[$pagename][$version]))) {
1695                 $cache[$pagename][$version][$nc] = 
1696                     $this->_backend->get_versiondata($pagename,$version, $need_content);
1697                 // If we have retrieved all data, we may as well set the cache for $need_content = false
1698                 if ($need_content){
1699                     $cache[$pagename][$version]['0'] = $cache[$pagename][$version]['1'];
1700                 }
1701             }
1702             $vdata = $cache[$pagename][$version][$nc];
1703         } else {
1704             $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
1705         }
1706         // FIXME: ugly
1707         if ($vdata && !empty($vdata['%pagedata']))
1708             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
1709         return $vdata;
1710     }
1711
1712     function set_versiondata($pagename, $version, $data) {
1713         $new = $this->_backend->set_versiondata($pagename, $version, $data);
1714         // Update the cache
1715         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
1716         // FIXME: hack
1717         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
1718         // Is this necessary?
1719         unset($this->_glv_cache[$pagename]);
1720     }
1721
1722     function update_versiondata($pagename, $version, $data) {
1723         $new = $this->_backend->update_versiondata($pagename, $version, $data);
1724         // Update the cache
1725         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
1726         // FIXME: hack
1727         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
1728         // Is this necessary?
1729         unset($this->_glv_cache[$pagename]);
1730     }
1731
1732     function delete_versiondata($pagename, $version) {
1733         $new = $this->_backend->delete_versiondata($pagename, $version);
1734         unset ($this->_versiondata_cache[$pagename][$version]['1']);
1735         unset ($this->_versiondata_cache[$pagename][$version]['0']);
1736         unset ($this->_glv_cache[$pagename]);
1737     }
1738         
1739     function get_latest_version($pagename)  {
1740         if (defined('USECACHE')){
1741             assert (is_string($pagename) && $pagename != '');
1742             $cache = &$this->_glv_cache;        
1743             if (!isset($cache[$pagename])) {
1744                 $cache[$pagename] = $this->_backend->get_latest_version($pagename);
1745                 if (empty($cache[$pagename]))
1746                     $cache[$pagename] = 0;
1747             }
1748             return $cache[$pagename];
1749         } else {
1750             return $this->_backend->get_latest_version($pagename); 
1751         }
1752     }
1753
1754 };
1755
1756 // $Log: not supported by cvs2svn $
1757 // Revision 1.51  2004/05/06 17:30:37  rurban
1758 // CategoryGroup: oops, dos2unix eol
1759 // improved phpwiki_version:
1760 //   pre -= .0001 (1.3.10pre: 1030.099)
1761 //   -p1 += .001 (1.3.9-p1: 1030.091)
1762 // improved InstallTable for mysql and generic SQL versions and all newer tables so far.
1763 // abstracted more ADODB/PearDB methods for action=upgrade stuff:
1764 //   backend->backendType(), backend->database(),
1765 //   backend->listOfFields(),
1766 //   backend->listOfTables(),
1767 //
1768 // Revision 1.50  2004/05/04 22:34:25  rurban
1769 // more pdf support
1770 //
1771 // Revision 1.49  2004/05/03 11:16:40  rurban
1772 // fixed sendPageChangeNotification
1773 // subject rewording
1774 //
1775 // Revision 1.48  2004/04/29 23:03:54  rurban
1776 // fixed sf.net bug #940996
1777 //
1778 // Revision 1.47  2004/04/29 19:39:44  rurban
1779 // special support for formatted plugins (one-liners)
1780 //   like <small><plugin BlaBla ></small>
1781 // iter->asArray() helper for PopularNearby
1782 // db_session for older php's (no &func() allowed)
1783 //
1784 // Revision 1.46  2004/04/26 20:44:34  rurban
1785 // locking table specific for better databases
1786 //
1787 // Revision 1.45  2004/04/20 00:06:03  rurban
1788 // themable paging support
1789 //
1790 // Revision 1.44  2004/04/19 18:27:45  rurban
1791 // Prevent from some PHP5 warnings (ref args, no :: object init)
1792 //   php5 runs now through, just one wrong XmlElement object init missing
1793 // Removed unneccesary UpgradeUser lines
1794 // Changed WikiLink to omit version if current (RecentChanges)
1795 //
1796 // Revision 1.43  2004/04/18 01:34:20  rurban
1797 // protect most_popular from sortby=mtime
1798 //
1799 // Revision 1.42  2004/04/18 01:11:51  rurban
1800 // more numeric pagename fixes.
1801 // fixed action=upload with merge conflict warnings.
1802 // charset changed from constant to global (dynamic utf-8 switching)
1803 //
1804
1805 // Local Variables:
1806 // mode: php
1807 // tab-width: 8
1808 // c-basic-offset: 4
1809 // c-hanging-comment-ender-p: nil
1810 // indent-tabs-mode: nil
1811 // End:   
1812 ?>