Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
+35
View File
@@ -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);
}
}
+321
View File
@@ -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
{
}
+47
View File
@@ -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;
}
}
+267
View File
@@ -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);
}
}
+153
View File
@@ -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);
}
}
+32
View File
@@ -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);
}
}
+543
View File
@@ -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&param2=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;
}
}
+557
View File
@@ -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;
}
}
+130
View File
@@ -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.');
}
}
}
+315
View File
@@ -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();
}
}
+223
View File
@@ -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`
+173
View File
@@ -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.
+182
View File
@@ -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.