implemented zerobin_db model, added more options for paste expiration, made comments and max data size configurable

This commit is contained in:
Simon Rupf 2012-05-19 23:59:41 +02:00
parent 7cee995cd7
commit 421e6cba97
7 changed files with 432 additions and 49 deletions

96
DOCUMENTATION.md Normal file
View file

@ -0,0 +1,96 @@
Documentation
=============
For Administrators
------------------
In the index.php in the main folder you can define a different PATH. This is
useful if you want to secure your installation and want to move the
configuration, data files, templates and PHP libraries (directories cfg, lib
and tpl) outside of your document root. This new location must still be
accessible to your webserver / PHP process.
> ### PATH Example ###
> Your zerobin installation lives in a subfolder called "paste" inside of your
> document root. The URL looks like this:
> http://example.com/paste/
> The ZeroBin folder on your webserver is really:
> /home/example.com/htdocs/paste
>
> When setting the path like this:
> define('PATH', '../../secret/zerobin/');
> ZeroBin will look for your includes here:
> /home/example.com/secret/zerobin
In the file "cfg/conf.ini" you can configure ZeroBin. The config file is
divided into multiple sections, which are enclosed in square brackets. In the
"[main]" section you can enable or disable the discussion feature, set the
limit of stored pastes and comments in bytes. The "[traffic]" section lets you
set a time limit in seconds. Users may not post more often the this limit to
your ZeroBin.
Finally the "[model]" and "[model_options]" sections let you configure your
favourite way of storing the pastes and discussions on your server.
"zerobin_data" is the default model, which stores everything in files in the
data folder. This is the recommended setup for low traffic sites. Under high
load, in distributed setups or if you are not allowed to store files locally,
you might want to switch to the "zerobin_db" model. This lets you store your
data in a database. Basically all databases, that are supported by PDO (PHP
data objects) may be used. Automatic table creation is provided for pdo_ibm,
pdo_informix, pdo_mssql, pdo_mysql, pdo_oci, pdo_pgsql and pdo_sqlite. You may
want to provide a table prefix, if you have to share the zerobin database with
another application. The table prefix option is called "tbl".
> ### Note ###
> The "zerobin_db" model has only been tested with sqlite and MySQL, although
> it would not be recommended to use sqlite in a production environment. If you
> gain any experience running ZeroBin on other RDBMS, let us know.
For reference or if you want to create the table schema for yourself:
CREATE TABLE prefix_paste (
dataid CHAR(16),
data TEXT,
postdate INT,
expiredate INT,
opendiscussion INT,
burnafterreading INT
);
CREATE TABLE prefix_comment (
dataid CHAR(16),
pasteid CHAR(16),
parentid CHAR(16),
data TEXT,
nickname VARCHAR(255),
vizhash TEXT,
postdate INT
);
For Developers
--------------
If you want to create your own data models, you might want to know how the arrays, that you have to store, look like:
public function create($pasteid, $paste)
{
$pasteid = substr(hash('md5', $paste['data']), 0, 16);
$paste['data'] // text
$paste['meta']['postdate'] // int UNIX timestamp
$paste['meta']['expire_date'] // int UNIX timestamp
$paste['meta']['opendiscussion'] // true (if false it is unset)
$paste['meta']['burnafterreading'] // true (if false it is unset; if true, then opendiscussion is unset)
}
public function createComment($pasteid, $parentid, $commentid, $comment)
{
$pasteid // the id of the paste this comment belongs to
$parentid // the id of the parent of this comment, may be the paste id itself
$commentid = substr(hash('md5', $paste['data']), 0, 16);
$paste['data'] // text
$paste['meta']['nickname'] // text or null (if anonymous)
$paste['meta']['vizhash'] // text or null (if anonymous)
$paste['meta']['postdate'] // int UNIX timestamp
}

View file

@ -7,28 +7,41 @@
; @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
; @version 0.15
; time limit between calls from the same IP address in seconds
traffic_limit = 10
traffic_dir = PATH "data"
[main]
; enable or disable discussions
opendiscussion = true
; size limit per paste or comment in bytes
size_limit = 2000000
sizelimit = 2097152
[traffic]
; time limit between calls from the same IP address in seconds
limit = 10
dir = PATH "data"
[model]
; name of data model class to load and directory for storage
; the default model "zerobin_data" stores everything in the filesystem
model = zerobin_data
model_options["dir"] = PATH "data"
class = zerobin_data
[model_options]
dir = PATH "data"
;[model]
; example of DB configuration for MySQL
;model = zerobin_db
;model_options["dsn"] = "mysql:host=localhost;dbname=zerobin"
;model_options["usr"] = "zerobin"
;model_options["pwd"] = "Z3r0P4ss"
;model_options["opt"][PDO::ATTR_PERSISTENT] = true
;class = zerobin_db
;[model_options]
;dsn = "mysql:host=localhost;dbname=zerobin;charset=UTF8"
;tbl = "zerobin_" ; table prefix
;usr = "zerobin"
;pwd = "Z3r0P4ss"
;opt[12] = true ; PDO::ATTR_PERSISTENT
;[model]
; example of DB configuration for SQLite
;model = zerobin_db
;model_options["dsn"] = "sqlite:" PATH "data"/db.sq3"
;model_options["usr"] = null
;model_options["pwd"] = null
;model_options["opt"] = null
;[model_options]
;class = zerobin_db
;dsn = "sqlite:" PATH "data/db.sq3"
;usr = null
;pwd = null
;opt[12] = true ; PDO::ATTR_PERSISTENT

View file

@ -103,8 +103,8 @@ class zerobin
);
}
$this->_conf = parse_ini_file(PATH . 'cfg/conf.ini');
$this->_model = $this->_conf['model'];
$this->_conf = parse_ini_file(PATH . 'cfg/conf.ini', true);
$this->_model = $this->_conf['model']['class'];
}
/**
@ -117,7 +117,10 @@ class zerobin
{
// if needed, initialize the model
if(is_string($this->_model)) {
$this->_model = forward_static_call(array($this->_model, 'getInstance'), $this->_conf['model_options']);
$this->_model = forward_static_call(
array($this->_model, 'getInstance'),
$this->_conf['model_options']
);
}
return $this->_model;
}
@ -129,7 +132,7 @@ class zerobin
* data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct)
*
* All optional data will go to meta information:
* expire (optional) = expiration delay (never,10min,1hour,1day,1month,1year,burn) (default:never)
* expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
* opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
* nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct)
* parentid (optional) = in discussion, which comment this comment replies to.
@ -143,18 +146,30 @@ class zerobin
header('Content-type: application/json');
$error = false;
// Make sure last paste from the IP address was more than 10 seconds ago.
trafficlimiter::setLimit($this->_conf['traffic_limit']);
trafficlimiter::setPath($this->_conf['traffic_dir']);
// Make sure last paste from the IP address was more than X seconds ago.
trafficlimiter::setLimit($this->_conf['traffic']['limit']);
trafficlimiter::setPath($this->_conf['traffic']['dir']);
if (
!trafficlimiter::canPass($_SERVER['REMOTE_ADDR'])
) $this->_return_message(1, 'Please wait 10 seconds between each post.');
) $this->_return_message(
1,
'Please wait ' .
$this->_conf['traffic']['limit'] .
' seconds between each post.'
);
// Make sure content is not too big.
$data = $_POST['data'];
if (
strlen($data) > 2000000
) $this->_return_message(1, 'Paste is limited to 2 MB of encrypted data.');
strlen($data) > $this->_conf['main']['sizelimit']
) $this->_return_message(
1,
'Paste is limited to ' .
$this->_conf['main']['sizelimit'] .
' ' .
filter::size_humanreadable($this->_conf['main']['sizelimit']) .
' of encrypted data.'
);
// Make sure format is correct.
if (!sjcl::isValid($data)) $this->_return_message(1, 'Invalid data.');
@ -167,6 +182,12 @@ class zerobin
{
switch ($_POST['expire'])
{
case 'burn':
$meta['burnafterreading'] = true;
break;
case '5min':
$meta['expire_date'] = time()+5*60;
break;
case '10min':
$meta['expire_date'] = time()+10*60;
break;
@ -176,19 +197,19 @@ class zerobin
case '1day':
$meta['expire_date'] = time()+24*60*60;
break;
case '1week':
$meta['expire_date'] = time()+7*24*60*60;
break;
case '1month':
$meta['expire_date'] = strtotime('+1 month');
break;
case '1year':
$meta['expire_date'] = strtotime('+1 year');
break;
case 'burn':
$meta['burnafterreading'] = true;
}
}
// Read open discussion flag.
if (!empty($_POST['opendiscussion']))
if ($this->_conf['main']['opendiscussion'] && !empty($_POST['opendiscussion']))
{
$opendiscussion = $_POST['opendiscussion'];
if ($opendiscussion != 0)
@ -381,6 +402,7 @@ class zerobin
// We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
$page->assign('CIPHERDATA', htmlspecialchars($this->_data, ENT_NOQUOTES));
$page->assign('ERRORMESSAGE', $this->_error);
$page->assign('OPENDISCUSSION', $this->_conf['main']['opendiscussion']);
$page->assign('VERSION', self::VERSION);
$page->draw('page');
}

View file

@ -49,7 +49,7 @@ abstract class zerobin_abstract
*
* @access public
* @static
* @return zerobin
* @return zerobin_abstract
*/
abstract public static function getInstance($options);

View file

@ -29,9 +29,9 @@ class zerobin_data extends zerobin_abstract
*
* @access public
* @static
* @return zerobin
* @return zerobin_data
*/
public static function getInstance($options)
public static function getInstance($options = null)
{
// if given update the data directory
if (

View file

@ -17,6 +17,13 @@
*/
class zerobin_db extends zerobin_abstract
{
/*
* @access private
* @static
* @var array to cache select queries
*/
private static $_cache = array();
/*
* @access private
* @static
@ -24,31 +31,137 @@ class zerobin_db extends zerobin_abstract
*/
private static $_db;
/*
* @access private
* @static
* @var string table prefix
*/
private static $_prefix = '';
/*
* @access private
* @static
* @var string database type
*/
private static $_type = '';
/**
* get instance of singleton
*
* @access public
* @static
* @return zerobin
* @throws Exception
* @return zerobin_db
*/
public static function getInstance($options)
public static function getInstance($options = null)
{
// if needed initialize the singleton
if(null === self::$_instance) {
parent::$_instance = new self;
}
if (
is_array($options) &&
array_key_exists('dsn', $options) &&
array_key_exists('usr', $options) &&
array_key_exists('pwd', $options) &&
array_key_exists('opt', $options)
) self::$_db = new PDO(
$options['dsn'],
$options['usr'],
$options['pwd'],
$options['opt']
);
if (is_array($options))
{
// set table prefix if given
if (array_key_exists('tbl', $options)) self::$_prefix = $options['tbl'];
// initialize the db connection with new options
if (
array_key_exists('dsn', $options) &&
array_key_exists('usr', $options) &&
array_key_exists('pwd', $options) &&
array_key_exists('opt', $options)
)
{
self::$_db = new PDO(
$options['dsn'],
$options['usr'],
$options['pwd'],
$options['opt']
);
// check if the database contains the required tables
self::$_type = strtolower(
substr($options['dsn'], 0, strpos($options['dsn'], ':'))
);
switch(self::$_type)
{
case 'ibm':
$sql = 'SELECT tabname FROM SYSCAT.TABLES ';
break;
case 'informix':
$sql = 'SELECT tabname FROM systables ';
break;
case 'mssql':
$sql = "SELECT name FROM sysobjects "
. "WHERE type = 'U' ORDER BY name";
break;
case 'mysql':
$sql = 'SHOW TABLES';
break;
case 'oci':
$sql = 'SELECT table_name FROM all_tables';
break;
case 'pgsql':
$sql = "SELECT c.relname AS table_name "
. "FROM pg_class c, pg_user u "
. "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND c.relname !~ '^(pg_|sql_)' "
. "UNION "
. "SELECT c.relname AS table_name "
. "FROM pg_class c "
. "WHERE c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
. "AND c.relname !~ '^pg_'";
break;
case 'sqlite':
$sql = "SELECT name FROM sqlite_master WHERE type='table' "
. "UNION ALL SELECT name FROM sqlite_temp_master "
. "WHERE type='table' ORDER BY name";
break;
default:
throw new Exception(
'PDO type ' .
self::$_type .
' is currently not supported.'
);
}
$statement = self::$_db->query($sql);
$tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if needed
if (!array_key_exists(self::$_prefix . 'paste', $tables))
{
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
'dataid CHAR(16), ' .
'data TEXT, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT );'
);
}
// create comment table if needed
if (!array_key_exists(self::$_prefix . 'comment', $tables))
{
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
'dataid CHAR(16), ' .
'pasteid CHAR(16), ' .
'parentid CHAR(16), ' .
'data TEXT, ' .
'nickname VARCHAR(255), ' .
'vizhash TEXT, ' .
'postdate INT );'
);
}
}
}
return parent::$_instance;
}
@ -58,10 +171,27 @@ class zerobin_db extends zerobin_abstract
* @access public
* @param string $pasteid
* @param array $paste
* @return int|false
* @return bool
*/
public function create($pasteid, $paste)
{
if (
!array_key_exists('opendiscussion', $paste['meta'])
) $paste['meta']['opendiscussion'] = false;
if (
!array_key_exists('burnafterreading', $paste['meta'])
) $paste['meta']['burnafterreading'] = false;
return self::_exec(
'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?)',
array(
$pasteid,
$paste['data'],
$paste['meta']['postdate'],
$paste['meta']['expire_date'],
(int) $paste['meta']['opendiscussion'],
(int) $paste['meta']['burnafterreading'],
)
);
}
/**
@ -73,6 +203,27 @@ class zerobin_db extends zerobin_abstract
*/
public function read($pasteid)
{
if (
!array_key_exists($pasteid, self::$_cache)
) self::$_cache[$pasteid] = self::_select(
'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
array($pasteid), true
);
// create object
$paste = new stdClass;
$paste->data = self::$_cache[$pasteid]['data'];
$paste->meta = new stdClass;
$paste->meta->postdate = (int) self::$_cache[$pasteid]['postdate'];
$paste->meta->expire_date = (int) self::$_cache[$pasteid]['expiredate'];
if (
self::$_cache[$pasteid]['opendiscussion']
) $paste->meta->opendiscussion = true;
if (
self::$_cache[$pasteid]['burnafterreading']
) $paste->meta->burnafterreading = true;
return $paste;
}
/**
@ -84,6 +235,14 @@ class zerobin_db extends zerobin_abstract
*/
public function delete($pasteid)
{
self::_exec(
'DELETE FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
array($pasteid)
);
self::_exec(
'DELETE FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
array($pasteid)
);
}
/**
@ -95,6 +254,13 @@ class zerobin_db extends zerobin_abstract
*/
public function exists($pasteid)
{
if (
!array_key_exists($pasteid, self::$_cache)
) self::$_cache[$pasteid] = self::_select(
'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
array($pasteid), true
);
return (bool) self::$_cache[$pasteid];
}
/**
@ -109,6 +275,18 @@ class zerobin_db extends zerobin_abstract
*/
public function createComment($pasteid, $parentid, $commentid, $comment)
{
return self::_exec(
'INSERT INTO ' . self::$_prefix . 'comment VALUES(?,?,?,?,?,?,?)',
array(
$pasteid,
$parentid,
$commentid,
$comment['data'],
$comment['meta']['nickname'],
$comment['meta']['vizhash'],
$comment['meta']['postdate'],
)
);
}
/**
@ -120,6 +298,33 @@ class zerobin_db extends zerobin_abstract
*/
public function readComments($pasteid)
{
$rows = self::_select(
'SELECT * FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
array($pasteid)
);
// create object
$commentTemplate = new stdClass;
$commentTemplate->meta = new stdClass;
// create comment list
$comments = array();
if (count($rows))
{
foreach ($rows as $row)
{
$i = (int) $row['postdate'];
$comments[$i] = clone $commentTemplate;
$comments[$i]->data = $row['data'];
$comments[$i]->meta->nickname = $row['nickname'];
$comments[$i]->meta->vizhash = $row['vizhash'];
$comments[$i]->meta->postdate = $i;
$comments[$i]->meta->commentid = $row['dataid'];
$comments[$i]->meta->parentid = $row['parentid'];
}
ksort($comments);
}
return $comments;
}
/**
@ -133,5 +338,50 @@ class zerobin_db extends zerobin_abstract
*/
public function existsComment($pasteid, $parentid, $commentid)
{
return (bool) self::_select(
'SELECT dataid FROM ' . self::$_prefix . 'comment ' .
'WHERE pasteid = ? AND parentid = ? AND dataid = ?',
array($pasteid, $parentid, $commentid), true
);
}
/**
* execute a statement
*
* @access private
* @static
* @param string $sql
* @param array $params
* @throws PDOException
* @return array
*/
private static function _exec($sql, array $params)
{
$statement = self::$_db->prepare($sql);
$result = $statement->execute($params);
$statement->closeCursor();
return $result;
}
/**
* run a select statement
*
* @access private
* @static
* @param string $sql
* @param array $params
* @param bool $firstOnly if only the first row should be returned
* @throws PDOException
* @return array
*/
private static function _select($sql, array $params, $firstOnly = false)
{
$statement = self::$_db->prepare($sql);
$statement->execute($params);
$result = $firstOnly ?
$statement->fetch(PDO::FETCH_ASSOC) :
$statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
return $result;
}
}

View file

@ -48,9 +48,11 @@
<div id="expiration" style="display:none;">Expire:
<select id="pasteExpiration" name="pasteExpiration">
<option value="burn">Burn after reading</option>
<option value="5min">5 minutes</option>
<option value="10min">10 minutes</option>
<option value="1hour">1 hour</option>
<option value="1day">1 day</option>
<option value="1week">1 week</option>
<option value="1month" selected="selected">1 month</option>
<option value="1year">1 year</option>
<option value="never">Never</option>
@ -67,7 +69,7 @@
</div>
<input id="password" value="Optional password..." style="display:none;" />
<div id="opendisc" class="button" style="display:none;">
<input type="checkbox" id="opendiscussion" name="opendiscussion" />
<input type="checkbox" id="opendiscussion" name="opendiscussion" {if="!$OPENDISCUSSION"} disabled="disabled"{/if} />
<label for="opendiscussion">Open discussion</label>
</div>
</div>