4 * Copyright 2004-2010 Reini Urban
6 * This file is part of PhpWiki.
8 * PhpWiki is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * PhpWiki is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 maintained by WikiPage
28 //:deleted (*) (Set if latest content is empty.)
36 %content (?should this be here?)
37 _supplanted : Time version ceased to be the current version
39 mtime (*) : Time of version edit.
42 author : nominal author
43 author_id : authenticated author
52 (types are scalars: strings, ints, bools)
56 * A WikiDB_backend handles the storage and retrieval of data for a WikiDB.
58 * It does not have to be this way, of course, but the standard WikiDB uses
59 * a WikiDB_backend. (Other WikiDB's could be written which use some other
60 * method to access their underlying data store.)
62 * The interface outlined here seems to work well with both RDBM based
63 * and flat DBM/hash based methods of data storage.
65 * Though it contains some default implementation of certain methods,
66 * this is an abstract base class. It is expected that most efficient
67 * backends will override nearly all the methods in this class.
75 * Get page meta-data from database.
77 * @param $pagename string Page name.
79 * Returns a hash containing the page meta-data.
80 * Returns an empty array if there is no meta-data for the requested page.
81 * Keys which might be present in the hash are:
83 * <dt> locked <dd> If the page is locked.
84 * <dt> hits <dd> The page hit count.
85 * <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
86 * don't think we need this...)
89 function get_pagedata($pagename) {
90 trigger_error("virtual", E_USER_ERROR);
94 * Update the page meta-data.
98 * Only meta-data whose keys are preset in $newdata is affected.
102 * $backend->update_pagedata($pagename, array('locked' => 1));
104 * will set the value of 'locked' to 1 for the specified page, but it
105 * will not affect the value of 'hits' (or whatever other meta-data
106 * may have been stored for the page.)
108 * To delete a particular piece of meta-data, set its value to false.
110 * $backend->update_pagedata($pagename, array('locked' => false));
113 * @param $pagename string Page name.
114 * @param $newdata hash New meta-data.
116 function update_pagedata($pagename, $newdata) {
117 trigger_error("virtual", E_USER_ERROR);
121 * Get the current version number for a page.
123 * @param $pagename string Page name.
124 * @return int The latest version number for the page. Returns zero if
125 * no versions of a page exist.
127 function get_latest_version($pagename) {
128 trigger_error("virtual", E_USER_ERROR);
132 * Get preceding version number.
134 * @param $pagename string Page name.
135 * @param $version int Find version before this one.
136 * @return int The version number of the version in the database which
137 * immediately preceeds $version.
139 function get_previous_version($pagename, $version) {
140 trigger_error("virtual", E_USER_ERROR);
144 * Get revision meta-data and content.
146 * @param $pagename string Page name.
147 * @param $version integer Which version to get.
148 * @param $want_content boolean
149 * Indicates the caller really wants the page content. If this
150 * flag is not set, the backend is free to skip fetching of the
151 * page content (as that may be expensive). If the backend omits
152 * the content, the backend might still want to set the value of
153 * '%content' to the empty string if it knows there's no content.
155 * @return hash The version data, or false if specified version does not
158 * Some keys which might be present in the $versiondata hash are:
161 * <dd> This is a pseudo-meta-data element (since it's actually
162 * the page data, get it?) containing the page content.
163 * If the content was not fetched, this key may not be present.
165 * For description of other version meta-data see WikiDB_PageRevision::get().
166 * @see WikiDB_PageRevision::get
168 function get_versiondata($pagename, $version, $want_content = false) {
169 trigger_error("virtual", E_USER_ERROR);
173 * Delete page from the database with backup possibility.
174 * This should remove all links (from the named page) from
177 * @param $pagename string Page name.
178 * i.e save_page('') and DELETE nonempty id
179 * Can be undone and is seen in RecentChanges.
181 function delete_page($pagename) {
183 $user =& $GLOBALS['request']->_user;
184 $vdata = array('author' => $user->getId(),
185 'author_id' => $user->getAuthenticatedId(),
188 $this->lock(); // critical section:
189 $version = $this->get_latest_version($pagename);
190 $this->set_versiondata($pagename, $version+1, $vdata);
191 $this->set_links($pagename, false); // links are purged.
192 // SQL needs to invalidate the non_empty id
193 if (! WIKIDB_NOCACHE_MARKUP) {
194 // need the hits, perms and LOCKED, otherwise you can reset the perm
195 // by action=remove and re-create it with default perms
196 $pagedata = $this->get_pagedata($pagename);
197 unset($pagedata['_cached_html']);
198 $this->update_pagedata($pagename, $pagedata);
204 * Delete page (and all its revisions) from the database.
207 function purge_page($pagename) {
208 trigger_error("virtual", E_USER_ERROR);
212 * Delete an old revision of a page.
214 * Note that one is never allowed to delete the most recent version,
215 * but that this requirement is enforced by WikiDB not by the backend.
217 * In fact, to be safe, backends should probably allow the deletion of
218 * the most recent version.
220 * @param $pagename string Page name.
221 * @param $version integer Version to delete.
223 function delete_versiondata($pagename, $version) {
224 trigger_error("virtual", E_USER_ERROR);
228 * Create a new page revision.
230 * If the given ($pagename,$version) is already in the database,
231 * this method completely overwrites any stored data for that version.
233 * @param $pagename string Page name.
234 * @param $version int New revisions content.
235 * @param $data hash New revision metadata.
237 * @see get_versiondata
239 function set_versiondata($pagename, $version, $data) {
240 trigger_error("virtual", E_USER_ERROR);
244 * Update page version meta-data.
246 * If the given ($pagename,$version) is already in the database,
247 * this method only changes those meta-data values whose keys are
248 * explicity listed in $newdata.
250 * @param $pagename string Page name.
251 * @param $version int New revisions content.
252 * @param $newdata hash New revision metadata.
253 * @see set_versiondata, get_versiondata
255 function update_versiondata($pagename, $version, $newdata) {
256 $data = $this->get_versiondata($pagename, $version, true);
261 foreach ($newdata as $key => $val) {
267 $this->set_versiondata($pagename, $version, $data);
271 * Set links for page.
273 * @param $pagename string Page name.
275 * @param $links array List of page(names) which page links to.
277 function set_links($pagename, $links) {
278 trigger_error("virtual", E_USER_ERROR);
282 * Find pages which link to or are linked from a page.
284 * @param $pagename string Page name.
285 * @param $reversed boolean True to get backlinks.
287 * FIXME: array or iterator?
288 * @return object A WikiDB_backend_iterator.
290 function get_links($pagename, $reversed, $include_empty=false,
291 $sortby='', $limit='', $exclude='') {
292 //FIXME: implement simple (but slow) link finder.
293 die("FIXME get_links");
297 * Get all revisions of a page.
299 * @param $pagename string The page name.
300 * @return object A WikiDB_backend_iterator.
302 function get_all_revisions($pagename) {
303 include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
304 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
308 * Get all pages in the database.
310 * Pages should be returned in alphabetical order if that is
315 * @param $include_defaulted boolean
316 * If set, even pages with no content will be returned
317 * --- but still only if they have at least one revision (not
318 * counting the default revision 0) entered in the database.
320 * Normally pages whose current revision has empty content
321 * are not returned as these pages are considered to be
324 * @return object A WikiDB_backend_iterator.
326 function get_all_pages($include_defaulted, $orderby=false, $limit='', $exclude='') {
327 trigger_error("virtual", E_USER_ERROR);
331 * Title or full text search.
333 * Pages should be returned in alphabetical order if that is
338 * @param $search object A TextSearchQuery object describing the parsed query string,
339 * with efficient methods for SQL and PCRE match.
341 * @param $fullsearch boolean If true, a full text search is performed,
342 * otherwise a title search is performed.
344 * @return object A WikiDB_backend_iterator.
346 * @see WikiDB::titleSearch
348 function text_search($search, $fulltext=false, $sortby='',
349 $limit='', $exclude='')
351 // This method implements a simple linear search
352 // through all the pages in the database.
354 // It is expected that most backends will overload
355 // this method with something more efficient.
356 include_once('lib/WikiDB/backend/dumb/TextSearchIter.php');
358 $pages = $this->get_all_pages(false, $sortby, false, $exclude);
359 return new WikiDB_backend_dumb_TextSearchIter($this, $pages, $search, $fulltext,
360 array('limit' => $limit,
361 'exclude' => $exclude));
367 * @param $pages object A TextSearchQuery object.
368 * @param $linkvalue object A TextSearchQuery object for the linkvalues
369 * (linkto, relation or backlinks or attribute values).
370 * @param $linktype string One of the 4 linktypes.
371 * @param $relation object A TextSearchQuery object or false.
372 * @param $options array Currently ignored. hash of sortby, limit, exclude.
373 * @return object A WikiDB_backend_iterator.
374 * @see WikiDB::linkSearch
376 function link_search( $pages, $linkvalue, $linktype, $relation=false, $options=array() ) {
377 include_once('lib/WikiDB/backend/dumb/LinkSearchIter.php');
378 $pageiter = $this->text_search($pages);
379 return new WikiDB_backend_dumb_LinkSearchIter($this, $pageiter, $linkvalue, $linktype, $relation, $options);
383 * Find pages with highest hit counts.
385 * Find the pages with the highest hit counts. The pages should
386 * be returned in reverse order by hit count.
389 * @param integer $limit No more than this many pages
390 * @return object A WikiDB_backend_iterator.
392 function most_popular($limit, $sortby='-hits') {
393 // This is method fetches all pages, then
394 // sorts them by hit count.
395 // (Not very efficient.)
397 // It is expected that most backends will overload
398 // method with something more efficient.
399 include_once('lib/WikiDB/backend/dumb/MostPopularIter.php');
400 $pages = $this->get_all_pages(false, $sortby, false);
401 return new WikiDB_backend_dumb_MostPopularIter($this, $pages, $limit);
405 * Find recent changes.
408 * @param $params hash See WikiDB::mostRecent for a description
409 * of parameters which can be included in this hash.
410 * @return object A WikiDB_backend_iterator.
411 * @see WikiDB::mostRecent
413 function most_recent($params) {
414 // This method is very inefficient and searches through
415 // all pages for the most recent changes.
417 // It is expected that most backends will overload
418 // method with something more efficient.
419 include_once('lib/WikiDB/backend/dumb/MostRecentIter.php');
420 $pages = $this->get_all_pages(true, '-mtime');
421 return new WikiDB_backend_dumb_MostRecentIter($this, $pages, $params);
424 function wanted_pages($exclude_from='', $exclude='', $sortby='', $limit='') {
425 include_once('lib/WikiDB/backend/dumb/WantedPagesIter.php');
426 $allpages = $this->get_all_pages(true,false,false,$exclude_from);
427 return new WikiDB_backend_dumb_WantedPagesIter($this, $allpages, $exclude, $sortby, $limit);
431 * Lock backend database.
433 * Calls may be nested.
435 * @param $write_lock boolean Unless this is set to false, a write lock
436 * is acquired, otherwise a read lock. If the backend doesn't support
437 * read locking, then it should make a write lock no matter which type
438 * of lock was requested.
440 * All backends <em>should</em> support write locking.
442 function lock($write_lock = true) {
446 * Unlock backend database.
448 * @param $force boolean Normally, the database is not unlocked until
449 * unlock() is called as many times as lock() has been. If $force is
450 * set to true, the the database is unconditionally unlocked.
452 function unlock($force = false) {
463 * Synchronize with filesystem.
465 * This should flush all unwritten data to the filesystem.
471 * Optimize the database.
473 function optimize() {
477 * Check database integrity.
479 * This should check the validity of the internal structure of the database.
480 * Errors should be reported via:
482 * trigger_error("Message goes here.", E_USER_WARNING);
485 * @return boolean True iff database is in a consistent state.
487 function check($args=false) {
491 * Put the database into a consistent state
492 * by reparsing and restoring all pages.
494 * This should put the database into a consistent state.
495 * (I.e. rebuild indexes, etc...)
497 * @return boolean True iff successful.
499 function rebuild($args=false) {
501 $dbh = $request->getDbh();
502 $iter = $dbh->getAllPages(false);
503 while ($page = $iter->next()) {
504 $current = $page->getCurrentRevision(true);
505 $pagename = $page->getName();
506 $meta = $current->_data;
507 $version = $current->getVersion();
508 $content =& $meta['%content'];
509 $formatted = new TransformedText($page, $content, $current->getMetaData());
510 $type = $formatted->getType();
511 $meta['pagetype'] = $type->getName();
512 $links = $formatted->getWikiPageLinks(); // linkto => relation
513 $this->lock(array('version','page','recent','link','nonempty'));
514 $this->set_versiondata($pagename, $version, $meta);
515 $this->set_links($pagename, $links);
516 $this->unlock(array('version','page','recent','link','nonempty'));
520 function _parse_searchwords($search) {
521 $search = strtolower(trim($search));
523 return array(array(),array());
525 $words = preg_split('/\s+/', $search);
527 foreach ($words as $key => $word) {
528 if ($word[0] == '-' && $word != '-') {
529 $word = substr($word, 1);
530 $exclude[] = preg_quote($word);
534 return array($words, $exclude);
538 * Split the given limit parameter into offset,limit. (offset is optional. default: 0)
539 * Duplicate the PageList function here to avoid loading the whole PageList.php
541 * list($offset,$count) = $this->limit($args['limit']);
543 function limit($limit) {
544 if (strstr($limit, ',')) {
545 list($from, $limit) = explode(',', $limit);
546 if ((!empty($from) && !is_numeric($from)) or (!empty($limit) && !is_numeric($limit))) {
547 return $this->error(_("Illegal 'limit' argument: must be numeric"));
549 return array($from, $limit);
552 if (!empty($limit) && !is_numeric($limit)) {
553 return $this->error(_("Illegal 'limit' argument: must be numeric"));
555 return array(0, $limit);
560 * Handle sortby requests for the DB iterator and table header links.
561 * Prefix the column with + or - like "+pagename","-mtime", ...
562 * supported actions: 'flip_order' "mtime" => "+mtime" => "-mtime" ...
563 * 'db' "-pagename" => "pagename DESC"
564 * In PageList all columns are sortable. (patch by DanFr)
565 * Here with the backend only some, the rest is delayed to PageList.
566 * (some kind of DumbIter)
567 * Duplicate the PageList function here to avoid loading the whole
568 * PageList.php, and it forces the backend specific sortable_columns()
570 function sortby ($column, $action, $sortable_columns=false) {
571 if (empty($column)) return '';
572 //support multiple comma-delimited sortby args: "+hits,+pagename"
573 if (strstr($column, ',')) {
575 foreach (explode(',', $column) as $col) {
577 $result[] = WikiDB_backend::sortby($col, $action);
579 $result[] = $this->sortby($col, $action);
581 return join(",",$result);
583 if (substr($column,0,1) == '+') {
584 $order = '+'; $column = substr($column,1);
585 } elseif (substr($column,0,1) == '-') {
586 $order = '-'; $column = substr($column,1);
588 // default order: +pagename, -mtime, -hits
590 if (in_array($column,array('mtime','hits')))
594 if ($action == 'flip_order') {
595 return ($order == '+' ? '-' : '+') . $column;
596 } elseif ($action == 'init') {
597 $this->_sortby[$column] = $order;
598 return $order . $column;
599 } elseif ($action == 'check') {
600 return (!empty($this->_sortby[$column]) or
601 ($GLOBALS['request']->getArg('sortby') and
602 strstr($GLOBALS['request']->getArg('sortby'),$column)));
603 } elseif ($action == 'db') {
604 // native sort possible?
605 if (!empty($this) and !$sortable_columns)
606 $sortable_columns = $this->sortable_columns();
607 if (in_array($column, $sortable_columns))
608 // asc or desc: +pagename, -pagename
609 return $column . ($order == '+' ? ' ASC' : ' DESC');
616 function sortable_columns() {
617 return array('pagename'/*,'mtime','author_id','author'*/);
620 // adds surrounding quotes
621 function quote ($s) { return "'".$s."'"; }
622 // no surrounding quotes because we know it's a string
623 function qstr ($s) { return $s; }
626 return in_array(DATABASE_TYPE, array('SQL','ADODB','PDO'));
629 function backendType() {
630 return DATABASE_TYPE;
633 function write_accesslog(&$entry) {
635 if (!$this->isSQL()) return;
637 $log_tbl = $entry->_accesslog->logtable;
638 // duration problem: sprintf "%f" might use comma e.g. "100,201" in european locales
639 $dbh->query("INSERT INTO $log_tbl"
640 . " (time_stamp,remote_host,remote_user,request_method,request_line,request_args,"
641 . "request_uri,request_time,status,bytes_sent,referer,agent,request_duration)"
642 . " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
644 // Problem: date formats are backend specific. Either use unixtime as %d (long),
645 // or the native timestamp format.
649 $entry->request_method,
651 $entry->request_args,
653 $entry->_ncsa_time($entry->time),
663 * Iterator returned by backend methods which (possibly) return
666 * FIXME: This might be two seperate classes: page_iter and version_iter.
667 * For the versions we have WikiDB_backend_dumb_AllRevisionsIter.
669 class WikiDB_backend_iterator
672 * Get the next record in the iterator set.
674 * This returns a hash. The hash may contain the following keys:
676 * <dt> pagename <dt> (string) the page name or linked page name on link iterators
677 * <dt> version <dt> (int) the version number
678 * <dt> pagedata <dt> (hash) page meta-data (as returned from backend::get_pagedata().)
679 * <dt> versiondata <dt> (hash) page meta-data (as returned from backend::get_versiondata().)
680 * <dt> linkrelation <dt> (string) the page naming the relation (e.g. isa:=page <=> isa)
682 * If this is a page iterator, it must contain the 'pagename' entry --- the others
685 * If this is a version iterator, the 'pagename', 'version', <strong>and</strong> 'versiondata'
686 * entries are mandatory. ('pagedata' is optional.)
688 * If this is a link iterator, the 'pagename' is mandatory, 'linkrelation' is optional.
691 trigger_error("virtual", E_USER_ERROR);
695 if (!empty($this->_pages))
696 return count($this->_pages);
702 if (!empty($this->_pages)) {
703 reset($this->_pages);
704 return $this->_pages;
707 while ($page = $this->next())
714 * limit - if empty the pagelist iterator will do nothing.
715 * Some backends limit the result set itself (dba, file, flatfile),
716 * Some SQL based leave it to WikiDB/PageList - deferred filtering in the iterator.
719 return empty($this->_options['limit']) ? 0 : $this->_options['limit'];
723 * Release resources held by this iterator.
730 * search baseclass, pcre-specific
732 class WikiDB_backend_search
734 function WikiDB_backend_search($search, &$dbh) {
736 $this->_case_exact = $search->_case_exact;
737 $this->_stoplist =& $search->_stoplist;
738 $this->stoplisted = array();
740 function _quote($word) {
741 return preg_quote($word, "/");
743 //TODO: use word anchors
744 function EXACT($word) { return "^".$this->_quote($word)."$"; }
745 function STARTS_WITH($word) { return "^".$this->_quote($word); }
746 function ENDS_WITH($word) { return $this->_quote($word)."$"; }
747 function WORD($word) { return $this->_quote($word); }
748 function REGEX($word) { return $word; }
750 function _pagename_match_clause($node) {
752 $word = $this->$method($node->word);
753 return "preg_match(\"/\".$word.\"/\"".($this->_case_exact ? "i":"").")";
755 /* Eliminate stoplist words.
756 * Keep a list of Stoplisted words to inform the poor user.
758 function isStoplisted ($node) {
759 // check only on WORD or EXACT fulltext search
760 if ($node->op != 'WORD' and $node->op != 'EXACT')
762 if (preg_match("/^".$this->_stoplist."$/i", $node->word)) {
763 array_push($this->stoplisted, $node->word);
768 function getStoplisted($word) {
769 return $this->stoplisted;
774 * search baseclass, sql-specific
776 class WikiDB_backend_search_sql extends WikiDB_backend_search
778 function _pagename_match_clause($node) {
779 // word already quoted by TextSearchQuery_node_word::_sql_quote()
780 $word = $node->sql();
781 if ($word == '%') // ALL shortcut
784 return ($this->_case_exact
785 ? "pagename LIKE '$word'"
786 : "LOWER(pagename) LIKE '$word'");
788 function _fulltext_match_clause($node) {
789 // force word-style %word% for fulltext search
790 $word = '%' . $node->_sql_quote($node->word) . '%';
791 // eliminate stoplist words
792 if ($this->isStoplisted($node))
793 return "1=1"; // and (pagename or 1) => and 1
795 return $this->_pagename_match_clause($node)
796 // probably convert this MATCH AGAINST or SUBSTR/POSITION without wildcards
797 . ($this->_case_exact ? " OR content LIKE '$word'"
798 : " OR LOWER(content) LIKE '$word'");
806 // c-hanging-comment-ender-p: nil
807 // indent-tabs-mode: nil