mirror of
https://github.com/PrivateBin/PrivateBin.git
synced 2024-03-22 13:10:41 +08:00
13c2f8d968
3 URLs of images used on social networks are passed in absolute URL. Note that I did not pass all the images in absolute URLs, but, it could be consistent to do so, but, if the images work, maybe a relative call is more efficient? Remove the version of PrivateBin, at the end of each image. This apparently prevents the opengraph from working, and, so I deleted on all of the images, to remain consistent at this level. This will make fewer requests, and, anyway, the images are not intended to change with each version.
451 lines
15 KiB
PHP
451 lines
15 KiB
PHP
<?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 1.3.4
|
|
*/
|
|
|
|
namespace PrivateBin;
|
|
|
|
use Exception;
|
|
use PrivateBin\Persistence\ServerSalt;
|
|
use PrivateBin\Persistence\TrafficLimiter;
|
|
|
|
/**
|
|
* Controller
|
|
*
|
|
* Puts it all together.
|
|
*/
|
|
class Controller
|
|
{
|
|
/**
|
|
* version
|
|
*
|
|
* @const string
|
|
*/
|
|
const VERSION = '1.3.4';
|
|
|
|
/**
|
|
* minimal required PHP version
|
|
*
|
|
* @const string
|
|
*/
|
|
const MIN_PHP_VERSION = '5.6.0';
|
|
|
|
/**
|
|
* show the same error message if the paste expired or does not exist
|
|
*
|
|
* @const string
|
|
*/
|
|
const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.';
|
|
|
|
/**
|
|
* configuration
|
|
*
|
|
* @access private
|
|
* @var Configuration
|
|
*/
|
|
private $_conf;
|
|
|
|
/**
|
|
* error message
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private $_error = '';
|
|
|
|
/**
|
|
* status message
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private $_status = '';
|
|
|
|
/**
|
|
* JSON message
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private $_json = '';
|
|
|
|
/**
|
|
* Factory of instance models
|
|
*
|
|
* @access private
|
|
* @var model
|
|
*/
|
|
private $_model;
|
|
|
|
/**
|
|
* request
|
|
*
|
|
* @access private
|
|
* @var request
|
|
*/
|
|
private $_request;
|
|
|
|
/**
|
|
* URL base
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private $_urlBase;
|
|
|
|
/**
|
|
* constructor
|
|
*
|
|
* initializes and runs PrivateBin
|
|
*
|
|
* @access public
|
|
* @throws Exception
|
|
*/
|
|
public function __construct()
|
|
{
|
|
if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
|
|
throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1);
|
|
}
|
|
if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
|
|
throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5);
|
|
}
|
|
|
|
// load config from ini file, initialize required classes
|
|
$this->_init();
|
|
|
|
switch ($this->_request->getOperation()) {
|
|
case 'create':
|
|
$this->_create();
|
|
break;
|
|
case 'delete':
|
|
$this->_delete(
|
|
$this->_request->getParam('pasteid'),
|
|
$this->_request->getParam('deletetoken')
|
|
);
|
|
break;
|
|
case 'read':
|
|
$this->_read($this->_request->getParam('pasteid'));
|
|
break;
|
|
case 'jsonld':
|
|
$this->_jsonld($this->_request->getParam('jsonld'));
|
|
return;
|
|
}
|
|
|
|
// output JSON or HTML
|
|
if ($this->_request->isJsonApiCall()) {
|
|
header('Content-type: ' . Request::MIME_JSON);
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
|
|
header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
|
|
echo $this->_json;
|
|
} else {
|
|
$this->_view();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* initialize PrivateBin
|
|
*
|
|
* @access private
|
|
* @throws Exception
|
|
*/
|
|
private function _init()
|
|
{
|
|
$this->_conf = new Configuration;
|
|
$this->_model = new Model($this->_conf);
|
|
$this->_request = new Request;
|
|
$this->_urlBase = $this->_request->getRequestUri();
|
|
ServerSalt::setPath($this->_conf->getKey('dir', 'traffic'));
|
|
|
|
// set default language
|
|
$lang = $this->_conf->getKey('languagedefault');
|
|
I18n::setLanguageFallback($lang);
|
|
// force default language, if language selection is disabled and a default is set
|
|
if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) {
|
|
$_COOKIE['lang'] = $lang;
|
|
setcookie('lang', $lang);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store new paste or comment
|
|
*
|
|
* POST contains one or both:
|
|
* data = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
|
|
* attachment = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
|
|
*
|
|
* All optional data will go to meta information:
|
|
* expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
|
|
* formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
|
|
* burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
|
|
* opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
|
|
* attachmentname = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
|
|
* nickname (optional) = in discussion, encoded FormatV2 encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
|
|
* parentid (optional) = in discussion, which comment this comment replies to.
|
|
* pasteid (optional) = in discussion, which paste this comment belongs to.
|
|
*
|
|
* @access private
|
|
* @return string
|
|
*/
|
|
private function _create()
|
|
{
|
|
try {
|
|
// Ensure last paste from visitors IP address was more than configured amount of seconds ago.
|
|
TrafficLimiter::setConfiguration($this->_conf);
|
|
if (!TrafficLimiter::canPass()) {
|
|
$this->_return_message(
|
|
1, I18n::_(
|
|
'Please wait %d seconds between each post.',
|
|
$this->_conf->getKey('limit', 'traffic')
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->_return_message(1, I18n::_($e->getMessage()));
|
|
return;
|
|
}
|
|
|
|
$data = $this->_request->getData();
|
|
$isComment = array_key_exists('pasteid', $data) &&
|
|
!empty($data['pasteid']) &&
|
|
array_key_exists('parentid', $data) &&
|
|
!empty($data['parentid']);
|
|
if (!FormatV2::isValid($data, $isComment)) {
|
|
$this->_return_message(1, I18n::_('Invalid data.'));
|
|
return;
|
|
}
|
|
$sizelimit = $this->_conf->getKey('sizelimit');
|
|
// Ensure content is not too big.
|
|
if (strlen($data['ct']) > $sizelimit) {
|
|
$this->_return_message(
|
|
1,
|
|
I18n::_(
|
|
'Paste is limited to %s of encrypted data.',
|
|
Filter::formatHumanReadableSize($sizelimit)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// The user posts a comment.
|
|
if ($isComment) {
|
|
$paste = $this->_model->getPaste($data['pasteid']);
|
|
if ($paste->exists()) {
|
|
try {
|
|
$comment = $paste->getComment($data['parentid']);
|
|
$comment->setData($data);
|
|
$comment->store();
|
|
} catch (Exception $e) {
|
|
$this->_return_message(1, $e->getMessage());
|
|
return;
|
|
}
|
|
$this->_return_message(0, $comment->getId());
|
|
} else {
|
|
$this->_return_message(1, I18n::_('Invalid data.'));
|
|
}
|
|
}
|
|
// The user posts a standard paste.
|
|
else {
|
|
$this->_model->purge();
|
|
$paste = $this->_model->getPaste();
|
|
try {
|
|
$paste->setData($data);
|
|
$paste->store();
|
|
} catch (Exception $e) {
|
|
return $this->_return_message(1, $e->getMessage());
|
|
}
|
|
$this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an existing paste
|
|
*
|
|
* @access private
|
|
* @param string $dataid
|
|
* @param string $deletetoken
|
|
*/
|
|
private function _delete($dataid, $deletetoken)
|
|
{
|
|
try {
|
|
$paste = $this->_model->getPaste($dataid);
|
|
if ($paste->exists()) {
|
|
// accessing this method ensures that the paste would be
|
|
// deleted if it has already expired
|
|
$paste->get();
|
|
if (hash_equals($paste->getDeleteToken(), $deletetoken)) {
|
|
// Paste exists and deletion token is valid: Delete the paste.
|
|
$paste->delete();
|
|
$this->_status = 'Paste was properly deleted.';
|
|
} else {
|
|
$this->_error = 'Wrong deletion token. Paste was not deleted.';
|
|
}
|
|
} else {
|
|
$this->_error = self::GENERIC_ERROR;
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->_error = $e->getMessage();
|
|
}
|
|
if ($this->_request->isJsonApiCall()) {
|
|
if (strlen($this->_error)) {
|
|
$this->_return_message(1, $this->_error);
|
|
} else {
|
|
$this->_return_message(0, $dataid);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read an existing paste or comment, only allowed via a JSON API call
|
|
*
|
|
* @access private
|
|
* @param string $dataid
|
|
*/
|
|
private function _read($dataid)
|
|
{
|
|
if (!$this->_request->isJsonApiCall()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$paste = $this->_model->getPaste($dataid);
|
|
if ($paste->exists()) {
|
|
$data = $paste->get();
|
|
if (array_key_exists('salt', $data['meta'])) {
|
|
unset($data['meta']['salt']);
|
|
}
|
|
$this->_return_message(0, $dataid, (array) $data);
|
|
} else {
|
|
$this->_return_message(1, self::GENERIC_ERROR);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->_return_message(1, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display frontend.
|
|
*
|
|
* @access private
|
|
*/
|
|
private function _view()
|
|
{
|
|
// set headers to disable caching
|
|
$time = gmdate('D, d M Y H:i:s \G\M\T');
|
|
header('Cache-Control: no-store, no-cache, no-transform, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
header('Expires: ' . $time);
|
|
header('Last-Modified: ' . $time);
|
|
header('Vary: Accept');
|
|
header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader'));
|
|
header('Referrer-Policy: no-referrer');
|
|
header('X-Xss-Protection: 1; mode=block');
|
|
header('X-Frame-Options: DENY');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
// label all the expiration options
|
|
$expire = array();
|
|
foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
|
|
$expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
|
|
}
|
|
|
|
// translate all the formatter options
|
|
$formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options'));
|
|
|
|
// set language cookie if that functionality was enabled
|
|
$languageselection = '';
|
|
if ($this->_conf->getKey('languageselection')) {
|
|
$languageselection = I18n::getLanguage();
|
|
setcookie('lang', $languageselection);
|
|
}
|
|
|
|
$page = new View;
|
|
$page->assign('NAME', $this->_conf->getKey('name'));
|
|
$page->assign('PATH', I18n::_($this->_conf->getKey('path')));
|
|
$page->assign('ERROR', I18n::_($this->_error));
|
|
$page->assign('STATUS', I18n::_($this->_status));
|
|
$page->assign('VERSION', self::VERSION);
|
|
$page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
|
|
$page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
|
|
$page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
|
|
$page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
|
|
$page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
|
|
$page->assign('FORMATTER', $formatters);
|
|
$page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
|
|
$page->assign('NOTICE', I18n::_($this->_conf->getKey('notice')));
|
|
$page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
|
|
$page->assign('PASSWORD', $this->_conf->getKey('password'));
|
|
$page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
|
|
$page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
|
|
$page->assign('LANGUAGESELECTION', $languageselection);
|
|
$page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
|
|
$page->assign('EXPIRE', $expire);
|
|
$page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
|
|
$page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
|
|
$page->assign('QRCODE', $this->_conf->getKey('qrcode'));
|
|
$page->assign('HTTPWARNING', $this->_conf->getKey('httpwarning'));
|
|
$page->assign('HTTPSLINK', 'https://' . $this->_request->getHost() . $this->_request->getRequestUri());
|
|
$page->assign('COMPRESSION', $this->_conf->getKey('compression'));
|
|
$page->draw($this->_conf->getKey('template'));
|
|
}
|
|
|
|
/**
|
|
* outputs requested JSON-LD context
|
|
*
|
|
* @access private
|
|
* @param string $type
|
|
*/
|
|
private function _jsonld($type)
|
|
{
|
|
if (
|
|
$type !== 'paste' && $type !== 'comment' &&
|
|
$type !== 'pastemeta' && $type !== 'commentmeta'
|
|
) {
|
|
$type = '';
|
|
}
|
|
$content = '{}';
|
|
$file = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
|
|
if (is_readable($file)) {
|
|
$content = str_replace(
|
|
'?jsonld=',
|
|
$this->_urlBase . '?jsonld=',
|
|
file_get_contents($file)
|
|
);
|
|
}
|
|
|
|
header('Content-type: application/ld+json');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET');
|
|
echo $content;
|
|
}
|
|
|
|
/**
|
|
* prepares JSON encoded status message
|
|
*
|
|
* @access private
|
|
* @param int $status
|
|
* @param string $message
|
|
* @param array $other
|
|
*/
|
|
private function _return_message($status, $message, $other = array())
|
|
{
|
|
$result = array('status' => $status);
|
|
if ($status) {
|
|
$result['message'] = I18n::_($message);
|
|
} else {
|
|
$result['id'] = $message;
|
|
$result['url'] = $this->_urlBase . '?' . $message;
|
|
}
|
|
$result += $other;
|
|
$this->_json = Json::encode($result);
|
|
}
|
|
}
|