diff --git a/.travis.yml b/.travis.yml index 7b8fb71348..c14543b5d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: php php: - - 5.3.3 - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - hhvm-nightly + - 5.3.3 + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm-nightly matrix: allow_failures: diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml index a1beee30a1..a6320b7f04 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml @@ -39,6 +39,7 @@ + %fragment.path% diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php index 23631b9943..2167138a1e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php @@ -1,3 +1,33 @@ This template is used for translation message extraction tests trans('single-quoted key') ?> trans("double-quoted key") ?> +trans(<< +trans(<<<'EOF' +nowdoc key +EOF +) ?> +trans( + "double-quoted key with whitespace and escaped \$\n\" sequences" +) ?> +trans( + 'single-quoted key with whitespace and nonescaped \$\n\' sequences' +) ?> +trans( << +trans( <<<'EOF' +nowdoc key with whitespace and nonescaped \$\n sequences +EOF +) ?> + +trans('single-quoted key with "quote mark at the end"') ?> + +transChoice( + '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples', + 10, + array('%count%' => 10) +) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php index d639f01806..52e20b874e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php @@ -27,10 +27,27 @@ class PhpExtractorTest extends TestCase // Act $extractor->extract(__DIR__.'/../Fixtures/Resources/views/', $catalogue); + $expectedHeredoc = <<assertCount(2, $catalogue->all('messages'), '->extract() should find 1 translation'); - $this->assertTrue($catalogue->has('single-quoted key'), '->extract() should find the "single-quoted key" message'); - $this->assertTrue($catalogue->has('double-quoted key'), '->extract() should find the "double-quoted key" message'); - $this->assertEquals('prefixsingle-quoted key', $catalogue->get('single-quoted key'), '->extract() should apply "prefix" as prefix'); + $expectedCatalogue = array('messages' => array( + 'single-quoted key' => 'prefixsingle-quoted key', + 'double-quoted key' => 'prefixdouble-quoted key', + 'heredoc key' => 'prefixheredoc key', + 'nowdoc key' => 'prefixnowdoc key', + "double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixdouble-quoted key with whitespace and escaped \$\n\" sequences", + 'single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixsingle-quoted key with whitespace and nonescaped \$\n\' sequences', + 'single-quoted key with "quote mark at the end"' => 'prefixsingle-quoted key with "quote mark at the end"', + $expectedHeredoc => "prefix".$expectedHeredoc, + $expectedNowdoc => "prefix".$expectedNowdoc, + '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples' => 'prefix{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples', + )); + $actualCatalogue = $catalogue->all(); + + $this->assertEquals($expectedCatalogue, $actualCatalogue); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index cda38061ef..9578c6ac08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -93,6 +93,16 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } + public function testTransWithCachingWithInvalidLocale() + { + $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); + $translator->setLocale('invalid locale'); + + $this->setExpectedException('\InvalidArgumentException'); + $translator->trans('foo'); + } + /** * @dataProvider getGetLocaleData */ @@ -102,7 +112,7 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase if ($inRequestScope) { $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); $request - ->expects($this->once()) + ->expects($this->any()) ->method('getLocale') ->will($this->returnValue('en')) ; @@ -135,6 +145,37 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase ); } + public function testGetLocaleWithInvalidLocale() + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + + $request + ->expects($this->any()) + ->method('getLocale') + ->will($this->returnValue('foo bar')) + ; + $request + ->expects($this->once()) + ->method('getDefaultLocale') + ->will($this->returnValue('en-US')) + ; + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container + ->expects($this->once()) + ->method('get') + ->with('request_stack') + ->will($this->returnValue($requestStack)) + ; + + $translator = new Translator($container, new MessageSelector()); + $this->assertSame('en-US', $translator->getLocale()); + } + + protected function getCatalogue($locale, $messages) { $catalogue = new MessageCatalogue($locale); @@ -215,9 +256,9 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase return $container; } - public function getTranslator($loader, $options = array()) + public function getTranslator($loader, $options = array(), $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') { - $translator = new Translator( + $translator = new $translatorClass( $this->getContainer($loader), new MessageSelector(), array('loader' => array('loader')), @@ -235,3 +276,14 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase return $translator; } } + +class TranslatorWithInvalidLocale extends Translator +{ + /** + * {@inheritdoc} + */ + public function setLocale($locale) + { + $this->locale = $locale; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php index 1b12c8ca9e..67de7da719 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php @@ -23,7 +23,6 @@ use Symfony\Component\Translation\Extractor\ExtractorInterface; class PhpExtractor implements ExtractorInterface { const MESSAGE_TOKEN = 300; - const IGNORE_TOKEN = 400; /** * Prefix for new found message. @@ -39,15 +38,16 @@ class PhpExtractor implements ExtractorInterface */ protected $sequences = array( array( - '$view', - '[', - '\'translator\'', - ']', '->', 'trans', '(', self::MESSAGE_TOKEN, - ')', + ), + array( + '->', + 'transChoice', + '(', + self::MESSAGE_TOKEN, ), ); @@ -76,6 +76,7 @@ class PhpExtractor implements ExtractorInterface * Normalizes a token. * * @param mixed $token + * * @return string */ protected function normalizeToken($token) @@ -87,6 +88,56 @@ class PhpExtractor implements ExtractorInterface return $token; } + /** + * Seeks to a non-whitespace token. + */ + private function seekToNextReleventToken(\Iterator $tokenIterator) + { + for (; $tokenIterator->valid(); $tokenIterator->next()) { + $t = $tokenIterator->current(); + if (!is_array($t) || ($t[0] !== T_WHITESPACE)) { + break; + } + } + } + + /** + * Extracts the message from the iterator while the tokens + * match allowed message tokens + */ + private function getMessage(\Iterator $tokenIterator) + { + $message = ''; + $docToken = ''; + + for (; $tokenIterator->valid(); $tokenIterator->next()) { + $t = $tokenIterator->current(); + if (!is_array($t)) { + break; + } + + switch ($t[0]) { + case T_START_HEREDOC: + $docToken = $t[1]; + break; + case T_ENCAPSED_AND_WHITESPACE: + case T_CONSTANT_ENCAPSED_STRING: + $message .= $t[1]; + break; + case T_END_HEREDOC: + return PhpStringTokenParser::parseDocString($docToken, $message); + default: + break 2; + } + } + + if ($message) { + $message = PhpStringTokenParser::parse($message); + } + + return $message; + } + /** * Extracts trans message from PHP tokens. * @@ -95,24 +146,27 @@ class PhpExtractor implements ExtractorInterface */ protected function parseTokens($tokens, MessageCatalogue $catalog) { - foreach ($tokens as $key => $token) { + $tokenIterator = new \ArrayIterator($tokens); + + for ($key = 0; $key < $tokenIterator->count(); $key++) { foreach ($this->sequences as $sequence) { $message = ''; + $tokenIterator->seek($key); - foreach ($sequence as $id => $item) { - if ($this->normalizeToken($tokens[$key + $id]) == $item) { + foreach ($sequence as $item) { + $this->seekToNextReleventToken($tokenIterator); + + if ($this->normalizeToken($tokenIterator->current()) == $item) { + $tokenIterator->next(); continue; } elseif (self::MESSAGE_TOKEN == $item) { - $message = $this->normalizeToken($tokens[$key + $id]); - } elseif (self::IGNORE_TOKEN == $item) { - continue; + $message = $this->getMessage($tokenIterator); + break; } else { break; } } - $message = trim($message, '\'"'); - if ($message) { $catalog->set($message, $this->prefix.$message); break; diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpStringTokenParser.php b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpStringTokenParser.php new file mode 100644 index 0000000000..0b29792e52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpStringTokenParser.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Translation; + +/* + * The following is derived from code at http://github.com/nikic/PHP-Parser + * + * Copyright (c) 2011 by Nikita Popov + * + * Some rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * * The names of the contributors may not be used to endorse or + * promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +class PhpStringTokenParser +{ + protected static $replacements = array( + '\\' => '\\', + '$' => '$', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'f' => "\f", + 'v' => "\v", + 'e' => "\x1B", + ); + + /** + * Parses a string token. + * + * @param string $str String token content + * + * @return string The parsed string + */ + public static function parse($str) + { + $bLength = 0; + if ('b' === $str[0]) { + $bLength = 1; + } + + if ('\'' === $str[$bLength]) { + return str_replace( + array('\\\\', '\\\''), + array( '\\', '\''), + substr($str, $bLength + 1, -1) + ); + } else { + return self::parseEscapeSequences(substr($str, $bLength + 1, -1), '"'); + } + } + + /** + * Parses escape sequences in strings (all string types apart from single quoted). + * + * @param string $str String without quotes + * @param null|string $quote Quote type + * + * @return string String with escape sequences parsed + */ + public static function parseEscapeSequences($str, $quote) + { + if (null !== $quote) { + $str = str_replace('\\' . $quote, $quote, $str); + } + + return preg_replace_callback( + '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3})~', + array(__CLASS__, 'parseCallback'), + $str + ); + } + + public static function parseCallback($matches) + { + $str = $matches[1]; + + if (isset(self::$replacements[$str])) { + return self::$replacements[$str]; + } elseif ('x' === $str[0] || 'X' === $str[0]) { + return chr(hexdec($str)); + } else { + return chr(octdec($str)); + } + } + + /** + * Parses a constant doc string. + * + * @param string $startToken Doc string start token content (<<locale && $request = $this->container->get('request_stack')->getCurrentRequest()) { $this->locale = $request->getLocale(); + try { + $this->setLocale($request->getLocale()); + } catch (\InvalidArgumentException $e) { + $this->setLocale($request->getDefaultLocale()); + } } return $this->locale; @@ -87,6 +92,8 @@ class Translator extends BaseTranslator return parent::loadCatalogue($locale); } + $this->assertValidLocale($locale); + $cache = new ConfigCache($this->options['cache_dir'].'/catalogue.'.$locale.'.php', $this->options['debug']); if (!$cache->isFresh()) { $this->initialize(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php b/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php new file mode 100644 index 0000000000..b140e302b1 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\HttpKernel\Profiler\Profiler; + +/** + * Exports a profile. + * + * @author Fabien Potencier + */ +class ExportCommand extends Command +{ + private $profiler; + + public function __construct(Profiler $profiler = null) + { + $this->profiler = $profiler; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function isEnabled() + { + if (null === $this->profiler) { + return false; + } + + return parent::isEnabled(); + } + + protected function configure() + { + $this + ->setName('profiler:export') + ->setDescription('Exports a profile') + ->setDefinition(array( + new InputArgument('token', InputArgument::REQUIRED, 'The profile token'), + )) + ->setHelp(<<%command.name% command exports a profile to the standard output: + +php %command.full_name% profile_token +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $token = $input->getArgument('token'); + + if (!$profile = $this->profiler->loadProfile($token)) { + throw new \LogicException(sprintf('Profile with token "%s" does not exist.', $token)); + } + + $output->writeln($this->profiler->export($profile), OutputInterface::OUTPUT_RAW); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php b/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php new file mode 100644 index 0000000000..70fecd45ea --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\HttpKernel\Profiler\Profiler; + +/** + * Imports a profile. + * + * @author Fabien Potencier + */ +class ImportCommand extends Command +{ + private $profiler; + + public function __construct(Profiler $profiler = null) + { + $this->profiler = $profiler; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function isEnabled() + { + if (null === $this->profiler) { + return false; + } + + return parent::isEnabled(); + } + + protected function configure() + { + $this + ->setName('profiler:import') + ->setDescription('Imports a profile') + ->setDefinition(array( + new InputArgument('filename', InputArgument::OPTIONAL, 'The profile path'), + )) + ->setHelp(<<%command.name% command imports a profile: + +php %command.full_name% profile_filepath + +You can also pipe the profile via STDIN: + +cat profile_file | php %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = ''; + if ($input->getArgument('filename')) { + $data = file_get_contents($input->getArgument('filename')); + } else { + if (0 !== ftell(STDIN)) { + throw new \RuntimeException('Please provide a filename or pipe the profile to STDIN.'); + } + + while (!feof(STDIN)) { + $data .= fread(STDIN, 1024); + } + } + + if (!$profile = $this->profiler->import($data)) { + throw new \LogicException('The profile already exists in the database.'); + } + + $output->writeln(sprintf('Profile "%s" has been successfully imported.', $profile->getToken())); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 8542f2865f..dea4f8883f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -111,33 +111,6 @@ class ProfilerController )), 200, array('Content-Type' => 'text/html')); } - /** - * Exports data for a given token. - * - * @param string $token The profiler token - * - * @return Response A Response instance - * - * @throws NotFoundHttpException - */ - public function exportAction($token) - { - if (null === $this->profiler) { - throw new NotFoundHttpException('The profiler must be enabled.'); - } - - $this->profiler->disable(); - - if (!$profile = $this->profiler->loadProfile($token)) { - throw new NotFoundHttpException(sprintf('Token "%s" does not exist.', $token)); - } - - return new Response($this->profiler->export($profile), 200, array( - 'Content-Type' => 'text/plain', - 'Content-Disposition' => 'attachment; filename= '.$token.'.txt', - )); - } - /** * Purges all tokens. * @@ -157,36 +130,6 @@ class ProfilerController return new RedirectResponse($this->generator->generate('_profiler_info', array('about' => 'purge')), 302, array('Content-Type' => 'text/html')); } - /** - * Imports token data. - * - * @param Request $request The current HTTP Request - * - * @return Response A Response instance - * - * @throws NotFoundHttpException - */ - public function importAction(Request $request) - { - if (null === $this->profiler) { - throw new NotFoundHttpException('The profiler must be enabled.'); - } - - $this->profiler->disable(); - - $file = $request->files->get('file'); - - if (empty($file) || !$file->isValid()) { - return new RedirectResponse($this->generator->generate('_profiler_info', array('about' => 'upload_error')), 302, array('Content-Type' => 'text/html')); - } - - if (!$profile = $this->profiler->import(file_get_contents($file->getPathname()))) { - return new RedirectResponse($this->generator->generate('_profiler_info', array('about' => 'already_exists')), 302, array('Content-Type' => 'text/html')); - } - - return new RedirectResponse($this->generator->generate('_profiler', array('token' => $profile->getToken())), 302, array('Content-Type' => 'text/html')); - } - /** * Displays information page. * diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml new file mode 100644 index 0000000000..d85b54d38d --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml @@ -0,0 +1,23 @@ + + + + + + Symfony\Bundle\WebProfilerBundle\Command\ImportCommand + Symfony\Bundle\WebProfilerBundle\Command\ExportCommand + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig index 3ca28ff162..a47b254a77 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig @@ -1,27 +1,10 @@ - +{% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php new file mode 100644 index 0000000000..572e9b3978 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\Command; + +use Symfony\Bundle\WebProfilerBundle\Command\ExportCommand; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Application; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class ExportCommandTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \LogicException + */ + public function testExecuteWithUnknownToken() + { + $profiler = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock() + ; + + $command = new ExportCommand($profiler); + $commandTester = new CommandTester($command); + $commandTester->execute(array('token' => 'TOKEN')); + } + + public function testExecuteWithToken() + { + $profiler = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock() + ; + + $profile = new Profile('TOKEN'); + $profiler->expects($this->once())->method('loadProfile')->with('TOKEN')->will($this->returnValue($profile)); + + $command = new ExportCommand($profiler); + $commandTester = new CommandTester($command); + $commandTester->execute(array('token' => 'TOKEN')); + $this->assertEquals($profiler->export($profile), $commandTester->getDisplay()); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php new file mode 100644 index 0000000000..f5121c12d6 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\Command; + +use Symfony\Bundle\WebProfilerBundle\Command\ImportCommand; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Application; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class ImportCommandTest extends \PHPUnit_Framework_TestCase +{ + public function testExecute() + { + $profiler = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock() + ; + + $profiler->expects($this->once())->method('import')->will($this->returnValue(new Profile('TOKEN'))); + + $command = new ImportCommand($profiler); + $commandTester = new CommandTester($command); + $commandTester->execute(array('filename' => __DIR__.'/../Fixtures/profile.data')); + $this->assertRegExp('/Profile "TOKEN" has been successfully imported\./', $commandTester->getDisplay()); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data new file mode 100644 index 0000000000..ab76cea63d --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data @@ -0,0 +1 @@ +Tzo0NToiU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlIjo4OntzOjUyOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHRva2VuIjtzOjU6IlRPS0VOIjtzOjUzOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHBhcmVudCI7TjtzOjU1OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAGNoaWxkcmVuIjthOjA6e31zOjU3OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAGNvbGxlY3RvcnMiO2E6MDp7fXM6NDk6IgBTeW1mb255XENvbXBvbmVudFxIdHRwS2VybmVsXFByb2ZpbGVyXFByb2ZpbGUAaXAiO047czo1MzoiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQBtZXRob2QiO047czo1MDoiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQB1cmwiO047czo1MToiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQB0aW1lIjtOO30= \ No newline at end of file diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 23047059c7..fff751e82f 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -52,7 +52,8 @@ class Filesystem if ($doCopy) { // https://bugs.php.net/bug.php?id=64634 $source = fopen($originFile, 'r'); - $target = fopen($targetFile, 'w'); + // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default + $target = fopen($targetFile, 'w', null, stream_context_create(array('ftp' => array('overwrite' => true)))); stream_copy_to_stream($source, $target); fclose($source); fclose($target); diff --git a/src/Symfony/Component/Finder/Tests/FinderTest.php b/src/Symfony/Component/Finder/Tests/FinderTest.php index 7634b99966..f7f0c21425 100644 --- a/src/Symfony/Component/Finder/Tests/FinderTest.php +++ b/src/Symfony/Component/Finder/Tests/FinderTest.php @@ -739,17 +739,30 @@ class FinderTest extends Iterator\RealIteratorTestCase $finder->files()->in(self::$tmpDir); // make 'foo' directory non-readable - chmod(self::$tmpDir.DIRECTORY_SEPARATOR.'foo', 0333); + $testDir = self::$tmpDir.DIRECTORY_SEPARATOR.'foo'; + chmod($testDir, 0333); - try { - $this->assertIterator($this->toAbsolute(array('foo bar', 'test.php', 'test.py')), $finder->getIterator()); - $this->fail('Finder should throw an exception when opening a non-readable directory.'); - } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\\Component\\Finder\\Exception\\AccessDeniedException', $e); + if (false === $couldRead = is_readable($testDir)) { + try { + $this->assertIterator($this->toAbsolute(array('foo bar', 'test.php', 'test.py')), $finder->getIterator()); + $this->fail('Finder should throw an exception when opening a non-readable directory.'); + } catch (\Exception $e) { + $expectedExceptionClass = 'Symfony\\Component\\Finder\\Exception\\AccessDeniedException'; + if ($e instanceof \PHPUnit_Framework_ExpectationFailedException) { + $this->fail(sprintf("Expected exception:\n%s\nGot:\n%s\nWith comparison failure:\n%s", $expectedExceptionClass, 'PHPUnit_Framework_ExpectationFailedException', $e->getComparisonFailure()->getExpectedAsString())); + } + + $this->assertInstanceOf($expectedExceptionClass, $e); + } } // restore original permissions - chmod(self::$tmpDir.DIRECTORY_SEPARATOR.'foo', 0777); + chmod($testDir, 0777); + clearstatcache($testDir); + + if ($couldRead) { + $this->markTestSkipped('could read test files while test requires unreadable'); + } } /** @@ -765,12 +778,20 @@ class FinderTest extends Iterator\RealIteratorTestCase $finder->files()->ignoreUnreadableDirs()->in(self::$tmpDir); // make 'foo' directory non-readable - chmod(self::$tmpDir.DIRECTORY_SEPARATOR.'foo', 0333); + $testDir = self::$tmpDir.DIRECTORY_SEPARATOR.'foo'; + chmod($testDir, 0333); - $this->assertIterator($this->toAbsolute(array('foo bar', 'test.php', 'test.py')), $finder->getIterator()); + if (false === ($couldRead = is_readable($testDir))) { + $this->assertIterator($this->toAbsolute(array('foo bar', 'test.php', 'test.py')), $finder->getIterator()); + } // restore original permissions - chmod(self::$tmpDir.DIRECTORY_SEPARATOR.'foo', 0777); + chmod($testDir, 0777); + clearstatcache($testDir); + + if ($couldRead) { + $this->markTestSkipped('could read test files while test requires unreadable'); + } } private function buildTestData(array $tests) diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index f68aa74978..06d98c8580 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1172,7 +1172,8 @@ class Request // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) - if ($host && !preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host)) { + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host)); } @@ -1381,6 +1382,16 @@ class Request } } + /** + * Get the default locale. + * + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + /** * Sets the locale. * diff --git a/src/Symfony/Component/HttpFoundation/ServerBag.php b/src/Symfony/Component/HttpFoundation/ServerBag.php index de6dbc9fb5..6a4f2c2b16 100644 --- a/src/Symfony/Component/HttpFoundation/ServerBag.php +++ b/src/Symfony/Component/HttpFoundation/ServerBag.php @@ -65,13 +65,13 @@ class ServerBag extends ParameterBag } if (null !== $authorizationHeader) { - if (0 === stripos($authorizationHeader, 'basic')) { + if (0 === stripos($authorizationHeader, 'basic ')) { // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic - $exploded = explode(':', base64_decode(substr($authorizationHeader, 6))); + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); if (count($exploded) == 2) { list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; } - } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest'))) { + } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { // In some circumstances PHP_AUTH_DIGEST needs to be set $headers['PHP_AUTH_DIGEST'] = $authorizationHeader; $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index f98864f4f1..ed06e550c7 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -42,7 +42,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals('bar', $request->attributes->get('foo'), '->initialize() takes an array of attributes as its third argument'); $request->initialize(array(), array(), array(), array(), array(), array('HTTP_FOO' => 'bar')); - $this->assertEquals('bar', $request->headers->get('FOO'), '->initialize() takes an array of HTTP headers as its fourth argument'); + $this->assertEquals('bar', $request->headers->get('FOO'), '->initialize() takes an array of HTTP headers as its sixth argument'); } public function testGetLocale() @@ -200,23 +200,23 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals('test.com:90', $request->getHttpHost()); $this->assertFalse($request->isSecure()); - $request = Request::create('http://test:test@test.com'); + $request = Request::create('http://username:password@test.com'); $this->assertEquals('http://test.com/', $request->getUri()); $this->assertEquals('/', $request->getPathInfo()); $this->assertEquals('', $request->getQueryString()); $this->assertEquals(80, $request->getPort()); $this->assertEquals('test.com', $request->getHttpHost()); - $this->assertEquals('test', $request->getUser()); - $this->assertEquals('test', $request->getPassword()); + $this->assertEquals('username', $request->getUser()); + $this->assertEquals('password', $request->getPassword()); $this->assertFalse($request->isSecure()); - $request = Request::create('http://testnopass@test.com'); + $request = Request::create('http://username@test.com'); $this->assertEquals('http://test.com/', $request->getUri()); $this->assertEquals('/', $request->getPathInfo()); $this->assertEquals('', $request->getQueryString()); $this->assertEquals(80, $request->getPort()); $this->assertEquals('test.com', $request->getHttpHost()); - $this->assertEquals('testnopass', $request->getUser()); + $this->assertEquals('username', $request->getUser()); $this->assertSame('',$request->getPassword()); $this->assertFalse($request->isSecure()); @@ -1657,6 +1657,59 @@ class RequestTest extends \PHPUnit_Framework_TestCase Request::setFactory(null); } + + /** + * @dataProvider getLongHostNames + */ + public function testVeryLongHosts($host) + { + $start = microtime(true); + + $request = Request::create('/'); + $request->headers->set('host', $host); + $this->assertEquals($host, $request->getHost()); + $this->assertLessThan(1, microtime(true) - $start); + } + + /** + * @dataProvider getHostValidities + */ + public function testHostValidity($host, $isValid, $expectedHost = null, $expectedPort = null) + { + $request = Request::create('/'); + $request->headers->set('host', $host); + + if ($isValid) { + $this->assertSame($expectedHost ?: $host, $request->getHost()); + if ($expectedPort) { + $this->assertSame($expectedPort, $request->getPort()); + } + } else { + $this->setExpectedException('UnexpectedValueException', 'Invalid Host'); + $request->getHost(); + } + } + + public function getHostValidities() + { + return array( + array('.a', false), + array('a..', false), + array('a.', true), + array("\xE9", false), + array('[::1]', true), + array('[::1]:80', true, '[::1]', 80), + array(str_repeat('.', 101), false), + ); + } + + public function getLongHostNames() + { + return array( + array('a'.str_repeat('.a', 40000)), + array(str_repeat(':', 101)), + ); + } } class RequestContentProxy extends Request diff --git a/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php index 89920f1fbc..662c5880b1 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php @@ -67,14 +67,24 @@ class ServerBagTest extends \PHPUnit_Framework_TestCase ), $bag->getHeaders()); } + public function testHttpBasicAuthWithPhpCgiBogus() + { + $bag = new ServerBag(array('HTTP_AUTHORIZATION' => 'Basic_'.base64_encode('foo:bar'))); + + // Username and passwords should not be set as the header is bogus + $headers = $bag->getHeaders(); + $this->assertFalse(isset($headers['PHP_AUTH_USER'])); + $this->assertFalse(isset($headers['PHP_AUTH_PW'])); + } + public function testHttpBasicAuthWithPhpCgiRedirect() { - $bag = new ServerBag(array('REDIRECT_HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:bar'))); + $bag = new ServerBag(array('REDIRECT_HTTP_AUTHORIZATION' => 'Basic '.base64_encode('username:pass:word'))); $this->assertEquals(array( - 'AUTHORIZATION' => 'Basic '.base64_encode('foo:bar'), - 'PHP_AUTH_USER' => 'foo', - 'PHP_AUTH_PW' => 'bar' + 'AUTHORIZATION' => 'Basic '.base64_encode('username:pass:word'), + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'pass:word' ), $bag->getHeaders()); } @@ -100,6 +110,17 @@ class ServerBagTest extends \PHPUnit_Framework_TestCase ), $bag->getHeaders()); } + public function testHttpDigestAuthWithPhpCgiBogus() + { + $digest = 'Digest_username="foo", realm="acme", nonce="'.md5('secret').'", uri="/protected, qop="auth"'; + $bag = new ServerBag(array('HTTP_AUTHORIZATION' => $digest)); + + // Username and passwords should not be set as the header is bogus + $headers = $bag->getHeaders(); + $this->assertFalse(isset($headers['PHP_AUTH_USER'])); + $this->assertFalse(isset($headers['PHP_AUTH_PW'])); + } + public function testHttpDigestAuthWithPhpCgiRedirect() { $digest = 'Digest username="foo", realm="acme", nonce="'.md5('secret').'", uri="/protected, qop="auth"'; diff --git a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php index ef3fad3d4c..1a198a55f6 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php @@ -12,11 +12,11 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -25,8 +25,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; * All URL paths starting with /_fragment are handled as * content fragments by this listener. * - * If the request does not come from a trusted IP, it throws an - * AccessDeniedHttpException exception. + * If throws an AccessDeniedHttpException exception if the request + * is not signed or if it is not an internal sub-request. * * @author Fabien Potencier */ @@ -62,7 +62,9 @@ class FragmentListener implements EventSubscriberInterface return; } - $this->validateRequest($request); + if ($event->isMasterRequest()) { + $this->validateRequest($request); + } parse_str($request->query->get('_path', ''), $attributes); $request->attributes->add($attributes); @@ -77,13 +79,6 @@ class FragmentListener implements EventSubscriberInterface throw new AccessDeniedHttpException(); } - // does the Request come from a trusted IP? - $trustedIps = array_merge($this->getLocalIpAddresses(), $request->getTrustedProxies()); - $remoteAddress = $request->server->get('REMOTE_ADDR'); - if (IpUtils::checkIp($remoteAddress, $trustedIps)) { - return; - } - // is the Request signed? // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) if ($this->signer->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().(null !== ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''))) { @@ -93,6 +88,11 @@ class FragmentListener implements EventSubscriberInterface throw new AccessDeniedHttpException(); } + /** + * @deprecated Deprecated since 2.3.19, to be removed in 3.0. + * + * @return string[] + */ protected function getLocalIpAddresses() { return array('127.0.0.1', 'fe80::1', '::1'); diff --git a/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php index 2fe5c0356c..4c10418d6e 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php @@ -25,7 +25,7 @@ abstract class SessionListener implements EventSubscriberInterface { public function onKernelRequest(GetResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php index 4d3232324a..794d45e720 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php @@ -30,7 +30,7 @@ abstract class TestSessionListener implements EventSubscriberInterface { public function onKernelRequest(GetResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } @@ -55,7 +55,7 @@ abstract class TestSessionListener implements EventSubscriberInterface */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php index 620c71a878..772884d634 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\HttpCache\Esi; +use Symfony\Component\HttpKernel\UriSigner; /** * Implements the ESI rendering strategy. @@ -25,6 +26,7 @@ class EsiFragmentRenderer extends RoutableFragmentRenderer { private $esi; private $inlineStrategy; + private $signer; /** * Constructor. @@ -34,11 +36,13 @@ class EsiFragmentRenderer extends RoutableFragmentRenderer * * @param Esi $esi An Esi instance * @param FragmentRendererInterface $inlineStrategy The inline strategy to use when ESI is not supported + * @param UriSigner $signer */ - public function __construct(Esi $esi = null, InlineFragmentRenderer $inlineStrategy) + public function __construct(Esi $esi = null, InlineFragmentRenderer $inlineStrategy, UriSigner $signer = null) { $this->esi = $esi; $this->inlineStrategy = $inlineStrategy; + $this->signer = $signer; } /** @@ -61,12 +65,12 @@ class EsiFragmentRenderer extends RoutableFragmentRenderer } if ($uri instanceof ControllerReference) { - $uri = $this->generateFragmentUri($uri, $request); + $uri = $this->generateSignedFragmentUri($uri, $request); } $alt = isset($options['alt']) ? $options['alt'] : null; if ($alt instanceof ControllerReference) { - $alt = $this->generateFragmentUri($alt, $request); + $alt = $this->generateSignedFragmentUri($alt, $request); } $tag = $this->esi->renderIncludeTag($uri, $alt, isset($options['ignore_errors']) ? $options['ignore_errors'] : false, isset($options['comment']) ? $options['comment'] : ''); @@ -81,4 +85,16 @@ class EsiFragmentRenderer extends RoutableFragmentRenderer { return 'esi'; } + + private function generateSignedFragmentUri($uri, Request $request) + { + if (null === $this->signer) { + throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.'); + } + + // we need to sign the absolute URI, but want to return the path only. + $fragmentUri = $this->signer->sign($this->generateFragmentUri($uri, $request, true)); + + return substr($fragmentUri, strlen($request->getSchemeAndHttpHost())); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php index ec9360a431..7ddb2fbbf2 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php @@ -47,19 +47,6 @@ class FragmentListenerTest extends \PHPUnit_Framework_TestCase $listener->onKernelRequest($event); } - /** - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function testAccessDeniedWithNonLocalIps() - { - $request = Request::create('http://example.com/_fragment', 'GET', array(), array(), array(), array('REMOTE_ADDR' => '10.0.0.1')); - - $listener = new FragmentListener(new UriSigner('foo')); - $event = $this->createGetResponseEvent($request); - - $listener->onKernelRequest($event); - } - /** * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php index 8acf45d96f..90768f9dac 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\UriSigner; class EsiFragmentRendererTest extends \PHPUnit_Framework_TestCase { @@ -41,7 +42,52 @@ class EsiFragmentRendererTest extends \PHPUnit_Framework_TestCase $this->assertEquals('', $strategy->render('/', $request)->getContent()); $this->assertEquals("\n", $strategy->render('/', $request, array('comment' => 'This is a comment'))->getContent()); $this->assertEquals('', $strategy->render('/', $request, array('alt' => 'foo'))->getContent()); - $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', array(), array()), $request, array('alt' => new ControllerReference('alt_controller', array(), array())))->getContent()); + } + + public function testRenderControllerReference() + { + $signer = new UriSigner('foo'); + $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(), $signer); + + $request = Request::create('/'); + $request->setLocale('fr'); + $request->headers->set('Surrogate-Capability', 'ESI/1.0'); + + $reference = new ControllerReference('main_controller', array(), array()); + $altReference = new ControllerReference('alt_controller', array(), array()); + + $this->assertEquals( + '', + $strategy->render($reference, $request, array('alt' => $altReference))->getContent() + ); + } + + /** + * @expectedException \LogicException + */ + public function testRenderControllerReferenceWithoutSignerThrowsException() + { + $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); + + $request = Request::create('/'); + $request->setLocale('fr'); + $request->headers->set('Surrogate-Capability', 'ESI/1.0'); + + $strategy->render(new ControllerReference('main_controller'), $request); + } + + /** + * @expectedException \LogicException + */ + public function testRenderAltControllerReferenceWithoutSignerThrowsException() + { + $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); + + $request = Request::create('/'); + $request->setLocale('fr'); + $request->headers->set('Surrogate-Capability', 'ESI/1.0'); + + $strategy->render('/', $request, array('alt' => new ControllerReference('alt_controller'))); } private function getInlineStrategy($called = false) diff --git a/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php b/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php index 89da98de66..e0366a52c1 100644 --- a/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php @@ -13,11 +13,49 @@ namespace Symfony\Component\Security\Core\Tests\Util; use Symfony\Component\Security\Core\Util\StringUtils; +/** + * Data from PHP.net's hash_equals tests + */ class StringUtilsTest extends \PHPUnit_Framework_TestCase { - public function testEquals() + public function dataProviderTrue() { - $this->assertTrue(StringUtils::equals('password', 'password')); - $this->assertFalse(StringUtils::equals('password', 'foo')); + return array( + array('same', 'same'), + array('', ''), + array(123, 123), + array(null, ''), + array(null, null), + ); + } + + public function dataProviderFalse() + { + return array( + array('not1same', 'not2same'), + array('short', 'longer'), + array('longer', 'short'), + array('', 'notempty'), + array('notempty', ''), + array(123, 'NaN'), + array('NaN', 123), + array(null, 123), + ); + } + + /** + * @dataProvider dataProviderTrue + */ + public function testEqualsTrue($known, $user) + { + $this->assertTrue(StringUtils::equals($known, $user)); + } + + /** + * @dataProvider dataProviderFalse + */ + public function testEqualsFalse($known, $user) + { + $this->assertFalse(StringUtils::equals($known, $user)); } } diff --git a/src/Symfony/Component/Security/Core/Util/StringUtils.php b/src/Symfony/Component/Security/Core/Util/StringUtils.php index d47bd4bb37..5e130375b7 100644 --- a/src/Symfony/Component/Security/Core/Util/StringUtils.php +++ b/src/Symfony/Component/Security/Core/Util/StringUtils.php @@ -35,23 +35,19 @@ class StringUtils */ public static function equals($knownString, $userInput) { - // Prevent issues if string length is 0 - $knownString .= chr(0); - $userInput .= chr(0); - $knownLen = strlen($knownString); $userLen = strlen($userInput); + // Extend the known string to avoid uninitialized string offsets + $knownString .= $userInput; + // Set the result to the difference between the lengths $result = $knownLen - $userLen; // Note that we ALWAYS iterate over the user-supplied length // This is to prevent leaking length information for ($i = 0; $i < $userLen; $i++) { - // Using % here is a trick to prevent notices - // It's safe, since if the lengths are different - // $result is already non-0 - $result |= (ord($knownString[$i % $knownLen]) ^ ord($userInput[$i])); + $result |= (ord($knownString[$i]) ^ ord($userInput[$i])); } // They are only identical strings if $result is exactly 0... diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index f6ae7d03de..32ae7650ff 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -316,7 +316,7 @@ class Translator implements TranslatorInterface * * @throws \InvalidArgumentException If the locale contains invalid characters */ - private function assertValidLocale($locale) + protected function assertValidLocale($locale) { if (1 !== preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));