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)
91 trigger_error("virtual", E_USER_ERROR);
95 * Update the page meta-data.
99 * Only meta-data whose keys are preset in $newdata is affected.
103 * $backend->update_pagedata($pagename, array('locked' => 1));
105 * will set the value of 'locked' to 1 for the specified page, but it
106 * will not affect the value of 'hits' (or whatever other meta-data
107 * may have been stored for the page.)
109 * To delete a particular piece of meta-data, set its value to false.
111 * $backend->update_pagedata($pagename, array('locked' => false));
114 * @param $pagename string Page name.
115 * @param $newdata hash New meta-data.
117 function update_pagedata($pagename, $newdata)
119 trigger_error("virtual", E_USER_ERROR);
123 * Get the current version number for a page.
125 * @param $pagename string Page name.
126 * @return int The latest version number for the page. Returns zero if
127 * no versions of a page exist.
129 function get_latest_version($pagename)
131 trigger_error("virtual", E_USER_ERROR);
135 * Get preceding version number.
137 * @param $pagename string Page name.
138 * @param $version int Find version before this one.
139 * @return int The version number of the version in the database which
140 * immediately preceeds $version.
142 function get_previous_version($pagename, $version)
144 trigger_error("virtual", E_USER_ERROR);
148 * Get revision meta-data and content.
150 * @param $pagename string Page name.
151 * @param $version integer Which version to get.
152 * @param $want_content boolean
153 * Indicates the caller really wants the page content. If this
154 * flag is not set, the backend is free to skip fetching of the
155 * page content (as that may be expensive). If the backend omits
156 * the content, the backend might still want to set the value of
157 * '%content' to the empty string if it knows there's no content.
159 * @return hash The version data, or false if specified version does not
162 * Some keys which might be present in the $versiondata hash are:
165 * <dd> This is a pseudo-meta-data element (since it's actually
166 * the page data, get it?) containing the page content.
167 * If the content was not fetched, this key may not be present.
169 * For description of other version meta-data see WikiDB_PageRevision::get().
170 * @see WikiDB_PageRevision::get
172 function get_versiondata($pagename, $version, $want_content = false)
174 trigger_error("virtual", E_USER_ERROR);
178 * Delete page from the database with backup possibility.
179 * This should remove all links (from the named page) from
182 * @param $pagename string Page name.
183 * i.e save_page('') and DELETE nonempty id
184 * Can be undone and is seen in RecentChanges.
186 function delete_page($pagename)
189 $user =& $GLOBALS['request']->_user;
190 $vdata = array('author' => $user->getId(),
191 'author_id' => $user->getAuthenticatedId(),
194 $this->lock(); // critical section:
195 $version = $this->get_latest_version($pagename);
196 $this->set_versiondata($pagename, $version + 1, $vdata);
197 $this->set_links($pagename, false); // links are purged.
198 // SQL needs to invalidate the non_empty id
199 if (!WIKIDB_NOCACHE_MARKUP) {
200 // need the hits, perms and LOCKED, otherwise you can reset the perm
201 // by action=remove and re-create it with default perms
202 $pagedata = $this->get_pagedata($pagename);
203 unset($pagedata['_cached_html']);
204 $this->update_pagedata($pagename, $pagedata);
210 * Delete page (and all its revisions) from the database.
213 function purge_page($pagename)
215 trigger_error("virtual", E_USER_ERROR);
219 * Delete an old revision of a page.
221 * Note that one is never allowed to delete the most recent version,
222 * but that this requirement is enforced by WikiDB not by the backend.
224 * In fact, to be safe, backends should probably allow the deletion of
225 * the most recent version.
227 * @param $pagename string Page name.
228 * @param $version integer Version to delete.
230 function delete_versiondata($pagename, $version)
232 trigger_error("virtual", E_USER_ERROR);
236 * Create a new page revision.
238 * If the given ($pagename,$version) is already in the database,
239 * this method completely overwrites any stored data for that version.
241 * @param $pagename string Page name.
242 * @param $version int New revisions content.
243 * @param $data hash New revision metadata.
245 * @see get_versiondata
247 function set_versiondata($pagename, $version, $data)
249 trigger_error("virtual", E_USER_ERROR);
253 * Update page version meta-data.
255 * If the given ($pagename,$version) is already in the database,
256 * this method only changes those meta-data values whose keys are
257 * explicity listed in $newdata.
259 * @param $pagename string Page name.
260 * @param $version int New revisions content.
261 * @param $newdata hash New revision metadata.
262 * @see set_versiondata, get_versiondata
264 function update_versiondata($pagename, $version, $newdata)
266 $data = $this->get_versiondata($pagename, $version, true);
271 foreach ($newdata as $key => $val) {
277 $this->set_versiondata($pagename, $version, $data);
281 * Set links for page.
283 * @param $pagename string Page name.
285 * @param $links array List of page(names) which page links to.
287 function set_links($pagename, $links)
289 trigger_error("virtual", E_USER_ERROR);
293 * Find pages which link to or are linked from a page.
295 * @param $pagename string Page name.
296 * @param $reversed boolean True to get backlinks.
298 * FIXME: array or iterator?
299 * @return object A WikiDB_backend_iterator.
301 function get_links($pagename, $reversed, $include_empty = false,
302 $sortby = '', $limit = '', $exclude = '')
304 //FIXME: implement simple (but slow) link finder.
305 die("FIXME get_links");
309 * Get all revisions of a page.
311 * @param $pagename string The page name.
312 * @return object A WikiDB_backend_iterator.
314 function get_all_revisions($pagename)
316 include_once 'lib/WikiDB/backend/dumb/AllRevisionsIter.php';
317 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
321 * Get all pages in the database.
323 * Pages should be returned in alphabetical order if that is
328 * @param $include_defaulted boolean
329 * If set, even pages with no content will be returned
330 * --- but still only if they have at least one revision (not
331 * counting the default revision 0) entered in the database.
333 * Normally pages whose current revision has empty content
334 * are not returned as these pages are considered to be
337 * @return object A WikiDB_backend_iterator.
339 function get_all_pages($include_defaulted, $orderby = false, $limit = '', $exclude = '')
341 trigger_error("virtual", E_USER_ERROR);
345 * Title or full text search.
347 * Pages should be returned in alphabetical order if that is
352 * @param $search object A TextSearchQuery object describing the parsed query string,
353 * with efficient methods for SQL and PCRE match.
355 * @param $fullsearch boolean If true, a full text search is performed,
356 * otherwise a title search is performed.
358 * @return object A WikiDB_backend_iterator.
360 * @see WikiDB::titleSearch
362 function text_search($search, $fulltext = false, $sortby = '',
363 $limit = '', $exclude = '')
365 // This method implements a simple linear search
366 // through all the pages in the database.
368 // It is expected that most backends will overload
369 // this method with something more efficient.
370 include_once 'lib/WikiDB/backend/dumb/TextSearchIter.php';
372 $pages = $this->get_all_pages(false, $sortby, false, $exclude);
373 return new WikiDB_backend_dumb_TextSearchIter($this, $pages, $search, $fulltext,
374 array('limit' => $limit,
375 'exclude' => $exclude));
381 * @param $pages object A TextSearchQuery object.
382 * @param $linkvalue object A TextSearchQuery object for the linkvalues
383 * (linkto, relation or backlinks or attribute values).
384 * @param $linktype string One of the 4 linktypes.
385 * @param $relation object A TextSearchQuery object or false.
386 * @param $options array Currently ignored. hash of sortby, limit, exclude.
387 * @return object A WikiDB_backend_iterator.
388 * @see WikiDB::linkSearch
390 function link_search($pages, $linkvalue, $linktype, $relation = false, $options = array())
392 include_once 'lib/WikiDB/backend/dumb/LinkSearchIter.php';
393 $pageiter = $this->text_search($pages);
394 return new WikiDB_backend_dumb_LinkSearchIter($this, $pageiter, $linkvalue, $linktype, $relation, $options);
398 * Find pages with highest hit counts.
400 * Find the pages with the highest hit counts. The pages should
401 * be returned in reverse order by hit count.
404 * @param integer $limit No more than this many pages
405 * @return object A WikiDB_backend_iterator.
407 function most_popular($limit, $sortby = '-hits')
409 // This is method fetches all pages, then
410 // sorts them by hit count.
411 // (Not very efficient.)
413 // It is expected that most backends will overload
414 // method with something more efficient.
415 include_once 'lib/WikiDB/backend/dumb/MostPopularIter.php';
416 $pages = $this->get_all_pages(false, $sortby, false);
417 return new WikiDB_backend_dumb_MostPopularIter($this, $pages, $limit);
421 * Find recent changes.
424 * @param $params hash See WikiDB::mostRecent for a description
425 * of parameters which can be included in this hash.
426 * @return object A WikiDB_backend_iterator.
427 * @see WikiDB::mostRecent
429 function most_recent($params)
431 // This method is very inefficient and searches through
432 // all pages for the most recent changes.
434 // It is expected that most backends will overload
435 // method with something more efficient.
436 include_once 'lib/WikiDB/backend/dumb/MostRecentIter.php';
437 $pages = $this->get_all_pages(true, '-mtime');
438 return new WikiDB_backend_dumb_MostRecentIter($this, $pages, $params);
441 function wanted_pages($exclude_from = '', $exclude = '', $sortby = '', $limit = '')
443 include_once 'lib/WikiDB/backend/dumb/WantedPagesIter.php';
444 $allpages = $this->get_all_pages(true, false, false, $exclude_from);
445 return new WikiDB_backend_dumb_WantedPagesIter($this, $allpages, $exclude, $sortby, $limit);
449 * Lock backend database.
451 * Calls may be nested.
453 * @param $write_lock boolean Unless this is set to false, a write lock
454 * is acquired, otherwise a read lock. If the backend doesn't support
455 * read locking, then it should make a write lock no matter which type
456 * of lock was requested.
458 * All backends <em>should</em> support write locking.
460 function lock($write_lock = true)
465 * Unlock backend database.
467 * @param $force boolean Normally, the database is not unlocked until
468 * unlock() is called as many times as lock() has been. If $force is
469 * set to true, the the database is unconditionally unlocked.
471 function unlock($force = false)
484 * Synchronize with filesystem.
486 * This should flush all unwritten data to the filesystem.
493 * Optimize the database.
500 * Check database integrity.
502 * This should check the validity of the internal structure of the database.
503 * Errors should be reported via:
505 * trigger_error("Message goes here.", E_USER_WARNING);
508 * @return boolean True iff database is in a consistent state.
510 function check($args = false)
515 * Put the database into a consistent state
516 * by reparsing and restoring all pages.
518 * This should put the database into a consistent state.
519 * (I.e. rebuild indexes, etc...)
521 * @return boolean True iff successful.
523 function rebuild($args = false)
526 $dbh = $request->getDbh();
527 $iter = $dbh->getAllPages(false);
528 while ($page = $iter->next()) {
529 $current = $page->getCurrentRevision(true);
530 $pagename = $page->getName();
531 $meta = $current->_data;
532 $version = $current->getVersion();
533 $content =& $meta['%content'];
534 $formatted = new TransformedText($page, $content, $current->getMetaData());
535 $type = $formatted->getType();
536 $meta['pagetype'] = $type->getName();
537 $links = $formatted->getWikiPageLinks(); // linkto => relation
538 $this->lock(array('version', 'page', 'recent', 'link', 'nonempty'));
539 $this->set_versiondata($pagename, $version, $meta);
540 $this->set_links($pagename, $links);
541 $this->unlock(array('version', 'page', 'recent', 'link', 'nonempty'));
545 function _parse_searchwords($search)
547 $search = strtolower(trim($search));
549 return array(array(), array());
551 $words = preg_split('/\s+/', $search);
553 foreach ($words as $key => $word) {
554 if ($word[0] == '-' && $word != '-') {
555 $word = substr($word, 1);
556 $exclude[] = preg_quote($word);
560 return array($words, $exclude);
564 * Split the given limit parameter into offset,limit. (offset is optional. default: 0)
565 * Duplicate the PageList function here to avoid loading the whole PageList.php
567 * list($offset,$count) = $this->limit($args['limit']);
569 function limit($limit)
571 if (strstr($limit, ',')) {
572 list($from, $limit) = explode(',', $limit);
573 if ((!empty($from) && !is_numeric($from)) or (!empty($limit) && !is_numeric($limit))) {
574 return $this->error(_("Illegal 'limit' argument: must be numeric"));
576 return array($from, $limit);
578 if (!empty($limit) && !is_numeric($limit)) {
579 return $this->error(_("Illegal 'limit' argument: must be numeric"));
581 return array(0, $limit);
586 * Handle sortby requests for the DB iterator and table header links.
587 * Prefix the column with + or - like "+pagename","-mtime", ...
588 * supported actions: 'flip_order' "mtime" => "+mtime" => "-mtime" ...
589 * 'db' "-pagename" => "pagename DESC"
590 * In PageList all columns are sortable. (patch by DanFr)
591 * Here with the backend only some, the rest is delayed to PageList.
592 * (some kind of DumbIter)
593 * Duplicate the PageList function here to avoid loading the whole
594 * PageList.php, and it forces the backend specific sortable_columns()
596 function sortby($column, $action, $sortable_columns = false)
598 if (empty($column)) return '';
599 //support multiple comma-delimited sortby args: "+hits,+pagename"
600 if (strstr($column, ',')) {
602 foreach (explode(',', $column) as $col) {
604 $result[] = WikiDB_backend::sortby($col, $action);
606 $result[] = $this->sortby($col, $action);
608 return join(",", $result);
610 if (substr($column, 0, 1) == '+') {
612 $column = substr($column, 1);
613 } elseif (substr($column, 0, 1) == '-') {
615 $column = substr($column, 1);
617 // default order: +pagename, -mtime, -hits
619 if (in_array($column, array('mtime', 'hits')))
623 if ($action == 'flip_order') {
624 return ($order == '+' ? '-' : '+') . $column;
625 } elseif ($action == 'init') {
626 $this->_sortby[$column] = $order;
627 return $order . $column;
628 } elseif ($action == 'check') {
629 return (!empty($this->_sortby[$column]) or
630 ($GLOBALS['request']->getArg('sortby') and
631 strstr($GLOBALS['request']->getArg('sortby'), $column)));
632 } elseif ($action == 'db') {
633 // native sort possible?
634 if (!empty($this) and !$sortable_columns)
635 $sortable_columns = $this->sortable_columns();
636 if (in_array($column, $sortable_columns))
637 // asc or desc: +pagename, -pagename
638 return $column . ($order == '+' ? ' ASC' : ' DESC');
645 function sortable_columns()
647 return array('pagename' /*,'mtime','author_id','author'*/);
650 // adds surrounding quotes
653 return "'" . $s . "'";
656 // no surrounding quotes because we know it's a string
664 return in_array(DATABASE_TYPE, array('SQL', 'ADODB', 'PDO'));
667 function backendType()
669 return DATABASE_TYPE;
672 function write_accesslog(&$entry)
675 if (!$this->isSQL()) return;
677 $log_tbl = $entry->_accesslog->logtable;
678 // duration problem: sprintf "%f" might use comma e.g. "100,201" in european locales
679 $dbh->query("INSERT INTO $log_tbl"
680 . " (time_stamp,remote_host,remote_user,request_method,request_line,request_args,"
681 . "request_uri,request_time,status,bytes_sent,referer,agent,request_duration)"
682 . " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
684 // Problem: date formats are backend specific. Either use unixtime as %d (long),
685 // or the native timestamp format.
689 $entry->request_method,
691 $entry->request_args,
693 $entry->_ncsa_time($entry->time),
703 * Iterator returned by backend methods which (possibly) return
706 * FIXME: This might be two separate classes: page_iter and version_iter.
707 * For the versions we have WikiDB_backend_dumb_AllRevisionsIter.
709 class WikiDB_backend_iterator
712 * Get the next record in the iterator set.
714 * This returns a hash. The hash may contain the following keys:
716 * <dt> pagename <dt> (string) the page name or linked page name on link iterators
717 * <dt> version <dt> (int) the version number
718 * <dt> pagedata <dt> (hash) page meta-data (as returned from backend::get_pagedata().)
719 * <dt> versiondata <dt> (hash) page meta-data (as returned from backend::get_versiondata().)
720 * <dt> linkrelation <dt> (string) the page naming the relation (e.g. isa:=page <=> isa)
722 * If this is a page iterator, it must contain the 'pagename' entry --- the others
725 * If this is a version iterator, the 'pagename', 'version', <strong>and</strong> 'versiondata'
726 * entries are mandatory. ('pagedata' is optional.)
728 * If this is a link iterator, the 'pagename' is mandatory, 'linkrelation' is optional.
732 trigger_error("virtual", E_USER_ERROR);
737 if (!empty($this->_pages))
738 return count($this->_pages);
745 if (!empty($this->_pages)) {
746 reset($this->_pages);
747 return $this->_pages;
750 while ($page = $this->next())
757 * limit - if empty the pagelist iterator will do nothing.
758 * Some backends limit the result set itself (dba, file, flatfile),
759 * Some SQL based leave it to WikiDB/PageList - deferred filtering in the iterator.
763 return empty($this->_options['limit']) ? 0 : $this->_options['limit'];
767 * Release resources held by this iterator.
775 * search baseclass, pcre-specific
777 class WikiDB_backend_search
779 function WikiDB_backend_search($search, &$dbh)
782 $this->_case_exact = $search->_case_exact;
783 $this->_stoplist =& $search->_stoplist;
784 $this->stoplisted = array();
787 function _quote($word)
789 return preg_quote($word, "/");
792 //TODO: use word anchors
793 function EXACT($word)
795 return "^" . $this->_quote($word) . "$";
798 function STARTS_WITH($word)
800 return "^" . $this->_quote($word);
803 function ENDS_WITH($word)
805 return $this->_quote($word) . "$";
810 return $this->_quote($word);
813 function REGEX($word)
819 function _pagename_match_clause($node)
822 $word = $this->$method($node->word);
823 return "preg_match(\"/\".$word.\"/\"" . ($this->_case_exact ? "i" : "") . ")";
826 /* Eliminate stoplist words.
827 * Keep a list of Stoplisted words to inform the poor user.
829 function isStoplisted($node)
831 // check only on WORD or EXACT fulltext search
832 if ($node->op != 'WORD' and $node->op != 'EXACT')
834 if (preg_match("/^" . $this->_stoplist . "$/i", $node->word)) {
835 array_push($this->stoplisted, $node->word);
841 function getStoplisted($word)
843 return $this->stoplisted;
848 * search baseclass, sql-specific
850 class WikiDB_backend_search_sql extends WikiDB_backend_search
852 function _pagename_match_clause($node)
854 // word already quoted by TextSearchQuery_node_word::_sql_quote()
855 $word = $node->sql();
856 if ($word == '%') // ALL shortcut
859 return ($this->_case_exact
860 ? "pagename LIKE '$word'"
861 : "LOWER(pagename) LIKE '$word'");
864 function _fulltext_match_clause($node)
866 // force word-style %word% for fulltext search
867 $word = '%' . $node->_sql_quote($node->word) . '%';
868 // eliminate stoplist words
869 if ($this->isStoplisted($node))
870 return "1=1"; // and (pagename or 1) => and 1
872 return $this->_pagename_match_clause($node)
873 // probably convert this MATCH AGAINST or SUBSTR/POSITION without wildcards
874 . ($this->_case_exact ? " OR content LIKE '$word'"
875 : " OR LOWER(content) LIKE '$word'");
883 // c-hanging-comment-ender-p: nil
884 // indent-tabs-mode: nil