<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Security\Http\Tests\Firewall;

use Doctrine\Persistence\Proxy;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Firewall\ContextListener;
use Symfony\Component\Security\Http\Tests\Fixtures\CustomUser;
use Symfony\Component\Security\Http\Tests\Fixtures\LazyDoctrinePersistenceUser;
use Symfony\Component\Security\Http\Tests\Fixtures\LazyVarExporterUser;
use Symfony\Component\Security\Http\Tests\Fixtures\NullUserToken;
use Symfony\Component\VarExporter\LazyObjectInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;

class ContextListenerTest extends TestCase
{
    public function testItRequiresContextKey()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('$contextKey must not be empty');
        new ContextListener(
            new TokenStorage(),
            [],
            ''
        );
    }

    public function testUserProvidersNeedToImplementAnInterface()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('User provider "stdClass" must implement "Symfony\Component\Security\Core\User\UserProviderInterface');
        $this->handleEventWithPreviousSession([new \stdClass()]);
    }

    public function testTokenReturnsNullUser()
    {
        $tokenStorage = new TokenStorage();
        $tokenStorage->setToken(new NullUserToken());

        $session = new Session(new MockArraySessionStorage());
        $session->set('_security_context_key', serialize($tokenStorage->getToken()));

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);

        $listener = new ContextListener($tokenStorage, [], 'context_key');

        $this->expectException(\UnexpectedValueException::class);
        $this->expectExceptionMessage('Cannot authenticate a "Symfony\Component\Security\Http\Tests\Fixtures\NullUserToken" token because it doesn\'t store a user.');

        $listener->authenticate(new RequestEvent(
            $this->createStub(HttpKernelInterface::class),
            $request,
            HttpKernelInterface::MAIN_REQUEST,
        ));
    }

    public function testOnKernelResponseWillAddSession()
    {
        $session = $this->runSessionOnKernelResponse(
            new UsernamePasswordToken(new InMemoryUser('test1', 'pass1'), 'phpunit', ['ROLE_USER']),
            null
        );

        $token = unserialize($session->get('_security_session'));
        $this->assertInstanceOf(UsernamePasswordToken::class, $token);
        $this->assertEquals('test1', $token->getUserIdentifier());
    }

    public function testOnKernelResponseWillReplaceSession()
    {
        $session = $this->runSessionOnKernelResponse(
            new UsernamePasswordToken(new InMemoryUser('test1', 'pass1'), 'phpunit', ['ROLE_USER']),
            'C:10:"serialized"'
        );

        $token = unserialize($session->get('_security_session'));
        $this->assertInstanceOf(UsernamePasswordToken::class, $token);
        $this->assertEquals('test1', $token->getUserIdentifier());
    }

    #[RequiresPhp('>=8.4.0')]
    public function testOnKernelResponseInitializesNativeLazyUser()
    {
        $initialized = false;
        $user = (new \ReflectionClass(InMemoryUser::class))->newLazyGhost(
            static function (InMemoryUser $u) use (&$initialized) {
                $u->__construct('test', 'pass');
                $initialized = true;
            },
            \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE,
        );

        $this->runSessionOnKernelResponse(new UsernamePasswordToken($user, 'phpunit', ['ROLE_USER']));

        $this->assertTrue($initialized);
    }

    public function testOnKernelResponseInitializesDoctrinePersistenceProxyUser()
    {
        if (!interface_exists(Proxy::class)) {
            $this->markTestSkipped('"doctrine/persistence" is not installed.');
        }

        $user = new LazyDoctrinePersistenceUser();
        $this->assertFalse($user->initialized);

        $this->runSessionOnKernelResponse(new UsernamePasswordToken($user, 'phpunit', ['ROLE_USER']));

        $this->assertTrue($user->initialized);
    }

    public function testOnKernelResponseInitializesVarExporterLazyUser()
    {
        if (!interface_exists(LazyObjectInterface::class)) {
            $this->markTestSkipped('"symfony/var-exporter" is not installed.');
        }

        $user = new LazyVarExporterUser();
        $this->assertFalse($user->initialized);

        $this->runSessionOnKernelResponse(new UsernamePasswordToken($user, 'phpunit', ['ROLE_USER']));

        $this->assertTrue($user->initialized);
    }

    public function testOnKernelResponseWillRemoveSession()
    {
        $session = $this->runSessionOnKernelResponse(
            null,
            'C:10:"serialized"'
        );

        $this->assertFalse($session->has('_security_session'));
    }

    public function testOnKernelResponseWithoutSession()
    {
        $tokenStorage = new TokenStorage();
        $tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('test1', 'pass1'), 'phpunit', ['ROLE_USER']));
        $request = new Request();
        $request->attributes->set('_security_firewall_run', '_security_session');
        $session = new Session(new MockArraySessionStorage());
        $request->setSession($session);

        $event = new ResponseEvent(
            $this->createStub(HttpKernelInterface::class),
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new Response()
        );

        $listener = new ContextListener($tokenStorage, [], 'session', null, new EventDispatcher());
        $listener->onKernelResponse($event);

        $this->assertTrue($session->isStarted());
    }

    public function testOnKernelResponseWithoutSessionNorToken()
    {
        $request = new Request();
        $session = new Session(new MockArraySessionStorage());
        $request->setSession($session);

        $event = new ResponseEvent(
            $this->createStub(HttpKernelInterface::class),
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new Response()
        );

        $listener = new ContextListener(new TokenStorage(), [], 'session', null, new EventDispatcher());
        $listener->onKernelResponse($event);

        $this->assertFalse($session->isStarted());
    }

    #[DataProvider('provideInvalidToken')]
    public function testInvalidTokenInSession($token)
    {
        $tokenStorage = $this->createMock(TokenStorageInterface::class);
        $session = new Session(new MockArraySessionStorage('SESSIONNAME'));
        $session->set('_security_key123', $token);
        $request = new Request([], [], [], ['SESSIONNAME' => true]);
        $request->setSession($session);

        $tokenStorage->expects($this->once())
            ->method('setToken')
            ->with(null);

        $listener = new ContextListener($tokenStorage, [], 'key123');
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
    }

    public static function provideInvalidToken()
    {
        return [
            ['foo'],
            ['O:8:"NotFound":0:{}'],
            [serialize(new \__PHP_Incomplete_Class())],
            [serialize(null)],
            [null],
        ];
    }

    public function testHandleAddsKernelResponseListener()
    {
        $tokenStorage = new TokenStorage();
        $dispatcher = $this->createMock(EventDispatcherInterface::class);
        $listener = new ContextListener($tokenStorage, [], 'key123', null, $dispatcher);

        $dispatcher->expects($this->once())
            ->method('addListener')
            ->with(KernelEvents::RESPONSE, $listener->onKernelResponse(...));

        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST));
    }

    public function testOnKernelResponseListenerRemovesItself()
    {
        $session = new Session(new MockArraySessionStorage('SESSIONNAME'));
        $tokenStorage = new TokenStorage();
        $dispatcher = $this->createMock(EventDispatcherInterface::class);

        $listener = new ContextListener($tokenStorage, [], 'key123', null, $dispatcher);

        $request = new Request();
        $request->attributes->set('_security_firewall_run', '_security_key123');
        $request->setSession($session);

        $event = new ResponseEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response());

        $dispatcher->expects($this->once())
            ->method('removeListener')
            ->with(KernelEvents::RESPONSE, $listener->onKernelResponse(...));

        $listener->onKernelResponse($event);
    }

    public function testHandleRemovesTokenIfNoPreviousSessionWasFound()
    {
        $request = new Request();

        $tokenStorage = $this->createMock(TokenStorageInterface::class);
        $tokenStorage->expects($this->once())->method('setToken')->with(null);

        $listener = new ContextListener($tokenStorage, [], 'key123');
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
    }

    public function testIfTokenIsDeauthenticated()
    {
        $refreshedUser = new InMemoryUser('foobar', 'baz');
        $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)]);

        $this->assertNull($tokenStorage->getToken());
    }

    public function testTokenIsNotDeauthenticatedOnUserChangeIfNotAnInstanceOfAbstractToken()
    {
        $tokenStorage = new TokenStorage();
        $refreshedUser = new InMemoryUser('changed', 'baz');

        $token = new CustomToken(new InMemoryUser('original', 'foo'), ['ROLE_FOO']);

        $session = new Session(new MockArraySessionStorage());
        $session->set('_security_context_key', serialize($token));

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);

        $listener = new ContextListener($tokenStorage, [new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], 'context_key');
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));

        $this->assertInstanceOf(CustomToken::class, $tokenStorage->getToken());
        $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser());
    }

    public function testIfTokenIsNotDeauthenticated()
    {
        $badRefreshedUser = new InMemoryUser('foobar', 'baz');
        $goodRefreshedUser = new InMemoryUser('foobar', 'bar');
        $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser);
        $this->assertSame($goodRefreshedUser, $tokenStorage->getToken()->getUser());
    }

    public function testSwitchUserTokenIsNotDeauthenticated()
    {
        $impersonated = new CustomUser('user', ['ROLE_USER'], 'pass', false);
        $impersonator = new CustomUser('admin', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'], 'pass', false);
        $originalToken = new UsernamePasswordToken($impersonator, 'context_key', $impersonator->getRoles());
        $token = new SwitchUserToken($impersonated, 'context_key', $impersonated->getRoles(), $originalToken);

        $session = new Session(new MockArraySessionStorage());
        $session->set('_security_context_key', serialize($token));

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);

        $provider = new class($impersonated) implements UserProviderInterface {
            public function __construct(private CustomUser $refreshedUser)
            {
            }

            public function loadUserByIdentifier(string $identifier): UserInterface
            {
                return $this->refreshedUser;
            }

            public function refreshUser(UserInterface $user): UserInterface
            {
                return $this->refreshedUser;
            }

            public function supportsClass(string $class): bool
            {
                return CustomUser::class === $class;
            }
        };

        $tokenStorage = new TokenStorage();
        $listener = new ContextListener($tokenStorage, [$provider], 'context_key');
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));

        $this->assertInstanceOf(SwitchUserToken::class, $tokenStorage->getToken());
        $this->assertSame($impersonated, $tokenStorage->getToken()->getUser());
    }

    public function testTryAllUserProvidersUntilASupportingUserProviderIsFound()
    {
        $refreshedUser = new InMemoryUser('foobar', 'baz');
        $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], $refreshedUser);

        $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser());
    }

    public function testNextSupportingUserProviderIsTriedIfPreviousSupportingUserProviderDidNotLoadTheUser()
    {
        $refreshedUser = new InMemoryUser('foobar', 'baz');
        $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], $refreshedUser);

        $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser());
    }

    public function testTokenIsSetToNullIfNoUserWasLoadedByTheRegisteredUserProviders()
    {
        $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider()]);

        $this->assertNull($tokenStorage->getToken());
    }

    public function testRuntimeExceptionIsThrownIfNoSupportingUserProviderWasRegistered()
    {
        $this->expectException(\RuntimeException::class);
        $this->handleEventWithPreviousSession([new NotSupportingUserProvider(false), new NotSupportingUserProvider(true)]);
    }

    public function testAcceptsProvidersAsTraversable()
    {
        $refreshedUser = new InMemoryUser('foobar', 'baz');
        $tokenStorage = $this->handleEventWithPreviousSession(new \ArrayObject([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)]), $refreshedUser);

        $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser());
    }

    public function testWithPreviousNotStartedSession()
    {
        $session = new Session(new MockArraySessionStorage());

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);

        $usageIndex = $session->getUsageIndex();

        $tokenStorage = new TokenStorage();
        $listener = new ContextListener($tokenStorage, [], 'context_key', null, null, null, $tokenStorage->getToken(...));
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));

        $this->assertSame($usageIndex, $session->getUsageIndex());
    }

    public function testSessionIsNotReported()
    {
        $this->expectNotToPerformAssertions();

        $usageReporter = static function (): void {
            throw new \LogicException('This should not be called');
        };

        $session = new Session(new MockArraySessionStorage(), null, null, $usageReporter);

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);

        $tokenStorage = new TokenStorage();

        $listener = new ContextListener($tokenStorage, [], 'context_key', null, null, null, $tokenStorage->getToken(...));
        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));

        $listener->onKernelResponse(new ResponseEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response()));
    }

    public function testOnKernelResponseRemoveListener()
    {
        $tokenStorage = new TokenStorage();
        $tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('test1', 'pass1'), 'phpunit', ['ROLE_USER']));

        $request = new Request();
        $request->attributes->set('_security_firewall_run', '_security_session');

        $session = new Session(new MockArraySessionStorage());
        $request->setSession($session);

        $dispatcher = new EventDispatcher();
        $httpKernel = $this->createStub(HttpKernelInterface::class);

        $listener = new ContextListener($tokenStorage, [], 'session', null, $dispatcher, null, $tokenStorage->getToken(...));
        $this->assertSame([], $dispatcher->getListeners());

        $listener->authenticate(new RequestEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST));
        $this->assertNotEmpty($dispatcher->getListeners());

        $listener->onKernelResponse(new ResponseEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response()));
        $this->assertSame([], $dispatcher->getListeners());
    }

    #[TestWith([true])]
    #[TestWith([false])]
    #[TestWith([null])]
    public function testNullOrHashedPasswordInSessionDoesntInvalidateTheToken(?bool $hashPassword)
    {
        $user = new CustomUser('user', ['ROLE_USER'], 'pass', $hashPassword);

        $userProvider = $this->createMock(UserProviderInterface::class);
        $userProvider->expects($this->once())
            ->method('supportsClass')
            ->with(CustomUser::class)
            ->willReturn(true);
        $userProvider->expects($this->once())
            ->method('refreshUser')
            ->willReturn($user);

        $tokenStorage = $this->handleEventWithPreviousSession([$userProvider], $user);

        $this->assertInstanceOf(UsernamePasswordToken::class, $tokenStorage->getToken());
        $this->assertSame($user, $tokenStorage->getToken()->getUser());
    }

    protected function runSessionOnKernelResponse($newToken, $original = null)
    {
        $session = new Session(new MockArraySessionStorage());
        $request = new Request();
        $request->attributes->set('_security_firewall_run', '_security_session');
        $request->setSession($session);
        $requestStack = new RequestStack();
        $requestStack->push($request);

        if (null !== $original) {
            $session->set('_security_session', $original);
        }

        $factories = ['request_stack' => static fn () => $requestStack];
        $tokenStorage = new UsageTrackingTokenStorage(new TokenStorage(), new class($factories) implements ContainerInterface {
            use ServiceLocatorTrait;
        });

        $tokenStorage->setToken($newToken);

        $request->cookies->set('MOCKSESSID', true);

        $sessionId = $session->getId();
        $usageIndex = $session->getUsageIndex();

        $event = new ResponseEvent(
            $this->createStub(HttpKernelInterface::class),
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new Response()
        );

        $listener = new ContextListener($tokenStorage, [], 'session', null, new EventDispatcher(), null, $tokenStorage->enableUsageTracking(...));
        $listener->onKernelResponse($event);

        if ($session->getId() === $sessionId) {
            $this->assertSame($usageIndex, $session->getUsageIndex());
        } else {
            $this->assertNotSame($usageIndex, $session->getUsageIndex());
        }

        return $session;
    }

    private function handleEventWithPreviousSession($userProviders, ?UserInterface $user = null)
    {
        $tokenUser = $user ?? new InMemoryUser('foo', 'bar');
        $session = new Session(new MockArraySessionStorage());
        $session->set('_security_context_key', serialize(new UsernamePasswordToken($tokenUser, 'context_key', ['ROLE_USER'])));

        $request = new Request();
        $request->setSession($session);
        $request->cookies->set('MOCKSESSID', true);
        $requestStack = new RequestStack();
        $requestStack->push($request);

        $tokenStorage = new TokenStorage();
        $usageIndex = $session->getUsageIndex();

        $factories = ['request_stack' => static fn () => $requestStack];
        $tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class($factories) implements ContainerInterface {
            use ServiceLocatorTrait;
        });
        $sessionTrackerEnabler = $tokenStorage->enableUsageTracking(...);

        $listener = new ContextListener($tokenStorage, $userProviders, 'context_key', null, null, null, $sessionTrackerEnabler);

        $listener->authenticate(new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));

        if (null !== $user) {
            ++$usageIndex;
        }

        $this->assertSame($usageIndex, $session->getUsageIndex());
        $tokenStorage->getToken();
        $this->assertSame(1 + $usageIndex, $session->getUsageIndex());

        return $tokenStorage;
    }
}

class NotSupportingUserProvider implements UserProviderInterface
{
    private bool $throwsUnsupportedException;

    public function __construct($throwsUnsupportedException)
    {
        $this->throwsUnsupportedException = $throwsUnsupportedException;
    }

    public function loadUserByUsername($username): UserInterface
    {
        throw new UserNotFoundException();
    }

    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        throw new UserNotFoundException();
    }

    public function refreshUser(UserInterface $user): UserInterface
    {
        if ($this->throwsUnsupportedException) {
            throw new UnsupportedUserException();
        }

        return $user;
    }

    public function supportsClass($class): bool
    {
        return false;
    }
}

class SupportingUserProvider implements UserProviderInterface
{
    private ?InMemoryUser $refreshedUser;

    public function __construct(?InMemoryUser $refreshedUser = null)
    {
        $this->refreshedUser = $refreshedUser;
    }

    public function loadUserByUsername($username): UserInterface
    {
    }

    public function loadUserByIdentifier(string $identifier): UserInterface
    {
    }

    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof InMemoryUser) {
            throw new UnsupportedUserException();
        }

        if (null === $this->refreshedUser) {
            throw new UserNotFoundException();
        }

        return $this->refreshedUser;
    }

    public function supportsClass($class): bool
    {
        return InMemoryUser::class === $class;
    }
}

class CustomToken implements TokenInterface
{
    private UserInterface $user;
    private array $roles;

    public function __construct(UserInterface $user, array $roles)
    {
        $this->user = $user;
        $this->roles = $roles;
    }

    public function __serialize(): array
    {
        return [$this->user, $this->roles];
    }

    public function __unserialize(array $data): void
    {
        [$this->user, $this->roles] = $data;
    }

    public function __toString(): string
    {
        return $this->user->getUserIdentifier();
    }

    public function getRoleNames(): array
    {
        return $this->roles;
    }

    public function getUser(): UserInterface
    {
        return $this->user;
    }

    public function setUser($user): void
    {
        $this->user = $user;
    }

    public function getUserIdentifier(): string
    {
        return $this->getUserIdentifier();
    }

    #[\Deprecated]
    public function eraseCredentials(): void
    {
    }

    public function getAttributes(): array
    {
        return [];
    }

    public function setAttributes(array $attributes): void
    {
    }

    public function hasAttribute(string $name): bool
    {
        return false;
    }

    public function getAttribute(string $name): mixed
    {
        return null;
    }

    public function setAttribute(string $name, $value): void
    {
    }
}
