Merge branch '2.2'

* 2.2:
  [HttpFoundation] fixed Request::create() method
  [HttpKernel] fixed the creation of the Profiler directory
  [HttpKernel] fixed the hinclude fragment renderer when the template is empty
  bumped Symfony version to 2.2.0-RC2-DEV
  [DependencyInjection] enhanced some error messages
  [FrameworkBundle] fixed typo
  fixed typo
  tweaked previous merge
  [Security] fixed interface implementation (closes #6974)
  Add "'property_path' => false" deprecation message for forms
  fixed CS
  Added BCrypt password encoder.
  updated VERSION for 2.2.0-RC1
  Removed underscores from test method names to be consistent with other components.
  [Security] fixed session creation when none is needed (closes #6917)
  [FrameworkBundle] removed obsolete comment (see 2e356c1)
  Micro-optimization
  [FrameworkBundle] removed extra whitespaces
  [Security] renamed Constraint namespace to Constraints for validator classes in order to be consistent with the whole current validator API.
  [FrameworkBundle] fixed wrong indentation on route debug output
This commit is contained in:
Fabien Potencier 2013-02-07 17:43:41 +01:00
commit 8df773201a
35 changed files with 577 additions and 122 deletions

View File

@ -567,6 +567,55 @@
trusted_proxies: ['127.0.0.1', '10.0.0.1'] # a list of proxy IPs you trust
```
### Security
* The existing ``UserPassword`` validator constraint class has been modified.
Its namespace has been changed to better fit the Symfony coding conventions.
Before:
```
use Symfony\Component\Security\Core\Validator\Constraint\UserPassword;
```
After: (note the `s` at the end of `Constraint`)
```
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
```
* The new ``UserPassword`` validator constraint class now accepts a new
``service`` option that allows to specify a custom validator service name in
order to validate the current logged-in user's password.
```
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
$constraint = new UserPassword(array(
'service' => 'my.custom.validator.user_password',
));
```
#### Deprecations
* The two previous ``UserPassword`` and ``UserPasswordValidator`` classes in
the ``Symfony\Component\Security\Core\Validator\Constraint`` namespace have
been deprecated and will be removed in 2.3.
Before:
```
use Symfony\Component\Security\Core\Validator\Constraint\UserPassword;
use Symfony\Component\Security\Core\Validator\Constraint\UserPasswordValidator;
```
After:
```
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator;
```
### Serializer
* All serializer interfaces (Serializer, Normalizer, Encoder) have been

View File

@ -128,10 +128,10 @@ EOF
$output->writeln($this->getHelper('formatter')->formatSection('router', sprintf('Route "%s"', $name)));
$output->writeln(sprintf('<comment>Name</comment> %s', $name));
$output->writeln(sprintf('<comment>Pattern</comment> %s', $route->getPath()));
$output->writeln(sprintf('<comment>Host</comment> %s', $host));
$output->writeln(sprintf('<comment>Class</comment> %s', get_class($route)));
$output->writeln(sprintf('<comment>Name</comment> %s', $name));
$output->writeln(sprintf('<comment>Pattern</comment> %s', $route->getPath()));
$output->writeln(sprintf('<comment>Host</comment> %s', $host));
$output->writeln(sprintf('<comment>Class</comment> %s', get_class($route)));
$defaults = '';
$d = $route->getDefaults();
@ -139,7 +139,7 @@ EOF
foreach ($d as $name => $value) {
$defaults .= ($defaults ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($value);
}
$output->writeln(sprintf('<comment>Defaults</comment> %s', $defaults));
$output->writeln(sprintf('<comment>Defaults</comment> %s', $defaults));
$requirements = '';
$r = $route->getRequirements();
@ -148,7 +148,7 @@ EOF
$requirements .= ($requirements ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($value);
}
$requirements = '' !== $requirements ? $requirements : 'NONE';
$output->writeln(sprintf('<comment>Requirements</comment> %s', $requirements));
$output->writeln(sprintf('<comment>Requirements</comment> %s', $requirements));
$options = '';
$o = $route->getOptions();
@ -156,8 +156,8 @@ EOF
foreach ($o as $name => $value) {
$options .= ($options ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($value);
}
$output->writeln(sprintf('<comment>Options</comment> %s', $options));
$output->write('<comment>Regex</comment> ');
$output->writeln(sprintf('<comment>Options</comment> %s', $options));
$output->write('<comment>Regex</comment> ');
$output->writeln(preg_replace('/^ /', '', preg_replace('/^/m', ' ', $route->compile()->getRegex())), OutputInterface::OUTPUT_RAW);
}

View File

@ -21,9 +21,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Sets the session in the request.
*
* This will also start the session if it was already started during a previous
* request.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class SessionListener implements EventSubscriberInterface

View File

@ -67,11 +67,11 @@ class HttpKernel extends ContainerAwareHttpKernel
* @throws \RuntimeException
* @throws \Exception
*
* @deprecated in 2.2, will be removed in 2.3 (use Symfony\Component\HttpKernel\FragmentRenderer::render() instead)
* @deprecated in 2.2, will be removed in 2.3 (use Symfony\Component\HttpKernel\Fragment\FragmentHandler::render() instead)
*/
public function render($uri, array $options = array())
{
trigger_error('render() is deprecated since version 2.2 and will be removed in 2.3. Use Symfony\Component\HttpKernel\FragmentRenderer::render() instead.', E_USER_DEPRECATED);
trigger_error('render() is deprecated since version 2.2 and will be removed in 2.3. Use Symfony\Component\HttpKernel\Fragment\FragmentHandler::render() instead.', E_USER_DEPRECATED);
$options = $this->renderer->fixOptions($options);

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* Added PBKDF2 Password encoder
* Added BCrypt password encoder
2.1.0
-----

View File

@ -383,6 +383,11 @@ class MainConfiguration implements ConfigurationInterface
->booleanNode('ignore_case')->defaultFalse()->end()
->booleanNode('encode_as_base64')->defaultTrue()->end()
->scalarNode('iterations')->defaultValue(5000)->end()
->integerNode('cost')
->min(4)
->max(31)
->defaultValue(13)
->end()
->scalarNode('id')->end()
->end()
->end()

View File

@ -13,6 +13,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -464,6 +465,19 @@ class SecurityExtension extends Extension
);
}
// bcrypt encoder
if ('bcrypt' === $config['algorithm']) {
$arguments = array(
new Reference('security.secure_random'),
$config['cost'],
);
return array(
'class' => new Parameter('security.encoder.bcrypt.class'),
'arguments' => $arguments,
);
}
// message digest encoder
$arguments = array(
$config['algorithm'],

View File

@ -13,6 +13,7 @@
<parameter key="security.encoder.digest.class">Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder</parameter>
<parameter key="security.encoder.plain.class">Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder</parameter>
<parameter key="security.encoder.pbkdf2.class">Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder</parameter>
<parameter key="security.encoder.bcrypt.class">Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder</parameter>
<parameter key="security.user.provider.in_memory.class">Symfony\Component\Security\Core\User\InMemoryUserProvider</parameter>
<parameter key="security.user.provider.in_memory.user.class">Symfony\Component\Security\Core\User\User</parameter>
@ -41,7 +42,7 @@
<parameter key="security.http_utils.class">Symfony\Component\Security\Http\HttpUtils</parameter>
<parameter key="security.validator.user_password.class">Symfony\Component\Security\Core\Validator\Constraint\UserPasswordValidator</parameter>
<parameter key="security.validator.user_password.class">Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator</parameter>
</parameters>
<services>

View File

@ -22,6 +22,10 @@ $container->loadFromExtension('security', array(
'iterations' => 5,
'key_length' => 30,
),
'JMS\FooBundle\Entity\User6' => array(
'algorithm' => 'bcrypt',
'cost' => 15,
),
),
'providers' => array(
'default' => array(

View File

@ -18,6 +18,8 @@
<encoder class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
<encoder class="JMS\FooBundle\Entity\User6" algorithm="bcrypt" cost="15" />
<provider name="default">
<memory>
<user name="foo" password="foo" roles="ROLE_USER" />

View File

@ -16,6 +16,9 @@ security:
encode_as_base64: false
iterations: 5
key_length: 30
JMS\FooBundle\Entity\User6:
algorithm: bcrypt
cost: 15
providers:
default:

View File

@ -158,6 +158,13 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase
'class' => new Parameter('security.encoder.pbkdf2.class'),
'arguments' => array('sha1', false, 5, 30),
),
'JMS\FooBundle\Entity\User6' => array(
'class' => new Parameter('security.encoder.bcrypt.class'),
'arguments' => array(
new Reference('security.secure_random'),
15,
)
),
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
}

View File

@ -192,14 +192,14 @@ class Container implements IntrospectableContainerInterface
public function set($id, $service, $scope = self::SCOPE_CONTAINER)
{
if (self::SCOPE_PROTOTYPE === $scope) {
throw new InvalidArgumentException('You cannot set services of scope "prototype".');
throw new InvalidArgumentException(sprintf('You cannot set service "%s" of scope "prototype".', $id));
}
$id = strtolower($id);
if (self::SCOPE_CONTAINER !== $scope) {
if (!isset($this->scopedServices[$scope])) {
throw new RuntimeException('You cannot set services of inactive scopes.');
throw new RuntimeException(sprintf('You cannot set service "%s" of inactive scope.', $id));
}
$this->scopedServices[$scope][$id] = $service;

View File

@ -857,7 +857,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
} elseif (null !== $definition->getFactoryService()) {
$factory = $this->get($parameterBag->resolveValue($definition->getFactoryService()));
} else {
throw new RuntimeException('Cannot create service from factory method without a factory service or factory class.');
throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $id));
}
$service = call_user_func_array(array($factory, $definition->getFactoryMethod()), $arguments);
@ -869,7 +869,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
throw new RuntimeException('You tried to create a service of an inactive scope.');
throw new RuntimeException(sprintf('You tried to create the "%s" service of an inactive scope.', $id));
}
$this->services[$lowerId = strtolower($id)] = $service;

View File

@ -746,7 +746,7 @@ EOF;
{
\$name = strtolower(\$name);
if (!array_key_exists(\$name, \$this->parameters)) {
if (!(isset(\$this->parameters[\$name]) || array_key_exists(\$name, \$this->parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', \$name));
}
@ -758,7 +758,9 @@ EOF;
*/
public function hasParameter(\$name)
{
return array_key_exists(strtolower(\$name), \$this->parameters);
\$name = strtolower(\$name);
return isset(\$this->parameters[\$name]) || array_key_exists(\$name, \$this->parameters);
}
/**

View File

@ -55,7 +55,7 @@ class ProjectServiceContainer extends Container
{
$name = strtolower($name);
if (!array_key_exists($name, $this->parameters)) {
if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));
}
@ -67,7 +67,9 @@ class ProjectServiceContainer extends Container
*/
public function hasParameter($name)
{
return array_key_exists(strtolower($name), $this->parameters);
$name = strtolower($name);
return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters);
}
/**

View File

@ -191,7 +191,7 @@ class ProjectServiceContainer extends Container
{
$name = strtolower($name);
if (!array_key_exists($name, $this->parameters)) {
if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));
}
@ -203,7 +203,9 @@ class ProjectServiceContainer extends Container
*/
public function hasParameter($name)
{
return array_key_exists(strtolower($name), $this->parameters);
$name = strtolower($name);
return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters);
}
/**

View File

@ -56,6 +56,10 @@ class FormType extends AbstractType
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
;
if (false === $options['property_path']) {
trigger_error('Setting "property_path" to "false" is deprecated since version 2.1 and will be removed in 2.3. Set "mapped" to "false" instead.', E_USER_DEPRECATED);
}
if ($options['trim']) {
$builder->addEventSubscriber(new TrimListener());
}

View File

@ -4,6 +4,7 @@ CHANGELOG
2.2.0
-----
* fixed the Request::create() precedence (URI information always take precedence now)
* added Request::getTrustedProxies()
* deprecated Request::isProxyTrusted()
* [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects

View File

@ -253,6 +253,9 @@ class Request
/**
* Creates a Request based on a given URI and configuration.
*
* The information contained in the URI always take precedence
* over the other information (server and parameters).
*
* @param string $uri The URI
* @param string $method The HTTP method
* @param array $parameters The query (GET) or request (POST) parameters
@ -267,7 +270,7 @@ class Request
*/
public static function create($uri, $method = 'GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null)
{
$defaults = array(
$server = array_replace(array(
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => 80,
'HTTP_HOST' => 'localhost',
@ -280,32 +283,38 @@ class Request
'SCRIPT_FILENAME' => '',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'REQUEST_TIME' => time(),
);
), $server);
$server['PATH_INFO'] = '';
$server['REQUEST_METHOD'] = strtoupper($method);
$components = parse_url($uri);
if (isset($components['host'])) {
$defaults['SERVER_NAME'] = $components['host'];
$defaults['HTTP_HOST'] = $components['host'];
$server['SERVER_NAME'] = $components['host'];
$server['HTTP_HOST'] = $components['host'];
}
if (isset($components['scheme'])) {
if ('https' === $components['scheme']) {
$defaults['HTTPS'] = 'on';
$defaults['SERVER_PORT'] = 443;
$server['HTTPS'] = 'on';
$server['SERVER_PORT'] = 443;
} else {
unset($server['HTTPS']);
$server['SERVER_PORT'] = 80;
}
}
if (isset($components['port'])) {
$defaults['SERVER_PORT'] = $components['port'];
$defaults['HTTP_HOST'] = $defaults['HTTP_HOST'].':'.$components['port'];
$server['SERVER_PORT'] = $components['port'];
$server['HTTP_HOST'] = $server['HTTP_HOST'].':'.$components['port'];
}
if (isset($components['user'])) {
$defaults['PHP_AUTH_USER'] = $components['user'];
$server['PHP_AUTH_USER'] = $components['user'];
}
if (isset($components['pass'])) {
$defaults['PHP_AUTH_PW'] = $components['pass'];
$server['PHP_AUTH_PW'] = $components['pass'];
}
if (!isset($components['path'])) {
@ -316,7 +325,7 @@ class Request
case 'POST':
case 'PUT':
case 'DELETE':
$defaults['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
case 'PATCH':
$request = $parameters;
$query = array();
@ -333,14 +342,8 @@ class Request
}
$queryString = http_build_query($query, '', '&');
$uri = $components['path'].('' !== $queryString ? '?'.$queryString : '');
$server = array_replace($defaults, $server, array(
'REQUEST_METHOD' => strtoupper($method),
'PATH_INFO' => '',
'REQUEST_URI' => $uri,
'QUERY_STRING' => $queryString,
));
$server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : '');
$server['QUERY_STRING'] = $queryString;
return new static($query, $request, array(), $cookies, $files, $server, $content);
}

View File

@ -222,6 +222,41 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($request->isSecure());
}
/**
* @covers Symfony\Component\HttpFoundation\Request::create
*/
public function testCreateCheckPrecedence()
{
// server is used by default
$request = Request::create('/', 'GET', array(), array(), array(), array(
'HTTP_HOST' => 'example.com',
'HTTPS' => 'on',
'SERVER_PORT' => 443,
'PHP_AUTH_USER' => 'fabien',
'PHP_AUTH_PW' => 'pa$$',
'QUERY_STRING' => 'foo=bar',
));
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(443, $request->getPort());
$this->assertTrue($request->isSecure());
$this->assertEquals('fabien', $request->getUser());
$this->assertEquals('pa$$', $request->getPassword());
$this->assertEquals('', $request->getQueryString());
// URI has precedence over server
$request = Request::create('http://thomas:pokemon@example.net:8080/?foo=bar', 'GET', array(), array(), array(), array(
'HTTP_HOST' => 'example.com',
'HTTPS' => 'on',
'SERVER_PORT' => 443,
));
$this->assertEquals('example.net', $request->getHost());
$this->assertEquals(8080, $request->getPort());
$this->assertFalse($request->isSecure());
$this->assertEquals('thomas', $request->getUser());
$this->assertEquals('pokemon', $request->getPassword());
$this->assertEquals('foo=bar', $request->getQueryString());
}
/**
* @covers Symfony\Component\HttpFoundation\Request::duplicate
*/

View File

@ -65,7 +65,7 @@ class HIncludeFragmentRenderer extends RoutableFragmentRenderer
}
$template = isset($options['default']) ? $options['default'] : $this->globalDefaultTemplate;
if (null !== $this->templating && $this->templateExists($template)) {
if (null !== $this->templating && $template && $this->templateExists($template)) {
$content = $this->templating->render($template);
} else {
$content = $template;

View File

@ -62,12 +62,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface
protected $classes;
protected $errorReportingLevel;
const VERSION = '2.2.0-DEV';
const VERSION = '2.2.0-RC2';
const VERSION_ID = '20100';
const MAJOR_VERSION = '2';
const MINOR_VERSION = '2';
const RELEASE_VERSION = '0';
const EXTRA_VERSION = 'DEV';
const EXTRA_VERSION = 'RC2';
/**
* Constructor.

View File

@ -41,7 +41,7 @@ class FileProfilerStorage implements ProfilerStorageInterface
$this->folder = substr($dsn, 5);
if (!is_dir($this->folder)) {
mkdir($this->folder);
mkdir($this->folder, 0777, true);
}
}

View File

@ -27,7 +27,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_noDotBeforeProperty()
public function testDotIsRequiredBeforeProperty()
{
new PropertyPath('[index]property');
}
@ -35,7 +35,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_dotAtTheBeginning()
public function testDotCannotBePresentAtTheBeginning()
{
new PropertyPath('.property');
}
@ -43,7 +43,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_unexpectedCharacters()
public function testUnexpectedCharacters()
{
new PropertyPath('property.$foo');
}
@ -51,7 +51,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_empty()
public function testPathCannotBeEmpty()
{
new PropertyPath('');
}
@ -59,7 +59,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_null()
public function testPathCannotBeNull()
{
new PropertyPath(null);
}
@ -67,31 +67,31 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_false()
public function testPathCannotBeFalse()
{
new PropertyPath(false);
}
public function testValidPropertyPath_zero()
public function testZeroIsValidPropertyPath()
{
new PropertyPath('0');
}
public function testGetParent_dot()
public function testGetParentWithDot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_index()
public function testGetParentWithIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_noParent()
public function testGetParentWhenThereIsNoParent()
{
$propertyPath = new PropertyPath('path');

View File

@ -9,6 +9,7 @@ CHANGELOG
implements EventSubscriberInterface
* added secure random number generator
* added PBKDF2 Password encoder
* added BCrypt password encoder
2.1.0
-----

View File

@ -0,0 +1,148 @@
<?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\Core\Encoder;
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
/**
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
* @author Terje Bråten <terje@braten.be>
*/
class BCryptPasswordEncoder extends BasePasswordEncoder
{
/**
* @var SecureRandomInterface
*/
private $secureRandom;
/**
* @var string
*/
private $cost;
private static $prefix = null;
/**
* Constructor.
*
* @param SecureRandomInterface $secureRandom A SecureRandomInterface instance
* @param integer $cost The algorithmic cost that should be used
*
* @throws \InvalidArgumentException if cost is out of range
*/
public function __construct(SecureRandomInterface $secureRandom, $cost)
{
$this->secureRandom = $secureRandom;
$cost = (int) $cost;
if ($cost < 4 || $cost > 31) {
throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
}
$this->cost = sprintf('%02d', $cost);
if (!self::$prefix) {
self::$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') ? '2y' : '2a').'$';
}
}
/**
* {@inheritdoc}
*/
public function encodePassword($raw, $salt)
{
if (function_exists('password_hash')) {
return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost));
}
$salt = self::$prefix.$this->cost.'$'.$this->encodeSalt($this->getRawSalt());
$encoded = crypt($raw, $salt);
if (!is_string($encoded) || strlen($encoded) <= 13) {
return false;
}
return $encoded;
}
/**
* {@inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
if (function_exists('password_verify')) {
return password_verify($raw, $encoded);
}
$crypted = crypt($raw, $encoded);
if (strlen($crypted) <= 13) {
return false;
}
return $this->comparePasswords($encoded, $crypted);
}
/**
* Encodes the salt to be used by Bcrypt.
*
* The blowfish/bcrypt algorithm used by PHP crypt expects a different
* set and order of characters than the usual base64_encode function.
* Regular b64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
* Bcrypt b64: ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* We care because the last character in our encoded string will
* only represent 2 bits. While two known implementations of
* bcrypt will happily accept and correct a salt string which
* has the 4 unused bits set to non-zero, we do not want to take
* chances and we also do not want to waste an additional byte
* of entropy.
*
* @param bytes $random a string of 16 random bytes
*
* @return string Properly encoded salt to use with php crypt function
*
* @throws \InvalidArgumentException if string of random bytes is too short
*/
protected function encodeSalt($random)
{
$len = strlen($random);
if ($len < 16) {
throw new \InvalidArgumentException('The bcrypt salt needs 16 random bytes.');
}
if ($len > 16) {
$random = substr($random, 0, 16);
}
$base64raw = str_replace('+', '.', base64_encode($random));
$salt128bit = substr($base64raw, 0, 21);
$lastchar = substr($base64raw, 21, 1);
$lastchar = strtr($lastchar, 'AQgw', '.Oeu');
$salt128bit .= $lastchar;
return $salt128bit;
}
/**
* @return bytes 16 random bytes to be used in the salt
*/
protected function getRawSalt()
{
$rawSalt = false;
$numBytes = 16;
if (function_exists('mcrypt_create_iv')) {
$rawSalt = mcrypt_create_iv($numBytes, MCRYPT_DEV_URANDOM);
}
if (!$rawSalt) {
$rawSalt = $this->secureRandom->nextBytes($numBytes);
}
return $rawSalt;
}
}

View File

@ -11,18 +11,19 @@
namespace Symfony\Component\Security\Core\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword as BaseUserPassword;
/**
* @Annotation
*
* @deprecated Deprecated since version 2.2, to be removed in 2.3.
*/
class UserPassword extends Constraint
class UserPassword extends BaseUserPassword
{
public $message = 'This value should be the user current password.';
public $service = 'security.validator.user_password';
public function validatedBy()
public function __construct($options = null)
{
return $this->service;
trigger_error('UserPassword class in Symfony\Component\Security\Core\Validator\Constraint namespace is deprecated since version 2.2 and will be removed in 2.3. Use the Symfony\Component\Security\Core\Validator\Constraints\UserPassword class instead.', E_USER_DEPRECATED);
parent::__construct($options);
}
}

View File

@ -11,36 +11,19 @@
namespace Symfony\Component\Security\Core\Validator\Constraint;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator as BaseUserPasswordValidator;
class UserPasswordValidator extends ConstraintValidator
/**
* @deprecated Deprecated since version 2.2, to be removed in 2.3.
*/
class UserPasswordValidator extends BaseUserPasswordValidator
{
private $securityContext;
private $encoderFactory;
public function __construct(SecurityContextInterface $securityContext, EncoderFactoryInterface $encoderFactory)
{
$this->securityContext = $securityContext;
$this->encoderFactory = $encoderFactory;
}
trigger_error('UserPasswordValidator class in Symfony\Component\Security\Core\Validator\Constraint namespace is deprecated since version 2.2 and will be removed in 2.3. Use the Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator class instead.', E_USER_DEPRECATED);
public function validate($password, Constraint $constraint)
{
$user = $this->securityContext->getToken()->getUser();
if (!$user instanceof UserInterface) {
throw new ConstraintDefinitionException('The User must extend UserInterface');
}
$encoder = $this->encoderFactory->getEncoder($user);
if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
$this->context->addViolation($constraint->message);
}
parent::__construct($securityContext, $encoderFactory);
}
}

View File

@ -0,0 +1,28 @@
<?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\Core\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class UserPassword extends Constraint
{
public $message = 'This value should be the user current password.';
public $service = 'security.validator.user_password';
public function validatedBy()
{
return $this->service;
}
}

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\Core\Validator\Constraints;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
class UserPasswordValidator extends ConstraintValidator
{
private $securityContext;
private $encoderFactory;
public function __construct(SecurityContextInterface $securityContext, EncoderFactoryInterface $encoderFactory)
{
$this->securityContext = $securityContext;
$this->encoderFactory = $encoderFactory;
}
public function validate($password, Constraint $constraint)
{
$user = $this->securityContext->getToken()->getUser();
if (!$user instanceof UserInterface) {
throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
}
$encoder = $this->encoderFactory->getEncoder($user);
if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
$this->context->addViolation($constraint->message);
}
}
}

View File

@ -70,7 +70,6 @@ class ContextListener implements ListenerInterface
}
$request = $event->getRequest();
$session = $request->hasPreviousSession() ? $request->getSession() : null;
if (null === $session || null === $token = $session->get('_security_'.$this->contextKey)) {
@ -117,7 +116,10 @@ class ContextListener implements ListenerInterface
$this->logger->debug('Write SecurityContext in the session');
}
if (null === $session = $event->getRequest()->getSession()) {
$request = $event->getRequest();
$session = $request->hasPreviousSession() ? $request->getSession() : null;
if (null === $session) {
return;
}

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\Tests\Core\Encoder;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
/**
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
*/
class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
{
const PASSWORD = 'password';
const BYTES = '0123456789abcdef';
const VALID_COST = '04';
const SECURE_RANDOM_INTERFACE = 'Symfony\Component\Security\Core\Util\SecureRandomInterface';
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $secureRandom;
protected function setUp()
{
$this->secureRandom = $this->getMock(self::SECURE_RANDOM_INTERFACE);
$this->secureRandom
->expects($this->any())
->method('nextBytes')
->will($this->returnValue(self::BYTES))
;
}
/**
* @expectedException \InvalidArgumentException
*/
public function testCostBelowRange()
{
new BCryptPasswordEncoder($this->secureRandom, 3);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testCostAboveRange()
{
new BCryptPasswordEncoder($this->secureRandom, 32);
}
public function testCostInRange()
{
for ($cost = 4; $cost <= 31; $cost++) {
new BCryptPasswordEncoder($this->secureRandom, $cost);
}
}
public function testResultLength()
{
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
$result = $encoder->encodePassword(self::PASSWORD, null);
$this->assertEquals(60, strlen($result));
}
public function testValidation()
{
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
$result = $encoder->encodePassword(self::PASSWORD, null);
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
}
public function testValidationKnownPassword()
{
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
? '2y' : '2a').'$';
$encrypted = $prefix.'04$ABCDEFGHIJKLMNOPQRSTU.uTmwd4KMSHxbUsG7bng8x7YdA0PM1iq';
$this->assertTrue($encoder->isPasswordValid($encrypted, self::PASSWORD, null));
}
public function testSecureRandomIsUsed()
{
if (function_exists('mcrypt_create_iv')) {
return;
}
$this->secureRandom
->expects($this->atLeastOnce())
->method('nextBytes')
;
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
$result = $encoder->encodePassword(self::PASSWORD, null);
$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
? '2y' : '2a').'$';
$salt = 'MDEyMzQ1Njc4OWFiY2RlZe';
$expected = crypt(self::PASSWORD, $prefix . self::VALID_COST . '$' . $salt);
$this->assertEquals($expected, $result);
}
}

View File

@ -9,10 +9,10 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Tests\Core\Validator\Constraint;
namespace Symfony\Component\Security\Tests\Core\Validator\Constraints;
use Symfony\Component\Security\Core\Validator\Constraint\UserPassword;
use Symfony\Component\Security\Core\Validator\Constraint\UserPasswordValidator;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator;
class UserPasswordValidatorTest extends \PHPUnit_Framework_TestCase
{

View File

@ -82,17 +82,11 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($session->has('_security_session'));
}
protected function runSessionOnKernelResponse($newToken, $original = null)
public function testOnKernelResponseWithoutSession()
{
$session = new Session(new MockArraySessionStorage());
if ($original !== null) {
$session->set('_security_session', $original);
}
$this->securityContext->setToken($newToken);
$this->securityContext->setToken(new UsernamePasswordToken('test1', 'pass1', 'phpunit'));
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$event = new FilterResponseEvent(
@ -105,25 +99,7 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
$listener = new ContextListener($this->securityContext, array(), 'session');
$listener->onKernelResponse($event);
return $session;
}
public function testOnKernelResponseWithoutSession()
{
$this->securityContext->setToken(new UsernamePasswordToken('test1', 'pass1', 'phpunit'));
$request = new Request();
$event = new FilterResponseEvent(
$this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'),
$request,
HttpKernelInterface::MASTER_REQUEST,
new Response()
);
$listener = new ContextListener($this->securityContext, array(), 'session');
$listener->onKernelResponse($event);
$this->assertFalse($request->hasSession());
$this->assertFalse($session->isStarted());
}
/**
@ -168,4 +144,30 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
array(null),
);
}
}
protected function runSessionOnKernelResponse($newToken, $original = null)
{
$session = new Session(new MockArraySessionStorage());
if ($original !== null) {
$session->set('_security_session', $original);
}
$this->securityContext->setToken($newToken);
$request = new Request();
$request->setSession($session);
$request->cookies->set('MOCKSESSID', true);
$event = new FilterResponseEvent(
$this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'),
$request,
HttpKernelInterface::MASTER_REQUEST,
new Response()
);
$listener = new ContextListener($this->securityContext, array(), 'session');
$listener->onKernelResponse($event);
return $session;
}}