<?php
/**
* File based NoteDriver for LCC
*
* PHP Version 4, 5
*
* @author <hide@address.com>
* @copyright Copyright (c) 2006, Benedikt Hallinger
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
/**
* A Driver that is capaple of storing and retrieving notes from flat files
*
* It is meant for small installations, because several race conditions exists where data could
* possibly be overriden, if too much people are editing the same note at the same time.
* Consider putting the storage place into a linux tmpfs or use another driver.
* However, it implements some kind of index file to speed up the note creation tasks etc a bit
*
* Possible configuration:
* 'datapath' Directory where the notes data is stored, full path with trailing slash.
* Default is the subdirectory 'filestore' of LCC_Core's 'datadir'
*/
class LCC_NoteDriver_File extends LCC_NoteDriver
{
/**
* Default config of the driver
*
* This gets overwritten by the core calling setConfig()
* The default value of 'datapath' is set
*
* @var array
*/
var $config = array(
'datapath' => '' //where to store the files; default set by init()
);
/**
* Used by the core to fetch all ids of notes for a specific event $event_id
*
*
* @param mixed $event_id Id of the event whose notes should be loaded
* @return array|false Array with Ids of the notes
*/
function getNotes($event_id)
{
$this->_checkConfig();
// Determine, what notes belong to the event
$index = $this->_getIndex();
$notes_for_this_event = array();
foreach ($index as $line) {
$row = split('\|', trim($line));
if (is_numeric($row[0]) && $row[1] == $event_id) {
array_push($notes_for_this_event, $row[0]);
}
}
return $notes_for_this_event;
}
/**
* Fetch data of a specific note
*
* This returns the data for a note as associative array
* containing field => value.
* The method is also used to determine if a note exists
*
* @param mixed $id ID of the note
* @return array|false
*/
function getData($id)
{
$this->_checkConfig();
// Construct the filename of the notes file
$data = false; // Assume note doesn't exist by default.
if ($this->_searchIndexForID($id)) {
$filename = $this->config['datapath'].'note_'.$id;
if (!is_readable($filename)) {
die('<b>LCC_NoteDriver_File ERROR:</b> Note file "'.$filename.'" is not readable!');
} else {
$file_conent = file($filename);
if (is_array($file_conent)) {
// Okay, parse the content and return the data
// Syntax is KEY=Value
$data = array();
foreach ($file_conent as $line) {
if (preg_match("/^(.+)=(.+)$/", $line, $match)) {
// There are storage problems with newlines in our flat files,
// here we decode what _writeNoteData() encodes
$data[$match[1]] = str_replace('-§-', PHP_EOL, $match[2]);
}
}
} else {
die('<b>LCC_NoteDriver_File ERROR:</b> Could not contents of note file "'.$filename.'"');
}
}
}
return $data;
}
/**
* This method is used to write data into the backend. $data is a associative array
*
* similar to the return value of getData().
* If $id is empty (= false, null or '') or not present in the backend, a new note must be created.
* Otherwise the present data must be updated.
* Return the ID of the note written or false, depending if storage succeedet
*
* @param int $id ID of the note
* @param int $id Data that should be written
* @return int|false
*/
function storeData($id, $data)
{
$this->_checkConfig();
$return = false;
if (is_array($data)) {
// Check if the note already exists
// no: determine new id and create file
if (!$this->_searchIndexForID($id)) {
// find the next free ID
$id = 0; // where to start from
$max_id = $id; // init max_id
$contents = $this->_getIndex();
if (empty($contents)) {
// no index file exists so far, start with ID=1
$id++;
} else {
// search for highest index number
foreach ($contents as $line_raw) {
$line = split('\|',$line_raw);
$index_id = $line[0];
if ($max_id < $index_id) $max_id = $index_id;
}
$id = $max_id +1;
}
}
// write new (or updated) data
// and update index
$result = $this->_writeNoteData($id, $data);
if ($result !== false) {
$this->_updateIndexFile($id, $data['event_id']);
$return = $id;
}
}
return $return;
}
/**
* This is used to delete this specific note in the backend.
*
* @return true|false
*/
function deleteNote($id)
{
$filename = $this->config['datapath'].'note_'.$id;
$this->_checkConfig();
$this->_updateIndexFile($id, 'delete'); // Delete from index
if (file_exists($filename)) {
return unlink($filename);
} else {
return true;
}
}
/**
* This, optional, method is called at driver loading time if it exists
*
* In this case, we need this feature, because we want to use LCC's
* default data directory to store our data into.
* This cant be set in a custom constructor since we have no access to the
* core object there.
*/
function init() {
/*
* set 'datapath' to datadir of LCC_Core
*/
$datadir = $this->lcc_core->getConfig('data_dir'); // fetch core config
// Correct the path if its not a path - a trailing / is expected
if (substr($datadir,-1,1) != '/') {
$datadir .= '/';
}
// set our default value
$this->setConfig(array('datapath' => $datadir.'filestore/'));
}
/*******************************************************************
* Here come some driver internal methods that make our life easier *
*******************************************************************/
/**
* Verify that the drivers config is loaded and sane
*
* @return true Return true or dies
* @access private
*/
function _checkConfig()
{
if(!is_array($this->config) ||
!isset($this->config['datapath'])) {
die('<b>LCC_NoteDriver_File ERROR:</b> Configuration of driver not found or not valid');
}
// Correct the path if its not a path - a trailing / is expected
if (substr($this->config['datapath'],-1,1) != '/') {
$this->config['datapath'] .= '/';
}
// check that filepath is accessible
if (!is_writable($this->config['datapath'])) {
die('<b>LCC_NoteDriver_File ERROR:</b> Configured datapath "'.$this->config['datapath'].'" is not writable!');
}
return true;
}
/**
* This actually fetches the data from the index file
*
* @todo maybe implement a cache
* @return array Content of the index file (raw data)
* @access private
*/
function _getIndex()
{
$this->_checkConfig();
$contents = array(); // index file is not here, so we assume there are no notes. just return an empty array
$indexfile = $this->config['datapath'].'note_index';
if (file_exists($indexfile)) {
if (!is_readable($indexfile)) {
die('<b>LCC_NoteDriver_File ERROR:</b> Could not open indexfile: '.$indexfile);
} else {
$contents = file($indexfile);
if (!is_array($contents)) {
die('<b>LCC_NoteDriver_File ERROR:</b> Could not get contents of indexfile (but is was here and readable?): '.$indexfile);
}
}
}
return $contents;
}
/**
* Update the index file entry
*
* @param mixed $id ID of the note to be updated
* @param mixed $event_id ID of the event this note belongs to
* @access private
*/
function _updateIndexFile($id, $event_id)
{
$this->_checkConfig();
if ($event_id === 'delete') {
$delete_note = true;
} else {
$delete_note = false;
}
$indexfile = $this->config['datapath'].'note_index';
if (!is_writable($indexfile) && file_exists($indexfile)) {
die('<b>LCC_NoteDriver_File ERROR:</b> Could not open indexfile in write mode: '.$indexfile);
} else {
if ($this->_searchIndexForID($id)) {
// Entry exists, update
// Maybe we have a nice possibility where we can just float over the contents
// and only replace the line neccessary
$contents = $this->_getIndex();
$i_handle = fopen($indexfile, 'w');
flock($i_handle, LOCK_EX);
foreach ($contents as $line) {
if (preg_match('/^'.$id.'\|/', $line)) {
// this is the id we want to update
// if we want to delete this note from index, just dont write it
if (!$delete_note) {
fwrite($i_handle, $id.'|'.$event_id.PHP_EOL);
}
} else {
fwrite($i_handle, $line);
}
}
flock($i_handle, LOCK_UN);
fclose($i_handle);
} else {
// Entry doesn't exist, add
$i_handle = fopen($indexfile, 'a');
if ($i_handle == false) {
die('<b>LCC_NoteDriver_File ERROR:</b> Unable to update indexfile '.$indexfile);
}
flock($i_handle, LOCK_EX);
fwrite($i_handle, $id.'|'.$event_id.PHP_EOL);
flock($i_handle, LOCK_UN);
fclose($i_handle);
}
}
}
/**
* Is there a note with id $id in the index file?
*
* @return boolean
*/
function _searchIndexForID($id) {
$contents = $this->_getIndex();
$return = false; // Index file is empty, so we assume no notes
if ($contents) {
foreach ($contents as $line_raw){
$line = split('\|',$line_raw);
$index_id = $line[0];
if ($id == $index_id) {
$return = true;
break;
}
}
}
return $return;
}
/**
* This writes data to a note file
*
* If the file exists, it will be updatet, if not it will be created.
*
* @param $id
* @param $data associative data array (field => value)
* @return boolean
* @access private
*/
function _writeNoteData($id, $data) {
$this->_checkConfig();
// Construct the filename of the note file
$filename = $this->config['datapath'].'note_'.$id;
if (file_exists($filename) && !is_writable($filename)) {
die('<b>LCC_NoteDriver_File ERROR:</b> Note file "'.$filename.'" is not writable!');
}
// Write to note file
$handle = fopen($filename, 'w');
if ($handle == false) {
die('<b>LCC_NoteDriver_File ERROR:</b> Unable to create new note file '.$filename);
}
flock($handle, LOCK_EX);
// there is a problem with newlines in our flat file, we must encode them
foreach ($data as $field => $value) {
fwrite($handle, $field.'='.str_replace(PHP_EOL, '-§-', $value).PHP_EOL);
}
flock($handle, LOCK_UN);
fclose($handle);
return true;
}
}
?>