4 * Copyright 1999, 2000, 2001, 2002, 2003 $ThePhpWikiProgrammingTeam
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.
24 * Backend for handling file storage.
26 * Author: Jochen Kalmbach, Jochen@kalmbachnet.de
31 * - Implement "optimize" / "sync" / "check" / "rebuild"
32 * - Optimize "get_previous_version"
33 * - Optimize "get_links" (reversed = true)
34 * - Optimize "get_all_revisions"
35 * - Optimize "most_popular" (separate file for "hitcount",
36 * which contains all pages)
37 * - Optimize "most_recent"
38 * - What should be done in "lock"/"unlock"/"close" ?
39 * - "WikiDB_backend_file_iter": Do I need to return 'version' and 'versiondata' ?
43 require_once 'lib/WikiDB/backend.php';
44 require_once 'lib/ErrorManager.php';
46 class WikiDB_backend_file
47 extends WikiDB_backend
52 public $_page_data; // temporarily stores the pagedata (via _loadPageData)
53 public $_page_version_data; // temporarily stores the versiondata (via _loadVersionData)
54 public $_latest_versions; // temporarily stores the latest version-numbers (for every pagename)
56 function WikiDB_backend_file($dbparam)
58 $this->data_dir = $dbparam['directory'];
59 if (file_exists($this->data_dir) and is_file($this->data_dir))
60 unlink($this->data_dir);
61 if (is_dir($this->data_dir) == false) {
62 mkdir($this->data_dir, 0755);
66 = array('ver_data' => $this->data_dir . '/' . 'ver_data',
67 'page_data' => $this->data_dir . '/' . 'page_data',
68 'latest_ver' => $this->data_dir . '/' . 'latest_ver',
69 'links' => $this->data_dir . '/' . 'links');
71 foreach ($this->_dir_names as $key => $val) {
72 if (file_exists($val) and is_file($val))
74 if (is_dir($val) == false)
78 $this->_page_data = NULL;
79 $this->_page_version_data = NULL;
80 $this->_latest_versions = NULL;
84 // *********************************************************************
85 // common file load / save functions:
86 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)
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)
123 $filename = $this->_pagename2filename($type, $pagename, $version);
124 if ($fd = fopen($filename, 'a+b')) {
125 $locked = flock($fd, 2); // Exclusive blocking lock
127 ExitWiki("Timeout while obtaining lock. Please try again");
132 $pagedata = serialize($data);
133 $len = strlen($pagedata);
134 $num = fwrite($fd, $pagedata, $len);
135 assert($num == $len);
138 ExitWiki("Error while writing page '$pagename'");
142 function _removePage($type, $pagename, $version)
144 $filename = $this->_pagename2filename($type, $pagename, $version);
145 if (!file_exists($filename)) return NULL;
146 $f = @unlink($filename);
148 trigger_error("delete file failed: " . $filename . " ver: " . $version, E_USER_WARNING);
151 // *********************************************************************
153 // *********************************************************************
154 // Load/Save Version-Data
155 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)
161 return $this->_page_version_data;
164 $vd = $this->_loadPage('ver_data', $pagename, $version);
166 $this->_page_version_data = $vd;
167 if (($this->_page_version_data['pagename'] == $pagename) &&
168 ($this->_page_version_data['version'] == $version)
170 return $this->_page_version_data;
176 function _saveVersionData($pagename, $version, $data)
178 $this->_savePage('ver_data', $pagename, $version, $data);
180 // check if this is a newer version:
181 if ($this->_getLatestVersion($pagename) < $version) {
182 // write new latest-version-info
183 $this->_setLatestVersion($pagename, $version);
187 // *********************************************************************
188 // Load/Save Page-Data
189 function _loadPageData($pagename)
191 if ($this->_page_data != NULL) {
192 if ($this->_page_data['pagename'] == $pagename) {
193 return $this->_page_data;
196 $pd = $this->_loadPage('page_data', $pagename, 0);
198 $this->_page_data = $pd;
199 if ($this->_page_data != NULL) {
200 if ($this->_page_data['pagename'] == $pagename) {
201 return $this->_page_data;
204 return array(); // no values found
207 function _savePageData($pagename, $data)
209 $this->_savePage('page_data', $pagename, 0, $data);
212 // *********************************************************************
213 // Load/Save Latest-Version
214 function _saveLatestVersions()
216 $data = $this->_latest_versions;
219 $this->_savePage('latest_ver', 'latest_versions', 0, $data);
222 function _setLatestVersion($pagename, $version)
224 // make sure the page version list is loaded:
225 $this->_getLatestVersion($pagename);
227 $this->_getLatestVersion($pagename);
228 $this->_latest_versions[$pagename] = $version;
230 // Remove this page from the Latest-Version-List:
231 unset($this->_latest_versions[$pagename]);
233 $this->_saveLatestVersions();
236 function _loadLatestVersions()
238 if ($this->_latest_versions != NULL)
241 $pd = $this->_loadPage('latest_ver', 'latest_versions', 0, false);
243 $this->_latest_versions = $pd;
245 $this->_latest_versions = array(); // empty array
248 function _getLatestVersion($pagename)
250 $this->_loadLatestVersions();
251 if (array_key_exists($pagename, $this->_latest_versions) == false)
252 return 0; // do version exists
253 return $this->_latest_versions[$pagename];
256 // *********************************************************************
257 // Load/Save Page-Links
258 function _loadPageLinks($pagename)
260 $pd = $this->_loadPage('links', $pagename, 0, false);
264 return array(); // no values found
267 function _savePageLinks($pagename, $links)
269 $this->_savePage('links', $pagename, 0, $links);
273 * Get page meta-data from database.
275 * @param $pagename string Page name.
277 * Returns a hash containing the page meta-data.
278 * Returns an empty array if there is no meta-data for the requested page.
279 * Keys which might be present in the hash are:
281 * <dt> locked <dd> If the page is locked.
282 * <dt> hits <dd> The page hit count.
283 * <dt> created <dd> Unix time of page creation. (FIXME: Deprecated: I
284 * don't think we need this...)
287 function get_pagedata($pagename)
289 return $this->_loadPageData($pagename);
293 * Update the page meta-data.
295 * Set page meta-data.
297 * Only meta-data whose keys are preset in $newdata is affected.
301 * $backend->update_pagedata($pagename, array('locked' => 1));
303 * will set the value of 'locked' to 1 for the specified page, but it
304 * will not affect the value of 'hits' (or whatever other meta-data
305 * may have been stored for the page.)
307 * To delete a particular piece of meta-data, set it's value to false.
309 * $backend->update_pagedata($pagename, array('locked' => false));
312 * @param $pagename string Page name.
313 * @param $newdata hash New meta-data.
316 * This will create a new page if page being requested does not
319 function update_pagedata($pagename, $newdata)
321 $data = $this->get_pagedata($pagename);
322 if (count($data) == 0) {
323 $this->_savePageData($pagename, $newdata); // create a new pagedata-file
327 foreach ($newdata as $key => $val) {
333 $this->_savePageData($pagename, $data); // write new pagedata-file
337 * Get the current version number for a page.
339 * @param $pagename string Page name.
340 * @return int The latest version number for the page. Returns zero if
341 * no versions of a page exist.
343 function get_latest_version($pagename)
345 return $this->_getLatestVersion($pagename);
349 * Get preceding version number.
351 * @param $pagename string Page name.
352 * @param $version int Find version before this one.
353 * @return int The version number of the version in the database which
354 * immediately preceeds $version.
356 * FIXED: Check if this version really exists!
358 function get_previous_version($pagename, $version)
360 $prev = ($version > 0 ? $version - 1 : 0);
361 while ($prev and !file_exists($this->_pagename2filename('ver_data', $pagename, $prev))) {
368 * Get revision meta-data and content.
370 * @param $pagename string Page name.
371 * @param $version integer Which version to get.
372 * @param $want_content boolean
373 * Indicates the caller really wants the page content. If this
374 * flag is not set, the backend is free to skip fetching of the
375 * page content (as that may be expensive). If the backend omits
376 * the content, the backend might still want to set the value of
377 * '%content' to the empty string if it knows there's no content.
379 * @return hash The version data, or false if specified version does not
382 * Some keys which might be present in the $versiondata hash are:
385 * <dd> This is a pseudo-meta-data element (since it's actually
386 * the page data, get it?) containing the page content.
387 * If the content was not fetched, this key may not be present.
389 * For description of other version meta-data see WikiDB_PageRevision::get().
390 * @see WikiDB_PageRevision::get
392 function get_versiondata($pagename, $version, $want_content = false)
394 $vd = $this->_loadVersionData($pagename, $version);
401 * Rename all files for this page
403 * @access protected Via WikiDB
405 function rename_page($pagename, $to)
407 $version = _getLatestVersion($pagename);
408 foreach ($this->_dir_names as $type => $path) {
410 $filename = $this->_pagename2filename($type, $pagename, $version);
411 $new = $this->_pagename2filename($type, $to, $version);
412 @rename($filename, $new);
415 $this->update_pagedata($pagename, array('pagename' => $to));
420 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
422 function delete_page($pagename)
424 $this->purge_page($pagename);
428 * Delete page from the database.
430 * Delete page (and all it's revisions) from the database.
432 * @param $pagename string Page name.
434 function purge_page($pagename)
436 $ver = $this->get_latest_version($pagename);
438 $this->_removePage('ver_data', $pagename, $ver);
439 $ver = $this->get_previous_version($pagename, $ver);
441 $this->_removePage('page_data', $pagename, 0);
442 $this->_removePage('links', $pagename, 0);
443 // remove page from latest_version...
444 $this->_setLatestVersion($pagename, 0);
448 * Delete an old revision of a page.
450 * Note that one is never allowed to delete the most recent version,
451 * but that this requirement is enforced by WikiDB not by the backend.
453 * In fact, to be safe, backends should probably allow the deletion of
454 * the most recent version.
456 * @param $pagename string Page name.
457 * @param $version integer Version to delete.
459 function delete_versiondata($pagename, $version)
461 if ($this->get_latest_version($pagename) == $version) {
462 // try to delete the latest version!
463 // so check if an older version exist:
464 if ($this->get_versiondata($pagename,
465 $this->get_previous_version($pagename, $version),
468 // there is no older version....
469 // so the completely page will be removed:
470 $this->delete_page($pagename);
474 $this->_removePage('ver_data', $pagename, $version);
478 * Create a new page revision.
480 * If the given ($pagename,$version) is already in the database,
481 * this method completely overwrites any stored data for that version.
483 * @param $pagename string Page name.
484 * @param $version int New revisions content.
485 * @param $data hash New revision metadata.
487 * @see get_versiondata
489 function set_versiondata($pagename, $version, $data)
491 $this->_saveVersionData($pagename, $version, $data);
495 * Update page version meta-data.
497 * If the given ($pagename,$version) is already in the database,
498 * this method only changes those meta-data values whose keys are
499 * explicity listed in $newdata.
501 * @param $pagename string Page name.
502 * @param $version int New revisions content.
503 * @param $newdata hash New revision metadata.
504 * @see set_versiondata, get_versiondata
506 function update_versiondata($pagename, $version, $newdata)
508 $data = $this->get_versiondata($pagename, $version, true);
513 foreach ($newdata as $key => $val) {
519 $this->set_versiondata($pagename, $version, $data);
523 * Set links for page.
525 * @param $pagename string Page name.
527 * @param $links array List of page(names) which page links to.
529 function set_links($pagename, $links)
531 $this->_savePageLinks($pagename, $links);
535 * Find pages which link to or are linked from a page.
537 * @param $pagename string Page name.
538 * @param $reversed boolean True to get backlinks.
540 * FIXME: array or iterator?
541 * @return object A WikiDB_backend_iterator.
543 function get_links($pagename, $reversed = true, $include_empty = false,
544 $sortby = '', $limit = '', $exclude = '',
545 $want_relations = false)
547 if ($reversed == false)
548 return new WikiDB_backend_file_iter($this, $this->_loadPageLinks($pagename));
550 $this->_loadLatestVersions();
551 $pagenames = $this->_latest_versions; // now we have an array with the key is the pagename of all pages
553 $out = array(); // create empty out array
555 foreach ($pagenames as $key => $val) {
556 $links = $this->_loadPageLinks($key);
557 foreach ($links as $key2 => $val2) {
558 if ($val2['linkto'] == $pagename)
559 array_push($out, $key);
562 return new WikiDB_backend_file_iter($this, $out);
566 * Get all revisions of a page.
568 * @param $pagename string The page name.
569 * @return object A WikiDB_backend_iterator.
572 function get_all_revisions($pagename) {
573 include_once 'lib/WikiDB/backend/dumb/AllRevisionsIter.php';
574 return new WikiDB_backend_dumb_AllRevisionsIter($this, $pagename);
579 * Get all pages in the database.
581 * Pages should be returned in alphabetical order if that is
586 * @param $include_defaulted boolean
587 * If set, even pages with no content will be returned
588 * --- but still only if they have at least one revision (not
589 * counting the default revision 0) entered in the database.
591 * Normally pages whose current revision has empty content
592 * are not returned as these pages are considered to be
595 * @return object A WikiDB_backend_iterator.
597 function get_all_pages($include_empty = false, $sortby = '', $limit = '', $exclude = '')
599 require_once 'lib/PageList.php';
600 $this->_loadLatestVersions();
601 $a = array_keys($this->_latest_versions);
603 return new WikiDB_backend_file_iter($this, $a);
604 $sortby = $this->sortby($sortby, 'db', $this->sortable_columns());
611 case 'pagename DESC':
615 return new WikiDB_backend_file_iter($this, $a);
618 function sortable_columns()
620 return array('pagename');
623 function numPages($filter = false, $exclude = '')
625 $this->_loadLatestVersions();
626 return count($this->_latest_versions);
630 * Lock backend database.
632 * Calls may be nested.
634 * @param $write_lock boolean Unless this is set to false, a write lock
635 * is acquired, otherwise a read lock. If the backend doesn't support
636 * read locking, then it should make a write lock no matter which type
637 * of lock was requested.
639 * All backends <em>should</em> support write locking.
641 function lock($write_lock = true)
643 //trigger_error("lock: Not Implemented", E_USER_WARNING);
647 * Unlock backend database.
649 * @param $force boolean Normally, the database is not unlocked until
650 * unlock() is called as many times as lock() has been. If $force is
651 * set to true, the the database is unconditionally unlocked.
653 function unlock($force = false)
655 //trigger_error("unlock: Not Implemented", E_USER_WARNING);
663 //trigger_error("close: Not Implemented", E_USER_WARNING);
667 * Synchronize with filesystem.
669 * This should flush all unwritten data to the filesystem.
673 //trigger_error("sync: Not Implemented", E_USER_WARNING);
677 * Optimize the database.
681 return 0; //trigger_error("optimize: Not Implemented", E_USER_WARNING);
685 * Check database integrity.
687 * This should check the validity of the internal structure of the database.
688 * Errors should be reported via:
690 * trigger_error("Message goes here.", E_USER_WARNING);
693 * @return boolean True iff database is in a consistent state.
697 //trigger_error("check: Not Implemented", E_USER_WARNING);
701 * Put the database into a consistent state.
703 * This should put the database into a consistent state.
704 * (I.e. rebuild indexes, etc...)
706 * @return boolean True iff successful.
710 //trigger_error("rebuild: Not Implemented", E_USER_WARNING);
713 function _parse_searchwords($search)
715 $search = strtolower(trim($search));
717 return array(array(), array());
719 $words = preg_split('/\s+/', $search);
721 foreach ($words as $key => $word) {
722 if ($word[0] == '-' && $word != '-') {
723 $word = substr($word, 1);
724 $exclude[] = preg_quote($word);
728 return array($words, $exclude);
733 class WikiDB_backend_file_iter extends WikiDB_backend_iterator
735 function WikiDB_backend_file_iter(&$backend, &$query_result, $options = array())
737 $this->_backend = &$backend;
738 $this->_result = $query_result;
739 $this->_options = $options;
741 if (count($this->_result) > 0)
742 reset($this->_result);
749 if (count($this->_result) <= 0)
752 $e = each($this->_result);
758 if (is_array($pn) and isset($pn['linkto'])) { // support relation link iterator
761 $pagedata = $this->_backend->get_pagedata($pn);
762 // don't pass _cached_html via iterators
763 if (isset($pagedata['_cached_html']))
764 unset($pagedata['_cached_html']);
765 unset($pagedata['pagename']);
766 $rec = array('pagename' => $pn,
767 'pagedata' => $pagedata);
768 if (is_array($e[1])) {
769 $rec['linkrelation'] = $e[1]['relation'];
771 //$rec['version'] = $backend->get_latest_version($pn);
772 //$rec['versiondata'] = $backend->get_versiondata($pn, $rec['version'], true);
778 reset($this->_result);
779 return $this->_result;
784 return count($this->_result);
789 $this->_result = array();
797 // c-hanging-comment-ender-p: nil
798 // indent-tabs-mode: nil