diff --git a/.travis.yml b/.travis.yml index 7b5413d8c6..85a1f7a074 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: services: mongodb before_script: - - sudo apt-get install parallel + - travis_retry sudo apt-get install parallel - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini; fi;' - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ $(php -r "echo PHP_MINOR_VERSION;") -le 4 ]; then echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' diff --git a/LICENSE b/LICENSE index 88a57f8d8d..0b3292cf90 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 Fabien Potencier +Copyright (c) 2004-2014 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 diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 7f5e323975..c1a4aa00f1 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -67,7 +67,7 @@ {% block choice_widget_collapsed %} {% spaceless %} - {% if required and empty_value is none and not empty_value_in_choices %} + {% if required and empty_value is none and not empty_value_in_choices and not multiple %} {% set required = false %} {% endif %} block($form, 'widget_attributes', array( - 'required' => $required && (null !== $empty_value || $empty_value_in_choices) + 'required' => $required )) ?> multiple="multiple" > diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php index 66a2f0abc1..3585b16a86 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php @@ -24,7 +24,7 @@ use Doctrine\DBAL\Schema\SchemaException; class InitAclCommand extends ContainerAwareCommand { /** - * @see Command + * {@inheritdoc} */ protected function configure() { @@ -47,7 +47,7 @@ EOF } /** - * @see Command::execute() + * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/src/Symfony/Component/Config/ConfigCache.php b/src/Symfony/Component/Config/ConfigCache.php index cbb1984272..73de9c2e5f 100644 --- a/src/Symfony/Component/Config/ConfigCache.php +++ b/src/Symfony/Component/Config/ConfigCache.php @@ -95,10 +95,12 @@ class ConfigCache { $mode = 0666 & ~umask(); $filesystem = new Filesystem(); - $filesystem->dumpFile($this->file, $content, $mode); + $filesystem->dumpFile($this->file, $content, null); + @chmod($this->file, $mode); if (null !== $metadata && true === $this->debug) { - $filesystem->dumpFile($this->getMetaFile(), serialize($metadata), $mode); + $filesystem->dumpFile($this->getMetaFile(), serialize($metadata), null); + @chmod($this->getMetaFile(), $mode); } } diff --git a/src/Symfony/Component/Config/Loader/DelegatingLoader.php b/src/Symfony/Component/Config/Loader/DelegatingLoader.php index 775946b77d..fa81311422 100644 --- a/src/Symfony/Component/Config/Loader/DelegatingLoader.php +++ b/src/Symfony/Component/Config/Loader/DelegatingLoader.php @@ -57,6 +57,6 @@ class DelegatingLoader extends Loader */ public function supports($resource, $type = null) { - return false === $this->resolver->resolve($resource, $type) ? false : true; + return false !== $this->resolver->resolve($resource, $type); } } diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php index be686c5cd7..1d116bc3cc 100644 --- a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php +++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php @@ -133,6 +133,45 @@ class XmlUtilsTest extends \PHPUnit_Framework_TestCase array(6, '0b0110'), ); } + + public function testLoadEmptyXmlFile() + { + $file = __DIR__.'/../Fixtures/foo.xml'; + $this->setExpectedException('InvalidArgumentException', 'File '.$file.' does not contain valid XML, it is empty.'); + XmlUtils::loadFile($file); + } + + // test for issue https://github.com/symfony/symfony/issues/9731 + public function testLoadWrongEmptyXMLWithErrorHandler() + { + $originalDisableEntities = libxml_disable_entity_loader(false); + $errorReporting = error_reporting(-1); + + set_error_handler(function ($errno, $errstr) { + throw new \Exception($errstr, $errno); + }); + + $file = __DIR__.'/../Fixtures/foo.xml'; + try { + XmlUtils::loadFile($file); + $this->fail('An exception should have been raised'); + } catch (\InvalidArgumentException $e) { + $this->assertEquals(sprintf('File %s does not contain valid XML, it is empty.', $file), $e->getMessage()); + } + + restore_error_handler(); + error_reporting($errorReporting); + + $disableEntities = libxml_disable_entity_loader(true); + libxml_disable_entity_loader($disableEntities); + + libxml_disable_entity_loader($originalDisableEntities); + + $this->assertFalse($disableEntities); + + // should not throw an exception + XmlUtils::loadFile(__DIR__.'/../Fixtures/Util/valid.xml', __DIR__.'/../Fixtures/Util/schema.xsd'); + } } interface Validator diff --git a/src/Symfony/Component/Config/Util/XmlUtils.php b/src/Symfony/Component/Config/Util/XmlUtils.php index 815fd3903e..9288e1edc3 100644 --- a/src/Symfony/Component/Config/Util/XmlUtils.php +++ b/src/Symfony/Component/Config/Util/XmlUtils.php @@ -40,13 +40,18 @@ class XmlUtils */ public static function loadFile($file, $schemaOrCallable = null) { + $content = @file_get_contents($file); + if ('' === trim($content)) { + throw new \InvalidArgumentException(sprintf('File %s does not contain valid XML, it is empty.', $file)); + } + $internalErrors = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); libxml_clear_errors(); $dom = new \DOMDocument(); $dom->validateOnParse = true; - if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { + if (!$dom->loadXML($content, LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { libxml_disable_entity_loader($disableEntities); throw new \InvalidArgumentException(implode("\n", static::getXmlErrors($internalErrors))); diff --git a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php index 7a90c3b69b..30f7189f06 100644 --- a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php +++ b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php @@ -49,7 +49,7 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase $translator->registerExtension(new HtmlExtension($translator)); $document = new \DOMDocument(); $document->strictErrorChecking = false; - libxml_use_internal_errors(true); + $internalErrors = libxml_use_internal_errors(true); $document->loadHTMLFile(__DIR__.'/Fixtures/ids.html'); $document = simplexml_import_dom($document); $elements = $document->xpath($translator->cssToXPath($css)); @@ -59,6 +59,8 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase $this->assertTrue(in_array($element->attributes()->id, $elementsId)); } } + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); } /** @dataProvider getHtmlShakespearTestData */ diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 4982f8f5d1..1f3765215c 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -151,7 +151,7 @@ class Crawler extends \SplObjectStorage */ public function addHtmlContent($content, $charset = 'UTF-8') { - $current = libxml_use_internal_errors(true); + $internalErrors = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); $dom = new \DOMDocument('1.0', $charset); @@ -171,9 +171,11 @@ class Crawler extends \SplObjectStorage } } - @$dom->loadHTML($content); + if ('' !== trim($content)) { + @$dom->loadHTML($content); + } - libxml_use_internal_errors($current); + libxml_use_internal_errors($internalErrors); libxml_disable_entity_loader($disableEntities); $this->addDocument($dom); @@ -215,14 +217,18 @@ class Crawler extends \SplObjectStorage $content = str_replace('xmlns', 'ns', $content); } - $current = libxml_use_internal_errors(true); + $internalErrors = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); $dom = new \DOMDocument('1.0', $charset); $dom->validateOnParse = true; - @$dom->loadXML($content, LIBXML_NONET); - libxml_use_internal_errors($current); + if ('' !== trim($content)) { + // remove the default namespace to make XPath expressions simpler + @$dom->loadXML(str_replace('xmlns', 'ns', $content), LIBXML_NONET); + } + + libxml_use_internal_errors($internalErrors); libxml_disable_entity_loader($disableEntities); $this->addDocument($dom); diff --git a/src/Symfony/Component/DomCrawler/Form.php b/src/Symfony/Component/DomCrawler/Form.php index 018d85a66a..ca2ca57d2c 100644 --- a/src/Symfony/Component/DomCrawler/Form.php +++ b/src/Symfony/Component/DomCrawler/Form.php @@ -143,8 +143,13 @@ class Form extends Link implements \ArrayAccess */ public function getPhpValues() { - $qs = http_build_query($this->getValues(), '', '&'); - parse_str($qs, $values); + $values = array(); + foreach ($this->getValues() as $name => $value) { + $qs = http_build_query(array($name => $value), '', '&'); + parse_str($qs, $expandedValue); + $varName = substr($name, 0, strlen(key($expandedValue))); + $values = array_replace_recursive($values, array($varName => current($expandedValue))); + } return $values; } @@ -161,8 +166,13 @@ class Form extends Link implements \ArrayAccess */ public function getPhpFiles() { - $qs = http_build_query($this->getFiles(), '', '&'); - parse_str($qs, $values); + $values = array(); + foreach ($this->getFiles() as $name => $value) { + $qs = http_build_query(array($name => $value), '', '&'); + parse_str($qs, $expandedValue); + $varName = substr($name, 0, strlen(key($expandedValue))); + $values = array_replace_recursive($values, array($varName => current($expandedValue))); + } return $values; } @@ -414,8 +424,7 @@ class Form extends Link implements \ArrayAccess // restore the original name of the input node $this->button->setAttribute('name', $name); - } - else { + } else { $this->set(new Field\InputFormField($document->importNode($this->button, true))); } } diff --git a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php index 8ffa473654..aa3082a23e 100644 --- a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php @@ -130,7 +130,7 @@ class CrawlerTest extends \PHPUnit_Framework_TestCase */ public function testAddHtmlContentWithErrors() { - libxml_use_internal_errors(true); + $internalErrors = libxml_use_internal_errors(true); $crawler = new Crawler(); $crawler->addHtmlContent(<<assertEquals("Tag nav invalid\n", $errors[0]->message); libxml_clear_errors(); - libxml_use_internal_errors(false); + libxml_use_internal_errors($internalErrors); } /** @@ -180,7 +180,7 @@ EOF */ public function testAddXmlContentWithErrors() { - libxml_use_internal_errors(true); + $internalErrors = libxml_use_internal_errors(true); $crawler = new Crawler(); $crawler->addXmlContent(<<assertTrue(count(libxml_get_errors()) > 1); libxml_clear_errors(); - libxml_use_internal_errors(false); + libxml_use_internal_errors($internalErrors); } /** diff --git a/src/Symfony/Component/DomCrawler/Tests/FormTest.php b/src/Symfony/Component/DomCrawler/Tests/FormTest.php index 6dae5e4035..765d85d132 100644 --- a/src/Symfony/Component/DomCrawler/Tests/FormTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/FormTest.php @@ -411,6 +411,12 @@ class FormTest extends \PHPUnit_Framework_TestCase { $form = $this->createForm('
'); $this->assertEquals(array('foo' => array('bar' => 'foo'), 'bar' => 'bar'), $form->getPhpValues(), '->getPhpValues() converts keys with [] to arrays'); + + $form = $this->createForm('
'); + $this->assertEquals(array('fo.o' => array('ba.r' => 'foo'), 'ba r' => 'bar'), $form->getPhpValues(), '->getPhpValues() preserves periods and spaces in names'); + + $form = $this->createForm('
'); + $this->assertEquals(array('fo.o' => array('ba.r' => array('foo', 'ba.z' => 'bar'))), $form->getPhpValues(), '->getPhpValues() preserves periods and spaces in names recursively'); } public function testGetFiles() @@ -438,6 +444,12 @@ class FormTest extends \PHPUnit_Framework_TestCase { $form = $this->createForm('
'); $this->assertEquals(array('foo' => array('bar' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0))), $form->getPhpFiles(), '->getPhpFiles() converts keys with [] to arrays'); + + $form = $this->createForm('
'); + $this->assertEquals(array('f.o o' => array('bar' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0))), $form->getPhpFiles(), '->getPhpFiles() preserves periods and spaces in names'); + + $form = $this->createForm('
'); + $this->assertEquals(array('f.o o' => array('bar' => array('ba.z' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0), array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0)))), $form->getPhpFiles(), '->getPhpFiles() preserves periods and spaces in names recursively'); } /** diff --git a/src/Symfony/Component/Filesystem/CHANGELOG.md b/src/Symfony/Component/Filesystem/CHANGELOG.md index e6aee66a57..5b5cd6a6c6 100644 --- a/src/Symfony/Component/Filesystem/CHANGELOG.md +++ b/src/Symfony/Component/Filesystem/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.3.12 +------ + + * deprecated dumpFile() file mode argument. + 2.3.0 ----- diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 7870eaa8b6..704dd444b4 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -433,10 +433,11 @@ class Filesystem /** * Atomically dumps content into a file. * - * @param string $filename The file to be written to. - * @param string $content The data to write into the file. - * @param integer $mode The file mode (octal). - * @throws IOException If the file cannot be written to. + * @param string $filename The file to be written to. + * @param string $content The data to write into the file. + * @param null|integer $mode The file mode (octal). If null, file permissions are not modified + * Deprecated since version 2.3.12, to be removed in 3.0. + * @throws IOException If the file cannot be written to. */ public function dumpFile($filename, $content, $mode = 0666) { @@ -455,7 +456,9 @@ class Filesystem } $this->rename($tmpFile, $filename, true); - $this->chmod($filename, $mode); + if (null !== $mode) { + $this->chmod($filename, $mode); + } } /** diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 44118fcdef..d5c8f7eac8 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -879,6 +879,21 @@ class FilesystemTest extends FilesystemTestCase } } + public function testDumpFileWithNullMode() + { + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + + $this->filesystem->dumpFile($filename, 'bar', null); + + $this->assertFileExists($filename); + $this->assertSame('bar', file_get_contents($filename)); + + // skip mode check on Windows + if (!defined('PHP_WINDOWS_VERSION_MAJOR')) { + $this->assertEquals(600, $this->getFilePermissions($filename)); + } + } + public function testDumpFileOverwritesAnExistingFile() { $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo.txt'; diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 7c7aa0c768..0f1437db21 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -128,11 +128,13 @@ class ObjectChoiceList extends ChoiceList if (null === $group) { $groupedChoices[$i] = $choice; } else { - if (!isset($groupedChoices[$group])) { - $groupedChoices[$group] = array(); + $groupName = (string) $group; + + if (!isset($groupedChoices[$groupName])) { + $groupedChoices[$groupName] = array(); } - $groupedChoices[$group][$i] = $choice; + $groupedChoices[$groupName][$i] = $choice; } } diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php index f1c39db245..207cbd3d38 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php @@ -139,11 +139,17 @@ class ResizeFormListener implements EventSubscriberInterface // The data mapper only adds, but does not remove items, so do this // here if ($this->allowDelete) { + $toDelete = array(); + foreach ($data as $name => $child) { if (!$form->has($name)) { - unset($data[$name]); + $toDelete[] = $name; } } + + foreach ($toDelete as $name) { + unset($data[$name]); + } } $event->setData($data); diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 28827e51b7..7aee4694e3 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -636,6 +636,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg { $form = $this->factory->createNamed('name', 'choice', array('&a'), array( 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'required' => true, 'multiple' => true, 'expanded' => false, )); @@ -643,6 +644,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg $this->assertWidgetMatchesXpath($form->createView(), array(), '/select [@name="name[]"] + [@required="required"] [@multiple="multiple"] [ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php index b70735f5d5..7bcc7f2d12 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php @@ -250,7 +250,20 @@ class ResizeFormListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(), $event->getData()); } - public function testOnSubmitDealsWithIteratorAggregate() + public function testOnSubmitDealsWithObjectBackedIteratorAggregate() + { + $this->form->add($this->getForm('1')); + + $data = new \ArrayObject(array(0 => 'first', 1 => 'second', 2 => 'third')); + $event = new FormEvent($this->form, $data); + $listener = new ResizeFormListener('text', array(), false, true); + $listener->onSubmit($event); + + $this->assertArrayNotHasKey(0, $event->getData()); + $this->assertArrayNotHasKey(2, $event->getData()); + } + + public function testOnSubmitDealsWithArrayBackedIteratorAggregate() { $this->form->add($this->getForm('1')); diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index 16c4cdb65e..6028259127 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -91,10 +91,6 @@ class UploadedFile extends File */ public function __construct($path, $originalName, $mimeType = null, $size = null, $error = null, $test = false) { - if (!ini_get('file_uploads')) { - throw new FileException(sprintf('Unable to create UploadedFile because "file_uploads" is disabled in your php.ini file (%s)', get_cfg_var('cfg_file_path'))); - } - $this->originalName = $this->getName($originalName); $this->mimeType = $mimeType ?: 'application/octet-stream'; $this->size = $size; @@ -108,7 +104,7 @@ class UploadedFile extends File * Returns the original file name. * * It is extracted from the request from which the file has been uploaded. - * Then is should not be considered as a safe value. + * Then it should not be considered as a safe value. * * @return string|null The original name * @@ -123,7 +119,7 @@ class UploadedFile extends File * Returns the original file extension * * It is extracted from the original file name that was uploaded. - * Then is should not be considered as a safe value. + * Then it should not be considered as a safe value. * * @return string The extension */ @@ -181,7 +177,7 @@ class UploadedFile extends File * Returns the file size. * * It is extracted from the request from which the file has been uploaded. - * Then is should not be considered as a safe value. + * Then it should not be considered as a safe value. * * @return integer|null The file size * diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index f80d6f03e7..3ab1b0d92a 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -308,7 +308,7 @@ class HttpCache implements HttpKernelInterface, TerminableInterface if ($this->options['allow_reload'] && $request->isNoCache()) { $this->record($request, 'reload'); - return $this->fetch($request); + return $this->fetch($request, $catch); } try { diff --git a/src/Symfony/Component/HttpKernel/KernelInterface.php b/src/Symfony/Component/HttpKernel/KernelInterface.php index 6905a1279c..8ba01b9dd0 100644 --- a/src/Symfony/Component/HttpKernel/KernelInterface.php +++ b/src/Symfony/Component/HttpKernel/KernelInterface.php @@ -27,7 +27,7 @@ use Symfony\Component\Config\Loader\LoaderInterface; interface KernelInterface extends HttpKernelInterface, \Serializable { /** - * Returns an array of bundles to registers. + * Returns an array of bundles to register. * * @return BundleInterface[] An array of bundle instances. * @@ -36,7 +36,7 @@ interface KernelInterface extends HttpKernelInterface, \Serializable public function registerBundles(); /** - * Loads the container configuration + * Loads the container configuration. * * @param LoaderInterface $loader A LoaderInterface instance * @@ -125,7 +125,7 @@ interface KernelInterface extends HttpKernelInterface, \Serializable public function locateResource($name, $dir = null, $first = true); /** - * Gets the name of the kernel + * Gets the name of the kernel. * * @return string The kernel name * diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index a2b38bd807..9e3f4a7411 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -923,6 +923,17 @@ class HttpCacheTest extends HttpCacheTestCase $this->assertExceptionsAreCaught(); } + public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest() + { + $this->catchExceptions(); + + $this->setNextResponse(); + $this->cacheConfig['allow_reload'] = true; + $this->request('GET', '/', array(), array(), false, array('Pragma' => 'no-cache')); + + $this->assertExceptionsAreCaught(); + } + public function testShouldNotCatchExceptions() { $this->catchExceptions(false); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index 9a1c7d767f..766e2b1d49 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -103,7 +103,7 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase $this->assertFalse($this->kernel->isCatchingExceptions()); } - public function request($method, $uri = '/', $server = array(), $cookies = array(), $esi = false) + public function request($method, $uri = '/', $server = array(), $cookies = array(), $esi = false, $headers = array()) { if (null === $this->kernel) { throw new \LogicException('You must call setNextResponse() before calling request().'); @@ -118,6 +118,7 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase $this->esi = $esi ? new Esi() : null; $this->cache = new HttpCache($this->kernel, $this->store, $this->esi, $this->cacheConfig); $this->request = Request::create($uri, $method, array(), $cookies, array(), $server); + $this->request->headers->add($headers); $this->response = $this->cache->handle($this->request, HttpKernelInterface::MASTER_REQUEST, $this->catch); diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index d6554baa0a..05dba32e40 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -276,7 +276,7 @@ class OptionsResolver implements OptionsResolverInterface ksort($diff); throw new MissingOptionsException(sprintf( - count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', + count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff)) )); } diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 6d8802b2de..a4bdfca90c 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -59,8 +59,8 @@ class Process private $enhanceSigchildCompatibility; private $process; private $status = self::STATUS_READY; - private $incrementalOutputOffset; - private $incrementalErrorOutputOffset; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; private $tty; private $useFileHandles = false; @@ -189,7 +189,8 @@ class Process * * @return integer The exit status code * - * @throws RuntimeException When process can't be launch or is stopped + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process stopped after receiving signal * * @api */ @@ -220,7 +221,7 @@ class Process * * @return Process The process itself * - * @throws RuntimeException When process can't be launch or is stopped + * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running */ public function start($callback = null) @@ -237,7 +238,11 @@ class Process $commandline = $this->commandline; if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) { - $commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"'; + $commandline = 'cmd /V:ON /E:ON /C "('.$commandline.')"'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' '.$offset.'>'.$filename; + } + if (!isset($this->options['bypass_shell'])) { $this->options['bypass_shell'] = true; } @@ -271,7 +276,7 @@ class Process * * @return Process The new process * - * @throws RuntimeException When process can't be launch or is stopped + * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * * @see start() @@ -301,9 +306,12 @@ class Process * * @throws RuntimeException When process timed out * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException When process is not yet started */ public function wait($callback = null) { + $this->requireProcessIsStarted(__FUNCTION__); + $this->updateStatus(false); if (null !== $callback) { $this->callback = $this->buildCallback($callback); @@ -321,10 +329,6 @@ class Process } if ($this->processInformation['signaled']) { - if ($this->isSigchildEnabled()) { - throw new RuntimeException('The process has been signaled.'); - } - throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); } @@ -353,6 +357,7 @@ class Process * Sends a POSIX signal to the process. * * @param integer $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * * @return Process * * @throws LogicException In case the process is not running @@ -361,17 +366,7 @@ class Process */ public function signal($signal) { - if (!$this->isRunning()) { - throw new LogicException('Can not send signal on a non running process.'); - } - - if ($this->isSigchildEnabled()) { - throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); - } - - if (true !== @proc_terminate($this->process, $signal)) { - throw new RuntimeException(sprintf('Error while sending signal `%d`.', $signal)); - } + $this->doSignal($signal, true); return $this; } @@ -381,10 +376,14 @@ class Process * * @return string The process output * + * @throws LogicException In case the process is not started + * * @api */ public function getOutput() { + $this->requireProcessIsStarted(__FUNCTION__); + $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true); return $this->stdout; @@ -396,10 +395,14 @@ class Process * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * + * @throws LogicException In case the process is not started + * * @return string The process output since the last call */ public function getIncrementalOutput() { + $this->requireProcessIsStarted(__FUNCTION__); + $data = $this->getOutput(); $latest = substr($data, $this->incrementalOutputOffset); @@ -426,10 +429,14 @@ class Process * * @return string The process error output * + * @throws LogicException In case the process is not started + * * @api */ public function getErrorOutput() { + $this->requireProcessIsStarted(__FUNCTION__); + $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true); return $this->stderr; @@ -442,10 +449,14 @@ class Process * whole error output, this one returns the new error output since the last * call. * + * @throws LogicException In case the process is not started + * * @return string The process error output since the last call */ public function getIncrementalErrorOutput() { + $this->requireProcessIsStarted(__FUNCTION__); + $data = $this->getErrorOutput(); $latest = substr($data, $this->incrementalErrorOutputOffset); @@ -470,7 +481,7 @@ class Process /** * Returns the exit code returned by the process. * - * @return integer The exit status code + * @return null|integer The exit status code, null if the Process is not terminated * * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled * @@ -493,14 +504,18 @@ class Process * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * - * @return string A string representation for the exit status code + * @return null|string A string representation for the exit status code, null if the Process is not terminated. + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText() { - $exitcode = $this->getExitCode(); + if (null === $exitcode = $this->getExitCode()) { + return; + } return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; } @@ -525,11 +540,14 @@ class Process * @return Boolean * * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated * * @api */ public function hasBeenSignaled() { + $this->requireProcessIsTerminated(__FUNCTION__); + if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } @@ -547,11 +565,14 @@ class Process * @return integer * * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated * * @api */ public function getTermSignal() { + $this->requireProcessIsTerminated(__FUNCTION__); + if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } @@ -568,10 +589,14 @@ class Process * * @return Boolean * + * @throws LogicException In case the process is not terminated + * * @api */ public function hasBeenStopped() { + $this->requireProcessIsTerminated(__FUNCTION__); + $this->updateStatus(false); return $this->processInformation['stopped']; @@ -584,10 +609,14 @@ class Process * * @return integer * + * @throws LogicException In case the process is not terminated + * * @api */ public function getStopSignal() { + $this->requireProcessIsTerminated(__FUNCTION__); + $this->updateStatus(false); return $this->processInformation['stopsig']; @@ -659,6 +688,12 @@ class Process { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { + if (defined('PHP_WINDOWS_VERSION_BUILD') && !$this->isSigchildEnabled()) { + exec(sprintf("taskkill /F /T /PID %d 2>&1", $this->getPid()), $output, $exitCode); + if ($exitCode > 0) { + throw new RuntimeException('Unable to kill the process'); + } + } proc_terminate($this->process); do { usleep(1000); @@ -666,7 +701,11 @@ class Process if ($this->isRunning() && !$this->isSigchildEnabled()) { if (null !== $signal || defined('SIGKILL')) { - $this->signal($signal ?: SIGKILL); + // avoid exception here : + // process is supposed to be running, but it might have stop + // just after this line. + // in any case, let's silently discard the error, we can not do anything + $this->doSignal($signal ?: SIGKILL, false); } } } @@ -676,8 +715,6 @@ class Process $this->close(); } - $this->status = self::STATUS_TERMINATED; - return $this->exitcode; } @@ -987,6 +1024,10 @@ class Process */ public function checkTimeout() { + if ($this->status !== self::STATUS_STARTED) { + return; + } + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); @@ -1053,7 +1094,7 @@ class Process /** * Updates the status of the process, reads pipes. * - * @param Boolean $blocking Whether to use a clocking read call. + * @param Boolean $blocking Whether to use a blocking read call. */ protected function updateStatus($blocking) { @@ -1068,7 +1109,6 @@ class Process if (!$this->processInformation['running']) { $this->close(); - $this->status = self::STATUS_TERMINATED; } } @@ -1153,17 +1193,17 @@ class Process */ private function close() { - $exitcode = -1; - $this->processPipes->close(); if (is_resource($this->process)) { $exitcode = proc_close($this->process); + } else { + $exitcode = -1; } - $this->exitcode = $this->exitcode !== null ? $this->exitcode : -1; - $this->exitcode = -1 != $exitcode ? $exitcode : $this->exitcode; + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; - if (-1 == $this->exitcode && null !== $this->fallbackExitcode) { + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { $this->exitcode = $this->fallbackExitcode; } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { // if process has been signaled, no exitcode but a valid termsig, apply Unix convention @@ -1190,4 +1230,73 @@ class Process $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; } + + /** + * Sends a POSIX signal to the process. + * + * @param integer $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * @param Boolean $throwException Whether to throw exception in case signal failed + * + * @return Boolean True if the signal was sent successfully, false otherwise + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + private function doSignal($signal, $throwException) + { + if (!$this->isRunning()) { + if ($throwException) { + throw new LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if (true !== @proc_terminate($this->process, $signal)) { + if ($throwException) { + throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + return true; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @param $functionName The function name that was called. + * + * @throws LogicException If the process has not run. + */ + private function requireProcessIsStarted($functionName) + { + if (!$this->isStarted()) { + throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. + * + * @param $functionName The function name that was called. + * + * @throws LogicException If the process is not yet terminated. + */ + private function requireProcessIsTerminated($functionName) + { + if (!$this->isTerminated()) { + throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } } diff --git a/src/Symfony/Component/Process/ProcessPipes.php b/src/Symfony/Component/Process/ProcessPipes.php index 67b11e615b..e6aabc5f99 100644 --- a/src/Symfony/Component/Process/ProcessPipes.php +++ b/src/Symfony/Component/Process/ProcessPipes.php @@ -21,6 +21,8 @@ class ProcessPipes /** @var array */ public $pipes = array(); /** @var array */ + private $files = array(); + /** @var array */ private $fileHandles = array(); /** @var array */ private $readBytes = array(); @@ -29,6 +31,8 @@ class ProcessPipes /** @var Boolean */ private $ttyMode; + const CHUNK_SIZE = 16384; + public function __construct($useFiles, $ttyMode) { $this->useFiles = (Boolean) $useFiles; @@ -37,20 +41,21 @@ class ProcessPipes // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. // - // Please note that this work around prevents hanging but - // another issue occurs : In some race conditions, some data may be - // lost or corrupted. - // // @see https://bugs.php.net/bug.php?id=51800 if ($this->useFiles) { - $this->fileHandles = array( - Process::STDOUT => tmpfile(), + $this->files = array( + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), ); - if (false === $this->fileHandles[Process::STDOUT]) { - throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } } $this->readBytes = array( Process::STDOUT => 0, + Process::STDERR => 0, ); } } @@ -58,6 +63,7 @@ class ProcessPipes public function __destruct() { $this->close(); + $this->removeFiles(); } /** @@ -103,11 +109,13 @@ class ProcessPipes public function getDescriptors() { if ($this->useFiles) { + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 + // So we redirect output within the commandline and pass the nul device to the process return array( array('pipe', 'r'), - $this->fileHandles[Process::STDOUT], - // Use a file handle only for STDOUT. Using for both STDOUT and STDERR would trigger https://bugs.php.net/bug.php?id=65650 - array('pipe', 'w'), + array('file', 'NUL', 'w'), + array('file', 'NUL', 'w'), ); } @@ -126,6 +134,20 @@ class ProcessPipes ); } + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return array + */ + public function getFiles() + { + if ($this->useFiles) { + return $this->files; + } + + return array(); + } + /** * Reads data in file handles and pipes. * @@ -233,7 +255,7 @@ class ProcessPipes $data = ''; $dataread = null; while (!feof($fileHandle)) { - if (false !== $dataread = fread($fileHandle, 16392)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { $data .= $dataread; } } @@ -291,7 +313,7 @@ class ProcessPipes $type = array_search($pipe, $this->pipes); $data = ''; - while ($dataread = fread($pipe, 8192)) { + while ($dataread = fread($pipe, self::CHUNK_SIZE)) { $data .= $dataread; } @@ -320,4 +342,17 @@ class ProcessPipes // stream_select returns false when the `select` system call is interrupted by an incoming signal return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); } + + /** + * Removes temporary files + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = array(); + } } diff --git a/src/Symfony/Component/Process/ProcessUtils.php b/src/Symfony/Component/Process/ProcessUtils.php index d5f5d9cb08..5317cd0883 100644 --- a/src/Symfony/Component/Process/ProcessUtils.php +++ b/src/Symfony/Component/Process/ProcessUtils.php @@ -46,22 +46,34 @@ class ProcessUtils } $escapedArgument = ''; - foreach (preg_split('/([%"])/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + $quote = false; + foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { if ('"' === $part) { $escapedArgument .= '\\"'; - } elseif ('%' === $part) { - $escapedArgument .= '^%'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; } else { + // escape trailing backslash if ('\\' === substr($part, -1)) { $part .= '\\'; } - $escapedArgument .= escapeshellarg($part); + $quote = true; + $escapedArgument .= $part; } } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } return $escapedArgument; } return escapeshellarg($argument); } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } } diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index 99239461b8..2f41d7627d 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\ProcessPipes; /** * @author Robert Schönthal @@ -88,18 +89,20 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase // has terminated so the internal pipes array is already empty. normally // the call to start() will not read any data as the process will not have // generated output, but this is non-deterministic so we must count it as - // a possibility. therefore we need 2 * 8192 plus another byte which will - // never be read. - $expectedOutputSize = 16385; + // a possibility. therefore we need 2 * ProcessPipes::CHUNK_SIZE plus + // another byte which will never be read. + $expectedOutputSize = ProcessPipes::CHUNK_SIZE * 2 + 2; $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); $p->start(); - usleep(250000); + // Let's wait enough time for process to finish... + // Here we don't call Process::run or Process::wait to avoid any read of pipes + usleep(500000); if ($p->isRunning()) { - $this->fail('Process execution did not complete in the required time frame'); + $this->markTestSkipped('Process execution did not complete in the required time frame'); } $o = $p->getOutput(); @@ -201,7 +204,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase public function testGetIncrementalErrorOutput() { - $p = $this->getProcess(sprintf('php -r %s', escapeshellarg('$n = 0; while ($n < 3) { usleep(50000); file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + $p = $this->getProcess(sprintf('php -r %s', escapeshellarg('$n = 0; while ($n < 3) { usleep(100000); file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); $p->start(); while ($p->isRunning()) { @@ -266,7 +269,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('Windows does have /dev/tty support'); } - $process = $this->getProcess('echo "foo" >> /dev/null'); + $process = $this->getProcess('echo "foo" >> /dev/null && php -r "usleep(100000);"'); $process->setTTY(true); $process->start(); $this->assertTrue($process->isRunning()); @@ -288,6 +291,12 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertTrue($process->isSuccessful()); } + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + $process = $this->getProcess(''); + $this->assertNull($process->getExitCodeText()); + } + public function testExitCodeText() { $process = $this->getProcess(''); @@ -487,7 +496,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $process1->run(); $process2 = $process1->restart(); - usleep(300000); // wait for output + $process2->wait(); // wait for output // Ensure that both processed finished and the output is numeric $this->assertFalse($process1->isRunning()); @@ -528,6 +537,19 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertLessThan($timeout + Process::TIMEOUT_PRECISION, $duration); } + public function testCheckTimeoutOnNonStartedProcess() + { + $process = $this->getProcess('php -r "sleep(3);"'); + $process->checkTimeout(); + } + + public function testCheckTimeoutOnTerminatedProcess() + { + $process = $this->getProcess('php -v'); + $process->run(); + $process->checkTimeout(); + } + public function testCheckTimeoutOnStartedProcess() { $timeout = 0.5; @@ -672,6 +694,55 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $process->signal(SIGHUP); } + /** + * @dataProvider provideMethodsThatNeedARunningProcess + */ + public function testMethodsThatNeedARunningProcess($method) + { + $process = $this->getProcess('php -m'); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method)); + call_user_func(array($process, $method)); + } + + public function provideMethodsThatNeedARunningProcess() + { + return array( + array('getOutput'), + array('getIncrementalOutput'), + array('getErrorOutput'), + array('getIncrementalErrorOutput'), + array('wait'), + ); + } + + /** + * @dataProvider provideMethodsThatNeedATerminatedProcess + */ + public function testMethodsThatNeedATerminatedProcess($method) + { + $process = $this->getProcess('php -r "sleep(1);"'); + $process->start(); + try { + call_user_func(array($process, $method)); + $process->stop(0); + $this->fail('A LogicException must have been thrown'); + } catch (\Exception $e) { + $this->assertInstanceOf('Symfony\Component\Process\Exception\LogicException', $e); + $this->assertEquals(sprintf('Process must be terminated before calling %s.', $method), $e->getMessage()); + } + $process->stop(0); + } + + public function provideMethodsThatNeedATerminatedProcess() + { + return array( + array('hasBeenSignaled'), + array('getTermSignal'), + array('hasBeenStopped'), + array('getStopSignal'), + ); + } + private function verifyPosixIsEnabled() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { diff --git a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php index 2982aff9f9..ee14fa9cbc 100644 --- a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php @@ -138,13 +138,13 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase public function testShouldEscapeArguments() { - $pb = new ProcessBuilder(array('%path%', 'foo " bar')); + $pb = new ProcessBuilder(array('%path%', 'foo " bar', '%baz%baz')); $proc = $pb->getProcess(); if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->assertSame('^%"path"^% "foo "\\"" bar"', $proc->getCommandLine()); + $this->assertSame('^%"path"^% "foo \\" bar" "%baz%baz"', $proc->getCommandLine()); } else { - $this->assertSame("'%path%' 'foo \" bar'", $proc->getCommandLine()); + $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); } } diff --git a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php index b9e8b0c76a..8ba94c114d 100644 --- a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php @@ -27,15 +27,17 @@ class ProcessUtilsTest extends \PHPUnit_Framework_TestCase { if (defined('PHP_WINDOWS_VERSION_BUILD')) { return array( + array('"\"php\" \"-v\""', '"php" "-v"'), array('"foo bar"', 'foo bar'), array('^%"path"^%', '%path%'), - array('"<|>"\\"" "\\""\'f"', '<|>" "\'f'), + array('"<|>\\" \\"\'f"', '<|>" "\'f'), array('""', ''), array('"with\trailingbs\\\\"', 'with\trailingbs\\'), ); } return array( + array("'\"php\" \"-v\"'", '"php" "-v"'), array("'foo bar'", 'foo bar'), array("'%path%'", '%path%'), array("'<|>\" \"'\\''f'", '<|>" "\'f'), diff --git a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php index 8580649b29..798e66a571 100644 --- a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php @@ -133,6 +133,15 @@ class SigchildDisabledProcessTest extends AbstractProcessTest $process->getExitCodeText(); } + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + parent::testExitCodeTextIsNullWhenExitCodeIsNull(); + } + /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. diff --git a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php index 524cc66374..65dd4bb573 100644 --- a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php @@ -112,6 +112,14 @@ class SigchildEnabledProcessTest extends AbstractProcessTest $this->markTestSkipped('Signal is not supported in sigchild environment'); } + public function testStartAfterATimeout() + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->markTestSkipped('Restarting a timed-out process on Windows is not supported in sigchild environment'); + } + parent::testStartAfterATimeout(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Process/Tests/SimpleProcessTest.php b/src/Symfony/Component/Process/Tests/SimpleProcessTest.php index 6655acf4ec..69ad3d5b09 100644 --- a/src/Symfony/Component/Process/Tests/SimpleProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SimpleProcessTest.php @@ -27,106 +27,123 @@ class SimpleProcessTest extends AbstractProcessTest public function testGetExitCode() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case parent::testGetExitCode(); } public function testExitCodeCommandFailed() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case parent::testExitCodeCommandFailed(); } public function testProcessIsSignaledIfStopped() { - $this->skipIfPHPSigchild(); + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); parent::testProcessIsSignaledIfStopped(); } public function testProcessWithTermSignal() { - $this->skipIfPHPSigchild(); + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); parent::testProcessWithTermSignal(); } public function testProcessIsNotSignaled() { - $this->skipIfPHPSigchild(); + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); parent::testProcessIsNotSignaled(); } public function testProcessWithoutTermSignal() { - $this->skipIfPHPSigchild(); + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); parent::testProcessWithoutTermSignal(); } public function testExitCodeText() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case parent::testExitCodeText(); } public function testIsSuccessful() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case parent::testIsSuccessful(); } public function testIsNotSuccessful() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case parent::testIsNotSuccessful(); } public function testGetPid() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case parent::testGetPid(); } public function testGetPidIsNullBeforeStart() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case parent::testGetPidIsNullBeforeStart(); } public function testGetPidIsNullAfterRun() { - $this->skipIfPHPSigchild(); + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case parent::testGetPidIsNullAfterRun(); } public function testSignal() { - $this->skipIfPHPSigchild(); + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); parent::testSignal(); } - /** - * @expectedException \Symfony\Component\Process\Exception\LogicException - */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testProcessThrowsExceptionWhenExternallySignaled(); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + parent::testExitCodeIsAvailableAfterSignal(); + } + public function testSignalProcessNotRunning() { - $this->skipIfPHPSigchild(); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Can not send signal on a non running process.'); parent::testSignalProcessNotRunning(); } - /** - * @expectedException \Symfony\Component\Process\Exception\RuntimeException - */ public function testSignalWithWrongIntSignal() { - $this->skipIfPHPSigchild(); + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `-4`.'); + } parent::testSignalWithWrongIntSignal(); } - /** - * @expectedException \Symfony\Component\Process\Exception\RuntimeException - */ public function testSignalWithWrongNonIntSignal() { - $this->skipIfPHPSigchild(); + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `Céphalopodes`.'); + } parent::testSignalWithWrongNonIntSignal(); } @@ -144,4 +161,11 @@ class SimpleProcessTest extends AbstractProcessTest $this->markTestSkipped('Your PHP has been compiled with --enable-sigchild, this test can not be executed'); } } + + private function expectExceptionIfPHPSigchild($classname, $message) + { + if ($this->enabledSigchild) { + $this->setExpectedException($classname, $message); + } + } } diff --git a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php index dd7a7d5547..0c084b9ea1 100644 --- a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php @@ -18,9 +18,6 @@ use Symfony\Component\Security\Http\HttpUtils; /** * Class with the default authentication success handling logic. * - * Can be optionally be extended from by the developer to alter the behaviour - * while keeping the default behaviour. - * * @author Fabien Potencier * @author Johannes M. Schmitt * @author Alexander diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 9cd2417b46..dd6c5f86ad 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -68,6 +68,10 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec */ public function decode($data, $format, array $context = array()) { + if ('' === trim($data)) { + throw new UnexpectedValueException('Invalid XML data, it can not be empty.'); + } + $internalErrors = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); libxml_clear_errors(); @@ -79,6 +83,8 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec libxml_disable_entity_loader($disableEntities); if ($error = libxml_get_last_error()) { + libxml_clear_errors(); + throw new UnexpectedValueException($error->message); } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 6ad2a6c2e2..6dcdf69be7 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -347,6 +347,12 @@ class XmlEncoderTest extends \PHPUnit_Framework_TestCase } } + public function testDecodeEmptyXml() + { + $this->setExpectedException('Symfony\Component\Serializer\Exception\UnexpectedValueException', 'Invalid XML data, it can not be empty.'); + $this->encoder->decode(' ', 'xml'); + } + protected function getXmlSource() { return ''."\n". diff --git a/src/Symfony/Component/Translation/Loader/QtFileLoader.php b/src/Symfony/Component/Translation/Loader/QtFileLoader.php index dbb56daec6..aacfb4a55e 100644 --- a/src/Symfony/Component/Translation/Loader/QtFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/QtFileLoader.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Translation\Loader; +use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; @@ -40,12 +41,15 @@ class QtFileLoader implements LoaderInterface throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } - $dom = new \DOMDocument(); - $current = libxml_use_internal_errors(true); - if (!@$dom->load($resource, defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0)) { - throw new InvalidResourceException(implode("\n", $this->getXmlErrors())); + try { + $dom = XmlUtils::loadFile($resource); + } catch (\InvalidArgumentException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e); } + $internalErrors = libxml_use_internal_errors(true); + libxml_clear_errors(); + $xpath = new \DOMXPath($dom); $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]'); @@ -67,33 +71,8 @@ class QtFileLoader implements LoaderInterface $catalogue->addResource(new FileResource($resource)); } - libxml_use_internal_errors($current); + libxml_use_internal_errors($internalErrors); return $catalogue; } - - /** - * Returns the XML errors of the internal XML parser - * - * @return array An array of errors - */ - private function getXmlErrors() - { - $errors = array(); - foreach (libxml_get_errors() as $error) { - $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', - LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', - $error->code, - trim($error->message), - $error->file ? $error->file : 'n/a', - $error->line, - $error->column - ); - } - - libxml_clear_errors(); - libxml_use_internal_errors(false); - - return $errors; - } } diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index 0cafc043ac..f46b5dfee4 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Translation\Loader; +use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; @@ -86,27 +87,13 @@ class XliffFileLoader implements LoaderInterface */ private function parseFile($file) { + try { + $dom = XmlUtils::loadFile($file); + } catch (\InvalidArgumentException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $file, $e->getMessage()), $e->getCode(), $e); + } + $internalErrors = libxml_use_internal_errors(true); - $disableEntities = libxml_disable_entity_loader(true); - libxml_clear_errors(); - - $dom = new \DOMDocument(); - $dom->validateOnParse = true; - if (!@$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { - libxml_disable_entity_loader($disableEntities); - - throw new InvalidResourceException(implode("\n", $this->getXmlErrors($internalErrors))); - } - - libxml_disable_entity_loader($disableEntities); - - foreach ($dom->childNodes as $child) { - if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { - libxml_use_internal_errors($internalErrors); - - throw new InvalidResourceException('Document types are not allowed.'); - } - } $location = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd'; $parts = explode('/', $location); diff --git a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php index 71338fdd0f..3aca86a53e 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php @@ -56,4 +56,12 @@ class QtFileLoaderTest extends \PHPUnit_Framework_TestCase $resource = __DIR__.'/../fixtures/invalid-xml-resources.xlf'; $loader->load($resource, 'en', 'domain1'); } + + public function testLoadEmptyResource() + { + $loader = new QtFileLoader(); + $resource = __DIR__.'/../fixtures/empty.xlf'; + $this->setExpectedException('Symfony\Component\Translation\Exception\InvalidResourceException', sprintf('Unable to load "%s".', $resource)); + $loader->load($resource, 'en', 'domain1'); + } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index da54169ef8..49a6265dc2 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -103,4 +103,12 @@ class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase $loader = new XliffFileLoader(); $loader->load(__DIR__.'/../fixtures/withdoctype.xlf', 'en', 'domain1'); } + + public function testParseEmptyFile() + { + $loader = new XliffFileLoader(); + $resource = __DIR__.'/../fixtures/empty.xlf'; + $this->setExpectedException('Symfony\Component\Translation\Exception\InvalidResourceException', sprintf('Unable to load "%s":', $resource)); + $loader->load($resource, 'en', 'domain1'); + } } diff --git a/src/Symfony/Component/Translation/Tests/fixtures/empty.xlf b/src/Symfony/Component/Translation/Tests/fixtures/empty.xlf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Symfony/Component/Validator/Constraints/IbanValidator.php b/src/Symfony/Component/Validator/Constraints/IbanValidator.php index 3ec4c6ea7a..d26363f37b 100644 --- a/src/Symfony/Component/Validator/Constraints/IbanValidator.php +++ b/src/Symfony/Component/Validator/Constraints/IbanValidator.php @@ -31,7 +31,7 @@ class IbanValidator extends ConstraintValidator } // An IBAN without a country code is not an IBAN. - if (0 === preg_match('/[A-Za-z]/', $value)) { + if (0 === preg_match('/[A-Z]/', $value)) { $this->context->addViolation($constraint->message, array('{{ value }}' => $value)); return; @@ -50,7 +50,7 @@ class IbanValidator extends ConstraintValidator .strval(ord($teststring{1}) - 55) .substr($teststring, 2, 2); - $teststring = preg_replace_callback('/[A-Za-z]/', function ($letter) { + $teststring = preg_replace_callback('/[A-Z]/', function ($letter) { return intval(ord(strtolower($letter[0])) - 87); }, $teststring); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php index d4add7930c..60ba94c32c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php @@ -182,6 +182,10 @@ class IbanValidatorTest extends \PHPUnit_Framework_TestCase array('foo'), array('123'), array('0750447346'), + + //Ibans with lower case values are invalid + array('Ae260211000000230064016'), + array('ae260211000000230064016') ); } }