diff options
author | msquare <msquare@notrademark.de> | 2018-09-04 18:24:11 +0200 |
---|---|---|
committer | msquare <msquare@notrademark.de> | 2018-09-04 18:24:11 +0200 |
commit | b320fc779063ee80b8f0ba505cb323287ccccbf5 (patch) | |
tree | 1e420597ae72c979361bf29b66ae7e27c73cf431 /src | |
parent | 9f1ee0c6c6497d43fb275491ec53fda420f64b81 (diff) | |
parent | 36dafdb68acbde2fe42ce36ef50f497c8c06411f (diff) |
Merge branch 'MyIgel-rebuild-psr7'
Diffstat (limited to 'src')
-rw-r--r-- | src/Application.php | 14 | ||||
-rw-r--r-- | src/Exceptions/Handler.php | 17 | ||||
-rw-r--r-- | src/Exceptions/Handlers/Whoops.php | 2 | ||||
-rw-r--r-- | src/Http/MessageTrait.php | 248 | ||||
-rw-r--r-- | src/Http/Psr7ServiceProvider.php | 29 | ||||
-rw-r--r-- | src/Http/Request.php | 447 | ||||
-rw-r--r-- | src/Http/Response.php | 75 | ||||
-rw-r--r-- | src/Http/ResponseServiceProvider.php | 14 | ||||
-rw-r--r-- | src/Middleware/Dispatcher.php | 110 | ||||
-rw-r--r-- | src/Middleware/ExceptionHandler.php | 48 | ||||
-rw-r--r-- | src/Middleware/LegacyMiddleware.php | 296 | ||||
-rw-r--r-- | src/Middleware/NotFoundResponse.php | 56 | ||||
-rw-r--r-- | src/Middleware/SendResponseHandler.php | 69 | ||||
-rw-r--r-- | src/helpers.php | 24 |
14 files changed, 1446 insertions, 3 deletions
diff --git a/src/Application.php b/src/Application.php index 68ce9e33..6644a6cf 100644 --- a/src/Application.php +++ b/src/Application.php @@ -7,6 +7,7 @@ use Engelsystem\Container\Container; use Engelsystem\Container\ServiceProvider; use Illuminate\Container\Container as IlluminateContainer; use Psr\Container\ContainerInterface; +use Psr\Http\Server\MiddlewareInterface; class Application extends Container { @@ -16,6 +17,9 @@ class Application extends Container /** @var bool */ protected $isBootstrapped = false; + /** @var MiddlewareInterface[]|string[] */ + protected $middleware; + /** * Registered service providers * @@ -85,6 +89,8 @@ class Application extends Container foreach ($config->get('providers', []) as $provider) { $this->register($provider); } + + $this->middleware = $config->get('middleware', []); } foreach ($this->serviceProviders as $provider) { @@ -136,4 +142,12 @@ class Application extends Container { return $this->isBootstrapped; } + + /** + * @return MiddlewareInterface[]|string[] + */ + public function getMiddleware() + { + return $this->middleware; + } } diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index 6b3300e6..b3d840c0 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -54,8 +54,10 @@ class Handler /** * @param Throwable $e + * @param bool $return + * @return string */ - public function exceptionHandler($e) + public function exceptionHandler($e, $return = false) { if (!$this->request instanceof Request) { $this->request = new Request(); @@ -63,8 +65,21 @@ class Handler $handler = $this->handler[$this->environment]; $handler->report($e); + ob_start(); $handler->render($this->request, $e); + + if ($return) { + $output = ob_get_contents(); + ob_end_clean(); + return $output; + } + + http_response_code(500); + ob_end_flush(); + $this->terminateApplicationImmediately(); + + return ''; } /** diff --git a/src/Exceptions/Handlers/Whoops.php b/src/Exceptions/Handlers/Whoops.php index 73352105..630aca1d 100644 --- a/src/Exceptions/Handlers/Whoops.php +++ b/src/Exceptions/Handlers/Whoops.php @@ -34,6 +34,8 @@ class Whoops extends Legacy implements HandlerInterface $whoops = $this->app->make(WhoopsRunner::class); $handler = $this->getPrettyPageHandler($e); $whoops->pushHandler($handler); + $whoops->writeToOutput(false); + $whoops->allowQuit(false); if ($request->isXmlHttpRequest()) { $handler = $this->getJsonResponseHandler(); diff --git a/src/Http/MessageTrait.php b/src/Http/MessageTrait.php new file mode 100644 index 00000000..e46d291e --- /dev/null +++ b/src/Http/MessageTrait.php @@ -0,0 +1,248 @@ +<?php + +namespace Engelsystem\Http; + + +use Psr\Http\Message\StreamInterface; +use Zend\Diactoros\Stream; + +/** + * @implements \Psr\Http\Message\MessageInterface + * @extends \Symfony\Component\HttpFoundation\Response + */ +trait MessageTrait +{ + /** + * Retrieves the HTTP protocol version as a string. + * + * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). + * + * @return string HTTP protocol version. + */ + public function getProtocolVersion() + { + return parent::getProtocolVersion(); + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * @return static + */ + public function withProtocolVersion($version) + { + $new = clone $this; + if (method_exists($new, 'setProtocolVersion')) { + $new->setProtocolVersion($version); + } else { + $new->server->set('SERVER_PROTOCOL', $version); + } + + return $new; + } + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders() + { + if (method_exists($this->headers, 'allPreserveCase')) { + return $this->headers->allPreserveCase(); + } + + return $this->headers->all(); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + return $this->headers->has($name); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name) + { + return $this->headers->get($name, null, false); + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name) + { + return implode(',', $this->getHeader($name)); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value) + { + $new = clone $this; + $new->headers->set($name, $value); + + return $new; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value) + { + $new = clone $this; + $new->headers->set($name, $value, false); + + return $new; + } + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name) + { + $new = clone $this; + $new->headers->remove($name); + + return $new; + } + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody() + { + $stream = new Stream('php://memory', 'wb+'); + $stream->write($this->getContent()); + $stream->rewind(); + + return $stream; + } + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body) + { + $new = clone $this; + + if (method_exists($new, 'setContent')) { + $new->setContent($body); + } else { + $new->content = $body; + } + + return $new; + } +} diff --git a/src/Http/Psr7ServiceProvider.php b/src/Http/Psr7ServiceProvider.php new file mode 100644 index 00000000..72fdef8e --- /dev/null +++ b/src/Http/Psr7ServiceProvider.php @@ -0,0 +1,29 @@ +<?php + +namespace Engelsystem\Http; + +use Engelsystem\Container\ServiceProvider; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; + + +class Psr7ServiceProvider extends ServiceProvider +{ + public function register() + { + /** @var DiactorosFactory $psr7Factory */ + $psr7Factory = $this->app->make(DiactorosFactory::class); + $this->app->instance('psr7.factory', $psr7Factory); + + /** @var Request $request */ + $request = $this->app->get('request'); + $this->app->instance('psr7.request', $request); + $this->app->bind(ServerRequestInterface::class, 'psr7.request'); + + /** @var Response $response */ + $response = $this->app->get('response'); + $this->app->instance('psr7.response', $response); + $this->app->bind(ResponseInterface::class, 'psr7.response'); + } +} diff --git a/src/Http/Request.php b/src/Http/Request.php index c6a9e5ad..4729606f 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,10 +2,18 @@ namespace Engelsystem\Http; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UploadedFileInterface; +use Psr\Http\Message\UriInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyFile; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Zend\Diactoros\UploadedFile; +use Zend\Diactoros\Uri; -class Request extends SymfonyRequest +class Request extends SymfonyRequest implements ServerRequestInterface { + use MessageTrait; + /** * Get POST input * @@ -64,4 +72,441 @@ class Request extends SymfonyRequest { return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/'); } + + /** + * Retrieves the message's request target. + * + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + $query = $this->getQueryString(); + return '/' . $this->path() . (!empty($query) ? '?' . $query : ''); + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various + * request-target forms allowed in request messages) + * @param mixed $requestTarget + * @return static + */ + public function withRequestTarget($requestTarget) + { + return $this->create($requestTarget); + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * @return static + * @throws \InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method) + { + $new = clone $this; + $new->setMethod($method); + + return $new; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $new = $this->create($uri); + if ($preserveHost) { + $new->headers->set('HOST', $this->getHost()); + } + + return $new; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @return string|UriInterface Returns a UriInterface instance + * representing the URI of the request. + */ + public function getUri() + { + $uri = parent::getUri(); + + return new Uri($uri); + } + + /** + * Retrieve server parameters. + * + * Retrieves data related to the incoming request environment, + * typically derived from PHP's $_SERVER superglobal. The data IS NOT + * REQUIRED to originate from $_SERVER. + * + * @return array + */ + public function getServerParams() + { + return $this->server->all(); + } + + /** + * Retrieve cookies. + * + * Retrieves cookies sent by the client to the server. + * + * The data MUST be compatible with the structure of the $_COOKIE + * superglobal. + * + * @return array + */ + public function getCookieParams() + { + return $this->cookies->all(); + } + + /** + * Return an instance with the specified cookies. + * + * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST + * be compatible with the structure of $_COOKIE. Typically, this data will + * be injected at instantiation. + * + * This method MUST NOT update the related Cookie header of the request + * instance, nor related values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated cookie values. + * + * @param array $cookies Array of key/value pairs representing cookies. + * @return static + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = clone $this->cookies; + $new->cookies->replace($cookies); + + return $new; + } + + /** + * Retrieve query string arguments. + * + * Retrieves the deserialized query string arguments, if any. + * + * Note: the query params might not be in sync with the URI or server + * params. If you need to ensure you are only getting the original + * values, you may need to parse the query string from `getUri()->getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams() + { + return $this->query->all(); + } + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->query = clone $this->query; + $new->query->replace($query); + + return $new; + } + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles() + { + $files = []; + foreach ($this->files as $file) { + /** @var SymfonyFile $file */ + + $files[] = new UploadedFile( + $file->getPath(), + $file->getSize(), + $file->getError(), + $file->getClientOriginalName(), + $file->getClientMimeType() + ); + } + + return $files; + } + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->files = clone $this->files; + + $files = []; + foreach ($uploadedFiles as $file) { + /** @var UploadedFileInterface $file */ + $filename = tempnam(sys_get_temp_dir(), 'upload'); + $handle = fopen($filename, "w"); + fwrite($handle, $file->getStream()->getContents()); + fclose($handle); + + $files[] = new SymfonyFile( + $filename, + $file->getClientFilename(), + $file->getClientMediaType(), + $file->getSize(), + $file->getError() + ); + } + $new->files->add($files); + + return $new; + } + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody() + { + return $this->request->all(); + } + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->request = clone $this->request; + + $new->request->replace($data); + + return $new; + } + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes() + { + return $this->attributes->all(); + } + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null) + { + return $this->attributes->get($name, $default); + } + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes = clone $this->attributes; + + $new->attributes->set($name, $value); + + return $new; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name) + { + $new = clone $this; + $new->attributes = clone $this->attributes; + + $new->attributes->remove($name); + + return $new; + } } diff --git a/src/Http/Response.php b/src/Http/Response.php new file mode 100644 index 00000000..9db6fa83 --- /dev/null +++ b/src/Http/Response.php @@ -0,0 +1,75 @@ +<?php + +namespace Engelsystem\Http; + +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; + +class Response extends SymfonyResponse implements ResponseInterface +{ + use MessageTrait; + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return static + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->setStatusCode($code, !empty($reasonPhrase) ? $reasonPhrase : null); + + return $new; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + return $this->statusText; + } + + /** + * Return an instance with the specified content. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @param mixed $content Content that can be cast to string + * @return static + */ + public function withContent($content) + { + $new = clone $this; + $new->setContent($content); + + return $new; + } +} diff --git a/src/Http/ResponseServiceProvider.php b/src/Http/ResponseServiceProvider.php new file mode 100644 index 00000000..f0d238ef --- /dev/null +++ b/src/Http/ResponseServiceProvider.php @@ -0,0 +1,14 @@ +<?php + +namespace Engelsystem\Http; + +use Engelsystem\Container\ServiceProvider; + +class ResponseServiceProvider extends ServiceProvider +{ + public function register() + { + $response = $this->app->make(Response::class); + $this->app->instance('response', $response); + } +} diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php new file mode 100644 index 00000000..f2a5b5d5 --- /dev/null +++ b/src/Middleware/Dispatcher.php @@ -0,0 +1,110 @@ +<?php + +namespace Engelsystem\Middleware; + +use Engelsystem\Application; +use InvalidArgumentException; +use LogicException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class Dispatcher implements MiddlewareInterface, RequestHandlerInterface +{ + /** @var MiddlewareInterface[]|string[] */ + protected $stack; + + /** @var Application */ + protected $container; + + /** @var RequestHandlerInterface */ + protected $next; + + /** + * @param MiddlewareInterface[]|string[] $stack + * @param Application|null $container + */ + public function __construct($stack = [], Application $container = null) + { + $this->stack = $stack; + $this->container = $container; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * Could be used to group middleware + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $this->next = $handler; + + return $this->handle($request); + } + + /** + * Handle the request and return a response. + * + * It calls all configured middleware and handles their response + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $middleware = array_shift($this->stack); + + if (!$middleware) { + if ($this->next) { + return $this->next->handle($request); + } + + throw new LogicException('Middleware queue is empty'); + } + + if (is_string($middleware)) { + $middleware = $this->resolveMiddleware($middleware); + } + + if (!$middleware instanceof MiddlewareInterface) { + throw new InvalidArgumentException('Middleware is no instance of ' . MiddlewareInterface::class); + } + + return $middleware->process($request, $this); + } + + /** + * Resolve the middleware with the container + * + * @param string $middleware + * @return MiddlewareInterface + */ + protected function resolveMiddleware($middleware) + { + if (!$this->container instanceof Application) { + throw new InvalidArgumentException('Unable to resolve middleware ' . $middleware); + } + + if ($this->container->has($middleware)) { + return $this->container->get($middleware); + } + + return $this->container->make($middleware); + } + + /** + * @param Application $container + */ + public function setContainer(Application $container) + { + $this->container = $container; + } +} diff --git a/src/Middleware/ExceptionHandler.php b/src/Middleware/ExceptionHandler.php new file mode 100644 index 00000000..a5db0337 --- /dev/null +++ b/src/Middleware/ExceptionHandler.php @@ -0,0 +1,48 @@ +<?php + +namespace Engelsystem\Middleware; + +use Engelsystem\Exceptions\Handler as ExceptionsHandler; +use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class ExceptionHandler implements MiddlewareInterface +{ + /** @var ContainerInterface */ + protected $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Handles any exceptions that occurred inside other middleware while returning it to the default response handler + * + * Should be added at the beginning + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + try { + return $handler->handle($request); + } catch (\Throwable $e) { + /** @var ExceptionsHandler $handler */ + $handler = $this->container->get('error.handler'); + $content = $handler->exceptionHandler($e, true); + + return response($content, 500); + } + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php new file mode 100644 index 00000000..714141de --- /dev/null +++ b/src/Middleware/LegacyMiddleware.php @@ -0,0 +1,296 @@ +<?php + +namespace Engelsystem\Middleware; + +use Engelsystem\Http\Request; +use Engelsystem\Http\Response; +use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class LegacyMiddleware implements MiddlewareInterface +{ + protected $free_pages = [ + 'admin_event_config', + 'angeltypes', + 'api', + 'atom', + 'credits', + 'ical', + 'login', + 'public_dashboard', + 'rooms', + 'shift_entries', + 'shifts', + 'shifts_json_export', + 'shifts_json_export_all', + 'stats', + 'users', + 'user_driver_licenses', + 'user_password_recovery', + 'user_worklog' + ]; + + /** @var ContainerInterface */ + protected $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Handle the request the old way + * + * Should be used before a 404 is send + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + global $user; + global $privileges; + + /** @var Request $appRequest */ + $appRequest = $this->container->get('request'); + $page = $appRequest->query->get('p'); + if (empty($page)) { + $page = $appRequest->path(); + $page = str_replace('-', '_', $page); + } + if ($page == '/') { + $page = isset($user) ? 'news' : 'login'; + } + + $title = $content = ''; + if ( + preg_match('~^\w+$~i', $page) + && ( + in_array($page, $this->free_pages) + || (isset($privileges) && in_array($page, $privileges)) + ) + ) { + list($title, $content) = $this->loadPage($page); + } + + if (empty($title) and empty($content)) { + return $handler->handle($request); + } + + return $this->renderPage($page, $title, $content); + } + + /** + * Get the legacy page content and title + * + * @param string $page + * @return array ['title', 'content'] + * @codeCoverageIgnore + */ + protected function loadPage($page) + { + $title = ucfirst($page); + switch ($page) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 'api': + error('Api disabled temporarily.'); + redirect(page_link_to()); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'ical': + require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php'); + user_ical(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'atom': + require_once realpath(__DIR__ . '/../../includes/pages/user_atom.php'); + user_atom(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'shifts_json_export': + require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); + shifts_json_export_controller(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'shifts_json_export_all': + require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); + shifts_json_export_all_controller(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'stats': + require_once realpath(__DIR__ . '/../../includes/pages/guest_stats.php'); + guest_stats(); + case 'user_password_recovery': + require_once realpath(__DIR__ . '/../../includes/controller/users_controller.php'); + $title = user_password_recovery_title(); + $content = user_password_recovery_controller(); + return [$title, $content]; + case 'public_dashboard': + return public_dashboard_controller(); + case 'angeltypes': + return angeltypes_controller(); + case 'shift_entries': + return shift_entries_controller(); + case 'shifts': + return shifts_controller(); + case 'users': + return users_controller(); + case 'user_angeltypes': + return user_angeltypes_controller(); + case 'user_driver_licenses': + return user_driver_licenses_controller(); + case 'shifttypes': + list($title, $content) = shifttypes_controller(); + return [$title, $content]; + case 'admin_event_config': + list($title, $content) = event_config_edit_controller(); + return [$title, $content]; + case 'rooms': + return rooms_controller(); + case 'news': + $title = news_title(); + $content = user_news(); + return [$title, $content]; + case 'news_comments': + require_once realpath(__DIR__ . '/../../includes/pages/user_news.php'); + $title = user_news_comments_title(); + $content = user_news_comments(); + return [$title, $content]; + case 'user_meetings': + $title = meetings_title(); + $content = user_meetings(); + return [$title, $content]; + case 'user_myshifts': + $title = myshifts_title(); + $content = user_myshifts(); + return [$title, $content]; + case 'user_shifts': + $title = shifts_title(); + $content = user_shifts(); + return [$title, $content]; + case 'user_worklog': + return user_worklog_controller(); + case 'user_messages': + $title = messages_title(); + $content = user_messages(); + return [$title, $content]; + case 'user_questions': + $title = questions_title(); + $content = user_questions(); + return [$title, $content]; + case 'user_settings': + $title = settings_title(); + $content = user_settings(); + return [$title, $content]; + case 'login': + $title = login_title(); + $content = guest_login(); + return [$title, $content]; + case 'register': + $title = register_title(); + $content = guest_register(); + return [$title, $content]; + case 'logout': + $title = logout_title(); + $content = guest_logout(); + return [$title, $content]; + case 'admin_questions': + $title = admin_questions_title(); + $content = admin_questions(); + return [$title, $content]; + case 'admin_user': + $title = admin_user_title(); + $content = admin_user(); + return [$title, $content]; + case 'admin_arrive': + $title = admin_arrive_title(); + $content = admin_arrive(); + return [$title, $content]; + case 'admin_active': + $title = admin_active_title(); + $content = admin_active(); + return [$title, $content]; + case 'admin_free': + $title = admin_free_title(); + $content = admin_free(); + return [$title, $content]; + case 'admin_news': + require_once realpath(__DIR__ . '/../../includes/pages/admin_news.php'); + $content = admin_news(); + return [$title, $content]; + case 'admin_rooms': + $title = admin_rooms_title(); + $content = admin_rooms(); + return [$title, $content]; + case 'admin_groups': + $title = admin_groups_title(); + $content = admin_groups(); + return [$title, $content]; + case 'admin_import': + $title = admin_import_title(); + $content = admin_import(); + return [$title, $content]; + case 'admin_shifts': + $title = admin_shifts_title(); + $content = admin_shifts(); + return [$title, $content]; + case 'admin_log': + $title = admin_log_title(); + $content = admin_log(); + return [$title, $content]; + case 'credits': + require_once realpath(__DIR__ . '/../../includes/pages/guest_credits.php'); + $title = credits_title(); + $content = guest_credits(); + return [$title, $content]; + } + + require_once realpath(__DIR__ . '/../../includes/pages/guest_start.php'); + $content = guest_start(); + return [$title, $content]; + } + + /** + * Render the template + * + * @param string $page + * @param string $title + * @param string $content + * @return Response + * @codeCoverageIgnore + */ + protected function renderPage($page, $title, $content) + { + global $user; + $event_config = EventConfig(); + $parameters = [ + 'key' => (isset($user) ? $user['api_key'] : ''), + ]; + if ($page == 'user_meetings') { + $parameters['meetings'] = 1; + } + + return response(view(__DIR__ . '/../../templates/layout.html', [ + 'theme' => isset($user) ? $user['color'] : config('theme'), + 'title' => $title, + 'atom_link' => ($page == 'news' || $page == 'user_meetings') + ? ' <link href="' + . page_link_to('atom', $parameters) + . '" type = "application/atom+xml" rel = "alternate" title = "Atom Feed">' + : '', + 'start_page_url' => page_link_to('/'), + 'credits_url' => page_link_to('credits'), + 'menu' => make_menu(), + 'content' => msg() . $content, + 'header_toolbar' => header_toolbar(), + 'faq_url' => config('faq_url'), + 'contact_email' => config('contact_email'), + 'locale' => locale(), + 'event_info' => EventConfig_info($event_config) . ' <br />' + ])); + } +} diff --git a/src/Middleware/NotFoundResponse.php b/src/Middleware/NotFoundResponse.php new file mode 100644 index 00000000..f9431c1d --- /dev/null +++ b/src/Middleware/NotFoundResponse.php @@ -0,0 +1,56 @@ +<?php + +namespace Engelsystem\Middleware; + +use Engelsystem\Http\Response; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class NotFoundResponse implements MiddlewareInterface +{ + /** + * Returns a 404: Page not found response + * + * Should be the last middleware + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $info = _('This page could not be found or you don\'t have permission to view it. You probably have to sign in or register in order to gain access!'); + + return $this->renderPage($info); + } + + /** + * @param string $content + * @return Response + * @codeCoverageIgnore + */ + protected function renderPage($content) + { + global $user; + $event_config = EventConfig(); + + return response(view(__DIR__ . '/../../templates/layout.html', [ + 'theme' => isset($user) ? $user['color'] : config('theme'), + 'title' => _('Page not found'), + 'atom_link' => '', + 'start_page_url' => page_link_to('/'), + 'credits_url' => page_link_to('credits'), + 'menu' => make_menu(), + 'content' => msg() . info($content), + 'header_toolbar' => header_toolbar(), + 'faq_url' => config('faq_url'), + 'contact_email' => config('contact_email'), + 'locale' => locale(), + 'event_info' => EventConfig_info($event_config) . ' <br />' + ]), 404); + } +} diff --git a/src/Middleware/SendResponseHandler.php b/src/Middleware/SendResponseHandler.php new file mode 100644 index 00000000..34e70a87 --- /dev/null +++ b/src/Middleware/SendResponseHandler.php @@ -0,0 +1,69 @@ +<?php + +namespace Engelsystem\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class SendResponseHandler implements MiddlewareInterface +{ + /** + * Send the server response to the client + * + * This should be the first middleware + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $response = $handler->handle($request); + + if (!$this->headersSent()) { + $this->sendHeader(sprintf( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), true, $response->getStatusCode()); + + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $this->sendHeader($name . ': ' . $value, false); + } + } + } + + echo $response->getBody(); + return $response; + } + + /** + * Checks if headers have been sent + * + * @return bool + * @codeCoverageIgnore + */ + protected function headersSent() + { + return headers_sent(); + } + + /** + * Send a raw HTTP header + * + * @param string $content + * @param bool $replace + * @param int $code + * @codeCoverageIgnore + */ + protected function sendHeader($content, $replace = true, $code = null) + { + header($content, $replace, $code); + } +} diff --git a/src/helpers.php b/src/helpers.php index 339936e3..50c5c837 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,6 +4,7 @@ use Engelsystem\Application; use Engelsystem\Config\Config; use Engelsystem\Http\Request; +use Engelsystem\Http\Response; use Engelsystem\Renderer\Renderer; use Engelsystem\Routing\UrlGeneratorInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; * Get the global app instance * * @param string $id - * @return mixed + * @return mixed|Application */ function app($instance_id = null) { @@ -81,6 +82,27 @@ function request($key = null, $default = null) } /** + * @param string $content + * @param int $status + * @param array $headers + * @return Response + */ +function response($content = '', $status = 200, $headers = []) +{ + /** @var Response $response */ + $response = app('psr7.response'); + $response = $response + ->withContent($content) + ->withStatus($status); + + foreach ($headers as $key => $value) { + $response = $response->withAddedHeader($key, $value); + } + + return $response; +} + +/** * @param string $key * @param mixed $default * @return SessionInterface|mixed |