mirror of
https://github.com/PrivateBin/PrivateBin.git
synced 2024-12-29 13:15:44 +01:00
working on JsonApi tests
This commit is contained in:
parent
76dc01b959
commit
59569bf9fc
10 changed files with 128 additions and 134 deletions
|
@ -199,50 +199,32 @@ class Controller
|
|||
TrafficLimiter::setConfiguration($this->_conf);
|
||||
if (!TrafficLimiter::canPass()) {
|
||||
return $this->_return_message(
|
||||
1, I18n::_(
|
||||
'Please wait %d seconds between each post.',
|
||||
$this->_conf->getKey('limit', 'traffic')
|
||||
)
|
||||
);
|
||||
1, I18n::_(
|
||||
'Please wait %d seconds between each post.',
|
||||
$this->_conf->getKey('limit', 'traffic')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = $this->_request->getParam('data');
|
||||
$attachment = $this->_request->getParam('attachment');
|
||||
$attachmentname = $this->_request->getParam('attachmentname');
|
||||
|
||||
// Ensure content is not too big.
|
||||
$data = $this->_request->getData();
|
||||
$sizelimit = $this->_conf->getKey('sizelimit');
|
||||
if (
|
||||
strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
|
||||
) {
|
||||
if (strlen($data['ct']) > $sizelimit) {
|
||||
return $this->_return_message(
|
||||
1,
|
||||
I18n::_(
|
||||
'Paste is limited to %s of encrypted data.',
|
||||
Filter::formatHumanReadableSize($sizelimit)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure attachment did not get lost due to webserver limits or Suhosin
|
||||
if (strlen($attachmentname) > 0 && strlen($attachment) == 0) {
|
||||
return $this->_return_message(1, 'Attachment missing in data received by server. Please check your webserver or suhosin configuration for maximum POST parameter limitations.');
|
||||
1,
|
||||
I18n::_(
|
||||
'Paste is limited to %s of encrypted data.',
|
||||
Filter::formatHumanReadableSize($sizelimit)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// The user posts a comment.
|
||||
$pasteid = $this->_request->getParam('pasteid');
|
||||
$parentid = $this->_request->getParam('parentid');
|
||||
if (!empty($pasteid) && !empty($parentid)) {
|
||||
$paste = $this->_model->getPaste($pasteid);
|
||||
if (!empty($data['pasteid']) && !empty($data['parentid'])) {
|
||||
$paste = $this->_model->getPaste($data['pasteid']);
|
||||
if ($paste->exists()) {
|
||||
try {
|
||||
$comment = $paste->getComment($parentid);
|
||||
|
||||
$nickname = $this->_request->getParam('nickname');
|
||||
if (!empty($nickname)) {
|
||||
$comment->setNickname($nickname);
|
||||
}
|
||||
|
||||
$comment = $paste->getComment($data['parentid']);
|
||||
$comment->setData($data);
|
||||
$comment->store();
|
||||
} catch (Exception $e) {
|
||||
|
@ -259,34 +241,6 @@ class Controller
|
|||
$paste = $this->_model->getPaste();
|
||||
try {
|
||||
$paste->setData($data);
|
||||
|
||||
if (!empty($attachment)) {
|
||||
$paste->setAttachment($attachment);
|
||||
if (!empty($attachmentname)) {
|
||||
$paste->setAttachmentName($attachmentname);
|
||||
}
|
||||
}
|
||||
|
||||
$expire = $this->_request->getParam('expire');
|
||||
if (!empty($expire)) {
|
||||
$paste->setExpiration($expire);
|
||||
}
|
||||
|
||||
$burnafterreading = $this->_request->getParam('burnafterreading');
|
||||
if (!empty($burnafterreading)) {
|
||||
$paste->setBurnafterreading($burnafterreading);
|
||||
}
|
||||
|
||||
$opendiscussion = $this->_request->getParam('opendiscussion');
|
||||
if (!empty($opendiscussion)) {
|
||||
$paste->setOpendiscussion($opendiscussion);
|
||||
}
|
||||
|
||||
$formatter = $this->_request->getParam('formatter');
|
||||
if (!empty($formatter)) {
|
||||
$paste->setFormatter($formatter);
|
||||
}
|
||||
|
||||
$paste->store();
|
||||
} catch (Exception $e) {
|
||||
return $this->_return_message(1, $e->getMessage());
|
||||
|
@ -307,22 +261,17 @@ class Controller
|
|||
try {
|
||||
$paste = $this->_model->getPaste($dataid);
|
||||
if ($paste->exists()) {
|
||||
// accessing this property ensures that the paste would be
|
||||
// accessing this method ensures that the paste would be
|
||||
// deleted if it has already expired
|
||||
$burnafterreading = $paste->isBurnafterreading();
|
||||
$paste->get();
|
||||
if (
|
||||
($burnafterreading && $deletetoken == 'burnafterreading') || // either we burn-after it has been read //@TODO: not needed anymore now?
|
||||
Filter::slowEquals($deletetoken, $paste->getDeleteToken()) // or we manually delete it with this secret token
|
||||
Filter::slowEquals($deletetoken, $paste->getDeleteToken())
|
||||
) {
|
||||
// Paste exists and deletion token (if required) is valid: Delete the paste.
|
||||
// Paste exists and deletion token is valid: Delete the paste.
|
||||
$paste->delete();
|
||||
$this->_status = 'Paste was properly deleted.';
|
||||
} else {
|
||||
if (!$burnafterreading && $deletetoken == 'burnafterreading') {
|
||||
$this->_error = 'Paste is not of burn-after-reading type.';
|
||||
} else {
|
||||
$this->_error = 'Wrong deletion token. Paste was not deleted.';
|
||||
}
|
||||
$this->_error = 'Wrong deletion token. Paste was not deleted.';
|
||||
}
|
||||
} else {
|
||||
$this->_error = self::GENERIC_ERROR;
|
||||
|
@ -355,8 +304,8 @@ class Controller
|
|||
$paste = $this->_model->getPaste($dataid);
|
||||
if ($paste->exists()) {
|
||||
$data = $paste->get();
|
||||
if (property_exists($data->meta, 'salt')) {
|
||||
unset($data->meta->salt);
|
||||
if (array_key_exists('salt', $data['meta'])) {
|
||||
unset($data['meta']['salt']);
|
||||
}
|
||||
$this->_return_message(0, $dataid, (array) $data);
|
||||
} else {
|
||||
|
|
|
@ -163,7 +163,7 @@ abstract class AbstractData
|
|||
/**
|
||||
* Get next free slot for comment from postdate.
|
||||
*
|
||||
* @access public
|
||||
* @access protected
|
||||
* @param array $comments
|
||||
* @param int|string $postdate
|
||||
* @return int|string
|
||||
|
@ -180,4 +180,25 @@ abstract class AbstractData
|
|||
}
|
||||
return $postdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade pre-version 1 pastes with attachment to version 1 format.
|
||||
*
|
||||
* @access protected
|
||||
* @static
|
||||
* @param array $paste
|
||||
* @return array
|
||||
*/
|
||||
protected static function upgradePreV1Format(array $paste)
|
||||
{
|
||||
if (array_key_exists('attachment', $paste['meta'])) {
|
||||
$paste['attachment'] = $paste['meta']['attachment'];
|
||||
unset($paste['meta']['attachment']);
|
||||
if (array_key_exists('attachmentname', $paste['meta'])) {
|
||||
$paste['attachmentname'] = $paste['meta']['attachmentname'];
|
||||
unset($paste['meta']['attachmentname']);
|
||||
}
|
||||
}
|
||||
return $paste;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,12 +249,12 @@ class Database extends AbstractData
|
|||
list($createdKey) = self::_getVersionedKeys(1);
|
||||
}
|
||||
|
||||
$meta = json_decode($paste['meta'], true);
|
||||
if (!is_array($meta)) {
|
||||
$meta = array();
|
||||
$paste['meta'] = json_decode($paste['meta'], true);
|
||||
if (!is_array($paste['meta'])) {
|
||||
$paste['meta'] = array();
|
||||
}
|
||||
|
||||
self::$_cache[$pasteid]['meta'] = $meta;
|
||||
$paste = self::upgradePreV1Format($paste);
|
||||
self::$_cache[$pasteid]['meta'] = $paste['meta'];
|
||||
self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste['postdate'];
|
||||
$expire_date = (int) $paste['expiredate'];
|
||||
if ($expire_date > 0) {
|
||||
|
@ -264,17 +264,8 @@ class Database extends AbstractData
|
|||
return self::$_cache[$pasteid];
|
||||
}
|
||||
|
||||
// support pre v1 attachments
|
||||
if (array_key_exists('attachment', $meta)) {
|
||||
self::$_cache[$pasteid]['attachment'] = $meta['attachment'];
|
||||
unset(self::$_cache[$pasteid]['meta']['attachment']);
|
||||
if (array_key_exists('attachmentname', $meta)) {
|
||||
self::$_cache[$pasteid]['attachmentname'] = $meta['attachmentname'];
|
||||
unset(self::$_cache[$pasteid]['meta']['attachmentname']);
|
||||
}
|
||||
}
|
||||
// support v1 attachments
|
||||
elseif (array_key_exists('attachment', $paste) && strlen($paste['attachment'])) {
|
||||
if (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'];
|
||||
|
|
|
@ -71,23 +71,16 @@ class Filesystem extends AbstractData
|
|||
*
|
||||
* @access public
|
||||
* @param string $pasteid
|
||||
* @return \stdClass|false
|
||||
* @return array|false
|
||||
*/
|
||||
public function read($pasteid)
|
||||
{
|
||||
if (!$this->exists($pasteid)) {
|
||||
return false;
|
||||
}
|
||||
$paste = DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php');
|
||||
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;
|
||||
return self::upgradePreV1Format(
|
||||
DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -173,13 +173,31 @@ class Paste extends AbstractModel
|
|||
*/
|
||||
public function getDeleteToken()
|
||||
{
|
||||
if (!property_exists($this->_data->meta, 'salt')) {
|
||||
if (!array_key_exists('salt', $this->_data['meta'])) {
|
||||
$this->get();
|
||||
}
|
||||
return hash_hmac(
|
||||
$this->_conf->getKey('zerobincompatibility') ? 'sha1' : 'sha256',
|
||||
$this->getId(),
|
||||
$this->_data->meta->salt
|
||||
$this->_data['meta']['salt']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if paste is of burn-after-reading type.
|
||||
*
|
||||
* @access public
|
||||
* @throws Exception
|
||||
* @return bool
|
||||
*/
|
||||
public function isBurnafterreading()
|
||||
{
|
||||
if (!array_key_exists('adata', $this->_data) && !array_key_exists('data', $this->_data)) {
|
||||
$this->get();
|
||||
}
|
||||
return (
|
||||
(array_key_exists('adata', $this->_data) && $this->_data['adata'][3] === 1) ||
|
||||
(array_key_exists('burnafterreading', $this->_data['meta']) && $this->_data['meta']['burnafterreading'])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -198,7 +216,7 @@ class Paste extends AbstractModel
|
|||
return (
|
||||
(array_key_exists('adata', $this->_data) && $this->_data['adata'][2] === 1) ||
|
||||
(array_key_exists('opendiscussion', $this->_data['meta']) && $this->_data['meta']['opendiscussion'])
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,7 +62,7 @@ class DataStore extends AbstractPersistence
|
|||
*/
|
||||
public static function get($filename)
|
||||
{
|
||||
return json_decode(substr(file_get_contents($filename), strlen(self::PROTECTION_LINE . PHP_EOL)));
|
||||
return json_decode(substr(file_get_contents($filename), strlen(self::PROTECTION_LINE . PHP_EOL)), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -126,8 +126,8 @@ class Request
|
|||
|
||||
// prepare operation, depending on current parameters
|
||||
if (
|
||||
(array_key_exists('data', $this->_params) && !empty($this->_params['data'])) ||
|
||||
(array_key_exists('attachment', $this->_params) && !empty($this->_params['attachment']))
|
||||
array_key_exists('ct', $this->_params) &&
|
||||
!empty($this->_params['ct'])
|
||||
) {
|
||||
$this->_operation = 'create';
|
||||
} elseif (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
|
||||
|
@ -152,6 +152,33 @@ class Request
|
|||
return $this->_operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data of paste or comment
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
$data = array(
|
||||
'adata' => json_decode($this->getParam('adata', '[]'), true)
|
||||
);
|
||||
$required_keys = array('v', 'ct');
|
||||
$meta = $this->getParam('meta');
|
||||
if (empty($meta)) {
|
||||
$required_keys[] = 'pasteid';
|
||||
$required_keys[] = 'parentid';
|
||||
} else {
|
||||
$data['meta'] = json_decode($meta, true);
|
||||
}
|
||||
foreach ($required_keys as $key) {
|
||||
$data[$key] = $this->getParam($key);
|
||||
}
|
||||
// forcing a cast to int or float
|
||||
$data['v'] = $data['v'] + 0;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a request parameter
|
||||
*
|
||||
|
|
|
@ -176,7 +176,8 @@ class Helper
|
|||
public static function getPastePost()
|
||||
{
|
||||
$example = self::getPaste();
|
||||
$example['meta'] = array('expire' => $example['meta']['expire']);
|
||||
$example['adata'] = json_encode($example['adata']);
|
||||
$example['meta'] = json_encode(array('expire' => $example['meta']['expire']));
|
||||
return $example;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@ class FormatV2Test extends PHPUnit_Framework_TestCase
|
|||
{
|
||||
public function testFormatV2ValidatorValidatesCorrectly()
|
||||
{
|
||||
$this->assertTrue(FormatV2::isValid(Helper::getPaste()), 'valid format');
|
||||
$this->assertTrue(FormatV2::isValid(Helper::getComment(), true), 'valid format');
|
||||
|
||||
$paste = Helper::getPaste();
|
||||
$paste['meta'] = array('expire' => $paste['meta']['expire']);
|
||||
$this->assertTrue(FormatV2::isValid($paste), 'valid format');
|
||||
$comment = Helper::getComment();
|
||||
unset($comment['meta']);
|
||||
$this->assertTrue(FormatV2::isValid($comment, true), 'valid format');
|
||||
|
||||
$paste['adata'][0][0] = '$';
|
||||
$this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of iv');
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$options = parse_ini_file(CONF, true);
|
||||
$options['traffic']['limit'] = 0;
|
||||
Helper::createIniFile(CONF, $options);
|
||||
$_POST = Helper::getPaste();
|
||||
$_POST = Helper::getPastePost();
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REMOTE_ADDR'] = '::1';
|
||||
|
@ -62,7 +62,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
|
||||
$paste = $this->_model->read($response['id']);
|
||||
$this->assertEquals(
|
||||
hash_hmac('sha256', $response['id'], $paste->meta->salt),
|
||||
hash_hmac('sha256', $response['id'], $paste['meta']['salt']),
|
||||
$response['deletetoken'],
|
||||
'outputs valid delete token'
|
||||
);
|
||||
|
@ -76,8 +76,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$options = parse_ini_file(CONF, true);
|
||||
$options['traffic']['limit'] = 0;
|
||||
Helper::createIniFile(CONF, $options);
|
||||
$paste = Helper::getPaste();
|
||||
unset($paste['meta']);
|
||||
$paste = Helper::getPastePost();
|
||||
$file = tempnam(sys_get_temp_dir(), 'FOO');
|
||||
file_put_contents($file, http_build_query($paste));
|
||||
Request::setInputStream($file);
|
||||
|
@ -98,7 +97,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
|
||||
$paste = $this->_model->read($response['id']);
|
||||
$this->assertEquals(
|
||||
hash_hmac('sha256', $response['id'], $paste->meta->salt),
|
||||
hash_hmac('sha256', $response['id'], $paste['meta']['salt']),
|
||||
$response['deletetoken'],
|
||||
'outputs valid delete token'
|
||||
);
|
||||
|
@ -114,7 +113,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$paste = $this->_model->read(Helper::getPasteId());
|
||||
$file = tempnam(sys_get_temp_dir(), 'FOO');
|
||||
file_put_contents($file, http_build_query(array(
|
||||
'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt),
|
||||
'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
|
||||
)));
|
||||
Request::setInputStream($file);
|
||||
$_SERVER['QUERY_STRING'] = Helper::getPasteId();
|
||||
|
@ -141,7 +140,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$paste = $this->_model->read(Helper::getPasteId());
|
||||
$_POST = array(
|
||||
'pasteid' => Helper::getPasteId(),
|
||||
'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt),
|
||||
'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
|
||||
);
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
@ -159,11 +158,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testRead()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste['meta']['attachment'] = $paste['attachment'];
|
||||
$paste['meta']['attachmentname'] = $paste['attachmentname'];
|
||||
unset($paste['attachment']);
|
||||
unset($paste['attachmentname']);
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_SERVER['QUERY_STRING'] = Helper::getPasteId();
|
||||
$_GET[Helper::getPasteId()] = '';
|
||||
|
@ -176,12 +171,8 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertEquals(0, $response['status'], 'outputs success status');
|
||||
$this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly');
|
||||
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
|
||||
$this->assertEquals($paste['data'], $response['data'], 'outputs data correctly');
|
||||
$this->assertEquals($paste['meta']['attachment'], $response['attachment'], 'outputs attachment correctly');
|
||||
$this->assertEquals($paste['meta']['attachmentname'], $response['attachmentname'], 'outputs attachmentname correctly');
|
||||
$this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly');
|
||||
$this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
|
||||
$this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
|
||||
$this->assertEquals($paste['ct'], $response['ct'], 'outputs data correctly');
|
||||
$this->assertEquals($paste['meta']['created'], $response['meta']['created'], 'outputs postdate correctly');
|
||||
$this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
|
||||
$this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
|
||||
}
|
||||
|
@ -191,7 +182,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testJsonLdPaste()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_GET['jsonld'] = 'paste';
|
||||
ob_start();
|
||||
|
@ -210,7 +201,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testJsonLdComment()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_GET['jsonld'] = 'comment';
|
||||
ob_start();
|
||||
|
@ -229,7 +220,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testJsonLdPasteMeta()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_GET['jsonld'] = 'pastemeta';
|
||||
ob_start();
|
||||
|
@ -248,7 +239,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testJsonLdCommentMeta()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_GET['jsonld'] = 'commentmeta';
|
||||
ob_start();
|
||||
|
@ -267,7 +258,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
|
|||
*/
|
||||
public function testJsonLdInvalid()
|
||||
{
|
||||
$paste = Helper::getPasteWithAttachment();
|
||||
$paste = Helper::getPaste();
|
||||
$this->_model->create(Helper::getPasteId(), $paste);
|
||||
$_GET['jsonld'] = CONF;
|
||||
ob_start();
|
||||
|
|
Loading…
Reference in a new issue