]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
Remove CVS backend
[SourceForge/phpwiki.git] / lib / WikiDB.php
1 <?php
2 require_once 'lib/PageType.php';
3
4 /**
5  * The classes in the file define the interface to the
6  * page database.
7  *
8  * @package WikiDB
9  * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
10  * Minor enhancements by Reini Urban
11  */
12
13 /**
14  * Force the creation of a new revision.
15  * @see WikiDB_Page::createRevision()
16  */
17 if (!defined('WIKIDB_FORCE_CREATE'))
18     define('WIKIDB_FORCE_CREATE', -1);
19
20 /**
21  * Abstract base class for the database used by PhpWiki.
22  *
23  * A <tt>WikiDB</tt> is a container for <tt>WikiDB_Page</tt>s which in
24  * turn contain <tt>WikiDB_PageRevision</tt>s.
25  *
26  * Conceptually a <tt>WikiDB</tt> contains all possible
27  * <tt>WikiDB_Page</tt>s, whether they have been initialized or not.
28  * Since all possible pages are already contained in a WikiDB, a call
29  * to WikiDB::getPage() will never fail (barring bugs and
30  * e.g. filesystem or SQL database problems.)
31  *
32  * Also each <tt>WikiDB_Page</tt> always contains at least one
33  * <tt>WikiDB_PageRevision</tt>: the default content (e.g. "Describe
34  * [PageName] here.").  This default content has a version number of
35  * zero.
36  *
37  * <tt>WikiDB_PageRevision</tt>s have read-only semantics. One can
38  * only create new revisions or delete old ones --- one can not modify
39  * an existing revision.
40  */
41 class WikiDB
42 {
43     /**
44      * @var WikiDB_backend $_backend
45      */
46     public $_backend;
47
48     /**
49      * @see open()
50      */
51     function __construct(&$backend, $dbparams)
52     {
53         /**
54          * @var WikiRequest $request
55          */
56         global $request;
57
58         $this->_backend =& $backend;
59         // don't do the following with the auth_dsn!
60         if (isset($dbparams['auth_dsn']))
61             return;
62
63         $this->_cache = new WikiDB_cache($backend);
64         if (!empty($request))
65             $request->_dbi = $this;
66
67         // If the database doesn't yet have a timestamp, initialize it now.
68         if ($this->get('_timestamp') === false)
69             $this->touch();
70
71         // devel checking.
72         if ((int)DEBUG & _DEBUG_SQL) {
73             $this->_backend->check();
74         }
75         // might be changed when opening the database fails
76         $this->readonly = defined("READONLY") ? READONLY : false;
77     }
78
79     /**
80      * Open a WikiDB database.
81      *
82      * This function inspects its arguments to determine the proper
83      * subclass of WikiDB to instantiate, and then it instantiates it.
84      *
85      * @param array $dbparams Database configuration parameters (hash).
86      * Some pertinent parameters are:
87      * <dl>
88      * <dt> dbtype
89      * <dd> The back-end type.  Current supported types are:
90      *   <dl>
91      *   <dt> SQL
92      *     <dd> Generic SQL backend based on the PEAR/DB database abstraction
93      *       library. (More stable and conservative)
94      *   <dt> ADODB
95      *     <dd> Another generic SQL backend. (More current features are tested here. Much faster)
96      *   <dt> dba
97      *     <dd> Dba based backend. The default and by far the fastest.
98      *   <dt> file
99      *     <dd> flat files
100      *   </dl>
101      *
102      * <dt> dsn
103      * <dd> (Used by the SQL and ADODB backends.)
104      *      The DSN specifying which database to connect to.
105      *
106      * <dt> prefix
107      * <dd> Prefix to be prepended to database tables (and file names).
108      *
109      * <dt> directory
110      * <dd> (Used by the dba backend.)
111      *      Which directory db files reside in.
112      *
113      * <dt> timeout
114      * <dd> Used only by the dba backend so far.
115      *      And: When optimizing mysql it closes timed out mysql processes.
116      *      otherwise only used for dba: Timeout in seconds for opening (and
117      *      obtaining lock) on the dbm file.
118      *
119      * <dt> dba_handler
120      * <dd> (Used by the dba backend.)
121      *
122      *      Which dba handler to use. Good choices are probably either
123      *      'gdbm' or 'db2'.
124      *
125      * <dt> readonly
126      * <dd> Either set by config.ini: READONLY = true or detected automatically
127      *      when a database can be read but cannot be updated.
128      * </dl>
129      *
130      * @return WikiDB A WikiDB object.
131      **/
132     public static function open($dbparams)
133     {
134         $dbtype = $dbparams{'dbtype'};
135         include_once("lib/WikiDB/$dbtype.php");
136
137         $class = 'WikiDB_' . $dbtype;
138         return new $class($dbparams);
139     }
140
141     /**
142      * Close database connection.
143      *
144      * The database may no longer be used after it is closed.
145      *
146      * Closing a WikiDB invalidates all <tt>WikiDB_Page</tt>s,
147      * <tt>WikiDB_PageRevision</tt>s and <tt>WikiDB_PageIterator</tt>s
148      * which have been obtained from it.
149      */
150     public function close()
151     {
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      * @param  string      $pagename Which page to get.
163      * @return WikiDB_Page The requested WikiDB_Page.
164      */
165     public function getPage($pagename)
166     {
167         static $error_displayed = false;
168         $pagename = (string)$pagename;
169         if ((int)DEBUG) {
170             if ($pagename === '') {
171                 if ($error_displayed) return false;
172                 $error_displayed = true;
173                 if (function_exists("xdebug_get_function_stack"))
174                     var_dump(xdebug_get_function_stack());
175                 trigger_error("empty pagename", E_USER_WARNING);
176                 return false;
177             }
178         } else {
179             assert($pagename != '');
180         }
181         return new WikiDB_Page($this, $pagename);
182     }
183
184     /**
185      * Determine whether page exists (in non-default form).
186      *
187      * <pre>
188      *   $is_page = $dbi->isWikiPage($pagename);
189      * </pre>
190      * is equivalent to
191      * <pre>
192      *   $page = $dbi->getPage($pagename);
193      *   $current = $page->getCurrentRevision();
194      *   $is_page = ! $current->hasDefaultContents();
195      * </pre>
196      * however isWikiPage may be implemented in a more efficient
197      * manner in certain back-ends.
198      *
199      * @param  string  $pagename string Which page to check.
200      * @return boolean True if the page actually exists with
201      * non-default contents in the WikiDataBase.
202      */
203     public function isWikiPage($pagename)
204     {
205         $page = $this->getPage($pagename);
206         return ($page and $page->exists());
207     }
208
209     /**
210      * Delete page from the WikiDB.
211      *
212      * Deletes the page from the WikiDB with the possibility to revert and diff.
213      * //Also resets all page meta-data to the default values.
214      *
215      * Note: purgePage() effectively destroys all revisions of the page from the WikiDB.
216      *
217      * @param string $pagename Name of page to delete.
218      * @see purgePage
219      * @return mixed
220      */
221     public function deletePage($pagename)
222     {
223         if (!empty($this->readonly)) {
224             trigger_error("readonly database", E_USER_WARNING);
225             return false;
226         }
227         // don't create empty revisions of already purged pages.
228         if ($this->_backend->get_latest_version($pagename))
229             $result = $this->_cache->delete_page($pagename);
230         else
231             $result = -1;
232
233         /* Generate notification emails */
234         if (defined('ENABLE_MAILNOTIFY') and ENABLE_MAILNOTIFY) {
235             include_once 'lib/MailNotify.php';
236             $MailNotify = new MailNotify($pagename);
237             $MailNotify->onDeletePage($this, $pagename);
238         }
239
240         //How to create a RecentChanges entry with explaining summary? Dynamically
241         /*
242         $page = $this->getPage($pagename);
243         $current = $page->getCurrentRevision();
244         $meta = $current->_data;
245         $version = $current->getVersion();
246         $meta['summary'] = _("removed");
247         $page->save($current->getPackedContent(), $version + 1, $meta);
248         */
249         return $result;
250     }
251
252     /**
253      * Completely remove the page from the WikiDB, without undo possibility.
254      * @param string $pagename Name of page to delete.
255      * @return bool
256      * @see deletePage
257      */
258     public function purgePage($pagename)
259     {
260         if (!empty($this->readonly)) {
261             trigger_error("readonly database", E_USER_WARNING);
262             return false;
263         }
264         $result = $this->_cache->purge_page($pagename);
265         $this->deletePage($pagename); // just for the notification
266         return $result;
267     }
268
269     /**
270      * Retrieve all pages.
271      *
272      * Gets the set of all pages with non-default contents.
273      *
274      * @param bool $include_empty Optional. Normally pages whose most
275      * recent revision has empty content are considered to be
276      * non-existant. Unless $include_defaulted is set to true, those
277      * pages will not be returned.
278      * @param string $sortby Optional. "+-column,+-column2".
279      *        If false the result is faster in natural order.
280      * @param string $limit Optional. Encoded as "$offset,$count".
281      *         $offset defaults to 0.
282      * @param string $exclude  Optional comma-separated list of pagenames.
283      *
284      * @return WikiDB_PageIterator A WikiDB_PageIterator which contains all pages
285      *     in the WikiDB which have non-default contents.
286      */
287     public function getAllPages($include_empty = false, $sortby = '', $limit = '', $exclude = '')
288     {
289         $result = $this->_backend->get_all_pages($include_empty, $sortby, $limit,
290             $exclude);
291         return new WikiDB_PageIterator($this, $result,
292             array('include_empty' => $include_empty,
293                 'exclude' => $exclude,
294                 'limit_by_db' => $result->_options['limit_by_db'],
295                 'limit' => $result->limit()));
296     }
297
298     /**
299      * @param boolean $include_empty If true include also empty pages
300      * @param string  $exclude:      comma-separated list of pagenames.
301      *             TBD: array of pagenames
302      * @return integer
303      *
304      */
305     public function numPages($include_empty = false, $exclude = '')
306     {
307         if (method_exists($this->_backend, 'numPages'))
308             // FIXME: currently are all args ignored.
309             $count = $this->_backend->numPages($include_empty, $exclude);
310         else {
311             // FIXME: exclude ignored.
312             $iter = $this->getAllPages($include_empty, false, false, $exclude);
313             $count = $iter->count();
314             $iter->free();
315         }
316         return (int)$count;
317     }
318
319     /**
320      * Title search.
321      *
322      * Search for pages containing (or not containing) certain words
323      * in their names.
324      *
325      * Pages are returned in alphabetical order whenever it is
326      * practical to do so.
327      * TODO: Sort by ranking. Only postgresql with tsearch2 can do ranking so far.
328      *
329      * @param TextSearchQuery $search A TextSearchQuery object
330      * @param string $sortby Optional. "+-column,+-column2".
331      *        If false the result is faster in natural order.
332      * @param string $limit Optional. Encoded as "$offset,$count".
333      *         $offset defaults to 0.
334      * @param  string $exclude Optional comma-separated list of pagenames.
335      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
336      * @see TextSearchQuery
337      */
338     public function titleSearch($search, $sortby = 'pagename', $limit = '', $exclude = '')
339     {
340         $result = $this->_backend->text_search($search, false, $sortby, $limit, $exclude);
341         $options = array('exclude' => $exclude,
342             'limit' => $limit);
343         //if (isset($result->_count)) $options['count'] = $result->_count;
344         return new WikiDB_PageIterator($this, $result, $options);
345     }
346
347     /**
348      * Full text search.
349      *
350      * Search for pages containing (or not containing) certain words
351      * in their entire text (this includes the page content and the
352      * page name).
353      *
354      * Pages are returned in alphabetical order whenever it is
355      * practical to do so.
356      * TODO: Sort by ranking. Only postgresql with tsearch2 can do ranking so far.
357      *
358      * @param TextSearchQuery $search A TextSearchQuery object.
359      * @param string $sortby Optional. "+-column,+-column2".
360      *        If false the result is faster in natural order.
361      * @param string $limit Optional. Encoded as "$offset,$count".
362      *         $offset defaults to 0.
363      * @param  string $exclude Optional comma-separated list of pagenames.
364      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching pages.
365      * @see TextSearchQuery
366      */
367     public function fullSearch($search, $sortby = 'pagename', $limit = '', $exclude = '')
368     {
369         $result = $this->_backend->text_search($search, true, $sortby, $limit, $exclude);
370         return new WikiDB_PageIterator($this, $result,
371             array('exclude' => $exclude,
372                 'limit' => $limit,
373                 'stoplisted' => $result->stoplisted
374             ));
375     }
376
377     /**
378      * Find the pages with the greatest hit counts.
379      *
380      * Pages are returned in reverse order by hit count.
381      *
382      * @param int $limit The maximum number of pages to return.
383      * Set $limit to zero to return all pages.  If $limit < 0, pages will
384      * be sorted in decreasing order of popularity.
385      * @param string $sortby Optional. "+-column,+-column2".
386      *        If false the result is faster in natural order.
387      *
388      * @return WikiDB_PageIterator A WikiDB_PageIterator containing the matching
389      * pages.
390      */
391     public function mostPopular($limit = 20, $sortby = '-hits')
392     {
393         $result = $this->_backend->most_popular($limit, $sortby);
394         return new WikiDB_PageIterator($this, $result);
395     }
396
397     /**
398      * Find recent page revisions.
399      *
400      * Revisions are returned in reverse order by creation time.
401      *
402      * @param array $params This hash is used to specify various optional
403      *   parameters:
404      * <dl>
405      * <dt> limit
406      *    <dd> (integer) At most this many revisions will be returned.
407      * <dt> since
408      *    <dd> (integer) Only revisions since this time (unix-timestamp)
409      *        will be returned.
410      * <dt> include_minor_revisions
411      *    <dd> (boolean) Also include minor revisions.  (Default is not to.)
412      * <dt> exclude_major_revisions
413      *    <dd> (boolean) Don't include non-minor revisions.
414      *         (Exclude_major_revisions implies include_minor_revisions.)
415      * <dt> include_all_revisions
416      *    <dd> (boolean) Return all matching revisions for each page.
417      *         Normally only the most recent matching revision is returned
418      *         for each page.
419      * </dl>
420      *
421      * @return WikiDB_PageRevisionIterator A WikiDB_PageRevisionIterator
422      * containing the matching revisions.
423      */
424     public function mostRecent($params = array())
425     {
426         $result = $this->_backend->most_recent($params);
427         return new WikiDB_PageRevisionIterator($this, $result);
428     }
429
430     /**
431      * @param string $exclude_from
432      * @param string $exclude
433      * @param string $sortby Optional. "+-column,+-column2".
434      *        If false the result is faster in natural order.
435      * @param string $limit Optional. Encoded as "$offset,$count".
436      *         $offset defaults to 0.
437      * @return Iterator A generic iterator containing rows of
438      *         (duplicate) pagename, wantedfrom.
439      */
440     public function wantedPages($exclude_from = '', $exclude = '', $sortby = '', $limit = '')
441     {
442         return $this->_backend->wanted_pages($exclude_from, $exclude, $sortby, $limit);
443     }
444
445     /**
446      * Generic interface to the link table. Esp. useful to search for rdf triples as in
447      * SemanticSearch and ListRelations.
448      *
449      * @param object $pages   A TextSearchQuery object.
450      * @param object $search  A TextSearchQuery object.
451      * @param string $linktype One of "linkto", "linkfrom", "relation", "attribute".
452      *   linktype parameter:
453      * <dl>
454      * <dt> "linkto"
455      *    <dd> search for simple out-links
456      * <dt> "linkfrom"
457      *    <dd> in-links, i.e BackLinks
458      * <dt> "relation"
459      *    <dd> the first part in a <>::<> link
460      * <dt> "attribute"
461      *    <dd> the first part in a <>:=<> link
462      * </dl>
463      * @param mixed $relation An optional TextSearchQuery to match the
464      * relation name. Ignored on simple in-out links.
465      *
466      * @return Iterator A generic iterator containing links to pages or values.
467      *                  hash of "pagename", "linkname", "linkvalue.
468      */
469     public function linkSearch($pages, $search, $linktype, $relation = false)
470     {
471         return $this->_backend->link_search($pages, $search, $linktype, $relation);
472     }
473
474     /**
475      * Return a simple list of all defined relations (and attributes), mainly
476      * for the SemanticSearch autocompletion.
477      *
478      * @param bool $also_attributes
479      * @param bool $only_attributes
480      * @param bool $sorted
481      * @return array of strings
482      */
483     public function listRelations($also_attributes = false, $only_attributes = false, $sorted = true)
484     {
485         if (method_exists($this->_backend, "list_relations"))
486             return $this->_backend->list_relations($also_attributes, $only_attributes, $sorted);
487         // dumb, slow fallback. no iter, so simply define it here.
488         $relations = array();
489         $iter = $this->getAllPages();
490         while ($page = $iter->next()) {
491             $reliter = $page->getRelations();
492             $names = array();
493             while ($rel = $reliter->next()) {
494                 // if there's no pagename it's an attribute
495                 $names[] = $rel->getName();
496             }
497             $relations = array_merge($relations, $names);
498             $reliter->free();
499         }
500         $iter->free();
501         if ($sorted) {
502             sort($relations);
503             reset($relations);
504         }
505         return $relations;
506     }
507
508     /**
509      * Call the appropriate backend method.
510      *
511      * @param  string  $from            Page to rename
512      * @param  string  $to              New name
513      * @param  boolean $updateWikiLinks If the text in all pages should be replaced.
514      * @return boolean true or false
515      */
516     public function renamePage($from, $to, $updateWikiLinks = false)
517     {
518         /**
519          * @var WikiRequest $request
520          */
521         global $request;
522
523         if (!empty($this->readonly)) {
524             trigger_error("readonly database", E_USER_WARNING);
525             return false;
526         }
527         assert(is_string($from) && $from != '');
528         assert(is_string($to) && $to != '');
529         $result = false;
530         if (method_exists($this->_backend, 'rename_page')) {
531             $oldpage = $this->getPage($from);
532             $newpage = $this->getPage($to);
533             //update all WikiLinks in existing pages
534             //non-atomic! i.e. if rename fails the links are not undone
535             if ($updateWikiLinks) {
536                 $lookbehind = "/(?<=[\W:])\Q";
537                 $lookahead = "\E(?=[\W:])/";
538                 require_once 'lib/plugin/WikiAdminSearchReplace.php';
539                 $links = $oldpage->getBackLinks();
540                 while ($linked_page = $links->next()) {
541                     WikiPlugin_WikiAdminSearchReplace::replaceHelper
542                     ($this,
543                         $linked_page->getName(),
544                         $lookbehind . $from . $lookahead, $to,
545                         true, true);
546                 }
547                 // FIXME: Disabled to avoid recursive modification when renaming
548                 // a page like 'PageFoo to 'PageFooTwo'
549                 if (0) {
550                     $links = $newpage->getBackLinks();
551                     while ($linked_page = $links->next()) {
552                         WikiPlugin_WikiAdminSearchReplace::replaceHelper
553                         ($this,
554                             $linked_page->getName(),
555                             $lookbehind . $from . $lookahead, $to,
556                             true, true);
557                     }
558                 }
559             }
560             if ($oldpage->exists() and !$newpage->exists()) {
561                 if ($result = $this->_backend->rename_page($from, $to)) {
562                     // create a RecentChanges entry with explaining summary
563                     $page = $this->getPage($to);
564                     $current = $page->getCurrentRevision();
565                     $meta = $current->_data;
566                     $version = $current->getVersion();
567                     $meta['summary'] = sprintf(_("renamed from %s"), $from);
568                     unset($meta['mtime']); // force new date
569                     $page->save($current->getPackedContent(), $version + 1, $meta);
570                 }
571             } elseif (!$oldpage->getCurrentRevision(false) and !$newpage->exists()) {
572                 // if a version 0 exists try it also.
573                 $result = $this->_backend->rename_page($from, $to);
574             }
575         } else {
576             trigger_error(_("WikiDB::renamePage() not yet implemented for this backend"),
577                 E_USER_WARNING);
578         }
579         /* Generate notification emails? */
580         if ($result and ENABLE_MAILNOTIFY and !is_a($request, 'MockRequest')) {
581             $notify = $this->get('notify');
582             if (!empty($notify) and is_array($notify)) {
583                 include_once 'lib/MailNotify.php';
584                 $MailNotify = new MailNotify($from);
585                 $MailNotify->onRenamePage($this, $from, $to);
586             }
587         }
588         return $result;
589     }
590
591     /** Get timestamp when database was last modified.
592      *
593      * @return string A string consisting of two integers,
594      * separated by a space.  The first is the time in
595      * unix timestamp format, the second is a modification
596      * count for the database.
597      *
598      * The idea is that you can cast the return value to an
599      * int to get a timestamp, or you can use the string value
600      * as a good hash for the entire database.
601      */
602     public function getTimestamp()
603     {
604         $ts = $this->get('_timestamp');
605         return sprintf("%d %d", $ts[0], $ts[1]);
606     }
607
608     /**
609      * Update the database timestamp.
610      */
611     public function touch()
612     {
613         $ts = $this->get('_timestamp');
614         $this->set('_timestamp', array(time(), $ts[1] + 1));
615     }
616
617     /**
618      * Roughly similar to the float in phpwiki_version(). Set by action=upgrade.
619      */
620     public function get_db_version()
621     {
622         return (float)$this->get('_db_version');
623     }
624
625     public function set_db_version($ver)
626     {
627         $this->set('_db_version', (float)$ver);
628     }
629
630     /**
631      * Access WikiDB global meta-data.
632      *
633      * NOTE: this is currently implemented in a hackish and
634      * not very efficient manner.
635      *
636      * @param string $key Which meta data to get.
637      * Some reserved meta-data keys are:
638      * <dl>
639      * <dt>'_timestamp' <dd> Data used by getTimestamp().
640      * </dl>
641      *
642      * @return mixed The requested value, or false if the requested data
643      * is not set.
644      */
645     public function get($key)
646     {
647         if (!$key || $key[0] == '%')
648             return false;
649         /*
650          * Hack Alert: We can use any page (existing or not) to store
651          * this data (as long as we always use the same one.)
652          */
653         $gd = $this->getPage('global_data');
654         $data = $gd->get('__global');
655
656         if ($data && isset($data[$key]))
657             return $data[$key];
658         else
659             return false;
660     }
661
662     /**
663      * Set global meta-data.
664      *
665      * NOTE: this is currently implemented in a hackish and
666      * not very efficient manner.
667      *
668      * @see get
669      *
670      * @param string $key    Meta-data key to set.
671      * @param string $newval New value.
672      */
673     public function set($key, $newval)
674     {
675         if (!empty($this->readonly)) {
676             trigger_error("readonly database", E_USER_WARNING);
677             return;
678         }
679         if (!$key || $key[0] == '%')
680             return;
681
682         $gd = $this->getPage('global_data');
683         $data = $gd->get('__global');
684         if ($data === false)
685             $data = array();
686
687         if (empty($newval))
688             unset($data[$key]);
689         else
690             $data[$key] = $newval;
691
692         $gd->set('__global', $data);
693     }
694
695     /* TODO: these are really backend methods */
696
697     // SQL result: for simple select or create/update queries
698     // returns the database specific resource type
699     public function genericSqlQuery($sql, $args = false)
700     {
701         echo "<pre>";
702         printSimpleTrace(debug_backtrace());
703         echo "</pre>\n";
704         trigger_error("no SQL database", E_USER_ERROR);
705         return false;
706     }
707
708     // SQL iter: for simple select or create/update queries
709     // returns the generic iterator object (count,next)
710     public function genericSqlIter($sql, $field_list = NULL)
711     {
712         echo "<pre>";
713         printSimpleTrace(debug_backtrace());
714         echo "</pre>\n";
715         trigger_error("no SQL database", E_USER_ERROR);
716         return false;
717     }
718
719     // see backend upstream methods
720     // ADODB adds surrounding quotes, SQL not yet!
721     public function quote($s)
722     {
723         return $s;
724     }
725
726     public function isOpen()
727     {
728         return false; /* so far only needed for sql so false it.
729                          later we have to check dba also */
730     }
731
732     public function getParam($param)
733     {
734         global $DBParams;
735         if (isset($DBParams[$param]))
736             return $DBParams[$param];
737         elseif ($param == 'prefix')
738             return '';
739         else
740             return false;
741     }
742
743     public function getAuthParam($param)
744     {
745         global $DBAuthParams;
746         if (isset($DBAuthParams[$param]))
747             return $DBAuthParams[$param];
748         elseif ($param == 'USER_AUTH_ORDER')
749             return $GLOBALS['USER_AUTH_ORDER'];
750         elseif ($param == 'USER_AUTH_POLICY')
751             return $GLOBALS['USER_AUTH_POLICY'];
752         else
753             return array();
754     }
755 }
756
757 /**
758  * A base class which representing a wiki-page within a
759  * WikiDB.
760  *
761  * A WikiDB_Page contains a number (at least one) of
762  * WikiDB_PageRevisions.
763  */
764 class WikiDB_Page
765 {
766     public $score;
767     public $_wikidb;
768     public $_pagename;
769
770     function __construct(&$wikidb, $pagename)
771     {
772         $this->_wikidb = &$wikidb;
773         $this->_pagename = $pagename;
774         if ((int)DEBUG) {
775             if (!(is_string($pagename) and $pagename != '')) {
776                 if (function_exists("xdebug_get_function_stack")) {
777                     echo "xdebug_get_function_stack(): ";
778                     var_dump(xdebug_get_function_stack());
779                 } else {
780                     printSimpleTrace(debug_backtrace());
781                 }
782                 trigger_error("empty pagename", E_USER_WARNING);
783                 return;
784             }
785         } else {
786             assert(is_string($pagename) and $pagename != '');
787         }
788     }
789
790     /**
791      * Get the name of the wiki page.
792      *
793      * @return string The page name.
794      */
795     public function getName()
796     {
797         return $this->_pagename;
798     }
799
800     // To reduce the memory footprint for larger sets of pagelists,
801     // we don't cache the content (only true or false) and
802     // we purge the pagedata (_cached_html) also
803     public function exists()
804     {
805         if (isset($this->_wikidb->_cache->_id_cache[$this->_pagename])) return true;
806         $current = $this->getCurrentRevision(false);
807         if (!$current) return false;
808         return !$current->hasDefaultContents();
809     }
810
811     public function getVersion()
812     {
813         $backend = &$this->_wikidb->_backend;
814         $pagename = &$this->_pagename;
815         return $backend->get_latest_version($pagename);
816     }
817
818     /**
819      * Delete an old revision of a WikiDB_Page.
820      *
821      * Deletes the specified revision of the page.
822      * It is a fatal error to attempt to delete the current revision.
823      *
824      * @param integer $version Which revision to delete.  (You can also
825      *  use a WikiDB_PageRevision object here.)
826      */
827     public function deleteRevision($version)
828     {
829         if ($this->_wikidb->readonly) {
830             trigger_error("readonly database", E_USER_WARNING);
831             return;
832         }
833         $backend = &$this->_wikidb->_backend;
834         $cache = &$this->_wikidb->_cache;
835         $pagename = &$this->_pagename;
836
837         $version = $this->_coerce_to_version($version);
838         if ($version == 0)
839             return;
840
841         $backend->lock(array('page', 'version'));
842         $latestversion = $cache->get_latest_version($pagename);
843         if ($latestversion && ($version == $latestversion)) {
844             $backend->unlock(array('page', 'version'));
845             trigger_error(sprintf("Attempt to delete most recent revision of “%s”",
846                 $pagename), E_USER_ERROR);
847             return;
848         }
849
850         $cache->delete_versiondata($pagename, $version);
851         $backend->unlock(array('page', 'version'));
852     }
853
854     /*
855      * Delete a revision, or possibly merge it with a previous
856      * revision.
857      *
858      * The idea is this:
859      * Suppose an author make a (major) edit to a page.  Shortly
860      * after that the same author makes a minor edit (e.g. to fix
861      * spelling mistakes he just made.)
862      *
863      * Now some time later, where cleaning out old saved revisions,
864      * and would like to delete his minor revision (since there's
865      * really no point in keeping minor revisions around for a long
866      * time.)
867      *
868      * Note that the text after the minor revision probably represents
869      * what the author intended to write better than the text after
870      * the preceding major edit.
871      *
872      * So what we really want to do is merge the minor edit with the
873      * preceding edit.
874      *
875      * We will only do this when:
876      * <ul>
877      * <li>The revision being deleted is a minor one, and
878      * <li>It has the same author as the immediately preceding revision.
879      * </ul>
880      */
881     public function mergeRevision($version)
882     {
883         if ($this->_wikidb->readonly) {
884             trigger_error("readonly database", E_USER_WARNING);
885             return;
886         }
887         $backend = &$this->_wikidb->_backend;
888         $cache = &$this->_wikidb->_cache;
889         $pagename = &$this->_pagename;
890
891         $version = $this->_coerce_to_version($version);
892         if ($version == 0)
893             return;
894
895         $backend->lock(array('version'));
896         $latestversion = $cache->get_latest_version($pagename);
897         if ($latestversion && $version == $latestversion) {
898             $backend->unlock(array('version'));
899             trigger_error(sprintf("Attempt to merge most recent revision of “%s”",
900                 $pagename), E_USER_ERROR);
901             return;
902         }
903
904         $versiondata = $cache->get_versiondata($pagename, $version, true);
905         if (!$versiondata) {
906             // Not there? ... we're done!
907             $backend->unlock(array('version'));
908             return;
909         }
910
911         if ($versiondata['is_minor_edit']) {
912             $previous = $backend->get_previous_version($pagename, $version);
913             if ($previous) {
914                 $prevdata = $cache->get_versiondata($pagename, $previous);
915                 if ($prevdata['author_id'] == $versiondata['author_id']) {
916                     // This is a minor revision, previous version is
917                     // by the same author. We will merge the
918                     // revisions.
919                     $cache->update_versiondata($pagename, $previous,
920                         array('%content' => $versiondata['%content'],
921                             '_supplanted' => $versiondata['_supplanted']));
922                 }
923             }
924         }
925
926         $cache->delete_versiondata($pagename, $version);
927         $backend->unlock(array('version'));
928     }
929
930     /**
931      * Create a new revision of a {@link WikiDB_Page}.
932      *
933      * @param int $version Version number for new revision.
934      * To ensure proper serialization of edits, $version must be
935      * exactly one higher than the current latest version.
936      * (You can defeat this check by setting $version to
937      * {@link WIKIDB_FORCE_CREATE} --- not usually recommended.)
938      *
939      * @param string $content Contents of new revision.
940      *
941      * @param array $metadata Metadata for new revision (hash).
942      * All values in the hash should be scalars (strings or integers).
943      *
944      * @param array $links List of linkto=>pagename, relation=>pagename which this page links to (hash).
945      *
946      * @return WikiDB_PageRevision Returns the new WikiDB_PageRevision object. If
947      * $version was incorrect, returns false
948      */
949     public function createRevision($version, &$content, $metadata, $links)
950     {
951         if ($this->_wikidb->readonly) {
952             trigger_error("readonly database", E_USER_WARNING);
953             return false;
954         }
955         $backend = &$this->_wikidb->_backend;
956         $cache = &$this->_wikidb->_cache;
957         $pagename = &$this->_pagename;
958         $cache->invalidate_cache($pagename);
959
960         $backend->lock(array('version', 'page', 'recent', 'link', 'nonempty'));
961
962         $latestversion = $backend->get_latest_version($pagename);
963         $newversion = ($latestversion ? $latestversion : 0) + 1;
964         assert($newversion >= 1);
965
966         if ($version != WIKIDB_FORCE_CREATE and $version != $newversion) {
967             $backend->unlock(array('version', 'page', 'recent', 'link', 'nonempty'));
968             return false;
969         }
970
971         $data = $metadata;
972
973         foreach ($data as $key => $val) {
974             if (empty($val) || $key[0] == '_' || $key[0] == '%')
975                 unset($data[$key]);
976         }
977
978         assert(!empty($data['author']));
979         if (empty($data['author_id']))
980             @$data['author_id'] = $data['author'];
981
982         if (empty($data['mtime']))
983             $data['mtime'] = time();
984
985         if ($latestversion and $version != WIKIDB_FORCE_CREATE) {
986             // Ensure mtimes are monotonic.
987             $pdata = $cache->get_versiondata($pagename, $latestversion);
988             if ($data['mtime'] < $pdata['mtime']) {
989                 trigger_error(sprintf(_("%s: Date of new revision is %s"),
990                         $pagename, "'non-monotonic'"),
991                     E_USER_NOTICE);
992                 $data['orig_mtime'] = $data['mtime'];
993                 $data['mtime'] = $pdata['mtime'];
994             }
995
996             // FIXME: use (possibly user specified) 'mtime' time or
997             // time()?
998             $cache->update_versiondata($pagename, $latestversion,
999                 array('_supplanted' => $data['mtime']));
1000         }
1001
1002         $data['%content'] = &$content;
1003
1004         $cache->set_versiondata($pagename, $newversion, $data);
1005
1006         //$cache->update_pagedata($pagename, array(':latestversion' => $newversion,
1007         //':deleted' => empty($content)));
1008
1009         $backend->set_links($pagename, $links);
1010
1011         $backend->unlock(array('version', 'page', 'recent', 'link', 'nonempty'));
1012
1013         return new WikiDB_PageRevision($this->_wikidb, $pagename, $newversion, $data);
1014     }
1015
1016     /** A higher-level interface to createRevision.
1017      *
1018      * This takes care of computing the links, and storing
1019      * a cached version of the transformed wiki-text.
1020      *
1021      * @param string $wikitext The page content.
1022      *
1023      * @param int $version Version number for new revision.
1024      * To ensure proper serialization of edits, $version must be
1025      * exactly one higher than the current latest version.
1026      * (You can defeat this check by setting $version to
1027      * {@link WIKIDB_FORCE_CREATE} --- not usually recommended.)
1028      *
1029      * @param array $meta Meta-data for new revision.
1030      *
1031      * @param mixed $formatted
1032      *
1033      * @return mixed
1034      */
1035     public function save($wikitext, $version, $meta, $formatted = null)
1036     {
1037         /**
1038          * @var WikiRequest $request
1039          */
1040         global $request;
1041
1042         if ($this->_wikidb->readonly) {
1043             trigger_error("readonly database", E_USER_WARNING);
1044             return false;
1045         }
1046         if (is_null($formatted))
1047             $formatted = new TransformedText($this, $wikitext, $meta);
1048         $type = $formatted->getType();
1049         $meta['pagetype'] = $type->getName();
1050         $links = $formatted->getWikiPageLinks(); // linkto => relation
1051         $attributes = array();
1052         foreach ($links as $link) {
1053             if ($link['linkto'] === "" and !empty($link['relation'])) {
1054                 $attributes[$link['relation']] = $this->getAttribute($link['relation']);
1055             }
1056         }
1057         $meta['attribute'] = $attributes;
1058
1059         $backend = &$this->_wikidb->_backend;
1060         $newrevision = $this->createRevision($version, $wikitext, $meta, $links);
1061         if ($newrevision and !WIKIDB_NOCACHE_MARKUP)
1062             $this->set('_cached_html', $formatted->pack());
1063
1064         // FIXME: probably should have some global state information
1065         // in the backend to control when to optimize.
1066         //
1067         // We're doing this here rather than in createRevision because
1068         // postgresql can't optimize while locked.
1069         if (((int)DEBUG & _DEBUG_SQL)
1070             or (DATABASE_OPTIMISE_FREQUENCY > 0 and
1071                 (time() % DATABASE_OPTIMISE_FREQUENCY == 0))
1072         ) {
1073             if ($backend->optimize()) {
1074                 if ((int)DEBUG) {
1075                     trigger_error(_("Optimizing database"), E_USER_NOTICE);
1076                 }
1077             }
1078         }
1079
1080         /* Generate notification emails? */
1081         if (ENABLE_MAILNOTIFY and is_a($newrevision, 'WikiDB_PageRevision')) {
1082             // Save didn't fail because of concurrent updates.
1083             $notify = $this->_wikidb->get('notify');
1084             if (!empty($notify)
1085                 and is_array($notify)
1086                     and !is_a($request, 'MockRequest')
1087             ) {
1088                 include_once 'lib/MailNotify.php';
1089                 $MailNotify = new MailNotify($newrevision->getName());
1090                 $MailNotify->onChangePage($this->_wikidb, $wikitext, $version, $meta);
1091             }
1092             $newrevision->_transformedContent = $formatted;
1093         }
1094         // more pagechange callbacks: (in a hackish manner for now)
1095         if (ENABLE_RECENTCHANGESBOX
1096             and empty($meta['is_minor_edit'])
1097                 and !in_array($request->getArg('action'),
1098                     array('loadfile', 'upgrade'))
1099         ) {
1100             require_once 'lib/WikiPlugin.php';
1101             $w = new WikiPluginLoader();
1102             $p = $w->getPlugin("RecentChangesCached", false);
1103             $p->box_update(false, $request, $this->_pagename);
1104         }
1105         return $newrevision;
1106     }
1107
1108     /**
1109      * Get the most recent revision of a page.
1110      *
1111      * @param bool $need_content
1112      * @return WikiDB_PageRevision The current WikiDB_PageRevision object.
1113      */
1114     public function getCurrentRevision($need_content = true)
1115     {
1116         $cache = &$this->_wikidb->_cache;
1117         $pagename = &$this->_pagename;
1118
1119         // Prevent deadlock in case of memory exhausted errors
1120         // Pure selection doesn't really need locking here.
1121         //   sf.net bug#927395
1122         // I know it would be better to lock, but with lots of pages this deadlock is more
1123         // severe than occasionally get not the latest revision.
1124         // In spirit to wikiwiki: read fast, edit slower.
1125         //$backend->lock();
1126         $version = $cache->get_latest_version($pagename);
1127         // getRevision gets the content also!
1128         $revision = $this->getRevision($version, $need_content);
1129         //$backend->unlock();
1130         assert($revision);
1131         return $revision;
1132     }
1133
1134     /**
1135      * Get a specific revision of a WikiDB_Page.
1136      *
1137      * @param int $version Which revision to get.
1138      * @param bool $need_content
1139      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or
1140      * false if the requested revision does not exist in the {@link WikiDB}.
1141      * Note that version zero of any page always exists.
1142      */
1143     public function getRevision($version, $need_content = true)
1144     {
1145         $cache = &$this->_wikidb->_cache;
1146         $pagename = &$this->_pagename;
1147
1148         if ((!$version) or ($version == 0) or ($version == -1)) { // 0 or false
1149             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
1150         }
1151
1152         assert($version > 0);
1153         $vdata = $cache->get_versiondata($pagename, $version, $need_content);
1154         if (!$vdata) {
1155             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
1156         }
1157         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
1158             $vdata);
1159     }
1160
1161     /**
1162      * Get previous page revision.
1163      *
1164      * This method find the most recent revision before a specified
1165      * version.
1166      *
1167      * @param bool|int|WikiDB_PageRevision $version Find most recent revision before this version.
1168      *
1169      * @param bool $need_content
1170      *
1171      * @return WikiDB_PageRevision|bool The requested WikiDB_PageRevision object, or false if the
1172      * requested revision does not exist in the {@link WikiDB}.  Note that
1173      * unless $version is greater than zero, a revision (perhaps version zero,
1174      * the default revision) will always be found.
1175      */
1176     public function getRevisionBefore($version = false, $need_content = true)
1177     {
1178         $backend = &$this->_wikidb->_backend;
1179         $pagename = &$this->_pagename;
1180         if ($version === false)
1181             $version = $this->_wikidb->_cache->get_latest_version($pagename);
1182         else
1183             $version = $this->_coerce_to_version($version);
1184
1185         if ($version == 0)
1186             return false;
1187         //$backend->lock();
1188         $previous = $backend->get_previous_version($pagename, $version);
1189         $revision = $this->getRevision($previous, $need_content);
1190         //$backend->unlock();
1191         assert($revision);
1192         return $revision;
1193     }
1194
1195     /**
1196      * Get all revisions of the WikiDB_Page.
1197      *
1198      * This does not include the version zero (default) revision in the
1199      * returned revision set.
1200      *
1201      * @return WikiDB_PageRevisionIterator A
1202      *   WikiDB_PageRevisionIterator containing all revisions of this
1203      *   WikiDB_Page in reverse order by version number.
1204      */
1205     public function getAllRevisions()
1206     {
1207         $backend = &$this->_wikidb->_backend;
1208         $revs = $backend->get_all_revisions($this->_pagename);
1209         return new WikiDB_PageRevisionIterator($this->_wikidb, $revs);
1210     }
1211
1212     /**
1213      * Find pages which link to or are linked from a page.
1214      * relations: $backend->get_links is responsible to add the relation to the pagehash
1215      * as 'linkrelation' key as pagename. See WikiDB_PageIterator::next
1216      *   if (isset($next['linkrelation']))
1217      *
1218      * @param bool $reversed Which links to find: true for backlinks (default).
1219      * @param bool $include_empty
1220      * @param string $sortby
1221      * @param string $limit
1222      * @param string $sortby
1223      * @param string $exclude
1224      * @param bool $want_relations
1225      * @return WikiDB_PageIterator A WikiDB_PageIterator containing
1226      * all matching pages.
1227      */
1228     public function getLinks($reversed = true, $include_empty = false, $sortby = '',
1229                       $limit = '', $exclude = '', $want_relations = false)
1230     {
1231         $backend = &$this->_wikidb->_backend;
1232         $result = $backend->get_links($this->_pagename, $reversed,
1233             $include_empty, $sortby, $limit, $exclude,
1234             $want_relations);
1235         return new WikiDB_PageIterator($this->_wikidb, $result,
1236             array('include_empty' => $include_empty,
1237                 'sortby' => $sortby,
1238                 'limit' => $limit,
1239                 'exclude' => $exclude,
1240                 'want_relations' => $want_relations));
1241     }
1242
1243     /**
1244      * All Links from other pages to this page.
1245      */
1246     public function getBackLinks($include_empty = false, $sortby = '', $limit = '', $exclude = '')
1247     {
1248         return $this->getLinks(true, $include_empty, $sortby, $limit, $exclude);
1249     }
1250
1251     /**
1252      * Forward Links: All Links from this page to other pages.
1253      */
1254     public function getPageLinks($include_empty = false, $sortby = '', $limit = '', $exclude = '')
1255     {
1256         return $this->getLinks(false, $include_empty, $sortby, $limit, $exclude);
1257     }
1258
1259     /**
1260      * Relations: All links from this page to other pages with relation <> 0.
1261      * is_a:=page or population:=number
1262      */
1263     public function getRelations($sortby = '', $limit = '', $exclude = '')
1264     {
1265         $backend = &$this->_wikidb->_backend;
1266         $result = $backend->get_links($this->_pagename, false, true,
1267             $sortby, $limit, $exclude,
1268             true);
1269         // we do not care for the linked page versiondata, just the pagename and linkrelation
1270         return new WikiDB_PageIterator($this->_wikidb, $result,
1271             array('include_empty' => true,
1272                 'sortby' => $sortby,
1273                 'limit' => $limit,
1274                 'exclude' => $exclude,
1275                 'want_relations' => true));
1276     }
1277
1278     /**
1279      * possibly faster link existance check. not yet accelerated.
1280      */
1281     public function existLink($link, $reversed = false)
1282     {
1283         $backend = &$this->_wikidb->_backend;
1284         if (method_exists($backend, 'exists_link'))
1285             return $backend->exists_link($this->_pagename, $link, $reversed);
1286         //$cache = &$this->_wikidb->_cache;
1287         // TODO: check cache if it is possible
1288         $iter = $this->getLinks($reversed, false);
1289         while ($page = $iter->next()) {
1290             if ($page->getName() == $link)
1291                 return $page;
1292         }
1293         $iter->free();
1294         return false;
1295     }
1296
1297     /* Semantic relations are links with the relation pointing to another page,
1298        the so-called "RDF Triple".
1299        [San Diego] is%20a::city
1300        => "At the page San Diego there is a relation link of 'is a' to the page 'city'."
1301      */
1302
1303     /* Semantic attributes for a page.
1304        [San Diego] population:=1,305,736
1305        Attributes are links with the relation pointing to another page.
1306     */
1307
1308     /**
1309      * Access WikiDB_Page non version-specific meta-data.
1310      *
1311      * @param string $key Which meta data to get.
1312      * Some reserved meta-data keys are:
1313      * <dl>
1314      * <dt>'date'  <dd> Created as unixtime
1315      * <dt>'locked'<dd> Is page locked? 'yes' or 'no'
1316      * <dt>'hits'  <dd> Page hit counter.
1317      * <dt>'_cached_html' <dd> Transformed CachedMarkup object, serialized + optionally gzipped.
1318      *                         In SQL stored now in an extra column.
1319      * Optional data:
1320      * <dt>'pref'  <dd> Users preferences, stored only in homepages.
1321      * <dt>'owner' <dd> Default: first author_id. We might add a group with a dot here:
1322      *                  E.g. "owner.users"
1323      * <dt>'perm'  <dd> Permission flag to authorize read/write/execution of
1324      *                  page-headers and content.
1325     + <dt>'moderation'<dd> ModeratedPage data. Handled by plugin/ModeratedPage
1326      * <dt>'rating' <dd> Page rating. Handled by plugin/RateIt
1327      * </dl>
1328      *
1329      * @return mixed The requested value, or false if the requested data
1330      * is not set.
1331      */
1332     public function get($key)
1333     {
1334         $cache = &$this->_wikidb->_cache;
1335         $backend = &$this->_wikidb->_backend;
1336         if (!$key || $key[0] == '%')
1337             return false;
1338         // several new SQL backends optimize this.
1339         if (!WIKIDB_NOCACHE_MARKUP
1340             and $key == '_cached_html'
1341                 and method_exists($backend, 'get_cached_html')
1342         ) {
1343             return $backend->get_cached_html($this->_pagename);
1344         }
1345         $data = $cache->get_pagedata($this->_pagename);
1346         return isset($data[$key]) ? $data[$key] : false;
1347     }
1348
1349     /**
1350      * Get all the page meta-data as a hash.
1351      *
1352      * @return array The page meta-data (hash).
1353      */
1354     public function getMetaData()
1355     {
1356         $cache = &$this->_wikidb->_cache;
1357         $data = $cache->get_pagedata($this->_pagename);
1358         $meta = array();
1359         foreach ($data as $key => $val) {
1360             if ( /*!empty($val) &&*/
1361                 $key[0] != '%'
1362             )
1363                 $meta[$key] = $val;
1364         }
1365         return $meta;
1366     }
1367
1368     /**
1369      * Set page meta-data.
1370      *
1371      * @see get
1372      *
1373      * @param string $key    Meta-data key to set.
1374      * @param string $newval New value.
1375      */
1376     public function set($key, $newval)
1377     {
1378         $cache = &$this->_wikidb->_cache;
1379         $backend = &$this->_wikidb->_backend;
1380         $pagename = &$this->_pagename;
1381
1382         assert($key && $key[0] != '%');
1383
1384         // several new SQL backends optimize this.
1385         if (!WIKIDB_NOCACHE_MARKUP
1386             and $key == '_cached_html'
1387                 and method_exists($backend, 'set_cached_html')
1388         ) {
1389             if ($this->_wikidb->readonly) {
1390                 trigger_error("readonly database", E_USER_WARNING);
1391                 return;
1392             }
1393             $backend->set_cached_html($pagename, $newval);
1394             return;
1395         }
1396
1397         $data = $cache->get_pagedata($pagename);
1398
1399         if (!empty($newval)) {
1400             if (!empty($data[$key]) && $data[$key] == $newval)
1401                 return; // values identical, skip update.
1402         } else {
1403             if (empty($data[$key]))
1404                 return; // values identical, skip update.
1405         }
1406
1407         if (isset($this->_wikidb->readonly) and ($this->_wikidb->readonly)) {
1408             trigger_error("readonly database", E_USER_WARNING);
1409             return;
1410         }
1411         $cache->update_pagedata($pagename, array($key => $newval));
1412     }
1413
1414     /**
1415      * Increase page hit count.
1416      *
1417      * FIXME: IS this needed?  Probably not.
1418      *
1419      * This is a convenience function.
1420      * <pre> $page->increaseHitCount(); </pre>
1421      * is functionally identical to
1422      * <pre> $page->set('hits',$page->get('hits')+1); </pre>
1423      * but less expensive (ignores the pagadata string)
1424      *
1425      * Note that this method may be implemented in more efficient ways
1426      * in certain backends.
1427      */
1428     public function increaseHitCount()
1429     {
1430         if ($this->_wikidb->readonly) {
1431             trigger_error("readonly database", E_USER_NOTICE);
1432             return;
1433         }
1434         if (method_exists($this->_wikidb->_backend, 'increaseHitCount'))
1435             $this->_wikidb->_backend->increaseHitCount($this->_pagename);
1436         else {
1437             @$newhits = $this->get('hits') + 1;
1438             $this->set('hits', $newhits);
1439         }
1440     }
1441
1442     /**
1443      * Return a string representation of the WikiDB_Page
1444      *
1445      * This is really only for debugging.
1446      *
1447      * @return string Printable representation of the WikiDB_Page.
1448      */
1449     public function asString()
1450     {
1451         ob_start();
1452         printf("[%s:%s\n", get_class($this), $this->getName());
1453         print_r($this->getMetaData());
1454         echo "]\n";
1455         $strval = ob_get_contents();
1456         ob_end_clean();
1457         return $strval;
1458     }
1459
1460     /**
1461      * @param int|object $version_or_pagerevision
1462      * Takes either the version number (and int) or a WikiDB_PageRevision
1463      * object.
1464      * @return integer The version number.
1465      */
1466     private function _coerce_to_version($version_or_pagerevision)
1467     {
1468         if (method_exists($version_or_pagerevision, "getContent"))
1469             $version = $version_or_pagerevision->getVersion();
1470         else
1471             $version = (int)$version_or_pagerevision;
1472
1473         assert($version >= 0);
1474         return $version;
1475     }
1476
1477     public function isUserPage($include_empty = true)
1478     {
1479         if (!$include_empty and !$this->exists()) return false;
1480         return $this->get('pref') ? true : false;
1481     }
1482
1483     // May be empty. Either the stored owner (/Chown), or the first authorized author
1484     public function getOwner()
1485     {
1486         if ($owner = $this->get('owner'))
1487             return $owner;
1488         // check all revisions forwards for the first author_id
1489         $backend = &$this->_wikidb->_backend;
1490         $pagename = &$this->_pagename;
1491         $latestversion = $backend->get_latest_version($pagename);
1492         for ($v = 1; $v <= $latestversion; $v++) {
1493             $rev = $this->getRevision($v, false);
1494             if ($rev and $owner = $rev->get('author_id')) {
1495                 return $owner;
1496             }
1497         }
1498         return '';
1499     }
1500
1501     // The authenticated author of the first revision or empty if not authenticated then.
1502     public function getCreator()
1503     {
1504         if ($current = $this->getRevision(1, false))
1505             return $current->get('author_id');
1506         else
1507             return '';
1508     }
1509
1510     // The authenticated author of the current revision.
1511     public function getAuthor()
1512     {
1513         if ($current = $this->getCurrentRevision(false))
1514             return $current->get('author_id');
1515         else
1516             return '';
1517     }
1518
1519     /* Semantic Web value, not stored in the links.
1520      * todo: unify with some unit knowledge
1521      */
1522     function setAttribute($relation, $value)
1523     {
1524         $attr = $this->get('attributes');
1525         if (empty($attr))
1526             $attr = array($relation => $value);
1527         else
1528             $attr[$relation] = $value;
1529         $this->set('attributes', $attr);
1530     }
1531
1532     public function getAttribute($relation)
1533     {
1534         $meta = $this->get('attributes');
1535         if (empty($meta))
1536             return '';
1537         else
1538             return $meta[$relation];
1539     }
1540
1541 }
1542
1543 /**
1544  * This class represents a specific revision of a WikiDB_Page within
1545  * a WikiDB.
1546  *
1547  * A WikiDB_PageRevision has read-only semantics. You may only create
1548  * new revisions (and delete old ones) --- you cannot modify existing
1549  * revisions.
1550  */
1551 class WikiDB_PageRevision
1552 {
1553     public $_wikidb;
1554     public $_pagename;
1555     public $_version;
1556     public $_data;
1557     public $_transformedContent = false; // set by WikiDB_Page::save()
1558
1559     function __construct(&$wikidb, $pagename, $version, $versiondata = array())
1560     {
1561         $this->_wikidb = &$wikidb;
1562         $this->_pagename = $pagename;
1563         $this->_version = $version;
1564         $this->_data = $versiondata ? $versiondata : array();
1565         $this->_transformedContent = false; // set by WikiDB_Page::save()
1566     }
1567
1568     /**
1569      * Get the WikiDB_Page which this revision belongs to.
1570      *
1571      * @return WikiDB_Page The WikiDB_Page which this revision belongs to.
1572      */
1573     public function getPage()
1574     {
1575         return new WikiDB_Page($this->_wikidb, $this->_pagename);
1576     }
1577
1578     /**
1579      * Get the version number of this revision.
1580      *
1581      * @return int The version number of this revision.
1582      */
1583     public function getVersion()
1584     {
1585         return $this->_version;
1586     }
1587
1588     /**
1589      * Determine whether this revision has defaulted content.
1590      *
1591      * The default revision (version 0) of each page, as well as any
1592      * pages which are created with empty content have their content
1593      * defaulted to something like:
1594      * <pre>
1595      *   Describe [ThisPage] here.
1596      * </pre>
1597      *
1598      * @return boolean Returns true if the page has default content.
1599      */
1600     public function hasDefaultContents()
1601     {
1602         $data = &$this->_data;
1603         if (!isset($data['%content'])) return true;
1604         if ($data['%content'] === true) return false;
1605         return $data['%content'] === false or $data['%content'] === "";
1606     }
1607
1608     /**
1609      * Get the content as an array of lines.
1610      *
1611      * @return array An array of lines.
1612      * The lines should contain no trailing white space.
1613      */
1614     public function getContent()
1615     {
1616         return explode("\n", $this->getPackedContent());
1617     }
1618
1619     /**
1620      * Get the pagename of the revision.
1621      *
1622      * @return string pagename.
1623      */
1624     public function getPageName()
1625     {
1626         return $this->_pagename;
1627     }
1628
1629     function getName()
1630     {
1631         return $this->_pagename;
1632     }
1633
1634     /**
1635      * Determine whether revision is the latest.
1636      *
1637      * @return boolean True iff the revision is the latest (most recent) one.
1638      */
1639     public function isCurrent()
1640     {
1641         if (!isset($this->_iscurrent)) {
1642             $page = $this->getPage();
1643             $current = $page->getCurrentRevision(false);
1644             $this->_iscurrent = $this->getVersion() == $current->getVersion();
1645         }
1646         return $this->_iscurrent;
1647     }
1648
1649     /**
1650      * Get the transformed content of a page.
1651      *
1652      * @param bool $pagetype_override
1653      * @return object An XmlContent-like object containing the page transformed
1654      * contents.
1655      */
1656     public function getTransformedContent($pagetype_override = false)
1657     {
1658         if ($pagetype_override) {
1659             // Figure out the normal page-type for this page.
1660             $type = PageType::GetPageType($this->get('pagetype'));
1661             if ($type->getName() == $pagetype_override)
1662                 $pagetype_override = false; // Not really an override...
1663         }
1664
1665         if ($pagetype_override) {
1666             // Overriden page type, don't cache (or check cache).
1667             return new TransformedText($this->getPage(),
1668                 $this->getPackedContent(),
1669                 $this->getMetaData(),
1670                 $pagetype_override);
1671         }
1672
1673         $possibly_cache_results = true;
1674
1675         if (!USECACHE or WIKIDB_NOCACHE_MARKUP) {
1676             if (WIKIDB_NOCACHE_MARKUP == 'purge') {
1677                 // flush cache for this page.
1678                 $page = $this->getPage();
1679                 $page->set('_cached_html', ''); // ignored with !USECACHE
1680             }
1681             $possibly_cache_results = false;
1682         } elseif (USECACHE and !$this->_transformedContent) {
1683             //$backend->lock();
1684             if ($this->isCurrent()) {
1685                 $page = $this->getPage();
1686                 $this->_transformedContent = TransformedText::unpack($page->get('_cached_html'));
1687             } else {
1688                 $possibly_cache_results = false;
1689             }
1690             //$backend->unlock();
1691         }
1692
1693         if (!$this->_transformedContent) {
1694             $this->_transformedContent
1695                 = new TransformedText($this->getPage(),
1696                 $this->getPackedContent(),
1697                 $this->getMetaData());
1698
1699             if ($possibly_cache_results and !WIKIDB_NOCACHE_MARKUP) {
1700                 // If we're still the current version, cache the transfomed page.
1701                 //$backend->lock();
1702                 if ($this->isCurrent()) {
1703                     $page->set('_cached_html', $this->_transformedContent->pack());
1704                 }
1705                 //$backend->unlock();
1706             }
1707         }
1708
1709         return $this->_transformedContent;
1710     }
1711
1712     /**
1713      * Get the content as a string.
1714      *
1715      * @return string The page content.
1716      * Lines are separated by new-lines.
1717      */
1718     public function getPackedContent()
1719     {
1720         /**
1721          * @var WikiRequest $request
1722          */
1723         global $request;
1724
1725         $data = &$this->_data;
1726
1727         if (empty($data['%content'])
1728             || (!$this->_wikidb->isWikiPage($this->_pagename)
1729                 && $this->isCurrent())
1730         ) {
1731             include_once 'lib/InlineParser.php';
1732
1733             // A feature similar to taglines at http://www.wlug.org.nz/
1734             // Lib from http://www.aasted.org/quote/
1735             if (defined('FORTUNE_DIR')
1736                 and is_dir(FORTUNE_DIR)
1737                     and in_array($request->getArg('action'),
1738                         array('create', 'edit'))
1739             ) {
1740                 include_once 'lib/fortune.php';
1741                 $fortune = new Fortune();
1742                 $quote = $fortune->quoteFromDir(FORTUNE_DIR);
1743                 if ($quote != -1)
1744                     $quote = "<verbatim>\n"
1745                         . str_replace("\n<br>", "\n", $quote)
1746                         . "</verbatim>\n\n";
1747                 else
1748                     $quote = "";
1749                 return $quote
1750                     . sprintf(_("Describe %s here."),
1751                         "[" . WikiEscape($this->_pagename) . "]");
1752             }
1753             // Replace empty content with default value.
1754             return sprintf(_("Describe %s here."),
1755                 "[" . WikiEscape($this->_pagename) . "]");
1756         }
1757
1758         // There is (non-default) content.
1759         assert($this->_version > 0);
1760
1761         if (!is_string($data['%content'])) {
1762             // Content was not provided to us at init time.
1763             // (This is allowed because for some backends, fetching
1764             // the content may be expensive, and often is not wanted
1765             // by the user.)
1766             //
1767             // In any case, now we need to get it.
1768             $data['%content'] = $this->_get_content();
1769             assert(is_string($data['%content']));
1770         }
1771
1772         return $data['%content'];
1773     }
1774
1775     public function _get_content()
1776     {
1777         $cache = &$this->_wikidb->_cache;
1778         $pagename = $this->_pagename;
1779         $version = $this->_version;
1780
1781         assert($version > 0);
1782
1783         $newdata = $cache->get_versiondata($pagename, $version, true);
1784         if ($newdata) {
1785             assert(is_string($newdata['%content']));
1786             return $newdata['%content'];
1787         } else {
1788             // else revision has been deleted... What to do?
1789             return __sprintf("Oops! Revision %s of %s seems to have been deleted!",
1790                 $version, $pagename);
1791         }
1792     }
1793
1794     /**
1795      * Get meta-data for this revision.
1796      *
1797      * @param string $key Which meta-data to access.
1798      *
1799      * Some reserved revision meta-data keys are:
1800      * <dl>
1801      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
1802      *        The 'mtime' meta-value is normally set automatically by the database
1803      *        backend, but it may be specified explicitly when creating a new revision.
1804      * <dt> orig_mtime
1805      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
1806      *       of a page must be monotonically increasing.  If an attempt is
1807      *       made to create a new revision with an mtime less than that of
1808      *       the preceeding revision, the new revisions timestamp is force
1809      *       to be equal to that of the preceeding revision.  In that case,
1810      *       the originally requested mtime is preserved in 'orig_mtime'.
1811      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
1812      *        This meta-value is <em>always</em> automatically maintained by the database
1813      *        backend.  (It is set from the 'mtime' meta-value of the superceding
1814      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
1815      *
1816      * FIXME: this could be refactored:
1817      * <dt> author
1818      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
1819      * <dt> author_id
1820      *  <dd> Authenticated author of a page.  This is used to identify
1821      *       the distinctness of authors when cleaning old revisions from
1822      *       the database.
1823      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
1824      * <dt> 'summary' <dd> Short change summary entered by page author.
1825      * </dl>
1826      *
1827      * Meta-data keys must be valid C identifers (they have to start with a letter
1828      * or underscore, and can contain only alphanumerics and underscores.)
1829      *
1830      * @return string The requested value, or false if the requested value
1831      * is not defined.
1832      */
1833     public function get($key)
1834     {
1835         if (!$key || $key[0] == '%')
1836             return false;
1837         $data = &$this->_data;
1838         return isset($data[$key]) ? $data[$key] : false;
1839     }
1840
1841     /**
1842      * Get all the revision page meta-data as a hash.
1843      *
1844      * @return array The revision meta-data.
1845      */
1846     public function getMetaData()
1847     {
1848         $meta = array();
1849         foreach ($this->_data as $key => $val) {
1850             if (!empty($val) && $key[0] != '%')
1851                 $meta[$key] = $val;
1852         }
1853         return $meta;
1854     }
1855
1856     /**
1857      * Return a string representation of the revision.
1858      *
1859      * This is really only for debugging.
1860      *
1861      * @return string Printable representation of the WikiDB_Page.
1862      */
1863     public function asString()
1864     {
1865         ob_start();
1866         printf("[%s:%d\n", get_class($this), $this->get('version'));
1867         print_r($this->_data);
1868         echo $this->getPackedContent() . "\n]\n";
1869         $strval = ob_get_contents();
1870         ob_end_clean();
1871         return $strval;
1872     }
1873 }
1874
1875 /**
1876  * Class representing a sequence of WikiDB_Pages.
1877  * TODO: Enhance to php5 iterators
1878  * TODO:
1879  *   apply filters for options like 'sortby', 'limit', 'exclude'
1880  *   for simple queries like titleSearch, where the backend is not ready yet.
1881  */
1882 class WikiDB_PageIterator
1883 {
1884     public $_iter;
1885     public $_wikidb;
1886     public $_options;
1887
1888     function __construct(&$wikidb, &$iter, $options = array())
1889     {
1890         $this->_iter = $iter; // a WikiDB_backend_iterator
1891         $this->_wikidb = &$wikidb;
1892         $this->_options = $options;
1893     }
1894
1895     public function count()
1896     {
1897         return $this->_iter->count();
1898     }
1899
1900     public function limit()
1901     {
1902         return empty($this->_options['limit']) ? 0 : $this->_options['limit'];
1903     }
1904
1905     /**
1906      * Get next WikiDB_Page in sequence.
1907      *
1908      * @return WikiDB_Page The next WikiDB_Page in the sequence.
1909      */
1910     public function next()
1911     {
1912         if (!($next = $this->_iter->next())) {
1913             return false;
1914         }
1915
1916         $pagename = &$next['pagename'];
1917         if (!is_string($pagename)) { // Bug #1327912 fixed by Joachim Lous
1918             trigger_error("WikiDB_PageIterator->next pagename", E_USER_WARNING);
1919         }
1920
1921         if (!$pagename) {
1922             if (isset($next['linkrelation'])
1923                 or isset($next['pagedata']['linkrelation'])
1924             ) {
1925                 return false;
1926             }
1927         }
1928
1929         // There's always hits, but we cache only if more
1930         // (well not with file and dba)
1931         if (isset($next['pagedata']) and count($next['pagedata']) > 1) {
1932             $this->_wikidb->_cache->cache_data($next);
1933             // cache existing page id's since we iterate over all links in GleanDescription
1934             // and need them later for LinkExistingWord
1935         } elseif ($this->_options and array_key_exists('include_empty', $this->_options)
1936             and !$this->_options['include_empty'] and isset($next['id'])
1937         ) {
1938             $this->_wikidb->_cache->_id_cache[$next['pagename']] = $next['id'];
1939         }
1940         $page = new WikiDB_Page($this->_wikidb, $pagename);
1941         if (isset($next['linkrelation']))
1942             $page->set('linkrelation', $next['linkrelation']);
1943         if (isset($next['score']))
1944             $page->score = $next['score'];
1945         return $page;
1946     }
1947
1948     /**
1949      * Release resources held by this iterator.
1950      *
1951      * The iterator may not be used after free() is called.
1952      *
1953      * There is no need to call free(), if next() has returned false.
1954      * (I.e. if you iterate through all the pages in the sequence,
1955      * you do not need to call free() --- you only need to call it
1956      * if you stop before the end of the iterator is reached.)
1957      */
1958     public function free()
1959     {
1960         // $this->_iter->free();
1961     }
1962
1963     public function reset()
1964     {
1965         $this->_iter->reset();
1966     }
1967
1968     public function asArray()
1969     {
1970         $result = array();
1971         while ($page = $this->next())
1972             $result[] = $page;
1973         $this->reset();
1974         return $result;
1975     }
1976 }
1977
1978 /**
1979  * A class which represents a sequence of WikiDB_PageRevisions.
1980  * TODO: Enhance to php5 iterators
1981  */
1982 class WikiDB_PageRevisionIterator
1983 {
1984     public $_revisions;
1985     public $_wikidb;
1986     public $_options;
1987
1988     function __construct(&$wikidb, &$revisions, $options = false)
1989     {
1990         $this->_revisions = $revisions;
1991         $this->_wikidb = &$wikidb;
1992         $this->_options = $options;
1993     }
1994
1995     public function count()
1996     {
1997         return $this->_revisions->count();
1998     }
1999
2000     /**
2001      * Get next WikiDB_PageRevision in sequence.
2002      *
2003      * @return WikiDB_PageRevision
2004      * The next WikiDB_PageRevision in the sequence.
2005      */
2006     public function next()
2007     {
2008         if (!($next = $this->_revisions->next()))
2009             return false;
2010
2011         //$this->_wikidb->_cache->cache_data($next);
2012
2013         $pagename = $next['pagename'];
2014         $version = $next['version'];
2015         $versiondata = $next['versiondata'];
2016         if ((int)DEBUG) {
2017             if (!(is_string($pagename) and $pagename != '')) {
2018                 trigger_error("empty pagename", E_USER_WARNING);
2019                 return false;
2020             }
2021         } else assert(is_string($pagename) and $pagename != '');
2022         if ((int)DEBUG) {
2023             if (!is_array($versiondata)) {
2024                 trigger_error("empty versiondata", E_USER_WARNING);
2025                 return false;
2026             }
2027         } else assert(is_array($versiondata));
2028         if ((int)DEBUG) {
2029             if (!($version > 0)) {
2030                 trigger_error("invalid version", E_USER_WARNING);
2031                 return false;
2032             }
2033         } else assert($version > 0);
2034
2035         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version, $versiondata);
2036     }
2037
2038     /**
2039      * Release resources held by this iterator.
2040      *
2041      * The iterator may not be used after free() is called.
2042      *
2043      * There is no need to call free(), if next() has returned false.
2044      * (I.e. if you iterate through all the revisions in the sequence,
2045      * you do not need to call free() --- you only need to call it
2046      * if you stop before the end of the iterator is reached.)
2047      */
2048     public function free()
2049     {
2050         $this->_revisions->free();
2051     }
2052
2053     public function asArray()
2054     {
2055         $result = array();
2056         while ($rev = $this->next())
2057             $result[] = $rev;
2058         $this->free();
2059         return $result;
2060     }
2061 }
2062
2063 /** pseudo iterator
2064  */
2065 class WikiDB_Array_PageIterator
2066 {
2067     public $_dbi;
2068     public $_pages;
2069
2070     function __construct ($pagenames)
2071     {
2072         /**
2073          * @var WikiRequest $request
2074          */
2075         global $request;
2076
2077         $this->_dbi = $request->getDbh();
2078         $this->_pages = $pagenames;
2079         reset($this->_pages);
2080     }
2081
2082     public function next()
2083     {
2084         $c = current($this->_pages);
2085         next($this->_pages);
2086         return $c !== false ? $this->_dbi->getPage($c) : false;
2087     }
2088
2089     public function count()
2090     {
2091         return count($this->_pages);
2092     }
2093
2094     public function reset()
2095     {
2096         reset($this->_pages);
2097     }
2098
2099     public function free()
2100     {
2101     }
2102
2103     public function asArray()
2104     {
2105         reset($this->_pages);
2106         return $this->_pages;
2107     }
2108 }
2109
2110 class WikiDB_Array_generic_iter
2111 {
2112     public $_array;
2113
2114     function __construct($result)
2115     {
2116         // $result may be either an array or a query result
2117         if (is_array($result)) {
2118             $this->_array = $result;
2119         } elseif (is_object($result)) {
2120             $this->_array = $result->asArray();
2121         } else {
2122             $this->_array = array();
2123         }
2124         if (!empty($this->_array))
2125             reset($this->_array);
2126     }
2127
2128     public function next()
2129     {
2130         $c = current($this->_array);
2131         next($this->_array);
2132         return $c !== false ? $c : false;
2133     }
2134
2135     public function count()
2136     {
2137         return count($this->_array);
2138     }
2139
2140     public function reset()
2141     {
2142         reset($this->_array);
2143     }
2144
2145     public function free()
2146     {
2147     }
2148
2149     public function asArray()
2150     {
2151         if (!empty($this->_array))
2152             reset($this->_array);
2153         return $this->_array;
2154     }
2155 }
2156
2157 /**
2158  * Data cache used by WikiDB.
2159  *
2160  * FIXME: Maybe rename this to caching_backend (or some such).
2161  *
2162  * @access private
2163  */
2164 class WikiDB_cache
2165 {
2166     // FIXME: beautify versiondata cache.  Cache only limited data?
2167
2168     public $_backend;
2169     public $_pagedata_cache;
2170     public $_versiondata_cache;
2171     public $_glv_cache;
2172     public $_id_cache;
2173     public $readonly;
2174
2175     function __construct(&$backend)
2176     {
2177         /**
2178          * @var WikiRequest $request
2179          */
2180         global $request;
2181
2182         $this->_backend = &$backend;
2183         $this->_pagedata_cache = array();
2184         $this->_versiondata_cache = array();
2185         array_push($this->_versiondata_cache, array());
2186         $this->_glv_cache = array();
2187         $this->_id_cache = array(); // formerly ->_dbi->_iwpcache (nonempty pages => id)
2188
2189         if (isset($request->_dbi))
2190             $this->readonly = $request->_dbi->readonly;
2191     }
2192
2193     public function close()
2194     {
2195         $this->_pagedata_cache = array();
2196         $this->_versiondata_cache = array();
2197         $this->_glv_cache = array();
2198         $this->_id_cache = array();
2199     }
2200
2201     public function get_pagedata($pagename)
2202     {
2203         assert(is_string($pagename) && $pagename != '');
2204         if (USECACHE) {
2205             $cache = &$this->_pagedata_cache;
2206             if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
2207                 $cache[$pagename] = $this->_backend->get_pagedata($pagename);
2208                 if (empty($cache[$pagename]))
2209                     $cache[$pagename] = array();
2210             }
2211             return $cache[$pagename];
2212         } else {
2213             return $this->_backend->get_pagedata($pagename);
2214         }
2215     }
2216
2217     public function update_pagedata($pagename, $newdata)
2218     {
2219         assert(is_string($pagename) && $pagename != '');
2220         if (!empty($this->readonly)) {
2221             trigger_error("readonly database", E_USER_WARNING);
2222             return;
2223         }
2224
2225         $this->_backend->update_pagedata($pagename, $newdata);
2226
2227         if (USECACHE) {
2228             if (!empty($this->_pagedata_cache[$pagename])
2229                 and is_array($this->_pagedata_cache[$pagename])
2230             ) {
2231                 $cachedata = &$this->_pagedata_cache[$pagename];
2232                 foreach ($newdata as $key => $val)
2233                     $cachedata[$key] = $val;
2234             } else
2235                 $this->_pagedata_cache[$pagename] = $newdata;
2236         }
2237     }
2238
2239     public function invalidate_cache($pagename)
2240     {
2241         unset ($this->_pagedata_cache[$pagename]);
2242         unset ($this->_versiondata_cache[$pagename]);
2243         unset ($this->_glv_cache[$pagename]);
2244         unset ($this->_id_cache[$pagename]);
2245         //unset ($this->_backend->_page_data);
2246     }
2247
2248     public function delete_page($pagename)
2249     {
2250         if (!empty($this->readonly)) {
2251             trigger_error("readonly database", E_USER_WARNING);
2252             return false;
2253         }
2254         $result = $this->_backend->delete_page($pagename);
2255         $this->invalidate_cache($pagename);
2256         return $result;
2257     }
2258
2259     public function purge_page($pagename)
2260     {
2261         if (!empty($this->readonly)) {
2262             trigger_error("readonly database", E_USER_WARNING);
2263             return false;
2264         }
2265         $result = $this->_backend->purge_page($pagename);
2266         $this->invalidate_cache($pagename);
2267         return $result;
2268     }
2269
2270     // FIXME: ugly and wrong. may overwrite full cache with partial cache
2271     public function cache_data($data)
2272     {
2273         ;
2274         //if (isset($data['pagedata']))
2275         //    $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
2276     }
2277
2278     public function get_versiondata($pagename, $version, $need_content = false)
2279     {
2280         //  FIXME: Seriously ugly hackage
2281         $readdata = false;
2282         if (USECACHE) { //temporary - for debugging
2283             assert(is_string($pagename) && $pagename != '');
2284             // There is a bug here somewhere which results in an assertion failure at line 105
2285             // of ArchiveCleaner.php  It goes away if we use the next line.
2286             //$need_content = true;
2287             $nc = $need_content ? '1' : '0';
2288             $cache = &$this->_versiondata_cache;
2289             if (!isset($cache[$pagename][$version][$nc])
2290                 || !(is_array($cache[$pagename]))
2291                 || !(is_array($cache[$pagename][$version]))
2292             ) {
2293                 $cache[$pagename][$version][$nc] =
2294                     $this->_backend->get_versiondata($pagename, $version, $need_content);
2295                 $readdata = true;
2296                 // If we have retrieved all data, we may as well set the cache for
2297                 // $need_content = false
2298                 if ($need_content) {
2299                     $cache[$pagename][$version]['0'] =& $cache[$pagename][$version]['1'];
2300                 }
2301             }
2302             $vdata = $cache[$pagename][$version][$nc];
2303         } else {
2304             $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
2305             $readdata = true;
2306         }
2307         if ($readdata && is_array($vdata) && !empty($vdata['%pagedata'])) {
2308             if (empty($this->_pagedata_cache))
2309                 $this->_pagedata_cache = array();
2310             /* PHP Fatal error:  Cannot create references to/from string offsets nor overloaded objects in /var/www/html/phpwiki/lib/WikiDB.php on line 2180, referer: wiki/TitleSearch?s=and&auto_redirect=1 */
2311             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
2312         }
2313         return $vdata;
2314     }
2315
2316     public function set_versiondata($pagename, $version, $data)
2317     {
2318         //unset($this->_versiondata_cache[$pagename][$version]);
2319
2320         if (!empty($this->readonly)) {
2321             trigger_error("readonly database", E_USER_WARNING);
2322             return;
2323         }
2324         $this->_backend->set_versiondata($pagename, $version, $data);
2325         // Update the cache
2326         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
2327         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
2328         // Is this necessary?
2329         unset($this->_glv_cache[$pagename]);
2330     }
2331
2332     function update_versiondata($pagename, $version, $data)
2333     {
2334         if (!empty($this->readonly)) {
2335             trigger_error("readonly database", E_USER_WARNING);
2336             return;
2337         }
2338         $this->_backend->update_versiondata($pagename, $version, $data);
2339         // Update the cache
2340         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
2341         // FIXME: hack
2342         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
2343         // Is this necessary?
2344         unset($this->_glv_cache[$pagename]);
2345     }
2346
2347     public function delete_versiondata($pagename, $version)
2348     {
2349         if (!empty($this->readonly)) {
2350             trigger_error("readonly database", E_USER_WARNING);
2351             return;
2352         }
2353         $this->_backend->delete_versiondata($pagename, $version);
2354         if (isset($this->_versiondata_cache[$pagename][$version]))
2355             unset ($this->_versiondata_cache[$pagename][$version]);
2356         // dirty latest version cache only if latest version gets deleted
2357         if (isset($this->_glv_cache[$pagename]) and $this->_glv_cache[$pagename] == $version)
2358             unset ($this->_glv_cache[$pagename]);
2359     }
2360
2361     public function get_latest_version($pagename)
2362     {
2363         if (USECACHE) {
2364             assert(is_string($pagename) && $pagename != '');
2365             $cache = &$this->_glv_cache;
2366             if (!isset($cache[$pagename])) {
2367                 $cache[$pagename] = $this->_backend->get_latest_version($pagename);
2368                 if (empty($cache[$pagename]))
2369                     $cache[$pagename] = 0;
2370             }
2371             return $cache[$pagename];
2372         } else {
2373             return $this->_backend->get_latest_version($pagename);
2374         }
2375     }
2376 }
2377
2378 // Local Variables:
2379 // mode: php
2380 // tab-width: 8
2381 // c-basic-offset: 4
2382 // c-hanging-comment-ender-p: nil
2383 // indent-tabs-mode: nil
2384 // End: