diff --git a/.travis.php b/.github/travis.php similarity index 82% rename from .travis.php rename to .github/travis.php index f0ec7f3df6..6773a6edcc 100644 --- a/.travis.php +++ b/.github/travis.php @@ -1,19 +1,23 @@ $_SERVER['argc']) { - echo "Usage: branch dir1 dir2 ... dirN\n"; + echo "Usage: branch version dir1 dir2 ... dirN\n"; exit(1); } $dirs = $_SERVER['argv']; array_shift($dirs); $branch = array_shift($dirs); +$version = array_shift($dirs); $packages = array(); $flags = PHP_VERSION_ID >= 50400 ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE : 0; foreach ($dirs as $dir) { - if (!`git diff --name-only $branch...HEAD -- $dir`) { + if (!system("git diff --name-only $branch...HEAD -- $dir", $exitStatus)) { + if ($exitStatus) { + exit($exitStatus); + } continue; } echo "$dir\n"; @@ -32,7 +36,7 @@ foreach ($dirs as $dir) { file_put_contents($dir.'/composer.json', $json); passthru("cd $dir && tar -cf package.tar --exclude='package.tar' *"); - $package->version = 'master' !== $branch ? $branch.'.x-dev' : 'dev-master'; + $package->version = 'master' !== $version ? $version.'.x-dev' : 'dev-master'; $package->dist['type'] = 'tar'; $package->dist['url'] = 'file://'.__DIR__."/$dir/package.tar"; diff --git a/.travis.yml b/.travis.yml index 4d8bac1c4b..a6f39ae486 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,7 +72,7 @@ before_install: install: - if [[ ! $skip ]]; then COMPONENTS=$(find src/Symfony -mindepth 3 -type f -name phpunit.xml.dist -printf '%h\n'); fi # Create local composer packages for each patched components and reference them in composer.json files when cross-testing components - - if [[ ! $skip && $deps ]]; then php .travis.php $TRAVIS_BRANCH $COMPONENTS; fi + - if [[ ! $skip && $deps ]]; then git fetch origin $TRAVIS_BRANCH && php .github/travis.php FETCH_HEAD $TRAVIS_BRANCH $COMPONENTS; fi # For the master branch when deps=high, the version before master is checked out and tested with the locally patched components - if [[ $deps = high && $TRAVIS_BRANCH = master ]]; then SYMFONY_VERSION=$(git ls-remote --heads | grep -o '/[1-9].*' | tail -n 1 | sed s/.//); else SYMFONY_VERSION=$(cat composer.json | grep '^ *"dev-master". *"[1-9]' | grep -o '[0-9.]*'); fi - if [[ $deps = high && $TRAVIS_BRANCH = master ]]; then git fetch origin $SYMFONY_VERSION; git checkout -m FETCH_HEAD; COMPONENTS=$(find src/Symfony -mindepth 3 -type f -name phpunit.xml.dist -printf '%h\n'); ./phpunit install; fi diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociationEntity2.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociationEntity2.php new file mode 100644 index 0000000000..a0a7612458 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociationEntity2.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +class AssociationEntity2 +{ + /** + * @var int + * @ORM\Id @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\ManyToOne(targetEntity="SingleIntIdNoToStringEntity") + * + * @var \Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity + */ + public $single; + + /** + * @ORM\ManyToOne(targetEntity="CompositeIntIdEntity") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="composite_id1", referencedColumnName="id1"), + * @ORM\JoinColumn(name="composite_id2", referencedColumnName="id2") + * }) + * + * @var \Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity + */ + public $composite; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 0511f8f848..9b3e6e78e4 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -19,6 +19,8 @@ use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2; +use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; @@ -127,9 +129,11 @@ class UniqueEntityValidatorTest extends AbstractConstraintValidatorTest $schemaTool = new SchemaTool($em); $schemaTool->createSchema(array( $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'), + $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity'), $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity'), $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'), $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity'), + $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2'), )); } @@ -408,6 +412,42 @@ class UniqueEntityValidatorTest extends AbstractConstraintValidatorTest ->assertRaised(); } + public function testValidateUniquenessNotToStringEntityWithAssociatedEntity() + { + $constraint = new UniqueEntity(array( + 'message' => 'myMessage', + 'fields' => array('single'), + 'em' => self::EM_NAME, + )); + + $entity1 = new SingleIntIdNoToStringEntity(1, 'foo'); + $associated = new AssociationEntity2(); + $associated->single = $entity1; + $associated2 = new AssociationEntity2(); + $associated2->single = $entity1; + + $this->em->persist($entity1); + $this->em->persist($associated); + $this->em->flush(); + + $this->validator->validate($associated, $constraint); + + $this->assertNoViolation(); + + $this->em->persist($associated2); + $this->em->flush(); + + $this->validator->validate($associated2, $constraint); + + $expectedValue = 'Object of class "Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2" identified by "2"'; + + $this->buildViolation('myMessage') + ->atPath('property.path.single') + ->setParameter('{{ value }}', $expectedValue) + ->setInvalidValue($expectedValue) + ->assertRaised(); + } + public function testAssociatedEntityWithNull() { $constraint = new UniqueEntity(array( diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 1b16f5237e..c85c344b94 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -127,6 +127,10 @@ class UniqueEntityValidator extends ConstraintValidator $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; $invalidValue = isset($criteria[$errorPath]) ? $criteria[$errorPath] : $criteria[$fields[0]]; + if (is_object($invalidValue) && !method_exists($invalidValue, '__toString')) { + $invalidValue = sprintf('Object of class "%s" identified by "%s"', get_class($entity), implode(', ', $class->getIdentifierValues($entity))); + } + $this->context->buildViolation($constraint->message) ->atPath($errorPath) ->setParameter('{{ value }}', $invalidValue) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 9d7b893f20..5c1f78b1c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -61,5 +61,9 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 77e1e900a7..c30d1dfe9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -24,7 +24,7 @@ "symfony/config": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/http-foundation": "~3.1", - "symfony/http-kernel": "~3.1", + "symfony/http-kernel": "~3.1.2|~3.2", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index 738455358b..de157d5182 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml index 4312d74741..eb22cbddf5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml @@ -1,8 +1,8 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index ea20c7c837..40d2e04741 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=5.5.9", - "symfony/security": "~3.1", + "symfony/security": "~3.1,>=3.1.2", "symfony/http-kernel": "~2.8|~3.0", "symfony/polyfill-php70": "~1.0" }, diff --git a/src/Symfony/Component/HttpKernel/EventListener/ValidateRequestListener.php b/src/Symfony/Component/HttpKernel/EventListener/ValidateRequestListener.php new file mode 100644 index 0000000000..00096ccf9e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/ValidateRequestListener.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Validates that the headers and other information indicating the + * client IP address of a request are consistent. + * + * @author Magnus Nordlander + */ +class ValidateRequestListener implements EventSubscriberInterface +{ + /** + * Performs the validation. + * + * @param GetResponseEvent $event + */ + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + $request = $event->getRequest(); + + if ($request::getTrustedProxies()) { + // This will throw an exception if the headers are inconsistent. + $request->getClientIps(); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array( + array('onKernelRequest', 256), + ), + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 573a1e232d..c63a6af832 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -67,6 +67,9 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface try { return $this->handleRaw($request, $type); } catch (\Exception $e) { + if ($e instanceof ConflictingHeadersException) { + $e = new BadRequestHttpException('The request headers contain conflicting information regarding the origin of this request.', $e); + } if (false === $catch) { $this->finishRequest($request, $type); @@ -119,13 +122,6 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface */ private function handleRaw(Request $request, $type = self::MASTER_REQUEST) { - if (self::MASTER_REQUEST === $type && $request::getTrustedProxies()) { - try { - $request->getClientIps(); - } catch (ConflictingHeadersException $e) { - throw new BadRequestHttpException('The request headers contain conflicting information regarding the origin of this request.', $e); - } - } $this->requestStack->push($request); // request diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php new file mode 100644 index 0000000000..842a3869cb --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\EventListener; + +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; + +class ValidateRequestListenerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException + */ + public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps() + { + $dispatcher = new EventDispatcher(); + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $request = new Request(); + $request->setTrustedProxies(array('1.1.1.1')); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('FORWARDED', '2.2.2.2'); + $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); + + $dispatcher->addListener(KernelEvents::REQUEST, array(new ValidateRequestListener(), 'onKernelRequest')); + $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $dispatcher->dispatch(KernelEvents::REQUEST, $event); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 66de833045..8587aa3aae 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -307,28 +307,21 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase */ public function testInconsistentClientIpsOnMasterRequests() { - $kernel = $this->getHttpKernel(new EventDispatcher()); $request = new Request(); $request->setTrustedProxies(array('1.1.1.1')); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $request->headers->set('FORWARDED', '2.2.2.2'); $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); + $dispatcher = new EventDispatcher(); + $dispatcher->addListener(KernelEvents::REQUEST, function ($event) { + $event->getRequest()->getClientIp(); + }); + + $kernel = $this->getHttpKernel($dispatcher); $kernel->handle($request, $kernel::MASTER_REQUEST, false); } - public function testInconsistentClientIpsOnSubRequests() - { - $kernel = $this->getHttpKernel(new EventDispatcher()); - $request = new Request(); - $request->setTrustedProxies(array('1.1.1.1')); - $request->server->set('REMOTE_ADDR', '1.1.1.1'); - $request->headers->set('FORWARDED', '2.2.2.2'); - $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); - - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $kernel->handle($request, $kernel::SUB_REQUEST, false)); - } - private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, RequestStack $requestStack = null, array $arguments = array()) { if (null === $controller) { diff --git a/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php index 540d998206..1a04bc1f60 100644 --- a/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php +++ b/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php @@ -26,17 +26,19 @@ class DebugAccessDecisionManager implements AccessDecisionManagerInterface { private $manager; private $strategy; - private $voters; + private $voters = array(); private $decisionLog = array(); - public function __construct(AccessDecisionManager $manager) + public function __construct(AccessDecisionManagerInterface $manager) { $this->manager = $manager; - // The strategy is stored in a private property of the decorated service - $reflection = new \ReflectionProperty($manager, 'strategy'); - $reflection->setAccessible(true); - $this->strategy = $reflection->getValue($manager); + if ($this->manager instanceof AccessDecisionManager) { + // The strategy is stored in a private property of the decorated service + $reflection = new \ReflectionProperty(AccessDecisionManager::class, 'strategy'); + $reflection->setAccessible(true); + $this->strategy = $reflection->getValue($manager); + } } /** @@ -60,6 +62,10 @@ class DebugAccessDecisionManager implements AccessDecisionManagerInterface */ public function setVoters(array $voters) { + if (!$this->manager instanceof AccessDecisionManager) { + return; + } + $this->voters = $voters; $this->manager->setVoters($voters); } @@ -72,7 +78,7 @@ class DebugAccessDecisionManager implements AccessDecisionManagerInterface // The $strategy property is misleading because it stores the name of its // method (e.g. 'decideAffirmative') instead of the original strategy name // (e.g. 'affirmative') - return strtolower(substr($this->strategy, 6)); + return null === $this->strategy ? '-' : strtolower(substr($this->strategy, 6)); } /**