2 rcs_id('$Id: file.php,v 1.22 2004-12-06 19:50:04 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
25 * Backend for handling file storage.
27 * Author: Jochen Kalmbach, Jochen@kalmbachnet.de
32 * - Implement "optimize" / "sync" / "check" / "rebuild"
33 * - Optimize "get_previous_version"
34 * - Optimize "get_links" (reversed = true)
35 * - Optimize "get_all_revisions"
36 * - Optimize "most_popular" (separate file for "hitcount",
37 * which contains all pages)
38 * - Optimize "most_recent"
39 * - What should be done in "lock"/"unlock"/"close" ?
40 * - "WikiDB_backend_file_iter": Do I need to return 'version' and 'versiondata' ?
44 require_once('lib/WikiDB/backend.php');
45 require_once('lib/ErrorManager.php');
47 class WikiDB_backend_file
48 extends WikiDB_backend
53 var $_page_data; // temporarily stores the pagedata (via _loadPageData)
54 var $_page_version_data; // temporarily stores the versiondata (via _loadVersionData)
55 var $_latest_versions; // temporarily stores the latest version-numbers (for every pagename)
57 function WikiDB_backend_file( $dbparam )
59 $this->data_dir = $dbparam['directory'];
60 if (file_exists($this->data_dir) and is_file($this->data_dir))
61 unlink($this->data_dir);
62 if (is_dir($this->data_dir) == false) {
63 mkdir($this->data_dir, 0755);
67 = array('ver_data' => $this->data_dir.'/'.'ver_data',
68 'page_data' => $this->data_dir.'/'.'page_data',
69 'latest_ver' => $this->data_dir.'/'.'latest_ver',
70 'links' => $this->data_dir.'/'.'links' );
72 foreach ($this->_dir_names as $key => $val) {
73 if (file_exists($val) and is_file($val))
75 if (is_dir($val) == false)
79 $this->_page_data = NULL;
80 $this->_page_version_data = NULL;
81 $this->_latest_versions = NULL;
86 // *********************************************************************
87 // common file load / save functions:
88 function _pagename2filename($type, $pagename, $version) {
90 return $this->_dir_names[$type].'/'.urlencode($pagename);
92 return $this->_dir_names[$type].'/'.urlencode($pagename).'--'.$version;
95 function _loadPage($type, $pagename, $version, $set_pagename = true) {
96 $filename = $this->_pagename2filename($type, $pagename, $version);
97 if (!file_exists($filename)) return NULL;
98 if (!filesize($filename)) return array();
99 if ($fd = @fopen($filename, "rb")) {
100 $locked = flock($fd, 1); # Read lock
102 ExitWiki("Timeout while obtaining lock. Please try again");
104 if ($data = fread($fd, filesize($filename))) {
105 $pd = unserialize($data);
106 if ($set_pagename == true)
107 $pd['pagename'] = $pagename;
109 $pd['version'] = $version;
111 ExitWiki(sprintf(gettext("'%s': corrupt file"),
112 htmlspecialchars($filename)));
121 function _savePage($type, $pagename, $version, $data) {
122 $filename = $this->_pagename2filename($type, $pagename, $version);
123 if($fd = fopen($filename, 'a+b')) {
124 $locked = flock($fd,2); #Exclusive blocking lock
126 ExitWiki("Timeout while obtaining lock. Please try again");
131 $pagedata = serialize($data);
132 fwrite($fd, $pagedata);
135 ExitWiki("Error while writing page '$pagename'");
139 function _removePage($type, $pagename, $version) {
140 $filename = $this->_pagename2filename($type, $pagename, $version);
141 if (!file_exists($filename)) return NULL;
142 $f = @unlink($filename);
144 trigger_error("delete file failed: ".$filename." ver: ".$version, E_USER_WARNING);
147 // *********************************************************************
149 // *********************************************************************
150 // Load/Save Version-Data
151 function _loadVersionData($pagename, $version) {
152 if ($this->_page_version_data != NULL) {
153 if ( ($this->_page_version_data['pagename'] == $pagename) &&
154 ($this->_page_version_data['version'] == $version) ) {
155 return $this->_page_version_data;
158 $vd = $this->_loadPage('ver_data', $pagename, $version);
160 $this->_page_version_data = $vd;
161 if ( ($this->_page_version_data['pagename'] == $pagename) &&
162 ($this->_page_version_data['version'] == $version) ) {
163 return $this->_page_version_data;
169 function _saveVersionData($pagename, $version, $data) {
170 $this->_savePage('ver_data', $pagename, $version, $data);
172 // check if this is a newer version:
173 if ($this->_getLatestVersion($pagename) < $version) {
174 // write new latest-version-info
175 $this->_setLatestVersion($pagename, $version);
180 // *********************************************************************
181 // Load/Save Page-Data
182 function _loadPageData($pagename) {
183 if ($this->_page_data != NULL) {
184 if ($this->_page_data['pagename'] == $pagename) {
185 return $this->_page_data;
188 $pd = $this->_loadPage('page_data', $pagename, 0);
190 $this->_page_data = $pd;
191 if ($this->_page_data != NULL) {
192 if ($this->_page_data['pagename'] == $pagename) {
193 return $this->_page_data;
196 return array(); // no values found
199 function _savePageData($pagename, $data) {
200 $this->_savePage('page_data', $pagename, 0, $data);
203 // *********************************************************************
204 // Load/Save Latest-Version
205 function _saveLatestVersions() {
206 $data = $this->_latest_versions;
209 $this->_savePage('latest_ver', 'latest_versions', 0, $data);
212 function _setLatestVersion($pagename, $version) {
213 // make sure the page version list is loaded:
214 $this->_getLatestVersion($pagename);
216 $this->_getLatestVersion($pagename);
217 $this->_latest_versions[$pagename] = $version;
220 // Remove this page from the Latest-Version-List:
221 unset($this->_latest_versions[$pagename]);
223 $this->_saveLatestVersions();
226 function _loadLatestVersions() {
227 if ($this->_latest_versions != NULL)
230 $pd = $this->_loadPage('latest_ver', 'latest_versions', 0, false);
232 $this->_latest_versions = $pd;
234 $this->_latest_versions = array(); // empty array
237 function _getLatestVersion($pagename) {
238 $this->_loadLatestVersions();
239 if (array_key_exists($pagename, $this->_latest_versions) == false)
240 return 0; // do version exists
241 return $this->_latest_versions[$pagename];
245 // *********************************************************************
246 // Load/Save Page-Links
247 function _loadPageLinks($pagename) {
248 $pd = $this->_loadPage('links', $pagename, 0, false);
251 return array(); // no values found
254 function _savePageLinks($pagename, $links) {
255 $this->_savePage('links', $pagename, 0, $links);
261 * Get page meta-data from database.
263 * @param $pagename string Page name.
265 * Returns a hash containing the page meta-data.
266 * Returns an empty array if there is no meta-data for the requested page.
267 * Keys which might be present in the hash are:
269 * <dt> locked <dd> If the page is locked.
270 * <dt> hits <dd> The page hit count.
271 * <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
272 * don't think we need this...)
275 function get_pagedata($pagename) {
276 return $this->_loadPageData($pagename);
280 * Update the page meta-data.
282 * Set page meta-data.
284 * Only meta-data whose keys are preset in $newdata is affected.
288 * $backend->update_pagedata($pagename, array('locked' => 1));
290 * will set the value of 'locked' to 1 for the specified page, but it
291 * will not affect the value of 'hits' (or whatever other meta-data
292 * may have been stored for the page.)
294 * To delete a particular piece of meta-data, set it's value to false.
296 * $backend->update_pagedata($pagename, array('locked' => false));
299 * @param $pagename string Page name.
300 * @param $newdata hash New meta-data.
303 * This will create a new page if page being requested does not
306 function update_pagedata($pagename, $newdata) {
307 $data = $this->get_pagedata($pagename);
308 if (count($data) == 0) {
309 $this->_savePageData($pagename, $newdata); // create a new pagedata-file
313 foreach ($newdata as $key => $val) {
319 $this->_savePageData($pagename, $data); // write new pagedata-file
324 * Get the current version number for a page.
326 * @param $pagename string Page name.
327 * @return int The latest version number for the page. Returns zero if
328 * no versions of a page exist.
330 function get_latest_version($pagename) {
331 return $this->_getLatestVersion($pagename);
335 * Get preceding version number.
337 * @param $pagename string Page name.
338 * @param $version int Find version before this one.
339 * @return int The version number of the version in the database which
340 * immediately preceeds $version.
342 * FIXED: Check if this version really exists!
344 function get_previous_version($pagename, $version) {
345 $prev = ($version > 0 ? $version - 1 : 0);
346 while ($prev and !file_exists($this->_pagename2filename('ver_data', $pagename, $prev))) {
353 * Get revision meta-data and content.
355 * @param $pagename string Page name.
356 * @param $version integer Which version to get.
357 * @param $want_content boolean
358 * Indicates the caller really wants the page content. If this
359 * flag is not set, the backend is free to skip fetching of the
360 * page content (as that may be expensive). If the backend omits
361 * the content, the backend might still want to set the value of
362 * '%content' to the empty string if it knows there's no content.
364 * @return hash The version data, or false if specified version does not
367 * Some keys which might be present in the $versiondata hash are:
370 * <dd> This is a pseudo-meta-data element (since it's actually
371 * the page data, get it?) containing the page content.
372 * If the content was not fetched, this key may not be present.
374 * For description of other version meta-data see WikiDB_PageRevision::get().
375 * @see WikiDB_PageRevision::get
377 function get_versiondata($pagename, $version, $want_content = false) {
378 $vd = $this->_loadVersionData($pagename, $version);
385 * Rename all files for this page
387 * @access protected Via WikiDB
389 function rename_page($pagename, $to) {
390 $version = _getLatestVersion($pagename);
391 foreach ($this->_dir_names as $type => $path) {
393 $filename = $this->_pagename2filename($type, $pagename, $version);
394 $new = $this->_pagename2filename($type, $to, $version);
395 @rename($filename,$new);
398 $this->update_pagedata($pagename, array('pagename' => $to));
403 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
405 function delete_page($pagename) {
406 $this->purge_page($pagename);
410 * Delete page from the database.
412 * Delete page (and all it's revisions) from the database.
414 * @param $pagename string Page name.
416 function purge_page($pagename) {
417 $ver = $this->get_latest_version($pagename);
419 $this->_removePage('ver_data', $pagename, $ver);
420 $ver = $this->get_previous_version($pagename, $ver);
422 $this->_removePage('page_data', $pagename, 0);
423 $this->_removePage('links', $pagename, 0);
424 // remove page from latest_version...
425 $this->_setLatestVersion($pagename, 0);
429 * Delete an old revision of a page.
431 * Note that one is never allowed to delete the most recent version,
432 * but that this requirement is enforced by WikiDB not by the backend.
434 * In fact, to be safe, backends should probably allow the deletion of
435 * the most recent version.
437 * @param $pagename string Page name.
438 * @param $version integer Version to delete.
440 function delete_versiondata($pagename, $version) {
441 if ($this->get_latest_version($pagename) == $version) {
442 // try to delete the latest version!
443 // so check if an older version exist:
444 if ($this->get_versiondata($pagename,
445 $this->get_previous_version($pagename, $version),
447 // there is no older version....
448 // so the completely page will be removed:
449 $this->delete_page($pagename);
453 $this->_removePage('ver_data', $pagename, $version);
457 * Create a new page revision.
459 * If the given ($pagename,$version) is already in the database,
460 * this method completely overwrites any stored data for that version.
462 * @param $pagename string Page name.
463 * @param $version int New revisions content.
464 * @param $data hash New revision metadata.
466 * @see get_versiondata
468 function set_versiondata($pagename, $version, $data) {
469 $this->_saveVersionData($pagename, $version, $data);
473 * Update page version meta-data.
475 * If the given ($pagename,$version) is already in the database,
476 * this method only changes those meta-data values whose keys are
477 * explicity listed in $newdata.
479 * @param $pagename string Page name.
480 * @param $version int New revisions content.
481 * @param $newdata hash New revision metadata.
482 * @see set_versiondata, get_versiondata
484 function update_versiondata($pagename, $version, $newdata) {
485 $data = $this->get_versiondata($pagename, $version, true);
490 foreach ($newdata as $key => $val) {
496 $this->set_versiondata($pagename, $version, $data);
500 * Set links for page.
502 * @param $pagename string Page name.
504 * @param $links array List of page(names) which page links to.
506 function set_links($pagename, $links) {
507 $this->_savePageLinks($pagename, $links);
511 * Find pages which link to or are linked from a page.
513 * @param $pagename string Page name.
514 * @param $reversed boolean True to get backlinks.
516 * FIXME: array or iterator?
517 * @return object A WikiDB_backend_iterator.
519 function get_links($pagename, $reversed=true, $include_empty=false,
520 $sortby=false, $limit=false, $exclude=false) {
521 if ($reversed == false)
522 return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
524 $this->_loadLatestVersions();
525 $pagenames = $this->_latest_versions; // now we have an array with the key is the pagename of all pages
527 $out = array(); // create empty out array
529 foreach ($pagenames as $key => $val) {
530 $links = $this->_loadPageLinks($key);
531 foreach ($links as $key2 => $val2) {
532 if ($val2 == $pagename)
533 array_push($out, $key);
536 return new WikiDB_backend_file_iter($this, $out);
540 * Get all revisions of a page.
542 * @param $pagename string The page name.
543 * @return object A WikiDB_backend_iterator.
546 function get_all_revisions($pagename) {
547 include_once('lib/WikiDB/backend/dumb/AllRevisionsIter.php');
548 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
553 * Get all pages in the database.
555 * Pages should be returned in alphabetical order if that is
560 * @param $include_defaulted boolean
561 * If set, even pages with no content will be returned
562 * --- but still only if they have at least one revision (not
563 * counting the default revision 0) entered in the database.
565 * Normally pages whose current revision has empty content
566 * are not returned as these pages are considered to be
569 * @return object A WikiDB_backend_iterator.
571 function get_all_pages($include_empty=false, $sortby=false, $limit=false, $exclude=false) {
572 require_once("lib/PageList.php");
573 $this->_loadLatestVersions();
574 $a = array_keys($this->_latest_versions);
576 return new WikiDB_backend_file_iter($this, $a);
577 $sortby = $this->sortby($sortby, 'db', $this->sortable_columns());
580 case 'pagename ASC': sort($a); break;
581 case 'pagename DESC': rsort($a); break;
583 return new WikiDB_backend_file_iter($this, $a);
586 function sortable_columns() {
587 return array('pagename');
590 function numPages($filter=false, $exclude='') {
591 $this->_loadLatestVersions();
592 return count($this->_latest_versions);
596 * Lock backend database.
598 * Calls may be nested.
600 * @param $write_lock boolean Unless this is set to false, a write lock
601 * is acquired, otherwise a read lock. If the backend doesn't support
602 * read locking, then it should make a write lock no matter which type
603 * of lock was requested.
605 * All backends <em>should</em> support write locking.
607 function lock($write_lock = true) {
608 //trigger_error("lock: Not Implemented", E_USER_WARNING);
612 * Unlock backend database.
614 * @param $force boolean Normally, the database is not unlocked until
615 * unlock() is called as many times as lock() has been. If $force is
616 * set to true, the the database is unconditionally unlocked.
618 function unlock($force = false) {
619 //trigger_error("unlock: Not Implemented", E_USER_WARNING);
627 //trigger_error("close: Not Implemented", E_USER_WARNING);
631 * Synchronize with filesystem.
633 * This should flush all unwritten data to the filesystem.
636 //trigger_error("sync: Not Implemented", E_USER_WARNING);
640 * Optimize the database.
642 function optimize() {
643 return 0;//trigger_error("optimize: Not Implemented", E_USER_WARNING);
647 * Check database integrity.
649 * This should check the validity of the internal structure of the database.
650 * Errors should be reported via:
652 * trigger_error("Message goes here.", E_USER_WARNING);
655 * @return boolean True iff database is in a consistent state.
658 //trigger_error("check: Not Implemented", E_USER_WARNING);
662 * Put the database into a consistent state.
664 * This should put the database into a consistent state.
665 * (I.e. rebuild indexes, etc...)
667 * @return boolean True iff successful.
670 //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
673 function _parse_searchwords($search) {
674 $search = strtolower(trim($search));
676 return array(array(),array());
678 $words = preg_split('/\s+/', $search);
680 foreach ($words as $key => $word) {
681 if ($word[0] == '-' && $word != '-') {
682 $word = substr($word, 1);
683 $exclude[] = preg_quote($word);
687 return array($words, $exclude);
692 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
694 function WikiDB_backend_file_iter(&$backend, &$query_result) {
695 $this->_backend = &$backend;
696 $this->_result = $query_result;
698 if (count($this->_result) > 0)
699 reset($this->_result);
705 if (count($this->_result) <= 0)
708 $e = each($this->_result);
714 $pagedata = $this->_backend->get_pagedata($pn);
715 // don't pass _cached_html via iterators
716 if (isset($pagedata['_cached_html']))
717 unset($pagedata['_cached_html']);
718 unset($pagedata['pagename']);
719 $rec = array('pagename' => $pn,
720 'pagedata' => $pagedata);
721 //$rec['version'] = $backend->get_latest_version($pn);
722 //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
726 reset($this->_result);
727 return $this->_result;
730 return count($this->_result);
733 $this->_result = array();
737 // $Log: not supported by cvs2svn $
738 // Revision 1.21 2004/11/25 17:20:52 rurban
739 // and again a couple of more native db args: backlinks
741 // Revision 1.20 2004/11/23 13:35:49 rurban
742 // add case_exact search
744 // Revision 1.19 2004/11/21 11:59:26 rurban
745 // remove final \n to be ob_cache independent
747 // Revision 1.18 2004/11/09 17:11:17 rurban
748 // * revert to the wikidb ref passing. there's no memory abuse there.
749 // * use new wikidb->_cache->_id_cache[] instead of wikidb->_iwpcache, to effectively
750 // store page ids with getPageLinks (GleanDescription) of all existing pages, which
751 // are also needed at the rendering for linkExistingWikiWord().
752 // pass options to pageiterator.
753 // use this cache also for _get_pageid()
754 // This saves about 8 SELECT count per page (num all pagelinks).
755 // * fix passing of all page fields to the pageiterator.
756 // * fix overlarge session data which got broken with the latest ACCESS_LOG_SQL changes
758 // Revision 1.17 2004/07/09 13:05:34 rurban
761 // Revision 1.16 2004/07/09 12:47:45 rurban
762 // dont cache _ cached_html and pagename in flatfile page iterators
764 // Revision 1.15 2004/07/09 10:06:50 rurban
765 // Use backend specific sortby and sortable_columns method, to be able to
766 // select between native (Db backend) and custom (PageList) sorting.
767 // Fixed PageList::AddPageList (missed the first)
768 // Added the author/creator.. name to AllPagesBy...
769 // display no pages if none matched.
770 // Improved dba and file sortby().
771 // Use &$request reference
773 // Revision 1.14 2004/07/08 17:31:43 rurban
774 // improve numPages for file (fixing AllPagesTest)
776 // Revision 1.13 2004/07/08 15:23:59 rurban
777 // less verbose for tests
779 // Revision 1.12 2004/07/08 13:50:32 rurban
780 // various unit test fixes: print error backtrace on _DEBUG_TRACE; allusers fix; new PHPWIKI_NOMAIN constant for omitting the mainloop
782 // Revision 1.11 2004/07/08 11:12:49 rurban
783 // quiet the testruns
785 // Revision 1.10 2004/06/03 22:08:17 rurban
786 // fix bug #963268 (check existing previous version)
788 // Revision 1.9 2004/04/27 16:03:05 rurban
789 // missing pageiter::count methods
791 // Revision 1.8 2004/03/01 13:48:45 rurban
793 // p[] consistency fix
795 // Revision 1.7 2004/02/12 14:11:36 rurban
796 // more rename_page backend methods: only tested for PearDB! please help
798 // Revision 1.6 2004/01/26 09:17:51 rurban
799 // * changed stored pref representation as before.
800 // the array of objects is 1) bigger and 2)
801 // less portable. If we would import packed pref
802 // objects and the object definition was changed, PHP would fail.
803 // This doesn't happen with an simple array of non-default values.
804 // * use $prefs->retrieve and $prefs->store methods, where retrieve
805 // understands the interim format of array of objects also.
806 // * simplified $prefs->get() and fixed $prefs->set()
807 // * added $user->_userid and class '_WikiUser' portability functions
808 // * fixed $user object ->_level upgrading, mostly using sessions.
809 // this fixes yesterdays problems with loosing authorization level.
810 // * fixed WikiUserNew::checkPass to return the _level
811 // * fixed WikiUserNew::isSignedIn
812 // * added explodePageList to class PageList, support sortby arg
813 // * fixed UserPreferences for WikiUserNew
814 // * fixed WikiPlugin for empty defaults array
815 // * UnfoldSubpages: added pagename arg, renamed pages arg,
816 // removed sort arg, support sortby arg
818 // Revision 1.5 2004/01/25 08:17:29 rurban
819 // ORDER BY support for all other backends,
820 // all non-SQL simply ignoring it, using plain old dumb_iter instead
822 // Revision 1.4 2003/02/24 01:53:28 dairiki
823 // Bug fix. Don't need to urldecode pagenames in WikiDB_backend_file_iter.
825 // Revision 1.3 2003/01/04 03:41:51 wainstead
826 // Added copyleft flowerboxes
828 // Revision 1.2 2003/01/04 03:30:34 wainstead
829 // added log tag, converted file to unix format
837 // c-hanging-comment-ender-p: nil
838 // indent-tabs-mode: nil