Merge branch '2.8' into 3.1

* 2.8:
  [Routing] Add missing options in docblock
  [VarDumper] Fix dumping continuations
  [HttpFoundation] fixed Request::getContent() reusage bug
  [Form] Skip CSRF validation on form when POST max size is exceeded
  Enhance the phpDoc return types so IDEs can handle the configuration tree.
  fixes
  Remove 3.0 from branch suggestions for fixes in PR template
  [Process] Strengthen Windows pipe files opening (again...)
  Fix #19531 [Form] DateType fails parsing when midnight is not a valid time
This commit is contained in:
Fabien Potencier 2016-08-16 07:58:24 -07:00
commit d7f8ca72e8
20 changed files with 191 additions and 59 deletions

View File

@ -1,6 +1,6 @@
| Q | A
| ------------- | ---
| Branch? | "master" for new features / 2.7, 2.8, 3.0 or 3.1 for fixes
| Branch? | "master" for new features / 2.7, 2.8 or 3.1 for fixes
| Bug fix? | yes/no
| New feature? | yes/no
| BC breaks? | yes/no

View File

@ -12,6 +12,7 @@
<argument>%form.type_extension.csrf.field_name%</argument>
<argument type="service" id="translator.default" />
<argument>%validator.translation_domain%</argument>
<argument type="service" id="form.server_params" />
</service>
</services>
</container>

View File

@ -138,7 +138,7 @@ class NodeBuilder implements NodeParentInterface
/**
* Returns the parent node.
*
* @return ParentNodeDefinitionInterface The parent node
* @return ParentNodeDefinitionInterface|NodeDefinition The parent node
*/
public function end()
{

View File

@ -107,7 +107,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Returns the parent node.
*
* @return NodeParentInterface|null The builder of the parent node
* @return NodeParentInterface|NodeBuilder|NodeDefinition|null The builder of the parent node
*/
public function end()
{

View File

@ -116,20 +116,29 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
return;
}
$timestamp = $this->getIntlDateFormatter()->parse($value);
// date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
// to DST changes
$dateOnly = $this->isPatternDateOnly();
$timestamp = $this->getIntlDateFormatter($dateOnly)->parse($value);
if (intl_get_error_code() != 0) {
throw new TransformationFailedException(intl_get_error_message());
}
try {
// read timestamp into DateTime object - the formatter delivers in UTC
$dateTime = new \DateTime(sprintf('@%s', $timestamp));
if ($dateOnly) {
// we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight
return new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->inputTimezone));
}
// read timestamp into DateTime object - the formatter delivers a timestamp
$dateTime = new \DateTime(sprintf('@%s', $timestamp), new \DateTimeZone($this->outputTimezone));
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
if ('UTC' !== $this->inputTimezone) {
if ($this->outputTimezone !== $this->inputTimezone) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
@ -139,15 +148,17 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
/**
* Returns a preconfigured IntlDateFormatter instance.
*
* @param bool $ignoreTimezone Use UTC regardless of the configured timezone.
*
* @return \IntlDateFormatter
*
* @throws TransformationFailedException in case the date formatter can not be constructed.
*/
protected function getIntlDateFormatter()
protected function getIntlDateFormatter($ignoreTimezone = false)
{
$dateFormat = $this->dateFormat;
$timeFormat = $this->timeFormat;
$timezone = $this->outputTimezone;
$timezone = $ignoreTimezone ? 'UTC' : $this->outputTimezone;
$calendar = $this->calendar;
$pattern = $this->pattern;
@ -162,4 +173,24 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
return $intlDateFormatter;
}
/**
* Checks if the pattern contains only a date.
*
* @param string $pattern The input pattern
*
* @return bool
*/
protected function isPatternDateOnly()
{
if (null === $this->pattern) {
return false;
}
// strip escaped text
$pattern = preg_replace("#'(.*?)'#", '', $this->pattern);
// check for the absence of time-related placeholders
return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
}
}

View File

@ -15,6 +15,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
@ -65,6 +66,11 @@ class CsrfValidationListener implements EventSubscriberInterface
*/
private $translationDomain;
/**
* @var ServerParams
*/
private $serverParams;
public static function getSubscribedEvents()
{
return array(
@ -72,7 +78,7 @@ class CsrfValidationListener implements EventSubscriberInterface
);
}
public function __construct($fieldName, CsrfTokenManagerInterface $tokenManager, $tokenId, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null)
public function __construct($fieldName, CsrfTokenManagerInterface $tokenManager, $tokenId, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null, ServerParams $serverParams = null)
{
$this->fieldName = $fieldName;
$this->tokenManager = $tokenManager;
@ -80,13 +86,15 @@ class CsrfValidationListener implements EventSubscriberInterface
$this->errorMessage = $errorMessage;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams ?: new ServerParams();
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$postRequestSizeExceeded = $form->getConfig()->getMethod() === 'POST' && $this->serverParams->hasPostMaxSizeBeenExceeded();
if ($form->isRoot() && $form->getConfig()->getOption('compound')) {
if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
$data = $event->getData();
if (!isset($data[$this->fieldName]) || !$this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName]))) {

View File

@ -16,6 +16,7 @@ use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
@ -50,13 +51,19 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
*/
private $translationDomain;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null)
/**
* @var ServerParams
*/
private $serverParams;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null, ServerParams $serverParams = null)
{
$this->defaultTokenManager = $defaultTokenManager;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams;
}
/**
@ -78,7 +85,8 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
$options['csrf_token_id'] ?: ($builder->getName() ?: get_class($builder->getType()->getInnerType())),
$options['csrf_message'],
$this->translator,
$this->translationDomain
$this->translationDomain,
$this->serverParams
))
;
}

View File

@ -73,10 +73,7 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
$contentLength = $this->serverParams->getContentLength();
$maxContentLength = $this->serverParams->getPostMaxSize();
if (!empty($maxContentLength) && $contentLength > $maxContentLength) {
if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
// Submit the form, but don't clear the default values
$form->submit(null, false);

View File

@ -81,10 +81,7 @@ class NativeRequestHandler implements RequestHandlerInterface
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
$contentLength = $this->serverParams->getContentLength();
$maxContentLength = $this->serverParams->getPostMaxSize();
if (!empty($maxContentLength) && $contentLength > $maxContentLength) {
if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
// Submit the form, but don't clear the default values
$form->submit(null, false);

View File

@ -227,6 +227,26 @@ class DateTimeToLocalizedStringTransformerTest extends DateTimeTestCase
$this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('02*2010*03 04|05|06'));
}
public function testReverseTransformDateOnlyWithDstIssue()
{
$transformer = new DateTimeToLocalizedStringTransformer('Europe/Rome', 'Europe/Rome', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, \IntlDateFormatter::GREGORIAN, 'dd/MM/yyyy');
$this->assertDateTimeEquals(
new \DateTime('1978-05-28', new \DateTimeZone('Europe/Rome')),
$transformer->reverseTransform('28/05/1978')
);
}
public function testReverseTransformDateOnlyWithDstIssueAndEscapedText()
{
$transformer = new DateTimeToLocalizedStringTransformer('Europe/Rome', 'Europe/Rome', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, \IntlDateFormatter::GREGORIAN, "'day': dd 'month': MM 'year': yyyy");
$this->assertDateTimeEquals(
new \DateTime('1978-05-28', new \DateTimeZone('Europe/Rome')),
$transformer->reverseTransform('day: 28 month: 05 year: 1978')
);
}
public function testReverseTransformEmpty()
{
$transformer = new DateTimeToLocalizedStringTransformer();

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Csrf\EventListener;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
@ -72,4 +73,25 @@ class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase
// Validate accordingly
$this->assertSame($data, $event->getData());
}
public function testMaxPostSizeExceeded()
{
$serverParams = $this
->getMockBuilder('\Symfony\Component\Form\Util\ServerParams')
->disableOriginalConstructor()
->getMock()
;
$serverParams
->expects($this->once())
->method('hasPostMaxSizeBeenExceeded')
->willReturn(true)
;
$event = new FormEvent($this->form, array('csrf' => 'token'));
$validation = new CsrfValidationListener('csrf', $this->tokenManager, 'unknown', 'Error message', null, null, $serverParams);
$validation->preSubmit($event);
$this->assertEmpty($this->form->getErrors());
}
}

View File

@ -25,6 +25,19 @@ class ServerParams
$this->requestStack = $requestStack;
}
/**
* Returns true if the POST max size has been exceeded in the request.
*
* @return bool
*/
public function hasPostMaxSizeBeenExceeded()
{
$contentLength = $this->getContentLength();
$maxContentLength = $this->getPostMaxSize();
return $maxContentLength && $contentLength > $maxContentLength;
}
/**
* Returns maximum post size in bytes.
*

View File

@ -1525,7 +1525,7 @@ class Request
return stream_get_contents($this->content);
}
if (null === $this->content) {
if (null === $this->content || false === $this->content) {
$this->content = file_get_contents('php://input');
}

View File

@ -1059,8 +1059,16 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$req->getContent($second);
}
public function getContentCantBeCalledTwiceWithResourcesProvider()
{
return array(
'Resource then fetch' => array(true, false),
'Resource then resource' => array(true, true),
);
}
/**
* @dataProvider getContentCantBeCalledTwiceWithResourcesProvider
* @dataProvider getContentCanBeCalledTwiceWithResourcesProvider
* @requires PHP 5.6
*/
public function testGetContentCanBeCalledTwiceWithResources($first, $second)
@ -1077,12 +1085,14 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$b = stream_get_contents($b);
}
$this->assertEquals($a, $b);
$this->assertSame($a, $b);
}
public function getContentCantBeCalledTwiceWithResourcesProvider()
public function getContentCanBeCalledTwiceWithResourcesProvider()
{
return array(
'Fetch then fetch' => array(false, false),
'Fetch then resource' => array(false, true),
'Resource then fetch' => array(true, false),
'Resource then resource' => array(true, true),
);

View File

@ -201,9 +201,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
foreach ($this->data as $dump) {
$dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth));
rewind($data);
$dump['data'] = stream_get_contents($data);
$dump['data'] = stream_get_contents($data, -1, 0);
ftruncate($data, 0);
rewind($data);
$dumps[] = $dump;

View File

@ -51,9 +51,10 @@ class WindowsPipes extends AbstractPipes
Process::STDOUT => Process::OUT,
Process::STDERR => Process::ERR,
);
$tmpCheck = false;
$tmpDir = sys_get_temp_dir();
$error = 'unknown reason';
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$lastError = 'unknown reason';
set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; });
for ($i = 0;; ++$i) {
foreach ($pipes as $pipe => $name) {
$file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name);
@ -61,7 +62,11 @@ class WindowsPipes extends AbstractPipes
continue 2;
}
$h = fopen($file, 'xb');
if (!$h && false === strpos($error, 'File exists')) {
if (!$h) {
$error = $lastError;
if ($tmpCheck || $tmpCheck = unlink(tempnam(false, 'sf_check_'))) {
continue;
}
restore_error_handler();
throw new RuntimeException(sprintf('A temporary file could not be opened to write the process output: %s', $error));
}

View File

@ -106,9 +106,19 @@ class Router implements RouterInterface, RequestMatcherInterface
*
* Available options:
*
* * cache_dir: The cache directory (or null to disable caching)
* * debug: Whether to enable debugging or not (false by default)
* * resource_type: Type hint for the main resource (optional)
* * cache_dir: The cache directory (or null to disable caching)
* * debug: Whether to enable debugging or not (false by default)
* * generator_class: The name of a UrlGeneratorInterface implementation
* * generator_base_class: The base class for the dumped generator class
* * generator_cache_class: The class name for the dumped generator class
* * generator_dumper_class: The name of a GeneratorDumperInterface implementation
* * matcher_class: The name of a UrlMatcherInterface implementation
* * matcher_base_class: The base class for the dumped matcher class
* * matcher_dumper_class: The class name for the dumped matcher class
* * matcher_cache_class: The name of a MatcherDumperInterface implementation
* * resource_type: Type hint for the main resource (optional)
* * strict_requirements: Configure strict requirement checking for generators
* implementing ConfigurableRequirementsInterface (default is true)
*
* @param array $options An array of options
*

View File

@ -4,7 +4,7 @@
<body>
<trans-unit id="1">
<source>This value should be false.</source>
<target>Deze waarde mag niet waar zijn.</target>
<target>Deze waarde moet onwaar zijn.</target>
</trans-unit>
<trans-unit id="2">
<source>This value should be true.</source>
@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="9">
<source>This field was not expected.</source>
<target>Dit veld was niet verwacht.</target>
<target>Dit veld werd niet verwacht.</target>
</trans-unit>
<trans-unit id="10">
<source>This field is missing.</source>
@ -56,7 +56,7 @@
</trans-unit>
<trans-unit id="14">
<source>The file could not be found.</source>
<target>Het bestand is niet gevonden.</target>
<target>Het bestand kon niet gevonden worden.</target>
</trans-unit>
<trans-unit id="15">
<source>The file is not readable.</source>
@ -100,7 +100,7 @@
</trans-unit>
<trans-unit id="25">
<source>This value is not valid.</source>
<target>Deze waarde is ongeldig.</target>
<target>Deze waarde is niet geldig.</target>
</trans-unit>
<trans-unit id="26">
<source>This value is not a valid time.</source>
@ -124,7 +124,7 @@
</trans-unit>
<trans-unit id="34">
<source>The file could not be uploaded.</source>
<target>Het bestand kon niet geüpload worden.</target>
<target>Het bestand kon niet worden geüpload.</target>
</trans-unit>
<trans-unit id="35">
<source>This value should be a valid number.</source>
@ -140,15 +140,15 @@
</trans-unit>
<trans-unit id="38">
<source>This value is not a valid language.</source>
<target>Deze waarde representeert geen geldige taal.</target>
<target>Deze waarde is geen geldige taal.</target>
</trans-unit>
<trans-unit id="39">
<source>This value is not a valid locale.</source>
<target>Deze waarde representeert geen geldige lokalisering.</target>
<target>Deze waarde is geen geldige locale.</target>
</trans-unit>
<trans-unit id="40">
<source>This value is not a valid country.</source>
<target>Deze waarde representeert geen geldig land.</target>
<target>Deze waarde is geen geldig land.</target>
</trans-unit>
<trans-unit id="41">
<source>This value is already used.</source>
@ -184,7 +184,7 @@
</trans-unit>
<trans-unit id="49">
<source>The file was only partially uploaded.</source>
<target>Het bestand is niet geheel geüpload.</target>
<target>Het bestand is slechts gedeeltelijk geüpload.</target>
</trans-unit>
<trans-unit id="50">
<source>No file was uploaded.</source>

View File

@ -54,18 +54,6 @@ class HtmlDumper extends CliDumper
$this->dumpId = 'sf-dump-'.mt_rand();
}
/**
* {@inheritdoc}
*/
public function setOutput($output)
{
if ($output !== $prev = parent::setOutput($output)) {
$this->headerIsDumped = false;
}
return $prev;
}
/**
* {@inheritdoc}
*/
@ -111,7 +99,7 @@ class HtmlDumper extends CliDumper
*/
protected function getDumpHeader()
{
$this->headerIsDumped = true;
$this->headerIsDumped = null !== $this->outputStream ? $this->outputStream : $this->lineDumper;
if (null !== $this->dumpHeader) {
return $this->dumpHeader;
@ -433,7 +421,7 @@ EOHTML;
if (-1 === $this->lastDepth) {
$this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line;
}
if (!$this->headerIsDumped) {
if ($this->headerIsDumped !== (null !== $this->outputStream ? $this->outputStream : $this->lineDumper)) {
$this->line = $this->getDumpHeader().$this->line;
}

View File

@ -122,8 +122,7 @@ EOTXT
$data = $cloner->cloneVar($var);
$out = fopen('php://memory', 'r+b');
$dumper->dump($data, $out);
rewind($out);
$out = stream_get_contents($out);
$out = stream_get_contents($out, -1, 0);
$this->assertStringMatchesFormat(
<<<EOTXT
@ -132,7 +131,32 @@ EOTXT
EOTXT
,
$out
);
}
public function testAppend()
{
$out = fopen('php://memory', 'r+b');
$dumper = new HtmlDumper();
$dumper->setDumpHeader('<foo></foo>');
$dumper->setDumpBoundaries('<bar>', '</bar>');
$cloner = new VarCloner();
$dumper->dump($cloner->cloneVar(123), $out);
$dumper->dump($cloner->cloneVar(456), $out);
$out = stream_get_contents($out, -1, 0);
$this->assertSame(<<<'EOTXT'
<foo></foo><bar><span class=sf-dump-num>123</span>
</bar>
<bar><span class=sf-dump-num>456</span>
</bar>
EOTXT
,
$out
);
}