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 along
20 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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;
85 // *********************************************************************
86 // common file load / save functions:
87 function _pagename2filename($type, $pagename, $version) {
89 return $this->_dir_names[$type].'/'.urlencode($pagename);
91 return $this->_dir_names[$type].'/'.urlencode($pagename).'--'.$version;
94 function _loadPage($type, $pagename, $version, $set_pagename = true) {
95 $filename = $this->_pagename2filename($type, $pagename, $version);
96 if (!file_exists($filename)) return NULL;
97 if (!filesize($filename)) return array();
98 if ($fd = @fopen($filename, "rb")) {
99 $locked = flock($fd, 1); # Read lock
101 ExitWiki("Timeout while obtaining lock. Please try again");
103 if ($data = fread($fd, filesize($filename))) {
104 $pd = unserialize($data);
105 if ($set_pagename == true)
106 $pd['pagename'] = $pagename;
108 $pd['version'] = $version;
110 ExitWiki(sprintf(gettext("'%s': corrupt file"),
111 htmlspecialchars($filename)));
120 function _savePage($type, $pagename, $version, $data) {
121 $filename = $this->_pagename2filename($type, $pagename, $version);
122 if($fd = fopen($filename, 'a+b')) {
123 $locked = flock($fd,2); // Exclusive blocking lock
125 ExitWiki("Timeout while obtaining lock. Please try again");
130 $pagedata = serialize($data);
131 $len = strlen($pagedata);
132 $num = fwrite($fd, $pagedata, $len);
133 assert($num == $len);
136 ExitWiki("Error while writing page '$pagename'");
140 function _removePage($type, $pagename, $version) {
141 $filename = $this->_pagename2filename($type, $pagename, $version);
142 if (!file_exists($filename)) return NULL;
143 $f = @unlink($filename);
145 trigger_error("delete file failed: ".$filename." ver: ".$version, E_USER_WARNING);
148 // *********************************************************************
150 // *********************************************************************
151 // Load/Save Version-Data
152 function _loadVersionData($pagename, $version) {
153 if ($this->_page_version_data != NULL) {
154 if ( ($this->_page_version_data['pagename'] == $pagename) &&
155 ($this->_page_version_data['version'] == $version) ) {
156 return $this->_page_version_data;
159 $vd = $this->_loadPage('ver_data', $pagename, $version);
161 $this->_page_version_data = $vd;
162 if ( ($this->_page_version_data['pagename'] == $pagename) &&
163 ($this->_page_version_data['version'] == $version) ) {
164 return $this->_page_version_data;
170 function _saveVersionData($pagename, $version, $data) {
171 $this->_savePage('ver_data', $pagename, $version, $data);
173 // check if this is a newer version:
174 if ($this->_getLatestVersion($pagename) < $version) {
175 // write new latest-version-info
176 $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];
244 // *********************************************************************
245 // Load/Save Page-Links
246 function _loadPageLinks($pagename) {
247 $pd = $this->_loadPage('links', $pagename, 0, false);
250 return array(); // no values found
253 function _savePageLinks($pagename, $links) {
254 $this->_savePage('links', $pagename, 0, $links);
258 * Get page meta-data from database.
260 * @param $pagename string Page name.
262 * Returns a hash containing the page meta-data.
263 * Returns an empty array if there is no meta-data for the requested page.
264 * Keys which might be present in the hash are:
266 * <dt> locked <dd> If the page is locked.
267 * <dt> hits <dd> The page hit count.
268 * <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
269 * don't think we need this...)
272 function get_pagedata($pagename) {
273 return $this->_loadPageData($pagename);
277 * Update the page meta-data.
279 * Set page meta-data.
281 * Only meta-data whose keys are preset in $newdata is affected.
285 * $backend->update_pagedata($pagename, array('locked' => 1));
287 * will set the value of 'locked' to 1 for the specified page, but it
288 * will not affect the value of 'hits' (or whatever other meta-data
289 * may have been stored for the page.)
291 * To delete a particular piece of meta-data, set it's value to false.
293 * $backend->update_pagedata($pagename, array('locked' => false));
296 * @param $pagename string Page name.
297 * @param $newdata hash New meta-data.
300 * This will create a new page if page being requested does not
303 function update_pagedata($pagename, $newdata) {
304 $data = $this->get_pagedata($pagename);
305 if (count($data) == 0) {
306 $this->_savePageData($pagename, $newdata); // create a new pagedata-file
310 foreach ($newdata as $key => $val) {
316 $this->_savePageData($pagename, $data); // write new pagedata-file
320 * Get the current version number for a page.
322 * @param $pagename string Page name.
323 * @return int The latest version number for the page. Returns zero if
324 * no versions of a page exist.
326 function get_latest_version($pagename) {
327 return $this->_getLatestVersion($pagename);
331 * Get preceding version number.
333 * @param $pagename string Page name.
334 * @param $version int Find version before this one.
335 * @return int The version number of the version in the database which
336 * immediately preceeds $version.
338 * FIXED: Check if this version really exists!
340 function get_previous_version($pagename, $version) {
341 $prev = ($version > 0 ? $version - 1 : 0);
342 while ($prev and !file_exists($this->_pagename2filename('ver_data', $pagename, $prev))) {
349 * Get revision meta-data and content.
351 * @param $pagename string Page name.
352 * @param $version integer Which version to get.
353 * @param $want_content boolean
354 * Indicates the caller really wants the page content. If this
355 * flag is not set, the backend is free to skip fetching of the
356 * page content (as that may be expensive). If the backend omits
357 * the content, the backend might still want to set the value of
358 * '%content' to the empty string if it knows there's no content.
360 * @return hash The version data, or false if specified version does not
363 * Some keys which might be present in the $versiondata hash are:
366 * <dd> This is a pseudo-meta-data element (since it's actually
367 * the page data, get it?) containing the page content.
368 * If the content was not fetched, this key may not be present.
370 * For description of other version meta-data see WikiDB_PageRevision::get().
371 * @see WikiDB_PageRevision::get
373 function get_versiondata($pagename, $version, $want_content = false) {
374 $vd = $this->_loadVersionData($pagename, $version);
381 * Rename all files for this page
383 * @access protected Via WikiDB
385 function rename_page($pagename, $to) {
386 $version = _getLatestVersion($pagename);
387 foreach ($this->_dir_names as $type => $path) {
389 $filename = $this->_pagename2filename($type, $pagename, $version);
390 $new = $this->_pagename2filename($type, $to, $version);
391 @rename($filename,$new);
394 $this->update_pagedata($pagename, array('pagename' => $to));
399 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
401 function delete_page($pagename) {
402 $this->purge_page($pagename);
406 * Delete page from the database.
408 * Delete page (and all it's revisions) from the database.
410 * @param $pagename string Page name.
412 function purge_page($pagename) {
413 $ver = $this->get_latest_version($pagename);
415 $this->_removePage('ver_data', $pagename, $ver);
416 $ver = $this->get_previous_version($pagename, $ver);
418 $this->_removePage('page_data', $pagename, 0);
419 $this->_removePage('links', $pagename, 0);
420 // remove page from latest_version...
421 $this->_setLatestVersion($pagename, 0);
425 * Delete an old revision of a page.
427 * Note that one is never allowed to delete the most recent version,
428 * but that this requirement is enforced by WikiDB not by the backend.
430 * In fact, to be safe, backends should probably allow the deletion of
431 * the most recent version.
433 * @param $pagename string Page name.
434 * @param $version integer Version to delete.
436 function delete_versiondata($pagename, $version) {
437 if ($this->get_latest_version($pagename) == $version) {
438 // try to delete the latest version!
439 // so check if an older version exist:
440 if ($this->get_versiondata($pagename,
441 $this->get_previous_version($pagename, $version),
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=true, $include_empty=false,
516 $sortby='', $limit='', $exclude='',
517 $want_relations=false)
519 if ($reversed == false)
520 return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
522 $this->_loadLatestVersions();
523 $pagenames = $this->_latest_versions; // now we have an array with the key is the pagename of all pages
525 $out = array(); // create empty out array
527 foreach ($pagenames as $key => $val) {
528 $links = $this->_loadPageLinks($key);
529 foreach ($links as $key2 => $val2) {
530 if ($val2['linkto'] == $pagename)
531 array_push($out, $key);
534 return new WikiDB_backend_file_iter($this, $out);
538 * Get all revisions of a page.
540 * @param $pagename string The page name.
541 * @return object A WikiDB_backend_iterator.
544 function get_all_revisions($pagename) {
545 include_once 'lib/WikiDB/backend/dumb/AllRevisionsIter.php';
546 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
551 * Get all pages in the database.
553 * Pages should be returned in alphabetical order if that is
558 * @param $include_defaulted boolean
559 * If set, even pages with no content will be returned
560 * --- but still only if they have at least one revision (not
561 * counting the default revision 0) entered in the database.
563 * Normally pages whose current revision has empty content
564 * are not returned as these pages are considered to be
567 * @return object A WikiDB_backend_iterator.
569 function get_all_pages($include_empty=false, $sortby='', $limit='', $exclude='') {
570 require_once 'lib/PageList.php';
571 $this->_loadLatestVersions();
572 $a = array_keys($this->_latest_versions);
574 return new WikiDB_backend_file_iter($this, $a);
575 $sortby = $this->sortby($sortby, 'db', $this->sortable_columns());
578 case 'pagename ASC': sort($a); break;
579 case 'pagename DESC': rsort($a); break;
581 return new WikiDB_backend_file_iter($this, $a);
584 function sortable_columns() {
585 return array('pagename');
588 function numPages($filter=false, $exclude='') {
589 $this->_loadLatestVersions();
590 return count($this->_latest_versions);
594 * Lock backend database.
596 * Calls may be nested.
598 * @param $write_lock boolean Unless this is set to false, a write lock
599 * is acquired, otherwise a read lock. If the backend doesn't support
600 * read locking, then it should make a write lock no matter which type
601 * of lock was requested.
603 * All backends <em>should</em> support write locking.
605 function lock($write_lock = true) {
606 //trigger_error("lock: Not Implemented", E_USER_WARNING);
610 * Unlock backend database.
612 * @param $force boolean Normally, the database is not unlocked until
613 * unlock() is called as many times as lock() has been. If $force is
614 * set to true, the the database is unconditionally unlocked.
616 function unlock($force = false) {
617 //trigger_error("unlock: Not Implemented", E_USER_WARNING);
624 //trigger_error("close: Not Implemented", E_USER_WARNING);
628 * Synchronize with filesystem.
630 * This should flush all unwritten data to the filesystem.
633 //trigger_error("sync: Not Implemented", E_USER_WARNING);
637 * Optimize the database.
639 function optimize() {
640 return 0;//trigger_error("optimize: Not Implemented", E_USER_WARNING);
644 * Check database integrity.
646 * This should check the validity of the internal structure of the database.
647 * Errors should be reported via:
649 * trigger_error("Message goes here.", E_USER_WARNING);
652 * @return boolean True iff database is in a consistent state.
655 //trigger_error("check: Not Implemented", E_USER_WARNING);
659 * Put the database into a consistent state.
661 * This should put the database into a consistent state.
662 * (I.e. rebuild indexes, etc...)
664 * @return boolean True iff successful.
667 //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
670 function _parse_searchwords($search) {
671 $search = strtolower(trim($search));
673 return array(array(),array());
675 $words = preg_split('/\s+/', $search);
677 foreach ($words as $key => $word) {
678 if ($word[0] == '-' && $word != '-') {
679 $word = substr($word, 1);
680 $exclude[] = preg_quote($word);
684 return array($words, $exclude);
689 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
691 function WikiDB_backend_file_iter(&$backend, &$query_result, $options=array()) {
692 $this->_backend = &$backend;
693 $this->_result = $query_result;
694 $this->_options = $options;
696 if (count($this->_result) > 0)
697 reset($this->_result);
703 if (count($this->_result) <= 0)
706 $e = each($this->_result);
712 if (is_array($pn) and isset($pn['linkto'])) { // support relation link iterator
715 $pagedata = $this->_backend->get_pagedata($pn);
716 // don't pass _cached_html via iterators
717 if (isset($pagedata['_cached_html']))
718 unset($pagedata['_cached_html']);
719 unset($pagedata['pagename']);
720 $rec = array('pagename' => $pn,
721 'pagedata' => $pagedata);
722 if (is_array($e[1])) {
723 $rec['linkrelation'] = $e[1]['relation'];
725 //$rec['version'] = $backend->get_latest_version($pn);
726 //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
730 reset($this->_result);
731 return $this->_result;
734 return count($this->_result);
737 $this->_result = array();
745 // c-hanging-comment-ender-p: nil
746 // indent-tabs-mode: nil