diff --git a/composer.json b/composer.json index 536aa44..b45d101 100644 --- a/composer.json +++ b/composer.json @@ -50,11 +50,15 @@ "config": { "allow-plugins": { "phpstan/extension-installer": true - } + }, + "process-timeout": 0, + "sort-packages": true }, "scripts": { "test": "phpunit", "test-coverage": "rm clover.xml && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage --coverage-clover=clover.xml && vendor/bin/coverage-check clover.xml 100", + "test-server": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server/", + "test-server-v2": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server-v2/", "test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100", "lint": "phpstan --no-progress -cphpstan.neon", "beautify": "phpcbf --standard=phpcs.xml", diff --git a/flight/Engine.php b/flight/Engine.php index 58b0859..ffd51e4 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -27,7 +27,7 @@ * # Core methods * @method void start() Starts engine * @method void stop() Stops framework and outputs current response - * @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response. + * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response. * * # Routing * @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') @@ -66,6 +66,15 @@ */ class Engine { + /** + * @var array List of methods that can be extended in the Engine class. + */ + private const MAPPABLE_METHODS = [ + 'start', 'stop', 'route', 'halt', 'error', 'notFound', + 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', + 'post', 'put', 'patch', 'delete', 'group', 'getUrl' + ]; + /** @var array Stored variables. */ protected array $vars = []; @@ -137,14 +146,7 @@ public function init(): void $view->extension = $self->get('flight.views.extension'); }); - // Register framework methods - $methods = [ - 'start', 'stop', 'route', 'halt', 'error', 'notFound', - 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', - 'post', 'put', 'patch', 'delete', 'group', 'getUrl', - ]; - - foreach ($methods as $name) { + foreach (self::MAPPABLE_METHODS as $name) { $this->dispatcher->set($name, [$this, "_$name"]); } @@ -156,6 +158,7 @@ public function init(): void $this->set('flight.views.path', './views'); $this->set('flight.views.extension', '.php'); $this->set('flight.content_length', true); + $this->set('flight.v2.output_buffering', false); // Startup configuration $this->before('start', function () use ($self) { @@ -169,6 +172,10 @@ public function init(): void $self->router()->case_sensitive = $self->get('flight.case_sensitive'); // Set Content-Length $self->response()->content_length = $self->get('flight.content_length'); + // This is to maintain legacy handling of output buffering + // which causes a lot of problems. This will be removed + // in v4 + $self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering'); }); $this->initialized = true; @@ -354,8 +361,67 @@ public function path(string $dir): void $this->loader->addDirectory($dir); } - // Extensible Methods + /** + * Processes each routes middleware. + * + * @param array $middleware Middleware attached to the route. + * @param array $params `$route->params`. + * @param string $event_name If this is the before or after method. + */ + protected function processMiddleware(array $middleware, array $params, string $event_name): bool + { + $at_least_one_middleware_failed = false; + + foreach ($middleware as $middleware) { + $middleware_object = false; + + if ($event_name === 'before') { + // can be a callable or a class + $middleware_object = (is_callable($middleware) === true + ? $middleware + : (method_exists($middleware, 'before') === true + ? [$middleware, 'before'] + : false + ) + ); + } elseif ($event_name === 'after') { + // must be an object. No functions allowed here + if ( + is_object($middleware) === true + && !($middleware instanceof Closure) + && method_exists($middleware, 'after') === true + ) { + $middleware_object = [$middleware, 'after']; + } + } + + if ($middleware_object === false) { + continue; + } + + if ($this->response()->v2_output_buffering === false) { + ob_start(); + } + + // It's assumed if you don't declare before, that it will be assumed as the before method + $middleware_result = $middleware_object($params); + if ($this->response()->v2_output_buffering === false) { + $this->response()->write(ob_get_clean()); + } + + if ($middleware_result === false) { + $at_least_one_middleware_failed = true; + break; + } + } + + return $at_least_one_middleware_failed; + } + + //////////////////////// + // Extensible Methods // + //////////////////////// /** * Starts the framework. * @@ -374,16 +440,20 @@ public function _start(): void $self->stop(); }); - // Flush any existing output - if (ob_get_length() > 0) { - $response->write(ob_get_clean()); // @codeCoverageIgnore - } + if ($response->v2_output_buffering === true) { + // Flush any existing output + if (ob_get_length() > 0) { + $response->write(ob_get_clean()); // @codeCoverageIgnore + } - // Enable output buffering - ob_start(); + // Enable output buffering + // This is closed in the Engine->_stop() method + ob_start(); + } // Route the request $failed_middleware_check = false; + while ($route = $router->route($request)) { $params = array_values($route->params); @@ -394,60 +464,39 @@ public function _start(): void // Run any before middlewares if (count($route->middleware) > 0) { - foreach ($route->middleware as $middleware) { - $middleware_object = (is_callable($middleware) === true - ? $middleware - : (method_exists($middleware, 'before') === true - ? [$middleware, 'before'] - : false)); - - if ($middleware_object === false) { - continue; - } - - // It's assumed if you don't declare before, that it will be assumed as the before method - $middleware_result = $middleware_object($route->params); - - if ($middleware_result === false) { - $failed_middleware_check = true; - break 2; - } + $at_least_one_middleware_failed = $this->processMiddleware($route->middleware, $route->params, 'before'); + if ($at_least_one_middleware_failed === true) { + $failed_middleware_check = true; + break; } } + if ($response->v2_output_buffering === false) { + ob_start(); + } + // Call route handler $continue = $this->dispatcher->execute( $route->callback, $params ); + if ($response->v2_output_buffering === false) { + $response->write(ob_get_clean()); + } // Run any before middlewares if (count($route->middleware) > 0) { // process the middleware in reverse order now - foreach (array_reverse($route->middleware) as $middleware) { - // must be an object. No functions allowed here - $middleware_object = false; - - if ( - is_object($middleware) === true - && !($middleware instanceof Closure) - && method_exists($middleware, 'after') === true - ) { - $middleware_object = [$middleware, 'after']; - } - - // has to have the after method, otherwise just skip it - if ($middleware_object === false) { - continue; - } - - $middleware_result = $middleware_object($route->params); - - if ($middleware_result === false) { - $failed_middleware_check = true; - break 2; - } + $at_least_one_middleware_failed = $this->processMiddleware( + array_reverse($route->middleware), + $route->params, + 'after' + ); + + if ($at_least_one_middleware_failed === true) { + $failed_middleware_check = true; + break; } } @@ -463,7 +512,7 @@ public function _start(): void } if ($failed_middleware_check === true) { - $this->halt(403, 'Forbidden'); + $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); } elseif ($dispatched === false) { $this->notFound(); } @@ -514,8 +563,9 @@ public function _stop(?int $code = null): void $response->status($code); } - $content = ob_get_clean(); - $response->write($content ?: ''); + if ($response->v2_output_buffering === true && ob_get_length() > 0) { + $response->write(ob_get_clean()); + } $response->send(); } @@ -599,16 +649,16 @@ public function _delete(string $pattern, callable $callback, bool $pass_route = * * @param int $code HTTP status code * @param string $message Response message + * @param bool $actuallyExit Whether to actually exit the script or just send response */ - public function _halt(int $code = 200, string $message = ''): void + public function _halt(int $code = 200, string $message = '', bool $actuallyExit = true): void { $this->response() ->clear() ->status($code) ->write($message) ->send(); - // apologies for the crappy hack here... - if ($message !== 'skip---exit') { + if ($actuallyExit === true) { exit(); // @codeCoverageIgnore } } @@ -742,7 +792,7 @@ public function _etag(string $id, string $type = 'strong'): void isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $id ) { - $this->halt(304); + $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); } } @@ -759,7 +809,7 @@ public function _lastModified(int $time): void isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time ) { - $this->halt(304); + $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); } } diff --git a/flight/Flight.php b/flight/Flight.php index c53b979..6e29781 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -22,7 +22,7 @@ * @method static void start() Starts the framework. * @method static void path(string $path) Adds a path for autoloading classes. * @method static void stop(?int $code = null) Stops the framework and sends a response. - * @method static void halt(int $code = 200, string $message = '') + * @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true) * Stop the framework with an optional status code and message. * * # Routing diff --git a/flight/database/PdoWrapper.php b/flight/database/PdoWrapper.php index ee6d4bf..842038c 100644 --- a/flight/database/PdoWrapper.php +++ b/flight/database/PdoWrapper.php @@ -136,6 +136,6 @@ protected function processInStatementSql(string $sql, array $params = []): array $current_index += strlen($question_marks) + 4; } - return [ 'sql' => $sql, 'params' => $params ]; + return ['sql' => $sql, 'params' => $params]; } } diff --git a/flight/net/Request.php b/flight/net/Request.php index 7fffd75..eb932c0 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -331,6 +331,49 @@ public static function getHeaders(): array return $headers; } + /** + * Alias of Request->getHeader(). Gets a single header. + * + * @param string $header Header name. Can be caps, lowercase, or mixed. + * @param string $default Default value if the header does not exist + * + * @return string + */ + public static function header(string $header, $default = '') + { + return self::getHeader($header, $default); + } + + /** + * Alias of Request->getHeaders(). Gets all the request headers + * + * @return array + */ + public static function headers(): array + { + return self::getHeaders(); + } + + /** + * Gets the full request URL. + * + * @return string URL + */ + public function getFullUrl(): string + { + return $this->scheme . '://' . $this->host . $this->url; + } + + /** + * Grabs the scheme and host. Does not end with a / + * + * @return string + */ + public function getBaseUrl(): string + { + return $this->scheme . '://' . $this->host; + } + /** * Parse query parameters from a URL. * diff --git a/flight/net/Response.php b/flight/net/Response.php index 55e2605..761d1a6 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -21,6 +21,15 @@ class Response */ public bool $content_length = true; + /** + * This is to maintain legacy handling of output buffering + * which causes a lot of problems. This will be removed + * in v4 + * + * @var boolean + */ + public bool $v2_output_buffering = false; + /** * HTTP status codes * @@ -96,6 +105,7 @@ class Response 510 => 'Not Extended', 511 => 'Network Authentication Required', ]; + /** * HTTP status */ @@ -163,6 +173,34 @@ public function header($name, ?string $value = null): self return $this; } + /** + * Gets a single header from the response. + * + * @param string $name the name of the header + * + * @return string|null + */ + public function getHeader(string $name): ?string + { + $headers = $this->headers; + // lowercase all the header keys + $headers = array_change_key_case($headers, CASE_LOWER); + return $headers[strtolower($name)] ?? null; + } + + /** + * Alias of Response->header(). Adds a header to the response. + * + * @param array|string $name Header name or array of names and values + * @param ?string $value Header value + * + * @return $this + */ + public function setHeader($name, ?string $value): self + { + return $this->header($name, $value); + } + /** * Returns the headers from the response. * @@ -173,6 +211,16 @@ public function headers(): array return $this->headers; } + /** + * Alias for Response->headers(). Returns the headers from the response. + * + * @return array> + */ + public function getHeaders(): array + { + return $this->headers(); + } + /** * Writes content to the response body. * @@ -198,6 +246,11 @@ public function clear(): self $this->headers = []; $this->body = ''; + // This needs to clear the output buffer if it's on + if ($this->v2_output_buffering === false && ob_get_length() > 0) { + ob_clean(); + } + return $this; } @@ -338,8 +391,11 @@ public function sent(): bool */ public function send(): void { - if (ob_get_length() > 0) { - ob_end_clean(); // @codeCoverageIgnore + // legacy way of handling this + if ($this->v2_output_buffering === true) { + if (ob_get_length() > 0) { + ob_end_clean(); // @codeCoverageIgnore + } } if (!headers_sent()) { diff --git a/flight/util/ReturnTypeWillChange.php b/flight/util/ReturnTypeWillChange.php index df7c49f..31a929b 100644 --- a/flight/util/ReturnTypeWillChange.php +++ b/flight/util/ReturnTypeWillChange.php @@ -1,4 +1,5 @@ + + + diff --git a/tests/DocExamplesTest.php b/tests/DocExamplesTest.php index 76bbb21..1518363 100644 --- a/tests/DocExamplesTest.php +++ b/tests/DocExamplesTest.php @@ -39,6 +39,25 @@ public function testMapNotFoundMethod() echo 'hello world!'; }); + Flight::start(); + $this->expectOutputString('[]'); + $this->assertEquals(404, Flight::response()->status()); + $this->assertEquals('[]', Flight::response()->getBody()); + } + + public function testMapNotFoundMethodV2OutputBuffering() + { + Flight::map('notFound', function () { + Flight::json([], 404); + }); + + Flight::request()->url = '/not-found'; + + Flight::route('/', function () { + echo 'hello world!'; + }); + + Flight::set('flight.v2.output_buffering', true); Flight::start(); ob_get_clean(); $this->assertEquals(404, Flight::response()->status()); diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 0a20b53..1f9d456 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -30,11 +30,31 @@ public function getInitializedVar() return $this->initialized; } }; + $this->assertTrue($engine->getInitializedVar()); + + // we need to setup a dummy route + $engine->route('/someRoute', function () { }); + $engine->request()->url = '/someRoute'; + $engine->start(); + + $this->assertFalse($engine->router()->case_sensitive); + $this->assertTrue($engine->response()->content_length); + } + + public function testInitBeforeStartV2OutputBuffering() + { + $engine = new class extends Engine { + public function getInitializedVar() + { + return $this->initialized; + } + }; + $engine->set('flight.v2.output_buffering', true); $this->assertTrue($engine->getInitializedVar()); $engine->start(); - // this is necessary cause it doesn't actually send the response correctly - ob_end_clean(); + // This is a necessary evil because of how the v2 output buffer works. + ob_end_clean(); $this->assertFalse($engine->router()->case_sensitive); $this->assertTrue($engine->response()->content_length); @@ -126,6 +146,26 @@ public function getInitializedVar() $this->expectOutputString('

404 Not Found

The page you have requested could not be found.

'); $engine->start(); } + + public function testStartWithRouteButReturnedValueThrows404V2OutputBuffering() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/someRoute'; + + $engine = new class extends Engine { + public function getInitializedVar() + { + return $this->initialized; + } + }; + $engine->set('flight.v2.output_buffering', true); + $engine->route('/someRoute', function () { + echo 'i ran'; + return true; + }, true); + $this->expectOutputString('

404 Not Found

The page you have requested could not be found.

'); + $engine->start(); + } public function testStopWithCode() { @@ -144,14 +184,40 @@ public function setRealHeader(string $header_string, bool $replace = true, int $ } }; }); - // need to add another one of these because _stop() stops and gets clean, but $response->send() does too..... - ob_start(); $engine->response()->write('I am a teapot'); $this->expectOutputString('I am a teapot'); $engine->stop(500); $this->assertEquals(500, $engine->response()->status()); } + public function testStopWithCodeV2OutputBuffering() + { + $engine = new class extends Engine { + public function getLoader() + { + return $this->loader; + } + }; + // doing this so we can overwrite some parts of the response + $engine->getLoader()->register('response', function () { + return new class extends Response { + public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self + { + return $this; + } + }; + }); + $engine->set('flight.v2.output_buffering', true); + $engine->route('/testRoute', function () use ($engine) { + echo 'I am a teapot'; + $engine->stop(500); + }); + $engine->request()->url = '/testRoute'; + $engine->start(); + $this->expectOutputString('I am a teapot'); + $this->assertEquals(500, $engine->response()->status()); + } + public function testPostRoute() { $engine = new Engine(); @@ -207,10 +273,6 @@ public function getLoader() // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { - public function __construct() - { - } - public function setRealHeader( string $header_string, bool $replace = true, @@ -220,9 +282,7 @@ public function setRealHeader( } }; }); - - $this->expectOutputString('skip---exit'); - $engine->halt(500, 'skip---exit'); + $engine->halt(500, '', false); $this->assertEquals(500, $engine->response()->status()); } @@ -280,17 +340,10 @@ public function testEtagSimple() public function testEtagWithHttpIfNoneMatch() { - // just need this not to exit... - $engine = new class extends Engine { - public function _halt(int $code = 200, string $message = ''): void - { - $this->response()->status($code); - $this->response()->write($message); - } - }; + $engine = new Engine; $_SERVER['HTTP_IF_NONE_MATCH'] = 'etag'; $engine->etag('etag'); - $this->assertEquals('"etag"', $engine->response()->headers()['ETag']); + $this->assertTrue(empty($engine->response()->headers()['ETag'])); $this->assertEquals(304, $engine->response()->status()); } @@ -303,17 +356,10 @@ public function testLastModifiedSimple() public function testLastModifiedWithHttpIfModifiedSince() { - // just need this not to exit... - $engine = new class extends Engine { - public function _halt(int $code = 200, string $message = ''): void - { - $this->response()->status($code); - $this->response()->write($message); - } - }; + $engine = new Engine; $_SERVER['HTTP_IF_MODIFIED_SINCE'] = 'Fri, 13 Feb 2009 23:31:30 GMT'; $engine->lastModified(1234567890); - $this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']); + $this->assertTrue(empty($engine->response()->headers()['Last-Modified'])); $this->assertEquals(304, $engine->response()->status()); } @@ -344,11 +390,6 @@ public function testMiddlewareCallableFunction() public function testMiddlewareCallableFunctionReturnFalse() { $engine = new class extends Engine { - public function _halt(int $code = 200, string $message = ''): void - { - $this->response()->status($code); - $this->response()->write($message); - } }; $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; @@ -359,7 +400,7 @@ public function _halt(int $code = 200, string $message = ''): void }); $engine->request()->url = '/path1/123'; $engine->start(); - $this->expectOutputString('Forbiddenbefore123'); + $this->expectOutputString('Forbidden'); $this->assertEquals(403, $engine->response()->status()); } @@ -410,7 +451,6 @@ public function testMiddlewareClassAfter() $middleware = new class { public function after($params) { - echo 'after' . $params['id']; } }; @@ -435,11 +475,6 @@ public function after($params) } }; $engine = new class extends Engine { - public function _halt(int $code = 200, string $message = ''): void - { - $this->response()->status($code); - $this->response()->write($message); - } }; $engine->route('/path1/@id', function ($id) { @@ -449,7 +484,7 @@ public function _halt(int $code = 200, string $message = ''): void $engine->request()->url = '/path1/123'; $engine->start(); $this->assertEquals(403, $engine->response()->status()); - $this->expectOutputString('ForbiddenOK123after123'); + $this->expectOutputString('Forbidden'); } public function testMiddlewareCallableFunctionMultiple() diff --git a/tests/FlightTest.php b/tests/FlightTest.php index c37a8fa..c068dbe 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -242,4 +242,40 @@ public function testRouteGetUrlNestedGroups() $this->assertEquals('/user/all_users/check_user/check_one/normalpath', $url); } + + public function testHookOutputBuffering() + { + Flight::route('/test', function () { + echo 'test'; + }); + + Flight::before('start', function ($output) { + echo 'hooked before start'; + }); + + Flight::request()->url = '/test'; + + $this->expectOutputString('hooked before starttest'); + Flight::start(); + $this->assertEquals('test', Flight::response()->getBody()); + } + + public function testHookOutputBufferingV2OutputBuffering() + { + Flight::route('/test', function () { + echo 'test'; + }); + + Flight::before('start', function ($output) { + echo 'hooked before start'; + }); + + Flight::set('flight.v2.output_buffering', true); + Flight::request()->url = '/test'; + + $this->expectOutputString('hooked before starttest'); + ob_start(); + Flight::start(); + $this->assertEquals('hooked before starttest', Flight::response()->getBody()); + } } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index e3cdfb5..a8b4310 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -205,10 +205,10 @@ public function testGetHeader() // or the headers that are already in $_SERVER $this->assertEquals('XMLHttpRequest', $request->getHeader('X-REqUesTed-WiTH')); - $this->assertEquals('32.32.32.32', $request->getHeader('X-Forwarded-For')); + $this->assertEquals('32.32.32.32', $request->header('X-Forwarded-For')); // default values - $this->assertEquals('default value', $request->getHeader('X-Non-Existent-Header', 'default value')); + $this->assertEquals('default value', $request->header('X-Non-Existent-Header', 'default value')); } public function testGetHeaders() @@ -231,7 +231,7 @@ public function testGetHeadersWithEmptyHeader() $_SERVER = []; $_SERVER['HTTP_X_CUSTOM_HEADER'] = ''; $request = new Request(); - $this->assertEquals(['X-Custom-Header' => ''], $request->getHeaders()); + $this->assertEquals(['X-Custom-Header' => ''], $request->headers()); } public function testGetHeadersWithMultipleHeaders() @@ -245,4 +245,38 @@ public function testGetHeadersWithMultipleHeaders() 'X-Custom-Header2' => 'custom header value 2' ], $request->getHeaders()); } + + public function testGetFullUrlNoHttps() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/page?id=1'; + $request = new Request(); + $this->assertEquals('http://example.com/page?id=1', $request->getFullUrl()); + } + + public function testGetFullUrlWithHttps() + { + $_SERVER['HTTP_HOST'] = 'localhost:8000'; + $_SERVER['REQUEST_URI'] = '/page?id=1'; + $_SERVER['HTTPS'] = 'on'; + $request = new Request(); + $this->assertEquals('https://localhost:8000/page?id=1', $request->getFullUrl()); + } + + public function testGetBaseUrlNoHttps() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/page?id=1'; + $request = new Request(); + $this->assertEquals('http://example.com', $request->getBaseUrl()); + } + + public function testGetBaseUrlWithHttps() + { + $_SERVER['HTTP_HOST'] = 'localhost:8000'; + $_SERVER['REQUEST_URI'] = '/page?id=1'; + $_SERVER['HTTPS'] = 'on'; + $request = new Request(); + $this->assertEquals('https://localhost:8000', $request->getBaseUrl()); + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 1b9c1da..d7155fc 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -65,7 +65,7 @@ public function testHeaderSingleKeepCaseSensitive() $response = new Response(); $response->header('content-type', 'text/html'); $response->header('x-test', 'test'); - $this->assertEquals(['content-type' => 'text/html', 'x-test' => 'test'], $response->headers()); + $this->assertEquals(['content-type' => 'text/html', 'x-test' => 'test'], $response->getHeaders()); } public function testHeaderArray() @@ -81,6 +81,13 @@ public function testHeaderReturnObject() $this->assertEquals($response, $response->header('Content-Type', 'text/html')); } + public function testGetHeaderCrazyCase() + { + $response = new Response(); + $response->setHeader('CoNtEnT-tYpE', 'text/html'); + $this->assertEquals('text/html', $response->getHeader('content-type')); + } + public function testWrite() { $response = new Response(); @@ -104,6 +111,9 @@ public function testWriteReturnObject() public function testClear() { $response = new Response(); + + // Should clear this echo out + echo 'hi'; $response->write('test'); $response->status(404); $response->header('Content-Type', 'text/html'); @@ -111,6 +121,7 @@ public function testClear() $this->assertEquals('', $response->getBody()); $this->assertEquals(200, $response->status()); $this->assertEquals([], $response->headers()); + $this->assertEquals(0, ob_get_length()); } public function testCacheSimple() @@ -219,8 +230,8 @@ public function setRealHeader(string $header_string, bool $replace = true, int $ return $this; } }; - $response->header('Content-Type', 'text/html'); - $response->header('X-Test', 'test'); + $response->setHeader('Content-Type', 'text/html'); + $response->setHeader('X-Test', 'test'); $response->write('Something'); $this->expectOutputString('Something'); diff --git a/tests/server-v2/index.php b/tests/server-v2/index.php new file mode 100644 index 0000000..1ebdd13 --- /dev/null +++ b/tests/server-v2/index.php @@ -0,0 +1,179 @@ +Route text: Root route works!'; +}); +Flight::route('/querytestpath', function () { + echo 'Route text: This ir query route
'; + echo "I got such query parameters:
";
+    print_r(Flight::request()->query);
+    echo "
"; +}, false, "querytestpath"); + +// Test 2: Simple route +Flight::route('/test', function () { + echo 'Route text: Test route works!'; +}); + +// Test 3: Route with parameter +Flight::route('/user/@name', function ($name) { + echo "Route text: Hello, $name!"; +}); +Flight::route('POST /postpage', function () { + echo 'Route text: THIS IS POST METHOD PAGE'; +}, false, "postpage"); + +// Test 4: Grouped routes +Flight::group('/group', function () { + Flight::route('/test', function () { + echo 'Route text: Group test route works!'; + }); + Flight::route('/user/@name', function ($name) { + echo "Route text: There is variable called name and it is $name"; + }); + Flight::group('/group1', function () { + Flight::group('/group2', function () { + Flight::group('/group3', function () { + Flight::group('/group4', function () { + Flight::group('/group5', function () { + Flight::group('/group6', function () { + Flight::group('/group7', function () { + Flight::group('/group8', function () { + Flight::route('/final_group', function () { + echo 'Mega Group test route works!'; + }, false, "final_group"); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +// Test 5: Route alias +Flight::route('/alias', function () { + echo 'Route text: Alias route works!'; +}, false, 'aliasroute'); +class AuthCheck +{ + public function before() + { + if (!isset($_COOKIE['user'])) { + echo 'Middleware text: You are not authorized to access this route!'; + } + } +} +$middle = new AuthCheck(); +// Test 6: Route with middleware +Flight::route('/protected', function () { + echo 'Route text: Protected route works!'; +})->addMiddleware([$middle]); + +// Test 7: Route with template +Flight::route('/template/@name', function ($name) { + Flight::render('template.phtml', ['name' => $name]); +}); +Flight::set('flight.views.path', './'); +Flight::map('error', function (Throwable $error) { + echo "

An error occurred, mapped error method worked, error bellow

"; + echo '
';
+    echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString());
+    echo "
"; + echo "Go back"; +}); +Flight::map('notFound', function () { + echo 'Route text: The requested URL was not found'; + echo "Go back"; +}); +echo ' + +'; +Flight::before('start', function ($params) { + echo '
'; +}); +Flight::after('start', function ($params) { + echo '
'; + echo '
'; + echo "Request information
";
+    print_r(Flight::request());
+    echo "
"; + echo "
"; +}); +Flight::start(); diff --git a/tests/server-v2/template.phtml b/tests/server-v2/template.phtml new file mode 100644 index 0000000..d2ab1fa --- /dev/null +++ b/tests/server-v2/template.phtml @@ -0,0 +1 @@ +Route text: Template works! diff --git a/tests/server/AuthCheck.php b/tests/server/AuthCheck.php new file mode 100644 index 0000000..58526b2 --- /dev/null +++ b/tests/server/AuthCheck.php @@ -0,0 +1,18 @@ +Middleware text: You are not authorized to access this route!'; + } + } +} diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php new file mode 100644 index 0000000..bb30bda --- /dev/null +++ b/tests/server/LayoutMiddleware.php @@ -0,0 +1,93 @@ + + ul { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #333; + } + + li { + float: left; + } + #infotext { + font-weight: bold; + color: blueviolet; + } + li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; + } + + li a:hover { + background-color: #111; + } + #container { + color: #333; + font-size: 16px; + line-height: 1.5; + margin: 20px 0; + padding: 10px; + border: 1px solid #ddd; + background-color: #f9f9f9; + } + #debugrequest { + color: #333; + font-size: 16px; + line-height: 1.5; + margin: 20px 0; + padding: 10px; + border: 1px solid #ddd; + background-color: #f9f9f9; + } + + +HTML; + echo '
'; + } + + public function after() + { + echo '
'; + echo '
'; + echo "

Request Information

";
+        print_r(Flight::request());
+        echo '

Raw Request Information

'; + print_r($_SERVER); + echo "

Response Information

";
+        print_r(Flight::response());
+        echo "
"; + echo "
"; + } +} diff --git a/tests/server/index.php b/tests/server/index.php new file mode 100644 index 0000000..b3d5230 --- /dev/null +++ b/tests/server/index.php @@ -0,0 +1,116 @@ +Route text: Root route works!'; + }); + Flight::route('/querytestpath', function () { + echo 'Route text: This ir query route
'; + echo "I got such query parameters:
";
+        print_r(Flight::request()->query);
+        echo "
"; + }, false, "querytestpath"); + + // Test 2: Simple route + Flight::route('/test', function () { + echo 'Route text: Test route works!'; + }); + + // Test 3: Route with parameter + Flight::route('/user/@name', function ($name) { + echo "Route text: Hello, $name!"; + }); + Flight::route('POST /postpage', function () { + echo 'Route text: THIS IS POST METHOD PAGE'; + }, false, "postpage"); + + // Test 4: Grouped routes + Flight::group('/group', function () { + Flight::route('/test', function () { + echo 'Route text: Group test route works!'; + }); + Flight::route('/user/@name', function ($name) { + echo "Route text: There is variable called name and it is $name"; + }); + Flight::group('/group1', function () { + Flight::group('/group2', function () { + Flight::group('/group3', function () { + Flight::group('/group4', function () { + Flight::group('/group5', function () { + Flight::group('/group6', function () { + Flight::group('/group7', function () { + Flight::group('/group8', function () { + Flight::route('/final_group', function () { + echo 'Mega Group test route works!'; + }, false, "final_group"); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + // Test 5: Route alias + Flight::route('/alias', function () { + echo 'Route text: Alias route works!'; + }, false, 'aliasroute'); + + /** Middleware test */ + include_once 'AuthCheck.php'; + $middle = new AuthCheck(); + // Test 6: Route with middleware + Flight::route('/protected', function () { + echo 'Route text: Protected route works!'; + })->addMiddleware([$middle]); + + // Test 7: Route with template + Flight::route('/template/@name', function ($name) { + Flight::render('template.phtml', ['name' => $name]); + }); + + // Test 8: Throw an error + Flight::route('/error', function () { + trigger_error('This is a successful error'); + }); +}, [ new LayoutMiddleware() ]); + +Flight::map('error', function (Throwable $e) { + echo sprintf( + '

500 Internal Server Error

' . + '

%s (%s)

' . + '
%s
', + $e->getMessage(), + $e->getCode(), + str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString()) + ); + echo "
Go back"; +}); +Flight::map('notFound', function () { + echo 'Route text: The requested URL was not found
'; + echo "Go back"; +}); + +Flight::start(); diff --git a/tests/server/template.phtml b/tests/server/template.phtml new file mode 100644 index 0000000..d2ab1fa --- /dev/null +++ b/tests/server/template.phtml @@ -0,0 +1 @@ +Route text: Template works!