diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 226a40d..9a45b54 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run test server: `composer test-server` or `composer test-server-v2` - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) -- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) +- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12) - Test coverage: `composer test-coverage` ## Coding Standards -- Follow PSR1 coding standards (enforced by PHPCS) +- Follow PSR12 coding standards (enforced by PHPCS) - Use strict comparisons (`===`, `!==`) - PHPStan level 6 compliance - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 226a40d..9a45b54 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run test server: `composer test-server` or `composer test-server-v2` - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) -- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) +- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12) - Test coverage: `composer test-coverage` ## Coding Standards -- Follow PSR1 coding standards (enforced by PHPCS) +- Follow PSR12 coding standards (enforced by PHPCS) - Use strict comparisons (`===`, `!==`) - PHPStan level 6 compliance - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) diff --git a/AGENTS.md b/AGENTS.md index 226a40d..9a45b54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run test server: `composer test-server` or `composer test-server-v2` - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) -- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) +- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12) - Test coverage: `composer test-coverage` ## Coding Standards -- Follow PSR1 coding standards (enforced by PHPCS) +- Follow PSR12 coding standards (enforced by PHPCS) - Use strict comparisons (`===`, `!==`) - PHPStan level 6 compliance - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b1af6d..1004dd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,19 +9,19 @@ Flight aims to be simple and fast. Anything that compromises either of those two * **Dependencies** - We strive to be dependency free in Flight. Yes even polyfills, yes even `Interface` only repos like `psr/container`. The fewer dependencies, the fewer your exposed attack vectors. -* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are: +* **Coding Standards** - We use PSR-12 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are: * PHPStan is at level 6. - * `===` instead of truthy or falsey statements like `==` or `!is_array()`. - + * `===` instead of truthy or falsy statements like `==`. + * **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4. * **Core functionality vs Plugin** - Have a conversation with us in the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) to know if your idea is worth makes sense in the framework or in a plugin. -* **Testing** - Until automated testing is put into place, any PRs must pass unit testing in PHP 7.4 and PHP 8.2+. Additionally you need to run `composer test-server` and `composer test-server-v2` and ensure all the header links work correctly. +* **Testing** - PRs must pass unit tests on PHP 7.4 through PHP 8.5+. Additionally you need to run `composer test-server` and `composer test-server-v2` and ensure all the header links work correctly. #### **Did you find a bug?** -* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely pass in the information related to the security vuln. +* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely share details about the security vulnerability. * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues). diff --git a/README.md b/README.md index 8e8c742..62e4911 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,10 @@ composer require flightphp/core or you can download a zip of this repo. Then you would have a basic `index.php` file like the following: ```php -// if installed with composer -require 'vendor/autoload.php'; -// or if installed manually by zip file -// require 'flight/Flight.php'; +require 'flight/autoload.php'; Flight::route('/', function () { - echo 'hello world!'; + echo 'hello world!'; }); Flight::start(); @@ -38,7 +35,7 @@ Flight::start(); Yes! Flight is fast. It is one of the fastest PHP frameworks available. You can see all the benchmarks at [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=frameworks) -See the benchmark below with some other popular PHP frameworks. This is measured in requests processed within the same timeframe. +See the benchmark below with some other popular PHP frameworks. This is measured in requests processed within the same timeframe. ```mermaid --- @@ -78,9 +75,10 @@ If you have a current project on v2, you should be able to upgrade to v3 with no > [!IMPORTANT] > Flight requires `PHP 7.4` or greater. -**Note:** PHP 7.4 is supported because at the current time of writing (2024) PHP 7.4 is the default version for some LTS Linux distributions. Forcing a move to PHP >8 would cause a lot of heartburn for those users. - -The framework also supports PHP >8. +> [!NOTE] +> PHP 7.4 is supported because at the current time of writing (2024) PHP 7.4 is the default version for some LTS Linux distributions. +> Forcing a move to PHP 8 would cause a lot of heartburn for those users. +> The framework also supports PHP 8. # Roadmap diff --git a/flight/Engine.php b/flight/Engine.php index db00d5b..ac56867 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -24,8 +24,7 @@ use Psr\Container\ContainerInterface; * It is responsible for loading an HTTP request, running the assigned services, * and generating an HTTP response. * - * @license MIT, https://docs.flightphp.com/license - * @copyright Copyright (c) 2011-2025, Mike Cao , n0nag0n + * @copyright 2011-2026, Mike Cao https://mikecao.com, n0nag0n * * @method void start() * @method void stop() @@ -75,10 +74,7 @@ use Psr\Container\ContainerInterface; * @phpstan-method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * @phpstan-method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) - * - * Note: IDEs will use standard @method tags for autocompletion, while PHPStan will use @phpstan-* tags for advanced type checking. - * - * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore + * @license https://docs.flightphp.com/license MIT */ class Engine { @@ -227,8 +223,24 @@ class Engine // which causes a lot of problems. This will be removed // in v4 $self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering'); - // Propagate method override setting to Request - $self->request()::$allowMethodOverride = (bool) $self->get('flight.allow_method_override'); + + // Propagate method override setting to Request. + // Assign the static on the class first — do not call request() before this, + // or Request's constructor caches the method while the flag is still the default (true). + Request::$allowMethodOverride = (bool) $self->get('flight.allow_method_override'); + + // If a Request was already built earlier (common: apps touch request() before start) + // while override was still enabled, refresh the cached verb when override is off + // and an override input is present. When no override input exists, leave any + // intentional manual method assignment alone. + if (Request::$allowMethodOverride === false) { + $hasOverrideInput = Request::getVar('HTTP_X_HTTP_METHOD_OVERRIDE') !== '' + || isset($_REQUEST['_method']); + if ($hasOverrideInput === true) { + $request = $self->request(); + $request->method = Request::getMethod(); + } + } }); $this->initialized = true; diff --git a/flight/Flight.php b/flight/Flight.php index ddb536f..b975197 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -15,7 +15,7 @@ use Psr\Container\ContainerInterface; * The Flight class is a static representation of the framework. * * @license MIT, https://docs.flightphp.com/license - * @copyright Copyright (c) 2011-2025, Mike Cao , n0nag0n + * @copyright 2011-2026, Mike Cao https://mikecao.com, n0nag0n * * @method static void start() * @method static void path(string $dir) @@ -77,11 +77,8 @@ use Psr\Container\ContainerInterface; * @phpstan-method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * @phpstan-method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) - * - * Note: IDEs will use standard @method tags for autocompletion, - * while PHPStan will use @phpstan-* tags for advanced type checking. + * @license https://docs.flightphp.com/license MIT */ -// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace class Flight { /** diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 62ff7f9..ea25e60 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -18,8 +18,8 @@ use TypeError; * allows you to hook other functions to an event that can modify the * input parameters and/or the output. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT * @phpstan-template EngineTemplate of object */ class Dispatcher diff --git a/flight/core/EventDispatcher.php b/flight/core/EventDispatcher.php index 8e60d1a..1a63731 100644 --- a/flight/core/EventDispatcher.php +++ b/flight/core/EventDispatcher.php @@ -6,134 +6,83 @@ namespace flight\core; class EventDispatcher { - /** @var self|null Singleton instance of the EventDispatcher */ private static ?self $instance = null; - /** @var array> */ + /** @var array */ protected array $listeners = []; - /** - * Singleton instance of the EventDispatcher. - * - * @return self - */ public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } + return self::$instance; } - /** - * Register a callback for an event. - * - * @param string $event Event name - * @param callable $callback Callback function - */ public function on(string $event, callable $callback): void { - if (isset($this->listeners[$event]) === false) { - $this->listeners[$event] = []; - } + $this->listeners[$event] ??= []; $this->listeners[$event][] = $callback; } /** - * Trigger an event with optional arguments. - * - * @param string $event Event name - * @param mixed ...$args Arguments to pass to the callbacks - * + * @param mixed ...$args Arguments to pass to the listeners. * @return mixed */ public function trigger(string $event, ...$args) { - $result = null; - if (isset($this->listeners[$event]) === true) { - foreach ($this->listeners[$event] as $callback) { - $result = call_user_func_array($callback, $args); - - // If you return false, it will break the loop and stop the other event listeners. - if ($result === false) { - break; // Stop executing further listeners - } + $listenerReturnValue = null; + + foreach ($this->getListeners($event) as $listener) { + $listenerReturnValue = $listener(...$args); + + if ($listenerReturnValue === false) { + break; } } - return $result; + + return $listenerReturnValue; } - /** - * Check if an event has any registered listeners. - * - * @param string $event Event name - * - * @return bool True if the event has listeners, false otherwise - */ public function hasListeners(string $event): bool { - return isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0; + return ( + isset($this->listeners[$event]) + && is_array($this->listeners[$event]) + && count($this->listeners[$event]) + ); } - /** - * Get all listeners registered for a specific event. - * - * @param string $event Event name - * - * @return array Array of callbacks registered for the event - */ + /** @return callable[] */ public function getListeners(string $event): array { return $this->listeners[$event] ?? []; } - /** - * Get a list of all events that have registered listeners. - * - * @return array Array of event names - */ + /** @return string[] */ public function getAllRegisteredEvents(): array { return array_keys($this->listeners); } - /** - * Remove a specific listener for an event. - * - * @param string $event the event name - * @param callable $callback the exact callback to remove - * - * @return void - */ public function removeListener(string $event, callable $callback): void { - if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) { - $this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) { - return $listener !== $callback; - }); - $this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array + if (!$this->hasListeners($event)) { + return; } + + $this->listeners[$event] = array_values(array_filter( + $this->getListeners($event), + static fn(callable $listener): bool => $listener !== $callback, + )); } - /** - * Remove all listeners for a specific event. - * - * @param string $event the event name - * - * @return void - */ public function removeAllListeners(string $event): void { - if (isset($this->listeners[$event]) === true) { - unset($this->listeners[$event]); - } + unset($this->listeners[$event]); } - /** - * Remove the current singleton instance of the EventDispatcher. - * - * @return void - */ public static function resetInstance(): void { self::$instance = null; diff --git a/flight/core/Loader.php b/flight/core/Loader.php index 92deceb..a3f52cb 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -13,8 +13,8 @@ use Exception; * instances with custom initialization parameters. It also performs * class autoloading. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT */ class Loader { diff --git a/flight/net/Request.php b/flight/net/Request.php index 24f02a5..a2dcb48 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -11,8 +11,8 @@ use flight\util\Collection; * all the super globals $_GET, $_POST, $_COOKIE, and $_FILES * are stored and accessible via the Request object. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT * * The default request properties are: * diff --git a/flight/net/Response.php b/flight/net/Response.php index 1a738cf..cd0e2a1 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -12,8 +12,8 @@ use flight\core\EventDispatcher; * contains the response headers, HTTP status code, and response * body. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT */ class Response { diff --git a/flight/net/Route.php b/flight/net/Route.php index 47f93dc..d27df37 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -9,8 +9,8 @@ namespace flight\net; * an assigned callback function. The Router tries to match the * requested URL against a series of URL patterns. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT */ class Route { diff --git a/flight/net/Router.php b/flight/net/Router.php index 0ec9724..770ab9c 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -12,8 +12,8 @@ use flight\net\Route; * an assigned callback function. The Router tries to match the * requested URL against a series of URL patterns. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT */ class Router { diff --git a/flight/template/View.php b/flight/template/View.php index f995d36..fae471f 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -9,8 +9,8 @@ namespace flight\template; * methods for managing view data and inserts the data into * view templates upon rendering. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com + * @license https://docs.flightphp.com/license MIT */ class View { diff --git a/flight/util/Collection.php b/flight/util/Collection.php index e17ed37..5b2c4a3 100644 --- a/flight/util/Collection.php +++ b/flight/util/Collection.php @@ -13,10 +13,10 @@ use JsonSerializable; * The Collection class allows you to access a set of data * using both array and object notation. * - * @license MIT, http://flightphp.com/license - * @copyright Copyright (c) 2011, Mike Cao + * @copyright 2011 Mike Cao https://mikecao.com * @implements ArrayAccess * @implements Iterator + * @license https://docs.flightphp.com/license MIT */ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable { diff --git a/tests/EngineTest.php b/tests/EngineTest.php index e2d8471..7c8226e 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -23,11 +23,18 @@ class EngineTest extends TestCase public function setUp(): void { $_SERVER = []; + $_REQUEST = []; + $_GET = []; + $_POST = []; + // Static flag leaks across tests (and across Engine instances). + Request::$allowMethodOverride = true; } public function tearDown(): void { $_SERVER = []; + $_REQUEST = []; + Request::$allowMethodOverride = true; } public function testInitBeforeStart(): void @@ -1207,4 +1214,170 @@ class EngineTest extends TestCase $this->expectExceptionMessage("/path/to/nowhere cannot be found."); $engine->download('/path/to/nowhere'); } + + /** + * Regression for GHSA method-override opt-out: setting the flag to false must + * prevent X-HTTP-Method-Override from selecting a different route. + * + * The original mitigation assigned Request::$allowMethodOverride via + * $self->request()::$allowMethodOverride, which built Request (and cached the + * overridden verb) before the static was set. + */ + public function testAllowMethodOverrideFalseBlocksHeaderOverrideOnStart(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + $engine = new Engine(); + $engine->set('flight.allow_method_override', false); + + $hit = null; + $engine->route('GET /test', function () use (&$hit) { + $hit = 'get'; + echo 'get'; + }); + $engine->route('DELETE /test', function () use (&$hit) { + $hit = 'delete'; + echo 'delete'; + }); + + $this->expectOutputString('get'); + $engine->start(); + + $this->assertSame('get', $hit); + $this->assertFalse(Request::$allowMethodOverride); + $this->assertSame('GET', $engine->request()->method); + } + + public function testAllowMethodOverrideFalseBlocksPostMethodFieldOnStart(): void + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['HTTP_HOST'] = 'localhost'; + $_REQUEST['_method'] = 'PUT'; + + $engine = new Engine(); + $engine->set('flight.allow_method_override', false); + + $hit = null; + $engine->route('POST /test', function () use (&$hit) { + $hit = 'post'; + echo 'post'; + }); + $engine->route('PUT /test', function () use (&$hit) { + $hit = 'put'; + echo 'put'; + }); + + $this->expectOutputString('post'); + $engine->start(); + + $this->assertSame('post', $hit); + $this->assertSame('POST', $engine->request()->method); + } + + public function testAllowMethodOverrideTrueStillHonorsHeaderOnStart(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + $engine = new Engine(); + // default is true; set explicitly for clarity + $engine->set('flight.allow_method_override', true); + + $hit = null; + $engine->route('GET /test', function () use (&$hit) { + $hit = 'get'; + echo 'get'; + }); + $engine->route('DELETE /test', function () use (&$hit) { + $hit = 'delete'; + echo 'delete'; + }); + + $this->expectOutputString('delete'); + $engine->start(); + + $this->assertSame('delete', $hit); + $this->assertTrue(Request::$allowMethodOverride); + $this->assertSame('DELETE', $engine->request()->method); + } + + /** + * App code often touches request() before start() (set url, inspect headers, etc.). + * The flag must still win even when Request was constructed early under the default. + */ + public function testAllowMethodOverrideFalseRefreshesMethodIfRequestBuiltEarly(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + $engine = new Engine(); + $engine->set('flight.allow_method_override', false); + + // Construct Request while static is still the default (true) + $this->assertTrue(Request::$allowMethodOverride); + $early = $engine->request(); + $this->assertSame('DELETE', $early->method, 'pre-start construction still sees default override=on'); + + $hit = null; + $engine->route('GET /test', function () use (&$hit) { + $hit = 'get'; + echo 'get'; + }); + $engine->route('DELETE /test', function () use (&$hit) { + $hit = 'delete'; + echo 'delete'; + }); + + $this->expectOutputString('get'); + $engine->start(); + + $this->assertSame('get', $hit); + $this->assertFalse(Request::$allowMethodOverride); + $this->assertSame('GET', $engine->request()->method); + $this->assertSame($early, $engine->request(), 'same Request instance is refreshed, not replaced'); + } + + public function testAllowMethodOverrideFalseDoesNotClobberManualMethodWithoutOverrideInput(): void + { + // No X-HTTP-Method-Override / _method — manual method assignment must survive start(). + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/someRoute'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + $engine = new Engine(); + $engine->set('flight.allow_method_override', false); + $engine->route('GET /someRoute', function () { + echo 'i ran'; + }, true); + $engine->request()->method = 'HEAD'; + $engine->request()->url = '/someRoute'; + + $this->expectOutputString(''); + $engine->start(); + + $this->assertSame('HEAD', $engine->request()->method); + } } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index acc6e18..7954228 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -31,11 +31,15 @@ class RequestTest extends TestCase $_COOKIE = []; $_FILES = []; + // Static flag leaks across tests; always restore the framework default. + Request::$allowMethodOverride = true; + $this->request = new Request(); } protected function tearDown(): void { + Request::$allowMethodOverride = true; unset($_REQUEST); unset($_SERVER); } @@ -127,6 +131,49 @@ class RequestTest extends TestCase $this->assertEquals('PUT', $request->method); } + public function testMethodOverrideDisabledIgnoresHeader(): void + { + Request::$allowMethodOverride = false; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; + + $request = new Request(); + + $this->assertSame('GET', $request->method); + $this->assertSame('GET', Request::getMethod()); + } + + public function testMethodOverrideDisabledIgnoresPostField(): void + { + Request::$allowMethodOverride = false; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_REQUEST['_method'] = 'PUT'; + + $request = new Request(); + + $this->assertSame('POST', $request->method); + $this->assertSame('POST', Request::getMethod()); + } + + public function testMethodOverrideFlagMustBeSetBeforeConstruction(): void + { + // Documents the caching behavior: flipping the static after construct + // does not rewrite the already-cached $request->method. + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; + + Request::$allowMethodOverride = true; + $request = new Request(); + $this->assertSame('DELETE', $request->method); + + Request::$allowMethodOverride = false; + $this->assertSame('DELETE', $request->method, 'cached method is not auto-refreshed'); + $this->assertSame('GET', Request::getMethod(), 'getMethod() respects the new flag'); + + $request->method = Request::getMethod(); + $this->assertSame('GET', $request->method); + } + public function testHttps(): void { $_SERVER['HTTPS'] = 'on';