Merge branch '2.7' into 2.8

* 2.7:
  [HttpFoundation] Use UPSERT for sessions stored in PgSql >= 9.5
  [Console] fixed PHPDoc
  [travis] HHVM 3.12 LTS
  Fix feature detection for IE
  [Form] Fixed collapsed choice attributes
  [Console] added explanation of messages usage in a progress bar
  force enabling the external XML entity loaders
  [Yaml] properly count skipped comment lines

Conflicts:
	src/Symfony/Component/Translation/Loader/XliffFileLoader.php
This commit is contained in:
Nicolas Grekas 2016-06-14 12:47:22 +02:00
commit 7fa2eab99f
18 changed files with 276 additions and 77 deletions

View File

@ -18,7 +18,7 @@ env:
matrix:
include:
# Use the newer stack for HHVM as HHVM does not support Precise anymore since a long time and so Precise has an outdated version
- php: hhvm
- php: hhvm-3.12
sudo: required
dist: trusty
group: edge

View File

@ -180,7 +180,7 @@ class DbalSessionHandler implements \SessionHandlerInterface
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt->execute();
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
// When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in
// duplicate key errors when the same session is written simultaneously. We can just catch such an
// error and re-execute the update. This is similar to a serializable transaction with retry logic
// on serialization failures but without the overhead and without possible false positives due to
@ -224,11 +224,11 @@ class DbalSessionHandler implements \SessionHandlerInterface
{
$platform = $this->con->getDatabasePlatform()->getName();
switch ($platform) {
case 'mysql':
switch (true) {
case 'mysql' === $platform:
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
case 'oracle':
case 'oracle' === $platform:
// DUAL is Oracle specific dummy table
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
@ -239,8 +239,11 @@ class DbalSessionHandler implements \SessionHandlerInterface
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
case 'sqlite':
case 'sqlite' === $platform:
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
case 'postgresql' === $platform && version_compare($this->con->getServerVersion(), '9.5', '>='):
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->timeCol) = (:data, :time) WHERE $this->idCol = :id";
}
}
}

View File

@ -0,0 +1,9 @@
id="<?php echo $view->escape($id) ?>" name="<?php echo $view->escape($full_name) ?>"
<?php if ($disabled): ?>disabled="disabled" <?php endif ?>
<?php foreach ($choice_attr as $k => $v): ?>
<?php if ($v === true): ?>
<?php printf('%s="%s" ', $view->escape($k), $view->escape($k)) ?>
<?php elseif ($v !== false): ?>
<?php printf('%s="%s" ', $view->escape($k), $view->escape($v)) ?>
<?php endif ?>
<?php endforeach ?>

View File

@ -8,6 +8,6 @@ $translatorHelper = $view['translator']; // outside of the loop for performance
<?php echo $formHelper->block($form, 'choice_widget_options', array('choices' => $choice)) ?>
</optgroup>
<?php else: ?>
<option value="<?php echo $view->escape($choice->value) ?>" <?php echo $view['form']->block($form, 'attributes', array('attr' => $choice->attr)) ?><?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape(false !== $choice_translation_domain ? $translatorHelper->trans($choice->label, array(), $choice_translation_domain) : $choice->label) ?></option>
<option value="<?php echo $view->escape($choice->value) ?>" <?php echo $formHelper->block($form, 'choice_attributes', array('choice_attr' => $choice->attr)) ?><?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape(false !== $choice_translation_domain ? $translatorHelper->trans($choice->label, array(), $choice_translation_domain) : $choice->label) ?></option>
<?php endif ?>
<?php endforeach ?>

View File

@ -211,7 +211,7 @@
var addEventListener;
var el = document.createElement('div');
if (!'addEventListener' in el) {
if (!('addEventListener' in el)) {
addEventListener = function (element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
};

View File

@ -42,7 +42,7 @@ class ProgressBar
private $stepWidth;
private $percent = 0.0;
private $formatLineCount;
private $messages;
private $messages = array();
private $overwrite = true;
private static $formatters;
@ -140,6 +140,16 @@ class ProgressBar
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
}
/**
* Associates a text with a named placeholder.
*
* The text is displayed when the progress bar is rendered but only
* when the corresponding placeholder is part of the custom format line
* (by wrapping the name with %).
*
* @param string $message The text to associate with the placeholder
* @param string $name The name of the placeholder
*/
public function setMessage($message, $name = 'message')
{
$this->messages[$name] = $message;

View File

@ -19,8 +19,7 @@ namespace Symfony\Component\Console\Helper;
class TableSeparator extends TableCell
{
/**
* @param string $value
* @param array $options
* @param array $options
*/
public function __construct(array $options = array())
{

View File

@ -73,21 +73,33 @@ class NullOutput implements OutputInterface
return self::VERBOSITY_QUIET;
}
/**
* {@inheritdoc}
*/
public function isQuiet()
{
return true;
}
/**
* {@inheritdoc}
*/
public function isVerbose()
{
return false;
}
/**
* {@inheritdoc}
*/
public function isVeryVerbose()
{
return false;
}
/**
* {@inheritdoc}
*/
public function isDebug()
{
return false;

View File

@ -94,21 +94,33 @@ abstract class Output implements OutputInterface
return $this->verbosity;
}
/**
* {@inheritdoc}
*/
public function isQuiet()
{
return self::VERBOSITY_QUIET === $this->verbosity;
}
/**
* {@inheritdoc}
*/
public function isVerbose()
{
return self::VERBOSITY_VERBOSE <= $this->verbosity;
}
/**
* {@inheritdoc}
*/
public function isVeryVerbose()
{
return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
}
/**
* {@inheritdoc}
*/
public function isDebug()
{
return self::VERBOSITY_DEBUG <= $this->verbosity;

View File

@ -338,7 +338,6 @@ class XmlFileLoader extends FileLoader
foreach ($definitions as $id => $def) {
list($domElement, $file, $wild) = $def;
if (null !== $definition = $this->parseDefinition($domElement, $file)) {
$this->container->setDefinition($id, $definition);
}
@ -508,7 +507,9 @@ $imports
EOF
;
$disableEntities = libxml_disable_entity_loader(false);
$valid = @$dom->schemaValidateSource($source);
libxml_disable_entity_loader($disableEntities);
foreach ($tmpfiles as $tmpfile) {
@unlink($tmpfile);

View File

@ -84,6 +84,19 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertInstanceOf('DOMDocument', $xml, '->parseFileToDOM() returns an SimpleXMLElement object');
}
public function testLoadWithExternalEntitiesDisabled()
{
$disableEntities = libxml_disable_entity_loader(true);
$containerBuilder = new ContainerBuilder();
$loader = new XmlFileLoader($containerBuilder, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services2.xml');
libxml_disable_entity_loader($disableEntities);
$this->assertTrue(count($containerBuilder->getParameterBag()->all()) > 0, 'Parameters can be read from the config file.');
}
public function testLoadParameters()
{
$container = new ContainerBuilder();

View File

@ -232,6 +232,68 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest
);
}
public function testSingleChoiceAttributesWithMainAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choices_as_values' => true,
'multiple' => false,
'expanded' => false,
'attr' => array('class' => 'bar&baz'),
));
$this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')),
'/select
[@name="name"]
[@class="bar&baz form-control"]
[not(@required)]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
);
}
public function testSingleExpandedChoiceAttributesWithMainAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choices_as_values' => true,
'multiple' => false,
'expanded' => true,
'attr' => array('class' => 'bar&baz'),
));
$this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')),
'/div
[@class="bar&baz"]
[
./div
[@class="radio"]
[
./label
[.=" [trans]Choice&A[/trans]"]
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="radio"]
[
./label
[.=" [trans]Choice&B[/trans]"]
[
./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
'
);
}
public function testSelectWithSizeBiggerThanOneCanBeRequired()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array(

View File

@ -641,6 +641,55 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg
);
}
public function testSingleChoiceAttributesWithMainAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choices_as_values' => true,
'multiple' => false,
'expanded' => false,
'attr' => array('class' => 'bar&baz'),
));
$this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')),
'/select
[@name="name"]
[@class="bar&baz"]
[not(@required)]
[
./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
/following-sibling::option[@value="&b"][not(@class)][not(@selected)][.="[trans]Choice&B[/trans]"]
]
[count(./option)=2]
'
);
}
public function testSingleExpandedChoiceAttributesWithMainAttributes()
{
$form = $this->factory->createNamed('name', 'choice', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choices_as_values' => true,
'multiple' => false,
'expanded' => true,
'attr' => array('class' => 'bar&baz'),
));
$this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')),
'/div
[@class="bar&baz"]
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
'
);
}
public function testSingleChoiceWithPreferred()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(

View File

@ -347,7 +347,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt->execute();
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
// When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in
// duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
// We can just catch such an error and re-execute the update. This is similar to a serializable
// transaction with retry logic on serialization failures but without the overhead and without possible
@ -659,11 +659,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/
private function getMergeSql()
{
switch ($this->driver) {
case 'mysql':
switch (true) {
case 'mysql' === $this->driver:
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
case 'oci':
case 'oci' === $this->driver:
// DUAL is Oracle specific dummy table
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
@ -674,8 +674,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;";
case 'sqlite':
case 'sqlite' === $this->driver:
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (:data, :lifetime, :time) WHERE $this->idCol = :id";
}
}

View File

@ -173,10 +173,16 @@ class XliffFileLoader implements LoaderInterface
{
$internalErrors = libxml_use_internal_errors(true);
$disableEntities = libxml_disable_entity_loader(false);
if (!@$dom->schemaValidateSource($schema)) {
libxml_disable_entity_loader($disableEntities);
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors))));
}
libxml_disable_entity_loader($disableEntities);
$dom->normalizeDocument();
libxml_clear_errors();

View File

@ -46,6 +46,20 @@ class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase
libxml_use_internal_errors($internalErrors);
}
public function testLoadWithExternalEntitiesDisabled()
{
$disableEntities = libxml_disable_entity_loader(true);
$loader = new XliffFileLoader();
$resource = __DIR__.'/../fixtures/resources.xlf';
$catalogue = $loader->load($resource, 'en', 'domain1');
libxml_disable_entity_loader($disableEntities);
$this->assertEquals('en', $catalogue->getLocale());
$this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
}
public function testLoadWithResname()
{
$loader = new XliffFileLoader();

View File

@ -30,17 +30,21 @@ class Parser
private $currentLineNb = -1;
private $currentLine = '';
private $refs = array();
private $skippedLineNumbers = array();
private $locallySkippedLineNumbers = array();
/**
* Constructor.
*
* @param int $offset The offset of YAML document (used for line numbers in error messages)
* @param int|null $totalNumberOfLines The overall number of lines being parsed
* @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser
*/
public function __construct($offset = 0, $totalNumberOfLines = null)
public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
{
$this->offset = $offset;
$this->totalNumberOfLines = $totalNumberOfLines;
$this->skippedLineNumbers = $skippedLineNumbers;
}
/**
@ -101,25 +105,18 @@ class Parser
// array
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c, $this->totalNumberOfLines);
$parser->refs = &$this->refs;
$data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
$data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
} else {
if (isset($values['leadspaces'])
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
) {
// this is a compact notation element, add to next block and parse
$c = $this->getRealCurrentLineNb();
$parser = new self($c, $this->totalNumberOfLines);
$parser->refs = &$this->refs;
$block = $values['value'];
if ($this->isNextLineIndented()) {
$block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
}
$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
$data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
} else {
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
}
@ -175,10 +172,7 @@ class Parser
} else {
$value = $this->getNextEmbedBlock();
}
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c, $this->totalNumberOfLines);
$parser->refs = &$this->refs;
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
$parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
if (!is_array($parsed)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
@ -226,10 +220,7 @@ class Parser
$data[$key] = null;
}
} else {
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c, $this->totalNumberOfLines);
$parser->refs = &$this->refs;
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
$value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
// Spec: Keys MUST be unique; first one wins.
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
@ -323,6 +314,24 @@ class Parser
return empty($data) ? null : $data;
}
private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
{
$skippedLineNumbers = $this->skippedLineNumbers;
foreach ($this->locallySkippedLineNumbers as $lineNumber) {
if ($lineNumber < $offset) {
continue;
}
$skippedLineNumbers[] = $lineNumber;
}
$parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
$parser->refs = &$this->refs;
return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
}
/**
* Returns the current line number (takes the offset into account).
*
@ -330,7 +339,17 @@ class Parser
*/
private function getRealCurrentLineNb()
{
return $this->currentLineNb + $this->offset;
$realCurrentLineNumber = $this->currentLineNb + $this->offset;
foreach ($this->skippedLineNumbers as $skippedLineNumber) {
if ($skippedLineNumber > $realCurrentLineNumber) {
break;
}
++$realCurrentLineNumber;
}
return $realCurrentLineNumber;
}
/**
@ -432,7 +451,15 @@ class Parser
}
// we ignore "comment" lines only when we are not inside a scalar block
if (empty($blockScalarIndentations) && $this->isCurrentLineComment() && false === $this->checkIfPreviousNonCommentLineIsCollectionItem()) {
if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
// remember ignored comment lines (they are used later in nested
// parser calls to determine real line numbers)
//
// CAUTION: beware to not populate the global property here as it
// will otherwise influence the getRealCurrentLineNb() call here
// for consecutive comment lines and subsequent embedded blocks
$this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
continue;
}
@ -802,44 +829,4 @@ class Parser
{
return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
}
/**
* Returns true if the current line is a collection item.
*
* @return bool
*/
private function isCurrentLineCollectionItem()
{
$ltrimmedLine = ltrim($this->currentLine, ' ');
return '' !== $ltrimmedLine && '-' === $ltrimmedLine[0];
}
/**
* Tests whether the current comment line is in a collection.
*
* @return bool
*/
private function checkIfPreviousNonCommentLineIsCollectionItem()
{
$isCollectionItem = false;
$moves = 0;
while ($this->moveToPreviousLine()) {
++$moves;
// If previous line is a comment, move back again.
if ($this->isCurrentLineComment()) {
continue;
}
$isCollectionItem = $this->isCurrentLineCollectionItem();
break;
}
// Move parser back to previous line.
while ($moves > 0) {
$this->moveToNextLine();
--$moves;
}
return $isCollectionItem;
}
}

View File

@ -656,6 +656,25 @@ EOT;
$this->assertSame($expected, $this->parser->parse($yaml));
}
public function testSequenceFollowedByCommentEmbeddedInMapping()
{
$yaml = <<<EOT
a:
b:
- c
# comment
d: e
EOT;
$expected = array(
'a' => array(
'b' => array('c'),
'd' => 'e',
),
);
$this->assertSame($expected, $this->parser->parse($yaml));
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
*/