fixing nasty deletion bug from #15, included unit tests to trigger it

and reworked persistence classes to through exceptions rather to fail
silently
This commit is contained in:
El RIDO 2015-08-27 21:41:21 +02:00
parent d042bb41ba
commit f775da3931
11 changed files with 334 additions and 134 deletions

View File

@ -10,7 +10,7 @@ Details about [installing phpDocumentor](http://phpdoc.org/docs/latest/getting-s
can be found in its own documentation. can be found in its own documentation.
Example for Debian and Ubuntu: Example for Debian and Ubuntu:
$ sudo aptitude install pear graphviz $ sudo aptitude install php-pear graphviz
$ sudo pear channel-discover pear.phpdoc.org $ sudo pear channel-discover pear.phpdoc.org
$ sudo pear install phpdoc/phpDocumentor $ sudo pear install phpdoc/phpDocumentor

View File

@ -67,7 +67,7 @@ abstract class persistence
protected static function _exists($filename) protected static function _exists($filename)
{ {
self::_initialize(); self::_initialize();
return is_file(self::$_path . '/' . $filename); return is_file(self::$_path . DIRECTORY_SEPARATOR . $filename);
} }
/** /**
@ -75,23 +75,29 @@ abstract class persistence
* *
* @access protected * @access protected
* @static * @static
* @throws Exception
* @return void * @return void
*/ */
protected static function _initialize() protected static function _initialize()
{ {
// Create storage directory if it does not exist. // Create storage directory if it does not exist.
if (!is_dir(self::$_path)) mkdir(self::$_path, 0705); if (!is_dir(self::$_path))
if (!@mkdir(self::$_path))
throw new Exception('unable to create directory ' . self::$_path, 10);
// Create .htaccess file if it does not exist. // Create .htaccess file if it does not exist.
$file = self::$_path . '/.htaccess'; $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
if (!is_file($file)) if (!is_file($file))
{ {
file_put_contents( $writtenBytes = @file_put_contents(
$file, $file,
'Allow from none' . PHP_EOL . 'Allow from none' . PHP_EOL .
'Deny from all'. PHP_EOL, 'Deny from all'. PHP_EOL,
LOCK_EX LOCK_EX
); );
if ($writtenBytes === false || $writtenBytes < 30) {
throw new Exception('unable to write to file ' . $file, 11);
}
} }
} }
@ -102,14 +108,17 @@ abstract class persistence
* @static * @static
* @param string $filename * @param string $filename
* @param string $data * @param string $data
* @throws Exception
* @return string * @return string
*/ */
protected static function _store($filename, $data) protected static function _store($filename, $data)
{ {
self::_initialize(); self::_initialize();
$file = self::$_path . '/' . $filename; $file = self::$_path . DIRECTORY_SEPARATOR . $filename;
file_put_contents($file, $data, LOCK_EX); $writtenBytes = @file_put_contents($file, $data, LOCK_EX);
chmod($file, 0705); if ($writtenBytes === false || $writtenBytes < strlen($data)) {
throw new Exception('unable to write to file ' . $file, 13);
}
return $file; return $file;
} }
} }

View File

@ -47,12 +47,11 @@ class serversalt extends persistence
} }
else // fallback to mt_rand() else // fallback to mt_rand()
{ {
for($i = 0; $i < 16; ++$i) { for($i = 0; $i < 256; ++$i) {
$randomSalt .= base_convert(mt_rand(), 10, 16); $randomSalt .= base_convert(mt_rand(), 10, 16);
} }
} }
self::$_salt = $randomSalt; return $randomSalt;
return self::$_salt;
} }
/** /**
@ -60,21 +59,41 @@ class serversalt extends persistence
* *
* @access public * @access public
* @static * @static
* @throws Exception
* @return string * @return string
*/ */
public static function get() public static function get()
{ {
if (strlen(self::$_salt)) return self::$_salt; if (strlen(self::$_salt)) return self::$_salt;
$file = 'salt.php'; $file = 'salt.php';
if (!self::_exists($file)) { if (self::_exists($file)) {
$items = explode('|', @file_get_contents(self::getPath($file)));
if (!is_array($items) || count($items) != 3) {
throw new Exception('unable to read file ' . self::getPath($file), 20);
}
self::$_salt = $items[1];
} else {
self::$_salt = self::generate();
self::_store( self::_store(
$file, $file,
'<?php /* |'. self::generate() . '| */ ?>' '<?php /* |'. self::$_salt . '| */ ?>'
); );
} }
$items = explode('|', file_get_contents(self::getPath($file))); return self::$_salt;
self::$_salt = $items[1]; }
return $items[1];
/**
* set the path
*
* @access public
* @static
* @param string $path
* @return void
*/
public static function setPath($path)
{
self::$_salt = '';
parent::setPath($path);
} }
} }

View File

@ -47,6 +47,7 @@ class trafficlimiter extends persistence
* @access public * @access public
* @static * @static
* @param string $ip * @param string $ip
* @throws Exception
* @return bool * @return bool
*/ */
public static function canPass($ip) public static function canPass($ip)

View File

@ -339,7 +339,7 @@ class zerobin
// Generate the "delete" token. // Generate the "delete" token.
// The token is the hmac of the pasteid signed with the server salt. // The token is the hmac of the pasteid signed with the server salt.
// The paste can be delete by calling http://myserver.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken> // The paste can be delete by calling http://myserver.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken>
$deletetoken = hash_hmac('sha1', $dataid , serversalt::get()); $deletetoken = hash_hmac('sha1', $dataid, serversalt::get());
// 0 = no error // 0 = no error
$this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken));
@ -373,7 +373,8 @@ class zerobin
} }
// Make sure token is valid. // Make sure token is valid.
if (filter::slow_equals($deletetoken, hash_hmac('sha1', $dataid , serversalt::get()))) serversalt::setPath($this->_conf['traffic']['dir']);
if (!filter::slow_equals($deletetoken, hash_hmac('sha1', $dataid, serversalt::get())))
{ {
$this->_error = 'Wrong deletion token. Paste was not deleted.'; $this->_error = 'Wrong deletion token. Paste was not deleted.';
return; return;

View File

@ -38,8 +38,8 @@ class zerobin_data extends zerobin_abstract
{ {
// if given update the data directory // if given update the data directory
if ( if (
is_array($options) && is_array($options) &&
array_key_exists('dir', $options) array_key_exists('dir', $options)
) self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR; ) self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
// if needed initialize the singleton // if needed initialize the singleton
if(!(self::$_instance instanceof zerobin_data)) { if(!(self::$_instance instanceof zerobin_data)) {

View File

@ -80,13 +80,6 @@ class zerobin_db extends zerobin_abstract
array_key_exists('opt', $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 // check if the database contains the required tables
self::$_type = strtolower( self::$_type = strtolower(
substr($options['dsn'], 0, strpos($options['dsn'], ':')) substr($options['dsn'], 0, strpos($options['dsn'], ':'))
@ -132,9 +125,16 @@ class zerobin_db extends zerobin_abstract
throw new Exception( throw new Exception(
'PDO type ' . 'PDO type ' .
self::$_type . self::$_type .
' is currently not supported.' ' is currently not supported.',
5
); );
} }
self::$_db = new PDO(
$options['dsn'],
$options['usr'],
$options['pwd'],
$options['opt']
);
$statement = self::$_db->query($sql); $statement = self::$_db->query($sql);
$tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0); $tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
@ -266,7 +266,7 @@ class zerobin_db extends zerobin_abstract
array($pasteid) array($pasteid)
); );
if ( if (
array_key_exists($pasteid, self::$_cache) array_key_exists($pasteid, self::$_cache)
) unset(self::$_cache[$pasteid]); ) unset(self::$_cache[$pasteid]);
} }

View File

@ -8,24 +8,24 @@ require PATH . 'lib/auto.php';
class helper class helper
{ {
public static function rmdir($path) public static function rmdir($path)
{ {
$path .= DIRECTORY_SEPARATOR; $path .= DIRECTORY_SEPARATOR;
$dir = dir($path); $dir = dir($path);
while(false !== ($file = $dir->read())) { while(false !== ($file = $dir->read())) {
if($file != '.' && $file != '..') { if($file != '.' && $file != '..') {
if(is_dir($path . $file)) { if(is_dir($path . $file)) {
self::rmdir($path . $file); self::rmdir($path . $file);
} elseif(is_file($path . $file)) { } elseif(is_file($path . $file)) {
if(!@unlink($path . $file)) { if(!@unlink($path . $file)) {
throw new Exception('Error deleting file "' . $path . $file . '".'); throw new Exception('Error deleting file "' . $path . $file . '".');
} }
} }
} }
} }
$dir->close(); $dir->close();
if(!@rmdir($path)) { if(!@rmdir($path)) {
throw new Exception('Error deleting directory "' . $path . '".'); throw new Exception('Error deleting directory "' . $path . '".');
} }
} }
} }

100
tst/serversalt.php Normal file
View File

@ -0,0 +1,100 @@
<?php
class serversaltTest extends PHPUnit_Framework_TestCase
{
private $_path;
private $_invalidPath;
private $_otherPath;
private $_invalidFile;
public function setUp()
{
/* Setup Routine */
$this->_path = PATH . 'data';
if(!is_dir($this->_path)) mkdir($this->_path);
serversalt::setPath($this->_path);
$this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo';
$this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
if(!is_dir($this->_invalidPath)) mkdir($this->_invalidPath);
$this->_invalidFile = $this->_invalidPath . DIRECTORY_SEPARATOR . 'salt.php';
}
public function tearDown()
{
/* Tear Down Routine */
chmod($this->_invalidPath, 0700);
helper::rmdir($this->_path);
}
public function testGeneration()
{
// generating new salt
serversalt::setPath($this->_path);
$salt = serversalt::get();
require 'mcrypt_mock.php';
$this->assertNotEquals($salt, serversalt::generate());
// try setting a different path and resetting it
serversalt::setPath($this->_otherPath);
$this->assertNotEquals($salt, serversalt::get());
serversalt::setPath($this->_path);
$this->assertEquals($salt, serversalt::get());
}
/**
* @expectedException Exception
* @expectedExceptionCode 11
*/
public function testPathShenanigans()
{
// try setting an invalid path
chmod($this->_invalidPath, 0000);
serversalt::setPath($this->_invalidPath);
serversalt::get();
}
/**
* @expectedException Exception
* @expectedExceptionCode 20
*/
public function testFileRead()
{
// try setting an invalid file
chmod($this->_invalidPath, 0700);
file_put_contents($this->_invalidFile, '');
chmod($this->_invalidFile, 0000);
serversalt::setPath($this->_invalidPath);
serversalt::get();
}
/**
* @expectedException Exception
* @expectedExceptionCode 13
*/
public function testFileWrite()
{
// try setting an invalid file
chmod($this->_invalidPath, 0700);
@unlink($this->_invalidFile);
file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . '.htaccess', '');
chmod($this->_invalidPath, 0500);
serversalt::setPath($this->_invalidPath);
serversalt::get();
}
/**
* @expectedException Exception
* @expectedExceptionCode 10
*/
public function testPermissionShenanigans()
{
// try creating an invalid path
chmod($this->_invalidPath, 0000);
serversalt::setPath($this->_invalidPath . DIRECTORY_SEPARATOR . 'baz');
serversalt::get();
}
}

View File

@ -10,22 +10,20 @@ class vizhash16x16Test extends PHPUnit_Framework_TestCase
public function setUp() public function setUp()
{ {
/* Setup Routine */ /* Setup Routine */
$this->_path = PATH . 'data' . DIRECTORY_SEPARATOR; $this->_path = PATH . 'data';
$this->_dataDirCreated = !is_dir($this->_path); if(!is_dir($this->_path)) mkdir($this->_path);
if($this->_dataDirCreated) mkdir($this->_path); $this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png';
$this->_file = $this->_path . 'vizhash.png'; serversalt::setPath($this->_path);
} }
public function tearDown() public function tearDown()
{ {
/* Tear Down Routine */ /* Tear Down Routine */
if($this->_dataDirCreated) { chmod($this->_path, 0700);
helper::rmdir($this->_path); if(!@unlink($this->_file)) {
} else { throw new Exception('Error deleting file "' . $this->_file . '".');
if(!@unlink($this->_file)) {
throw new Exception('Error deleting file "' . $this->_file . '".');
}
} }
helper::rmdir($this->_path);
} }
public function testVizhashGeneratesUniquePngsPerIp() public function testVizhashGeneratesUniquePngsPerIp()
@ -37,10 +35,5 @@ class vizhash16x16Test extends PHPUnit_Framework_TestCase
$this->assertEquals('image/png', $finfo->file($this->_file)); $this->assertEquals('image/png', $finfo->file($this->_file));
$this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe')); $this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe'));
$this->assertEquals($pngdata, $vz->generate('127.0.0.1')); $this->assertEquals($pngdata, $vz->generate('127.0.0.1'));
// generating new salt
$salt = serversalt::get();
require 'mcrypt_mock.php';
$this->assertNotEquals($salt, serversalt::generate());
} }
} }

View File

@ -1,68 +1,145 @@
<?php <?php
class zerobin_dbTest extends PHPUnit_Framework_TestCase class zerobin_dbTest extends PHPUnit_Framework_TestCase
{ {
private static $pasteid = '501f02e9eeb8bcec'; private static $pasteid = '501f02e9eeb8bcec';
private static $paste = array( private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}', 'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'meta' => array( 'meta' => array(
'postdate' => 1344803344, 'postdate' => 1344803344,
'expire_date' => 1344803644, 'expire_date' => 1344803644,
'opendiscussion' => true, 'opendiscussion' => true,
), ),
); );
private static $commentid = 'c47efb4741195f42'; private static $commentid = 'c47efb4741195f42';
private static $comment = array( private static $comment = array(
'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}', 'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'meta' => array( 'meta' => array(
'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}', 'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
'vizhash' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=', 'vizhash' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
'postdate' => 1344803528, 'postdate' => 1344803528,
), ),
); );
private $_model; private $_model;
public function setUp() public function setUp()
{ {
/* Setup Routine */ /* Setup Routine */
$this->_model = zerobin_db::getInstance( $this->_model = zerobin_db::getInstance(
array( array(
'dsn' => 'sqlite::memory:', 'dsn' => 'sqlite::memory:',
'usr' => null, 'usr' => null,
'pwd' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
) )
); );
} }
public function testDatabaseBasedDataStoreWorks() public function testDatabaseBasedDataStoreWorks()
{ {
// storing pastes // storing pastes
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist'); $this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist');
$this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste'); $this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it'); $this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it');
$this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice'); $this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice');
$this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid)); $this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid));
// storing comments // storing comments
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist'); $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist');
$this->assertTrue($this->_model->createComment(self::$pasteid, self::$pasteid, self::$commentid, self::$comment) !== false, 'store comment'); $this->assertTrue($this->_model->createComment(self::$pasteid, self::$pasteid, self::$commentid, self::$comment) !== false, 'store comment');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it'); $this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it');
$comment = json_decode(json_encode(self::$comment)); $comment = json_decode(json_encode(self::$comment));
$comment->meta->commentid = self::$commentid; $comment->meta->commentid = self::$commentid;
$comment->meta->parentid = self::$pasteid; $comment->meta->parentid = self::$pasteid;
$this->assertEquals( $this->assertEquals(
array($comment->meta->postdate => $comment), array($comment->meta->postdate => $comment),
$this->_model->readComments(self::$pasteid) $this->_model->readComments(self::$pasteid)
); );
// deleting pastes // deleting pastes
$this->_model->delete(self::$pasteid); $this->_model->delete(self::$pasteid);
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste'); $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste');
$this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found'); $this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found');
} }
}
/**
* @expectedException PDOException
*/
public function testGetIbmInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'ibm:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException PDOException
*/
public function testGetInformixInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'informix:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException PDOException
*/
public function testGetMssqlInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'mssql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException PDOException
*/
public function testGetMysqlInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'mysql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException PDOException
*/
public function testGetOciInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'oci:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException PDOException
*/
public function testGetPgsqlInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'pgsql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
));
}
/**
* @expectedException Exception
* @expectedExceptionCode 5
*/
public function testGetFooInstance()
{
zerobin_db::getInstance(array(
'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null
));
}
}