mirror of
https://github.com/PrivateBin/PrivateBin.git
synced 2025-10-02 00:18:42 -04:00
Merge branch 'master' into wasm-streaming
This commit is contained in:
commit
6b180ac7b1
236 changed files with 20731 additions and 6269 deletions
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,13 +7,11 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
|
@ -40,11 +38,12 @@ class Configuration
|
|||
'basepath' => '',
|
||||
'discussion' => true,
|
||||
'opendiscussion' => false,
|
||||
'discussiondatedisplay' => true,
|
||||
'password' => true,
|
||||
'fileupload' => false,
|
||||
'burnafterreadingselected' => false,
|
||||
'defaultformatter' => 'plaintext',
|
||||
'syntaxhighlightingtheme' => null,
|
||||
'syntaxhighlightingtheme' => '',
|
||||
'sizelimit' => 10485760,
|
||||
'template' => 'bootstrap',
|
||||
'info' => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
|
||||
|
@ -53,8 +52,9 @@ class Configuration
|
|||
'languagedefault' => '',
|
||||
'urlshortener' => '',
|
||||
'qrcode' => true,
|
||||
'email' => true,
|
||||
'icon' => 'identicon',
|
||||
'cspheader' => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' resource:; style-src \'self\'; font-src \'self\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
|
||||
'cspheader' => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'wasm-unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
|
||||
'zerobincompatibility' => false,
|
||||
'httpwarning' => true,
|
||||
'compression' => 'zlib',
|
||||
|
@ -78,9 +78,10 @@ class Configuration
|
|||
'markdown' => 'Markdown',
|
||||
),
|
||||
'traffic' => array(
|
||||
'limit' => 10,
|
||||
'header' => null,
|
||||
'exemptedIp' => null,
|
||||
'limit' => 10,
|
||||
'header' => '',
|
||||
'exempted' => '',
|
||||
'creators' => '',
|
||||
),
|
||||
'purge' => array(
|
||||
'limit' => 300,
|
||||
|
@ -92,6 +93,27 @@ class Configuration
|
|||
'model_options' => array(
|
||||
'dir' => 'data',
|
||||
),
|
||||
'yourls' => array(
|
||||
'signature' => '',
|
||||
'apiurl' => '',
|
||||
),
|
||||
// update this array when adding/changing/removing js files
|
||||
'sri' => array(
|
||||
'js/base-x-4.0.0.js' => 'sha512-nNPg5IGCwwrveZ8cA/yMGr5HiRS5Ps2H+s0J/mKTPjCPWUgFGGw7M5nqdnPD3VsRwCVysUh3Y8OWjeSKGkEQJQ==',
|
||||
'js/base64-1.7.js' => 'sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==',
|
||||
'js/bootstrap-3.4.1.js' => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
|
||||
'js/bootstrap-5.3.3.js' => 'sha512-in2rcOpLTdJ7/pw5qjF4LWHFRtgoBDxXCy49H4YGOcVdGiPaQucGIbOqxt1JvmpvOpq3J/C7VTa0FlioakB2gQ==',
|
||||
'js/dark-mode-switch.js' => 'sha512-CCbdHdeWDbDO7aqFFmhgnvFESzaILHbUYmbhNjTpcjyO/XYdouQ9Pw8W9rpV8oJT1TsK5FbwSHU1oazmnb7BWA==',
|
||||
'js/jquery-3.7.1.js' => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
|
||||
'js/kjua-0.9.0.js' => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==',
|
||||
'js/legacy.js' => 'sha512-p76t5AT6YHgvhG5RqWGOQ6o87aObfYWYwOPHYhhN4KfExVEZJ0/I0D+1daKprxgbL37/gtXxbd1qZx4PIhSU3g==',
|
||||
'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
|
||||
'js/privatebin.js' => 'sha512-JUj/Sbl/bMHlIoIUT1U9e89JU33fDBxCxLSGxwwaeydBFXOBHyfdF7hwSIjgbPxb4d9CO7CSe4meouTIRMy8Vg==',
|
||||
'js/purify-3.1.7.js' => 'sha512-LegvqULiMtOfboJZw9MpETN/b+xnLRXZI90gG7oIFHW+yAeHmKvRtEUbiMFx2WvUqQoL9XB3gwU+hWXUT0X+8A==',
|
||||
'js/rawinflate-0.3.js' => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==',
|
||||
'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
|
||||
'js/zlib-1.3.1.js' => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -101,16 +123,23 @@ class Configuration
|
|||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$basePaths = array();
|
||||
$config = array();
|
||||
$basePath = (getenv('CONFIG_PATH') !== false ? getenv('CONFIG_PATH') : PATH . 'cfg') . DIRECTORY_SEPARATOR;
|
||||
$configFile = $basePath . 'conf.php';
|
||||
|
||||
if (is_readable($configFile)) {
|
||||
$config = parse_ini_file($configFile, true);
|
||||
foreach (array('main', 'model', 'model_options') as $section) {
|
||||
if (!array_key_exists($section, $config)) {
|
||||
throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 2);
|
||||
$configPath = getenv('CONFIG_PATH');
|
||||
if ($configPath !== false && !empty($configPath)) {
|
||||
$basePaths[] = $configPath;
|
||||
}
|
||||
$basePaths[] = PATH . 'cfg';
|
||||
foreach ($basePaths as $basePath) {
|
||||
$configFile = $basePath . DIRECTORY_SEPARATOR . 'conf.php';
|
||||
if (is_readable($configFile)) {
|
||||
$config = parse_ini_file($configFile, true);
|
||||
foreach (array('main', 'model', 'model_options') as $section) {
|
||||
if (!array_key_exists($section, $config)) {
|
||||
throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 2);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +165,7 @@ class Configuration
|
|||
'tbl' => null,
|
||||
'usr' => null,
|
||||
'pwd' => null,
|
||||
'opt' => array(PDO::ATTR_PERSISTENT => true),
|
||||
'opt' => array(),
|
||||
);
|
||||
} elseif (
|
||||
$section == 'model_options' && in_array(
|
||||
|
@ -145,8 +174,25 @@ class Configuration
|
|||
)
|
||||
) {
|
||||
$values = array(
|
||||
'bucket' => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null,
|
||||
'prefix' => 'pastes',
|
||||
'bucket' => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null,
|
||||
'prefix' => 'pastes',
|
||||
'uniformacl' => false,
|
||||
);
|
||||
} elseif (
|
||||
$section == 'model_options' && in_array(
|
||||
$this->_configuration['model']['class'],
|
||||
array('S3Storage')
|
||||
)
|
||||
) {
|
||||
$values = array(
|
||||
'region' => null,
|
||||
'version' => null,
|
||||
'endpoint' => null,
|
||||
'accesskey' => null,
|
||||
'secretkey' => null,
|
||||
'use_path_style_endpoint' => null,
|
||||
'bucket' => null,
|
||||
'prefix' => '',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -163,6 +209,10 @@ class Configuration
|
|||
}
|
||||
// check for missing keys and set defaults if necessary
|
||||
else {
|
||||
// preserve configured SRI hashes
|
||||
if ($section == 'sri' && array_key_exists($section, $config)) {
|
||||
$this->_configuration[$section] = $config[$section];
|
||||
}
|
||||
foreach ($values as $key => $val) {
|
||||
if ($key == 'dir') {
|
||||
$val = PATH . $val;
|
||||
|
@ -184,6 +234,8 @@ class Configuration
|
|||
$result = (int) $config[$section][$key];
|
||||
} elseif (is_string($val) && !empty($config[$section][$key])) {
|
||||
$result = (string) $config[$section][$key];
|
||||
} elseif (is_array($val) && is_array($config[$section][$key])) {
|
||||
$result = $config[$section][$key];
|
||||
}
|
||||
}
|
||||
$this->_configuration[$section][$key] = $result;
|
||||
|
@ -207,6 +259,14 @@ class Configuration
|
|||
if (!array_key_exists($this->_configuration['expire']['default'], $this->_configuration['expire_options'])) {
|
||||
$this->_configuration['expire']['default'] = key($this->_configuration['expire_options']);
|
||||
}
|
||||
|
||||
// ensure the basepath ends in a slash, if one is set
|
||||
if (
|
||||
!empty($this->_configuration['main']['basepath']) &&
|
||||
substr_compare($this->_configuration['main']['basepath'], '/', -1) !== 0
|
||||
) {
|
||||
$this->_configuration['main']['basepath'] .= '/';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
@ -28,14 +27,14 @@ class Controller
|
|||
*
|
||||
* @const string
|
||||
*/
|
||||
const VERSION = '1.3.5';
|
||||
const VERSION = '1.7.5';
|
||||
|
||||
/**
|
||||
* minimal required PHP version
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
const MIN_PHP_VERSION = '5.6.0';
|
||||
const MIN_PHP_VERSION = '7.3.0';
|
||||
|
||||
/**
|
||||
* show the same error message if the paste expired or does not exist
|
||||
|
@ -68,6 +67,14 @@ class Controller
|
|||
*/
|
||||
private $_status = '';
|
||||
|
||||
/**
|
||||
* status message
|
||||
*
|
||||
* @access private
|
||||
* @var bool
|
||||
*/
|
||||
private $_is_deleted = false;
|
||||
|
||||
/**
|
||||
* JSON message
|
||||
*
|
||||
|
@ -111,10 +118,12 @@ class Controller
|
|||
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);
|
||||
error_log(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION));
|
||||
return;
|
||||
}
|
||||
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);
|
||||
error_log(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR));
|
||||
return;
|
||||
}
|
||||
|
||||
// load config from ini file, initialize required classes
|
||||
|
@ -136,14 +145,21 @@ class Controller
|
|||
case 'jsonld':
|
||||
$this->_jsonld($this->_request->getParam('jsonld'));
|
||||
return;
|
||||
case 'yourlsproxy':
|
||||
$this->_yourlsproxy($this->_request->getParam('link'));
|
||||
break;
|
||||
}
|
||||
|
||||
$this->_setCacheHeaders();
|
||||
|
||||
// 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');
|
||||
header('X-Uncompressed-Content-Length: ' . strlen($this->_json));
|
||||
header('Access-Control-Expose-Headers: X-Uncompressed-Content-Length');
|
||||
echo $this->_json;
|
||||
} else {
|
||||
$this->_view();
|
||||
|
@ -169,10 +185,26 @@ class Controller
|
|||
// 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, 0, '', '', true);
|
||||
setcookie('lang', $lang, array('SameSite' => 'Lax', 'Secure' => true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off browser caching
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function _setCacheHeaders()
|
||||
{
|
||||
// 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new paste or comment
|
||||
*
|
||||
|
@ -199,13 +231,10 @@ class Controller
|
|||
ServerSalt::setStore($this->_model->getStore());
|
||||
TrafficLimiter::setConfiguration($this->_conf);
|
||||
TrafficLimiter::setStore($this->_model->getStore());
|
||||
if (!TrafficLimiter::canPass()) {
|
||||
$this->_return_message(
|
||||
1, I18n::_(
|
||||
'Please wait %d seconds between each post.',
|
||||
$this->_conf->getKey('limit', 'traffic')
|
||||
)
|
||||
);
|
||||
try {
|
||||
TrafficLimiter::canPass();
|
||||
} catch (Exception $e) {
|
||||
$this->_return_message(1, $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -250,7 +279,14 @@ class Controller
|
|||
}
|
||||
// The user posts a standard paste.
|
||||
else {
|
||||
$this->_model->purge();
|
||||
try {
|
||||
$this->_model->purge();
|
||||
} catch (Exception $e) {
|
||||
error_log('Error purging pastes: ' . $e->getMessage() . PHP_EOL .
|
||||
'Use the administration scripts statistics to find ' .
|
||||
'damaged paste IDs and either delete them or restore them ' .
|
||||
'from backup.');
|
||||
}
|
||||
$paste = $this->_model->getPaste();
|
||||
try {
|
||||
$paste->setData($data);
|
||||
|
@ -280,7 +316,8 @@ class Controller
|
|||
if (hash_equals($paste->getDeleteToken(), $deletetoken)) {
|
||||
// Paste exists and deletion token is valid: Delete the paste.
|
||||
$paste->delete();
|
||||
$this->_status = 'Paste was properly deleted.';
|
||||
$this->_status = 'Paste was properly deleted.';
|
||||
$this->_is_deleted = true;
|
||||
} else {
|
||||
$this->_error = 'Wrong deletion token. Paste was not deleted.';
|
||||
}
|
||||
|
@ -291,10 +328,10 @@ class Controller
|
|||
$this->_error = $e->getMessage();
|
||||
}
|
||||
if ($this->_request->isJsonApiCall()) {
|
||||
if (strlen($this->_error)) {
|
||||
$this->_return_message(1, $this->_error);
|
||||
} else {
|
||||
if (empty($this->_error)) {
|
||||
$this->_return_message(0, $dataid);
|
||||
} else {
|
||||
$this->_return_message(1, $this->_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -334,18 +371,14 @@ class Controller
|
|||
*/
|
||||
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('Cross-Origin-Resource-Policy: same-origin');
|
||||
header('Cross-Origin-Embedder-Policy: require-corp');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
header('Permissions-Policy: interest-cohort=()');
|
||||
// disabled, because it prevents links from a paste to the same site to
|
||||
// be opened. Didn't work with `same-origin-allow-popups` either.
|
||||
// See issue https://github.com/PrivateBin/PrivateBin/issues/970 for details.
|
||||
// header('Cross-Origin-Opener-Policy: same-origin');
|
||||
header('Permissions-Policy: browsing-topics=()');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: deny');
|
||||
|
@ -364,14 +397,31 @@ class Controller
|
|||
$languageselection = '';
|
||||
if ($this->_conf->getKey('languageselection')) {
|
||||
$languageselection = I18n::getLanguage();
|
||||
setcookie('lang', $languageselection, 0, '', '', true);
|
||||
setcookie('lang', $languageselection, array('SameSite' => 'Lax', 'Secure' => true));
|
||||
}
|
||||
|
||||
// strip policies that are unsupported in meta tag
|
||||
$metacspheader = str_replace(
|
||||
array(
|
||||
'frame-ancestors \'none\'; ',
|
||||
'; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
|
||||
),
|
||||
'',
|
||||
$this->_conf->getKey('cspheader')
|
||||
);
|
||||
|
||||
$page = new View;
|
||||
$page->assign('NAME', $this->_conf->getKey('name'));
|
||||
$page->assign('BASEPATH', I18n::_($this->_conf->getKey('basepath')));
|
||||
$page->assign('CSPHEADER', $metacspheader);
|
||||
$page->assign('ERROR', I18n::_($this->_error));
|
||||
$page->assign('NAME', $this->_conf->getKey('name'));
|
||||
if ($this->_request->getOperation() === 'yourlsproxy') {
|
||||
$page->assign('SHORTURL', $this->_status);
|
||||
$page->draw('yourlsproxy');
|
||||
return;
|
||||
}
|
||||
$page->assign('BASEPATH', I18n::_($this->_conf->getKey('basepath')));
|
||||
$page->assign('STATUS', I18n::_($this->_status));
|
||||
$page->assign('ISDELETED', I18n::_(json_encode($this->_is_deleted)));
|
||||
$page->assign('VERSION', self::VERSION);
|
||||
$page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
|
||||
$page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
|
||||
|
@ -392,9 +442,11 @@ class Controller
|
|||
$page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
|
||||
$page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
|
||||
$page->assign('QRCODE', $this->_conf->getKey('qrcode'));
|
||||
$page->assign('EMAIL', $this->_conf->getKey('email'));
|
||||
$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->assign('SRI', $this->_conf->getSection('sri'));
|
||||
$page->draw($this->_conf->getKey('template'));
|
||||
}
|
||||
|
||||
|
@ -406,10 +458,13 @@ class Controller
|
|||
*/
|
||||
private function _jsonld($type)
|
||||
{
|
||||
if (
|
||||
$type !== 'paste' && $type !== 'comment' &&
|
||||
$type !== 'pastemeta' && $type !== 'commentmeta'
|
||||
) {
|
||||
if (!in_array($type, array(
|
||||
'comment',
|
||||
'commentmeta',
|
||||
'paste',
|
||||
'pastemeta',
|
||||
'types',
|
||||
))) {
|
||||
$type = '';
|
||||
}
|
||||
$content = '{}';
|
||||
|
@ -421,6 +476,13 @@ class Controller
|
|||
file_get_contents($file)
|
||||
);
|
||||
}
|
||||
if ($type === 'types') {
|
||||
$content = str_replace(
|
||||
implode('", "', array_keys($this->_conf->getDefaults()['expire_options'])),
|
||||
implode('", "', array_keys($this->_conf->getSection('expire_options'))),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
header('Content-type: application/ld+json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
@ -428,6 +490,22 @@ class Controller
|
|||
echo $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* proxies link to YOURLS, updates status or error with response
|
||||
*
|
||||
* @access private
|
||||
* @param string $link
|
||||
*/
|
||||
private function _yourlsproxy($link)
|
||||
{
|
||||
$yourls = new YourlsProxy($this->_conf, $link);
|
||||
if ($yourls->isError()) {
|
||||
$this->_error = $yourls->getError();
|
||||
} else {
|
||||
$this->_status = $yourls->getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* prepares JSON encoded status message
|
||||
*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Data;
|
||||
|
@ -15,61 +14,17 @@ namespace PrivateBin\Data;
|
|||
/**
|
||||
* AbstractData
|
||||
*
|
||||
* Abstract model for data access, implemented as a singleton.
|
||||
* Abstract model for data access
|
||||
*/
|
||||
abstract class AbstractData
|
||||
{
|
||||
/**
|
||||
* Singleton instance
|
||||
*
|
||||
* @access protected
|
||||
* @static
|
||||
* @var AbstractData
|
||||
*/
|
||||
protected static $_instance = null;
|
||||
|
||||
/**
|
||||
* cache for the traffic limiter
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected static $_last_cache = array();
|
||||
|
||||
/**
|
||||
* Enforce singleton, disable constructor
|
||||
*
|
||||
* Instantiate using {@link getInstance()}, this object implements the singleton pattern.
|
||||
*
|
||||
* @access protected
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce singleton, disable cloning
|
||||
*
|
||||
* Instantiate using {@link getInstance()}, this object implements the singleton pattern.
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function __clone()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance of singleton
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @return AbstractData
|
||||
*/
|
||||
public static function getInstance(array $options)
|
||||
{
|
||||
}
|
||||
protected $_last_cache = array();
|
||||
|
||||
/**
|
||||
* Create a paste.
|
||||
|
@ -150,9 +105,9 @@ abstract class AbstractData
|
|||
public function purgeValues($namespace, $time)
|
||||
{
|
||||
if ($namespace === 'traffic_limiter') {
|
||||
foreach (self::$_last_cache as $key => $last_submission) {
|
||||
foreach ($this->_last_cache as $key => $last_submission) {
|
||||
if ($last_submission <= $time) {
|
||||
unset(self::$_last_cache[$key]);
|
||||
unset($this->_last_cache[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,6 +162,14 @@ abstract class AbstractData
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all paste ids
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getAllPastes();
|
||||
|
||||
/**
|
||||
* Get next free slot for comment from postdate.
|
||||
*
|
||||
|
@ -218,7 +181,7 @@ abstract class AbstractData
|
|||
protected function getOpenSlot(array &$comments, $postdate)
|
||||
{
|
||||
if (array_key_exists($postdate, $comments)) {
|
||||
$parts = explode('.', $postdate, 2);
|
||||
$parts = explode('.', (string) $postdate, 2);
|
||||
if (!array_key_exists(1, $parts)) {
|
||||
$parts[1] = 0;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,12 +7,12 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Data;
|
||||
|
||||
use Exception;
|
||||
use GlobIterator;
|
||||
use PrivateBin\Json;
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,22 @@ use PrivateBin\Json;
|
|||
*/
|
||||
class Filesystem extends AbstractData
|
||||
{
|
||||
/**
|
||||
* glob() pattern of the two folder levels and the paste files under the
|
||||
* configured path. Needs to return both files with and without .php suffix,
|
||||
* so they can be hardened by _prependRename(), which is hooked into exists().
|
||||
*
|
||||
* > Note that wildcard patterns are not regular expressions, although they
|
||||
* > are a bit similar.
|
||||
*
|
||||
* @link https://man7.org/linux/man-pages/man7/glob.7.html
|
||||
* @const string
|
||||
*/
|
||||
const PASTE_FILE_PATTERN = DIRECTORY_SEPARATOR . '[a-f0-9][a-f0-9]' .
|
||||
DIRECTORY_SEPARATOR . '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR .
|
||||
'[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]' .
|
||||
'[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]*';
|
||||
|
||||
/**
|
||||
* first line in paste or comment files, to protect their contents from browsing exposed data directories
|
||||
*
|
||||
|
@ -40,33 +56,25 @@ class Filesystem extends AbstractData
|
|||
* path in which to persist something
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string
|
||||
*/
|
||||
private static $_path = 'data';
|
||||
private $_path = 'data';
|
||||
|
||||
/**
|
||||
* get instance of singleton
|
||||
* instantiates a new Filesystem data backend
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @return Filesystem
|
||||
*/
|
||||
public static function getInstance(array $options)
|
||||
public function __construct(array $options)
|
||||
{
|
||||
// if needed initialize the singleton
|
||||
if (!(self::$_instance instanceof self)) {
|
||||
self::$_instance = new self;
|
||||
}
|
||||
// if given update the data directory
|
||||
if (
|
||||
is_array($options) &&
|
||||
array_key_exists('dir', $options)
|
||||
) {
|
||||
self::$_path = $options['dir'];
|
||||
$this->_path = $options['dir'];
|
||||
}
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,7 +87,7 @@ class Filesystem extends AbstractData
|
|||
*/
|
||||
public function create($pasteid, array $paste)
|
||||
{
|
||||
$storagedir = self::_dataid2path($pasteid);
|
||||
$storagedir = $this->_dataid2path($pasteid);
|
||||
$file = $storagedir . $pasteid . '.php';
|
||||
if (is_file($file)) {
|
||||
return false;
|
||||
|
@ -87,7 +95,7 @@ class Filesystem extends AbstractData
|
|||
if (!is_dir($storagedir)) {
|
||||
mkdir($storagedir, 0700, true);
|
||||
}
|
||||
return self::_store($file, $paste);
|
||||
return $this->_store($file, $paste);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +109,7 @@ class Filesystem extends AbstractData
|
|||
{
|
||||
if (
|
||||
!$this->exists($pasteid) ||
|
||||
!$paste = self::_get(self::_dataid2path($pasteid) . $pasteid . '.php')
|
||||
!$paste = $this->_get($this->_dataid2path($pasteid) . $pasteid . '.php')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
@ -116,7 +124,7 @@ class Filesystem extends AbstractData
|
|||
*/
|
||||
public function delete($pasteid)
|
||||
{
|
||||
$pastedir = self::_dataid2path($pasteid);
|
||||
$pastedir = $this->_dataid2path($pasteid);
|
||||
if (is_dir($pastedir)) {
|
||||
// Delete the paste itself.
|
||||
if (is_file($pastedir . $pasteid . '.php')) {
|
||||
|
@ -124,7 +132,7 @@ class Filesystem extends AbstractData
|
|||
}
|
||||
|
||||
// Delete discussion if it exists.
|
||||
$discdir = self::_dataid2discussionpath($pasteid);
|
||||
$discdir = $this->_dataid2discussionpath($pasteid);
|
||||
if (is_dir($discdir)) {
|
||||
// Delete all files in discussion directory
|
||||
$dir = dir($discdir);
|
||||
|
@ -148,20 +156,20 @@ class Filesystem extends AbstractData
|
|||
*/
|
||||
public function exists($pasteid)
|
||||
{
|
||||
$basePath = self::_dataid2path($pasteid) . $pasteid;
|
||||
$basePath = $this->_dataid2path($pasteid) . $pasteid;
|
||||
$pastePath = $basePath . '.php';
|
||||
// convert to PHP protected files if needed
|
||||
if (is_readable($basePath)) {
|
||||
self::_prependRename($basePath, $pastePath);
|
||||
$this->_prependRename($basePath, $pastePath);
|
||||
|
||||
// convert comments, too
|
||||
$discdir = self::_dataid2discussionpath($pasteid);
|
||||
$discdir = $this->_dataid2discussionpath($pasteid);
|
||||
if (is_dir($discdir)) {
|
||||
$dir = dir($discdir);
|
||||
while (false !== ($filename = $dir->read())) {
|
||||
if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) {
|
||||
$commentFilename = $discdir . $filename . '.php';
|
||||
self::_prependRename($discdir . $filename, $commentFilename);
|
||||
$this->_prependRename($discdir . $filename, $commentFilename);
|
||||
}
|
||||
}
|
||||
$dir->close();
|
||||
|
@ -182,7 +190,7 @@ class Filesystem extends AbstractData
|
|||
*/
|
||||
public function createComment($pasteid, $parentid, $commentid, array $comment)
|
||||
{
|
||||
$storagedir = self::_dataid2discussionpath($pasteid);
|
||||
$storagedir = $this->_dataid2discussionpath($pasteid);
|
||||
$file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php';
|
||||
if (is_file($file)) {
|
||||
return false;
|
||||
|
@ -190,7 +198,7 @@ class Filesystem extends AbstractData
|
|||
if (!is_dir($storagedir)) {
|
||||
mkdir($storagedir, 0700, true);
|
||||
}
|
||||
return self::_store($file, $comment);
|
||||
return $this->_store($file, $comment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,7 +211,7 @@ class Filesystem extends AbstractData
|
|||
public function readComments($pasteid)
|
||||
{
|
||||
$comments = array();
|
||||
$discdir = self::_dataid2discussionpath($pasteid);
|
||||
$discdir = $this->_dataid2discussionpath($pasteid);
|
||||
if (is_dir($discdir)) {
|
||||
$dir = dir($discdir);
|
||||
while (false !== ($filename = $dir->read())) {
|
||||
|
@ -212,14 +220,19 @@ class Filesystem extends AbstractData
|
|||
// - commentid is the comment identifier itself.
|
||||
// - parentid is the comment this comment replies to (It can be pasteid)
|
||||
if (is_file($discdir . $filename)) {
|
||||
$comment = self::_get($discdir . $filename);
|
||||
$comment = $this->_get($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']['created']);
|
||||
$key = $this->getOpenSlot(
|
||||
$comments,
|
||||
(int) array_key_exists('created', $comment['meta']) ?
|
||||
$comment['meta']['created'] : // v2 comments
|
||||
$comment['meta']['postdate'] // v1 comments
|
||||
);
|
||||
$comments[$key] = $comment;
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +256,7 @@ class Filesystem extends AbstractData
|
|||
public function existsComment($pasteid, $parentid, $commentid)
|
||||
{
|
||||
return is_file(
|
||||
self::_dataid2discussionpath($pasteid) .
|
||||
$this->_dataid2discussionpath($pasteid) .
|
||||
$pasteid . '.' . $commentid . '.' . $parentid . '.php'
|
||||
);
|
||||
}
|
||||
|
@ -261,20 +274,20 @@ class Filesystem extends AbstractData
|
|||
{
|
||||
switch ($namespace) {
|
||||
case 'purge_limiter':
|
||||
return self::_storeString(
|
||||
self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
|
||||
return $this->_storeString(
|
||||
$this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
|
||||
'<?php' . PHP_EOL . '$GLOBALS[\'purge_limiter\'] = ' . $value . ';'
|
||||
);
|
||||
case 'salt':
|
||||
return self::_storeString(
|
||||
self::$_path . DIRECTORY_SEPARATOR . 'salt.php',
|
||||
return $this->_storeString(
|
||||
$this->_path . DIRECTORY_SEPARATOR . 'salt.php',
|
||||
'<?php # |' . $value . '|'
|
||||
);
|
||||
case 'traffic_limiter':
|
||||
self::$_last_cache[$key] = $value;
|
||||
return self::_storeString(
|
||||
self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
|
||||
'<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export(self::$_last_cache, true) . ';'
|
||||
$this->_last_cache[$key] = $value;
|
||||
return $this->_storeString(
|
||||
$this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
|
||||
'<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export($this->_last_cache, true) . ';'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
@ -292,14 +305,14 @@ class Filesystem extends AbstractData
|
|||
{
|
||||
switch ($namespace) {
|
||||
case 'purge_limiter':
|
||||
$file = self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
|
||||
$file = $this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
|
||||
if (is_readable($file)) {
|
||||
require $file;
|
||||
return $GLOBALS['purge_limiter'];
|
||||
}
|
||||
break;
|
||||
case 'salt':
|
||||
$file = self::$_path . DIRECTORY_SEPARATOR . 'salt.php';
|
||||
$file = $this->_path . DIRECTORY_SEPARATOR . 'salt.php';
|
||||
if (is_readable($file)) {
|
||||
$items = explode('|', file_get_contents($file));
|
||||
if (is_array($items) && count($items) == 3) {
|
||||
|
@ -308,12 +321,12 @@ class Filesystem extends AbstractData
|
|||
}
|
||||
break;
|
||||
case 'traffic_limiter':
|
||||
$file = self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
|
||||
$file = $this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
|
||||
if (is_readable($file)) {
|
||||
require $file;
|
||||
self::$_last_cache = $GLOBALS['traffic_limiter'];
|
||||
if (array_key_exists($key, self::$_last_cache)) {
|
||||
return self::$_last_cache[$key];
|
||||
$this->_last_cache = $GLOBALS['traffic_limiter'];
|
||||
if (array_key_exists($key, $this->_last_cache)) {
|
||||
return $this->_last_cache[$key];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -325,11 +338,10 @@ class Filesystem extends AbstractData
|
|||
* get the data
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $filename
|
||||
* @return array|false $data
|
||||
*/
|
||||
private static function _get($filename)
|
||||
private function _get($filename)
|
||||
{
|
||||
return Json::decode(
|
||||
substr(
|
||||
|
@ -348,65 +360,42 @@ class Filesystem extends AbstractData
|
|||
*/
|
||||
protected function _getExpiredPastes($batchsize)
|
||||
{
|
||||
$pastes = array();
|
||||
$firstLevel = array_filter(
|
||||
scandir(self::$_path),
|
||||
'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::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
|
||||
'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::$_path . DIRECTORY_SEPARATOR .
|
||||
$firstLevel[$firstKey] . DIRECTORY_SEPARATOR .
|
||||
$secondLevel[$secondKey];
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
$thirdLevel = array_filter(
|
||||
array_map(
|
||||
function ($filename) {
|
||||
return strlen($filename) >= 20 ?
|
||||
substr($filename, 0, -4) :
|
||||
$filename;
|
||||
},
|
||||
scandir($path)
|
||||
),
|
||||
'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 (
|
||||
array_key_exists('expire_date', $data['meta']) &&
|
||||
$data['meta']['expire_date'] < time()
|
||||
) {
|
||||
$pastes[] = $pasteid;
|
||||
if (count($pastes) >= $batchsize) {
|
||||
break;
|
||||
}
|
||||
$pastes = array();
|
||||
$count = 0;
|
||||
$opened = 0;
|
||||
$limit = $batchsize * 10; // try at most 10 times $batchsize pastes before giving up
|
||||
$time = time();
|
||||
$files = $this->getAllPastes();
|
||||
shuffle($files);
|
||||
foreach ($files as $pasteid) {
|
||||
if ($this->exists($pasteid)) {
|
||||
$data = $this->read($pasteid);
|
||||
if (
|
||||
array_key_exists('expire_date', $data['meta']) &&
|
||||
$data['meta']['expire_date'] < $time
|
||||
) {
|
||||
$pastes[] = $pasteid;
|
||||
if (++$count >= $batchsize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (++$opened >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pastes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAllPastes()
|
||||
{
|
||||
$pastes = array();
|
||||
foreach (new GlobIterator($this->_path . self::PASTE_FILE_PATTERN) as $file) {
|
||||
if ($file->isFile()) {
|
||||
$pastes[] = $file->getBasename('.php');
|
||||
}
|
||||
}
|
||||
return $pastes;
|
||||
|
@ -423,13 +412,12 @@ class Filesystem extends AbstractData
|
|||
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $dataid
|
||||
* @return string
|
||||
*/
|
||||
private static function _dataid2path($dataid)
|
||||
private function _dataid2path($dataid)
|
||||
{
|
||||
return self::$_path . DIRECTORY_SEPARATOR .
|
||||
return $this->_path . DIRECTORY_SEPARATOR .
|
||||
substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
|
||||
substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
@ -440,56 +428,27 @@ class Filesystem extends AbstractData
|
|||
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @param string $dataid
|
||||
* @return string
|
||||
*/
|
||||
private static function _dataid2discussionpath($dataid)
|
||||
private function _dataid2discussionpath($dataid)
|
||||
{
|
||||
return self::_dataid2path($dataid) . $dataid .
|
||||
return $this->_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::$_path . 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* store the data
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $filename
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
private static function _store($filename, array $data)
|
||||
private function _store($filename, array $data)
|
||||
{
|
||||
try {
|
||||
return self::_storeString(
|
||||
return $this->_storeString(
|
||||
$filename,
|
||||
self::PROTECTION_LINE . PHP_EOL . Json::encode($data)
|
||||
);
|
||||
|
@ -502,20 +461,19 @@ class Filesystem extends AbstractData
|
|||
* store a string
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $filename
|
||||
* @param string $data
|
||||
* @return bool
|
||||
*/
|
||||
private static function _storeString($filename, $data)
|
||||
private function _storeString($filename, $data)
|
||||
{
|
||||
// Create storage directory if it does not exist.
|
||||
if (!is_dir(self::$_path)) {
|
||||
if (!@mkdir(self::$_path, 0700)) {
|
||||
if (!is_dir($this->_path)) {
|
||||
if (!@mkdir($this->_path, 0700)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
|
||||
$file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
|
||||
if (!is_file($file)) {
|
||||
$writtenBytes = 0;
|
||||
if ($fileCreated = @touch($file)) {
|
||||
|
@ -553,12 +511,11 @@ class Filesystem extends AbstractData
|
|||
* rename a file, prepending the protection line at the beginning
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $srcFile
|
||||
* @param string $destFile
|
||||
* @return void
|
||||
*/
|
||||
private static function _prependRename($srcFile, $destFile)
|
||||
private function _prependRename($srcFile, $destFile)
|
||||
{
|
||||
// don't overwrite already converted file
|
||||
if (!is_readable($destFile)) {
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Data;
|
||||
|
||||
|
@ -14,45 +23,42 @@ class GoogleCloudStorage extends AbstractData
|
|||
* GCS client
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var StorageClient
|
||||
*/
|
||||
private static $_client = null;
|
||||
private $_client = null;
|
||||
|
||||
/**
|
||||
* GCS bucket
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var Bucket
|
||||
*/
|
||||
private static $_bucket = null;
|
||||
private $_bucket = null;
|
||||
|
||||
/**
|
||||
* object prefix
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string
|
||||
*/
|
||||
private static $_prefix = 'pastes';
|
||||
private $_prefix = 'pastes';
|
||||
|
||||
/**
|
||||
* returns a Google Cloud Storage data backend.
|
||||
* bucket acl type
|
||||
*
|
||||
* @access private
|
||||
* @var bool
|
||||
*/
|
||||
private $_uniformacl = false;
|
||||
|
||||
/**
|
||||
* instantiantes a new Google Cloud Storage data backend.
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param array $options
|
||||
* @return GoogleCloudStorage
|
||||
*/
|
||||
public static function getInstance(array $options)
|
||||
public function __construct(array $options)
|
||||
{
|
||||
// if needed initialize the singleton
|
||||
if (!(self::$_instance instanceof self)) {
|
||||
self::$_instance = new self;
|
||||
}
|
||||
|
||||
$bucket = null;
|
||||
if (getenv('PRIVATEBIN_GCS_BUCKET')) {
|
||||
$bucket = getenv('PRIVATEBIN_GCS_BUCKET');
|
||||
}
|
||||
|
@ -60,21 +66,22 @@ class GoogleCloudStorage extends AbstractData
|
|||
$bucket = $options['bucket'];
|
||||
}
|
||||
if (is_array($options) && array_key_exists('prefix', $options)) {
|
||||
self::$_prefix = $options['prefix'];
|
||||
$this->_prefix = $options['prefix'];
|
||||
}
|
||||
if (is_array($options) && array_key_exists('uniformacl', $options)) {
|
||||
$this->_uniformacl = $options['uniformacl'];
|
||||
}
|
||||
|
||||
if (empty(self::$_client)) {
|
||||
self::$_client = class_exists('StorageClientStub', false) ?
|
||||
new \StorageClientStub(array()) :
|
||||
new StorageClient(array('suppressKeyFileNotice' => true));
|
||||
$this->_client = class_exists('StorageClientStub', false) ?
|
||||
new \StorageClientStub(array()) :
|
||||
new StorageClient(array('suppressKeyFileNotice' => true));
|
||||
if (isset($bucket)) {
|
||||
$this->_bucket = $this->_client->bucket($bucket);
|
||||
}
|
||||
self::$_bucket = self::$_client->bucket($bucket);
|
||||
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the google storage object key for $pasteid in self::$_bucket.
|
||||
* returns the google storage object key for $pasteid in $this->_bucket.
|
||||
*
|
||||
* @access private
|
||||
* @param $pasteid string to get the key for
|
||||
|
@ -82,14 +89,14 @@ class GoogleCloudStorage extends AbstractData
|
|||
*/
|
||||
private function _getKey($pasteid)
|
||||
{
|
||||
if (self::$_prefix != '') {
|
||||
return self::$_prefix . '/' . $pasteid;
|
||||
if ($this->_prefix != '') {
|
||||
return $this->_prefix . '/' . $pasteid;
|
||||
}
|
||||
return $pasteid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the payload in the self::$_bucket under the specified key.
|
||||
* Uploads the payload in the $this->_bucket under the specified key.
|
||||
* The entire payload is stored as a JSON document. The metadata is replicated
|
||||
* as the GCS object's metadata except for the fields attachment, attachmentname
|
||||
* and salt.
|
||||
|
@ -106,17 +113,20 @@ class GoogleCloudStorage extends AbstractData
|
|||
$metadata[$k] = strval($v);
|
||||
}
|
||||
try {
|
||||
self::$_bucket->upload(Json::encode($payload), array(
|
||||
$data = array(
|
||||
'name' => $key,
|
||||
'chunkSize' => 262144,
|
||||
'predefinedAcl' => 'private',
|
||||
'metadata' => array(
|
||||
'content-type' => 'application/json',
|
||||
'metadata' => $metadata,
|
||||
),
|
||||
));
|
||||
);
|
||||
if (!$this->_uniformacl) {
|
||||
$data['predefinedAcl'] = 'private';
|
||||
}
|
||||
$this->_bucket->upload(Json::encode($payload), $data);
|
||||
} catch (Exception $e) {
|
||||
error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' .
|
||||
error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
|
@ -141,13 +151,13 @@ class GoogleCloudStorage extends AbstractData
|
|||
public function read($pasteid)
|
||||
{
|
||||
try {
|
||||
$o = self::$_bucket->object($this->_getKey($pasteid));
|
||||
$o = $this->_bucket->object($this->_getKey($pasteid));
|
||||
$data = $o->downloadAsString();
|
||||
return Json::decode($data);
|
||||
} catch (NotFoundException $e) {
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket->name() . ', ' .
|
||||
error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
|
@ -161,9 +171,9 @@ class GoogleCloudStorage extends AbstractData
|
|||
$name = $this->_getKey($pasteid);
|
||||
|
||||
try {
|
||||
foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
|
||||
foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
|
||||
try {
|
||||
self::$_bucket->object($comment->name())->delete();
|
||||
$this->_bucket->object($comment->name())->delete();
|
||||
} catch (NotFoundException $e) {
|
||||
// ignore if already deleted.
|
||||
}
|
||||
|
@ -173,7 +183,7 @@ class GoogleCloudStorage extends AbstractData
|
|||
}
|
||||
|
||||
try {
|
||||
self::$_bucket->object($name)->delete();
|
||||
$this->_bucket->object($name)->delete();
|
||||
} catch (NotFoundException $e) {
|
||||
// ignore if already deleted
|
||||
}
|
||||
|
@ -184,7 +194,7 @@ class GoogleCloudStorage extends AbstractData
|
|||
*/
|
||||
public function exists($pasteid)
|
||||
{
|
||||
$o = self::$_bucket->object($this->_getKey($pasteid));
|
||||
$o = $this->_bucket->object($this->_getKey($pasteid));
|
||||
return $o->exists();
|
||||
}
|
||||
|
||||
|
@ -208,8 +218,8 @@ class GoogleCloudStorage extends AbstractData
|
|||
$comments = array();
|
||||
$prefix = $this->_getKey($pasteid) . '/discussion/';
|
||||
try {
|
||||
foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) {
|
||||
$comment = JSON::decode(self::$_bucket->object($key->name())->downloadAsString());
|
||||
foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
|
||||
$comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
|
||||
$comment['id'] = basename($key->name());
|
||||
$slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
|
||||
$comments[$slot] = $comment;
|
||||
|
@ -226,7 +236,7 @@ class GoogleCloudStorage extends AbstractData
|
|||
public function existsComment($pasteid, $parentid, $commentid)
|
||||
{
|
||||
$name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
|
||||
$o = self::$_bucket->object($name);
|
||||
$o = $this->_bucket->object($name);
|
||||
return $o->exists();
|
||||
}
|
||||
|
||||
|
@ -237,7 +247,7 @@ class GoogleCloudStorage extends AbstractData
|
|||
{
|
||||
$path = 'config/' . $namespace;
|
||||
try {
|
||||
foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) {
|
||||
foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
|
||||
$name = $object->name();
|
||||
if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
|
||||
continue;
|
||||
|
@ -277,17 +287,20 @@ class GoogleCloudStorage extends AbstractData
|
|||
$metadata['value'] = strval($value);
|
||||
}
|
||||
try {
|
||||
self::$_bucket->upload($value, array(
|
||||
$data = array(
|
||||
'name' => $key,
|
||||
'chunkSize' => 262144,
|
||||
'predefinedAcl' => 'private',
|
||||
'metadata' => array(
|
||||
'content-type' => 'application/json',
|
||||
'metadata' => $metadata,
|
||||
),
|
||||
));
|
||||
);
|
||||
if (!$this->_uniformacl) {
|
||||
$data['predefinedAcl'] = 'private';
|
||||
}
|
||||
$this->_bucket->upload($value, $data);
|
||||
} catch (Exception $e) {
|
||||
error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' .
|
||||
error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
|
@ -305,7 +318,7 @@ class GoogleCloudStorage extends AbstractData
|
|||
$key = 'config/' . $namespace . '/' . $key;
|
||||
}
|
||||
try {
|
||||
$o = self::$_bucket->object($key);
|
||||
$o = $this->_bucket->object($key);
|
||||
return $o->downloadAsString();
|
||||
} catch (NotFoundException $e) {
|
||||
return '';
|
||||
|
@ -320,12 +333,12 @@ class GoogleCloudStorage extends AbstractData
|
|||
$expired = array();
|
||||
|
||||
$now = time();
|
||||
$prefix = self::$_prefix;
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
try {
|
||||
foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) {
|
||||
foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
|
||||
$metadata = $object->info()['metadata'];
|
||||
if ($metadata != null && array_key_exists('expire_date', $metadata)) {
|
||||
$expire_at = intval($metadata['expire_date']);
|
||||
|
@ -343,4 +356,28 @@ class GoogleCloudStorage extends AbstractData
|
|||
}
|
||||
return $expired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAllPastes()
|
||||
{
|
||||
$pastes = array();
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
|
||||
$candidate = substr($object->name(), strlen($prefix));
|
||||
if (strpos($candidate, '/') === false) {
|
||||
$pastes[] = $candidate;
|
||||
}
|
||||
}
|
||||
} catch (NotFoundException $e) {
|
||||
// no objects in the bucket yet
|
||||
}
|
||||
return $pastes;
|
||||
}
|
||||
}
|
||||
|
|
474
lib/Data/S3Storage.php
Normal file
474
lib/Data/S3Storage.php
Normal file
|
@ -0,0 +1,474 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
* a zero-knowledge paste bin
|
||||
*
|
||||
* @link https://github.com/PrivateBin/PrivateBin
|
||||
* @copyright 2022 Felix J. Ogris (https://ogris.de/)
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
*
|
||||
* an S3 compatible data backend for PrivateBin with CEPH/RadosGW in mind
|
||||
* see https://docs.ceph.com/en/latest/radosgw/s3/php/
|
||||
*
|
||||
* Installation:
|
||||
* 1. Make sure you have composer.lock and composer.json in the document root of your PasteBin
|
||||
* 2. If not, grab a copy from https://github.com/PrivateBin/PrivateBin
|
||||
* 3. As non-root user, install the AWS SDK for PHP:
|
||||
* composer require aws/aws-sdk-php
|
||||
* (On FreeBSD, install devel/php-composer2 prior, e.g.: make -C /usr/ports/devel/php-composer2 install clean)
|
||||
* 4. In cfg/conf.php, comment out all [model] and [model_options] settings
|
||||
* 5. Still in cfg/conf.php, add a new [model] section:
|
||||
* [model]
|
||||
* class = S3Storage
|
||||
* 6. Add a new [model_options] as well, e.g. for a Rados gateway as part of your CEPH cluster:
|
||||
* [model_options]
|
||||
* region = ""
|
||||
* version = "2006-03-01"
|
||||
* endpoint = "https://s3.my-ceph.invalid"
|
||||
* use_path_style_endpoint = true
|
||||
* bucket = "my-bucket"
|
||||
* prefix = "privatebin" (place all PrivateBin data beneath this prefix)
|
||||
* accesskey = "my-rados-user"
|
||||
* secretkey = "my-rados-pass"
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Data;
|
||||
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use Aws\S3\S3Client;
|
||||
use PrivateBin\Json;
|
||||
|
||||
class S3Storage extends AbstractData
|
||||
{
|
||||
/**
|
||||
* S3 client
|
||||
*
|
||||
* @access private
|
||||
* @var S3Client
|
||||
*/
|
||||
private $_client = null;
|
||||
|
||||
/**
|
||||
* S3 client options
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $_options = array();
|
||||
|
||||
/**
|
||||
* S3 bucket
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $_bucket = null;
|
||||
|
||||
/**
|
||||
* S3 prefix for all PrivateBin data in this bucket
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $_prefix = '';
|
||||
|
||||
/**
|
||||
* instantiates a new S3 data backend.
|
||||
*
|
||||
* @access public
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (is_array($options)) {
|
||||
// AWS SDK will try to load credentials from environment if credentials are not passed via configuration
|
||||
// ref: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials.html#default-credential-chain
|
||||
if (isset($options['accesskey']) && isset($options['secretkey'])) {
|
||||
$this->_options['credentials'] = array();
|
||||
|
||||
$this->_options['credentials']['key'] = $options['accesskey'];
|
||||
$this->_options['credentials']['secret'] = $options['secretkey'];
|
||||
}
|
||||
if (array_key_exists('region', $options)) {
|
||||
$this->_options['region'] = $options['region'];
|
||||
}
|
||||
if (array_key_exists('version', $options)) {
|
||||
$this->_options['version'] = $options['version'];
|
||||
}
|
||||
if (array_key_exists('endpoint', $options)) {
|
||||
$this->_options['endpoint'] = $options['endpoint'];
|
||||
}
|
||||
if (array_key_exists('use_path_style_endpoint', $options)) {
|
||||
$this->_options['use_path_style_endpoint'] = filter_var($options['use_path_style_endpoint'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
if (array_key_exists('bucket', $options)) {
|
||||
$this->_bucket = $options['bucket'];
|
||||
}
|
||||
if (array_key_exists('prefix', $options)) {
|
||||
$this->_prefix = $options['prefix'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->_client = new S3Client($this->_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all objects in the given prefix.
|
||||
*
|
||||
* @access private
|
||||
* @param $prefix string with prefix
|
||||
* @return array all objects in the given prefix
|
||||
*/
|
||||
private function _listAllObjects($prefix)
|
||||
{
|
||||
$allObjects = array();
|
||||
$options = array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Prefix' => $prefix,
|
||||
);
|
||||
|
||||
do {
|
||||
$objectsListResponse = $this->_client->listObjects($options);
|
||||
$objects = $objectsListResponse['Contents'] ?? array();
|
||||
foreach ($objects as $object) {
|
||||
$allObjects[] = $object;
|
||||
$options['Marker'] = $object['Key'];
|
||||
}
|
||||
} while ($objectsListResponse['IsTruncated']);
|
||||
|
||||
return $allObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the S3 storage object key for $pasteid in $this->_bucket.
|
||||
*
|
||||
* @access private
|
||||
* @param $pasteid string to get the key for
|
||||
* @return string
|
||||
*/
|
||||
private function _getKey($pasteid)
|
||||
{
|
||||
if ($this->_prefix != '') {
|
||||
return $this->_prefix . '/' . $pasteid;
|
||||
}
|
||||
return $pasteid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the payload in the $this->_bucket under the specified key.
|
||||
* The entire payload is stored as a JSON document. The metadata is replicated
|
||||
* as the S3 object's metadata except for the fields attachment, attachmentname
|
||||
* and salt.
|
||||
*
|
||||
* @param $key string to store the payload under
|
||||
* @param $payload array to store
|
||||
* @return bool true if successful, otherwise false.
|
||||
*/
|
||||
private function _upload($key, $payload)
|
||||
{
|
||||
$metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
|
||||
unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
|
||||
foreach ($metadata as $k => $v) {
|
||||
$metadata[$k] = strval($v);
|
||||
}
|
||||
try {
|
||||
$this->_client->putObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $key,
|
||||
'Body' => Json::encode($payload),
|
||||
'ContentType' => 'application/json',
|
||||
'Metadata' => $metadata,
|
||||
));
|
||||
} catch (S3Exception $e) {
|
||||
error_log('failed to upload ' . $key . ' to ' . $this->_bucket . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function create($pasteid, array $paste)
|
||||
{
|
||||
if ($this->exists($pasteid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->_upload($this->_getKey($pasteid), $paste);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function read($pasteid)
|
||||
{
|
||||
try {
|
||||
$object = $this->_client->getObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $this->_getKey($pasteid),
|
||||
));
|
||||
$data = $object['Body']->getContents();
|
||||
return Json::decode($data);
|
||||
} catch (S3Exception $e) {
|
||||
error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete($pasteid)
|
||||
{
|
||||
$name = $this->_getKey($pasteid);
|
||||
|
||||
try {
|
||||
$comments = $this->_listAllObjects($name . '/discussion/');
|
||||
foreach ($comments as $comment) {
|
||||
try {
|
||||
$this->_client->deleteObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $comment['Key'],
|
||||
));
|
||||
} catch (S3Exception $e) {
|
||||
// ignore if already deleted.
|
||||
}
|
||||
}
|
||||
} catch (S3Exception $e) {
|
||||
// there are no discussions associated with the paste
|
||||
}
|
||||
|
||||
try {
|
||||
$this->_client->deleteObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $name,
|
||||
));
|
||||
} catch (S3Exception $e) {
|
||||
// ignore if already deleted
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function exists($pasteid)
|
||||
{
|
||||
return $this->_client->doesObjectExistV2($this->_bucket, $this->_getKey($pasteid));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createComment($pasteid, $parentid, $commentid, array $comment)
|
||||
{
|
||||
if ($this->existsComment($pasteid, $parentid, $commentid)) {
|
||||
return false;
|
||||
}
|
||||
$key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
|
||||
return $this->_upload($key, $comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function readComments($pasteid)
|
||||
{
|
||||
$comments = array();
|
||||
$prefix = $this->_getKey($pasteid) . '/discussion/';
|
||||
try {
|
||||
$entries = $this->_listAllObjects($prefix);
|
||||
foreach ($entries as $entry) {
|
||||
$object = $this->_client->getObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $entry['Key'],
|
||||
));
|
||||
$body = JSON::decode($object['Body']->getContents());
|
||||
$items = explode('/', $entry['Key']);
|
||||
$body['id'] = $items[3];
|
||||
$body['parentid'] = $items[2];
|
||||
$slot = $this->getOpenSlot($comments, (int) $object['Metadata']['created']);
|
||||
$comments[$slot] = $body;
|
||||
}
|
||||
} catch (S3Exception $e) {
|
||||
// no comments found
|
||||
}
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function existsComment($pasteid, $parentid, $commentid)
|
||||
{
|
||||
$name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
|
||||
return $this->_client->doesObjectExistV2($this->_bucket, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function purgeValues($namespace, $time)
|
||||
{
|
||||
$path = $this->_prefix;
|
||||
if ($path != '') {
|
||||
$path .= '/';
|
||||
}
|
||||
$path .= 'config/' . $namespace;
|
||||
|
||||
try {
|
||||
foreach ($this->_listAllObjects($path) as $object) {
|
||||
$name = $object['Key'];
|
||||
if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
|
||||
continue;
|
||||
}
|
||||
$head = $this->_client->headObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $name,
|
||||
));
|
||||
if ($head->get('Metadata') != null && array_key_exists('value', $head->get('Metadata'))) {
|
||||
$value = $head->get('Metadata')['value'];
|
||||
if (is_numeric($value) && intval($value) < $time) {
|
||||
try {
|
||||
$this->_client->deleteObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $name,
|
||||
));
|
||||
} catch (S3Exception $e) {
|
||||
// deleted by another instance.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (S3Exception $e) {
|
||||
// no objects in the bucket yet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For S3, the value will also be stored in the metadata for the
|
||||
* namespaces traffic_limiter and purge_limiter.
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setValue($value, $namespace, $key = '')
|
||||
{
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
|
||||
if ($key === '') {
|
||||
$key = $prefix . 'config/' . $namespace;
|
||||
} else {
|
||||
$key = $prefix . 'config/' . $namespace . '/' . $key;
|
||||
}
|
||||
|
||||
$metadata = array('namespace' => $namespace);
|
||||
if ($namespace != 'salt') {
|
||||
$metadata['value'] = strval($value);
|
||||
}
|
||||
try {
|
||||
$this->_client->putObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $key,
|
||||
'Body' => $value,
|
||||
'ContentType' => 'application/json',
|
||||
'Metadata' => $metadata,
|
||||
));
|
||||
} catch (S3Exception $e) {
|
||||
error_log('failed to set key ' . $key . ' to ' . $this->_bucket . ', ' .
|
||||
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getValue($namespace, $key = '')
|
||||
{
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
|
||||
if ($key === '') {
|
||||
$key = $prefix . 'config/' . $namespace;
|
||||
} else {
|
||||
$key = $prefix . 'config/' . $namespace . '/' . $key;
|
||||
}
|
||||
|
||||
try {
|
||||
$object = $this->_client->getObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $key,
|
||||
));
|
||||
return $object['Body']->getContents();
|
||||
} catch (S3Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _getExpiredPastes($batchsize)
|
||||
{
|
||||
$expired = array();
|
||||
$now = time();
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($this->_listAllObjects($prefix) as $object) {
|
||||
$head = $this->_client->headObject(array(
|
||||
'Bucket' => $this->_bucket,
|
||||
'Key' => $object['Key'],
|
||||
));
|
||||
if ($head->get('Metadata') != null && array_key_exists('expire_date', $head->get('Metadata'))) {
|
||||
$expire_at = intval($head->get('Metadata')['expire_date']);
|
||||
if ($expire_at != 0 && $expire_at < $now) {
|
||||
array_push($expired, $object['Key']);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($expired) > $batchsize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (S3Exception $e) {
|
||||
// no objects in the bucket yet
|
||||
}
|
||||
return $expired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAllPastes()
|
||||
{
|
||||
$pastes = array();
|
||||
$prefix = $this->_prefix;
|
||||
if ($prefix != '') {
|
||||
$prefix .= '/';
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($this->_listAllObjects($prefix) as $object) {
|
||||
$candidate = substr($object['Key'], strlen($prefix));
|
||||
if (strpos($candidate, '/') === false) {
|
||||
$pastes[] = $candidate;
|
||||
}
|
||||
}
|
||||
} catch (S3Exception $e) {
|
||||
// no objects in the bucket yet
|
||||
}
|
||||
return $pastes;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
@ -66,6 +65,6 @@ class Filter
|
|||
$size = $size / 1024;
|
||||
++$i;
|
||||
}
|
||||
return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . I18n::_($iec[$i]);
|
||||
return number_format($size, $i ? 2 : 0, '.', ' ') . ' ' . I18n::_($iec[$i]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
|
80
lib/I18n.php
80
lib/I18n.php
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,11 +7,13 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
||||
use AppendIterator;
|
||||
use GlobIterator;
|
||||
|
||||
/**
|
||||
* I18n
|
||||
*
|
||||
|
@ -78,13 +80,13 @@ class I18n
|
|||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $messageId
|
||||
* @param string|array $messageId
|
||||
* @param mixed $args one or multiple parameters injected into placeholders
|
||||
* @return string
|
||||
*/
|
||||
public static function _($messageId)
|
||||
public static function _($messageId, ...$args)
|
||||
{
|
||||
return forward_static_call_array('self::translate', func_get_args());
|
||||
return forward_static_call_array('PrivateBin\I18n::translate', func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,16 +94,16 @@ class I18n
|
|||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $messageId
|
||||
* @param string|array $messageId
|
||||
* @param mixed $args one or multiple parameters injected into placeholders
|
||||
* @return string
|
||||
*/
|
||||
public static function translate($messageId)
|
||||
public static function translate($messageId, ...$args)
|
||||
{
|
||||
if (empty($messageId)) {
|
||||
return $messageId;
|
||||
}
|
||||
if (count(self::$_translations) === 0) {
|
||||
if (empty(self::$_translations)) {
|
||||
self::loadTranslations();
|
||||
}
|
||||
$messages = $messageId;
|
||||
|
@ -111,7 +113,7 @@ class I18n
|
|||
if (!array_key_exists($messageId, self::$_translations)) {
|
||||
self::$_translations[$messageId] = $messages;
|
||||
}
|
||||
$args = func_get_args();
|
||||
array_unshift($args, $messageId);
|
||||
if (is_array(self::$_translations[$messageId])) {
|
||||
$number = (int) $args[1];
|
||||
$key = self::_getPluralForm($number);
|
||||
|
@ -127,11 +129,9 @@ class I18n
|
|||
}
|
||||
// encode any non-integer arguments and the message ID, if it doesn't contain a link
|
||||
$argsCount = count($args);
|
||||
if ($argsCount > 1) {
|
||||
for ($i = 0; $i < $argsCount; ++$i) {
|
||||
if (($i > 0 && !is_int($args[$i])) || strpos($args[0], '<a') === false) {
|
||||
$args[$i] = self::encode($args[$i]);
|
||||
}
|
||||
for ($i = 0; $i < $argsCount; ++$i) {
|
||||
if ($i > 0 ? !is_int($args[$i]) : strpos($args[0], '<a') === false) {
|
||||
$args[$i] = self::encode($args[$i]);
|
||||
}
|
||||
}
|
||||
return call_user_func_array('sprintf', $args);
|
||||
|
@ -193,10 +193,14 @@ class I18n
|
|||
public static function getAvailableLanguages()
|
||||
{
|
||||
if (count(self::$_availableLanguages) == 0) {
|
||||
$i18n = dir(self::_getPath());
|
||||
while (false !== ($file = $i18n->read())) {
|
||||
if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1) {
|
||||
self::$_availableLanguages[] = $match[1];
|
||||
self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
|
||||
$languageIterator = new AppendIterator();
|
||||
$languageIterator->append(new GlobIterator(self::_getPath('??.json')));
|
||||
$languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
|
||||
foreach ($languageIterator as $file) {
|
||||
$language = $file->getBasename('.json');
|
||||
if ($language != 'en') {
|
||||
self::$_availableLanguages[] = $language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +276,18 @@ class I18n
|
|||
return array_intersect_key(self::$_languageLabels, array_flip($languages));
|
||||
}
|
||||
|
||||
/**
|
||||
* determines if the current language is written right-to-left (RTL)
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @return bool
|
||||
*/
|
||||
public static function isRtl()
|
||||
{
|
||||
return in_array(self::$_language, array('ar', 'he'));
|
||||
}
|
||||
|
||||
/**
|
||||
* set the default language
|
||||
*
|
||||
|
@ -296,16 +312,16 @@ class I18n
|
|||
*/
|
||||
protected static function _getPath($file = '')
|
||||
{
|
||||
if (strlen(self::$_path) == 0) {
|
||||
if (empty(self::$_path)) {
|
||||
self::$_path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
|
||||
}
|
||||
return self::$_path . (strlen($file) ? DIRECTORY_SEPARATOR . $file : '');
|
||||
return self::$_path . (empty($file) ? '' : DIRECTORY_SEPARATOR . $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* determines the plural form to use based on current language and given number
|
||||
*
|
||||
* From: https://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
|
||||
* From: https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
|
||||
*
|
||||
* @access protected
|
||||
* @static
|
||||
|
@ -315,28 +331,38 @@ class I18n
|
|||
protected static function _getPluralForm($n)
|
||||
{
|
||||
switch (self::$_language) {
|
||||
case 'ar':
|
||||
return $n === 0 ? 0 : ($n === 1 ? 1 : ($n === 2 ? 2 : ($n % 100 >= 3 && $n % 100 <= 10 ? 3 : ($n % 100 >= 11 ? 4 : 5))));
|
||||
case 'cs':
|
||||
return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
|
||||
case 'sk':
|
||||
return $n === 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
|
||||
case 'co':
|
||||
case 'fr':
|
||||
case 'oc':
|
||||
case 'tr':
|
||||
case 'zh':
|
||||
return $n > 1 ? 1 : 0;
|
||||
case 'he':
|
||||
return $n === 1 ? 0 : ($n === 2 ? 1 : (($n < 0 || $n > 10) && ($n % 10 === 0) ? 2 : 3));
|
||||
case 'id':
|
||||
case 'ja':
|
||||
case 'jbo':
|
||||
case 'th':
|
||||
return 0;
|
||||
case 'lt':
|
||||
return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
|
||||
case 'pl':
|
||||
return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
|
||||
return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
|
||||
case 'ro':
|
||||
return $n === 1 ? 0 : (($n === 0 || ($n % 100 > 0 && $n % 100 < 20)) ? 1 : 2);
|
||||
case 'ru':
|
||||
case 'uk':
|
||||
return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
|
||||
return $n % 10 === 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
|
||||
case 'sl':
|
||||
return $n % 100 == 1 ? 1 : ($n % 100 == 2 ? 2 : ($n % 100 == 3 || $n % 100 == 4 ? 3 : 0));
|
||||
// bg, ca, de, en, es, et, hu, it, nl, no, pt
|
||||
return $n % 100 === 1 ? 1 : ($n % 100 === 2 ? 2 : ($n % 100 === 3 || $n % 100 === 4 ? 3 : 0));
|
||||
default:
|
||||
return $n != 1 ? 1 : 0;
|
||||
// bg, ca, de, el, en, es, et, fi, hu, it, nl, no, pt
|
||||
return $n !== 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
@ -81,10 +80,8 @@ class Model
|
|||
public function getStore()
|
||||
{
|
||||
if ($this->_store === null) {
|
||||
$this->_store = forward_static_call(
|
||||
'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model') . '::getInstance',
|
||||
$this->_conf->getSection('model_options')
|
||||
);
|
||||
$class = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
|
||||
$this->_store = new $class($this->_conf->getSection('model_options'));
|
||||
}
|
||||
return $this->_store;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Model;
|
||||
|
@ -108,7 +107,7 @@ abstract class AbstractModel
|
|||
$this->_data = $data;
|
||||
|
||||
// calculate a 64 bit checksum to avoid collisions
|
||||
$this->setId(hash(version_compare(PHP_VERSION, '5.6', '<') ? 'fnv164' : 'fnv1a64', $data['ct']));
|
||||
$this->setId(hash('fnv1a64', $data['ct']));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,13 +7,13 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Model;
|
||||
|
||||
use Exception;
|
||||
use Identicon\Identicon;
|
||||
use Jdenticon\Identicon as Jdenticon;
|
||||
use PrivateBin\Persistence\TrafficLimiter;
|
||||
use PrivateBin\Vizhash16x16;
|
||||
|
||||
|
@ -167,6 +167,16 @@ class Comment extends AbstractModel
|
|||
if ($icon == 'identicon') {
|
||||
$identicon = new Identicon();
|
||||
$pngdata = $identicon->getImageDataUri($hmac, 16);
|
||||
} elseif ($icon == 'jdenticon') {
|
||||
$jdenticon = new Jdenticon(array(
|
||||
'hash' => $hmac,
|
||||
'size' => 16,
|
||||
'style' => array(
|
||||
'backgroundColor' => '#fff0', // fully transparent, for dark mode
|
||||
'padding' => 0,
|
||||
),
|
||||
));
|
||||
$pngdata = $jdenticon->getImageDataUri('png');
|
||||
} elseif ($icon == 'vizhash') {
|
||||
$vh = new Vizhash16x16();
|
||||
$pngdata = 'data:image/png;base64,' . base64_encode(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Model;
|
||||
|
@ -37,7 +36,7 @@ class Paste extends AbstractModel
|
|||
throw new Exception(Controller::GENERIC_ERROR, 64);
|
||||
}
|
||||
|
||||
// check if paste has expired and delete it if neccessary.
|
||||
// check if paste has expired and delete it if necessary.
|
||||
if (array_key_exists('expire_date', $data['meta'])) {
|
||||
if ($data['meta']['expire_date'] < time()) {
|
||||
$this->delete();
|
||||
|
@ -47,6 +46,11 @@ class Paste extends AbstractModel
|
|||
$data['meta']['time_to_live'] = $data['meta']['expire_date'] - time();
|
||||
unset($data['meta']['expire_date']);
|
||||
}
|
||||
foreach (array('created', 'postdate') as $key) {
|
||||
if (array_key_exists($key, $data['meta'])) {
|
||||
unset($data['meta'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// check if non-expired burn after reading paste needs to be deleted
|
||||
if (
|
||||
|
@ -92,8 +96,7 @@ class Paste extends AbstractModel
|
|||
throw new Exception('You are unlucky. Try again.', 75);
|
||||
}
|
||||
|
||||
$this->_data['meta']['created'] = time();
|
||||
$this->_data['meta']['salt'] = ServerSalt::generate();
|
||||
$this->_data['meta']['salt'] = ServerSalt::generate();
|
||||
|
||||
// store paste
|
||||
if (
|
||||
|
@ -159,7 +162,17 @@ class Paste extends AbstractModel
|
|||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->_store->readComments($this->getId());
|
||||
if ($this->_conf->getKey('discussiondatedisplay')) {
|
||||
return $this->_store->readComments($this->getId());
|
||||
}
|
||||
return array_map(function ($comment) {
|
||||
foreach (array('created', 'postdate') as $key) {
|
||||
if (array_key_exists($key, $comment['meta'])) {
|
||||
unset($comment['meta'][$key]);
|
||||
}
|
||||
}
|
||||
return $comment;
|
||||
}, $this->_store->readComments($this->getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Persistence;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Persistence;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Persistence;
|
||||
|
@ -56,7 +55,7 @@ class ServerSalt extends AbstractPersistence
|
|||
*/
|
||||
public static function get()
|
||||
{
|
||||
if (strlen(self::$_salt)) {
|
||||
if (!empty(self::$_salt)) {
|
||||
return self::$_salt;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PrivateBin
|
||||
|
@ -8,13 +8,15 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin\Persistence;
|
||||
|
||||
use Exception;
|
||||
use IPLib\Factory;
|
||||
use IPLib\ParseStringFlag;
|
||||
use PrivateBin\Configuration;
|
||||
use PrivateBin\I18n;
|
||||
|
||||
/**
|
||||
* TrafficLimiter
|
||||
|
@ -24,22 +26,22 @@ use PrivateBin\Configuration;
|
|||
class TrafficLimiter extends AbstractPersistence
|
||||
{
|
||||
/**
|
||||
* time limit in seconds, defaults to 10s
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var int
|
||||
*/
|
||||
private static $_limit = 10;
|
||||
|
||||
/**
|
||||
* listed ips are exempted from limits, defaults to null
|
||||
* listed IPs are the only ones allowed to create, defaults to null
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string|null
|
||||
*/
|
||||
private static $_exemptedIp = null;
|
||||
private static $_creators = null;
|
||||
|
||||
/**
|
||||
* listed IPs are exempted from limits, defaults to null
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
* @var string|null
|
||||
*/
|
||||
private static $_exempted = null;
|
||||
|
||||
/**
|
||||
* key to fetch IP address
|
||||
|
@ -51,28 +53,13 @@ class TrafficLimiter extends AbstractPersistence
|
|||
private static $_ipKey = 'REMOTE_ADDR';
|
||||
|
||||
/**
|
||||
* set the time limit in seconds
|
||||
* time limit in seconds, defaults to 10s
|
||||
*
|
||||
* @access public
|
||||
* @access private
|
||||
* @static
|
||||
* @param int $limit
|
||||
* @var int
|
||||
*/
|
||||
public static function setLimit($limit)
|
||||
{
|
||||
self::$_limit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* set a list of ip(ranges) as string
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $exemptedIps
|
||||
*/
|
||||
public static function setExemptedIp($exemptedIp)
|
||||
{
|
||||
self::$_exemptedIp = $exemptedIp;
|
||||
}
|
||||
private static $_limit = 10;
|
||||
|
||||
/**
|
||||
* set configuration options of the traffic limiter
|
||||
|
@ -83,10 +70,11 @@ class TrafficLimiter extends AbstractPersistence
|
|||
*/
|
||||
public static function setConfiguration(Configuration $conf)
|
||||
{
|
||||
self::setCreators($conf->getKey('creators', 'traffic'));
|
||||
self::setExempted($conf->getKey('exempted', 'traffic'));
|
||||
self::setLimit($conf->getKey('limit', 'traffic'));
|
||||
self::setExemptedIp($conf->getKey('exemptedIp', 'traffic'));
|
||||
|
||||
if (($option = $conf->getKey('header', 'traffic')) !== null) {
|
||||
if (($option = $conf->getKey('header', 'traffic')) !== '') {
|
||||
$httpHeader = 'HTTP_' . $option;
|
||||
if (array_key_exists($httpHeader, $_SERVER) && !empty($_SERVER[$httpHeader])) {
|
||||
self::$_ipKey = $httpHeader;
|
||||
|
@ -94,6 +82,42 @@ class TrafficLimiter extends AbstractPersistence
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set a list of creator IP(-ranges) as string
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $creators
|
||||
*/
|
||||
public static function setCreators($creators)
|
||||
{
|
||||
self::$_creators = $creators;
|
||||
}
|
||||
|
||||
/**
|
||||
* set a list of exempted IP(-ranges) as string
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param string $exempted
|
||||
*/
|
||||
public static function setExempted($exempted)
|
||||
{
|
||||
self::$_exempted = $exempted;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the time limit in seconds
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @param int $limit
|
||||
*/
|
||||
public static function setLimit($limit)
|
||||
{
|
||||
self::$_limit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* get a HMAC of the current visitors IP address
|
||||
*
|
||||
|
@ -108,7 +132,7 @@ class TrafficLimiter extends AbstractPersistence
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate $_ipKey against configured ipranges. If matched we will ignore the ip
|
||||
* validate $_ipKey against configured ipranges. If matched we will ignore the ip
|
||||
*
|
||||
* @access private
|
||||
* @static
|
||||
|
@ -120,8 +144,11 @@ class TrafficLimiter extends AbstractPersistence
|
|||
if (is_string($ipRange)) {
|
||||
$ipRange = trim($ipRange);
|
||||
}
|
||||
$address = Factory::addressFromString($_SERVER[self::$_ipKey]);
|
||||
$range = Factory::rangeFromString($ipRange);
|
||||
$address = Factory::parseAddressString($_SERVER[self::$_ipKey]);
|
||||
$range = Factory::parseRangeString(
|
||||
$ipRange,
|
||||
ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
|
||||
);
|
||||
|
||||
// address could not be parsed, we might not be in IP space and try a string comparison instead
|
||||
if (is_null($address)) {
|
||||
|
@ -136,24 +163,35 @@ class TrafficLimiter extends AbstractPersistence
|
|||
}
|
||||
|
||||
/**
|
||||
* traffic limiter
|
||||
*
|
||||
* Make sure the IP address makes at most 1 request every 10 seconds.
|
||||
* make sure the IP address is allowed to perfom a request
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
* @return true
|
||||
*/
|
||||
public static function canPass()
|
||||
{
|
||||
// if creators are defined, the traffic limiter will only allow creation
|
||||
// for these, with no limits, and skip any other rules
|
||||
if (!empty(self::$_creators)) {
|
||||
$creatorIps = explode(',', self::$_creators);
|
||||
foreach ($creatorIps as $ipRange) {
|
||||
if (self::matchIp($ipRange) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
throw new Exception(I18n::_('Your IP is not authorized to create pastes.'));
|
||||
}
|
||||
|
||||
// disable limits if set to less then 1
|
||||
if (self::$_limit < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if $_ipKey is exempted from ratelimiting
|
||||
if (!is_null(self::$_exemptedIp)) {
|
||||
$exIp_array = explode(',', self::$_exemptedIp);
|
||||
// check if $_ipKey is exempted from ratelimiting
|
||||
if (!empty(self::$_exempted)) {
|
||||
$exIp_array = explode(',', self::$_exempted);
|
||||
foreach ($exIp_array as $ipRange) {
|
||||
if (self::matchIp($ipRange) === true) {
|
||||
return true;
|
||||
|
@ -161,7 +199,7 @@ class TrafficLimiter extends AbstractPersistence
|
|||
}
|
||||
}
|
||||
|
||||
// this hash is used as an array key, hence a shorter algo is used
|
||||
// used as array key, which are limited in length, hence using algo with shorter range
|
||||
$hash = self::getHash('sha256');
|
||||
$now = time();
|
||||
$tl = (int) self::$_store->getValue('traffic_limiter', $hash);
|
||||
|
@ -175,6 +213,12 @@ class TrafficLimiter extends AbstractPersistence
|
|||
if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) {
|
||||
error_log('failed to store the traffic limiter, it probably contains outdated information');
|
||||
}
|
||||
return $result;
|
||||
if ($result) {
|
||||
return true;
|
||||
}
|
||||
throw new Exception(I18n::_(
|
||||
'Please wait %d seconds between each post.',
|
||||
self::$_limit
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -7,11 +7,12 @@
|
|||
* @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.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Request
|
||||
*
|
||||
|
@ -80,13 +81,11 @@ class Request
|
|||
*/
|
||||
private function getPasteId()
|
||||
{
|
||||
// RegEx to check for valid paste ID (16 base64 chars)
|
||||
$pasteIdRegEx = '/^[a-f0-9]{16}$/';
|
||||
|
||||
foreach ($_GET as $key => $value) {
|
||||
// only return if value is empty and key matches RegEx
|
||||
if (($value === '') and preg_match($pasteIdRegEx, $key, $match)) {
|
||||
return $match[0];
|
||||
// only return if value is empty and key is 16 hex chars
|
||||
$key = (string) $key;
|
||||
if (($value === '') && strlen($key) === 16 && ctype_xdigit($key)) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,16 +109,27 @@ class Request
|
|||
case 'POST':
|
||||
// it might be a creation or a deletion, the latter is detected below
|
||||
$this->_operation = 'create';
|
||||
$this->_params = Json::decode(
|
||||
file_get_contents(self::$_inputStream)
|
||||
);
|
||||
try {
|
||||
$this->_params = Json::decode(
|
||||
file_get_contents(self::$_inputStream)
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
// ignore error, $this->_params will remain empty
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$this->_params = $_GET;
|
||||
$this->_params = filter_var_array($_GET, array(
|
||||
'deletetoken' => FILTER_SANITIZE_SPECIAL_CHARS,
|
||||
'jsonld' => FILTER_SANITIZE_SPECIAL_CHARS,
|
||||
'link' => FILTER_SANITIZE_URL,
|
||||
'pasteid' => FILTER_SANITIZE_SPECIAL_CHARS,
|
||||
'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS,
|
||||
), false);
|
||||
}
|
||||
if (
|
||||
!array_key_exists('pasteid', $this->_params) &&
|
||||
!array_key_exists('jsonld', $this->_params) &&
|
||||
!array_key_exists('link', $this->_params) &&
|
||||
array_key_exists('QUERY_STRING', $_SERVER) &&
|
||||
!empty($_SERVER['QUERY_STRING'])
|
||||
) {
|
||||
|
@ -135,6 +145,10 @@ class Request
|
|||
}
|
||||
} elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) {
|
||||
$this->_operation = 'jsonld';
|
||||
} elseif (array_key_exists('link', $this->_params) && !empty($this->_params['link'])) {
|
||||
if (strpos($this->getRequestUri(), '/shortenviayourls') !== false || array_key_exists('shortenviayourls', $this->_params)) {
|
||||
$this->_operation = 'yourlsproxy';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,9 +212,8 @@ class Request
|
|||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return array_key_exists('HTTP_HOST', $_SERVER) ?
|
||||
htmlspecialchars($_SERVER['HTTP_HOST']) :
|
||||
'localhost';
|
||||
$host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : '';
|
||||
return empty($host) ? 'localhost' : $host;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,10 +224,8 @@ class Request
|
|||
*/
|
||||
public function getRequestUri()
|
||||
{
|
||||
return array_key_exists('REQUEST_URI', $_SERVER) ?
|
||||
htmlspecialchars(
|
||||
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
|
||||
) : '/';
|
||||
$uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
|
||||
return empty($uri) ? '/' : $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -264,10 +275,9 @@ class Request
|
|||
}
|
||||
|
||||
// advanced case: media type negotiation
|
||||
$mediaTypes = array();
|
||||
if ($hasAcceptHeader) {
|
||||
$mediaTypeRanges = explode(',', trim($acceptHeader));
|
||||
foreach ($mediaTypeRanges as $mediaTypeRange) {
|
||||
$mediaTypes = array();
|
||||
foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) {
|
||||
if (preg_match(
|
||||
'#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#',
|
||||
trim($mediaTypeRange), $match
|
||||
|
@ -276,6 +286,9 @@ class Request
|
|||
$match[2] = '1.0';
|
||||
} else {
|
||||
$match[2] = (string) floatval($match[2]);
|
||||
if ($match[2] === '0.0') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!isset($mediaTypes[$match[2]])) {
|
||||
$mediaTypes[$match[2]] = array();
|
||||
|
@ -285,9 +298,6 @@ class Request
|
|||
}
|
||||
krsort($mediaTypes);
|
||||
foreach ($mediaTypes as $acceptedQuality => $acceptedValues) {
|
||||
if ($acceptedQuality === '0.0') {
|
||||
continue;
|
||||
}
|
||||
foreach ($acceptedValues as $acceptedValue) {
|
||||
if (
|
||||
strpos($acceptedValue, self::MIME_HTML) === 0 ||
|
||||
|
|
26
lib/View.php
26
lib/View.php
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* PrivateBin
|
||||
*
|
||||
|
@ -6,8 +6,7 @@
|
|||
*
|
||||
* @link https://github.com/PrivateBin/PrivateBin
|
||||
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
||||
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
* @version 1.3.5
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
@ -50,7 +49,7 @@ class View
|
|||
*/
|
||||
public function draw($template)
|
||||
{
|
||||
$file = substr($template, 0, 9) === 'bootstrap' ? 'bootstrap' : $template;
|
||||
$file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template;
|
||||
$path = PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php';
|
||||
if (!file_exists($path)) {
|
||||
throw new Exception('Template ' . $template . ' not found!', 80);
|
||||
|
@ -58,4 +57,23 @@ class View
|
|||
extract($this->_variables);
|
||||
include $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* echo script tag incl. SRI hash for given script file
|
||||
*
|
||||
* @access private
|
||||
* @param string $file
|
||||
* @param string $attributes additional attributes to add into the script tag
|
||||
*/
|
||||
private function _scriptTag($file, $attributes = '')
|
||||
{
|
||||
$sri = array_key_exists($file, $this->_variables['SRI']) ?
|
||||
' integrity="' . $this->_variables['SRI'][$file] . '"' : '';
|
||||
// if the file isn't versioned (ends in a digit), add our own version
|
||||
$cacheBuster = ctype_digit(substr($file, -4, 1)) ?
|
||||
'' : '?' . rawurlencode($this->_variables['VERSION']);
|
||||
echo '<script ', $attributes,
|
||||
' type="text/javascript" data-cfasync="false" src="', $file,
|
||||
$cacheBuster, '"', $sri, ' crossorigin="anonymous"></script>', PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* VizHash_GD
|
||||
*
|
||||
* Visual Hash implementation in php4+GD,
|
||||
* stripped down and modified version for PrivateBin
|
||||
* stripped down from version 0.0.5 beta, modified for PrivateBin
|
||||
*
|
||||
* @link http://sebsauvage.net/wiki/doku.php?id=php:vizhash_gd
|
||||
* @link https://sebsauvage.net/wiki/doku.php?id=php:vizhash_gd
|
||||
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
||||
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
||||
* @version 0.0.5 beta PrivateBin 1.3.5
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
@ -109,9 +108,9 @@ class Vizhash16x16
|
|||
for ($i = 0; $i < 7; ++$i) {
|
||||
$action = $this->getInt();
|
||||
$color = imagecolorallocate($image, $r, $g, $b);
|
||||
$r = $r0 = ($r0 + $this->getInt() / 25) % 256;
|
||||
$g = $g0 = ($g0 + $this->getInt() / 25) % 256;
|
||||
$b = $b0 = ($b0 + $this->getInt() / 25) % 256;
|
||||
$r = $r0 = (int) ($r0 + $this->getInt() / 25) % 256;
|
||||
$g = $g0 = (int) ($g0 + $this->getInt() / 25) % 256;
|
||||
$b = $b0 = (int) ($b0 + $this->getInt() / 25) % 256;
|
||||
$this->drawshape($image, $action, $color);
|
||||
}
|
||||
|
||||
|
@ -136,7 +135,7 @@ class Vizhash16x16
|
|||
{
|
||||
$v = $this->VALUES[$this->VALUES_INDEX];
|
||||
++$this->VALUES_INDEX;
|
||||
$this->VALUES_INDEX %= count($this->VALUES); // Warp around the array
|
||||
$this->VALUES_INDEX %= count($this->VALUES); // Wrap around the array
|
||||
return $v;
|
||||
}
|
||||
|
||||
|
@ -148,7 +147,7 @@ class Vizhash16x16
|
|||
*/
|
||||
private function getX()
|
||||
{
|
||||
return $this->width * $this->getInt() / 256;
|
||||
return (int) ($this->width * $this->getInt() / 256);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,14 +158,14 @@ class Vizhash16x16
|
|||
*/
|
||||
private function getY()
|
||||
{
|
||||
return $this->height * $this->getInt() / 256;
|
||||
return (int) ($this->height * $this->getInt() / 256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gradient function
|
||||
*
|
||||
* taken from:
|
||||
* http://www.supportduweb.com/scripts_tutoriaux-code-source-41-gd-faire-un-degrade-en-php-gd-fonction-degrade-imagerie.html
|
||||
* @link https://www.supportduweb.com/scripts_tutoriaux-code-source-41-gd-faire-un-degrade-en-php-gd-fonction-degrade-imagerie.html
|
||||
*
|
||||
* @access private
|
||||
* @param resource $img
|
||||
|
@ -185,14 +184,14 @@ class Vizhash16x16
|
|||
$sizeinv = imagesx($img);
|
||||
}
|
||||
$diffs = array(
|
||||
(($color2[0] - $color1[0]) / $size),
|
||||
(($color2[1] - $color1[1]) / $size),
|
||||
(($color2[2] - $color1[2]) / $size),
|
||||
($color2[0] - $color1[0]) / $size,
|
||||
($color2[1] - $color1[1]) / $size,
|
||||
($color2[2] - $color1[2]) / $size,
|
||||
);
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
$r = $color1[0] + ($diffs[0] * $i);
|
||||
$g = $color1[1] + ($diffs[1] * $i);
|
||||
$b = $color1[2] + ($diffs[2] * $i);
|
||||
$r = $color1[0] + ((int) $diffs[0] * $i);
|
||||
$g = $color1[1] + ((int) $diffs[1] * $i);
|
||||
$b = $color1[2] + ((int) $diffs[2] * $i);
|
||||
if ($direction == 'h') {
|
||||
imageline($img, $i, 0, $i, $sizeinv, imagecolorallocate($img, $r, $g, $b));
|
||||
} else {
|
||||
|
@ -222,11 +221,11 @@ class Vizhash16x16
|
|||
break;
|
||||
case 3:
|
||||
$points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY());
|
||||
imagefilledpolygon($image, $points, 4, $color);
|
||||
version_compare(PHP_VERSION, '8.1', '<') ? imagefilledpolygon($image, $points, 4, $color) : imagefilledpolygon($image, $points, $color);
|
||||
break;
|
||||
default:
|
||||
$start = $this->getInt() * 360 / 256;
|
||||
$end = $start + $this->getInt() * 180 / 256;
|
||||
$start = (int) ($this->getInt() * 360 / 256);
|
||||
$end = (int) ($start + $this->getInt() * 180 / 256);
|
||||
imagefilledarc($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $start, $end, $color, IMG_ARC_PIE);
|
||||
}
|
||||
}
|
||||
|
|
131
lib/YourlsProxy.php
Normal file
131
lib/YourlsProxy.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
namespace PrivateBin;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* YourlsProxy
|
||||
*
|
||||
* Forwards a URL for shortening to YOURLS (your own URL shortener) and stores
|
||||
* the result.
|
||||
*/
|
||||
class YourlsProxy
|
||||
{
|
||||
/**
|
||||
* error message
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $_error = '';
|
||||
|
||||
/**
|
||||
* shortened URL
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $_url = '';
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* initializes and runs PrivateBin
|
||||
*
|
||||
* @access public
|
||||
* @param string $link
|
||||
*/
|
||||
public function __construct(Configuration $conf, $link)
|
||||
{
|
||||
if (strpos($link, $conf->getKey('basepath') . '?') !== 0) {
|
||||
$this->_error = 'Trying to shorten a URL that isn\'t pointing at our instance.';
|
||||
return;
|
||||
}
|
||||
|
||||
$yourls_api_url = $conf->getKey('apiurl', 'yourls');
|
||||
if (empty($yourls_api_url)) {
|
||||
$this->_error = 'Error calling YOURLS. Probably a configuration issue, like wrong or missing "apiurl" or "signature".';
|
||||
return;
|
||||
}
|
||||
|
||||
$data = file_get_contents(
|
||||
$yourls_api_url, false, stream_context_create(
|
||||
array(
|
||||
'http' => array(
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
|
||||
'content' => http_build_query(
|
||||
array(
|
||||
'signature' => $conf->getKey('signature', 'yourls'),
|
||||
'format' => 'json',
|
||||
'action' => 'shorturl',
|
||||
'url' => $link,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
try {
|
||||
$data = Json::decode($data);
|
||||
} catch (Exception $e) {
|
||||
$this->_error = 'Error calling YOURLS. Probably a configuration issue, like wrong or missing "apiurl" or "signature".';
|
||||
error_log('Error calling YOURLS: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!is_null($data) &&
|
||||
array_key_exists('statusCode', $data) &&
|
||||
$data['statusCode'] == 200 &&
|
||||
array_key_exists('shorturl', $data)
|
||||
) {
|
||||
$this->_url = $data['shorturl'];
|
||||
} else {
|
||||
$this->_error = 'Error parsing YOURLS response.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (untranslated) error message
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
return $this->_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shortened URL
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any error has occurred
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function isError()
|
||||
{
|
||||
return !empty($this->_error);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue