Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use Xentral\Components\Http\Session\SessionHandler;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'Request' => 'onInitRequest',
|
||||
'Session' => 'onInitSession',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public static function onInitRequest()
|
||||
{
|
||||
return Request::createFromGlobals();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Session\Session
|
||||
*/
|
||||
public static function onInitSession()
|
||||
{
|
||||
return SessionHandler::createSession();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
use ArrayIterator;
|
||||
use Iterator;
|
||||
use Xentral\Components\Http\File\FileUpload;
|
||||
|
||||
class FilesCollection implements Iterator
|
||||
{
|
||||
/** @var ArrayIterator $iterator */
|
||||
protected $iterator;
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
*/
|
||||
public function __construct(array $files = [])
|
||||
{
|
||||
$files = $this->loadFilesArray($files);
|
||||
$this->iterator = new ArrayIterator($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all file uploads.
|
||||
*
|
||||
* @return FileUpload[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->iterator->getArrayCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a file upload entry with this name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return $this->iterator->offsetExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an file upload entry.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return FileUpload|mixed
|
||||
*/
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
return $this->has($name) ? $this->iterator->offsetGet($name) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all upload files are valid.
|
||||
*
|
||||
* @return bool true=all upload files are valid
|
||||
*/
|
||||
public function allValid()
|
||||
{
|
||||
foreach ($this->all() as $name => $upload) {
|
||||
if (!$upload->isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least one file upload has an error.
|
||||
*
|
||||
* @return bool true=there is at least one error
|
||||
*/
|
||||
public function hasErrors()
|
||||
{
|
||||
foreach ($this->all() as $name => $upload) {
|
||||
if ($upload->hasError()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @return mixed FileUpload Object, null on failure
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->iterator->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->iterator->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @return mixed on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->iterator->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @return boolean true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->iterator->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->iterator->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileEntries
|
||||
*
|
||||
* @return bool true=is alternate Array
|
||||
*/
|
||||
protected function isAlternateArray($fileEntries)
|
||||
{
|
||||
if (
|
||||
!isset($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
|
||||
|| is_array($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fileEntries
|
||||
*
|
||||
* @return FileUpload[] $files
|
||||
*/
|
||||
protected function loadFilesArray($fileEntries)
|
||||
{
|
||||
if (empty($fileEntries)) {
|
||||
return [];
|
||||
}
|
||||
if ($this->isAlternateArray($fileEntries)) {
|
||||
return $this->convertFilesArray($fileEntries);
|
||||
}
|
||||
|
||||
$files = [];
|
||||
foreach ($fileEntries as $name => $upload) {
|
||||
if (empty($upload['tmp_name'])) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($upload['tmp_name'])) {
|
||||
$files[$name] = FileUpload::fromFilesArray($upload);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds the array with FileUpload instances from alternative array format.
|
||||
*
|
||||
* tested up to 3rd level nesting
|
||||
*
|
||||
* @param array $files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertFilesArray(array $files)
|
||||
{
|
||||
$totalUploads = [];
|
||||
foreach ($files as $name => $file) {
|
||||
$keys = array_keys($file);
|
||||
if (count(array_intersect($keys, ['name', 'tmp_name'])) === 2) {
|
||||
$uploads = [];
|
||||
foreach ($file['tmp_name'] as $k => $v) {
|
||||
$upload = [
|
||||
'tmp_name' => array_key_exists('tmp_name', $file) ? $file['tmp_name'][$k] : null,
|
||||
'name' => array_key_exists('name', $file) ? $file['name'][$k] : null,
|
||||
'type' => array_key_exists('type', $file) ? $file['type'][$k] : null,
|
||||
'size' => array_key_exists('size', $file) ? $file['size'][$k] : null,
|
||||
'error' => array_key_exists('error', $file) ? $file['error'][$k] : null,
|
||||
];
|
||||
if (!empty($upload['tmp_name'])) {
|
||||
$uploads[$k] = FileUpload::fromFilesArray($upload);
|
||||
}
|
||||
}
|
||||
$totalUploads[$name] = $uploads;
|
||||
} else {
|
||||
if (!(isset($file['tmp_name']) && $file['tmp_name'] === '')) {
|
||||
$totalUploads[$name] = $this->convertFilesArray($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $totalUploads;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
class ParameterCollection extends ReadonlyParameterCollection
|
||||
{
|
||||
/**
|
||||
* Sets a parameter; Existing parameter value will be overwritten
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set($name, $value)
|
||||
{
|
||||
$this->params[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets multiple parameters; Existing parameters will be overwritten
|
||||
*
|
||||
* @param array $values
|
||||
*/
|
||||
public function add(array $values)
|
||||
{
|
||||
$this->params = array_merge($this->params, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a parameter
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
unset($this->params[$name]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
class ReadonlyParameterCollection
|
||||
{
|
||||
/** @var array $params */
|
||||
protected $params;
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->params = [];
|
||||
foreach ($params as $name => $value) {
|
||||
$this->params[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all parameters and values as associative array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if parameter is available
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return array_key_exists($name, $this->params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
return isset($this->params[$name]) ? $this->params[$name] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value casted to boolean
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $default
|
||||
*
|
||||
* @return bool Returns true for "1", "true", "on" and "yes"; otherwise false
|
||||
*/
|
||||
public function getBool($name, $default = false)
|
||||
{
|
||||
return filter_var($this->get($name, $default), FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value casted to integer
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $default
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInt($name, $default = 0)
|
||||
{
|
||||
return (int)$this->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with digits only (0-9)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDigits($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^0-9]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphabetic characters only (A-Z and a-z)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlpha($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphanumeric characters only (A-Z, a-z and 0-9)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlphaNum($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z0-9]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphanumeric characters and dashes only (A-Z, a-z, 0-9, Minus and Underscore)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlphaNumWithDashes($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z0-9_-]#', '', $this->get($name, $default));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
class ServerParameter extends ReadonlyParameterCollection
|
||||
{
|
||||
/**
|
||||
* Returns all http request headers
|
||||
*
|
||||
* Emulates php's getallheaders() function
|
||||
* getallheaders is not available in all environments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
$header = [];
|
||||
|
||||
if (isset($this->params['CONTENT_TYPE'])) {
|
||||
$header['Content-Type'] = $this->params['CONTENT_TYPE'];
|
||||
}
|
||||
|
||||
foreach ($this->params as $name => $value) {
|
||||
if (strpos($name, 'HTTP_') === 0) {
|
||||
$header[$this->transformHeaderName($name)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth-Header ist bereits gesetzt durch $_SERVER[HTTP_AUTHORIZATION]
|
||||
if (!empty($header['Authorization'])) {
|
||||
return $header;
|
||||
}
|
||||
|
||||
// Basic-Auth
|
||||
if (isset($this->params['PHP_AUTH_USER'])) {
|
||||
$authString = base64_encode($this->params['PHP_AUTH_USER'] . ':' . $this->params['PHP_AUTH_PW']);
|
||||
$header['Authorization'] = sprintf('Basic %s', $authString);
|
||||
}
|
||||
|
||||
// Digest-Auth
|
||||
if (isset($this->params['PHP_AUTH_DIGEST'])) {
|
||||
$header['Authorization'] = sprintf('Digest %s', $this->params['PHP_AUTH_DIGEST']);
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform header names
|
||||
*
|
||||
* Transforms php $_SERVER formattet header names to
|
||||
* Browser style formatted header names.
|
||||
*
|
||||
* @example Transforms "HTTP_USER_AGENT" to "User-Agent"
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function transformHeaderName($name)
|
||||
{
|
||||
$name = substr($name, 5); // HTTP-Prefix entfernen
|
||||
$name = (string)str_replace('_', ' ', $name);
|
||||
$name = strtolower($name);
|
||||
$name = ucwords($name);
|
||||
|
||||
return str_replace(' ', '-', $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Cookie;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
class Cookie
|
||||
{
|
||||
/** @var string SAMESITE_LAX */
|
||||
const SAMESITE_LAX = 'Lax';
|
||||
|
||||
/** @var string SAMESITE_STRICT */
|
||||
const SAMESITE_STRICT = 'Strict';
|
||||
|
||||
/** @var string SAMESITE_NONE */
|
||||
const SAMESITE_NONE = '';
|
||||
|
||||
/** @var string $name */
|
||||
private $name;
|
||||
|
||||
/** @var string $value */
|
||||
private $value;
|
||||
|
||||
/** @var DateTime $expire */
|
||||
private $expire;
|
||||
|
||||
/** @var string $path */
|
||||
private $path;
|
||||
|
||||
/** @var string $domain */
|
||||
private $domain;
|
||||
|
||||
/** @var bool $secure */
|
||||
private $secure;
|
||||
|
||||
/** @var bool $httpOnly */
|
||||
private $httpOnly;
|
||||
|
||||
/** @var string $sameSite */
|
||||
private $sameSite;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param int $timeToLive
|
||||
* @param string $path
|
||||
* @param string $domain
|
||||
* @param bool $secure
|
||||
* @param bool $httpOnly
|
||||
* @param string $sameSite
|
||||
*/
|
||||
public function __construct(
|
||||
$name,
|
||||
$value,
|
||||
$timeToLive = 0,
|
||||
$path = '/',
|
||||
$domain = '',
|
||||
$secure = true,
|
||||
$httpOnly = true,
|
||||
$sameSite = self::SAMESITE_STRICT
|
||||
) {
|
||||
if (!$this->isValidCookieName($name)) {
|
||||
throw new InvalidArgumentException('Invalid Cookie name.');
|
||||
}
|
||||
if (!$this->isValidCookieValue($value)) {
|
||||
throw new InvalidArgumentException('Invalid Cookie value.');
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->setTimeToLive($timeToLive);
|
||||
$this->setPath($path);
|
||||
$this->setDomain($domain);
|
||||
$this->secure = $secure;
|
||||
$this->httpOnly = $httpOnly;
|
||||
$this->setSameSite($sameSite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of cookie to be sent in Http response
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHttpHeader()
|
||||
{
|
||||
$header = sprintf('Set-Cookie: %s=%s', $this->name, $this->value);
|
||||
if ($this->expire !== null) {
|
||||
$header .= sprintf('; Expires=%s',
|
||||
gmdate(DateTimeInterface::RFC7231, $this->expire->getTimestamp()));
|
||||
}
|
||||
if ($this->path !== '') {
|
||||
$header .= sprintf('; Path=%s', $this->path);
|
||||
}
|
||||
if ($this->domain !== '') {
|
||||
$header .= sprintf('; Domain=%s', $this->domain);
|
||||
}
|
||||
if ($this->isSecure()) {
|
||||
$header .= '; Secure';
|
||||
}
|
||||
if ($this->isHttpOnly()) {
|
||||
$header .= '; HttpOnly';
|
||||
}
|
||||
if (in_array($this->sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT], true)) {
|
||||
$header .= sprintf('; SameSite=%s', $this->sameSite);
|
||||
}
|
||||
|
||||
return StringUtil::toAscii($header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cookie expiry time to current time
|
||||
*
|
||||
* The client will delete this cookie.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function expireNow()
|
||||
{
|
||||
$this->setTimeToLive(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTime
|
||||
*/
|
||||
public function getExpire()
|
||||
{
|
||||
return $this->expire;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets date and time of expiration of the cookie
|
||||
*
|
||||
* @param DateTimeInterface $expirationDate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setExpirationDate(DateTimeInterface $expirationDate)
|
||||
{
|
||||
try {
|
||||
$this->expire = new DateTime($expirationDate->format(DateTimeInterface::RFC7231));
|
||||
} catch (Exception $e) {
|
||||
$this->expire = null;
|
||||
throw new InvalidArgumentException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets date and time of expiration based on specific time to live
|
||||
*
|
||||
* @param int $timeToLive in seconds
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setTimeToLive($timeToLive)
|
||||
{
|
||||
if ($timeToLive === 0) {
|
||||
$this->expire = null;
|
||||
} else {
|
||||
try {
|
||||
$this->expire = new DateTime();
|
||||
$time = time() + $timeToLive;
|
||||
$this->expire->setTimestamp($time);
|
||||
} catch (Exception $e) {
|
||||
$this->expire = null;
|
||||
throw new InvalidArgumentException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
if ($path === '' || $this->isValidCookieValue($path)) {
|
||||
$this->path = $path;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid path value.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
if ($domain === '' || $this->isValidCookieValue($domain)) {
|
||||
$this->domain = $domain;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid domain value.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSecure()
|
||||
{
|
||||
return $this->secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Http secure flag
|
||||
*
|
||||
* @param bool $secure true=cookie will only be sent over secure connection
|
||||
*/
|
||||
public function setSecure($secure)
|
||||
{
|
||||
$this->secure = $secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isHttpOnly()
|
||||
{
|
||||
return $this->httpOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HttpOnly flag
|
||||
*
|
||||
* @param bool $httpOnly true=cookie will only be sent in http responses
|
||||
*/
|
||||
public function setHttpOnly($httpOnly)
|
||||
{
|
||||
$this->httpOnly = $httpOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSameSite()
|
||||
{
|
||||
return $this->sameSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sameSite token
|
||||
*
|
||||
*Values:
|
||||
* 'lax': cookie can be sent cross-site for top-level navigation and GET, HEAD, OPTIONS and TRACE requests
|
||||
* 'strict': cookie can never be sent cross-site
|
||||
* '': disable the sameSite token
|
||||
*
|
||||
* @param string $sameSite values: 'lax'|'scrict'|'none'
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setSameSite($sameSite)
|
||||
{
|
||||
if (!in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE], true)) {
|
||||
throw new InvalidArgumentException('Invalid "samesite" attribute.');
|
||||
}
|
||||
$this->sameSite = $sameSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toHttpHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isValidCookieName($name)
|
||||
{
|
||||
return (bool)preg_match('/^[a-zA-Z0-9\\\\!#$%&\'*+.\-^_`|~]+$/', $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isValidCookieValue($value)
|
||||
{
|
||||
return (bool)preg_match('/^"?[a-zA-Z0-9\\\\!#$%&\'()*+\-.\/:<=>?@\[\]^_`{|}~]+"?$/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Cookie;
|
||||
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
|
||||
class CookieCollection implements Iterator, Countable, ArrayAccess
|
||||
{
|
||||
/** @var Cookie[] $cookies */
|
||||
private $cookies;
|
||||
|
||||
/**
|
||||
* CookieCollection constructor.
|
||||
*
|
||||
* @param Cookie[] $cookies
|
||||
*/
|
||||
public function __construct($cookies = [])
|
||||
{
|
||||
$this->cookies = new ArrayIterator($cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns http headers to be used in response
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function toHttpHeaders()
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->cookies as $cookie) {
|
||||
$result[] = $cookie->toHttpHeader();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cookie
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->cookies->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->cookies->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->cookies->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->cookies->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->cookies->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return $this->cookies->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return array_key_exists($offset, $this->cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return Cookie
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->cookies[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param Cookie $value
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
$this->cookies[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
unset($this->cookies[$offset]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CsrfTokenException extends RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
class FileExistsException extends \RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
class FileNotFoundException extends \RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface HttpComponentExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class HttpException extends RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
/** @var int $statusCode */
|
||||
protected $statusCode = 500;
|
||||
|
||||
/** @var array $errors */
|
||||
protected $errors;
|
||||
|
||||
/**
|
||||
* @param int $statusCode
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param array $errors
|
||||
* @param Throwable|null $previous
|
||||
*/
|
||||
public function __construct(
|
||||
$statusCode = 500,
|
||||
$message = '',
|
||||
$code = 0,
|
||||
Throwable $previous = null,
|
||||
array $errors = []
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->statusCode = $statusCode;
|
||||
$this->errors = $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int HTTP-Statuscode
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasErrors()
|
||||
{
|
||||
return count($this->errors) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class HttpHeaderValueException extends RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
public function __construct($message = '', $code = 0, Throwable $previous = null, $headerValue = null)
|
||||
{
|
||||
$headerString = '';
|
||||
if ($headerValue === null) {
|
||||
$headerString = 'null';
|
||||
}
|
||||
if (is_array($headerValue)) {
|
||||
$headerString = sprintf('[%s]', implode(',', $headerValue));
|
||||
}
|
||||
$headerString = strval($headerString);
|
||||
$message = sprintf('%s value:"%s"', $message, $headerString);
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class MethodNotAllowedException extends HttpException
|
||||
{
|
||||
public function __construct(
|
||||
array $allowedMethods,
|
||||
$message = null,
|
||||
$code = 0,
|
||||
Throwable $previous = null
|
||||
) {
|
||||
if ($message === null) {
|
||||
$message = sprintf('Method is not allowed. Allowed: %s', implode(', ', $allowedMethods));
|
||||
}
|
||||
|
||||
parent::__construct(405, $message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class NoUploadErrorException extends LogicException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
class SessionException extends \RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Exception;
|
||||
|
||||
class SessionSegmentException extends \RuntimeException implements HttpComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\File;
|
||||
|
||||
use SplFileInfo;
|
||||
use Xentral\Components\Http\Exception\FileNotFoundException;
|
||||
|
||||
class FileInfo extends SplFileInfo
|
||||
{
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @param bool $checkExistence
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function __construct($filePath, $checkExistence = true)
|
||||
{
|
||||
if ($checkExistence === true && !is_file($filePath)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $filePath));
|
||||
}
|
||||
|
||||
parent::__construct($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of the file.
|
||||
*
|
||||
* @example CSV file -> 'text/plain'
|
||||
* @example PDF file -> 'application/pdf'
|
||||
*
|
||||
* @return string Mime type
|
||||
*/
|
||||
public function getMimeType()
|
||||
{
|
||||
$finfo = finfo_open(FILEINFO_MIME);
|
||||
$mimetype = finfo_file($finfo, $this->getRealPath());
|
||||
finfo_close($finfo);
|
||||
|
||||
if ($mimetype !== false) {
|
||||
$mimetype = preg_replace('/^(.+);.+$/', '\1', $mimetype);
|
||||
} else {
|
||||
$mimetype = 'application/octet-stream';
|
||||
}
|
||||
|
||||
return $mimetype;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\File;
|
||||
|
||||
use Xentral\Components\Http\Exception\FileExistsException;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Http\Exception\NoUploadErrorException;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
class FileUpload extends FileInfo
|
||||
{
|
||||
/** @var string $clientFileName Original file name on client side (without path) */
|
||||
protected $clientFileName;
|
||||
|
||||
/** @var string $clientMimeType Mime type on client side */
|
||||
protected $clientMimeType;
|
||||
|
||||
/** @var int|null $clientSize */
|
||||
protected $clientSize;
|
||||
|
||||
/** @var int $errorCode Error code */
|
||||
protected $errorCode;
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @param string $clientName
|
||||
* @param string|null $mimeType
|
||||
* @param int|null $fileSize
|
||||
* @param int|null $errorCode
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
$filePath,
|
||||
$clientName,
|
||||
$mimeType = null,
|
||||
$fileSize = null,
|
||||
$errorCode = null
|
||||
) {
|
||||
if (empty($filePath)) {
|
||||
throw new InvalidArgumentException('File upload information is invalid. File path is missing.');
|
||||
}
|
||||
if (empty($clientName)) {
|
||||
throw new InvalidArgumentException('File upload information is invalid. Original file name is missing.');
|
||||
}
|
||||
|
||||
parent::__construct((string)$filePath);
|
||||
|
||||
$this->clientFileName = (string)$clientName;
|
||||
$this->clientMimeType = $mimeType !== null ? $mimeType : 'application/octet-stream';
|
||||
$this->clientSize = $fileSize;
|
||||
$this->errorCode = $errorCode !== null ? (int)$errorCode : UPLOAD_ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $file
|
||||
*
|
||||
* @return FileUpload
|
||||
*/
|
||||
public static function fromFilesArray(array $file)
|
||||
{
|
||||
foreach (['tmp_name', 'name', 'type', 'size', 'error'] as $key) {
|
||||
if (!array_key_exists($key, $file)) {
|
||||
$file[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new self($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client file size may not be set. Use self::getSize()
|
||||
*
|
||||
* @return int|null File size
|
||||
*/
|
||||
public function getClientSize()
|
||||
{
|
||||
return $this->clientSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client mime type may not correct. Use self::getMimeType()
|
||||
*
|
||||
* @return string 'application/octet-stream' if not set
|
||||
*/
|
||||
public function getClientMimeType()
|
||||
{
|
||||
return $this->clientMimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an error other than 0 exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasError()
|
||||
{
|
||||
return $this->errorCode !== UPLOAD_ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error message.
|
||||
*
|
||||
* @throws NoUploadErrorException
|
||||
*
|
||||
* @return string error message
|
||||
*/
|
||||
public function getErrorMessage()
|
||||
{
|
||||
if (!$this->hasError()) {
|
||||
throw new NoUploadErrorException('There is no error message. Please call first hasError()');
|
||||
}
|
||||
|
||||
switch ($this->getErrorCode()) {
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
$message = sprintf(
|
||||
'Die Datei "%s" überschreitet die \'upload_max_filesize\' Einstellung (%s) der in php.ini.',
|
||||
$this->getClientFileName(),
|
||||
ini_get('upload_max_filesize')// @todo format using Stringutils
|
||||
);
|
||||
break;
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
$message = sprintf(
|
||||
'Der Datei "%s" überschreitet die MAX_FILE_SIZE Einstellung des HTML-Formulars.',
|
||||
$this->getClientFileName()
|
||||
);
|
||||
break;
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
$message = sprintf(
|
||||
'Die Datei "%s" wurde nicht vollständig übertragen.',
|
||||
$this->getClientFileName()
|
||||
);
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
$message = 'Es wurde keine Datei ausgewählt.';
|
||||
break;
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
$message = 'Temporärer Ordner fehlt.';
|
||||
break;
|
||||
case UPLOAD_ERR_CANT_WRITE:
|
||||
$message = sprintf(
|
||||
'Die Datei "%s" konnte nicht abgespeichert werden.',
|
||||
$this->getClientFileName()
|
||||
);
|
||||
break;
|
||||
case UPLOAD_ERR_EXTENSION:
|
||||
$message = 'Der Upload wurde durch eine PHP-Erweiterung gestoppt.';
|
||||
break;
|
||||
default:
|
||||
$message = 'Unbekannter Upload-Fehler.';
|
||||
break;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns file upload error code.
|
||||
*
|
||||
* @see http://php.net/manual/de/features.file-upload.errors.php
|
||||
*
|
||||
* @return int File upload error code
|
||||
*/
|
||||
public function getErrorCode()
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client's file name
|
||||
*
|
||||
* @return string file name on client side
|
||||
*/
|
||||
public function getClientFileName()
|
||||
{
|
||||
return $this->clientFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the file.
|
||||
*
|
||||
* @return string file contents
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return file_get_contents($this->getRealPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a readonly stream to the file.
|
||||
*
|
||||
* @return resource file contents as stream
|
||||
*/
|
||||
public function createContentStream()
|
||||
{
|
||||
return fopen($this->getRealPath(), 'rb');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the file is an image.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isImage()
|
||||
{
|
||||
return in_array(
|
||||
$this->getMimeType(),
|
||||
['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/tiff', 'image/tif'],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the file is a Pdf file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPdf()
|
||||
{
|
||||
return $this->getMimeType() === 'application/pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if file is valid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return is_uploaded_file($this->getRealPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves file to specific location.
|
||||
*
|
||||
* @param string $targetDir
|
||||
* @param string|null $targetName
|
||||
*
|
||||
* @return FileInfo file at new location
|
||||
*/
|
||||
public function move($targetDir, $targetName = null)
|
||||
{
|
||||
if (!is_dir($targetDir)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('The target directory "%s" does not exist or is no directory.', $targetDir)
|
||||
);
|
||||
}
|
||||
if ($targetName === '') {
|
||||
throw new InvalidArgumentException('The target file name can not be empty.');
|
||||
}
|
||||
if ($targetName === null) {
|
||||
$targetName = StringUtil::toFilename( $this->getClientFileName());
|
||||
}
|
||||
|
||||
$targetFilePath = $targetDir . '/' . $targetName;
|
||||
|
||||
if (file_exists($targetFilePath)) {
|
||||
throw new FileExistsException(
|
||||
sprintf('Cannot move file. Target file "%s" already exists', $targetFilePath)
|
||||
);
|
||||
}
|
||||
|
||||
move_uploaded_file($this->getRealPath(), $targetFilePath);
|
||||
|
||||
return new FileInfo($targetFilePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Xentral\Components\Http\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Http\File\FileInfo;
|
||||
|
||||
class FileResponse extends Response
|
||||
{
|
||||
/** @var FileInfo $file */
|
||||
protected $file;
|
||||
|
||||
/** @var bool $deleteFileAfterDownload */
|
||||
protected $deleteFileAfterDownload = false;
|
||||
|
||||
/**
|
||||
* Creates a Http response to send a file to the client.
|
||||
*
|
||||
* @param string $filePath server-file to send
|
||||
* @param string $clientFileName filename for the download dialog
|
||||
* @param string $contentType determined by mimetype of the file if not set
|
||||
* @param bool $deleteAfterDownload true=remove the file when response was sent
|
||||
*
|
||||
* @return FileResponse
|
||||
*/
|
||||
public static function createFromFile($filePath, $clientFileName, $contentType = null, $deleteAfterDownload = false)
|
||||
{
|
||||
$fileResponse = new self();
|
||||
$fileResponse->setContentFile($filePath, $deleteAfterDownload);
|
||||
if ($contentType === null) {
|
||||
$contentType = $fileResponse->file->getMimeType();
|
||||
}
|
||||
$fileResponse->setContentType($contentType);
|
||||
$fileResponse->setContentDisposition(self::DISPOSITION_ATTACHMENT, $clientFileName);
|
||||
|
||||
return $fileResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file response with forced download header
|
||||
*
|
||||
* Useful for images and pdf.
|
||||
* Avoids that browsers open pdfs in viewer but download them directly.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @param string $clientFileName
|
||||
* @param bool $deleteAfterDownload
|
||||
*
|
||||
* @return FileResponse
|
||||
* @internal Content-Description -> optionaler MIME header https://tools.ietf.org/html/rfc1521#section-1
|
||||
*
|
||||
*/
|
||||
public static function createForcedDownload($filePath, $clientFileName, $deleteAfterDownload = false)
|
||||
{
|
||||
$fileResponse = new self();
|
||||
$fileResponse->setContentFile($filePath, $deleteAfterDownload);
|
||||
$fileResponse->setContentDisposition(self::DISPOSITION_ATTACHMENT, $clientFileName);
|
||||
$fileResponse->setContentType('application/force-download');
|
||||
$fileResponse->addHeader('Content-Description', 'File Transfer');
|
||||
|
||||
return $fileResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file as response body.
|
||||
*
|
||||
* @param string $contentFile file to send
|
||||
* @param bool $deleteFile delete file after response was sent
|
||||
*/
|
||||
public function setContentFile($contentFile, $deleteFile = false)
|
||||
{
|
||||
$fileInfo = new FileInfo($contentFile, true);
|
||||
$this->file = $fileInfo;
|
||||
$this->deleteFileAfterDownload = $deleteFile;
|
||||
$this->setHeader('Content-Length', (string)filesize($fileInfo->getRealPath()));
|
||||
$this->setContentDisposition(self::DISPOSITION_ATTACHMENT, $fileInfo->getFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the FileResponse to the client.
|
||||
*
|
||||
* @param DateTimeInterface|null $sendTime leave empty
|
||||
* @param int $chunkSize output content will be chunked
|
||||
*/
|
||||
public function send(DateTimeInterface $sendTime = null, $chunkSize = 65536)
|
||||
{
|
||||
if ($this->file === null) {
|
||||
throw new FileNotFoundException('No content File Available');
|
||||
}
|
||||
if ($chunkSize < 1) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid chunk size %s', $chunkSize));
|
||||
}
|
||||
parent::send($sendTime);
|
||||
$this->sendStreamedContent($chunkSize);
|
||||
|
||||
if ($this->deleteFileAfterDownload) {
|
||||
unlink($this->file->getRealPath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content File
|
||||
*
|
||||
* @return FileInfo|null
|
||||
*/
|
||||
public function getContentFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not available in FileResponse. Use setContentFile instead.
|
||||
*
|
||||
* @param string|null $content
|
||||
*/
|
||||
public function setContent($content)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* obsolete in FileResponse. Use getContentFile instead.
|
||||
*
|
||||
* @return FileInfo|null
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->getContentFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file content in portions to safe RAM.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
*/
|
||||
protected function sendStreamedContent($chunkSize = 65536)
|
||||
{
|
||||
$inStream = @fopen($this->file->getRealPath(), 'rb');
|
||||
if ($inStream === false) {
|
||||
throw new FileNotFoundException('Error reading file.');
|
||||
}
|
||||
|
||||
while (!feof($inStream)) {
|
||||
$dataChunk = @fread($inStream, $chunkSize);
|
||||
if ($dataChunk === false) {
|
||||
throw new FileNotFoundException('Error reading file.');
|
||||
}
|
||||
echo $dataChunk;
|
||||
}
|
||||
fclose($inStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
|
||||
class JsonResponse extends Response
|
||||
{
|
||||
/**
|
||||
* @param array|JsonSerializable $data
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
*/
|
||||
public function __construct($data = [], $statusCode = self::HTTP_OK, array $headers = [])
|
||||
{
|
||||
if (is_object($data) && !$data instanceof JsonSerializable) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Class "%s" can not be serialized. It does not implement JsonSerializable', get_class($data)
|
||||
));
|
||||
}
|
||||
|
||||
if (!is_object($data) && !is_array($data)) {
|
||||
throw new InvalidArgumentException('Parameter $data has to be an array or JsonSerializable.');
|
||||
}
|
||||
|
||||
$content = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
|
||||
$headers['Content-Type'] = 'application/json; charset=utf8';
|
||||
|
||||
parent::__construct($content, $statusCode, $headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
class RedirectResponse extends Response
|
||||
{
|
||||
/**
|
||||
* @param string $url Absolute or relative url
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromUrl($url)
|
||||
{
|
||||
$content = self::getRedirectTemplate($url);
|
||||
|
||||
return new self(
|
||||
$content,
|
||||
Response::HTTP_MOVED_TEMPORARILY,
|
||||
[
|
||||
'Length' => (string)strlen($content),
|
||||
'Location' => $url,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function getRedirectTemplate($url)
|
||||
{
|
||||
$template = <<<'HTML'
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="refresh" content="0;URL='%1$s'" />
|
||||
<title>Redirecting</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You are being redirected. If nothing happens, please <a href="%1$s">follow this link</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
return sprintf($template, $url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use Xentral\Components\Http\Collection\FilesCollection;
|
||||
use Xentral\Components\Http\Collection\ParameterCollection;
|
||||
use Xentral\Components\Http\Collection\ReadonlyParameterCollection;
|
||||
use Xentral\Components\Http\Collection\ServerParameter;
|
||||
use Xentral\Components\Http\Exception\HttpException;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Http\Exception\MethodNotAllowedException;
|
||||
use Xentral\Components\Http\File\FileUpload;
|
||||
|
||||
class Request
|
||||
{
|
||||
/** @var array $supportedMethods */
|
||||
protected static $supportedMethods = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
];
|
||||
|
||||
/** @var ReadonlyParameterCollection $get $_GET-Parameter */
|
||||
public $get;
|
||||
|
||||
/** @var ReadonlyParameterCollection $post $_POST-Parameter */
|
||||
public $post;
|
||||
|
||||
/** @var ReadonlyParameterCollection $cookie $_COOKIE-Parameter */
|
||||
public $cookie;
|
||||
|
||||
/** @var FilesCollection $files $_FILES-Parameter */
|
||||
public $files;
|
||||
|
||||
/** @var ServerParameter $server $_SERVER-Parameter */
|
||||
public $server;
|
||||
|
||||
/** @var ReadonlyParameterCollection $header */
|
||||
public $header;
|
||||
|
||||
/** @var ParameterCollection $attributes Custom request attributes (e.g. Router arguments) */
|
||||
public $attributes;
|
||||
|
||||
/** @var string $method */
|
||||
protected $method;
|
||||
|
||||
/** @var string $pathInfo */
|
||||
protected $pathInfo;
|
||||
|
||||
/** @var string $requestUri */
|
||||
protected $requestUri;
|
||||
|
||||
/** @var string|resource|null $content */
|
||||
protected $content;
|
||||
|
||||
/** @var array $acceptableContentTypes */
|
||||
protected $acceptableContentTypes;
|
||||
|
||||
/**
|
||||
* @param array $get
|
||||
* @param array $post
|
||||
* @param array $files
|
||||
* @param array $server
|
||||
* @param array $cookie
|
||||
* @param string|resource|null $content
|
||||
*/
|
||||
public function __construct(
|
||||
array $get = [],
|
||||
array $post = [],
|
||||
array $files = [],
|
||||
array $server = [],
|
||||
array $cookie = [],
|
||||
$content = null
|
||||
) {
|
||||
$this->get = new ReadonlyParameterCollection(!empty($get) ? $get : (array)$_GET);
|
||||
$this->post = new ReadonlyParameterCollection(!empty($post) ? $post : (array)$_POST);
|
||||
$this->files = new FilesCollection(!empty($files) ? $files : (array)$_FILES);
|
||||
$this->server = new ServerParameter(!empty($server) ? $server : (array)$_SERVER);
|
||||
$this->cookie = new ReadonlyParameterCollection(!empty($cookie) ? $cookie : (array)$_COOKIE);
|
||||
$this->header = new ReadonlyParameterCollection($this->server->getHeaders());
|
||||
$this->attributes = new ParameterCollection([]);
|
||||
$this->method = $this->getMethod();
|
||||
$this->requestUri = $this->getRequestUri();
|
||||
$this->pathInfo = $this->getPathInfo();
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of Request created with php's superglobals.
|
||||
*
|
||||
* @param string|null $content
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public static function createFromGlobals($content = null)
|
||||
{
|
||||
$request = new static((array)$_GET, (array)$_POST, (array)$_FILES, (array)$_SERVER, (array)$_COOKIE, $content);
|
||||
|
||||
if (
|
||||
$request->server->get('CONTENT_TYPE') === 'application/x-www-form-urlencoded' &&
|
||||
strtoupper($request->server->get('REQUEST_METHOD')) === 'PUT'
|
||||
) {
|
||||
parse_str($request->getContent(), $postParams);
|
||||
$request->post = new ReadonlyParameterCollection($postParams);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_GET parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGet($name, $default = null)
|
||||
{
|
||||
return $this->get->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_POST parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPost($name, $default = null)
|
||||
{
|
||||
return $this->post->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_GET or $_POST parameter value
|
||||
*
|
||||
* Looks at $_GET parameters first.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParam($name, $default = null)
|
||||
{
|
||||
if ($this->get->has($name)) {
|
||||
return $this->get->get($name);
|
||||
}
|
||||
|
||||
if ($this->post->has($name)) {
|
||||
return $this->post->get($name);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_FILES parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return FileUpload|mixed
|
||||
*/
|
||||
public function getFile($name, $default = null)
|
||||
{
|
||||
return $this->files->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_SERVER parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServer($name, $default = null)
|
||||
{
|
||||
return $this->server->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a HTTP header value
|
||||
*
|
||||
* Use the HTTP header name as used in the Browser.
|
||||
*
|
||||
* @example getHeader('Content-Type') returns 'text'
|
||||
* @example getHeader('CONTENT_TYPE') returns null
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHeader($name, $default = null)
|
||||
{
|
||||
return $this->header->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a $_COOKIE parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCookie($name, $default = null)
|
||||
{
|
||||
return $this->cookie->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a secure protocol was used.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSecure()
|
||||
{
|
||||
if ($this->server->get('HTTPS') === 'on') {
|
||||
return true;
|
||||
}
|
||||
if ($this->server->get('HTTP_X_FORWARDED_SSL') === 'on') {
|
||||
return true;
|
||||
}
|
||||
if ($this->server->get('HTTP_X_FORWARDED_PROTO') === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request is an ajax request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAjax()
|
||||
{
|
||||
return strtolower($this->server->get('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request was issued via command line.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCli()
|
||||
{
|
||||
return php_sapi_name() === 'cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTTP method.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
if ($this->method === null) {
|
||||
$method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
|
||||
if (!in_array($method, self::$supportedMethods, true)) {
|
||||
throw new MethodNotAllowedException(self::$supportedMethods);
|
||||
}
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URI.
|
||||
*
|
||||
* Same as $_SERVER['REQUEST_URI']
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRequestUri()
|
||||
{
|
||||
if (null === $this->requestUri) {
|
||||
$this->requestUri = $this->server->get('REQUEST_URI');
|
||||
}
|
||||
|
||||
return $this->requestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info from the request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPathInfo()
|
||||
{
|
||||
if (null === $this->pathInfo) {
|
||||
$this->pathInfo = !empty($this->server->get('PATH_INFO')) ? $this->server->get('PATH_INFO') : '';
|
||||
}
|
||||
|
||||
return $this->pathInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns protocol and hostname.
|
||||
*
|
||||
* @example 'http://www.xentral.com'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSchemeAndHttpHost()
|
||||
{
|
||||
$protocol = 'http';
|
||||
if ($this->isSecure() || strtolower($this->server->get('REQUEST_SCHEME')) === 'https') {
|
||||
$protocol = 'https';
|
||||
}
|
||||
return sprintf(
|
||||
'%s://%s',
|
||||
$protocol,
|
||||
$this->server->get('HTTP_HOST')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full URL with GET parameters.
|
||||
*
|
||||
* @example ->'http://www.xentral.com/path/index.php?param=1¶m2=2'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullUrl()
|
||||
{
|
||||
return $this->getSchemeAndHttpHost() . $this->getRequestUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL without GET parameters.
|
||||
*
|
||||
* @example 'http://www.xentral.com/path/index.php?var=1' -> 'http://www.xentral.com/path'
|
||||
* @example 'http://www.xentral.com/path/?var=1' -> 'http://www.xentral.com/path'
|
||||
* @example 'http://www.xentral.com/path?var=1' -> 'http://www.xentral.com'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
$baseUrl = '';
|
||||
$uri = $this->getRequestUri();
|
||||
$uri = preg_replace('/([^?]+)[?]?.*/', '\1', $uri);
|
||||
|
||||
$uriParts = explode('/', $uri);
|
||||
for ($i=0; $i < count($uriParts)-1; $i++) {
|
||||
if( $uriParts[$i] !== '') {
|
||||
$baseUrl .= '/' . $uriParts[$i];
|
||||
}
|
||||
}
|
||||
|
||||
$baseUrl = $this->getSchemeAndHttpHost() . $baseUrl;
|
||||
|
||||
//Failsafe?
|
||||
// $queryString = $this->server->get('QUERY_STRING');
|
||||
// parse_str($queryString, $queryParts);
|
||||
//
|
||||
// /** @see /www/api/docs.html#failsafe */
|
||||
// if (isset($queryParts['path'])) {
|
||||
// return sprintf('%s?path=%s', $baseUrl, $queryParts['path']);
|
||||
// }
|
||||
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends specified path to the URL.
|
||||
*
|
||||
* @example getUrlForPath('/path') -> 'http://www.xentral.com/path'
|
||||
*
|
||||
* @param string $path must starts with a slash '/'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrlForPath($path)
|
||||
{
|
||||
if (!preg_match('/^\/.*$/', $path)) {
|
||||
throw new InvalidArgumentException('The first argument must start with a slash "/"');
|
||||
}
|
||||
|
||||
$schemeAndHost = $this->getSchemeAndHttpHost();
|
||||
$urlPath = preg_replace('/^(.*)\/[^\/]*$/', '\1', $this->server->get('SCRIPT_NAME'));
|
||||
|
||||
return sprintf('%s%s%s', $schemeAndHost, $urlPath, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns the path between the URI and the current SCRIPT_NAME
|
||||
*
|
||||
* @example 'http://www.xentral.com/www/api/v1/dateien/50' -> '/v1/dateien/50'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBasePath()
|
||||
{
|
||||
$basePath = '';
|
||||
$scriptNameParts = explode('/', $this->server->get('SCRIPT_NAME'));
|
||||
$uri = preg_replace('/([^?]+)[?]?.*/', '\1', $this->server->get('REQUEST_URI'));
|
||||
$uriParts = explode('/', $uri);
|
||||
|
||||
for($i=0; $i < count($uriParts)-0; $i++) {
|
||||
if (!isset($scriptNameParts[$i]) || $scriptNameParts[$i] !== $uriParts[$i]) {
|
||||
$basePath .= '/'. $uriParts[$i];
|
||||
}
|
||||
}
|
||||
|
||||
if($basePath === '') {
|
||||
$basePath = '/';
|
||||
}
|
||||
|
||||
return $basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getFullUrl or getBaseUrl instead!
|
||||
*
|
||||
* @param bool $withQueryParams GET-Parameter mitliefern?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullUri($withQueryParams = true)
|
||||
{
|
||||
$scheme = $this->server->get('REQUEST_SCHEME');
|
||||
$hostAndPort = $this->server->get('HTTP_HOST');
|
||||
$requestUri = $this->server->get('REQUEST_URI');
|
||||
|
||||
$fullUriWithQueryParams = sprintf('%s://%s%s', $scheme, $hostAndPort, $requestUri);
|
||||
if ($withQueryParams === true) {
|
||||
return $fullUriWithQueryParams;
|
||||
}
|
||||
|
||||
/*
|
||||
* Nachfolgend werden die GET-Parameter aus der Uri entfernt
|
||||
*/
|
||||
|
||||
$offset = strpos($fullUriWithQueryParams, '?');
|
||||
$fullUriWithoutQueryParams = $offset !== false
|
||||
? substr_replace($fullUriWithQueryParams, '', $offset)
|
||||
: $fullUriWithQueryParams;
|
||||
|
||||
// Query-String zerlegen
|
||||
$queryString = $this->server->get('QUERY_STRING');
|
||||
parse_str($queryString, $queryParts);
|
||||
|
||||
/** @see /www/api/docs.html#failsafe */
|
||||
if (isset($queryParts['path'])) {
|
||||
return $fullUriWithoutQueryParams . '?path=' . $queryParts['path'];
|
||||
}
|
||||
|
||||
return $fullUriWithoutQueryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the failsafe URL was used for the request
|
||||
*
|
||||
* @example Failsafe-Uri: /api/index.php?path=/v1/adressen
|
||||
*
|
||||
* @see /www/api/docs.html#failsafe
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailsafeUri()
|
||||
{
|
||||
$queryString = $this->server->get('QUERY_STRING');
|
||||
parse_str($queryString, $queryParts);
|
||||
|
||||
return isset($queryParts['path']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request body.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if (null === $this->content) {
|
||||
$this->content = file_get_contents('php://input');
|
||||
}
|
||||
|
||||
return !empty($this->content) ? $this->content : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content type of the request.
|
||||
*
|
||||
* @return string|null [json|xml|html|...] null if not set
|
||||
*/
|
||||
public function getContentType()
|
||||
{
|
||||
$contentTypeRaw = $this->header->get('Content-Type');
|
||||
if ($contentTypeRaw === null || $contentTypeRaw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Boundary bei Multipart Content-Type entfernen
|
||||
// @example "Content-Type: multipart/form-data; boundary=gc0p4Jq0M2Yt08jU534c0p"
|
||||
$posSemicolon = strpos($contentTypeRaw, ';');
|
||||
if ($posSemicolon !== false) {
|
||||
$contentTypeRaw = trim(substr($contentTypeRaw, 0, $posSemicolon));
|
||||
}
|
||||
|
||||
$typeParts = explode('/', strtolower($contentTypeRaw));
|
||||
if (count($typeParts) < 2) {
|
||||
throw new HttpException(400, sprintf('Invalid content type "%s".', $contentTypeRaw));
|
||||
}
|
||||
|
||||
return $typeParts[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data types from the HTTP Accept header.
|
||||
*
|
||||
* @return array [] if not set
|
||||
*/
|
||||
public function getAcceptableContentTypes()
|
||||
{
|
||||
if (null === $this->acceptableContentTypes) {
|
||||
$acceptHeaderRaw = $this->header->get('Accept');
|
||||
$acceptParts = explode(',', $acceptHeaderRaw);
|
||||
|
||||
$acceptable = [];
|
||||
foreach ($acceptParts as $acceptPart) {
|
||||
if ($pos = strpos($acceptPart, ';')) {
|
||||
// Priorität abschneiden
|
||||
$acceptPart = substr($acceptPart, 0, $pos);
|
||||
}
|
||||
$acceptable[] = trim($acceptPart);
|
||||
}
|
||||
|
||||
$this->acceptableContentTypes = $acceptable;
|
||||
}
|
||||
|
||||
return $this->acceptableContentTypes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Xentral\Components\Http\Cookie\Cookie;
|
||||
use Xentral\Components\Http\Cookie\CookieCollection;
|
||||
use Xentral\Components\Http\Exception\HttpHeaderValueException;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
class Response
|
||||
{
|
||||
const HTTP_CONTINUE = 100;
|
||||
const HTTP_SWITCHING_PROTOCOLS = 101;
|
||||
const HTTP_OK = 200;
|
||||
const HTTP_CREATED = 201;
|
||||
const HTTP_ACCEPT = 202;
|
||||
const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
|
||||
const HTTP_NO_CONTENT = 204;
|
||||
const HTTP_RESET_CONTENT = 205;
|
||||
const HTTP_PARTIAL_CONTENT = 206;
|
||||
const HTTP_MULTIPLE_CHOICES = 300;
|
||||
const HTTP_MOVED_PERMANENTLY = 301;
|
||||
const HTTP_MOVED_TEMPORARILY = 302;
|
||||
const HTTP_SEE_OTHER = 303;
|
||||
const HTTP_NOT_MODIFIED = 304;
|
||||
const HTTP_USE_PROXY = 305;
|
||||
const HTTP_TEMPORARY_REDIRECT = 307;
|
||||
const HTTP_BAD_REQUEST = 400;
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
const HTTP_PAYMENT_REQUIRED = 402;
|
||||
const HTTP_FORBIDDEN = 403;
|
||||
const HTTP_NOT_FOUND = 404;
|
||||
const HTTP_METHOD_NOT_ALLOWED = 405;
|
||||
const HTTP_NOT_ACCEPTABLE = 406;
|
||||
const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
|
||||
const HTTP_REQUEST_TIMEOUT = 408;
|
||||
const HTTP_CONFILICT = 409;
|
||||
const HTTP_GONE = 410;
|
||||
const HTTP_LENGTH_REQUIRED = 411;
|
||||
const HTTP_PRECONDITION_FAILED = 412;
|
||||
const HTTP_PAYLOAD_TOO_LARGE = 413;
|
||||
const HTTP_URI_TOO_LONG = 414;
|
||||
const HTTP_UNSUPPORTET_MEDIA_TYPE = 415;
|
||||
const HTTP_RANGE_NOT_SATISFIABLE = 416;
|
||||
const HTTP_EXPECTATION_FAILED = 417;
|
||||
const HTTP_UNPROCESSABLE_ENTITY = 422;
|
||||
const HTTP_UPGRADE_REQUIRED = 426;
|
||||
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
||||
const HTTP_NOT_IMPLEMENTED = 501;
|
||||
const HTTP_BAD_GATEWAY = 502;
|
||||
const HTTP_SERVICE_UNAVAILABLE = 503;
|
||||
const HTTP_GATEWAY_TIMEOUT = 504;
|
||||
const HTTP_VERSION_NOT_SUPPORTED = 505;
|
||||
const DISPOSITION_INLINE = 'inline';
|
||||
const DISPOSITION_ATTACHMENT = 'attachment';
|
||||
|
||||
/** @var array $statusMessages */
|
||||
protected static $statusMessages = [
|
||||
self::HTTP_CONTINUE => 'Continue',
|
||||
self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols',
|
||||
self::HTTP_NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information',
|
||||
self::HTTP_OK => 'OK',
|
||||
self::HTTP_CREATED => 'Created',
|
||||
self::HTTP_ACCEPT => 'Accepted',
|
||||
self::HTTP_NO_CONTENT => 'No Content',
|
||||
self::HTTP_RESET_CONTENT => 'Reset Content',
|
||||
self::HTTP_PARTIAL_CONTENT => 'Partial Content',
|
||||
self::HTTP_MULTIPLE_CHOICES => 'Multiple Choices',
|
||||
self::HTTP_MOVED_PERMANENTLY => 'Moved Permanently',
|
||||
self::HTTP_MOVED_TEMPORARILY => 'Found',
|
||||
self::HTTP_SEE_OTHER => 'See Other',
|
||||
self::HTTP_NOT_MODIFIED => 'Not Modified',
|
||||
self::HTTP_USE_PROXY => 'Use Proxy',
|
||||
self::HTTP_TEMPORARY_REDIRECT => 'Temporary Redirect',
|
||||
self::HTTP_BAD_REQUEST => 'Bad Request',
|
||||
self::HTTP_UNAUTHORIZED => 'Unauthorized',
|
||||
self::HTTP_PAYMENT_REQUIRED => 'Payment Required',
|
||||
self::HTTP_FORBIDDEN => 'Forbidden',
|
||||
self::HTTP_NOT_FOUND => 'Not Found',
|
||||
self::HTTP_METHOD_NOT_ALLOWED => 'Method Not Allowed',
|
||||
self::HTTP_NOT_ACCEPTABLE => 'Not Acceptable',
|
||||
self::HTTP_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
|
||||
self::HTTP_REQUEST_TIMEOUT => 'Request Timeout',
|
||||
self::HTTP_CONFILICT => 'Conflict',
|
||||
self::HTTP_GONE => 'Gone',
|
||||
self::HTTP_LENGTH_REQUIRED => 'Length Required',
|
||||
self::HTTP_PRECONDITION_FAILED => 'Precondition Failed',
|
||||
self::HTTP_PAYLOAD_TOO_LARGE => 'Payload Too Large',
|
||||
self::HTTP_URI_TOO_LONG => 'URI Too Long',
|
||||
self::HTTP_UNSUPPORTET_MEDIA_TYPE => 'Unsupported Media Type',
|
||||
self::HTTP_RANGE_NOT_SATISFIABLE => 'Range Not Satisfiable',
|
||||
self::HTTP_EXPECTATION_FAILED => 'Expectation Failed',
|
||||
self::HTTP_UPGRADE_REQUIRED => 'Upgrade Required',
|
||||
self::HTTP_INTERNAL_SERVER_ERROR => 'Internal Server Error',
|
||||
self::HTTP_NOT_IMPLEMENTED => 'Not Implemented',
|
||||
self::HTTP_BAD_GATEWAY => 'Bad Gateway',
|
||||
self::HTTP_SERVICE_UNAVAILABLE => 'Service Unavailable',
|
||||
self::HTTP_GATEWAY_TIMEOUT => 'Gateway Timeout',
|
||||
self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',
|
||||
];
|
||||
|
||||
/** @var array $oneValueHeaders headers which can hold ONE value ONLY */
|
||||
protected static $oneValueHeaders = ['content-type', 'content-length', 'content-disposition', 'date'];
|
||||
|
||||
/** @var array $headers */
|
||||
protected $headers = [];
|
||||
|
||||
/** @var array $headerNames */
|
||||
protected $headerNames = [];
|
||||
|
||||
/** @var string $content Response content */
|
||||
protected $content;
|
||||
|
||||
/** @var int $statusCode HTTP status code */
|
||||
protected $statusCode;
|
||||
|
||||
/** @var string $statusText HTTP status text */
|
||||
protected $statusText;
|
||||
|
||||
/** @var string $protocolVersion HTTP protocol version */
|
||||
protected $protocolVersion;
|
||||
|
||||
/** @var CookieCollection $cookies */
|
||||
protected $cookies;
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
* @param string $protocolVersion
|
||||
* @param null $statusText
|
||||
* @param CookieCollection|array $cookies
|
||||
*/
|
||||
public function __construct(
|
||||
$content = null,
|
||||
$statusCode = self::HTTP_OK,
|
||||
array $headers = [],
|
||||
$protocolVersion = '1.1',
|
||||
$statusText = null,
|
||||
$cookies = []
|
||||
) {
|
||||
$this->statusCode = (int)$statusCode;
|
||||
$this->headers = [];
|
||||
foreach ($headers as $name => $value) {
|
||||
$this->setHeader($name, $value);
|
||||
}
|
||||
if (!$this->hasHeader('content-type')) {
|
||||
$this->setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
}
|
||||
$this->setContent($content);
|
||||
$this->protocolVersion = $protocolVersion;
|
||||
$this->statusText = $statusText;
|
||||
$this->setCookies($cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the response headers and content to the client.
|
||||
*
|
||||
* @param DateTimeInterface|null $sendTime
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function send(DateTimeInterface $sendTime = null)
|
||||
{
|
||||
header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getStatusText()));
|
||||
|
||||
if (!$this->hasHeader('date')) {
|
||||
if ($sendTime === null) {
|
||||
$time = time();
|
||||
} else {
|
||||
$time = $sendTime->getTimestamp();
|
||||
}
|
||||
$this->setHeader('Date', gmdate('D, d M Y H:i:s \G\M\T', $time));
|
||||
}
|
||||
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$replace = strtolower($name) === 'content-type';
|
||||
$value = implode(', ', $value);
|
||||
header(sprintf('%s: %s', $this->headerNames[$name], $value), $replace, $this->getStatusCode());
|
||||
}
|
||||
foreach ($this->cookies as $key => $cookie) {
|
||||
header($cookie->toHttpHeader(), false);
|
||||
}
|
||||
|
||||
if ($this->content !== null) {
|
||||
echo $this->content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response body.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response body.
|
||||
*
|
||||
* @param string|null $content
|
||||
*/
|
||||
public function setContent($content)
|
||||
{
|
||||
if ($content !== null) {
|
||||
$this->content = (string)$content;
|
||||
$this->setHeader('Content-Length', (string)strlen($this->content));
|
||||
} else {
|
||||
$this->content = null;
|
||||
unset($this->headers['content-type'], $this->headers['content-length']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP status code.
|
||||
*
|
||||
* @return int HTTP status code
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code.
|
||||
*
|
||||
* @param int $statusCode HTTP status code
|
||||
*/
|
||||
public function setStatusCode($statusCode)
|
||||
{
|
||||
if (!array_key_exists($statusCode, self::$statusMessages)) {
|
||||
throw new InvalidArgumentException(sprintf('Status Code %s is not supported.', $statusCode));
|
||||
}
|
||||
|
||||
$this->statusCode = (int)$statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTTP status text (=reason).
|
||||
*
|
||||
* @return string HTTP status text
|
||||
*/
|
||||
public function getStatusText()
|
||||
{
|
||||
if ($this->statusText === null) {
|
||||
$this->statusText = self::$statusMessages[$this->statusCode];
|
||||
}
|
||||
|
||||
return $this->statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status text (=reason).
|
||||
*
|
||||
* @param string $message
|
||||
*/
|
||||
public function setStatusText($message)
|
||||
{
|
||||
$this->statusText = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProtocolVersion()
|
||||
{
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets header (overwrites existing header).
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|string[] $values
|
||||
*/
|
||||
public function setHeader($name, $values)
|
||||
{
|
||||
if (!is_string($values) && !is_array($values)) {
|
||||
throw new HttpHeaderValueException(
|
||||
'Invalid header, only string|string[] allowed',
|
||||
0,
|
||||
null,
|
||||
$values
|
||||
);
|
||||
}
|
||||
if ($values === '' || $values === []) {
|
||||
throw new HttpHeaderValueException('Empty header not allowed', 0, null, $values);
|
||||
}
|
||||
if (!is_array($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
foreach ($values as $value) {
|
||||
$value = $this->sanitizeHeaderValue($value);
|
||||
if (!is_string($value) || $value === '') {
|
||||
throw new HttpHeaderValueException('Invalid header', 0, null, $value);
|
||||
}
|
||||
}
|
||||
$normalized = $this->normalizeHeaderName($name);
|
||||
$this->headers[$normalized] = $values;
|
||||
$this->headerNames[$normalized] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets header (appends existing header).
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
*/
|
||||
public function addHeader($name, $value)
|
||||
{
|
||||
if (in_array($this->normalizeHeaderName($name), self::$oneValueHeaders, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Cannot append header "%s".', $name));
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
throw new HttpHeaderValueException('Invalid header', 0, null, $value);
|
||||
}
|
||||
$value = $this->sanitizeHeaderValue($value);
|
||||
if ($value === '') {
|
||||
throw new HttpHeaderValueException('Empty header not allowed', 0, null, $value);
|
||||
}
|
||||
|
||||
$normalized = $this->normalizeHeaderName($name);
|
||||
$header = $this->getHeader($normalized);
|
||||
if (count($header) === 0) {
|
||||
$this->headerNames[$normalized] = $name;
|
||||
}
|
||||
|
||||
if (count(array_intersect($header, [$value])) === 0) {
|
||||
$header[] = $value;
|
||||
$this->headers[$normalized] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets header as array.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string[]|array empty if not set
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
$normalized = $this->normalizeHeaderName($name);
|
||||
if ($this->hasHeader($normalized)) {
|
||||
return $this->headers[$normalized];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets header as comma-seperated string.
|
||||
*
|
||||
* @example getHeaderLine(Headername) -> 'value1, value2'
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null null if header not set
|
||||
*/
|
||||
public function getHeaderLine($name)
|
||||
{
|
||||
$header = $this->getHeader($name);
|
||||
if (count($header) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return implode(', ', $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the header exists.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasHeader($name)
|
||||
{
|
||||
return array_key_exists($this->normalizeHeaderName($name), $this->headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all headers
|
||||
*
|
||||
* @return array all headers
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$headers[$this->headerNames[$name]] = $value;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all headers with normalized names
|
||||
*
|
||||
* @return array all headers
|
||||
*/
|
||||
public function getHeadersNormalized()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the Content-Type header.
|
||||
*
|
||||
* @example getContentType() -> 'text/html; charset=utf-8'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType()
|
||||
{
|
||||
return $this->getHeaderLine('content-type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the Content-Type header.
|
||||
*
|
||||
* @param string $contentType
|
||||
* @param string $charset
|
||||
*/
|
||||
public function setContentType($contentType, $charset = 'utf-8')
|
||||
{
|
||||
$this->setHeader('Content-Type', sprintf('%s; charset=%s', $contentType, $charset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value of the Content-Disposition header.
|
||||
*
|
||||
* @example getContentDisposition() -> 'attachment; filename*="file.txt"; filename="file.txt"'
|
||||
*
|
||||
* @return string Content-Disposition header
|
||||
*/
|
||||
public function getContentDisposition()
|
||||
{
|
||||
return $this->getHeaderLine('content-disposition');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Disposition HTTP header.
|
||||
*
|
||||
* @param string $disposition values: 'inline'|'attachment'
|
||||
* @param string $clientFileName file name for download on client
|
||||
*/
|
||||
public function setContentDisposition($disposition = self::DISPOSITION_ATTACHMENT, $clientFileName = '')
|
||||
{
|
||||
$disposition = strtolower($disposition);
|
||||
if (!in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE], true)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid Content-Disposition "%s".', $disposition));
|
||||
}
|
||||
if ($clientFileName === '') {
|
||||
throw new InvalidArgumentException('Filename required.');
|
||||
}
|
||||
|
||||
$encodedName = urlencode(StringUtil::toFilename($clientFileName));
|
||||
$fallbackName = StringUtil::toAscii($clientFileName);
|
||||
|
||||
$header = sprintf('%s; filename*="%s"', $disposition, $encodedName);
|
||||
if ($fallbackName !== '') {
|
||||
$header .= sprintf('; filename="%s"', $fallbackName);
|
||||
}
|
||||
$this->setHeader('Content-Disposition', $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all non-ASCII characters from a header value
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitizeHeaderValue($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new HttpHeaderValueException('Invalid header', 0, null, $value);
|
||||
}
|
||||
|
||||
return StringUtil::toAscii($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param int $timeToLive 0 = for ever
|
||||
*/
|
||||
public function addSimpleCookie($name, $value, $timeToLive = 0)
|
||||
{
|
||||
$this->addCookie(new Cookie($name, $value, $timeToLive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cookie $cookie
|
||||
*/
|
||||
public function addCookie(Cookie $cookie)
|
||||
{
|
||||
$this->cookies[] = $cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cookieName
|
||||
*/
|
||||
public function removeCookie($cookieName)
|
||||
{
|
||||
foreach ($this->cookies as $key => $cookie) {
|
||||
if ($cookieName === $cookie->getName()) {
|
||||
unset($this->cookies[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CookieCollection
|
||||
*/
|
||||
public function getCookies()
|
||||
{
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CookieCollection|array $cookies
|
||||
*/
|
||||
protected function setCookies($cookies)
|
||||
{
|
||||
if (is_object($cookies) && get_class($cookies) === CookieCollection::class) {
|
||||
$this->cookies = $cookies;
|
||||
} else {
|
||||
$this->cookies = new CookieCollection($cookies);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms header name to normalized form.
|
||||
*
|
||||
* @example 'HEADER-NAME' -> 'header-name'
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeHeaderName($name)
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9\-]+$/', $name)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid character in header name "%s".', $name));
|
||||
}
|
||||
|
||||
return str_replace('_', '-', strtolower($name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
use Xentral\Components\Http\Exception\CsrfTokenException;
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
final class CsrfTokenManager
|
||||
{
|
||||
/** @var int CSRF_TOKEN_LENGTH */
|
||||
const CSRF_TOKEN_LENGTH = 32;
|
||||
/** @var array $tokenData */
|
||||
private $tokenData;
|
||||
|
||||
/**
|
||||
* @param array $tokenData
|
||||
*/
|
||||
public function __construct($tokenData = [])
|
||||
{
|
||||
$this->tokenData = $tokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @param bool $remove
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isTokenValid($key, $value, $remove = true)
|
||||
{
|
||||
$this->ensureTokenKeyFormat($key);
|
||||
$valid = false;
|
||||
if (array_key_exists($key, $this->tokenData) && $this->tokenData[$key] === $value) {
|
||||
$valid = true;
|
||||
}
|
||||
if ($valid === true && $remove === true) {
|
||||
$this->removeToken($key);
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
*
|
||||
* @throws CsrfTokenException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function createToken($key)
|
||||
{
|
||||
$this->ensureTokenKeyFormat($key);
|
||||
$token = StringUtil::random(self::CSRF_TOKEN_LENGTH, true);
|
||||
$this->tokenData[$key] = $token;
|
||||
|
||||
if (strlen($token) < self::CSRF_TOKEN_LENGTH) {
|
||||
throw new CsrfTokenException('Could not create CSRF token.');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeToken($key)
|
||||
{
|
||||
$this->ensureTokenKeyFormat($key);
|
||||
unset($this->tokenData[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function refreshToken($key)
|
||||
{
|
||||
$this->ensureTokenKeyFormat($key);
|
||||
$this->removeToken($key);
|
||||
|
||||
return $this->createToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $target
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function dumpTokens(&$target)
|
||||
{
|
||||
$target = $this->tokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function ensureTokenKeyFormat($key)
|
||||
{
|
||||
if (!preg_match('/^[a-z][a-z0-9_]*$/', $key)) {
|
||||
throw new InvalidArgumentException('Invalid token key format.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
final class FlashMessageCollection
|
||||
{
|
||||
/** @var FlashMessageData[] $data */
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* FlashMessageCollection constructor.
|
||||
*
|
||||
* @param array $flashMessageData array structure from session
|
||||
*/
|
||||
public function __construct($flashMessageData = [])
|
||||
{
|
||||
$data = $this->fromSessionArray($flashMessageData);
|
||||
$this->data = $this->sortByPriority($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and removes all flash messages from previous session
|
||||
*
|
||||
* @return FlashMessageData[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
$result = $this->data;
|
||||
$this->data = [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and removes flash messages filtered
|
||||
*
|
||||
* @param string|null $segment filter for segment (null=deactivated)
|
||||
* @param string|null $type filter for type (null=deactivated)
|
||||
*
|
||||
* @return FlashMessageData[] sorted by priority
|
||||
*/
|
||||
public function getMessages($segment = null, $type = null)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->data as $key => $item) {
|
||||
if (
|
||||
($segment === null || $segment === $item->getSegmentName())
|
||||
&& ($type === null || $type === $item->getType())
|
||||
) {
|
||||
$result[] = $item;
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and removes flash messages filtered
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return FlashMessageData[] sorted by priority
|
||||
*/
|
||||
public function getMessagesByType($type)
|
||||
{
|
||||
return $this->getMessages(null, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and removes flash messages filtered
|
||||
*
|
||||
* @param string $segment
|
||||
*
|
||||
* @return FlashMessageData[] sorted by priority
|
||||
*/
|
||||
public function getMessagesBySegment($segment)
|
||||
{
|
||||
return $this->getMessages($segment, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array to be stored in session
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSessionArray()
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->data as $item) {
|
||||
$result[] = $item->toSessionArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts flash messages by priority (descending)
|
||||
*
|
||||
* @param FlashMessageData[] $messages
|
||||
*
|
||||
* @return FlashMessageData[]
|
||||
*/
|
||||
public function sortByPriority($messages)
|
||||
{
|
||||
usort($messages, [$this, 'comparePriorityCallback']);
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*
|
||||
* @return FlashMessageData[]
|
||||
*/
|
||||
private function fromSessionArray($data)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $item) {
|
||||
$result[] = FlashMessageData::createFromArray($item);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callable compare function for sorting.
|
||||
*
|
||||
* @param FlashMessageData $insert
|
||||
* @param FlashMessageData $exist
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function comparePriorityCallback($insert, $exist)
|
||||
{
|
||||
if ($insert->getPriority() === $exist->getPriority()) {
|
||||
return 0;
|
||||
}
|
||||
if ($insert->getPriority() > $exist->getPriority()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
|
||||
final class FlashMessageData
|
||||
{
|
||||
/** @var string FLASHTYPE_DEFAULT */
|
||||
const FLASHTYPE_DEFAULT = 'default';
|
||||
|
||||
/** @var string FLASHTYPE_NOTICE */
|
||||
const FLASHTYPE_NOTICE = 'notice';
|
||||
|
||||
/** @var string FLASHTYPE_SUCCESS */
|
||||
const FLASHTYPE_SUCCESS = 'success';
|
||||
|
||||
/** @var string FLASHTYPE_WARNING */
|
||||
const FLASHTYPE_WARNING = 'warning';
|
||||
|
||||
/** @var string FLASHTYPE_ERROR */
|
||||
const FLASHTYPE_ERROR = 'error';
|
||||
|
||||
/** @var array $flashTypes */
|
||||
public static $flashTypes = [
|
||||
self::FLASHTYPE_DEFAULT,
|
||||
self::FLASHTYPE_NOTICE,
|
||||
self::FLASHTYPE_SUCCESS,
|
||||
self::FLASHTYPE_WARNING,
|
||||
self::FLASHTYPE_ERROR,
|
||||
];
|
||||
|
||||
/**@var string $type */
|
||||
private $type;
|
||||
|
||||
/** @var string $message */
|
||||
private $message;
|
||||
|
||||
/** @var int $priority */
|
||||
private $priority;
|
||||
|
||||
/** @var string $segmentName */
|
||||
private $segmentName;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @param string $segmentName
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct($message, $type, $segmentName = '', $priority = 0)
|
||||
{
|
||||
$this->setType($type);
|
||||
$this->message = $message;
|
||||
$this->setSegmentName($segmentName);
|
||||
$this->priority = (int)$priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create FlashMessageData object from session array entry
|
||||
*
|
||||
* @param array $data required keys: 'priority', 'segment, 'type', 'message'
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return FlashMessageData
|
||||
*/
|
||||
public static function createFromArray($data)
|
||||
{
|
||||
if (
|
||||
!array_key_exists('priority', $data)
|
||||
|| !array_key_exists('segment', $data)
|
||||
|| !array_key_exists('type', $data)
|
||||
|| !array_key_exists('message', $data)
|
||||
) {
|
||||
throw new InvalidArgumentException('Invalid array data for FlashMessageData.');
|
||||
}
|
||||
|
||||
return new self($data['message'], $data['type'], $data['segment'], $data['priority']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getSegmentName()
|
||||
{
|
||||
return $this->segmentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array to store in the session
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSessionArray()
|
||||
{
|
||||
return [
|
||||
'priority' => $this->getPriority(),
|
||||
'segment' => $this->getSegmentName(),
|
||||
'type' => $this->getType(),
|
||||
'message' => $this->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function setType($type)
|
||||
{
|
||||
if (!in_array($type, self::$flashTypes, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown message type "%s".', $type));
|
||||
}
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setSegmentName($name)
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9_]*$/', $name)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid Segment Name "%s".', $name));
|
||||
}
|
||||
|
||||
$this->segmentName = $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
|
||||
final class Segment
|
||||
{
|
||||
/** @var Session $session */
|
||||
private $session;
|
||||
|
||||
/** @var string $name */
|
||||
private $name;
|
||||
|
||||
/** @var array $data */
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @param Session $session
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(Session $session, $name, $data = [])
|
||||
{
|
||||
$this->session = $session;
|
||||
$this->name = $name;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an entry to the segment
|
||||
*
|
||||
* @param string $key
|
||||
* @param int|float|string|array $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($key, $value)
|
||||
{
|
||||
$this->ensureSegmentKeyFormat($key);
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value with a specific key
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed|null $default
|
||||
* @param bool $clear true=remove entry from the session
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue($key, $default = null, $clear = false)
|
||||
{
|
||||
$this->ensureSegmentKeyFormat($key);
|
||||
$value = $default;
|
||||
if (isset($this->data[$key])) {
|
||||
$value = $this->data[$key];
|
||||
}
|
||||
if ($clear === true) {
|
||||
$this->removeValue($key);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new flash message to the segment
|
||||
*
|
||||
* @internal The actual segment which holds the flashes is 'flash_messages'.
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @param int $priority sorted highest to lowest
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addFlashMessage($message, $type = FlashMessageData::FLASHTYPE_DEFAULT, $priority = 0)
|
||||
{
|
||||
$this->session->addFlashMessage($this->name, $message, $type, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes single entry
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeValue($key)
|
||||
{
|
||||
$this->ensureSegmentKeyFormat($key);
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries and flashes of this segment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
$this->data = [];
|
||||
$this->session->getFlashMessages($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all entries
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function ensureSegmentKeyFormat($key)
|
||||
{
|
||||
if (!preg_match('/^[a-z][a-z0-9_]*$/', $key)) {
|
||||
throw new InvalidArgumentException('Invalid segment key format.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
use Xentral\Components\Http\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Http\Exception\SessionSegmentException;
|
||||
|
||||
class Session
|
||||
{
|
||||
/** @var string FLASH_SEGMENTKEY */
|
||||
const FLASH_SEGMENTKEY = 'flash_messages';
|
||||
|
||||
/** @var string CSRF_SEGMENTKEY */
|
||||
const CSRF_SEGMENTKEY = 'csrf_tokens';
|
||||
|
||||
/** @var array $data */
|
||||
protected $data;
|
||||
|
||||
/** @var Segment[] $segments */
|
||||
protected $segments;
|
||||
|
||||
/** @var FlashMessageCollection $flashData */
|
||||
protected $flashMessages;
|
||||
|
||||
/** @var CsrfTokenManager $csrfTokens */
|
||||
protected $csrfTokens;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct($data = [])
|
||||
{
|
||||
$this->data = (array)$data;
|
||||
$this->segments = [];
|
||||
$this->flashMessages = $this->createFlashMessageCollection();
|
||||
$this->csrfTokens = $this->createCsrfTokenManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all Session data and flash messages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
$this->data = [];
|
||||
$this->segments = [];
|
||||
$this->flashMessages = new FlashMessageCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value with a specific key from the current Segment
|
||||
*
|
||||
* @param string $segment
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @param bool $clear true=remove entry from the session
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue($segment, $key, $default = null, $clear = false)
|
||||
{
|
||||
|
||||
return $this->getSegment($segment)->getValue($key, $default, $clear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an entry to the current Segment
|
||||
*
|
||||
* @param string $segment
|
||||
* @param string $key
|
||||
* @param string|int|float|array $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($segment, $key, $value)
|
||||
{
|
||||
$this->getSegment($segment)->setValue($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single Entry from the current segment
|
||||
*
|
||||
* @param string $segment
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeValue($segment, $key)
|
||||
{
|
||||
$this->getSegment($segment)->removeValue($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a segment object by it's name
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return Segment
|
||||
*/
|
||||
public function getSegment($name = '')
|
||||
{
|
||||
if ($name === self::FLASH_SEGMENTKEY || $name === self::CSRF_SEGMENTKEY) {
|
||||
throw new SessionSegmentException(
|
||||
sprintf('"%s" is a reserved segment name.', self::FLASH_SEGMENTKEY)
|
||||
);
|
||||
}
|
||||
$segmentId = $this->getSegmentKey($name);
|
||||
if (array_key_exists($segmentId, $this->segments)) {
|
||||
return $this->segments[$segmentId];
|
||||
}
|
||||
$data = [];
|
||||
if (array_key_exists($segmentId, $this->data)) {
|
||||
$data = $this->data[$segmentId];
|
||||
}
|
||||
$segment = new Segment($this, $name, $data);
|
||||
$this->segments[$segmentId] = $segment;
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the whole session into specific target variable.
|
||||
*
|
||||
* @param mixed $targetVariable
|
||||
*/
|
||||
public function dumpSession(&$targetVariable)
|
||||
{
|
||||
$this->mergeSession();
|
||||
$targetVariable = $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
$this->dumpSession($dump);
|
||||
$this->csrfTokens->dumpTokens($tokendump);
|
||||
$tokendump = array_keys($tokendump);
|
||||
|
||||
return [
|
||||
'data' => $dump,
|
||||
'tokens' => $tokendump
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new flash message to specific segment.
|
||||
*
|
||||
* @param string $segment if empty: default segment will be used
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @param int $priority
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addFlashMessage($segment, $message, $type = FlashMessageData::FLASHTYPE_DEFAULT, $priority = 0)
|
||||
{
|
||||
$flash = new FlashMessageData($message, $type, $segment, $priority);
|
||||
$flashData = $flash->toSessionArray();
|
||||
$key = (string)$this->getSegmentKey(self::FLASH_SEGMENTKEY);
|
||||
$this->data[$key][] = $flashData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets flash message(s) by specific filter conditions.
|
||||
*
|
||||
* The flash message will be cleared from the session after retrieving
|
||||
*
|
||||
* @param string|null $segment filter for segment name
|
||||
* @param string|null $type filter for message type
|
||||
*
|
||||
* @return FlashMessageData[] flash messages sorted by priority
|
||||
*/
|
||||
public function getFlashMessages($segment = null, $type = null)
|
||||
{
|
||||
return $this->flashMessages->getMessages($segment, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CSRF Token and stores it in the Session
|
||||
*
|
||||
* @param string $tokenKey
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function createCsrfToken($tokenKey)
|
||||
{
|
||||
return $this->csrfTokens->createToken($tokenKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified Token is valid
|
||||
*
|
||||
* @param string $tokenKey
|
||||
* @param string $tokenValue
|
||||
* @param bool $remove true=remove token from session to mitigate second use
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCsrfTokenValid($tokenKey, $tokenValue, $remove = false)
|
||||
{
|
||||
return $this->csrfTokens->isTokenValid($tokenKey, $tokenValue, $remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $segmentName
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getSegmentKey($segmentName)
|
||||
{
|
||||
$this->ensureSegmentNameFormat($segmentName);
|
||||
|
||||
return sprintf('segment_%s', $segmentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge all Segments and Flashmessages and CsrfTokens into the session array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function mergeSession()
|
||||
{
|
||||
foreach ($this->segments as $key => $segment) {
|
||||
$segmentData = $segment->getAll();
|
||||
if (count($segmentData) > 0) {
|
||||
$this->data[$key] = $segmentData;
|
||||
}
|
||||
}
|
||||
|
||||
$flashKey = $this->getSegmentKey(self::FLASH_SEGMENTKEY);
|
||||
$newFlashes = [];
|
||||
if (array_key_exists($flashKey, $this->data)) {
|
||||
$newFlashes = $this->data[$flashKey];
|
||||
}
|
||||
$oldFlashes = $this->flashMessages->toSessionArray();
|
||||
$this->flashMessages = new FlashMessageCollection();
|
||||
$allFlashes = array_merge($oldFlashes, $newFlashes);
|
||||
if (count($allFlashes) > 0) {
|
||||
$this->data[$flashKey] = $allFlashes;
|
||||
} else {
|
||||
unset($this->data[$flashKey]);
|
||||
}
|
||||
|
||||
$this->csrfTokens->dumpTokens($tokens);
|
||||
$tokenSegmentKey = $this->getSegmentKey(self::CSRF_SEGMENTKEY);
|
||||
if (is_array($tokens) && count($tokens) > 0) {
|
||||
$this->data[$tokenSegmentKey] = $tokens;
|
||||
} else {
|
||||
unset($this->data[$tokenSegmentKey]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlashMessageCollection
|
||||
*/
|
||||
private function createFlashMessageCollection()
|
||||
{
|
||||
$key = $this->getSegmentKey(self::FLASH_SEGMENTKEY);
|
||||
if (!array_key_exists($key, $this->data)) {
|
||||
return new FlashMessageCollection();
|
||||
}
|
||||
$messages = $this->data[$key];
|
||||
$result = new FlashMessageCollection($messages);
|
||||
unset($this->data[$key]);
|
||||
$this->data[$key] = [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CsrfTokenManager
|
||||
*/
|
||||
private function createCsrfTokenManager()
|
||||
{
|
||||
$key = $this->getSegmentKey(self::CSRF_SEGMENTKEY);
|
||||
if (!array_key_exists($key, $this->data)) {
|
||||
return new CsrfTokenManager();
|
||||
}
|
||||
$tokens = $this->data[$key];
|
||||
$result = new CsrfTokenManager($tokens);
|
||||
unset($this->data[$key]);
|
||||
$this->data[$key] = [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function ensureSegmentNameFormat($name)
|
||||
{
|
||||
if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
|
||||
throw new InvalidArgumentException('Invalid segment name format.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Session;
|
||||
|
||||
use Xentral\Components\Http\Exception\SessionException;
|
||||
|
||||
class SessionHandler
|
||||
{
|
||||
/**
|
||||
* Create a session object with actual session data
|
||||
*
|
||||
* @throws SessionException
|
||||
*
|
||||
* @return Session Session object
|
||||
*/
|
||||
public static function createSession()
|
||||
{
|
||||
if (!extension_loaded('session')) {
|
||||
throw new SessionException('PHP extension "session" is missing.');
|
||||
}
|
||||
|
||||
if (self::isSessionStarted()) {
|
||||
throw new SessionException('Failed to create session. Session can be started only once.');
|
||||
}
|
||||
if (session_status() === PHP_SESSION_DISABLED) {
|
||||
throw new SessionException('Failed to create session. Sessions are disabled.');
|
||||
}
|
||||
|
||||
$isStarted = session_start();
|
||||
if ($isStarted === false) {
|
||||
throw new SessionException('Failed to create session. Initialization failed.');
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
throw new SessionException('Failed to create session. Unexpected status: PHP_SESSION_NONE');
|
||||
}
|
||||
|
||||
return new Session($_SESSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSessionStarted()
|
||||
{
|
||||
$status = session_status();
|
||||
|
||||
return $status === PHP_SESSION_ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and close the session
|
||||
*
|
||||
* @param Session $session
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function commitSession(Session $session)
|
||||
{
|
||||
$session->dumpSession($_SESSION);
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
# Http-Komponente
|
||||
|
||||
Die Http-Komponente ist eine objektorientierte Abstraction der HTTP-Spezifikation.
|
||||
|
||||
## Request-Klasse
|
||||
|
||||
Die Request-Klasse beinhaltet `$_GET`, `$_POST`, `$_FILES`, `$_COOKIE` (todo), und `$_SERVER`.
|
||||
|
||||
### Request erstellen
|
||||
|
||||
```php
|
||||
$request = Request::createFromGlobals();
|
||||
```
|
||||
|
||||
ist das gleiche wie
|
||||
|
||||
```php
|
||||
$request = new Request(
|
||||
$_GET,
|
||||
$_POST,
|
||||
$_FILES,
|
||||
$_SERVER
|
||||
$_COOKIE,
|
||||
);
|
||||
```
|
||||
|
||||
createFromGlobals ist die empfohlene Methode
|
||||
|
||||
##### Request aus Container holen
|
||||
|
||||
```php
|
||||
$request = $container->get('Request');
|
||||
```
|
||||
|
||||
Im alten Bereich:
|
||||
|
||||
```php
|
||||
$request = $this->app->Container->get('Request');
|
||||
```
|
||||
|
||||
### Request-Parameter abrufen
|
||||
|
||||
* `$request->getGet()` für den Zugriff auf `$_GET`
|
||||
* `$request->getPost()` für den Zugriff auf `$_POST`
|
||||
* `$request->getFiles()` für den Zugriff auf `$_FILES`
|
||||
* `$request->getServer()` für den Zugriff auf `$_SERVER`
|
||||
* `$request->getCookie()` für den Zugriff auf `$_COOKIE` (TODO)
|
||||
|
||||
###### Beispiele
|
||||
|
||||
* `$request->getGet('value')` wie `$_GET['value']`
|
||||
* `$request->getPost('value')` wie `$_POST['value']`
|
||||
* `$request->getServer('SERVER_NAME')` wie `$_SERVER['SERVER_NAME']`
|
||||
|
||||
#### ReadonlyParameterCollection
|
||||
|
||||
Die public Eigenschaften `get`, `post`, `files` und `server` liefern Instanzen der `ReadonlyParameterCollection`-Klasse.
|
||||
Die Klasse bietet einige Hilfsmethoden:
|
||||
|
||||
* `has()` – Gibt `true` zurück wenn der Parameter gesetzt wurde
|
||||
* `get()` – Gibt den Parameter zurück falls dieser gesetzt wurde; andernfalls `null`
|
||||
* `all()` – Gibt alle gesetzten Parameter zurück
|
||||
|
||||
|
||||
* `getBool()` – Wandelt den Wert zu Boolean
|
||||
* `getInt()` – Wandelt den Wert zu Integer
|
||||
* `getDigits()` – Wandelt den Wert zu String und entfernt alle Zeichen außer Zahlen `[0-9]`
|
||||
* `getAlpha()` – Wandelt den Wert zu String und entfernt alle Zeichen außer Buchstaben `[a-z, A-Z]`
|
||||
* `getAlphaNum()` – wie `getAlpha()` zusätzlich Zahlen `[a-z, A-Z, 0-9]`
|
||||
* `getAlphaNumWithDashes()` – wie `getAlphaDigit()` zusätzlich Minus und Unterstrich `[a-z, A-Z, 0-9, -, _]`
|
||||
|
||||
##### Default-Werte
|
||||
|
||||
Die Getter-Methoden der `ParameterCollection` nehmen als zweiten Parameter einen Default-Wert entgegen.
|
||||
Der Default-Wert wird verwendet wenn der Parameter nicht gesetzt ist.
|
||||
|
||||
###### Beispiele
|
||||
|
||||
* `$request->post->get('cmd', 'download')`
|
||||
Gibt `'download'` zurück, falls `$_POST['cmd']'` nicht gesetzt ist.
|
||||
* `$request->post->getBool('active', true)`
|
||||
Gibt `true` zurück, falls `$_POST['active']'` nicht gesetzt ist.
|
||||
|
||||
### Nützliches
|
||||
|
||||
Nachfolgende Beispielausgaben gehen von folgendem Request aus:
|
||||
|
||||
```http request
|
||||
POST /wawision-19.1/www/index.php?module=welcome&action=settings HTTP/1.1
|
||||
|
||||
Host: 192.168.0.177
|
||||
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
Accept-Language: de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3
|
||||
Accept-Encoding: gzip, deflate
|
||||
Referer: http://192.168.0.177/wawision-19.1/www/index.php?module=welcome&action=settings
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Length: 101
|
||||
Connection: keep-alive
|
||||
Cookie: PHPSESSID=19n48qro8d9blluqveg3dm1qth
|
||||
Upgrade-Insecure-Requests: 1
|
||||
|
||||
startseite=&defaultcolor=%23FFFFFF&chat_popup=1&callcenter_notification=1&submit_startseite=Speichern
|
||||
```
|
||||
|
||||
#### `$request->isSecure()`
|
||||
|
||||
Kam der Request über eine geschützte Verbindung?
|
||||
|
||||
Ausgabe: `false`
|
||||
|
||||
#### `$request->isAjax()`
|
||||
|
||||
Kam der Request über XHR?
|
||||
|
||||
Ausgabe: `false`
|
||||
|
||||
#### `$request->isCli()`
|
||||
|
||||
Kam der Request über eine Kommandozeile?
|
||||
|
||||
Ausgabe: `false`
|
||||
|
||||
#### `$request->getMethod()`
|
||||
|
||||
HTTP-Verb in Grossbuchstaben.
|
||||
|
||||
Ausgabe: `POST`
|
||||
|
||||
#### `$request->getContentType()`
|
||||
|
||||
Der hintere Teil von Content-Type Header.
|
||||
|
||||
Ausgabe: `x-www-form-urlencoded`
|
||||
|
||||
Beispiele:
|
||||
* `json` bei `application/json`
|
||||
* `html` bei `text/html`
|
||||
|
||||
#### `$request->getAcceptableContentTypes()`
|
||||
|
||||
```php
|
||||
array (
|
||||
0 => 'text/html',
|
||||
1 => 'application/xhtml+xml',
|
||||
2 => 'application/xml',
|
||||
3 => '*/*',
|
||||
)
|
||||
```
|
||||
|
||||
#### `$request->getContent()`
|
||||
|
||||
Gibt den Request-Body zurück.
|
||||
|
||||
Ausgabe: `startseite=&defaultcolor=%23FFFFFF&chat_popup=1&callcenter_notification=1&submit_startseite=Speichern`
|
||||
|
||||
#### `$request->getFullUri()`
|
||||
|
||||
Nicht mehr benutzen; stattdessen getFullUrl oder getBaseUrl verwenden.
|
||||
|
||||
`http://192.168.0.177/wawision-19.1/www/index.php?module=welcome&action=settings`
|
||||
|
||||
#### `$request->getFullUrl()`
|
||||
|
||||
Gibt die komplette Url zurück.
|
||||
|
||||
Ausgabe: `http://192.168.0.177/wawision-19.1/www/index.php?module=welcome&action=settings`
|
||||
|
||||
#### `$request->getBaseUrl()`
|
||||
|
||||
Gibt die Url ohne GET parameter zurück.
|
||||
|
||||
Ausgabe: `http://192.168.0.177/wawision-19.1/www/index.php`
|
||||
|
||||
#### `$request->getUrlForPath('/mypath')`
|
||||
|
||||
Gibt die URL um den angegebenen Pfad erweitert zurück.
|
||||
Der Pfad muss mit `/` beginnen.
|
||||
|
||||
Ausgabe: `http://192.168.0.177/wawision-19.1/www/mypath`
|
||||
|
||||
#### `$request->getBasePath()`
|
||||
|
||||
Gibt den Pfad zwischen URL und aktuellen SCRIPT_NAME an.
|
||||
|
||||
##### Beispiel1:
|
||||
|
||||
```php
|
||||
$_SERVER[
|
||||
'REQUEST_URI' => '/www/path/?value=1',
|
||||
'SCRIPT_NAME' => '/www/path/index.php',
|
||||
]
|
||||
```
|
||||
|
||||
Ausgabe: `/`
|
||||
|
||||
##### Beispiel2:
|
||||
|
||||
URL: 'http://192.168.0.177/wawision/www/api/v1/dateien/50' => '/v1/dateien/50'
|
||||
|
||||
#### `$request->getRequestUri()`
|
||||
|
||||
Gibt die relative Url ab dem Host zurück; wie `$_SERVER['REQUEST_URI']`
|
||||
|
||||
Ausgabe: `/wawision-19.1/www/index.php?module=welcome&action=settings`
|
||||
|
||||
#### `$request->isFailsafeUri()`
|
||||
|
||||
Gibt `true` zurück, wenn die Failsafe-URI im Request benutzt wurde.
|
||||
|
||||
Beispiel Failsafe-Uri: /api/index.php?path=/v1/adressen
|
||||
|
||||
#### `$request->getPathInfo()`
|
||||
|
||||
Gibt den PathInfo-Teil der Url zurück.
|
||||
|
||||
Ausgabe: `''`
|
||||
|
||||
#### `$request->getSchemeAndHttpHost()`
|
||||
|
||||
Gibt HTTP Schema und Host aus.
|
||||
|
||||
Ausgabe: `http://localhost`
|
||||
@@ -0,0 +1,173 @@
|
||||
# Http-Response
|
||||
|
||||
Die Response-Klassen dienen dazu eine gültige Response zu erstellen und an den Client zu senden.
|
||||
|
||||
Für die unterschiedliche Response-Arten gibt es mehrere Klassen um die Anwendung zu erleichtern:
|
||||
|
||||
* `Response` – Universelle Klasse
|
||||
* `FileResponse` – Zum Senden von Dateien
|
||||
* `JsonResponse` – Zum Senden von JSON-Inhalten (z.B. für AJAX-Requests)
|
||||
* `RedirectResponse` – Zum Umleiten des Clients auf eine andere URL
|
||||
|
||||
## Response Klasse
|
||||
|
||||
Die Klasse `Response` wird für alle Response-Arten benutzt, für die keine spezielle Klasse existiert.
|
||||
|
||||
#### Beispiel
|
||||
|
||||
```php
|
||||
use Xentral\Components\Http\Response;
|
||||
|
||||
$response = new Response('This is my response body.');
|
||||
$response->setContentType('text/html', 'utf-8');
|
||||
$response->addHeader('Cache-Control', 'no-cache');
|
||||
$response->send();
|
||||
```
|
||||
|
||||
### Überblick
|
||||
|
||||
1. Response erstellen
|
||||
2. Eigene Header anfügen oder überschreiben
|
||||
3. Response-Body setzen
|
||||
4. Response an Client senden
|
||||
|
||||
### Response erstellen
|
||||
|
||||
```php
|
||||
use Xentral\Components\Http\Response;
|
||||
|
||||
$response = new Response(
|
||||
'This is my response Content.',
|
||||
Response::HTTP_CREATED, //alle HTTP status Codes sind als Konstante verfügbar
|
||||
['Cache-Control' => ['no-cache']],
|
||||
'1.0',
|
||||
'Created'
|
||||
);
|
||||
```
|
||||
Erzeugt folgende Response:
|
||||
```http request
|
||||
HTTP/1.0 201 Created
|
||||
|
||||
Cache-Control: no-cache
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 28
|
||||
|
||||
This is my response Content.
|
||||
```
|
||||
|
||||
### Header hinzufügen/ändern
|
||||
|
||||
#### `addHeader`
|
||||
|
||||
Mit `addHeader('Header-Name', 'Value')` wird ein neuer Header bzw.
|
||||
ein weiterer Wert zu einem bestehenden Header hinzugefügt.
|
||||
|
||||
**Hinweis:** Bei einigen Headern wie z.B. `Content-Type`, `Content-Length`, `Content-Disposition` und `Date` kann nur
|
||||
ein Wert zugewiesen werden. Bei der Übergabe von mehreren Werten wird ein `InvalidArgumentException` geworfen.
|
||||
|
||||
#### `setHeader`
|
||||
|
||||
Mit `setHeader('Header-Name', ['Value1', 'value2])` wird ein neuer Header gesetzt und dabei ein
|
||||
bestehender Header überschrieben. Hier können mehrere Werte in einem `array` übergeben werden.
|
||||
|
||||
### Response-Body setzen
|
||||
|
||||
Mit `setContent('Mein Inhalt als String')` wird der Response-Body gesetzt.
|
||||
|
||||
**Hinweis:** `setContent` berechnet und setzt zusätzlich den `Content-Length` Header. Wird `null` als Parameter
|
||||
übergeben, so wird der `Content-Length` Header entfernt.
|
||||
|
||||
### Response an Client senden
|
||||
|
||||
Mit `send()` wird die Response abgeschickt. Vor dem Senden wird die Response noch modifiziert:
|
||||
- Falls noch nicht vorhanden wird der `Date` Header gesetzt.
|
||||
- Falls der Response-Body `null` ist, werden der `Content-Type`- und `Content-Length` Header entfernt.
|
||||
|
||||
|
||||
## RedirectResponse Klasse
|
||||
|
||||
Die RedirectResponse-Klasse vereinfacht das Umleiten auf andere Seiten.
|
||||
|
||||
#### Beispiel
|
||||
|
||||
```php
|
||||
use Xentral\Components\Http\Response;
|
||||
use Xentral\Components\Http\RedirectResponse;
|
||||
|
||||
$redirect = RedirectResponse::createFromUrl('index.php?module=auftrag&action=list');
|
||||
$redirect->setStatusCode(Response::HTTP_MOVED_PERMANENTLY);
|
||||
$redirect->send();
|
||||
```
|
||||
|
||||
Im Beispiel wird der Statuscode geändert. Per default ist `302 Found` als HTTP Status gesetzt.
|
||||
|
||||
## JsonResponse Klasse
|
||||
|
||||
Die JsonResponse Klasse vereinfacht das Erstellen von JSON-formatierten Antworten.
|
||||
|
||||
#### Beispiel
|
||||
|
||||
```php
|
||||
use Xentral\Components\Http\JsonResponse;
|
||||
|
||||
$data = [
|
||||
'data' => [
|
||||
'id' => '1234',
|
||||
'typ' => 'herr',
|
||||
'name' => 'Max Mustermann',
|
||||
]
|
||||
];
|
||||
|
||||
$response = new JsonResponse($data);
|
||||
```
|
||||
|
||||
Erzeugt folgende Response:
|
||||
|
||||
```http request
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Length: 59
|
||||
|
||||
{"data":{"id":"1234","typ":"herr","name":"Max Mustermann"}}
|
||||
```
|
||||
|
||||
Als FileResponse kann ein `array` oder ein `JsonSerializable`-Objekt übergeben werden.
|
||||
|
||||
## FileResponse Klasse
|
||||
|
||||
Die FileResponse Klasse vereinfacht das Erstellen von Datei-Downloads.
|
||||
|
||||
### `FileResponse::createFromFile()`
|
||||
|
||||
Im folgenden Beispiel enthält `/tmp/file.txt` den Text `Hallo Welt`.
|
||||
|
||||
```php
|
||||
use Xentral\Components\Http\FileResponse;
|
||||
|
||||
$fileResponse = FileResponse::createFromFile('/tmp/file.txt', 'download.txt');
|
||||
```
|
||||
|
||||
Erzeugt folgende Response:
|
||||
|
||||
```http request
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Content-Disposition: attachment; filename*="download.txt"; filename="download.txt"
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Length: 20
|
||||
|
||||
Hallo Welt
|
||||
```
|
||||
|
||||
Es wird versucht den Content-Type anhand des Mimetyps der Datei zu ermitteln. Falls die Erkennung fehlschlägt wird
|
||||
der Content-Type auf `application/octet-stream` gesetzt. Der Content-Type kann mit `$response->setContentType()`
|
||||
überschrieben werden.
|
||||
|
||||
### `FileResponse::createForcedDownload()`
|
||||
|
||||
Erzwingt den Download des Response-Contents. Das ist bei PDF's und Bildern besonders nützlich.
|
||||
Diese werden im Browser oft im Viewer geöffnet anstatt heruntergeladen zu werden.
|
||||
|
||||
`createForcedDownload` setzt den Content-Type `application/force-download`, dadurch erhält der User
|
||||
den "Speichern unter"-Dialog.
|
||||
@@ -0,0 +1,182 @@
|
||||
# Http Dateiuploads
|
||||
|
||||
In den Folgenden Beispielen wird gezeigt, wie man Http Datei Uploads realisieren und verarbeiten kann.
|
||||
|
||||
## Einfacher Dateiupload
|
||||
|
||||
### Frontend (HTML)
|
||||
|
||||
Der Dateiupload nutzt im Frontend ein HTML-Formular.
|
||||
Wichtig: Parameter `enctype="multipart/form-data` muss gesetzt sein.
|
||||
|
||||
```html
|
||||
<form action="?module=upload&action=upload" method="post" enctype="multipart/form-data">
|
||||
<input name="file" type="file">
|
||||
<input name="anotherfile" type="file">
|
||||
<button type="submit">UPLOAD</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Verarbeitung in PHP
|
||||
|
||||
Bei einem Dateiupload werden die Dateien von PHP vor der Programmausführung entgegengenommen und in `/tmp` gespeichert.
|
||||
Zusätzlich werden die Informationen über den Upload in der `$_FILES` Variable gespeichert.
|
||||
|
||||
`var_dump($_FILES)` erzeugt diesen Output:
|
||||
|
||||
```php
|
||||
array (size=2)
|
||||
'file' =>
|
||||
array (size=5)
|
||||
'name' => string 'my_uploaded_file.md' (length=19)
|
||||
'type' => string 'text/markdown' (length=13)
|
||||
'tmp_name' => string '/tmp/phphoXkRU' (length=14)
|
||||
'error' => int 0
|
||||
'size' => int 8972
|
||||
'anotherfile' =>
|
||||
array (size=5)
|
||||
'name' => string 'my_other_file.txt.xml' (length=28)
|
||||
'type' => string 'text/xml' (length=8)
|
||||
'tmp_name' => string '/tmp/php2NqM0Z' (length=14)
|
||||
'error' => int 0
|
||||
'size' => int 9499
|
||||
```
|
||||
|
||||
### Verarbeitung im Controller
|
||||
|
||||
Auf die Uploadinfo kann man pro datei über die Request Klasse zugreifen:
|
||||
|
||||
```php
|
||||
$request = $this->app->Container->get('Request');
|
||||
$fileUpload = $request->getFile('file');
|
||||
```
|
||||
|
||||
`getFile` liefert eine Instanz von `FileUpload` zurück.
|
||||
|
||||
#### Validierung
|
||||
|
||||
Bevor der FileUpload weiterverwertet wird, sollte aus Sicherheitsgründen die Integrität des Uploads überprüft werden.
|
||||
Dafür stehen mehrere Methoden zur Verfügung:
|
||||
|
||||
* `$fileUpload->isValid()` prüft, ob die Datei zu einem gültigen `POST` Upload gehört und sollte **immer** ausgeführt werden.
|
||||
* `$fileUpload->isFile()` prüft, ob die Datei im `/tmp` existiert.
|
||||
* `$fileUpload->isReadable()` prüft, ob die Datei gelesen werden kann.
|
||||
* `$fileUpload->hasError()` prüft, ob ein Http Error vorliegt.
|
||||
|
||||
**Hinweis:** Wenn ein Http Fehler vorliegt, kann man den Grund dafür per `$fileUpload->getErrorMessage()` ermitteln
|
||||
und ggf. an den Client weitergeben. Liegt allerdings kein Fehler vor, führt diese Methode zu einer Exception!
|
||||
|
||||
#### Speicherung
|
||||
|
||||
Wenn ein Upload dauerhaft gespeichert werden soll, muss die Datei aus dem `/temp` an einen dauerhaften Speicherort
|
||||
verschoben werden:
|
||||
|
||||
```php
|
||||
$fileUpload->move('/var/storage', 'upload.txt');
|
||||
```
|
||||
|
||||
**Hinweis:** Wenn sich bereits eine Datei mit diesem Namen am Zielort befindet, wird eine Exception geworfen und die
|
||||
Datei wird **nicht** überschrieben oder anderweitig gespeichert.
|
||||
|
||||
#### Zugriff
|
||||
|
||||
Man kann auf zwei Arten auf den Inhalt des Uploads zugreifen:
|
||||
|
||||
* `$fileUpload->getContent()` liefert den Inhalt als `string` zurück. (empfohlen für kleinere Dateien)
|
||||
* `$fileUpload->createContentStream()` liefert eine `resource`, aus der der Inhalt gestreamed werden kann.
|
||||
|
||||
**Tipp:** Mit `$fileUpload->getMimeType()` kann man vor dem Einlesen nocht prüfen, ob der User die Datei im
|
||||
richtigen Format (json, csv, xml usw.) hochgeladen hat.
|
||||
|
||||
## Dateiupload in Array
|
||||
|
||||
### Frontend (HTML)
|
||||
|
||||
Für den Dateiupload können Dateien auch in zusammengehörigen Arrays hochgeladen werden.
|
||||
|
||||
Beispiel: Upload einer eigenen Schriftart. Hier werden vier dateien hochgeladen, die aber semantisch zusammengehören.
|
||||
|
||||
```html
|
||||
<form action="?module=upload&action=upload" method="post" enctype="multipart/form-data">
|
||||
<input name="font[default]" type="file">
|
||||
<input name="font[bold]" type="file">
|
||||
<input name="font[italic]" type="file">
|
||||
<input name="font[bolditalic]" type="file">
|
||||
<button type="submit">UPLOAD</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Verarbeitung in PHP
|
||||
|
||||
In diesem Fall wird die `$_FILES` Variable in einer anderen hierarchie aufgebaut:
|
||||
|
||||
```php
|
||||
array (size=1)
|
||||
'font' =>
|
||||
array (size=5)
|
||||
'name' =>
|
||||
array (size=4)
|
||||
'regular' => string 'LiberationMono-Regular.ttf' (length=26)
|
||||
'bold' => string 'LiberationMono-Bold.ttf' (length=23)
|
||||
'italic' => string 'LiberationMono-Italic.ttf' (length=25)
|
||||
'bolditalic' => string 'LiberationMono-BoldItalic.ttf' (length=29)
|
||||
'type' =>
|
||||
array (size=4)
|
||||
'regular' => string 'font/ttf' (length=8)
|
||||
'bold' => string 'font/ttf' (length=8)
|
||||
'italic' => string 'font/ttf' (length=8)
|
||||
'bolditalic' => string 'font/ttf' (length=8)
|
||||
'tmp_name' =>
|
||||
array (size=4)
|
||||
'regular' => string '/tmp/phpN3NU7D' (length=14)
|
||||
'bold' => string '/tmp/phpKzBu8u' (length=14)
|
||||
'italic' => string '/tmp/php9wVd9l' (length=14)
|
||||
'bolditalic' => string '/tmp/phpRYicad' (length=14)
|
||||
'error' =>
|
||||
array (size=4)
|
||||
'regular' => int 0
|
||||
'bold' => int 0
|
||||
'italic' => int 0
|
||||
'bolditalic' => int 0
|
||||
'size' =>
|
||||
array (size=4)
|
||||
'regular' => int 108172
|
||||
'bold' => int 105460
|
||||
'italic' => int 124012
|
||||
'bolditalic' => int 118296
|
||||
```
|
||||
|
||||
### Verarbeitung im Controller
|
||||
|
||||
Auf die Uploadinfo muss jetzt anders zugegriffen werden, da `getFile` nur die oberste Ebene des Arrays ausgibt.
|
||||
|
||||
```php
|
||||
$request = $this->app->Container->get('Request');
|
||||
$fileArray = $request->getFile('font');
|
||||
$fontRegular = $fileArray['regular'];
|
||||
$fontBold = $fileArray['bold'];
|
||||
```
|
||||
|
||||
Alternativ ist auch möglich:
|
||||
|
||||
```php
|
||||
$request = $this->app->Container->get('Request');
|
||||
$fileArray = $request->files->all();
|
||||
$fontRegular = $fileArray['font']['regular'];
|
||||
$fontBold = $fileArray['font']['bold'];
|
||||
```
|
||||
|
||||
Beispiel: über alle Uploads iterieren:
|
||||
|
||||
```php
|
||||
$request = $this->app->Container->get('Request');
|
||||
foreach($request->files as $fontType => $file) {
|
||||
doSomething($fontType, $file);
|
||||
}
|
||||
```
|
||||
|
||||
Mit den einzelnen `FileUpload` Objekten verfährt man nun genau wie im oberen Beispiel.
|
||||
|
||||
**Hinweis:** Die Hierarchie der Dateiuploads kann beliebig tief geschachtelt sein. Getestet wird aber nur bis zur
|
||||
dritten Ebene.
|
||||
|
||||
Reference in New Issue
Block a user