]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB.php
Remove MockRequest
[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) {
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             ) {
1087                 include_once 'lib/MailNotify.php';
1088                 $MailNotify = new MailNotify($newrevision->getName());
1089                 $MailNotify->onChangePage($this->_wikidb, $wikitext, $version, $meta);
1090             }
1091             $newrevision->_transformedContent = $formatted;
1092         }
1093         // more pagechange callbacks: (in a hackish manner for now)
1094         if (ENABLE_RECENTCHANGESBOX
1095             and empty($meta['is_minor_edit'])
1096                 and !in_array($request->getArg('action'),
1097                     array('loadfile', 'upgrade'))
1098         ) {
1099             require_once 'lib/WikiPlugin.php';
1100             $w = new WikiPluginLoader();
1101             $p = $w->getPlugin("RecentChangesCached", false);
1102             $p->box_update(false, $request, $this->_pagename);
1103         }
1104         return $newrevision;
1105     }
1106
1107     /**
1108      * Get the most recent revision of a page.
1109      *
1110      * @param bool $need_content
1111      * @return WikiDB_PageRevision The current WikiDB_PageRevision object.
1112      */
1113     public function getCurrentRevision($need_content = true)
1114     {
1115         $cache = &$this->_wikidb->_cache;
1116         $pagename = &$this->_pagename;
1117
1118         // Prevent deadlock in case of memory exhausted errors
1119         // Pure selection doesn't really need locking here.
1120         //   sf.net bug#927395
1121         // I know it would be better to lock, but with lots of pages this deadlock is more
1122         // severe than occasionally get not the latest revision.
1123         // In spirit to wikiwiki: read fast, edit slower.
1124         //$backend->lock();
1125         $version = $cache->get_latest_version($pagename);
1126         // getRevision gets the content also!
1127         $revision = $this->getRevision($version, $need_content);
1128         //$backend->unlock();
1129         assert($revision);
1130         return $revision;
1131     }
1132
1133     /**
1134      * Get a specific revision of a WikiDB_Page.
1135      *
1136      * @param int $version Which revision to get.
1137      * @param bool $need_content
1138      * @return WikiDB_PageRevision The requested WikiDB_PageRevision object, or
1139      * false if the requested revision does not exist in the {@link WikiDB}.
1140      * Note that version zero of any page always exists.
1141      */
1142     public function getRevision($version, $need_content = true)
1143     {
1144         $cache = &$this->_wikidb->_cache;
1145         $pagename = &$this->_pagename;
1146
1147         if ((!$version) or ($version == 0) or ($version == -1)) { // 0 or false
1148             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
1149         }
1150
1151         assert($version > 0);
1152         $vdata = $cache->get_versiondata($pagename, $version, $need_content);
1153         if (!$vdata) {
1154             return new WikiDB_PageRevision($this->_wikidb, $pagename, 0);
1155         }
1156         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version,
1157             $vdata);
1158     }
1159
1160     /**
1161      * Get previous page revision.
1162      *
1163      * This method find the most recent revision before a specified
1164      * version.
1165      *
1166      * @param bool|int|WikiDB_PageRevision $version Find most recent revision before this version.
1167      *
1168      * @param bool $need_content
1169      *
1170      * @return WikiDB_PageRevision|bool The requested WikiDB_PageRevision object, or false if the
1171      * requested revision does not exist in the {@link WikiDB}.  Note that
1172      * unless $version is greater than zero, a revision (perhaps version zero,
1173      * the default revision) will always be found.
1174      */
1175     public function getRevisionBefore($version = false, $need_content = true)
1176     {
1177         $backend = &$this->_wikidb->_backend;
1178         $pagename = &$this->_pagename;
1179         if ($version === false)
1180             $version = $this->_wikidb->_cache->get_latest_version($pagename);
1181         else
1182             $version = $this->_coerce_to_version($version);
1183
1184         if ($version == 0)
1185             return false;
1186         //$backend->lock();
1187         $previous = $backend->get_previous_version($pagename, $version);
1188         $revision = $this->getRevision($previous, $need_content);
1189         //$backend->unlock();
1190         assert($revision);
1191         return $revision;
1192     }
1193
1194     /**
1195      * Get all revisions of the WikiDB_Page.
1196      *
1197      * This does not include the version zero (default) revision in the
1198      * returned revision set.
1199      *
1200      * @return WikiDB_PageRevisionIterator A
1201      *   WikiDB_PageRevisionIterator containing all revisions of this
1202      *   WikiDB_Page in reverse order by version number.
1203      */
1204     public function getAllRevisions()
1205     {
1206         $backend = &$this->_wikidb->_backend;
1207         $revs = $backend->get_all_revisions($this->_pagename);
1208         return new WikiDB_PageRevisionIterator($this->_wikidb, $revs);
1209     }
1210
1211     /**
1212      * Find pages which link to or are linked from a page.
1213      * relations: $backend->get_links is responsible to add the relation to the pagehash
1214      * as 'linkrelation' key as pagename. See WikiDB_PageIterator::next
1215      *   if (isset($next['linkrelation']))
1216      *
1217      * @param bool $reversed Which links to find: true for backlinks (default).
1218      * @param bool $include_empty
1219      * @param string $sortby
1220      * @param string $limit
1221      * @param string $sortby
1222      * @param string $exclude
1223      * @param bool $want_relations
1224      * @return WikiDB_PageIterator A WikiDB_PageIterator containing
1225      * all matching pages.
1226      */
1227     public function getLinks($reversed = true, $include_empty = false, $sortby = '',
1228                       $limit = '', $exclude = '', $want_relations = false)
1229     {
1230         $backend = &$this->_wikidb->_backend;
1231         $result = $backend->get_links($this->_pagename, $reversed,
1232             $include_empty, $sortby, $limit, $exclude,
1233             $want_relations);
1234         return new WikiDB_PageIterator($this->_wikidb, $result,
1235             array('include_empty' => $include_empty,
1236                 'sortby' => $sortby,
1237                 'limit' => $limit,
1238                 'exclude' => $exclude,
1239                 'want_relations' => $want_relations));
1240     }
1241
1242     /**
1243      * All Links from other pages to this page.
1244      */
1245     public function getBackLinks($include_empty = false, $sortby = '', $limit = '', $exclude = '')
1246     {
1247         return $this->getLinks(true, $include_empty, $sortby, $limit, $exclude);
1248     }
1249
1250     /**
1251      * Forward Links: All Links from this page to other pages.
1252      */
1253     public function getPageLinks($include_empty = false, $sortby = '', $limit = '', $exclude = '')
1254     {
1255         return $this->getLinks(false, $include_empty, $sortby, $limit, $exclude);
1256     }
1257
1258     /**
1259      * Relations: All links from this page to other pages with relation <> 0.
1260      * is_a:=page or population:=number
1261      */
1262     public function getRelations($sortby = '', $limit = '', $exclude = '')
1263     {
1264         $backend = &$this->_wikidb->_backend;
1265         $result = $backend->get_links($this->_pagename, false, true,
1266             $sortby, $limit, $exclude,
1267             true);
1268         // we do not care for the linked page versiondata, just the pagename and linkrelation
1269         return new WikiDB_PageIterator($this->_wikidb, $result,
1270             array('include_empty' => true,
1271                 'sortby' => $sortby,
1272                 'limit' => $limit,
1273                 'exclude' => $exclude,
1274                 'want_relations' => true));
1275     }
1276
1277     /**
1278      * possibly faster link existance check. not yet accelerated.
1279      */
1280     public function existLink($link, $reversed = false)
1281     {
1282         $backend = &$this->_wikidb->_backend;
1283         if (method_exists($backend, 'exists_link'))
1284             return $backend->exists_link($this->_pagename, $link, $reversed);
1285         //$cache = &$this->_wikidb->_cache;
1286         // TODO: check cache if it is possible
1287         $iter = $this->getLinks($reversed, false);
1288         while ($page = $iter->next()) {
1289             if ($page->getName() == $link)
1290                 return $page;
1291         }
1292         $iter->free();
1293         return false;
1294     }
1295
1296     /* Semantic relations are links with the relation pointing to another page,
1297        the so-called "RDF Triple".
1298        [San Diego] is%20a::city
1299        => "At the page San Diego there is a relation link of 'is a' to the page 'city'."
1300      */
1301
1302     /* Semantic attributes for a page.
1303        [San Diego] population:=1,305,736
1304        Attributes are links with the relation pointing to another page.
1305     */
1306
1307     /**
1308      * Access WikiDB_Page non version-specific meta-data.
1309      *
1310      * @param string $key Which meta data to get.
1311      * Some reserved meta-data keys are:
1312      * <dl>
1313      * <dt>'date'  <dd> Created as unixtime
1314      * <dt>'locked'<dd> Is page locked? 'yes' or 'no'
1315      * <dt>'hits'  <dd> Page hit counter.
1316      * <dt>'_cached_html' <dd> Transformed CachedMarkup object, serialized + optionally gzipped.
1317      *                         In SQL stored now in an extra column.
1318      * Optional data:
1319      * <dt>'pref'  <dd> Users preferences, stored only in homepages.
1320      * <dt>'owner' <dd> Default: first author_id. We might add a group with a dot here:
1321      *                  E.g. "owner.users"
1322      * <dt>'perm'  <dd> Permission flag to authorize read/write/execution of
1323      *                  page-headers and content.
1324     + <dt>'moderation'<dd> ModeratedPage data. Handled by plugin/ModeratedPage
1325      * <dt>'rating' <dd> Page rating. Handled by plugin/RateIt
1326      * </dl>
1327      *
1328      * @return mixed The requested value, or false if the requested data
1329      * is not set.
1330      */
1331     public function get($key)
1332     {
1333         $cache = &$this->_wikidb->_cache;
1334         $backend = &$this->_wikidb->_backend;
1335         if (!$key || $key[0] == '%')
1336             return false;
1337         // several new SQL backends optimize this.
1338         if (!WIKIDB_NOCACHE_MARKUP
1339             and $key == '_cached_html'
1340                 and method_exists($backend, 'get_cached_html')
1341         ) {
1342             return $backend->get_cached_html($this->_pagename);
1343         }
1344         $data = $cache->get_pagedata($this->_pagename);
1345         return isset($data[$key]) ? $data[$key] : false;
1346     }
1347
1348     /**
1349      * Get all the page meta-data as a hash.
1350      *
1351      * @return array The page meta-data (hash).
1352      */
1353     public function getMetaData()
1354     {
1355         $cache = &$this->_wikidb->_cache;
1356         $data = $cache->get_pagedata($this->_pagename);
1357         $meta = array();
1358         foreach ($data as $key => $val) {
1359             if ( /*!empty($val) &&*/
1360                 $key[0] != '%'
1361             )
1362                 $meta[$key] = $val;
1363         }
1364         return $meta;
1365     }
1366
1367     /**
1368      * Set page meta-data.
1369      *
1370      * @see get
1371      *
1372      * @param string $key    Meta-data key to set.
1373      * @param string $newval New value.
1374      */
1375     public function set($key, $newval)
1376     {
1377         $cache = &$this->_wikidb->_cache;
1378         $backend = &$this->_wikidb->_backend;
1379         $pagename = &$this->_pagename;
1380
1381         assert($key && $key[0] != '%');
1382
1383         // several new SQL backends optimize this.
1384         if (!WIKIDB_NOCACHE_MARKUP
1385             and $key == '_cached_html'
1386                 and method_exists($backend, 'set_cached_html')
1387         ) {
1388             if ($this->_wikidb->readonly) {
1389                 trigger_error("readonly database", E_USER_WARNING);
1390                 return;
1391             }
1392             $backend->set_cached_html($pagename, $newval);
1393             return;
1394         }
1395
1396         $data = $cache->get_pagedata($pagename);
1397
1398         if (!empty($newval)) {
1399             if (!empty($data[$key]) && $data[$key] == $newval)
1400                 return; // values identical, skip update.
1401         } else {
1402             if (empty($data[$key]))
1403                 return; // values identical, skip update.
1404         }
1405
1406         if (isset($this->_wikidb->readonly) and ($this->_wikidb->readonly)) {
1407             trigger_error("readonly database", E_USER_WARNING);
1408             return;
1409         }
1410         $cache->update_pagedata($pagename, array($key => $newval));
1411     }
1412
1413     /**
1414      * Increase page hit count.
1415      *
1416      * FIXME: IS this needed?  Probably not.
1417      *
1418      * This is a convenience function.
1419      * <pre> $page->increaseHitCount(); </pre>
1420      * is functionally identical to
1421      * <pre> $page->set('hits',$page->get('hits')+1); </pre>
1422      * but less expensive (ignores the pagadata string)
1423      *
1424      * Note that this method may be implemented in more efficient ways
1425      * in certain backends.
1426      */
1427     public function increaseHitCount()
1428     {
1429         if ($this->_wikidb->readonly) {
1430             trigger_error("readonly database", E_USER_NOTICE);
1431             return;
1432         }
1433         if (method_exists($this->_wikidb->_backend, 'increaseHitCount'))
1434             $this->_wikidb->_backend->increaseHitCount($this->_pagename);
1435         else {
1436             @$newhits = $this->get('hits') + 1;
1437             $this->set('hits', $newhits);
1438         }
1439     }
1440
1441     /**
1442      * Return a string representation of the WikiDB_Page
1443      *
1444      * This is really only for debugging.
1445      *
1446      * @return string Printable representation of the WikiDB_Page.
1447      */
1448     public function asString()
1449     {
1450         ob_start();
1451         printf("[%s:%s\n", get_class($this), $this->getName());
1452         print_r($this->getMetaData());
1453         echo "]\n";
1454         $strval = ob_get_contents();
1455         ob_end_clean();
1456         return $strval;
1457     }
1458
1459     /**
1460      * @param int|object $version_or_pagerevision
1461      * Takes either the version number (and int) or a WikiDB_PageRevision
1462      * object.
1463      * @return integer The version number.
1464      */
1465     private function _coerce_to_version($version_or_pagerevision)
1466     {
1467         if (method_exists($version_or_pagerevision, "getContent"))
1468             $version = $version_or_pagerevision->getVersion();
1469         else
1470             $version = (int)$version_or_pagerevision;
1471
1472         assert($version >= 0);
1473         return $version;
1474     }
1475
1476     public function isUserPage($include_empty = true)
1477     {
1478         if (!$include_empty and !$this->exists()) return false;
1479         return $this->get('pref') ? true : false;
1480     }
1481
1482     // May be empty. Either the stored owner (/Chown), or the first authorized author
1483     public function getOwner()
1484     {
1485         if ($owner = $this->get('owner'))
1486             return $owner;
1487         // check all revisions forwards for the first author_id
1488         $backend = &$this->_wikidb->_backend;
1489         $pagename = &$this->_pagename;
1490         $latestversion = $backend->get_latest_version($pagename);
1491         for ($v = 1; $v <= $latestversion; $v++) {
1492             $rev = $this->getRevision($v, false);
1493             if ($rev and $owner = $rev->get('author_id')) {
1494                 return $owner;
1495             }
1496         }
1497         return '';
1498     }
1499
1500     // The authenticated author of the first revision or empty if not authenticated then.
1501     public function getCreator()
1502     {
1503         if ($current = $this->getRevision(1, false))
1504             return $current->get('author_id');
1505         else
1506             return '';
1507     }
1508
1509     // The authenticated author of the current revision.
1510     public function getAuthor()
1511     {
1512         if ($current = $this->getCurrentRevision(false))
1513             return $current->get('author_id');
1514         else
1515             return '';
1516     }
1517
1518     /* Semantic Web value, not stored in the links.
1519      * todo: unify with some unit knowledge
1520      */
1521     public function setAttribute($relation, $value)
1522     {
1523         $attr = $this->get('attributes');
1524         if (empty($attr))
1525             $attr = array($relation => $value);
1526         else
1527             $attr[$relation] = $value;
1528         $this->set('attributes', $attr);
1529     }
1530
1531     public function getAttribute($relation)
1532     {
1533         $meta = $this->get('attributes');
1534         if (empty($meta))
1535             return '';
1536         else
1537             return $meta[$relation];
1538     }
1539
1540 }
1541
1542 /**
1543  * This class represents a specific revision of a WikiDB_Page within
1544  * a WikiDB.
1545  *
1546  * A WikiDB_PageRevision has read-only semantics. You may only create
1547  * new revisions (and delete old ones) --- you cannot modify existing
1548  * revisions.
1549  */
1550 class WikiDB_PageRevision
1551 {
1552     public $_wikidb;
1553     public $_pagename;
1554     public $_version;
1555     public $_data;
1556     public $_transformedContent = false; // set by WikiDB_Page::save()
1557
1558     function __construct(&$wikidb, $pagename, $version, $versiondata = array())
1559     {
1560         $this->_wikidb = &$wikidb;
1561         $this->_pagename = $pagename;
1562         $this->_version = $version;
1563         $this->_data = $versiondata ? $versiondata : array();
1564         $this->_transformedContent = false; // set by WikiDB_Page::save()
1565     }
1566
1567     /**
1568      * Get the WikiDB_Page which this revision belongs to.
1569      *
1570      * @return WikiDB_Page The WikiDB_Page which this revision belongs to.
1571      */
1572     public function getPage()
1573     {
1574         return new WikiDB_Page($this->_wikidb, $this->_pagename);
1575     }
1576
1577     /**
1578      * Get the version number of this revision.
1579      *
1580      * @return int The version number of this revision.
1581      */
1582     public function getVersion()
1583     {
1584         return $this->_version;
1585     }
1586
1587     /**
1588      * Determine whether this revision has defaulted content.
1589      *
1590      * The default revision (version 0) of each page, as well as any
1591      * pages which are created with empty content have their content
1592      * defaulted to something like:
1593      * <pre>
1594      *   Describe [ThisPage] here.
1595      * </pre>
1596      *
1597      * @return boolean Returns true if the page has default content.
1598      */
1599     public function hasDefaultContents()
1600     {
1601         $data = &$this->_data;
1602         if (!isset($data['%content'])) return true;
1603         if ($data['%content'] === true) return false;
1604         return $data['%content'] === false or $data['%content'] === "";
1605     }
1606
1607     /**
1608      * Get the content as an array of lines.
1609      *
1610      * @return array An array of lines.
1611      * The lines should contain no trailing white space.
1612      */
1613     public function getContent()
1614     {
1615         return explode("\n", $this->getPackedContent());
1616     }
1617
1618     /**
1619      * Get the pagename of the revision.
1620      *
1621      * @return string pagename.
1622      */
1623     public function getPageName()
1624     {
1625         return $this->_pagename;
1626     }
1627
1628     public function getName()
1629     {
1630         return $this->_pagename;
1631     }
1632
1633     /**
1634      * Determine whether revision is the latest.
1635      *
1636      * @return boolean True iff the revision is the latest (most recent) one.
1637      */
1638     public function isCurrent()
1639     {
1640         if (!isset($this->_iscurrent)) {
1641             $page = $this->getPage();
1642             $current = $page->getCurrentRevision(false);
1643             $this->_iscurrent = $this->getVersion() == $current->getVersion();
1644         }
1645         return $this->_iscurrent;
1646     }
1647
1648     /**
1649      * Get the transformed content of a page.
1650      *
1651      * @param bool $pagetype_override
1652      * @return object An XmlContent-like object containing the page transformed
1653      * contents.
1654      */
1655     public function getTransformedContent($pagetype_override = false)
1656     {
1657         if ($pagetype_override) {
1658             // Figure out the normal page-type for this page.
1659             $type = PageType::GetPageType($this->get('pagetype'));
1660             if ($type->getName() == $pagetype_override)
1661                 $pagetype_override = false; // Not really an override...
1662         }
1663
1664         if ($pagetype_override) {
1665             // Overriden page type, don't cache (or check cache).
1666             return new TransformedText($this->getPage(),
1667                 $this->getPackedContent(),
1668                 $this->getMetaData(),
1669                 $pagetype_override);
1670         }
1671
1672         $possibly_cache_results = true;
1673
1674         if (!USECACHE or WIKIDB_NOCACHE_MARKUP) {
1675             if (WIKIDB_NOCACHE_MARKUP == 'purge') {
1676                 // flush cache for this page.
1677                 $page = $this->getPage();
1678                 $page->set('_cached_html', ''); // ignored with !USECACHE
1679             }
1680             $possibly_cache_results = false;
1681         } elseif (USECACHE and !$this->_transformedContent) {
1682             //$backend->lock();
1683             if ($this->isCurrent()) {
1684                 $page = $this->getPage();
1685                 $this->_transformedContent = TransformedText::unpack($page->get('_cached_html'));
1686             } else {
1687                 $possibly_cache_results = false;
1688             }
1689             //$backend->unlock();
1690         }
1691
1692         if (!$this->_transformedContent) {
1693             $this->_transformedContent
1694                 = new TransformedText($this->getPage(),
1695                 $this->getPackedContent(),
1696                 $this->getMetaData());
1697
1698             if ($possibly_cache_results and !WIKIDB_NOCACHE_MARKUP) {
1699                 // If we're still the current version, cache the transformed page.
1700                 //$backend->lock();
1701                 if ($this->isCurrent()) {
1702                     $page->set('_cached_html', $this->_transformedContent->pack());
1703                 }
1704                 //$backend->unlock();
1705             }
1706         }
1707
1708         return $this->_transformedContent;
1709     }
1710
1711     /**
1712      * Get the content as a string.
1713      *
1714      * @return string The page content.
1715      * Lines are separated by new-lines.
1716      */
1717     public function getPackedContent()
1718     {
1719         /**
1720          * @var WikiRequest $request
1721          */
1722         global $request;
1723
1724         $data = &$this->_data;
1725
1726         if (empty($data['%content'])
1727             || (!$this->_wikidb->isWikiPage($this->_pagename)
1728                 && $this->isCurrent())
1729         ) {
1730             include_once 'lib/InlineParser.php';
1731
1732             // A feature similar to taglines at http://www.wlug.org.nz/
1733             // Lib from http://www.aasted.org/quote/
1734             if (defined('FORTUNE_DIR')
1735                 and is_dir(FORTUNE_DIR)
1736                     and in_array($request->getArg('action'),
1737                         array('create', 'edit'))
1738             ) {
1739                 include_once 'lib/fortune.php';
1740                 $fortune = new Fortune();
1741                 $quote = $fortune->quoteFromDir(FORTUNE_DIR);
1742                 if ($quote != -1)
1743                     $quote = "<verbatim>\n"
1744                         . str_replace("\n<br>", "\n", $quote)
1745                         . "</verbatim>\n\n";
1746                 else
1747                     $quote = "";
1748                 return $quote
1749                     . sprintf(_("Describe %s here."),
1750                         "[" . WikiEscape($this->_pagename) . "]");
1751             }
1752             // Replace empty content with default value.
1753             return sprintf(_("Describe %s here."),
1754                 "[" . WikiEscape($this->_pagename) . "]");
1755         }
1756
1757         // There is (non-default) content.
1758         assert($this->_version > 0);
1759
1760         if (!is_string($data['%content'])) {
1761             // Content was not provided to us at init time.
1762             // (This is allowed because for some backends, fetching
1763             // the content may be expensive, and often is not wanted
1764             // by the user.)
1765             //
1766             // In any case, now we need to get it.
1767             $data['%content'] = $this->_get_content();
1768             assert(is_string($data['%content']));
1769         }
1770
1771         return $data['%content'];
1772     }
1773
1774     public function _get_content()
1775     {
1776         $cache = &$this->_wikidb->_cache;
1777         $pagename = $this->_pagename;
1778         $version = $this->_version;
1779
1780         assert($version > 0);
1781
1782         $newdata = $cache->get_versiondata($pagename, $version, true);
1783         if ($newdata) {
1784             assert(is_string($newdata['%content']));
1785             return $newdata['%content'];
1786         } else {
1787             // else revision has been deleted... What to do?
1788             return __sprintf("Oops! Revision %s of %s seems to have been deleted!",
1789                 $version, $pagename);
1790         }
1791     }
1792
1793     /**
1794      * Get meta-data for this revision.
1795      *
1796      * @param string $key Which meta-data to access.
1797      *
1798      * Some reserved revision meta-data keys are:
1799      * <dl>
1800      * <dt> 'mtime' <dd> Time this revision was created (seconds since midnight Jan 1, 1970.)
1801      *        The 'mtime' meta-value is normally set automatically by the database
1802      *        backend, but it may be specified explicitly when creating a new revision.
1803      * <dt> orig_mtime
1804      *  <dd> To ensure consistency of RecentChanges, the mtimes of the versions
1805      *       of a page must be monotonically increasing.  If an attempt is
1806      *       made to create a new revision with an mtime less than that of
1807      *       the preceeding revision, the new revisions timestamp is force
1808      *       to be equal to that of the preceeding revision.  In that case,
1809      *       the originally requested mtime is preserved in 'orig_mtime'.
1810      * <dt> '_supplanted' <dd> Time this revision ceased to be the most recent.
1811      *        This meta-value is <em>always</em> automatically maintained by the database
1812      *        backend.  (It is set from the 'mtime' meta-value of the superceding
1813      *        revision.)  '_supplanted' has a value of 'false' for the current revision.
1814      *
1815      * FIXME: this could be refactored:
1816      * <dt> author
1817      *  <dd> Author of the page (as he should be reported in, e.g. RecentChanges.)
1818      * <dt> author_id
1819      *  <dd> Authenticated author of a page.  This is used to identify
1820      *       the distinctness of authors when cleaning old revisions from
1821      *       the database.
1822      * <dt> 'is_minor_edit' <dd> Set if change was marked as a minor revision by the author.
1823      * <dt> 'summary' <dd> Short change summary entered by page author.
1824      * </dl>
1825      *
1826      * Meta-data keys must be valid C identifers (they have to start with a letter
1827      * or underscore, and can contain only alphanumerics and underscores.)
1828      *
1829      * @return string The requested value, or false if the requested value
1830      * is not defined.
1831      */
1832     public function get($key)
1833     {
1834         if (!$key || $key[0] == '%')
1835             return false;
1836         $data = &$this->_data;
1837         return isset($data[$key]) ? $data[$key] : false;
1838     }
1839
1840     /**
1841      * Get all the revision page meta-data as a hash.
1842      *
1843      * @return array The revision meta-data.
1844      */
1845     public function getMetaData()
1846     {
1847         $meta = array();
1848         foreach ($this->_data as $key => $val) {
1849             if (!empty($val) && $key[0] != '%')
1850                 $meta[$key] = $val;
1851         }
1852         return $meta;
1853     }
1854
1855     /**
1856      * Return a string representation of the revision.
1857      *
1858      * This is really only for debugging.
1859      *
1860      * @return string Printable representation of the WikiDB_Page.
1861      */
1862     public function asString()
1863     {
1864         ob_start();
1865         printf("[%s:%d\n", get_class($this), $this->get('version'));
1866         print_r($this->_data);
1867         echo $this->getPackedContent() . "\n]\n";
1868         $strval = ob_get_contents();
1869         ob_end_clean();
1870         return $strval;
1871     }
1872 }
1873
1874 /**
1875  * Class representing a sequence of WikiDB_Pages.
1876  * TODO: Enhance to php5 iterators
1877  * TODO:
1878  *   apply filters for options like 'sortby', 'limit', 'exclude'
1879  *   for simple queries like titleSearch, where the backend is not ready yet.
1880  */
1881 class WikiDB_PageIterator
1882 {
1883     public $_iter;
1884     public $_wikidb;
1885     public $_options;
1886
1887     function __construct(&$wikidb, &$iter, $options = array())
1888     {
1889         $this->_iter = $iter; // a WikiDB_backend_iterator
1890         $this->_wikidb = &$wikidb;
1891         $this->_options = $options;
1892     }
1893
1894     public function count()
1895     {
1896         return $this->_iter->count();
1897     }
1898
1899     public function limit()
1900     {
1901         return empty($this->_options['limit']) ? 0 : $this->_options['limit'];
1902     }
1903
1904     /**
1905      * Get next WikiDB_Page in sequence.
1906      *
1907      * @return WikiDB_Page The next WikiDB_Page in the sequence.
1908      */
1909     public function next()
1910     {
1911         if (!($next = $this->_iter->next())) {
1912             return false;
1913         }
1914
1915         $pagename = &$next['pagename'];
1916         if (!is_string($pagename)) { // Bug #1327912 fixed by Joachim Lous
1917             trigger_error("WikiDB_PageIterator->next pagename", E_USER_WARNING);
1918         }
1919
1920         if (!$pagename) {
1921             if (isset($next['linkrelation'])
1922                 or isset($next['pagedata']['linkrelation'])
1923             ) {
1924                 return false;
1925             }
1926         }
1927
1928         // There's always hits, but we cache only if more
1929         // (well not with file and dba)
1930         if (isset($next['pagedata']) and count($next['pagedata']) > 1) {
1931             $this->_wikidb->_cache->cache_data($next);
1932             // cache existing page id's since we iterate over all links in GleanDescription
1933             // and need them later for LinkExistingWord
1934         } elseif ($this->_options and array_key_exists('include_empty', $this->_options)
1935             and !$this->_options['include_empty'] and isset($next['id'])
1936         ) {
1937             $this->_wikidb->_cache->_id_cache[$next['pagename']] = $next['id'];
1938         }
1939         $page = new WikiDB_Page($this->_wikidb, $pagename);
1940         if (isset($next['linkrelation']))
1941             $page->set('linkrelation', $next['linkrelation']);
1942         if (isset($next['score']))
1943             $page->score = $next['score'];
1944         return $page;
1945     }
1946
1947     /**
1948      * Release resources held by this iterator.
1949      *
1950      * The iterator may not be used after free() is called.
1951      *
1952      * There is no need to call free(), if next() has returned false.
1953      * (I.e. if you iterate through all the pages in the sequence,
1954      * you do not need to call free() --- you only need to call it
1955      * if you stop before the end of the iterator is reached.)
1956      */
1957     public function free()
1958     {
1959         // $this->_iter->free();
1960     }
1961
1962     public function reset()
1963     {
1964         $this->_iter->reset();
1965     }
1966
1967     public function asArray()
1968     {
1969         $result = array();
1970         while ($page = $this->next())
1971             $result[] = $page;
1972         $this->reset();
1973         return $result;
1974     }
1975 }
1976
1977 /**
1978  * A class which represents a sequence of WikiDB_PageRevisions.
1979  * TODO: Enhance to php5 iterators
1980  */
1981 class WikiDB_PageRevisionIterator
1982 {
1983     public $_revisions;
1984     public $_wikidb;
1985     public $_options;
1986
1987     function __construct(&$wikidb, &$revisions, $options = false)
1988     {
1989         $this->_revisions = $revisions;
1990         $this->_wikidb = &$wikidb;
1991         $this->_options = $options;
1992     }
1993
1994     public function count()
1995     {
1996         return $this->_revisions->count();
1997     }
1998
1999     /**
2000      * Get next WikiDB_PageRevision in sequence.
2001      *
2002      * @return WikiDB_PageRevision
2003      * The next WikiDB_PageRevision in the sequence.
2004      */
2005     public function next()
2006     {
2007         if (!($next = $this->_revisions->next()))
2008             return false;
2009
2010         //$this->_wikidb->_cache->cache_data($next);
2011
2012         $pagename = $next['pagename'];
2013         $version = $next['version'];
2014         $versiondata = $next['versiondata'];
2015         if ((int)DEBUG) {
2016             if (!(is_string($pagename) and $pagename != '')) {
2017                 trigger_error("empty pagename", E_USER_WARNING);
2018                 return false;
2019             }
2020         } else assert(is_string($pagename) and $pagename != '');
2021         if ((int)DEBUG) {
2022             if (!is_array($versiondata)) {
2023                 trigger_error("empty versiondata", E_USER_WARNING);
2024                 return false;
2025             }
2026         } else assert(is_array($versiondata));
2027         if ((int)DEBUG) {
2028             if (!($version > 0)) {
2029                 trigger_error("invalid version", E_USER_WARNING);
2030                 return false;
2031             }
2032         } else assert($version > 0);
2033
2034         return new WikiDB_PageRevision($this->_wikidb, $pagename, $version, $versiondata);
2035     }
2036
2037     /**
2038      * Release resources held by this iterator.
2039      *
2040      * The iterator may not be used after free() is called.
2041      *
2042      * There is no need to call free(), if next() has returned false.
2043      * (I.e. if you iterate through all the revisions in the sequence,
2044      * you do not need to call free() --- you only need to call it
2045      * if you stop before the end of the iterator is reached.)
2046      */
2047     public function free()
2048     {
2049         $this->_revisions->free();
2050     }
2051
2052     public function asArray()
2053     {
2054         $result = array();
2055         while ($rev = $this->next())
2056             $result[] = $rev;
2057         $this->free();
2058         return $result;
2059     }
2060 }
2061
2062 /** pseudo iterator
2063  */
2064 class WikiDB_Array_PageIterator
2065 {
2066     public $_dbi;
2067     public $_pages;
2068
2069     function __construct ($pagenames)
2070     {
2071         /**
2072          * @var WikiRequest $request
2073          */
2074         global $request;
2075
2076         $this->_dbi = $request->getDbh();
2077         $this->_pages = $pagenames;
2078         reset($this->_pages);
2079     }
2080
2081     public function next()
2082     {
2083         $c = current($this->_pages);
2084         next($this->_pages);
2085         return $c !== false ? $this->_dbi->getPage($c) : false;
2086     }
2087
2088     public function count()
2089     {
2090         return count($this->_pages);
2091     }
2092
2093     public function reset()
2094     {
2095         reset($this->_pages);
2096     }
2097
2098     public function free()
2099     {
2100     }
2101
2102     public function asArray()
2103     {
2104         reset($this->_pages);
2105         return $this->_pages;
2106     }
2107 }
2108
2109 class WikiDB_Array_generic_iter
2110 {
2111     public $_array;
2112
2113     function __construct($result)
2114     {
2115         // $result may be either an array or a query result
2116         if (is_array($result)) {
2117             $this->_array = $result;
2118         } elseif (is_object($result)) {
2119             $this->_array = $result->asArray();
2120         } else {
2121             $this->_array = array();
2122         }
2123         if (!empty($this->_array))
2124             reset($this->_array);
2125     }
2126
2127     public function next()
2128     {
2129         $c = current($this->_array);
2130         next($this->_array);
2131         return $c !== false ? $c : false;
2132     }
2133
2134     public function count()
2135     {
2136         return count($this->_array);
2137     }
2138
2139     public function reset()
2140     {
2141         reset($this->_array);
2142     }
2143
2144     public function free()
2145     {
2146     }
2147
2148     public function asArray()
2149     {
2150         if (!empty($this->_array))
2151             reset($this->_array);
2152         return $this->_array;
2153     }
2154 }
2155
2156 /**
2157  * Data cache used by WikiDB.
2158  *
2159  * FIXME: Maybe rename this to caching_backend (or some such).
2160  *
2161  * @access private
2162  */
2163 class WikiDB_cache
2164 {
2165     // FIXME: beautify versiondata cache.  Cache only limited data?
2166
2167     public $_backend;
2168     public $_pagedata_cache;
2169     public $_versiondata_cache;
2170     public $_glv_cache;
2171     public $_id_cache;
2172     public $readonly;
2173
2174     function __construct(&$backend)
2175     {
2176         /**
2177          * @var WikiRequest $request
2178          */
2179         global $request;
2180
2181         $this->_backend = &$backend;
2182         $this->_pagedata_cache = array();
2183         $this->_versiondata_cache = array();
2184         array_push($this->_versiondata_cache, array());
2185         $this->_glv_cache = array();
2186         $this->_id_cache = array(); // formerly ->_dbi->_iwpcache (nonempty pages => id)
2187
2188         if (isset($request->_dbi))
2189             $this->readonly = $request->_dbi->readonly;
2190     }
2191
2192     public function close()
2193     {
2194         $this->_pagedata_cache = array();
2195         $this->_versiondata_cache = array();
2196         $this->_glv_cache = array();
2197         $this->_id_cache = array();
2198     }
2199
2200     public function get_pagedata($pagename)
2201     {
2202         assert(is_string($pagename) && $pagename != '');
2203         if (USECACHE) {
2204             $cache = &$this->_pagedata_cache;
2205             if (!isset($cache[$pagename]) || !is_array($cache[$pagename])) {
2206                 $cache[$pagename] = $this->_backend->get_pagedata($pagename);
2207                 if (empty($cache[$pagename]))
2208                     $cache[$pagename] = array();
2209             }
2210             return $cache[$pagename];
2211         } else {
2212             return $this->_backend->get_pagedata($pagename);
2213         }
2214     }
2215
2216     public function update_pagedata($pagename, $newdata)
2217     {
2218         assert(is_string($pagename) && $pagename != '');
2219         if (!empty($this->readonly)) {
2220             trigger_error("readonly database", E_USER_WARNING);
2221             return;
2222         }
2223
2224         $this->_backend->update_pagedata($pagename, $newdata);
2225
2226         if (USECACHE) {
2227             if (!empty($this->_pagedata_cache[$pagename])
2228                 and is_array($this->_pagedata_cache[$pagename])
2229             ) {
2230                 $cachedata = &$this->_pagedata_cache[$pagename];
2231                 foreach ($newdata as $key => $val)
2232                     $cachedata[$key] = $val;
2233             } else
2234                 $this->_pagedata_cache[$pagename] = $newdata;
2235         }
2236     }
2237
2238     public function invalidate_cache($pagename)
2239     {
2240         unset ($this->_pagedata_cache[$pagename]);
2241         unset ($this->_versiondata_cache[$pagename]);
2242         unset ($this->_glv_cache[$pagename]);
2243         unset ($this->_id_cache[$pagename]);
2244         //unset ($this->_backend->_page_data);
2245     }
2246
2247     public function delete_page($pagename)
2248     {
2249         if (!empty($this->readonly)) {
2250             trigger_error("readonly database", E_USER_WARNING);
2251             return false;
2252         }
2253         $result = $this->_backend->delete_page($pagename);
2254         $this->invalidate_cache($pagename);
2255         return $result;
2256     }
2257
2258     public function purge_page($pagename)
2259     {
2260         if (!empty($this->readonly)) {
2261             trigger_error("readonly database", E_USER_WARNING);
2262             return false;
2263         }
2264         $result = $this->_backend->purge_page($pagename);
2265         $this->invalidate_cache($pagename);
2266         return $result;
2267     }
2268
2269     // FIXME: ugly and wrong. may overwrite full cache with partial cache
2270     public function cache_data($data)
2271     {
2272         ;
2273         //if (isset($data['pagedata']))
2274         //    $this->_pagedata_cache[$data['pagename']] = $data['pagedata'];
2275     }
2276
2277     public function get_versiondata($pagename, $version, $need_content = false)
2278     {
2279         //  FIXME: Seriously ugly hackage
2280         $readdata = false;
2281         if (USECACHE) { //temporary - for debugging
2282             assert(is_string($pagename) && $pagename != '');
2283             // There is a bug here somewhere which results in an assertion failure at line 105
2284             // of ArchiveCleaner.php  It goes away if we use the next line.
2285             //$need_content = true;
2286             $nc = $need_content ? '1' : '0';
2287             $cache = &$this->_versiondata_cache;
2288             if (!isset($cache[$pagename][$version][$nc])
2289                 || !(is_array($cache[$pagename]))
2290                 || !(is_array($cache[$pagename][$version]))
2291             ) {
2292                 $cache[$pagename][$version][$nc] =
2293                     $this->_backend->get_versiondata($pagename, $version, $need_content);
2294                 $readdata = true;
2295                 // If we have retrieved all data, we may as well set the cache for
2296                 // $need_content = false
2297                 if ($need_content) {
2298                     $cache[$pagename][$version]['0'] =& $cache[$pagename][$version]['1'];
2299                 }
2300             }
2301             $vdata = $cache[$pagename][$version][$nc];
2302         } else {
2303             $vdata = $this->_backend->get_versiondata($pagename, $version, $need_content);
2304             $readdata = true;
2305         }
2306         if ($readdata && is_array($vdata) && !empty($vdata['%pagedata'])) {
2307             if (empty($this->_pagedata_cache))
2308                 $this->_pagedata_cache = array();
2309             /* 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 */
2310             $this->_pagedata_cache[$pagename] = $vdata['%pagedata'];
2311         }
2312         return $vdata;
2313     }
2314
2315     public function set_versiondata($pagename, $version, $data)
2316     {
2317         //unset($this->_versiondata_cache[$pagename][$version]);
2318
2319         if (!empty($this->readonly)) {
2320             trigger_error("readonly database", E_USER_WARNING);
2321             return;
2322         }
2323         $this->_backend->set_versiondata($pagename, $version, $data);
2324         // Update the cache
2325         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
2326         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
2327         // Is this necessary?
2328         unset($this->_glv_cache[$pagename]);
2329     }
2330
2331     public function update_versiondata($pagename, $version, $data)
2332     {
2333         if (!empty($this->readonly)) {
2334             trigger_error("readonly database", E_USER_WARNING);
2335             return;
2336         }
2337         $this->_backend->update_versiondata($pagename, $version, $data);
2338         // Update the cache
2339         $this->_versiondata_cache[$pagename][$version]['1'] = $data;
2340         // FIXME: hack
2341         $this->_versiondata_cache[$pagename][$version]['0'] = $data;
2342         // Is this necessary?
2343         unset($this->_glv_cache[$pagename]);
2344     }
2345
2346     public function delete_versiondata($pagename, $version)
2347     {
2348         if (!empty($this->readonly)) {
2349             trigger_error("readonly database", E_USER_WARNING);
2350             return;
2351         }
2352         $this->_backend->delete_versiondata($pagename, $version);
2353         if (isset($this->_versiondata_cache[$pagename][$version]))
2354             unset ($this->_versiondata_cache[$pagename][$version]);
2355         // dirty latest version cache only if latest version gets deleted
2356         if (isset($this->_glv_cache[$pagename]) and $this->_glv_cache[$pagename] == $version)
2357             unset ($this->_glv_cache[$pagename]);
2358     }
2359
2360     public function get_latest_version($pagename)
2361     {
2362         if (USECACHE) {
2363             assert(is_string($pagename) && $pagename != '');
2364             $cache = &$this->_glv_cache;
2365             if (!isset($cache[$pagename])) {
2366                 $cache[$pagename] = $this->_backend->get_latest_version($pagename);
2367                 if (empty($cache[$pagename]))
2368                     $cache[$pagename] = 0;
2369             }
2370             return $cache[$pagename];
2371         } else {
2372             return $this->_backend->get_latest_version($pagename);
2373         }
2374     }
2375 }
2376
2377 // Local Variables:
2378 // mode: php
2379 // tab-width: 8
2380 // c-basic-offset: 4
2381 // c-hanging-comment-ender-p: nil
2382 // indent-tabs-mode: nil
2383 // End: