Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
62ccc84
Introduce `ErrorHandler` and `StatelessApplication` implementation wi…
terabytesoftw Jul 23, 2025
84baf19
Apply fixes from StyleCI
StyleCIBot Jul 23, 2025
1ad1ab9
fix(composer): add missing `psr/http-server-handler` dependency.
terabytesoftw Jul 23, 2025
50e49ad
Merge branch 'main' into feature-5
terabytesoftw Jul 25, 2025
38ee5ec
Add `composer-require-checker.json` and update version in `StatelessA…
terabytesoftw Jul 25, 2025
e031014
fix(ErrorHandler): move server variable logging inside debug condition.
terabytesoftw Jul 25, 2025
bab0cb5
fix(StatelessApplication): simplify return statement in clean method.
terabytesoftw Jul 25, 2025
78322c8
fix(composer.json): add ext-uopz to require-dev dependencies
terabytesoftw Jul 25, 2025
c157169
fix(workflows): add uopz extension to various workflow configurations.
terabytesoftw Jul 25, 2025
7036222
fix(build): specify uopz extension version in workflow configuration.
terabytesoftw Jul 25, 2025
f75a4d7
fix(workflows): remove uopz extension from various workflow configura…
terabytesoftw Jul 25, 2025
3d6a280
feat(ErrorHandler): implement custom error handling for PSR-7 integra…
terabytesoftw Jul 25, 2025
6070ef8
refactor(ErrorHandler): Enhance documentation and improve exception h…
terabytesoftw Jul 26, 2025
52fd171
Merge branch 'main' into feature-5
terabytesoftw Jul 26, 2025
0e42a74
Merge branch 'main' into feature-5
terabytesoftw Jul 26, 2025
a77708e
feat(tests): Add `StatelessApplicationTest` class and `SiteController…
terabytesoftw Jul 27, 2025
f33e2ef
Apply fixes from StyleCI
StyleCIBot Jul 27, 2025
03b49bf
fix(tests): Update log file path and disable auto-login in test confi…
terabytesoftw Jul 27, 2025
2bfb4ff
Merge branch 'feature-5' of github-principal:yii2-extensions/psr-brid…
terabytesoftw Jul 27, 2025
4ba9bad
Merge branch 'main' into feature-5
terabytesoftw Jul 27, 2025
e408b54
Add more tests.
terabytesoftw Jul 27, 2025
4ac6541
Apply fixes from StyleCI
StyleCIBot Jul 27, 2025
a9a1766
refactor(tests): Remove unused test methods and clean up `SiteControl…
terabytesoftw Jul 27, 2025
bdc9b1a
Apply fixes from StyleCI
StyleCIBot Jul 27, 2025
d83e987
fix(dependencies): Ensure `ext-mbstring` is required before `psr/http…
terabytesoftw Jul 27, 2025
1764231
Merge branch 'feature-5' of github-principal:yii2-extensions/psr-brid…
terabytesoftw Jul 27, 2025
d2cfb44
test: Enhance `ServerRequestCreatorTest` class with additional assert…
terabytesoftw Jul 27, 2025
e0639a4
test: Add exception handling for maximum nesting depth in `UploadedFi…
terabytesoftw Jul 27, 2025
1234153
test: Replace backslash with constant for UPLOAD_ERR_OK in `UploadedF…
terabytesoftw Jul 27, 2025
1ae3af0
test: Update assertions in `UploadedFileCreatorTest` for clarity on d…
terabytesoftw Jul 27, 2025
ef1f192
test: Refactor deeply nested file structure creation in `UploadedFile…
terabytesoftw Jul 27, 2025
48da767
test: Remove redundant test for depth parameter starting at zero in `…
terabytesoftw Jul 27, 2025
2a76779
test: Add core components configuration test and alias setup verifica…
terabytesoftw Jul 28, 2025
be2ac73
test: Add tests to verify triggering of before and after request even…
terabytesoftw Jul 28, 2025
6b78f5c
test: Swap before and after request event triggers in `StatelessAppli…
terabytesoftw Jul 28, 2025
54625da
refactor: Remove redundant request handling in bootstrap method of `S…
terabytesoftw Jul 28, 2025
1b59b7d
Apply fixes from StyleCI
StyleCIBot Jul 28, 2025
2a66744
refactor: Remove bootstrap method and its documentation from `Statele…
terabytesoftw Jul 28, 2025
8a7fb6c
Merge branch 'feature-5' of github-principal:yii2-extensions/psr-brid…
terabytesoftw Jul 28, 2025
3184963
refactor: Remove unnecessary `ensureBehaviors()` method call in `Stat…
terabytesoftw Jul 28, 2025
7ac3960
test: Add test for handling unlimited `memory_limit` in `StatelessA…
terabytesoftw Jul 28, 2025
3d46cbc
test: Add test for parsing memory limit with suffix in `StatelessAppl…
terabytesoftw Jul 28, 2025
a2d90a0
feat: Add `setMemoryLimit()` method to `StatelessApplication` class a…
terabytesoftw Jul 28, 2025
39466d4
Apply fixes from StyleCI
StyleCIBot Jul 28, 2025
bd137c6
test: Update `testGetMemoryLimitRecalculatesWhenMemoryLimitIsZero()` …
terabytesoftw Jul 28, 2025
e900d51
test: Update `testGetMemoryLimitHandlesUnlimitedMemoryCorrectly()` to…
terabytesoftw Jul 28, 2025
69b7f62
test: Update `testGetMemoryLimitHandlesUnlimitedMemoryCorrectly()` to…
terabytesoftw Jul 28, 2025
74f8616
refactor: Rename `memory_limit` methods for clarity and update tests …
terabytesoftw Jul 28, 2025
ee68a32
feat: Enhance memory management in `StatelessApplication` class with …
terabytesoftw Jul 28, 2025
d33d44e
feat: Add additional symbols to the whitelist in `composer-require-ch…
terabytesoftw Jul 28, 2025
608ef66
refactor: Simplify test structure and add event data provider for `St…
terabytesoftw Jul 28, 2025
9482b36
refactor: Simplify memory limit test by removing try-finally and unne…
terabytesoftw Jul 28, 2025
50d3e90
feat: Add tests for memory limit recalculation and handling in `State…
terabytesoftw Jul 28, 2025
5074da5
fix: Reset session `ID` to ensure a new session is created when `sess…
terabytesoftw Jul 28, 2025
a2251c7
feat: Implement session handling tests to ensure isolation between re…
terabytesoftw Jul 29, 2025
97e1853
feat: Add session configuration to test case for improved session han…
terabytesoftw Jul 29, 2025
9c1c0f4
feat: Implement user authentication and session handling in `SiteCont…
terabytesoftw Jul 29, 2025
5962efb
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
155942a
feat: Add 'gd' extension to PHPUnit workflow for enhanced image proce…
terabytesoftw Jul 29, 2025
07958da
fix: Update file streaming response to use a fixed filename instead o…
terabytesoftw Jul 29, 2025
c5ab8b4
feat: Set session save path in TestCase for improved session management.
terabytesoftw Jul 29, 2025
5fdbcf8
feat: Update session handling in TestCase to use dedicated session di…
terabytesoftw Jul 29, 2025
812acc0
fix: Ensure proper session cleanup in `tearDown()` method of `TestCas…
terabytesoftw Jul 29, 2025
d77535b
fix: Simplify session cleanup in `tearDown()` method of `TestCase` cl…
terabytesoftw Jul 29, 2025
22e522d
fix: Simplify session `ID` retrieval in `StatelessApplication` class …
terabytesoftw Jul 29, 2025
1cbb9aa
fix: Update regex for session ID validation to allow alphanumeric cha…
terabytesoftw Jul 29, 2025
93cd36d
fix: Update session `ID` retrieval to use a new session `ID` if none …
terabytesoftw Jul 29, 2025
34945de
fix: Add `session_create_id` to symbol whitelist in composer require …
terabytesoftw Jul 29, 2025
95f22cc
fix: Refactor authorization token handling and improve identity valid…
terabytesoftw Jul 29, 2025
54d1a40
test: Add session handling tests for multiple requests in worker mode…
terabytesoftw Jul 29, 2025
24891fe
fix: Simplify session handling by removing redundant checks and clear…
terabytesoftw Jul 29, 2025
21c6134
fix: Update session handling in tests to use dynamic session name and…
terabytesoftw Jul 29, 2025
190b469
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
31a909e
fix: Update session handling in `StatelessApplicationTest` to use dyn…
terabytesoftw Jul 29, 2025
8fa3359
fix: Update session handling in `StatelessApplicationTest` to use dyn…
terabytesoftw Jul 29, 2025
680f7b7
fix: Update session handling in `StatelessApplicationTest` to use dyn…
terabytesoftw Jul 29, 2025
e4d9b37
fix: Clarify comment to specify skipping the session cookie header in…
terabytesoftw Jul 29, 2025
958fbca
fix: Adjust comment formatting to maintain consistency in `StatelessA…
terabytesoftw Jul 29, 2025
ea62a8f
fix: Correct variable assignment in `testCaptchaSessionIsolation` met…
terabytesoftw Jul 29, 2025
b6578e7
feat: Implement flash message handling in `SiteController` with `acti…
terabytesoftw Jul 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ name: build
jobs:
phpunit:
uses: php-forge/actions/.github/workflows/phpunit.yml@main
with:
extensions: mbstring, gd
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
6 changes: 6 additions & 0 deletions composer-require-checker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"symbol-whitelist": [
"uopz_redefine",
"YII_DEBUG"
]
}
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"prefer-stable": true,
"require": {
"php": ">=8.1",
"psr/http-message": "^2.0",
"ext-mbstring": "*",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0",
"yiisoft/yii2": "^2.0.53|^22"
},
"require-dev": {
Expand All @@ -28,6 +30,9 @@
"xepozz/internal-mocker": "^1.4",
"yii2-extensions/phpstan": "^0.3"
},
"suggest": {
"ext-uopz": "*"
},
"autoload": {
"psr-4": {
"yii2\\extensions\\psrbridge\\": "src"
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ parameters:

tmpDir: %currentWorkingDirectory%/runtime

yii2:
config_path: %currentWorkingDirectory%/tests/phpstan-config.php

# Enable strict advanced checks
checkImplicitMixed: true
checkBenevolentUnionTypes: true
Expand Down
190 changes: 190 additions & 0 deletions src/http/ErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

declare(strict_types=1);

namespace yii2\extensions\psrbridge\http;

use Throwable;
use Yii;
use yii\base\{InvalidRouteException, UserException};
use yii\console\Exception;
use yii\helpers\VarDumper;
use yii\web\HttpException;

use function array_diff_key;
use function array_flip;
use function htmlspecialchars;
use function http_response_code;
use function ini_set;

/**
* Error handler extension with PSR-7 bridge support for Yii2 applications.
*
* Provides a drop-in replacement for {@see \yii\web\ErrorHandler} that integrates PSR-7 ResponseInterface handling,
* enabling seamless interoperability with PSR-7 compatible HTTP stacks and modern PHP runtimes.
*
* This class overrides exception handling to produce PSR-7 ResponseInterface objects, supporting custom error views,
* fallback rendering, and Yii2 error action integration.
*
* All exception handling is performed in a type-safe, immutable manner, ensuring compatibility with legacy Yii2
* workflows and modern middleware stacks.
*
* Key features.
* - Custom error view and error action support for HTML responses.
* - Exception-safe conversion to PSR-7 ResponseInterface objects.
* - Fallback rendering for nested exceptions and debug output.
* - Integration with Yii2 error action and view rendering.
* - Type-safe, immutable error handling for modern runtimes.
*
* @copyright Copyright (C) 2025 Terabytesoftw.
* @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
final class ErrorHandler extends \yii\web\ErrorHandler
{
/**
* Handles exceptions and produces a PSR-7 ResponseInterface object.
*
* Overrides the default Yii2 exception handling to generate a PSR-7 ResponseInterface instance, supporting custom
* error views, fallback rendering, and integration with Yii2 error actions.
*
* Ensures type-safe, immutable error handling for modern runtimes.
*
* This method guarantees that all exceptions are converted to PSR-7 ResponseInterface, maintaining compatibility
* with both legacy Yii2 workflows and modern middleware stacks.
*
* @param Throwable $exception Exception to handle and convert to a PSR-7 ResponseInterface object.
*
* @return Response PSR-7 ResponseInterface representing the handled exception.
*/
public function handleException($exception): Response
{
$this->exception = $exception;

$this->unregister();

if (PHP_SAPI !== 'cli') {
$statusCode = 500;

if ($exception instanceof HttpException) {
$statusCode = $exception->statusCode;
}

http_response_code($statusCode);
}

try {
$this->logException($exception);

if ($this->discardExistingOutput) {
$this->clearOutput();
}

$response = $this->renderException($exception);
} catch (Throwable $e) {
return $this->handleFallbackExceptionMessage($e, $exception);
}

$this->exception = null;

return $response;
}

/**
* Handles fallback exception rendering when an error occurs during exception processing.
*
* Produces a {@see Response} object with a generic error message and, in debug mode, includes detailed exception
* information and a sanitized snapshot of server variables, excluding sensitive keys.
*
* This method ensures that nested or secondary exceptions do not expose sensitive data and provides a minimal
* diagnostic output for debugging purposes.
*
* @param Throwable $exception Exception thrown during error handling.
* @param Throwable $previousException Original exception that triggered error handling.
*
* @return Response Object containing the fallback error message and debug output if enabled.
*/
protected function handleFallbackExceptionMessage($exception, $previousException): Response
{
$response = new Response();

$msg = "An Error occurred while handling another error:\n";

$msg .= $exception;

$msg .= "\nPrevious exception:\n";

$msg .= $previousException;

$response->data = 'An internal server error occurred.';

if (YII_DEBUG) {
$response->data = '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
$safeServerVars = array_diff_key(
$_SERVER,
array_flip(
[
'API_KEY',
'AUTH_TOKEN',
'DB_PASSWORD',
'SECRET_KEY',
],
),
);
$response->data .= "\n\$_SERVER = " . VarDumper::export($safeServerVars);
}

return $response;
}

/**
* Renders the exception and produces a {@see Response} object with appropriate error content.
*
* Handles exception rendering for HTML, raw, and array formats, supporting custom error views and error actions.
*
* This method ensures type-safe, immutable error handling and maintains compatibility with Yii2 error actions and
* view rendering.
*
* @param Throwable $exception Exception to render and convert to a {@see Response} object.
*
* @throws Exception if an error occurs during error action execution.
* @throws InvalidRouteException if the error action route is invalid or cannot be resolved.
*
* @return Response Object containing the rendered exception output.
*/
protected function renderException($exception): Response
{
$response = new Response();

$response->setStatusCodeByException($exception);

$useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);

if ($useErrorView && $this->errorAction !== null) {
$result = Yii::$app->runAction($this->errorAction);

if ($result instanceof Response) {
$response = $result;
} else {
$response->data = $result;
}
} elseif ($response->format === Response::FORMAT_HTML) {
if ($this->shouldRenderSimpleHtml()) {
$response->data = '<pre>' . $this->htmlEncode(self::convertExceptionToString($exception)) . '</pre>';
} else {
if (YII_DEBUG) {
ini_set('display_errors', '1');
}

$file = $useErrorView ? $this->errorView : $this->exceptionView;

$response->data = $this->renderFile($file, ['exception' => $exception]);
}
} elseif ($response->format === Response::FORMAT_RAW) {
$response->data = self::convertExceptionToString($exception);
} else {
$response->data = $this->convertExceptionToArray($exception);
}

return $response;
}
}
82 changes: 82 additions & 0 deletions src/http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
use yii2\extensions\psrbridge\adapter\ServerRequestAdapter;
use yii2\extensions\psrbridge\exception\Message;

use function base64_decode;
use function count;
use function explode;
use function is_array;
use function is_string;
use function mb_check_encoding;
use function mb_substr;
use function strncasecmp;

/**
* HTTP Request extension with PSR-7 bridge and worker mode support.
Expand Down Expand Up @@ -43,6 +50,12 @@
*/
final class Request extends \yii\web\Request
{
/**
* @var string A secret key used for cookie validation. This property must be set if {@see enableCookieValidation}
* is 'true'.
*/
public $cookieValidationKey = '';

/**
* Whether the request is in worker mode.
*/
Expand All @@ -56,6 +69,75 @@ final class Request extends \yii\web\Request
*/
private ServerRequestAdapter|null $adapter = null;

/**
* Retrieves HTTP Basic authentication credentials from the current request.
*
* Returns an array containing the username and password sent via HTTP authentication, supporting both standard PHP
* SAPI variables and the 'Authorization' header for environments where credentials are not passed directly.
*
* The method first checks for credentials in the PHP_AUTH_USER and PHP_AUTH_PW server variables. If not present, it
* attempts to extract and decode credentials from the 'Authorization' header, handling Apache php-cgi scenarios and
* validating the decoded data for UTF-8 encoding and proper format.
*
* Usage example:
* ```php
* [$username, $password] = $request->getAuthCredentials();
* ```
*
* @return array Contains exactly two elements.
* - 0: username sent via HTTP authentication, `null` if the username is not given.
* - 1: password sent via HTTP authentication, `null` if the password is not given.
*
* @phpstan-return array{0: string|null, 1: string|null}
*/
public function getAuthCredentials(): array
{
$username = isset($_SERVER['PHP_AUTH_USER']) && is_string($_SERVER['PHP_AUTH_USER'])
? $_SERVER['PHP_AUTH_USER']
: null;
$password = isset($_SERVER['PHP_AUTH_PW']) && is_string($_SERVER['PHP_AUTH_PW'])
? $_SERVER['PHP_AUTH_PW']
: null;

if ($username !== null || $password !== null) {
return [$username, $password];
}

/**
* Apache with php-cgi does not pass HTTP Basic authentication to PHP by default.
* To make it work, add one of the following lines to to your .htaccess file:
*
* SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
* --OR--
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*/
$authToken = $this->getHeaders()->get('Authorization');

/** @phpstan-ignore-next-line */
if ($authToken !== null && strncasecmp($authToken, 'basic', 5) === 0) {
$encoded = mb_substr($authToken, 6);
$decoded = base64_decode($encoded, true); // strict mode

// validate decoded data
if ($decoded === false || mb_check_encoding($decoded, 'UTF-8') === false) {
return [null, null]; // return null for malformed credentials
}

$parts = explode(':', $decoded, 2);

if (count($parts) < 2) {
return [$parts[0] === '' ? null : $parts[0], null];
}

return [
$parts[0] === '' ? null : $parts[0],
(isset($parts[1]) && $parts[1] !== '') ? $parts[1] : null,
];
}

return [null, null];
}

/**
* Retrieves the request body parameters, excluding the HTTP method override parameter if present.
*
Expand Down
Loading