diff --git a/CHANGELOG b/CHANGELOG index 2b8373edd..8feae8baf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,7 +15,7 @@ * 1.22.3 (2015-XX-XX) - * n/a + * changed template cache names to take into account the Twig C extension * 1.22.2 (2015-09-22) diff --git a/lib/Twig/Cache/Filesystem.php b/lib/Twig/Cache/Filesystem.php index 2e1c0a069..463104463 100644 --- a/lib/Twig/Cache/Filesystem.php +++ b/lib/Twig/Cache/Filesystem.php @@ -38,7 +38,7 @@ class Twig_Cache_Filesystem implements Twig_CacheInterface { $hash = hash('sha256', $className); - return $this->directory.'/'.$hash[0].'/'.$hash[1].'/'.$hash.'.php'; + return $this->directory.'/'.$hash[0].$hash[1].'/'.$hash.'.php'; } /** diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 0e3322358..fd026f217 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -248,14 +248,22 @@ class Twig_Environment /** * Gets the template class associated with the given string. * - * @param string $name The name for which to calculate the template class name - * @param int $index The index if it is an embedded template + * The generated template class is based on the following parameters: + * + * * The cache key for the given template; + * * The currently enabled extensions; + * * Whether the Twig C extension is available or not. + * + * @param string $name The name for which to calculate the template class name + * @param int|null $index The index if it is an embedded template * * @return string The template class name */ public function getTemplateClass($name, $index = null) { - $key = $this->getLoader()->getCacheKey($name).'__'.implode('__', array_keys($this->extensions)); + $key = $this->getLoader()->getCacheKey($name); + $key .= json_encode(array_keys($this->extensions)); + $key .= function_exists('twig_template_get_attributes'); return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '_'.$index); } diff --git a/lib/Twig/Extension.php b/lib/Twig/Extension.php index 5d3f0bcf7..7a3c85985 100644 --- a/lib/Twig/Extension.php +++ b/lib/Twig/Extension.php @@ -11,20 +11,14 @@ abstract class Twig_Extension implements Twig_ExtensionInterface { /** - * Initializes the runtime environment. - * - * This is where you can load some file that contains filter functions for instance. - * - * @param Twig_Environment $environment The current Twig_Environment instance + * {@inheritdoc} */ public function initRuntime(Twig_Environment $environment) { } /** - * Returns the token parser instances to add to the existing list. - * - * @return array An array of Twig_TokenParserInterface instances + * {@inheritdoc} */ public function getTokenParsers() { @@ -32,9 +26,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns the node visitor instances to add to the existing list. - * - * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances + * {@inheritdoc} */ public function getNodeVisitors() { @@ -42,9 +34,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns a list of filters to add to the existing list. - * - * @return array An array of filters + * {@inheritdoc} */ public function getFilters() { @@ -52,9 +42,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns a list of tests to add to the existing list. - * - * @return array An array of tests + * {@inheritdoc} */ public function getTests() { @@ -62,9 +50,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns a list of functions to add to the existing list. - * - * @return array An array of functions + * {@inheritdoc} */ public function getFunctions() { @@ -72,9 +58,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns a list of operators to add to the existing list. - * - * @return array An array of operators + * {@inheritdoc} */ public function getOperators() { @@ -82,9 +66,7 @@ abstract class Twig_Extension implements Twig_ExtensionInterface } /** - * Returns a list of global variables to add to the existing list. - * - * @return array An array of global variables + * {@inheritdoc} */ public function getGlobals() { diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 03abd1913..2474429fa 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -966,7 +966,7 @@ function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', if (!is_string($string)) { if (is_object($string) && method_exists($string, '__toString')) { $string = (string) $string; - } else { + } elseif (in_array($strategy, array('html', 'js', 'css', 'html_attr', 'url'))) { return $string; } } diff --git a/lib/Twig/ExtensionInterface.php b/lib/Twig/ExtensionInterface.php index 2d5a52b05..92abf7db2 100644 --- a/lib/Twig/ExtensionInterface.php +++ b/lib/Twig/ExtensionInterface.php @@ -28,7 +28,7 @@ interface Twig_ExtensionInterface /** * Returns the token parser instances to add to the existing list. * - * @return array An array of Twig_TokenParserInterface instances + * @return Twig_TokenParserInterface[] */ public function getTokenParsers(); @@ -42,21 +42,21 @@ interface Twig_ExtensionInterface /** * Returns a list of filters to add to the existing list. * - * @return array An array of filters + * @return Twig_SimpleFilter[] */ public function getFilters(); /** * Returns a list of tests to add to the existing list. * - * @return array An array of tests + * @return Twig_SimpleTest[] */ public function getTests(); /** * Returns a list of functions to add to the existing list. * - * @return array An array of functions + * @return Twig_SimpleFunction[] */ public function getFunctions(); diff --git a/lib/Twig/Test/NodeTestCase.php b/lib/Twig/Test/NodeTestCase.php index d75e98485..e77dbf33f 100644 --- a/lib/Twig/Test/NodeTestCase.php +++ b/lib/Twig/Test/NodeTestCase.php @@ -25,7 +25,7 @@ abstract class Twig_Test_NodeTestCase extends PHPUnit_Framework_TestCase $compiler = $this->getCompiler($environment); $compiler->compile($node); - $this->assertEquals($source, trim($compiler->getSource())); + $this->assertStringMatchesFormat($source, trim($compiler->getSource())); } protected function getCompiler(Twig_Environment $environment = null) diff --git a/test/Twig/Tests/Cache/FilesystemTest.php b/test/Twig/Tests/Cache/FilesystemTest.php new file mode 100644 index 000000000..20e83efd5 --- /dev/null +++ b/test/Twig/Tests/Cache/FilesystemTest.php @@ -0,0 +1,169 @@ +nonce = hash('sha256', uniqid(mt_rand(), true)); + $this->classname = '__Twig_Tests_Cache_FilesystemTest_Template_'.$this->nonce; + $this->directory = sys_get_temp_dir().'/twig-test-'.$this->nonce; + $this->cache = new Twig_Cache_Filesystem($this->directory); + } + + protected function tearDown() + { + if (file_exists($this->directory)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory), RecursiveIteratorIterator::CHILD_FIRST); + foreach ($iterator as $filename => $fileInfo) { + if (!$iterator->isDot()) { + if ($fileInfo->isDir()) { + rmdir($filename); + } else { + unlink($filename); + } + } + } + rmdir($this->directory); + } + } + + public function testLoad() + { + $key = $this->directory.'/cache/cachefile.php'; + + $dir = dirname($key); + @mkdir($dir, 0777, true); + $this->assertTrue(is_dir($dir)); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + + $this->cache->load($key); + + $this->assertTrue(class_exists($this->classname, false)); + } + + public function testLoadMissing() + { + $key = $this->directory.'/cache/cachefile.php'; + + $this->assertFalse(class_exists($this->classname, false)); + + $this->cache->load($key); + + $this->assertFalse(class_exists($this->classname, false)); + } + + public function testWrite() + { + $key = $this->directory.'/cache/cachefile.php'; + $content = $this->generateSource(); + + $this->assertFalse(file_exists($key)); + $this->assertFalse(file_exists($this->directory)); + + $this->cache->write($key, $content); + + $this->assertTrue(file_exists($this->directory)); + $this->assertTrue(file_exists($key)); + $this->assertSame(file_get_contents($key), $content); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessageRegExp #^Unable to create the cache directory # + */ + public function testWriteFailMkdir() + { + $key = $this->directory.'/cache/cachefile.php'; + $content = $this->generateSource(); + + $this->assertFalse(file_exists($key)); + + // Create read-only root directory. + @mkdir($this->directory, 0555, true); + $this->assertTrue(is_dir($this->directory)); + + $this->cache->write($key, $content); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessageRegExp #^Unable to write in the cache directory # + */ + public function testWriteFailDirWritable() + { + $key = $this->directory.'/cache/cachefile.php'; + $content = $this->generateSource(); + + $this->assertFalse(file_exists($key)); + + // Create root directory. + @mkdir($this->directory, 0777, true); + // Create read-only subdirectory. + @mkdir($this->directory.'/cache' , 0555); + $this->assertTrue(is_dir($this->directory.'/cache')); + + $this->cache->write($key, $content); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessageRegExp #^Failed to write cache file # + */ + public function testWriteFailWriteFile() + { + $key = $this->directory.'/cache/cachefile.php'; + $content = $this->generateSource(); + + $this->assertFalse(file_exists($key)); + + // Create a directory in the place of the cache file. + @mkdir($key, 0777, true); + $this->assertTrue(is_dir($key)); + + $this->cache->write($key, $content); + } + + public function testGetTimestamp() + { + $key = $this->directory.'/cache/cachefile.php'; + + $dir = dirname($key); + @mkdir($dir, 0777, true); + $this->assertTrue(is_dir($dir)); + + // Create the file with a specific modification time. + touch($key, 1234567890); + + $this->assertSame(1234567890, $this->cache->getTimestamp($key)); + } + + public function testGetTimestampMissingFile() + { + $key = $this->directory.'/cache/cachefile.php'; + $this->assertSame(0, $this->cache->getTimestamp($key)); + } + + private function generateSource() + { + return strtr(' $this->classname, + )); + } +} diff --git a/test/Twig/Tests/EnvironmentTest.php b/test/Twig/Tests/EnvironmentTest.php index cc48909aa..c74d169aa 100644 --- a/test/Twig/Tests/EnvironmentTest.php +++ b/test/Twig/Tests/EnvironmentTest.php @@ -165,6 +165,85 @@ class Twig_Tests_EnvironmentTest extends PHPUnit_Framework_TestCase unlink($key); } + public function testAutoReloadCacheMiss() + { + $templateName = __FUNCTION__; + $templateContent = __FUNCTION__; + + $cache = $this->getMock('Twig_CacheInterface'); + $loader = $this->getMockLoader($templateName, $templateContent); + $twig = new Twig_Environment($loader, array('cache' => $cache, 'auto_reload' => true, 'debug' => false)); + + // Cache miss: getTimestamp returns 0 and as a result the load() is + // skipped. + $cache->expects($this->once()) + ->method('generateKey') + ->will($this->returnValue('key')); + $cache->expects($this->once()) + ->method('getTimestamp') + ->will($this->returnValue(0)); + $loader->expects($this->never()) + ->method('isFresh'); + $cache->expects($this->never()) + ->method('load'); + + $twig->loadTemplate($templateName); + } + + public function testAutoReloadCacheHit() + { + $templateName = __FUNCTION__; + $templateContent = __FUNCTION__; + + $cache = $this->getMock('Twig_CacheInterface'); + $loader = $this->getMockLoader($templateName, $templateContent); + $twig = new Twig_Environment($loader, array('cache' => $cache, 'auto_reload' => true, 'debug' => false)); + + $now = time(); + + // Cache hit: getTimestamp returns something > extension timestamps and + // the loader returns true for isFresh(). + $cache->expects($this->once()) + ->method('generateKey') + ->will($this->returnValue('key')); + $cache->expects($this->once()) + ->method('getTimestamp') + ->will($this->returnValue($now)); + $loader->expects($this->once()) + ->method('isFresh') + ->will($this->returnValue(true)); + $cache->expects($this->once()) + ->method('load'); + + $twig->loadTemplate($templateName); + } + + public function testAutoReloadOutdatedCacheHit() + { + $templateName = __FUNCTION__; + $templateContent = __FUNCTION__; + + $cache = $this->getMock('Twig_CacheInterface'); + $loader = $this->getMockLoader($templateName, $templateContent); + $twig = new Twig_Environment($loader, array('cache' => $cache, 'auto_reload' => true, 'debug' => false)); + + $now = time(); + + $cache->expects($this->once()) + ->method('generateKey') + ->will($this->returnValue('key')); + $cache->expects($this->once()) + ->method('getTimestamp') + ->will($this->returnValue($now)); + $loader->expects($this->once()) + ->method('isFresh') + ->will($this->returnValue(false)); + $cache->expects($this->never()) + ->method('load'); + + $twig->loadTemplate($templateName); + } + public function testAddExtension() { $twig = new Twig_Environment($this->getMock('Twig_LoaderInterface')); @@ -180,6 +259,21 @@ class Twig_Tests_EnvironmentTest extends PHPUnit_Framework_TestCase $visitors = $twig->getNodeVisitors(); $this->assertEquals('Twig_Tests_EnvironmentTest_NodeVisitor', get_class($visitors[2])); } + + protected function getMockLoader($templateName, $templateContent) + { + $loader = $this->getMock('Twig_LoaderInterface'); + $loader->expects($this->any()) + ->method('getSource') + ->with($templateName) + ->will($this->returnValue($templateContent)); + $loader->expects($this->any()) + ->method('getCacheKey') + ->with($templateName) + ->will($this->returnValue($templateName)); + + return $loader; + } } class Twig_Tests_EnvironmentTest_Extension extends Twig_Extension diff --git a/test/Twig/Tests/Extension/CoreTest.php b/test/Twig/Tests/Extension/CoreTest.php index a4692c267..cd49e2f5e 100644 --- a/test/Twig/Tests/Extension/CoreTest.php +++ b/test/Twig/Tests/Extension/CoreTest.php @@ -121,6 +121,8 @@ class Twig_Tests_Extension_CoreTest extends PHPUnit_Framework_TestCase $twig->getExtension('core')->setEscaper('foo', 'foo_escaper_for_test'); $this->assertEquals('fooUTF-8', twig_escape_filter($twig, 'foo', 'foo')); + $this->assertEquals('UTF-8', twig_escape_filter($twig, null, 'foo')); + $this->assertEquals('42UTF-8', twig_escape_filter($twig, 42, 'foo')); } /** diff --git a/test/Twig/Tests/Node/ModuleTest.php b/test/Twig/Tests/Node/ModuleTest.php index 19a4be9ff..ca784e51e 100644 --- a/test/Twig/Tests/Node/ModuleTest.php +++ b/test/Twig/Tests/Node/ModuleTest.php @@ -46,7 +46,7 @@ class Twig_Tests_Node_ModuleTest extends Twig_Test_NodeTestCase