]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend.php
support new non-destructive delete_page via generic backend method
[SourceForge/phpwiki.git] / lib / WikiDB / backend.php
1 <?php // -*-php-*-
2 rcs_id('$Id: backend.php,v 1.21 2004-12-08 12:55:50 rurban Exp $');
3
4 /*
5   Pagedata
6
7    maintained by WikiPage 
8     //:latestversion
9     //:deleted (*)     (Set if latest content is empty.)
10     //:pagename (*)
11
12     hits
13     is_locked
14
15   Versiondata
16
17     %content (?should this be here?)
18     _supplanted : Time version ceased to be the current version
19
20     mtime (*)   : Time of version edit.
21     orig_mtime
22     is_minor_edit (*)
23     author      : nominal author
24     author_id   : authenticated author
25     summary
26
27     //version
28     //created (*)
29     //%superceded
30         
31     //:serial
32
33      (types are scalars: strings, ints, bools)
34 */     
35
36 /**
37  * A WikiDB_backend handles the storage and retrieval of data for a WikiDB.
38  *
39  * A WikiDB_backend handles the storage and retrieval of data for a WikiDB.
40  * It does not have to be this way, of course, but the standard WikiDB uses
41  * a WikiDB_backend.  (Other WikiDB's could be written which use some other
42  * method to access their underlying data store.)
43  *
44  * The interface outlined here seems to work well with both RDBM based
45  * and flat DBM/hash based methods of data storage.
46  *
47  * Though it contains some default implementation of certain methods,
48  * this is an abstract base class.  It is expected that most effificient
49  * backends will override nearly all the methods in this class.
50  *
51  * @access protected
52  * @see WikiDB
53  */
54 class WikiDB_backend
55 {
56     /**
57      * Get page meta-data from database.
58      *
59      * @param $pagename string Page name.
60      * @return hash
61      * Returns a hash containing the page meta-data.
62      * Returns an empty array if there is no meta-data for the requested page.
63      * Keys which might be present in the hash are:
64      * <dl>
65      *  <dt> locked  <dd> If the page is locked.
66      *  <dt> hits    <dd> The page hit count.
67      *  <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
68      *                    don't think we need this...) 
69      * </dl>
70      */
71     function get_pagedata($pagename) {
72         trigger_error("virtual", E_USER_ERROR);
73     }
74
75     /**
76      * Update the page meta-data.
77      *
78      * Set page meta-data.
79      *
80      * Only meta-data whose keys are preset in $newdata is affected.
81      *
82      * For example:
83      * <pre>
84      *   $backend->update_pagedata($pagename, array('locked' => 1)); 
85      * </pre>
86      * will set the value of 'locked' to 1 for the specified page, but it
87      * will not affect the value of 'hits' (or whatever other meta-data
88      * may have been stored for the page.)
89      *
90      * To delete a particular piece of meta-data, set it's value to false.
91      * <pre>
92      *   $backend->update_pagedata($pagename, array('locked' => false)); 
93      * </pre>
94      *
95      * @param $pagename string Page name.
96      * @param $newdata hash New meta-data.
97      */
98     function update_pagedata($pagename, $newdata) {
99         trigger_error("virtual", E_USER_ERROR);
100     }
101     
102
103     /**
104      * Get the current version number for a page.
105      *
106      * @param $pagename string Page name.
107      * @return int The latest version number for the page.  Returns zero if
108      *  no versions of a page exist.
109      */
110     function get_latest_version($pagename) {
111         trigger_error("virtual", E_USER_ERROR);
112     }
113     
114     /**
115      * Get preceding version number.
116      *
117      * @param $pagename string Page name.
118      * @param $version int Find version before this one.
119      * @return int The version number of the version in the database which
120      *  immediately preceeds $version.
121      */
122     function get_previous_version($pagename, $version) {
123         trigger_error("virtual", E_USER_ERROR);
124     }
125     
126     /**
127      * Get revision meta-data and content.
128      *
129      * @param $pagename string Page name.
130      * @param $version integer Which version to get.
131      * @param $want_content boolean
132      *  Indicates the caller really wants the page content.  If this
133      *  flag is not set, the backend is free to skip fetching of the
134      *  page content (as that may be expensive).  If the backend omits
135      *  the content, the backend might still want to set the value of
136      *  '%content' to the empty string if it knows there's no content.
137      *
138      * @return hash The version data, or false if specified version does not
139      *    exist.
140      *
141      * Some keys which might be present in the $versiondata hash are:
142      * <dl>
143      * <dt> %content
144      *  <dd> This is a pseudo-meta-data element (since it's actually
145      *       the page data, get it?) containing the page content.
146      *       If the content was not fetched, this key may not be present.
147      * </dl>
148      * For description of other version meta-data see WikiDB_PageRevision::get().
149      * @see WikiDB_PageRevision::get
150      */
151     function get_versiondata($pagename, $version, $want_content = false) {
152         trigger_error("virtual", E_USER_ERROR);
153     }
154
155     /**
156      * Delete page from the database with backup possibility.
157      * This should remove all links (from the named page) from
158      * the link database.
159      *
160      * @param $pagename string Page name.
161      * i.e save_page('') and DELETE nonempty id
162      * Can be undone and is seen in RecentChanges.
163      */
164     function delete_page($pagename) {
165         $mtime = time();
166         $user =& $GLOBALS['request']->_user;
167         $vdata = array('author' => $user->getId(),
168                        'author_id' => $user->getAuthenticatedId(),
169                        'mtime' => $mtime);
170
171         $this->lock(); // critical section:
172         $version = $this->get_latest_version($pagename);
173         $this->set_versiondata($pagename, $version+1, $vdata);
174         $this->set_links($pagename, false); // links are purged.
175         if (! WIKIDB_NOCACHE_MARKUP) {
176             // need the hits, perms and LOCKED, otherwise you can reset the perm 
177             // by action=remove and re-create it with default perms
178             $pagedata = $this->get_pagedata($pagename); 
179             unset($pagedata['_cached_html']);
180             $this->update_pagedata($pagename, $pagedata);
181         }
182         $this->unlock();
183     }
184
185     /**
186      * Delete page (and all it's revisions) from the database.
187      *
188      */
189     function purge_page($pagename) {
190         trigger_error("virtual", E_USER_ERROR);
191     }
192
193     /**
194      * Delete an old revision of a page.
195      *
196      * Note that one is never allowed to delete the most recent version,
197      * but that this requirement is enforced by WikiDB not by the backend.
198      *
199      * In fact, to be safe, backends should probably allow the deletion of
200      * the most recent version.
201      *
202      * @param $pagename string Page name.
203      * @param $version integer Version to delete.
204      */
205     function delete_versiondata($pagename, $version) {
206         trigger_error("virtual", E_USER_ERROR);
207     }
208
209     /**
210      * Create a new page revision.
211      *
212      * If the given ($pagename,$version) is already in the database,
213      * this method completely overwrites any stored data for that version.
214      *
215      * @param $pagename string Page name.
216      * @param $version int New revisions content.
217      * @param $data hash New revision metadata.
218      *
219      * @see get_versiondata
220      */
221     function set_versiondata($pagename, $version, $data) {
222         trigger_error("virtual", E_USER_ERROR);
223     }
224
225     /**
226      * Update page version meta-data.
227      *
228      * If the given ($pagename,$version) is already in the database,
229      * this method only changes those meta-data values whose keys are
230      * explicity listed in $newdata.
231      *
232      * @param $pagename string Page name.
233      * @param $version int New revisions content.
234      * @param $newdata hash New revision metadata.
235      * @see set_versiondata, get_versiondata
236      */
237     function update_versiondata($pagename, $version, $newdata) {
238         $data = $this->get_versiondata($pagename, $version, true);
239         if (!$data) {
240             assert($data);
241             return;
242         }
243         foreach ($newdata as $key => $val) {
244             if (empty($val))
245                 unset($data[$key]);
246             else
247                 $data[$key] = $val;
248         }
249         $this->set_versiondata($pagename, $version, $data);
250     }
251     
252     /**
253      * Set links for page.
254      *
255      * @param $pagename string Page name.
256      *
257      * @param $links array List of page(names) which page links to.
258      */
259     function set_links($pagename, $links) {
260         trigger_error("virtual", E_USER_ERROR);
261     }
262         
263     /**
264      * Find pages which link to or are linked from a page.
265      *
266      * @param $pagename string Page name.
267      * @param $reversed boolean True to get backlinks.
268      *
269      * FIXME: array or iterator?
270      * @return object A WikiDB_backend_iterator.
271      */
272     function get_links($pagename, $reversed, $include_empty=false,
273                        $sortby=false, $limit=false, $exclude=false) {
274         //FIXME: implement simple (but slow) link finder.
275         die("FIXME get_links");
276     }
277
278     /**
279      * Get all revisions of a page.
280      *
281      * @param $pagename string The page name.
282      * @return object A WikiDB_backend_iterator.
283      */
284     function get_all_revisions($pagename) {
285         include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
286         return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
287     }
288     
289     /**
290      * Get all pages in the database.
291      *
292      * Pages should be returned in alphabetical order if that is
293      * feasable.
294      *
295      * @access protected
296      *
297      * @param $include_defaulted boolean
298      * If set, even pages with no content will be returned
299      * --- but still only if they have at least one revision (not
300      * counting the default revision 0) entered in the database.
301      *
302      * Normally pages whose current revision has empty content
303      * are not returned as these pages are considered to be
304      * non-existing.
305      *
306      * @return object A WikiDB_backend_iterator.
307      */
308     function get_all_pages($include_defaulted, $orderby=false, $limit=false, $exclude=false) {
309         trigger_error("virtual", E_USER_ERROR);
310     }
311         
312     /**
313      * Title or full text search.
314      *
315      * Pages should be returned in alphabetical order if that is
316      * feasable.
317      *
318      * @access protected
319      *
320      * @param $search object A TextSearchQuery object describing the parsed query string, 
321      *                       with efficient methods for SQL and PCRE match.
322      *
323      * @param $fullsearch boolean If true, a full text search is performed,
324      *  otherwise a title search is performed.
325      *
326      * @return object A WikiDB_backend_iterator.
327      *
328      * @see WikiDB::titleSearch
329      */
330     function text_search($search, $fulltext=false) {
331         // This is method implements a simple linear search
332         // through all the pages in the database.
333         //
334         // It is expected that most backends will overload
335         // this method with something more efficient.
336         include_once('lib/WikiDB/backend/dumb/TextSearchIter.php');
337         $pages = $this->get_all_pages(false);
338         return new WikiDB_backend_dumb_TextSearchIter($this, $pages, $search, $fulltext);
339     }
340
341     /**
342      * Find pages with highest hit counts.
343      *
344      * Find the pages with the highest hit counts.  The pages should
345      * be returned in reverse order by hit count.
346      *
347      * @access protected
348      * @param $limit integer  No more than this many pages
349      * @return object A WikiDB_backend_iterator.
350      */
351     function most_popular($limit, $sortby='-hits') {
352         // This is method fetches all pages, then
353         // sorts them by hit count.
354         // (Not very efficient.)
355         //
356         // It is expected that most backends will overload
357         // method with something more efficient.
358         include_once('lib/WikiDB/backend/dumb/MostPopularIter.php');
359         $pages = $this->get_all_pages(false, $sortby, $limit);
360         return new WikiDB_backend_dumb_MostPopularIter($this, $pages, $limit);
361     }
362
363     /**
364      * Find recent changes.
365      *
366      * @access protected
367      * @param $params hash See WikiDB::mostRecent for a description
368      *  of parameters which can be included in this hash.
369      * @return object A WikiDB_backend_iterator.
370      * @see WikiDB::mostRecent
371      */
372     function most_recent($params) {
373         // This method is very inefficient and searches through
374         // all pages for the most recent changes.
375         //
376         // It is expected that most backends will overload
377         // method with something more efficient.
378         include_once('lib/WikiDB/backend/dumb/MostRecentIter.php');
379         $pages = $this->get_all_pages(true, '-mtime');
380         return new WikiDB_backend_dumb_MostRecentIter($this, $pages, $params);
381     }
382
383     function wanted_pages($exclude_from='', $exclude='', $sortby=false, $limit=false) {
384         include_once('lib/WikiDB/backend/dumb/WantedPagesIter.php');
385         $allpages = $this->get_all_pages(true,false,false,$exclude_from);
386         return new WikiDB_backend_dumb_WantedPagesIter($this, $allpages, $exclude, $sortby, $limit);
387     }
388
389     /**
390      * Lock backend database.
391      *
392      * Calls may be nested.
393      *
394      * @param $write_lock boolean Unless this is set to false, a write lock
395      *     is acquired, otherwise a read lock.  If the backend doesn't support
396      *     read locking, then it should make a write lock no matter which type
397      *     of lock was requested.
398      *
399      *     All backends <em>should</em> support write locking.
400      */
401     function lock($write_lock = true) {
402     }
403
404     /**
405      * Unlock backend database.
406      *
407      * @param $force boolean Normally, the database is not unlocked until
408      *  unlock() is called as many times as lock() has been.  If $force is
409      *  set to true, the the database is unconditionally unlocked.
410      */
411     function unlock($force = false) {
412     }
413
414
415     /**
416      * Close database.
417      */
418     function close () {
419     }
420
421     /**
422      * Synchronize with filesystem.
423      *
424      * This should flush all unwritten data to the filesystem.
425      */
426     function sync() {
427     }
428
429     /**
430      * Optimize the database.
431      */
432     function optimize() {
433     }
434
435     /**
436      * Check database integrity.
437      *
438      * This should check the validity of the internal structure of the database.
439      * Errors should be reported via:
440      * <pre>
441      *   trigger_error("Message goes here.", E_USER_WARNING);
442      * </pre>
443      *
444      * @return boolean True iff database is in a consistent state.
445      */
446     function check() {
447     }
448
449     /**
450      * Put the database into a consistent state.
451      *
452      * This should put the database into a consistent state.
453      * (I.e. rebuild indexes, etc...)
454      *
455      * @return boolean True iff successful.
456      */
457     function rebuild() {
458     }
459
460     function _parse_searchwords($search) {
461         $search = strtolower(trim($search));
462         if (!$search)
463             return array(array(),array());
464         
465         $words = preg_split('/\s+/', $search);
466         $exclude = array();
467         foreach ($words as $key => $word) {
468             if ($word[0] == '-' && $word != '-') {
469                 $word = substr($word, 1);
470                 $exclude[] = preg_quote($word);
471                 unset($words[$key]);
472             }
473         }
474         return array($words, $exclude);
475     }
476
477     /** 
478      * Split the given limit parameter into offset,pagesize. (offset is optional. default: 0)
479      * Duplicate the PageList function here to avoid loading the whole PageList.php 
480      * Usage: 
481      *   list($offset,$pagesize) = $this->limit($args['limit']);
482      */
483     function limit($limit) {
484         if (strstr($limit, ','))
485             return split(',', $limit);
486         else
487             return array(0, $limit);
488     }
489     
490     /** 
491      * Handle sortby requests for the DB iterator and table header links.
492      * Prefix the column with + or - like "+pagename","-mtime", ...
493      * supported actions: 'flip_order' "mtime" => "+mtime" => "-mtime" ...
494      *                    'db'         "-pagename" => "pagename DESC"
495      * In PageList all columns are sortable. (patch by DanFr)
496      * Here with the backend only some, the rest is delayed to PageList.
497      * (some kind of DumbIter)
498      * Duplicate the PageList function here to avoid loading the whole 
499      * PageList.php, and it forces the backend specific sortable_columns()
500      */
501     function sortby ($column, $action, $sortable_columns=false) {
502         if (empty($column)) return '';
503         //support multiple comma-delimited sortby args: "+hits,+pagename"
504         if (strstr($column, ',')) {
505             $result = array();
506             foreach (explode(',', $column) as $col) {
507                 if (empty($this))
508                     $result[] = WikiDB_backend::sortby($col, $action);
509                 else
510                     $result[] = $this->sortby($col, $action);
511             }
512             return join(",",$result);
513         }
514         if (substr($column,0,1) == '+') {
515             $order = '+'; $column = substr($column,1);
516         } elseif (substr($column,0,1) == '-') {
517             $order = '-'; $column = substr($column,1);
518         }
519         // default order: +pagename, -mtime, -hits
520         if (empty($order))
521             if (in_array($column,array('mtime','hits')))
522                 $order = '-';
523             else
524                 $order = '+';
525         if ($action == 'flip_order') {
526             return ($order == '+' ? '-' : '+') . $column;
527         } elseif ($action == 'init') {
528             $this->_sortby[$column] = $order;
529             return $order . $column;
530         } elseif ($action == 'check') {
531             return (!empty($this->_sortby[$column]) or 
532                     ($GLOBALS['request']->getArg('sortby') and 
533                      strstr($GLOBALS['request']->getArg('sortby'),$column)));
534         } elseif ($action == 'db') {
535             // native sort possible?
536             if (!empty($this) and !$sortable_columns)
537                 $sortable_columns = $this->sortable_columns();
538             if (in_array($column, $sortable_columns))
539                 // asc or desc: +pagename, -pagename
540                 return $column . ($order == '+' ? ' ASC' : ' DESC');
541             else 
542                 return '';
543         }
544         return '';
545     }
546
547     function sortable_columns() {
548         return array('pagename'/*,'mtime','author_id','author'*/);
549     }
550
551     // adds surrounding quotes 
552     function quote ($s) { return "'".$s."'"; }
553     // no surrounding quotes because we know it's a string
554     function qstr ($s) {  return $s; }
555
556 };
557
558 /**
559  * Iterator returned by backend methods which (possibly) return
560  * multiple records.
561  *
562  * FIXME: This might be two seperate classes: page_iter and version_iter.
563  * For the versions we have WikiDB_backend_dumb_AllRevisionsIter.
564  */
565 class WikiDB_backend_iterator
566 {
567     /**
568      * Get the next record in the iterator set.
569      *
570      * This returns a hash. The hash may contain the following keys:
571      * <dl>
572      * <dt> pagename <dt> (string) the page name
573      * <dt> version  <dt> (int) the version number
574      * <dt> pagedata <dt> (hash) page meta-data (as returned from backend::get_pagedata().)
575      * <dt> versiondata <dt> (hash) page meta-data (as returned from backend::get_versiondata().)
576      *
577      * If this is a page iterator, it must contain the 'pagename' entry --- the others
578      * are optional.
579      *
580      * If this is a version iterator, the 'pagename', 'version', <strong>and</strong> 'versiondata'
581      * entries are mandatory.  ('pagedata' is optional.)
582      */
583     function next() {
584         trigger_error("virtual", E_USER_ERROR);
585     }
586
587     function count() {
588         return count($this->_pages);
589     }
590
591     /**
592      * Release resources held by this iterator.
593      */
594     function free() {
595     }
596 };
597
598 /**
599  * search baseclass, pcre-specific. only overriden by sql backends.
600  */
601 class WikiDB_backend_search
602 {
603     function WikiDB_backend_search(&$dbh, $search) {
604         $this->_dbh = $dbh;
605         $this->_case_exact = $search->_case_exact;
606     }
607     function _quote($word) {
608         return preg_quote($word, "/");
609     }
610     //TODO: use word anchors
611     function EXACT($word) { return "^".$this->_quote($word)."$"; }
612     function STARTS_WITH($word) { return "^".$this->_quote($word); }
613     function ENDS_WITH($word) { return $this->_quote($word)."$"; }
614     function WORD($word) { return $this->_quote($word); }
615     function REGEX($word) { return $word; }
616     //TESTME
617     function _pagename_match_clause($node) {
618         $method = $node->op;
619         $word = $this->$method($node->word);
620         return "preg_match(\"/\".$word.\"/\"".($this->_case_exact ? "i":"").")";
621     }
622
623 }
624
625 // For emacs users
626 // Local Variables:
627 // mode: php
628 // tab-width: 8
629 // c-basic-offset: 4
630 // c-hanging-comment-ender-p: nil
631 // indent-tabs-mode: nil
632 // End:
633 ?>