N°8771 - Add Symfony form component to iTop core (#760)

- Add Symfony Form Component
- Add Symfony CSRF security component
- Add iTop default form template
- Add Twig debug extension to Twig Environment
- Add iTop abstract controller facility to get form builder
- Add Twig filter to make trans an alias of dict_s filter
This commit is contained in:
Benjamin Dalsass
2025-10-10 16:02:25 +02:00
committed by GitHub
parent 82395727bf
commit 5dd450e9bf
605 changed files with 60106 additions and 12 deletions

View File

@@ -0,0 +1,13 @@
CHANGELOG
=========
6.0
---
* Remove the `SessionInterface $session` constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
* Using `SessionTokenStorage` outside a request context throws a `SessionNotFoundException`
5.3
---
The CHANGELOG for version 5.3 and earlier can be found at https://github.com/symfony/symfony/blob/5.3/src/Symfony/Component/Security/CHANGELOG.md

View File

@@ -0,0 +1,53 @@
<?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\Csrf;
/**
* A CSRF token.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfToken
{
private string $id;
private string $value;
public function __construct(string $id, #[\SensitiveParameter] ?string $value)
{
$this->id = $id;
$this->value = $value ?? '';
}
/**
* Returns the ID of the CSRF token.
*/
public function getId(): string
{
return $this->id;
}
/**
* Returns the value of the CSRF token.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Returns the value of the CSRF token.
*/
public function __toString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,141 @@
<?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\Csrf;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
/**
* Default implementation of {@link CsrfTokenManagerInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CsrfTokenManager implements CsrfTokenManagerInterface
{
private TokenGeneratorInterface $generator;
private TokenStorageInterface $storage;
private \Closure|string $namespace;
/**
* @param $namespace
* * null: generates a namespace using $_SERVER['HTTPS']
* * string: uses the given string
* * RequestStack: generates a namespace using the current main request
* * callable: uses the result of this callable (must return a string)
*/
public function __construct(?TokenGeneratorInterface $generator = null, ?TokenStorageInterface $storage = null, string|RequestStack|callable|null $namespace = null)
{
$this->generator = $generator ?? new UriSafeTokenGenerator();
$this->storage = $storage ?? new NativeSessionTokenStorage();
$superGlobalNamespaceGenerator = fn () => !empty($_SERVER['HTTPS']) && 'off' !== strtolower($_SERVER['HTTPS']) ? 'https-' : '';
if (null === $namespace) {
$this->namespace = $superGlobalNamespaceGenerator;
} elseif ($namespace instanceof RequestStack) {
$this->namespace = function () use ($namespace, $superGlobalNamespaceGenerator) {
if ($request = $namespace->getMainRequest()) {
return $request->isSecure() ? 'https-' : '';
}
return $superGlobalNamespaceGenerator();
};
} elseif ($namespace instanceof \Closure || \is_string($namespace)) {
$this->namespace = $namespace;
} elseif (\is_callable($namespace)) {
$this->namespace = $namespace(...);
} else {
throw new InvalidArgumentException(\sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', get_debug_type($namespace)));
}
}
public function getToken(string $tokenId): CsrfToken
{
$namespacedId = $this->getNamespace().$tokenId;
if ($this->storage->hasToken($namespacedId)) {
$value = $this->storage->getToken($namespacedId);
} else {
$value = $this->generator->generateToken();
$this->storage->setToken($namespacedId, $value);
}
return new CsrfToken($tokenId, $this->randomize($value));
}
public function refreshToken(string $tokenId): CsrfToken
{
$namespacedId = $this->getNamespace().$tokenId;
$value = $this->generator->generateToken();
$this->storage->setToken($namespacedId, $value);
return new CsrfToken($tokenId, $this->randomize($value));
}
public function removeToken(string $tokenId): ?string
{
return $this->storage->removeToken($this->getNamespace().$tokenId);
}
public function isTokenValid(CsrfToken $token): bool
{
$namespacedId = $this->getNamespace().$token->getId();
if (!$this->storage->hasToken($namespacedId)) {
return false;
}
return hash_equals($this->storage->getToken($namespacedId), $this->derandomize($token->getValue()));
}
private function getNamespace(): string
{
return \is_callable($ns = $this->namespace) ? $ns() : $ns;
}
private function randomize(string $value): string
{
$key = random_bytes(32);
$value = $this->xor($value, $key);
return \sprintf('%s.%s.%s', substr(hash('xxh128', $key), 0, 1 + (\ord($key[0]) % 32)), rtrim(strtr(base64_encode($key), '+/', '-_'), '='), rtrim(strtr(base64_encode($value), '+/', '-_'), '='));
}
private function derandomize(string $value): string
{
$parts = explode('.', $value);
if (3 !== \count($parts)) {
return $value;
}
$key = base64_decode(strtr($parts[1], '-_', '+/'));
if ('' === $key || false === $key) {
return $value;
}
$value = base64_decode(strtr($parts[2], '-_', '+/'));
return $this->xor($value, $key);
}
private function xor(string $value, string $key): string
{
if (\strlen($value) > \strlen($key)) {
$key = str_repeat($key, ceil(\strlen($value) / \strlen($key)));
}
return $value ^ $key;
}
}

View File

@@ -0,0 +1,57 @@
<?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\Csrf;
/**
* Manages CSRF tokens.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface CsrfTokenManagerInterface
{
/**
* Returns a CSRF token for the given ID.
*
* If previously no token existed for the given ID, a new token is
* generated. Otherwise the existing token is returned (with the same value,
* not the same instance).
*
* @param string $tokenId The token ID. You may choose an arbitrary value
* for the ID
*/
public function getToken(string $tokenId): CsrfToken;
/**
* Generates a new token value for the given ID.
*
* This method will generate a new token for the given token ID, independent
* of whether a token value previously existed or not. It can be used to
* enforce once-only tokens in environments with high security needs.
*
* @param string $tokenId The token ID. You may choose an arbitrary value
* for the ID
*/
public function refreshToken(string $tokenId): CsrfToken;
/**
* Invalidates the CSRF token with the given ID, if one exists.
*
* @return string|null Returns the removed token value if one existed, NULL
* otherwise
*/
public function removeToken(string $tokenId): ?string;
/**
* Returns whether the given CSRF token is valid.
*/
public function isTokenValid(CsrfToken $token): bool;
}

View File

@@ -0,0 +1,21 @@
<?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\Csrf\Exception;
use Symfony\Component\Security\Core\Exception\RuntimeException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TokenNotFoundException extends RuntimeException
{
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,29 @@
Security Component - CSRF
=========================
The Security CSRF (cross-site request forgery) component provides a class
`CsrfTokenManager` for generating and validating CSRF tokens.
Sponsor
-------
The Security component for Symfony 6.4 is [backed][1] by [SymfonyCasts][2].
Learn Symfony faster by watching real projects being built and actively coding
along with them. SymfonyCasts bridges that learning gap, bringing you video
tutorials and coding challenges. Code on!
Help Symfony by [sponsoring][3] its development!
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/security.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://symfonycasts.com
[3]: https://symfony.com/sponsor

View File

@@ -0,0 +1,25 @@
<?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\Csrf\TokenGenerator;
/**
* Generates CSRF tokens.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TokenGeneratorInterface
{
/**
* Generates a CSRF token.
*/
public function generateToken(): string;
}

View File

@@ -0,0 +1,46 @@
<?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\Csrf\TokenGenerator;
/**
* Generates CSRF tokens.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class UriSafeTokenGenerator implements TokenGeneratorInterface
{
private int $entropy;
/**
* Generates URI-safe CSRF tokens.
*
* @param int $entropy The amount of entropy collected for each token (in bits)
*/
public function __construct(int $entropy = 256)
{
if ($entropy <= 7) {
throw new \InvalidArgumentException('Entropy should be greater than 7.');
}
$this->entropy = $entropy;
}
public function generateToken(): string
{
// Generate an URI safe base64 encoded string that does not contain "+",
// "/" or "=" which need to be URL encoded and make URLs unnecessarily
// longer.
$bytes = random_bytes(intdiv($this->entropy, 8));
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
}

View File

@@ -0,0 +1,25 @@
<?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\Csrf\TokenStorage;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
interface ClearableTokenStorageInterface extends TokenStorageInterface
{
/**
* Removes all CSRF tokens.
*
* @return void
*/
public function clear();
}

View File

@@ -0,0 +1,112 @@
<?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\Csrf\TokenStorage;
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
/**
* Token storage that uses PHP's native session handling.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeSessionTokenStorage implements ClearableTokenStorageInterface
{
/**
* The namespace used to store values in the session.
*/
public const SESSION_NAMESPACE = '_csrf';
private bool $sessionStarted = false;
private string $namespace;
/**
* Initializes the storage with a session namespace.
*
* @param string $namespace The namespace under which the token is stored in the session
*/
public function __construct(string $namespace = self::SESSION_NAMESPACE)
{
$this->namespace = $namespace;
}
public function getToken(string $tokenId): string
{
if (!$this->sessionStarted) {
$this->startSession();
}
if (!isset($_SESSION[$this->namespace][$tokenId])) {
throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.');
}
return (string) $_SESSION[$this->namespace][$tokenId];
}
/**
* @return void
*/
public function setToken(string $tokenId, #[\SensitiveParameter] string $token)
{
if (!$this->sessionStarted) {
$this->startSession();
}
$_SESSION[$this->namespace][$tokenId] = $token;
}
public function hasToken(string $tokenId): bool
{
if (!$this->sessionStarted) {
$this->startSession();
}
return isset($_SESSION[$this->namespace][$tokenId]);
}
public function removeToken(string $tokenId): ?string
{
if (!$this->sessionStarted) {
$this->startSession();
}
if (!isset($_SESSION[$this->namespace][$tokenId])) {
return null;
}
$token = (string) $_SESSION[$this->namespace][$tokenId];
unset($_SESSION[$this->namespace][$tokenId]);
if (!$_SESSION[$this->namespace]) {
unset($_SESSION[$this->namespace]);
}
return $token;
}
/**
* @return void
*/
public function clear()
{
unset($_SESSION[$this->namespace]);
}
private function startSession(): void
{
if (\PHP_SESSION_NONE === session_status()) {
session_start();
}
$this->sessionStarted = true;
}
}

View File

@@ -0,0 +1,112 @@
<?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\Csrf\TokenStorage;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
/**
* Token storage that uses a Symfony Session object.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorage implements ClearableTokenStorageInterface
{
/**
* The namespace used to store values in the session.
*/
public const SESSION_NAMESPACE = '_csrf';
private RequestStack $requestStack;
private string $namespace;
/**
* Initializes the storage with a RequestStack object and a session namespace.
*
* @param string $namespace The namespace under which the token is stored in the requestStack
*/
public function __construct(RequestStack $requestStack, string $namespace = self::SESSION_NAMESPACE)
{
$this->requestStack = $requestStack;
$this->namespace = $namespace;
}
public function getToken(string $tokenId): string
{
$session = $this->getSession();
if (!$session->isStarted()) {
$session->start();
}
if (!$session->has($this->namespace.'/'.$tokenId)) {
throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.');
}
return (string) $session->get($this->namespace.'/'.$tokenId);
}
/**
* @return void
*/
public function setToken(string $tokenId, #[\SensitiveParameter] string $token)
{
$session = $this->getSession();
if (!$session->isStarted()) {
$session->start();
}
$session->set($this->namespace.'/'.$tokenId, $token);
}
public function hasToken(string $tokenId): bool
{
$session = $this->getSession();
if (!$session->isStarted()) {
$session->start();
}
return $session->has($this->namespace.'/'.$tokenId);
}
public function removeToken(string $tokenId): ?string
{
$session = $this->getSession();
if (!$session->isStarted()) {
$session->start();
}
return $session->remove($this->namespace.'/'.$tokenId);
}
/**
* @return void
*/
public function clear()
{
$session = $this->getSession();
foreach (array_keys($session->all()) as $key) {
if (str_starts_with($key, $this->namespace.'/')) {
$session->remove($key);
}
}
}
/**
* @throws SessionNotFoundException
*/
private function getSession(): SessionInterface
{
return $this->requestStack->getSession();
}
}

View File

@@ -0,0 +1,47 @@
<?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\Csrf\TokenStorage;
/**
* Stores CSRF tokens.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TokenStorageInterface
{
/**
* Reads a stored CSRF token.
*
* @throws \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException If the token ID does not exist
*/
public function getToken(string $tokenId): string;
/**
* Stores a CSRF token.
*
* @return void
*/
public function setToken(string $tokenId, #[\SensitiveParameter] string $token);
/**
* Removes a CSRF token.
*
* @return string|null Returns the removed token if one existed, NULL
* otherwise
*/
public function removeToken(string $tokenId): ?string;
/**
* Checks whether a token with the given token ID exists.
*/
public function hasToken(string $tokenId): bool;
}

View File

@@ -0,0 +1,35 @@
{
"name": "symfony/security-csrf",
"type": "library",
"description": "Symfony Security Component - CSRF Library",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1",
"symfony/security-core": "^5.4|^6.0|^7.0"
},
"require-dev": {
"symfony/http-foundation": "^5.4|^6.0|^7.0"
},
"conflict": {
"symfony/http-foundation": "<5.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Security\\Csrf\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}