Skip to content
Open
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,471 changes: 1 addition & 1,470 deletions phpstan-baseline.neon

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use function is_array;
use function is_iterable;
use function is_string;

final class AuthenticationCompilerPass implements CompilerPassInterface
{
Expand All @@ -39,24 +42,39 @@ private function registerLoginRouteLoader(ContainerBuilder $container): void
$routeLoader = $container->getDefinition(LoginPageRouteLoader::class);
$firewalls = $container->getParameter('security.firewalls');

if (! is_iterable($firewalls)) {
return;
}

$authenticators = [];

foreach ($firewalls as $firewall) {
if (! is_string($firewall)) {
continue;
}

if (! $container->hasDefinition('security.authenticator.form_login.' . $firewall)) {
continue;
}

$authenticator = $container->getDefinition('security.authenticator.form_login.' . $firewall);
$authenticators[$firewall] = $authenticator->getArgument(4) + [
$options = $authenticator->getArgument(4);
if (! is_array($options)) {
continue;
}

$authenticators[$firewall] = $options + [
'remember_me_parameter' => null,
'always_remember_me' => false,
];

if ($container->hasDefinition('security.authenticator.remember_me_handler.' . $firewall)) {
$rememberMeArguments = $container->getDefinition('security.authenticator.remember_me_handler.' . $firewall)->getArgument(3);

$authenticators[$firewall]['remember_me_parameter'] = $rememberMeArguments['remember_me_parameter'];
$authenticators[$firewall]['always_remember_me'] = $rememberMeArguments['always_remember_me'];
if (is_array($rememberMeArguments)) {
$authenticators[$firewall]['remember_me_parameter'] = $rememberMeArguments['remember_me_parameter'] ?? null;
$authenticators[$firewall]['always_remember_me'] = $rememberMeArguments['always_remember_me'] ?? false;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use function is_array;

final class MenuCompilerPass implements CompilerPassInterface
{
Expand All @@ -37,6 +38,9 @@ public function process(ContainerBuilder $container): void

foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
if (! is_array($attributes)) {
continue;
}

$wrapperDefinition = (new Definition(Closure::class))
->addArgument([new Reference($id), $attributes['method']])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class TwoFactorExtension
/**
* @param array{name: string, base_template: string} $config
*/
public static function enable(ContainerBuilder $container, array $config = []): void
public static function enable(ContainerBuilder $container, array $config): void
{
$container
->setDefinition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Knp\Menu\Provider\MenuProviderInterface;
use Override;
use ReflectionMethod;
use Reflector;
use SolidWorx\Platform\PlatformBundle\Attributes\Menu\MenuBuilder;
use SolidWorx\Platform\PlatformBundle\Config\PlatformConfiguration;
use SolidWorx\Platform\PlatformBundle\Controller\Security\ResendTwoFactorCode;
Expand Down Expand Up @@ -68,10 +69,14 @@ public function load(array $configs, ContainerBuilder $container): void
$container->registerForAutoconfiguration(MenuProviderInterface::class)
->addTag('knp_menu.provider');

$container->registerAttributeForAutoconfiguration(MenuBuilder::class, static function (ChildDefinition $definition, MenuBuilder $attribute, ReflectionMethod $reflectionMethod): void {
$container->registerAttributeForAutoconfiguration(MenuBuilder::class, static function (ChildDefinition $definition, MenuBuilder $attribute, Reflector $reflector): void {
if (! $reflector instanceof ReflectionMethod) {
return;
}

$definition->addTag(Util::tag('menu.builder'), [
'alias' => $attribute->name,
'method' => $reflectionMethod->getName(),
'method' => $reflector->getName(),
'priority' => $attribute->priority,
'role' => $attribute->role,
]);
Expand Down
13 changes: 12 additions & 1 deletion src/Bundle/Platform/Feature/FeatureValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

namespace SolidWorx\Platform\PlatformBundle\Feature;

use Stringable;
use function is_array;
use function is_bool;
use function is_int;
use function is_scalar;

final readonly class FeatureValue
{
Expand Down Expand Up @@ -75,7 +77,16 @@ public function asBool(): bool
public function asString(): string
{
if (is_array($this->value)) {
return implode(',', $this->value);
$parts = [];
foreach ($this->value as $item) {
if (is_scalar($item)) {
$parts[] = (string) $item;
} elseif ($item instanceof Stringable) {
$parts[] = (string) $item;
}
}

return implode(',', $parts);
}

if (is_bool($this->value)) {
Expand Down
15 changes: 12 additions & 3 deletions src/Bundle/Platform/Form/Type/Security/LoginType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace SolidWorx\Platform\PlatformBundle\Form\Type\Security;

use InvalidArgumentException;
use Override;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
Expand All @@ -21,17 +22,25 @@
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use function is_string;

/**
* @extends AbstractType<array{code: string|null}>
* @extends AbstractType<mixed>
*/
final class LoginType extends AbstractType
{
#[Override]
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$usernameParameter = $options['username_parameter'];
$passwordParameter = $options['password_parameter'];

if (! is_string($usernameParameter) || ! is_string($passwordParameter)) {
throw new InvalidArgumentException('The "username_parameter" and "password_parameter" options must be strings.');
}

$builder
->add($options['username_parameter'], EmailType::class, [
->add($usernameParameter, EmailType::class, [
'label' => 'Email address',
'placeholder' => 'your@email.com',
'required' => true,
Expand All @@ -47,7 +56,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
],
]);

$builder->add($options['password_parameter'], PasswordType::class, [
$builder->add($passwordParameter, PasswordType::class, [
'label' => 'Password',
'required' => true,
'attr' => [
Expand Down
14 changes: 11 additions & 3 deletions src/Bundle/Platform/Form/Type/Security/TwoFactorVerifyType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace SolidWorx\Platform\PlatformBundle\Form\Type\Security;

use InvalidArgumentException;
use Override;
use SolidWorx\Platform\PlatformBundle\Validator\Constraint\TwoFactorCode;
use Symfony\Component\Form\AbstractType;
Expand All @@ -21,15 +22,22 @@
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use function is_string;

/**
* @extends AbstractType<array{code: string|null}>
* @extends AbstractType<mixed>
*/
final class TwoFactorVerifyType extends AbstractType
{
#[Override]
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$secret = $options['secret'];

if (! is_string($secret)) {
throw new InvalidArgumentException('The "secret" option must be a string.');
}

$builder
->add('code', NumberType::class, [
'label' => 'Enter the verification code generated by your mobile application.',
Expand All @@ -44,11 +52,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
],
'constraints' => [
new NotBlank(),
new TwoFactorCode(secret: (string) $options['secret']),
new TwoFactorCode(secret: $secret),
],
])
->add('secret', HiddenType::class, [
'data' => $options['secret'],
'data' => $secret,
]);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Bundle/Platform/Form/Type/TextEditorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
* The field always renders a real `<textarea>` (so it degrades gracefully without JavaScript) and stores
* either sanitized HTML (default) or a validated Tiptap JSON document. All output is filtered server-side,
* which is the security boundary regardless of what the browser submits.
*
* @extends AbstractType<mixed>
*/
final class TextEditorType extends AbstractType
{
Expand Down
80 changes: 63 additions & 17 deletions src/Bundle/Platform/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
Expand All @@ -34,7 +35,9 @@
use function file_exists;
use function glob;
use function implode;
use function is_array;
use function is_file;
use function is_string;
use function pathinfo;
use function sprintf;

Expand Down Expand Up @@ -70,14 +73,19 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
return parent::handle($request, $type, $catch);
}

/**
* @return iterable<Bundle>
*/
#[Override]
public function registerBundles(): iterable
{
yield from $this->registerBundlesTrait();

$platformConfig = $this->rawConfig['platform'] ?? [];
foreach ($this->registerBundlesTrait() as $bundle) {
if ($bundle instanceof Bundle) {
yield $bundle;
}
}

if (($platformConfig['security']['two_factor']['enabled'] ?? false) === true) {
if ($this->isTwoFactorEnabled()) {
yield new SchebTwoFactorBundle();
}

Expand Down Expand Up @@ -106,7 +114,7 @@ protected function initializeBundles(): void
$section = $key !== ''
? ($this->rawConfig[$key] ?? [])
: ($this->rawConfig['platform'] ?? []);
$bundle->setPlatformRawConfig($section);
$bundle->setPlatformRawConfig(self::toConfigArray($section));
}

$this->bundles[$name] = $bundle;
Expand All @@ -120,6 +128,26 @@ protected function configureRoutes(RoutingConfigurator $routes): void
$routes->import('.', '_solidworx_platform_auth_routes');
}

private function isTwoFactorEnabled(): bool
{
$platformConfig = $this->rawConfig['platform'] ?? [];
if (! is_array($platformConfig)) {
return false;
}

$security = $platformConfig['security'] ?? [];
if (! is_array($security)) {
return false;
}

$twoFactor = $security['two_factor'] ?? [];
if (! is_array($twoFactor)) {
return false;
}

return ($twoFactor['enabled'] ?? false) === true;
}

private function processPlatformConfig(): void
{
if ($this->rawConfig !== null) {
Expand All @@ -139,21 +167,38 @@ private function processPlatformConfig(): void
$ext = pathinfo($configFile, PATHINFO_EXTENSION);

$this->rawConfig = match ($ext) {
'yaml', 'yml' => Yaml::parseFile($configFile) ?? [],
'json' => (static function (string $path): array {
$decoded = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
return is_array($decoded) ? $decoded : [];
})($configFile),
'php' => (static function (string $path): array {
$result = require $path;
return is_array($result) ? $result : [];
})($configFile),
'yaml', 'yml' => self::toConfigArray(Yaml::parseFile($configFile)),
'json' => self::toConfigArray(json_decode((string) file_get_contents($configFile), true, 512, JSON_THROW_ON_ERROR)),
'php' => self::toConfigArray(require $configFile),
default => throw new RuntimeException(sprintf('Unsupported platform configuration file format: .%s', $ext)),
};

$this->publishPlatformConfigState();
}

/**
* Normalises a decoded configuration value into a string-keyed map.
*
* The root of a configuration file is always an associative map, so any non-array
* value is treated as empty and the keys are normalised to strings.
*
* @return array<string, mixed>
*/
private static function toConfigArray(mixed $value): array
{
if (! is_array($value)) {
return [];
}

$config = [];

foreach ($value as $key => $item) {
$config[(string) $key] = $item;
}

return $config;
}

/**
* Publishes the parsed `platform:` section so compile-time helpers (e.g. the security
* config helpers) can read it while the container is being built.
Expand All @@ -169,16 +214,17 @@ private function resolveConfigFile(): ?string
{
// Allow an explicit path override via environment variable
$envOverride = $_ENV['PLATFORM_CONFIG_FILE'] ?? $_SERVER['PLATFORM_CONFIG_FILE'] ?? null;
if ($envOverride !== null && is_file((string) $envOverride)) {
return (string) $envOverride;
if (is_string($envOverride) && is_file($envOverride)) {
return $envOverride;
}

$glob = $this->getProjectDir() . '/' . self::DEFAULT_CONFIG_GLOB;

$configFiles = [];

if (defined('GLOB_BRACE')) {
$configFiles = glob($glob, GLOB_BRACE) ?: [];
$globResult = glob($glob, GLOB_BRACE);
$configFiles = $globResult !== false ? $globResult : [];
} else {
foreach (['yml', 'yaml', 'json', 'php'] as $ext) {
$candidate = $this->getProjectDir() . '/platform.' . $ext;
Expand Down
Loading
Loading