PrivateBin/lib/Request.php
El RIDO 65a626f940 inputs sanitation & remove some obsolete version checks
using filter_vars instead of filter_input, because our unit tests depend on manipulating global arrays, which are not used by filter_input - we would have to mock the function in the unit testing, it therefore is cleaner to use the same code paths in testing as in production

some inputs in I18n and TrafficLimiter remain unfiltered, since we already validate them by other means (IP lib and/or preg_match)

our minimum PHP version is 7.3, so we can drop the two < 5.6 fallback checks
2024-03-23 11:27:25 +01:00

316 lines
9.0 KiB
PHP

<?php
/**
* PrivateBin
*
* a zero-knowledge paste bin
*
* @link https://github.com/PrivateBin/PrivateBin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 1.7.1
*/
namespace PrivateBin;
use Exception;
/**
* Request
*
* parses request parameters and provides helper functions for routing
*/
class Request
{
/**
* MIME type for JSON
*
* @const string
*/
const MIME_JSON = 'application/json';
/**
* MIME type for HTML
*
* @const string
*/
const MIME_HTML = 'text/html';
/**
* MIME type for XHTML
*
* @const string
*/
const MIME_XHTML = 'application/xhtml+xml';
/**
* Input stream to use for PUT parameter parsing
*
* @access private
* @var string
*/
private static $_inputStream = 'php://input';
/**
* Operation to perform
*
* @access private
* @var string
*/
private $_operation = 'view';
/**
* Request parameters
*
* @access private
* @var array
*/
private $_params = array();
/**
* If we are in a JSON API context
*
* @access private
* @var bool
*/
private $_isJsonApi = false;
/**
* Return the paste ID of the current paste.
*
* @access private
* @return string
*/
private function getPasteId()
{
foreach ($_GET as $key => $value) {
// only return if value is empty and key is 16 hex chars
if (($value === '') && strlen($key) === 16 && ctype_xdigit($key)) {
return $key;
}
}
return 'invalid id';
}
/**
* Constructor
*
* @access public
*/
public function __construct()
{
// decide if we are in JSON API or HTML context
$this->_isJsonApi = $this->_detectJsonRequest();
// parse parameters, depending on request type
switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET') {
case 'DELETE':
case 'PUT':
case 'POST':
// it might be a creation or a deletion, the latter is detected below
$this->_operation = 'create';
try {
$this->_params = Json::decode(
file_get_contents(self::$_inputStream)
);
} catch (Exception $e) {
// ignore error, $this->_params will remain empty
}
break;
default:
$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'])
) {
$this->_params['pasteid'] = $this->getPasteId();
}
// prepare operation, depending on current parameters
if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) {
$this->_operation = 'delete';
} elseif ($this->_operation != 'create') {
$this->_operation = 'read';
}
} 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';
}
}
}
/**
* Get current operation
*
* @access public
* @return string
*/
public function getOperation()
{
return $this->_operation;
}
/**
* Get data of paste or comment
*
* @access public
* @return array
*/
public function getData()
{
$data = array(
'adata' => $this->getParam('adata'),
);
$required_keys = array('v', 'ct');
$meta = $this->getParam('meta');
if (empty($meta)) {
$required_keys[] = 'pasteid';
$required_keys[] = 'parentid';
} else {
$data['meta'] = $meta;
}
foreach ($required_keys as $key) {
$data[$key] = $this->getParam($key, $key == 'v' ? 1 : '');
}
// forcing a cast to int or float
$data['v'] = $data['v'] + 0;
return $data;
}
/**
* Get a request parameter
*
* @access public
* @param string $param
* @param string $default
* @return string
*/
public function getParam($param, $default = '')
{
return array_key_exists($param, $this->_params) ?
$this->_params[$param] : $default;
}
/**
* Get host as requested by the client
*
* @access public
* @return string
*/
public function getHost()
{
$host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : '';
return empty($host) ? 'localhost' : $host;
}
/**
* Get request URI
*
* @access public
* @return string
*/
public function getRequestUri()
{
$uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
return empty($uri) ? '/' : $uri;
}
/**
* If we are in a JSON API context
*
* @access public
* @return bool
*/
public function isJsonApiCall()
{
return $this->_isJsonApi;
}
/**
* Override the default input stream source, used for unit testing
*
* @param string $input
*/
public static function setInputStream($input)
{
self::$_inputStream = $input;
}
/**
* Detect the clients supported media type and decide if its a JSON API call or not
*
* Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
*
* @access private
* @return bool
*/
private function _detectJsonRequest()
{
$hasAcceptHeader = array_key_exists('HTTP_ACCEPT', $_SERVER);
$acceptHeader = $hasAcceptHeader ? $_SERVER['HTTP_ACCEPT'] : '';
// simple cases
if (
(array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') ||
($hasAcceptHeader &&
strpos($acceptHeader, self::MIME_JSON) !== false &&
strpos($acceptHeader, self::MIME_HTML) === false &&
strpos($acceptHeader, self::MIME_XHTML) === false)
) {
return true;
}
// advanced case: media type negotiation
if ($hasAcceptHeader) {
$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
)) {
if (!isset($match[2])) {
$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();
}
$mediaTypes[$match[2]][] = strtolower($match[1]);
}
}
krsort($mediaTypes);
foreach ($mediaTypes as $acceptedQuality => $acceptedValues) {
foreach ($acceptedValues as $acceptedValue) {
if (
strpos($acceptedValue, self::MIME_HTML) === 0 ||
strpos($acceptedValue, self::MIME_XHTML) === 0
) {
return false;
} elseif (strpos($acceptedValue, self::MIME_JSON) === 0) {
return true;
}
}
}
}
return false;
}
}