Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- feat(ui): add shared profiler normalization and Timeline rendering contracts.
- feat(tests): add unit tests for panel snapshots and enhance existing test coverage.
- feat(ui): share User guest and RBAC section rendering and support selecting filtered tabs.
- feat(ui): add sensitive queue-payload redaction and recognize Yii3 queue producers for Dump, Mail, and Queue parity.
57 changes: 57 additions & 0 deletions src/Helper/SensitiveDataRedactor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Helper;

use function array_fill_keys;
use function array_map;
use function is_array;
use function is_string;
use function strtolower;

/**
* Replaces values whose array keys match an explicitly configured sensitive-key list.
*/
final class SensitiveDataRedactor
{
public const string PLACEHOLDER = '[redacted]';

/**
* Redacts configured keys case-insensitively throughout a nested array.
*
* @param array<array-key, mixed> $value Value tree to sanitize.
* @param list<string> $sensitiveKeys Exact key names to redact.
*
* @return array<array-key, mixed> Sanitized tree with keys and non-sensitive values preserved.
*/
public static function redact(array $value, array $sensitiveKeys): array
{
$keys = array_fill_keys(array_map(strtolower(...), $sensitiveKeys), true);

return self::walk($value, $keys);
}

/**
* @param array<array-key, mixed> $value
* @param array<string, true> $sensitiveKeys
*
* @return array<array-key, mixed>
*/
private static function walk(array $value, array $sensitiveKeys): array
{
$redacted = [];

foreach ($value as $key => $item) {
if (is_string($key) && isset($sensitiveKeys[strtolower($key)])) {
$redacted[$key] = self::PLACEHOLDER;

continue;
}

$redacted[$key] = is_array($item) ? self::walk($item, $sensitiveKeys) : $item;
}

return $redacted;
}
}
10 changes: 10 additions & 0 deletions src/Panel/Queue/QueueDriverDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use function count;
use function explode;
use function in_array;
use function str_ends_with;
use function str_starts_with;
use function strtolower;
use function ucfirst;

Expand Down Expand Up @@ -62,6 +64,14 @@ public static function detect(string $fqcn): array
return self::$cache[$fqcn];
}

if (str_starts_with($fqcn, 'Yiisoft\\Queue\\') && str_ends_with($fqcn, '\\SyncQueueProducer')) {
return self::$cache[$fqcn] = ['Sync', false];
}

if (str_starts_with($fqcn, 'Yiisoft\\Queue\\') && str_ends_with($fqcn, '\\AsyncQueueProducer')) {
return self::$cache[$fqcn] = ['Async', true];
}

$token = self::extractDriverToken($fqcn);

$name = array_key_exists($token, self::DRIVER_LABELS)
Expand Down
51 changes: 51 additions & 0 deletions tests/Helper/SensitiveDataRedactorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Tests\Helper;

use PHPForge\Debug\Helper\SensitiveDataRedactor;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

/**
* Unit tests for {@see SensitiveDataRedactor} covering exact, case-insensitive, and nested key replacement.
*
* @since 0.1
*/
#[Group('helpers')]
final class SensitiveDataRedactorTest extends TestCase
{
public function testRedactPreservesNumericKeysAndNonSensitiveValues(): void
{
self::assertSame(
[0 => 'first', 'user' => ['name' => 'Ada']],
SensitiveDataRedactor::redact([0 => 'first', 'user' => ['name' => 'Ada']], ['password']),
'Unmatched values and numeric keys must remain unchanged.',
);
}

public function testRedactReplacesConfiguredKeysCaseInsensitivelyAtEveryDepth(): void
{
self::assertSame(
[
'Password' => SensitiveDataRedactor::PLACEHOLDER,
'nested' => [
'accessToken' => SensitiveDataRedactor::PLACEHOLDER,
'tokenSuffix' => 'visible',
],
],
SensitiveDataRedactor::redact(
[
'Password' => 'secret',
'nested' => [
'accessToken' => 'token',
'tokenSuffix' => 'visible',
],
],
['password', 'ACCESSTOKEN'],
),
'Configured keys must match exactly and without case sensitivity throughout nested arrays.',
);
}
}
28 changes: 28 additions & 0 deletions tests/Panel/Queue/QueueDriverDetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ public function testDetectClassifiesSyncDriverAsRunInProcess(): void
);
}

public function testDetectDoesNotClassifySameSuffixOutsideYii3Namespace(): void
{
self::assertSame(
['Queue', true],
QueueDriverDetector::detect('App\\Queue\\SyncQueueProducer'),
'Same-suffix producers outside Yiisoft Queue must retain generic driver detection.',
);
self::assertSame(
['Other', true],
QueueDriverDetector::detect('Vendor\\Other\\AsyncQueueProducer'),
'Unrelated async-producer class names must not be classified as Yii3 producers.',
);
}

public function testDetectFallsBackToLowercasedFqcnForSingleSegmentClass(): void
{
self::setDetectorCache(
Expand All @@ -84,6 +98,20 @@ public function testDetectFallsBackToLowercasedFqcnForSingleSegmentClass(): void
);
}

public function testDetectRecognizesYii3ProducerClasses(): void
{
self::assertSame(
['Sync', false],
QueueDriverDetector::detect('Yiisoft\\Queue\\SyncQueueProducer'),
'Yii3 synchronous producers must use the shared Sync label and in-process flag.',
);
self::assertSame(
['Async', true],
QueueDriverDetector::detect('Yiisoft\\Queue\\AsyncQueueProducer'),
'Yii3 asynchronous producers must surface their async execution model.',
);
}

public function testDetectReturnsUnknownForEmptyFqcn(): void
{
self::setDetectorCache(
Expand Down
Loading