2 rcs_id('$Id: file.php,v 1.13 2004-07-08 15:23:59 rurban Exp $');
5 Copyright 1999, 2000, 2001, 2002, 2003 $ThePhpWikiProgrammingTeam
7 This file is part of PhpWiki.
9 PhpWiki is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 PhpWiki is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with PhpWiki; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27 * Backend for handling file storage.
29 * Author: Jochen Kalmbach, Jochen@kalmbachnet.de
34 * - Implement "optimize" / "sync" / "check" / "rebuild"
35 * - Optimize "get_previous_version"
36 * - Optimize "get_links" (reversed = true)
37 * - Optimize "get_all_revisions"
38 * - Optimize "most_popular" (separate file for "hitcount",
39 * which contains all pages)
40 * - Optimize "most_recent"
41 * - What should be done in "lock"/"unlock"/"close" ?
42 * - "WikiDB_backend_file_iter": Do I need to return 'version' and 'versiondata' ?
46 require_once('lib/WikiDB/backend.php');
47 require_once('lib/ErrorManager.php');
49 class WikiDB_backend_file
50 extends WikiDB_backend
55 var $_page_data; // temporarily stores the pagedata (via _loadPageData)
56 var $_page_version_data; // temporarily stores the versiondata (via _loadVersionData)
57 var $_latest_versions; // temporarily stores the latest version-numbers (for every pagename) (via _loadLatestVersions)
60 function WikiDB_backend_file( $dbparam )
62 $this->data_dir = $dbparam['directory'];
63 if (file_exists($this->data_dir) and is_file($this->data_dir))
64 unlink($this->data_dir);
65 if (is_dir($this->data_dir) == false) {
66 mkdir($this->data_dir, 0755);
70 = array('ver_data' => $this->data_dir.'/'.'ver_data',
71 'page_data' => $this->data_dir.'/'.'page_data',
72 'latest_ver' => $this->data_dir.'/'.'latest_ver',
73 'links' => $this->data_dir.'/'.'links' );
75 foreach ($this->_dir_names as $key => $val) {
76 if (file_exists($val) and is_file($val))
78 if (is_dir($val) == false)
82 $this->_page_data = NULL;
83 $this->_page_version_data = NULL;
84 $this->_latest_versions = NULL;
89 // *********************************************************************
90 // common file load / save functions:
91 function _pagename2filename($type, $pagename, $version) {
93 return $this->_dir_names[$type].'/'.urlencode($pagename);
95 return $this->_dir_names[$type].'/'.urlencode($pagename).'--'.$version;
98 function _loadPage($type, $pagename, $version, $set_pagename = true) {
99 $filename = $this->_pagename2filename($type, $pagename, $version);
100 if (!file_exists($filename)) return NULL;
101 if (!filesize($filename)) return array();
102 if ($fd = @fopen($filename, "rb")) {
103 $locked = flock($fd, 1); # Read lock
105 ExitWiki("Timeout while obtaining lock. Please try again");
107 if ($data = fread($fd, filesize($filename))) {
108 $pd = unserialize($data);
109 if ($set_pagename == true)
110 $pd['pagename'] = $pagename;
112 $pd['version'] = $version;
114 ExitWiki(sprintf(gettext("'%s': corrupt file"),
115 htmlspecialchars($filename)));
124 function _savePage($type, $pagename, $version, $data) {
125 $filename = $this->_pagename2filename($type, $pagename, $version);
126 if($fd = fopen($filename, 'a+b')) {
127 $locked = flock($fd,2); #Exclusive blocking lock
129 ExitWiki("Timeout while obtaining lock. Please try again");
134 $pagedata = serialize($data);
135 fwrite($fd, $pagedata);
138 ExitWiki("Error while writing page '$pagename'");
142 function _removePage($type, $pagename, $version) {
143 $filename = $this->_pagename2filename($type, $pagename, $version);
144 if (!file_exists($filename)) return NULL;
145 $f = @unlink($filename);
147 trigger_error("delete file failed: ".$filename." ver: ".$version, E_USER_WARNING);
151 // *********************************************************************
154 // *********************************************************************
155 // Load/Save Version-Data
156 function _loadVersionData($pagename, $version) {
157 if ($this->_page_version_data != NULL) {
158 if ( ($this->_page_version_data['pagename'] == $pagename) &&
159 ($this->_page_version_data['version'] == $version) ) {
160 return $this->_page_version_data;
163 $vd = $this->_loadPage('ver_data', $pagename, $version);
165 $this->_page_version_data = $vd;
166 if ( ($this->_page_version_data['pagename'] == $pagename) &&
167 ($this->_page_version_data['version'] == $version) ) {
168 return $this->_page_version_data;
174 function _saveVersionData($pagename, $version, $data) {
175 $this->_savePage('ver_data', $pagename, $version, $data);
177 // check if this is a newer version:
178 if ($this->_getLatestVersion($pagename) < $version) {
179 // write new latest-version-info
180 $this->_setLatestVersion($pagename, $version);
185 // *********************************************************************
186 // Load/Save Page-Data
187 function _loadPageData($pagename) {
188 if ($this->_page_data != NULL) {
189 if ($this->_page_data['pagename'] == $pagename) {
190 return $this->_page_data;
193 $pd = $this->_loadPage('page_data', $pagename, 0);
195 $this->_page_data = $pd;
196 if ($this->_page_data != NULL) {
197 if ($this->_page_data['pagename'] == $pagename) {
198 return $this->_page_data;
201 return array(); // no values found
204 function _savePageData($pagename, $data) {
205 $this->_savePage('page_data', $pagename, 0, $data);
208 // *********************************************************************
209 // Load/Save Latest-Version
210 function _saveLatestVersions() {
211 $data = $this->_latest_versions;
214 $this->_savePage('latest_ver', 'latest_versions', 0, $data);
217 function _setLatestVersion($pagename, $version) {
218 // make sure the page version list is loaded:
219 $this->_getLatestVersion($pagename);
221 $this->_getLatestVersion($pagename);
222 $this->_latest_versions[$pagename] = $version;
225 // Remove this page from the Latest-Version-List:
226 unset($this->_latest_versions[$pagename]);
228 $this->_saveLatestVersions();
231 function _loadLatestVersions() {
232 if ($this->_latest_versions != NULL)
235 $pd = $this->_loadPage('latest_ver', 'latest_versions', 0, false);
237 $this->_latest_versions = $pd;
239 $this->_latest_versions = array(); // empty array
242 function _getLatestVersion($pagename) {
243 $this->_loadLatestVersions();
244 if (array_key_exists($pagename, $this->_latest_versions) == false)
245 return 0; // do version exists
246 return $this->_latest_versions[$pagename];
250 // *********************************************************************
251 // Load/Save Page-Links
252 function _loadPageLinks($pagename) {
253 $pd = $this->_loadPage('links', $pagename, 0, false);
256 return array(); // no values found
259 function _savePageLinks($pagename, $links) {
260 $this->_savePage('links', $pagename, 0, $links);
266 * Get page meta-data from database.
268 * @param $pagename string Page name.
270 * Returns a hash containing the page meta-data.
271 * Returns an empty array if there is no meta-data for the requested page.
272 * Keys which might be present in the hash are:
274 * <dt> locked <dd> If the page is locked.
275 * <dt> hits <dd> The page hit count.
276 * <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
277 * don't think we need this...)
280 function get_pagedata($pagename) {
281 return $this->_loadPageData($pagename);
285 * Update the page meta-data.
287 * Set page meta-data.
289 * Only meta-data whose keys are preset in $newdata is affected.
293 * $backend->update_pagedata($pagename, array('locked' => 1));
295 * will set the value of 'locked' to 1 for the specified page, but it
296 * will not affect the value of 'hits' (or whatever other meta-data
297 * may have been stored for the page.)
299 * To delete a particular piece of meta-data, set it's value to false.
301 * $backend->update_pagedata($pagename, array('locked' => false));
304 * @param $pagename string Page name.
305 * @param $newdata hash New meta-data.
308 * This will create a new page if page being requested does not
311 function update_pagedata($pagename, $newdata) {
312 $data = $this->get_pagedata($pagename);
313 if (count($data) == 0) {
314 $this->_savePageData($pagename, $newdata); // create a new pagedata-file
318 foreach ($newdata as $key => $val) {
324 $this->_savePageData($pagename, $data); // write new pagedata-file
329 * Get the current version number for a page.
331 * @param $pagename string Page name.
332 * @return int The latest version number for the page. Returns zero if
333 * no versions of a page exist.
335 function get_latest_version($pagename) {
336 return $this->_getLatestVersion($pagename);
340 * Get preceding version number.
342 * @param $pagename string Page name.
343 * @param $version int Find version before this one.
344 * @return int The version number of the version in the database which
345 * immediately preceeds $version.
347 * FIXED: Check if this version really exists!
349 function get_previous_version($pagename, $version) {
350 $prev = ($version > 0 ? $version - 1 : 0);
351 while ($prev and !file_exists($this->_pagename2filename('ver_data', $pagename, $prev))) {
358 * Get revision meta-data and content.
360 * @param $pagename string Page name.
361 * @param $version integer Which version to get.
362 * @param $want_content boolean
363 * Indicates the caller really wants the page content. If this
364 * flag is not set, the backend is free to skip fetching of the
365 * page content (as that may be expensive). If the backend omits
366 * the content, the backend might still want to set the value of
367 * '%content' to the empty string if it knows there's no content.
369 * @return hash The version data, or false if specified version does not
372 * Some keys which might be present in the $versiondata hash are:
375 * <dd> This is a pseudo-meta-data element (since it's actually
376 * the page data, get it?) containing the page content.
377 * If the content was not fetched, this key may not be present.
379 * For description of other version meta-data see WikiDB_PageRevision::get().
380 * @see WikiDB_PageRevision::get
382 function get_versiondata($pagename, $version, $want_content = false) {
383 $vd = $this->_loadVersionData($pagename, $version);
390 * Rename all files for this page
392 * @access protected Via WikiDB
394 function rename_page($pagename, $to) {
395 $version = _getLatestVersion($pagename);
396 foreach ($this->_dir_names as $type => $path) {
398 $filename = $this->_pagename2filename($type, $pagename, $version);
399 $new = $this->_pagename2filename($type, $to, $version);
400 @rename($filename,$new);
403 $this->update_pagedata($pagename, array('pagename' => $to));
408 * Delete page from the database.
410 * Delete page (and all it's revisions) from the database.
412 * @param $pagename string Page name.
414 function delete_page($pagename) {
415 $ver = $this->get_latest_version($pagename);
417 $this->_removePage('ver_data', $pagename, $ver);
418 $ver = $this->get_previous_version($pagename, $ver);
420 $this->_removePage('page_data', $pagename, 0);
421 $this->_removePage('links', $pagename, 0);
422 // remove page from latest_version...
423 $this->_setLatestVersion($pagename, 0);
427 * Delete an old revision of a page.
429 * Note that one is never allowed to delete the most recent version,
430 * but that this requirement is enforced by WikiDB not by the backend.
432 * In fact, to be safe, backends should probably allow the deletion of
433 * the most recent version.
435 * @param $pagename string Page name.
436 * @param $version integer Version to delete.
438 function delete_versiondata($pagename, $version) {
439 if ($this->get_latest_version($pagename) == $version) {
440 // try to delete the latest version!
441 // so check if an older version exist:
442 if ($this->get_versiondata($pagename, $this->get_previous_version($pagename, $version), false) == false) {
443 // there is no older version....
444 // so the completely page will be removed:
445 $this->delete_page($pagename);
449 $this->_removePage('ver_data', $pagename, $version);
453 * Create a new page revision.
455 * If the given ($pagename,$version) is already in the database,
456 * this method completely overwrites any stored data for that version.
458 * @param $pagename string Page name.
459 * @param $version int New revisions content.
460 * @param $data hash New revision metadata.
462 * @see get_versiondata
464 function set_versiondata($pagename, $version, $data) {
465 $this->_saveVersionData($pagename, $version, $data);
469 * Update page version meta-data.
471 * If the given ($pagename,$version) is already in the database,
472 * this method only changes those meta-data values whose keys are
473 * explicity listed in $newdata.
475 * @param $pagename string Page name.
476 * @param $version int New revisions content.
477 * @param $newdata hash New revision metadata.
478 * @see set_versiondata, get_versiondata
480 function update_versiondata($pagename, $version, $newdata) {
481 $data = $this->get_versiondata($pagename, $version, true);
486 foreach ($newdata as $key => $val) {
492 $this->set_versiondata($pagename, $version, $data);
496 * Set links for page.
498 * @param $pagename string Page name.
500 * @param $links array List of page(names) which page links to.
502 function set_links($pagename, $links) {
503 $this->_savePageLinks($pagename, $links);
507 * Find pages which link to or are linked from a page.
509 * @param $pagename string Page name.
510 * @param $reversed boolean True to get backlinks.
512 * FIXME: array or iterator?
513 * @return object A WikiDB_backend_iterator.
515 function get_links($pagename, $reversed) {
516 if ($reversed == false)
517 return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
519 $this->_loadLatestVersions();
520 $pagenames = $this->_latest_versions; // now we have an array with the key is the pagename of all pages
522 $out = array(); // create empty out array
524 foreach ($pagenames as $key => $val) {
525 $links = $this->_loadPageLinks($key);
526 foreach ($links as $key2 => $val2) {
527 if ($val2 == $pagename)
528 array_push($out, $key);
531 return new WikiDB_backend_file_iter($this, $out);
535 * Get all revisions of a page.
537 * @param $pagename string The page name.
538 * @return object A WikiDB_backend_iterator.
540 function get_all_revisions($pagename) {
541 include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
542 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
546 * Get all pages in the database.
548 * Pages should be returned in alphabetical order if that is
553 * @param $include_defaulted boolean
554 * If set, even pages with no content will be returned
555 * --- but still only if they have at least one revision (not
556 * counting the default revision 0) entered in the database.
558 * Normally pages whose current revision has empty content
559 * are not returned as these pages are considered to be
562 * @return object A WikiDB_backend_iterator.
564 function get_all_pages($include_deleted=false, $orderby='pagename') {
565 $this->_loadLatestVersions();
566 $a = array_keys($this->_latest_versions);
568 return new WikiDB_backend_file_iter($this, $a);
572 * Title or full text search.
574 * Pages should be returned in alphabetical order if that is
579 * @param $search object A TextSearchQuery object describing what pages
580 * are to be searched for.
582 * @param $fullsearch boolean If true, a full text search is performed,
583 * otherwise a title search is performed.
585 * @return object A WikiDB_backend_iterator.
587 * @see WikiDB::titleSearch
589 function text_search($search = '', $fullsearch = false) {
590 // This is method implements a simple linear search
591 // through all the pages in the database.
593 // It is expected that most backends will overload
594 // method with something more efficient.
595 include_once('lib/WikiDB/backend/dumb/TextSearchIter.php');
596 $pages = $this->get_all_pages(false);
597 return new WikiDB_backend_dumb_TextSearchIter($this, $pages, $search, $fullsearch);
601 * Find pages with highest hit counts.
603 * Find the pages with the highest hit counts. The pages should
604 * be returned in reverse order by hit count.
607 * @param $limit integer No more than this many pages
608 * @return object A WikiDB_backend_iterator.
610 function most_popular($limit,$sortby = '') {
611 // This is method fetches all pages, then
612 // sorts them by hit count.
613 // (Not very efficient.)
615 // It is expected that most backends will overload
616 // method with something more efficient.
617 include_once('lib/WikiDB/backend/dumb/MostPopularIter.php');
618 $pages = $this->get_all_pages(false,'hits DESC');
620 return new WikiDB_backend_dumb_MostPopularIter($this, $pages, $limit);
624 * Find recent changes.
627 * @param $params hash See WikiDB::mostRecent for a description
628 * of parameters which can be included in this hash.
629 * @return object A WikiDB_backend_iterator.
630 * @see WikiDB::mostRecent
632 function most_recent($params) {
633 // This method is very inefficient and searches through
634 // all pages for the most recent changes.
636 // It is expected that most backends will overload
637 // method with something more efficient.
638 include_once('lib/WikiDB/backend/dumb/MostRecentIter.php');
639 $pages = $this->get_all_pages(true,'mtime DESC');
640 return new WikiDB_backend_dumb_MostRecentIter($this, $pages, $params);
644 * Lock backend database.
646 * Calls may be nested.
648 * @param $write_lock boolean Unless this is set to false, a write lock
649 * is acquired, otherwise a read lock. If the backend doesn't support
650 * read locking, then it should make a write lock no matter which type
651 * of lock was requested.
653 * All backends <em>should</em> support write locking.
655 function lock($write_lock = true) {
656 //trigger_error("lock: Not Implemented", E_USER_WARNING);
660 * Unlock backend database.
662 * @param $force boolean Normally, the database is not unlocked until
663 * unlock() is called as many times as lock() has been. If $force is
664 * set to true, the the database is unconditionally unlocked.
666 function unlock($force = false) {
667 //trigger_error("unlock: Not Implemented", E_USER_WARNING);
675 //trigger_error("close: Not Implemented", E_USER_WARNING);
679 * Synchronize with filesystem.
681 * This should flush all unwritten data to the filesystem.
684 //trigger_error("sync: Not Implemented", E_USER_WARNING);
688 * Optimize the database.
690 function optimize() {
691 //trigger_error("optimize: Not Implemented", E_USER_WARNING);
695 * Check database integrity.
697 * This should check the validity of the internal structure of the database.
698 * Errors should be reported via:
700 * trigger_error("Message goes here.", E_USER_WARNING);
703 * @return boolean True iff database is in a consistent state.
706 //trigger_error("check: Not Implemented", E_USER_WARNING);
710 * Put the database into a consistent state.
712 * This should put the database into a consistent state.
713 * (I.e. rebuild indexes, etc...)
715 * @return boolean True iff successful.
718 //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
721 function _parse_searchwords($search) {
722 $search = strtolower(trim($search));
724 return array(array(),array());
726 $words = preg_split('/\s+/', $search);
728 foreach ($words as $key => $word) {
729 if ($word[0] == '-' && $word != '-') {
730 $word = substr($word, 1);
731 $exclude[] = preg_quote($word);
735 return array($words, $exclude);
740 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
742 function WikiDB_backend_file_iter(&$backend, &$query_result) {
743 $this->_backend = &$backend;
744 $this->_result = $query_result;
746 if (count($this->_result) > 0)
747 reset($this->_result);
751 $backend = &$this->_backend;
756 if (count($this->_result) <= 0)
759 $e = each($this->_result);
765 $pagedata = $backend->get_pagedata($pn);
766 $rec = array('pagename' => $pn,
767 'pagedata' => $pagedata);
769 //$rec['version'] = $backend->get_latest_version($pn);
770 //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
776 return count($this->_result);
783 // $Log: not supported by cvs2svn $
784 // Revision 1.12 2004/07/08 13:50:32 rurban
785 // various unit test fixes: print error backtrace on _DEBUG_TRACE; allusers fix; new PHPWIKI_NOMAIN constant for omitting the mainloop
787 // Revision 1.11 2004/07/08 11:12:49 rurban
788 // quiet the testruns
790 // Revision 1.10 2004/06/03 22:08:17 rurban
791 // fix bug #963268 (check existing previous version)
793 // Revision 1.9 2004/04/27 16:03:05 rurban
794 // missing pageiter::count methods
796 // Revision 1.8 2004/03/01 13:48:45 rurban
798 // p[] consistency fix
800 // Revision 1.7 2004/02/12 14:11:36 rurban
801 // more rename_page backend methods: only tested for PearDB! please help
803 // Revision 1.6 2004/01/26 09:17:51 rurban
804 // * changed stored pref representation as before.
805 // the array of objects is 1) bigger and 2)
806 // less portable. If we would import packed pref
807 // objects and the object definition was changed, PHP would fail.
808 // This doesn't happen with an simple array of non-default values.
809 // * use $prefs->retrieve and $prefs->store methods, where retrieve
810 // understands the interim format of array of objects also.
811 // * simplified $prefs->get() and fixed $prefs->set()
812 // * added $user->_userid and class '_WikiUser' portability functions
813 // * fixed $user object ->_level upgrading, mostly using sessions.
814 // this fixes yesterdays problems with loosing authorization level.
815 // * fixed WikiUserNew::checkPass to return the _level
816 // * fixed WikiUserNew::isSignedIn
817 // * added explodePageList to class PageList, support sortby arg
818 // * fixed UserPreferences for WikiUserNew
819 // * fixed WikiPlugin for empty defaults array
820 // * UnfoldSubpages: added pagename arg, renamed pages arg,
821 // removed sort arg, support sortby arg
823 // Revision 1.5 2004/01/25 08:17:29 rurban
824 // ORDER BY support for all other backends,
825 // all non-SQL simply ignoring it, using plain old dumb_iter instead
827 // Revision 1.4 2003/02/24 01:53:28 dairiki
828 // Bug fix. Don't need to urldecode pagenames in WikiDB_backend_file_iter.
830 // Revision 1.3 2003/01/04 03:41:51 wainstead
831 // Added copyleft flowerboxes
833 // Revision 1.2 2003/01/04 03:30:34 wainstead
834 // added log tag, converted file to unix format
842 // c-hanging-comment-ender-p: nil
843 // indent-tabs-mode: nil