Introduce PSR-4 autoloading
This commit is contained in:
176
lib/data/AbstractData.php
Normal file
176
lib/data/AbstractData.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
* a zero-knowledge paste bin
|
||||
*
|
||||
* @link https://github.com/PrivateBin/PrivateBin
|
||||
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
* @version 0.22
|
||||
*/
|
||||
|
||||
namespace PrivateBin\data;
|
||||
|
||||
/**
|
||||
* privatebin_abstract
|
||||
*
|
||||
* Abstract model for PrivateBin data access, implemented as a singleton.
|
||||
*/
|
||||
abstract class AbstractData
|
||||
{
|
||||
/**
|
||||
* singleton instance
|
||||
*
|
||||
* @access protected
|
||||
* @static
|
||||
* @var privatebin_abstract
|
||||
*/
|
||||
protected static $_instance = null;
|
||||
|
||||
/**
|
||||
* enforce singleton, disable constructor
|
||||
*
|
||||
* Instantiate using {@link getInstance()}, privatebin is a singleton object.
|
||||
*
|
||||
* @access protected
|
||||
*/
|
||||
protected function __construct() {}
|
||||
|
||||
/**
|
||||
* enforce singleton, disable cloning
|
||||
*
|
||||
* Instantiate using {@link getInstance()}, privatebin is a singleton object.
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* get instance of singleton
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @return privatebin_abstract
|
||||
*/
|
||||
public static function getInstance($options) {}
|
||||
|
||||
/**
|
||||
* Create a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param array $paste
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function create($pasteid, $paste);
|
||||
|
||||
/**
|
||||
* Read a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return stdClass|false
|
||||
*/
|
||||
abstract public function read($pasteid);
|
||||
|
||||
/**
|
||||
* Delete a paste and its discussion.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return void
|
||||
*/
|
||||
abstract public function delete($pasteid);
|
||||
|
||||
/**
|
||||
* Test if a paste exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function exists($pasteid);
|
||||
|
||||
/**
|
||||
* Create a comment in a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @param array $comment
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function createComment($pasteid, $parentid, $commentid, $comment);
|
||||
|
||||
/**
|
||||
* Read all comments of paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return array
|
||||
*/
|
||||
abstract public function readComments($pasteid);
|
||||
|
||||
/**
|
||||
* Test if a comment exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @return void
|
||||
*/
|
||||
abstract public function existsComment($pasteid, $parentid, $commentid);
|
||||
|
||||
/**
|
||||
* Returns up to batch size number of paste ids that have expired
|
||||
*
|
||||
* @access protected
|
||||
* @param int $batchsize
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function _getExpiredPastes($batchsize);
|
||||
|
||||
/**
|
||||
* Perform a purge of old pastes, at most the given batchsize is deleted.
|
||||
*
|
||||
* @access public
|
||||
* @param int $batchsize
|
||||
* @return void
|
||||
*/
|
||||
public function purge($batchsize)
|
||||
{
|
||||
if ($batchsize < 1) return;
|
||||
$pastes = $this->_getExpiredPastes($batchsize);
|
||||
if (count($pastes))
|
||||
{
|
||||
foreach ($pastes as $pasteid)
|
||||
{
|
||||
$this->delete($pasteid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next free slot for comment from postdate.
|
||||
*
|
||||
* @access public
|
||||
* @param array $comments
|
||||
* @param int|string $postdate
|
||||
* @return int|string
|
||||
*/
|
||||
protected function getOpenSlot(&$comments, $postdate)
|
||||
{
|
||||
if (array_key_exists($postdate, $comments))
|
||||
{
|
||||
$parts = explode('.', $postdate, 2);
|
||||
if (!array_key_exists(1, $parts)) $parts[1] = 0;
|
||||
++$parts[1];
|
||||
return $this->getOpenSlot($comments, implode('.', $parts));
|
||||
}
|
||||
return $postdate;
|
||||
}
|
||||
}
|
||||
362
lib/data/data.php
Normal file
362
lib/data/data.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
* a zero-knowledge paste bin
|
||||
*
|
||||
* @link https://github.com/PrivateBin/PrivateBin
|
||||
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
* @version 0.22
|
||||
*/
|
||||
|
||||
namespace PrivateBin\data;
|
||||
|
||||
/**
|
||||
* privatebin_data
|
||||
*
|
||||
* Model for data access, implemented as a singleton.
|
||||
*/
|
||||
class data extends AbstractData
|
||||
{
|
||||
/**
|
||||
* directory where data is stored
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string
|
||||
*/
|
||||
private static $_dir = 'data/';
|
||||
|
||||
/**
|
||||
* get instance of singleton
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @return privatebin_data
|
||||
*/
|
||||
public static function getInstance($options = null)
|
||||
{
|
||||
// if given update the data directory
|
||||
if (
|
||||
is_array($options) &&
|
||||
array_key_exists('dir', $options)
|
||||
) self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
|
||||
// if needed initialize the singleton
|
||||
if (!(self::$_instance instanceof privatebin_data)) {
|
||||
self::$_instance = new self;
|
||||
self::_init();
|
||||
}
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param array $paste
|
||||
* @return bool
|
||||
*/
|
||||
public function create($pasteid, $paste)
|
||||
{
|
||||
$storagedir = self::_dataid2path($pasteid);
|
||||
if (is_file($storagedir . $pasteid)) return false;
|
||||
if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
|
||||
return (bool) @file_put_contents($storagedir . $pasteid, json_encode($paste));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return stdClass|false
|
||||
*/
|
||||
public function read($pasteid)
|
||||
{
|
||||
if (!$this->exists($pasteid)) return false;
|
||||
$paste = json_decode(
|
||||
file_get_contents(self::_dataid2path($pasteid) . $pasteid)
|
||||
);
|
||||
if (property_exists($paste->meta, 'attachment'))
|
||||
{
|
||||
$paste->attachment = $paste->meta->attachment;
|
||||
unset($paste->meta->attachment);
|
||||
if (property_exists($paste->meta, 'attachmentname'))
|
||||
{
|
||||
$paste->attachmentname = $paste->meta->attachmentname;
|
||||
unset($paste->meta->attachmentname);
|
||||
}
|
||||
}
|
||||
return $paste;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a paste and its discussion.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return void
|
||||
*/
|
||||
public function delete($pasteid)
|
||||
{
|
||||
// Delete the paste itself.
|
||||
@unlink(self::_dataid2path($pasteid) . $pasteid);
|
||||
|
||||
// Delete discussion if it exists.
|
||||
$discdir = self::_dataid2discussionpath($pasteid);
|
||||
if (is_dir($discdir))
|
||||
{
|
||||
// Delete all files in discussion directory
|
||||
$dir = dir($discdir);
|
||||
while (false !== ($filename = $dir->read()))
|
||||
{
|
||||
if (is_file($discdir . $filename)) @unlink($discdir . $filename);
|
||||
}
|
||||
$dir->close();
|
||||
|
||||
// Delete the discussion directory.
|
||||
@rmdir($discdir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a paste exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return void
|
||||
*/
|
||||
public function exists($pasteid)
|
||||
{
|
||||
return is_file(self::_dataid2path($pasteid) . $pasteid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a comment in a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @param array $comment
|
||||
* @return bool
|
||||
*/
|
||||
public function createComment($pasteid, $parentid, $commentid, $comment)
|
||||
{
|
||||
$storagedir = self::_dataid2discussionpath($pasteid);
|
||||
$filename = $pasteid . '.' . $commentid . '.' . $parentid;
|
||||
if (is_file($storagedir . $filename)) return false;
|
||||
if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
|
||||
return (bool) @file_put_contents($storagedir . $filename, json_encode($comment));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all comments of paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return array
|
||||
*/
|
||||
public function readComments($pasteid)
|
||||
{
|
||||
$comments = array();
|
||||
$discdir = self::_dataid2discussionpath($pasteid);
|
||||
if (is_dir($discdir))
|
||||
{
|
||||
// Delete all files in discussion directory
|
||||
$dir = dir($discdir);
|
||||
while (false !== ($filename = $dir->read()))
|
||||
{
|
||||
// Filename is in the form pasteid.commentid.parentid:
|
||||
// - pasteid is the paste this reply belongs to.
|
||||
// - commentid is the comment identifier itself.
|
||||
// - parentid is the comment this comment replies to (It can be pasteid)
|
||||
if (is_file($discdir . $filename))
|
||||
{
|
||||
$comment = json_decode(file_get_contents($discdir . $filename));
|
||||
$items = explode('.', $filename);
|
||||
// Add some meta information not contained in file.
|
||||
$comment->id = $items[1];
|
||||
$comment->parentid = $items[2];
|
||||
|
||||
// Store in array
|
||||
$key = $this->getOpenSlot($comments, (int) $comment->meta->postdate);
|
||||
$comments[$key] = $comment;
|
||||
}
|
||||
}
|
||||
$dir->close();
|
||||
|
||||
// Sort comments by date, oldest first.
|
||||
ksort($comments);
|
||||
}
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a comment exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @return void
|
||||
*/
|
||||
public function existsComment($pasteid, $parentid, $commentid)
|
||||
{
|
||||
return is_file(
|
||||
self::_dataid2discussionpath($pasteid) .
|
||||
$pasteid . '.' . $commentid . '.' . $parentid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns up to batch size number of paste ids that have expired
|
||||
*
|
||||
* @access private
|
||||
* @param int $batchsize
|
||||
* @return array
|
||||
*/
|
||||
protected function _getExpiredPastes($batchsize)
|
||||
{
|
||||
$pastes = array();
|
||||
$firstLevel = array_filter(
|
||||
scandir(self::$_dir),
|
||||
array('self', '_isFirstLevelDir')
|
||||
);
|
||||
if (count($firstLevel) > 0)
|
||||
{
|
||||
// try at most 10 times the $batchsize pastes before giving up
|
||||
for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i)
|
||||
{
|
||||
$firstKey = array_rand($firstLevel);
|
||||
$secondLevel = array_filter(
|
||||
scandir(self::$_dir . $firstLevel[$firstKey]),
|
||||
array('self', '_isSecondLevelDir')
|
||||
);
|
||||
|
||||
// skip this folder in the next checks if it is empty
|
||||
if (count($secondLevel) == 0)
|
||||
{
|
||||
unset($firstLevel[$firstKey]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$secondKey = array_rand($secondLevel);
|
||||
$path = self::$_dir . $firstLevel[$firstKey] .
|
||||
DIRECTORY_SEPARATOR . $secondLevel[$secondKey];
|
||||
if (!is_dir($path)) continue;
|
||||
$thirdLevel = array_filter(
|
||||
scandir($path),
|
||||
array('PrivateBin\\model\\paste', 'isValidId')
|
||||
);
|
||||
if (count($thirdLevel) == 0) continue;
|
||||
$thirdKey = array_rand($thirdLevel);
|
||||
$pasteid = $thirdLevel[$thirdKey];
|
||||
if (in_array($pasteid, $pastes)) continue;
|
||||
|
||||
if ($this->exists($pasteid))
|
||||
{
|
||||
$data = $this->read($pasteid);
|
||||
if (
|
||||
property_exists($data->meta, 'expire_date') &&
|
||||
$data->meta->expire_date < time()
|
||||
)
|
||||
{
|
||||
$pastes[] = $pasteid;
|
||||
if (count($pastes) >= $batchsize) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pastes;
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize privatebin
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @return void
|
||||
*/
|
||||
private static function _init()
|
||||
{
|
||||
// Create storage directory if it does not exist.
|
||||
if (!is_dir(self::$_dir)) mkdir(self::$_dir, 0705);
|
||||
// Create .htaccess file if it does not exist.
|
||||
if (!is_file(self::$_dir . '.htaccess'))
|
||||
{
|
||||
file_put_contents(
|
||||
self::$_dir . '.htaccess',
|
||||
'Allow from none' . PHP_EOL .
|
||||
'Deny from all' . PHP_EOL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert paste id to storage path.
|
||||
*
|
||||
* The idea is to creates subdirectories in order to limit the number of files per directory.
|
||||
* (A high number of files in a single directory can slow things down.)
|
||||
* eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
|
||||
* High-trafic websites may want to deepen the directory structure (like Squid does).
|
||||
*
|
||||
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $dataid
|
||||
* @return void
|
||||
*/
|
||||
private static function _dataid2path($dataid)
|
||||
{
|
||||
return self::$_dir . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
|
||||
substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert paste id to discussion storage path.
|
||||
*
|
||||
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $dataid
|
||||
* @return void
|
||||
*/
|
||||
private static function _dataid2discussionpath($dataid)
|
||||
{
|
||||
return self::_dataid2path($dataid) . $dataid .
|
||||
'.discussion' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the given element is a valid first level directory.
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $element
|
||||
* @return bool
|
||||
*/
|
||||
private static function _isFirstLevelDir($element)
|
||||
{
|
||||
return self::_isSecondLevelDir($element) &&
|
||||
is_dir(self::$_dir . DIRECTORY_SEPARATOR . $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the given element is a valid second level directory.
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $element
|
||||
* @return bool
|
||||
*/
|
||||
private static function _isSecondLevelDir($element)
|
||||
{
|
||||
return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
|
||||
}
|
||||
}
|
||||
719
lib/data/db.php
Normal file
719
lib/data/db.php
Normal file
@@ -0,0 +1,719 @@
|
||||
<?php
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
* a zero-knowledge paste bin
|
||||
*
|
||||
* @link https://github.com/PrivateBin/PrivateBin
|
||||
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
* @version 0.22
|
||||
*/
|
||||
|
||||
namespace PrivateBin\data;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PrivateBin\privatebin;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* privatebin_db
|
||||
*
|
||||
* Model for DB access, implemented as a singleton.
|
||||
*/
|
||||
class db extends AbstractData
|
||||
{
|
||||
/**
|
||||
* cache for select queries
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $_cache = array();
|
||||
|
||||
/**
|
||||
* instance of database connection
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var PDO
|
||||
*/
|
||||
private static $_db;
|
||||
|
||||
/**
|
||||
* table prefix
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string
|
||||
*/
|
||||
private static $_prefix = '';
|
||||
|
||||
/**
|
||||
* database type
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string
|
||||
*/
|
||||
private static $_type = '';
|
||||
|
||||
/**
|
||||
* get instance of singleton
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @throws Exception
|
||||
* @return privatebin_db
|
||||
*/
|
||||
public static function getInstance($options = null)
|
||||
{
|
||||
// if needed initialize the singleton
|
||||
if(!(self::$_instance instanceof privatebin_db)) {
|
||||
self::$_instance = new self;
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
{
|
||||
// set default options
|
||||
$options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
|
||||
$options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
|
||||
$options['opt'][PDO::ATTR_PERSISTENT] = true;
|
||||
$db_tables_exist = true;
|
||||
|
||||
// setup type and dabase connection
|
||||
self::$_type = strtolower(
|
||||
substr($options['dsn'], 0, strpos($options['dsn'], ':'))
|
||||
);
|
||||
$tableQuery = self::_getTableQuery(self::$_type);
|
||||
self::$_db = new PDO(
|
||||
$options['dsn'],
|
||||
$options['usr'],
|
||||
$options['pwd'],
|
||||
$options['opt']
|
||||
);
|
||||
|
||||
// check if the database contains the required tables
|
||||
$tables = self::$_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
|
||||
// create paste table if necessary
|
||||
if (!in_array(self::_sanitizeIdentifier('paste'), $tables))
|
||||
{
|
||||
self::_createPasteTable();
|
||||
$db_tables_exist = false;
|
||||
}
|
||||
|
||||
// create comment table if necessary
|
||||
if (!in_array(self::_sanitizeIdentifier('comment'), $tables))
|
||||
{
|
||||
self::_createCommentTable();
|
||||
$db_tables_exist = false;
|
||||
}
|
||||
|
||||
// create config table if necessary
|
||||
$db_version = privatebin::VERSION;
|
||||
if (!in_array(self::_sanitizeIdentifier('config'), $tables))
|
||||
{
|
||||
self::_createConfigTable();
|
||||
// if we only needed to create the config table, the DB is older then 0.22
|
||||
if ($db_tables_exist) $db_version = '0.21';
|
||||
}
|
||||
else
|
||||
{
|
||||
$db_version = self::_getConfig('VERSION');
|
||||
}
|
||||
|
||||
// update database structure if necessary
|
||||
if (version_compare($db_version, privatebin::VERSION, '<'))
|
||||
{
|
||||
self::_upgradeDatabase($db_version);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(
|
||||
'Missing configuration for key dsn, usr, pwd or opt in the section model_options, please check your configuration file', 6
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param array $paste
|
||||
* @return bool
|
||||
*/
|
||||
public function create($pasteid, $paste)
|
||||
{
|
||||
if (
|
||||
array_key_exists($pasteid, self::$_cache)
|
||||
) {
|
||||
if(false !== self::$_cache[$pasteid]) {
|
||||
return false;
|
||||
} else {
|
||||
unset(self::$_cache[$pasteid]);
|
||||
}
|
||||
}
|
||||
|
||||
$opendiscussion = $burnafterreading = false;
|
||||
$attachment = $attachmentname = '';
|
||||
$meta = $paste['meta'];
|
||||
unset($meta['postdate']);
|
||||
$expire_date = 0;
|
||||
if (array_key_exists('expire_date', $paste['meta']))
|
||||
{
|
||||
$expire_date = (int) $paste['meta']['expire_date'];
|
||||
unset($meta['expire_date']);
|
||||
}
|
||||
if (array_key_exists('opendiscussion', $paste['meta']))
|
||||
{
|
||||
$opendiscussion = (bool) $paste['meta']['opendiscussion'];
|
||||
unset($meta['opendiscussion']);
|
||||
}
|
||||
if (array_key_exists('burnafterreading', $paste['meta']))
|
||||
{
|
||||
$burnafterreading = (bool) $paste['meta']['burnafterreading'];
|
||||
unset($meta['burnafterreading']);
|
||||
}
|
||||
if (array_key_exists('attachment', $paste['meta']))
|
||||
{
|
||||
$attachment = $paste['meta']['attachment'];
|
||||
unset($meta['attachment']);
|
||||
}
|
||||
if (array_key_exists('attachmentname', $paste['meta']))
|
||||
{
|
||||
$attachmentname = $paste['meta']['attachmentname'];
|
||||
unset($meta['attachmentname']);
|
||||
}
|
||||
return self::_exec(
|
||||
'INSERT INTO ' . self::_sanitizeIdentifier('paste') .
|
||||
' VALUES(?,?,?,?,?,?,?,?,?)',
|
||||
array(
|
||||
$pasteid,
|
||||
$paste['data'],
|
||||
$paste['meta']['postdate'],
|
||||
$expire_date,
|
||||
(int) $opendiscussion,
|
||||
(int) $burnafterreading,
|
||||
json_encode($meta),
|
||||
$attachment,
|
||||
$attachmentname,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return stdClass|false
|
||||
*/
|
||||
public function read($pasteid)
|
||||
{
|
||||
if (
|
||||
!array_key_exists($pasteid, self::$_cache)
|
||||
) {
|
||||
self::$_cache[$pasteid] = false;
|
||||
$paste = self::_select(
|
||||
'SELECT * FROM ' . self::_sanitizeIdentifier('paste') .
|
||||
' WHERE dataid = ?', array($pasteid), true
|
||||
);
|
||||
|
||||
if(false !== $paste) {
|
||||
// create object
|
||||
self::$_cache[$pasteid] = new stdClass;
|
||||
self::$_cache[$pasteid]->data = $paste['data'];
|
||||
|
||||
$meta = json_decode($paste['meta']);
|
||||
if (!is_object($meta)) $meta = new stdClass;
|
||||
|
||||
// support older attachments
|
||||
if (property_exists($meta, 'attachment'))
|
||||
{
|
||||
self::$_cache[$pasteid]->attachment = $meta->attachment;
|
||||
unset($meta->attachment);
|
||||
if (property_exists($meta, 'attachmentname'))
|
||||
{
|
||||
self::$_cache[$pasteid]->attachmentname = $meta->attachmentname;
|
||||
unset($meta->attachmentname);
|
||||
}
|
||||
}
|
||||
// support current attachments
|
||||
elseif (array_key_exists('attachment', $paste) && strlen($paste['attachment']))
|
||||
{
|
||||
self::$_cache[$pasteid]->attachment = $paste['attachment'];
|
||||
if (array_key_exists('attachmentname', $paste) && strlen($paste['attachmentname']))
|
||||
{
|
||||
self::$_cache[$pasteid]->attachmentname = $paste['attachmentname'];
|
||||
}
|
||||
}
|
||||
self::$_cache[$pasteid]->meta = $meta;
|
||||
self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
|
||||
$expire_date = (int) $paste['expiredate'];
|
||||
if (
|
||||
$expire_date > 0
|
||||
) self::$_cache[$pasteid]->meta->expire_date = $expire_date;
|
||||
if (
|
||||
$paste['opendiscussion']
|
||||
) self::$_cache[$pasteid]->meta->opendiscussion = true;
|
||||
if (
|
||||
$paste['burnafterreading']
|
||||
) self::$_cache[$pasteid]->meta->burnafterreading = true;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$_cache[$pasteid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a paste and its discussion.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return void
|
||||
*/
|
||||
public function delete($pasteid)
|
||||
{
|
||||
self::_exec(
|
||||
'DELETE FROM ' . self::_sanitizeIdentifier('paste') .
|
||||
' WHERE dataid = ?', array($pasteid)
|
||||
);
|
||||
self::_exec(
|
||||
'DELETE FROM ' . self::_sanitizeIdentifier('comment') .
|
||||
' WHERE pasteid = ?', array($pasteid)
|
||||
);
|
||||
if (
|
||||
array_key_exists($pasteid, self::$_cache)
|
||||
) unset(self::$_cache[$pasteid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a paste exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return void
|
||||
*/
|
||||
public function exists($pasteid)
|
||||
{
|
||||
if (
|
||||
!array_key_exists($pasteid, self::$_cache)
|
||||
) self::$_cache[$pasteid] = $this->read($pasteid);
|
||||
return (bool) self::$_cache[$pasteid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a comment in a paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @param array $comment
|
||||
* @return bool
|
||||
*/
|
||||
public function createComment($pasteid, $parentid, $commentid, $comment)
|
||||
{
|
||||
foreach (array('nickname', 'vizhash') as $key)
|
||||
{
|
||||
if (!array_key_exists($key, $comment['meta']))
|
||||
{
|
||||
$comment['meta'][$key] = null;
|
||||
}
|
||||
}
|
||||
return self::_exec(
|
||||
'INSERT INTO ' . self::_sanitizeIdentifier('comment') .
|
||||
' VALUES(?,?,?,?,?,?,?)',
|
||||
array(
|
||||
$commentid,
|
||||
$pasteid,
|
||||
$parentid,
|
||||
$comment['data'],
|
||||
$comment['meta']['nickname'],
|
||||
$comment['meta']['vizhash'],
|
||||
$comment['meta']['postdate'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all comments of paste.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return array
|
||||
*/
|
||||
public function readComments($pasteid)
|
||||
{
|
||||
$rows = self::_select(
|
||||
'SELECT * FROM ' . self::_sanitizeIdentifier('comment') .
|
||||
' WHERE pasteid = ?', array($pasteid)
|
||||
);
|
||||
|
||||
// create comment list
|
||||
$comments = array();
|
||||
if (count($rows))
|
||||
{
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$i = $this->getOpenSlot($comments, (int) $row['postdate']);
|
||||
$comments[$i] = new stdClass;
|
||||
$comments[$i]->id = $row['dataid'];
|
||||
$comments[$i]->parentid = $row['parentid'];
|
||||
$comments[$i]->data = $row['data'];
|
||||
$comments[$i]->meta = new stdClass;
|
||||
$comments[$i]->meta->postdate = (int) $row['postdate'];
|
||||
if (array_key_exists('nickname', $row) && !empty($row['nickname']))
|
||||
$comments[$i]->meta->nickname = $row['nickname'];
|
||||
if (array_key_exists('vizhash', $row) && !empty($row['vizhash']))
|
||||
$comments[$i]->meta->vizhash = $row['vizhash'];
|
||||
}
|
||||
ksort($comments);
|
||||
}
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a comment exists.
|
||||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @param string $parentid
|
||||
* @param string $commentid
|
||||
* @return void
|
||||
*/
|
||||
public function existsComment($pasteid, $parentid, $commentid)
|
||||
{
|
||||
return (bool) self::_select(
|
||||
'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') .
|
||||
' WHERE pasteid = ? AND parentid = ? AND dataid = ?',
|
||||
array($pasteid, $parentid, $commentid), true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns up to batch size number of paste ids that have expired
|
||||
*
|
||||
* @access private
|
||||
* @param int $batchsize
|
||||
* @return array
|
||||
*/
|
||||
protected function _getExpiredPastes($batchsize)
|
||||
{
|
||||
$pastes = array();
|
||||
$rows = self::_select(
|
||||
'SELECT dataid FROM ' . self::_sanitizeIdentifier('paste') .
|
||||
' WHERE expiredate < ? LIMIT ?', array(time(), $batchsize)
|
||||
);
|
||||
if (count($rows))
|
||||
{
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$pastes[] = $row['dataid'];
|
||||
}
|
||||
}
|
||||
return $pastes;
|
||||
}
|
||||
|
||||
/**
|
||||
* execute a statement
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $sql
|
||||
* @param array $params
|
||||
* @throws PDOException
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* get table list query, depending on the database type
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $type
|
||||
* @throws Exception
|
||||
* @return string
|
||||
*/
|
||||
private static function _getTableQuery($type)
|
||||
{
|
||||
switch($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 $type is currently not supported.", 5
|
||||
);
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* get a value by key from the config table
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $key
|
||||
* @throws PDOException
|
||||
* @return string
|
||||
*/
|
||||
private static function _getConfig($key)
|
||||
{
|
||||
$row = self::_select(
|
||||
'SELECT value FROM ' . self::_sanitizeIdentifier('config') .
|
||||
' WHERE id = ?', array($key), true
|
||||
);
|
||||
return $row['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the primary key clauses, depending on the database driver
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
private static function _getPrimaryKeyClauses($key = 'dataid')
|
||||
{
|
||||
$main_key = $after_key = '';
|
||||
if (self::$_type === 'mysql')
|
||||
{
|
||||
$after_key = ", PRIMARY KEY ($key)";
|
||||
}
|
||||
else
|
||||
{
|
||||
$main_key = ' PRIMARY KEY';
|
||||
}
|
||||
return array($main_key, $after_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* create the paste table
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @return void
|
||||
*/
|
||||
private static function _createPasteTable()
|
||||
{
|
||||
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
|
||||
$dataType = self::$_type === 'pgsql' ? 'TEXT' : 'BLOB';
|
||||
self::$_db->exec(
|
||||
'CREATE TABLE ' . self::_sanitizeIdentifier('paste') . ' ( ' .
|
||||
"dataid CHAR(16) NOT NULL$main_key, " .
|
||||
"data $dataType, " .
|
||||
'postdate INT, ' .
|
||||
'expiredate INT, ' .
|
||||
'opendiscussion INT, ' .
|
||||
'burnafterreading INT, ' .
|
||||
'meta TEXT, ' .
|
||||
'attachment ' . (self::$_type === 'pgsql' ? 'TEXT' : 'MEDIUMBLOB') . ', ' .
|
||||
"attachmentname $dataType$after_key );"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* create the paste table
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @return void
|
||||
*/
|
||||
private static function _createCommentTable()
|
||||
{
|
||||
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
|
||||
$dataType = self::$_type === 'pgsql' ? 'text' : 'BLOB';
|
||||
self::$_db->exec(
|
||||
'CREATE TABLE ' . self::_sanitizeIdentifier('comment') . ' ( ' .
|
||||
"dataid CHAR(16) NOT NULL$main_key, " .
|
||||
'pasteid CHAR(16), ' .
|
||||
'parentid CHAR(16), ' .
|
||||
"data $dataType, " .
|
||||
"nickname $dataType, " .
|
||||
"vizhash $dataType, " .
|
||||
"postdate INT$after_key );"
|
||||
);
|
||||
self::$_db->exec(
|
||||
'CREATE INDEX IF NOT EXISTS comment_parent ON ' .
|
||||
self::_sanitizeIdentifier('comment') . '(pasteid);'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* create the paste table
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @return void
|
||||
*/
|
||||
private static function _createConfigTable()
|
||||
{
|
||||
list($main_key, $after_key) = self::_getPrimaryKeyClauses('id');
|
||||
self::$_db->exec(
|
||||
'CREATE TABLE ' . self::_sanitizeIdentifier('config') .
|
||||
" ( id CHAR(16) NOT NULL$main_key, value TEXT$after_key );"
|
||||
);
|
||||
self::_exec(
|
||||
'INSERT INTO ' . self::_sanitizeIdentifier('config') .
|
||||
' VALUES(?,?)',
|
||||
array('VERSION', privatebin::VERSION)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* sanitizes identifiers
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $identifier
|
||||
* @return string
|
||||
*/
|
||||
private static function _sanitizeIdentifier($identifier)
|
||||
{
|
||||
return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* upgrade the database schema from an old version
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $oldversion
|
||||
* @return void
|
||||
*/
|
||||
private static function _upgradeDatabase($oldversion)
|
||||
{
|
||||
$dataType = self::$_type === 'pgsql' ? 'TEXT' : 'BLOB';
|
||||
switch ($oldversion)
|
||||
{
|
||||
case '0.21':
|
||||
// create the meta column if necessary (pre 0.21 change)
|
||||
try {
|
||||
self::$_db->exec('SELECT meta FROM ' . self::_sanitizeIdentifier('paste') . ' LIMIT 1;');
|
||||
} catch (PDOException $e) {
|
||||
self::$_db->exec('ALTER TABLE ' . self::_sanitizeIdentifier('paste') . ' ADD COLUMN meta TEXT;');
|
||||
}
|
||||
// SQLite only allows one ALTER statement at a time...
|
||||
self::$_db->exec(
|
||||
'ALTER TABLE ' . self::_sanitizeIdentifier('paste') .
|
||||
' ADD COLUMN attachment ' .
|
||||
(self::$_type === 'pgsql' ? 'TEXT' : 'MEDIUMBLOB') . ';'
|
||||
);
|
||||
self::$_db->exec(
|
||||
'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . " ADD COLUMN attachmentname $dataType;"
|
||||
);
|
||||
// SQLite doesn't support MODIFY, but it allows TEXT of similar
|
||||
// size as BLOB, so there is no need to change it there
|
||||
if (self::$_type !== 'sqlite')
|
||||
{
|
||||
self::$_db->exec(
|
||||
'ALTER TABLE ' . self::_sanitizeIdentifier('paste') .
|
||||
' ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType;'
|
||||
);
|
||||
self::$_db->exec(
|
||||
'ALTER TABLE ' . self::_sanitizeIdentifier('comment') .
|
||||
" ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType, " .
|
||||
"MODIFY COLUMN nickname $dataType, MODIFY COLUMN vizhash $dataType;"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
self::$_db->exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS paste_dataid ON ' .
|
||||
self::_sanitizeIdentifier('paste') . '(dataid);'
|
||||
);
|
||||
self::$_db->exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS comment_dataid ON ' .
|
||||
self::_sanitizeIdentifier('comment') . '(dataid);'
|
||||
);
|
||||
}
|
||||
self::$_db->exec(
|
||||
'CREATE INDEX IF NOT EXISTS comment_parent ON ' .
|
||||
self::_sanitizeIdentifier('comment') . '(pasteid);'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user