diff --git a/app/cache.inc.php b/app/cache.inc.php index aa20bcc0..a18ca80c 100644 --- a/app/cache.inc.php +++ b/app/cache.inc.php @@ -68,7 +68,8 @@ function save_full_cache($full_cache, $hash) { } function create_full_cache($pages = null) { - $search_fields = array('url', 'file_path', 'title', 'thumb', 'content', 'date', 'slug'); + # $search_fields = array('thumb', 'parents', 'url', 'file_path', 'title', 'heading', 'author', 'byline', 'content', 'subheading', 'intro', 'architect', 'facts', 'photo', 'address'); + $search_fields = array('thumb', 'url', 'slug', 'file_path', 'title', 'date', 'content', 'root_path', 'permalink', 'parents', 'parent'); $store = array(); if (!isset($pages)) $pages = Helpers::file_cache('./content'); foreach ($pages as $page) { diff --git a/app/parsers/Twig/Autoloader.inc.php b/app/parsers/Twig/Autoloader.inc.php old mode 100755 new mode 100644 index a93b8caf..7007d315 --- a/app/parsers/Twig/Autoloader.inc.php +++ b/app/parsers/Twig/Autoloader.inc.php @@ -12,28 +12,30 @@ /** * Autoloads Twig classes. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Autoloader { /** * Registers Twig_Autoloader as an SPL autoloader. + * + * @param Boolean $prepend Whether to prepend the autoloader or not. */ - static public function register() + public static function register($prepend = false) { - ini_set('unserialize_callback_func', 'spl_autoload_call'); - spl_autoload_register(array(new self, 'autoload')); + if (version_compare(phpversion(), '5.3.0', '>=')) { + spl_autoload_register(array(new self, 'autoload'), true, $prepend); + } else { + spl_autoload_register(array(new self, 'autoload')); + } } /** * Handles autoloading of classes. * - * @param string $class A class name. - * - * @return boolean Returns true if the class has been loaded + * @param string $class A class name. */ - static public function autoload($class) + public static function autoload($class) { if (0 !== strpos($class, 'Twig')) { return; diff --git a/app/parsers/Twig/Compiler.php b/app/parsers/Twig/Compiler.php old mode 100755 new mode 100644 index db2e8de4..99aecbcc --- a/app/parsers/Twig/Compiler.php +++ b/app/parsers/Twig/Compiler.php @@ -13,8 +13,7 @@ /** * Compiles a node to PHP code. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Compiler implements Twig_CompilerInterface { @@ -22,6 +21,10 @@ class Twig_Compiler implements Twig_CompilerInterface protected $source; protected $indentation; protected $env; + protected $debugInfo; + protected $sourceOffset; + protected $sourceLine; + protected $filename; /** * Constructor. @@ -31,6 +34,12 @@ class Twig_Compiler implements Twig_CompilerInterface public function __construct(Twig_Environment $env) { $this->env = $env; + $this->debugInfo = array(); + } + + public function getFilename() + { + return $this->filename; } /** @@ -56,8 +65,8 @@ public function getSource() /** * Compiles a node. * - * @param Twig_NodeInterface $node The node to compile - * @param integer $indent The current indentation + * @param Twig_NodeInterface $node The node to compile + * @param integer $indentation The current indentation * * @return Twig_Compiler The current compiler instance */ @@ -65,8 +74,15 @@ public function compile(Twig_NodeInterface $node, $indentation = 0) { $this->lastLine = null; $this->source = ''; + $this->sourceOffset = 0; + // source code starts at 1 (as we then increment it when we encounter new lines) + $this->sourceLine = 1; $this->indentation = $indentation; + if ($node instanceof Twig_Node_Module) { + $this->filename = $node->getAttribute('filename'); + } + $node->compile($this); return $this; @@ -86,7 +102,7 @@ public function subcompile(Twig_NodeInterface $node, $raw = true) /** * Adds a raw string to the compiled code. * - * @param string $string The string + * @param string $string The string * * @return Twig_Compiler The current compiler instance */ @@ -113,6 +129,11 @@ public function write() return $this; } + /** + * Appends an indentation to the current PHP code after compilation. + * + * @return Twig_Compiler The current compiler instance + */ public function addIndentation() { $this->source .= str_repeat(' ', $this->indentation * 4); @@ -123,7 +144,7 @@ public function addIndentation() /** * Adds a quoted string to the compiled code. * - * @param string $string The string + * @param string $value The string * * @return Twig_Compiler The current compiler instance */ @@ -137,19 +158,27 @@ public function string($value) /** * Returns a PHP representation of a given value. * - * @param mixed $value The value to convert + * @param mixed $value The value to convert * * @return Twig_Compiler The current compiler instance */ public function repr($value) { if (is_int($value) || is_float($value)) { + if (false !== $locale = setlocale(LC_NUMERIC, 0)) { + setlocale(LC_NUMERIC, 'C'); + } + $this->raw($value); - } else if (null === $value) { + + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + } elseif (null === $value) { $this->raw('null'); - } else if (is_bool($value)) { + } elseif (is_bool($value)) { $this->raw($value ? 'true' : 'false'); - } else if (is_array($value)) { + } elseif (is_array($value)) { $this->raw('array('); $i = 0; foreach ($value as $key => $value) { @@ -178,17 +207,35 @@ public function repr($value) public function addDebugInfo(Twig_NodeInterface $node) { if ($node->getLine() != $this->lastLine) { - $this->lastLine = $node->getLine(); $this->write("// line {$node->getLine()}\n"); + + // when mbstring.func_overload is set to 2 + // mb_substr_count() replaces substr_count() + // but they have different signatures! + if (((int) ini_get('mbstring.func_overload')) & 2) { + // this is much slower than the "right" version + $this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "\n"); + } else { + $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); + } + $this->sourceOffset = strlen($this->source); + $this->debugInfo[$this->sourceLine] = $node->getLine(); + + $this->lastLine = $node->getLine(); } return $this; } + public function getDebugInfo() + { + return $this->debugInfo; + } + /** * Indents the generated code. * - * @param integer $indent The number of indentation to add + * @param integer $step The number of indentation to add * * @return Twig_Compiler The current compiler instance */ @@ -202,18 +249,19 @@ public function indent($step = 1) /** * Outdents the generated code. * - * @param integer $indent The number of indentation to remove + * @param integer $step The number of indentation to remove * * @return Twig_Compiler The current compiler instance */ public function outdent($step = 1) { - $this->indentation -= $step; - - if ($this->indentation < 0) { - throw new Twig_Error('Unable to call outdent() as the indentation would become negative'); + // can't outdent by more steps than the current indentation level + if ($this->indentation < $step) { + throw new LogicException('Unable to call outdent() as the indentation would become negative'); } + $this->indentation -= $step; + return $this; } } diff --git a/app/parsers/Twig/CompilerInterface.php b/app/parsers/Twig/CompilerInterface.php old mode 100755 new mode 100644 index 0a13edf2..e293ec91 --- a/app/parsers/Twig/CompilerInterface.php +++ b/app/parsers/Twig/CompilerInterface.php @@ -12,24 +12,24 @@ /** * Interface implemented by compiler classes. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_CompilerInterface { /** * Compiles a node. * - * @param Twig_NodeInterface $node The node to compile + * @param Twig_NodeInterface $node The node to compile * * @return Twig_CompilerInterface The current compiler instance */ - function compile(Twig_NodeInterface $node); + public function compile(Twig_NodeInterface $node); /** * Gets the current PHP code after compilation. * * @return string The PHP code */ - function getSource(); + public function getSource(); } diff --git a/app/parsers/Twig/Environment.php b/app/parsers/Twig/Environment.php old mode 100755 new mode 100644 index e93a1870..3afa73d6 --- a/app/parsers/Twig/Environment.php +++ b/app/parsers/Twig/Environment.php @@ -12,12 +12,11 @@ /** * Stores the Twig configuration. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Environment { - const VERSION = '1.4.0-RC2'; + const VERSION = '1.13.1'; protected $charset; protected $loader; @@ -36,6 +35,7 @@ class Twig_Environment protected $functions; protected $globals; protected $runtimeInitialized; + protected $extensionInitialized; protected $loadedTemplates; protected $strictVariables; protected $unaryOperators; @@ -50,17 +50,16 @@ class Twig_Environment * * Available options: * - * * debug: When set to `true`, the generated templates have a __toString() - * method that you can use to display the generated nodes (default to - * false). + * * debug: When set to true, it automatically set "auto_reload" to true as + * well (default to false). * - * * charset: The charset used by the templates (default to utf-8). + * * charset: The charset used by the templates (default to UTF-8). * * * base_template_class: The base template class to use for generated * templates (default to Twig_Template). * * * cache: An absolute path where to store the compiled templates, or - * false to disable compilation cache (default) + * false to disable compilation cache (default). * * * auto_reload: Whether to reload the template is the original source changed. * If you don't provide the auto_reload option, it will be @@ -69,14 +68,18 @@ class Twig_Environment * * strict_variables: Whether to ignore invalid variables in templates * (default to false). * - * * autoescape: Whether to enable auto-escaping (default to true); + * * autoescape: Whether to enable auto-escaping (default to html): + * * false: disable auto-escaping + * * true: equivalent to html + * * html, js: set the autoescaping to one of the supported strategies + * * PHP callback: a PHP callback that returns an escaping strategy based on the template "filename" * * * optimizations: A flag that indicates which optimizations to apply * (default to -1 which means that all optimizations are enabled; - * set it to 0 to disable) + * set it to 0 to disable). * - * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance - * @param array $options An array of options + * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance + * @param array $options An array of options */ public function __construct(Twig_LoaderInterface $loader = null, $options = array()) { @@ -89,26 +92,27 @@ public function __construct(Twig_LoaderInterface $loader = null, $options = arra 'charset' => 'UTF-8', 'base_template_class' => 'Twig_Template', 'strict_variables' => false, - 'autoescape' => true, + 'autoescape' => 'html', 'cache' => false, 'auto_reload' => null, 'optimizations' => -1, ), $options); $this->debug = (bool) $options['debug']; - $this->charset = $options['charset']; + $this->charset = strtoupper($options['charset']); $this->baseTemplateClass = $options['base_template_class']; $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; - $this->extensions = array( - 'core' => new Twig_Extension_Core(), - 'escaper' => new Twig_Extension_Escaper((bool) $options['autoescape']), - 'optimizer' => new Twig_Extension_Optimizer($options['optimizations']), - ); $this->strictVariables = (bool) $options['strict_variables']; $this->runtimeInitialized = false; $this->setCache($options['cache']); $this->functionCallbacks = array(); $this->filterCallbacks = array(); + + $this->addExtension(new Twig_Extension_Core()); + $this->addExtension(new Twig_Extension_Escaper($options['autoescape'])); + $this->addExtension(new Twig_Extension_Optimizer($options['optimizations'])); + $this->extensionInitialized = false; + $this->staging = new Twig_Extension_Staging(); } /** @@ -251,13 +255,14 @@ public function getCacheFilename($name) /** * Gets the template class associated with the given string. * - * @param string $name The name for which to calculate the template class name + * @param string $name The name for which to calculate the template class name + * @param integer $index The index if it is an embedded template * * @return string The template class name */ - public function getTemplateClass($name) + public function getTemplateClass($name, $index = null) { - return $this->templateClassPrefix.md5($this->loader->getCacheKey($name)); + return $this->templateClassPrefix.md5($this->getLoader()->getCacheKey($name)).(null === $index ? '' : '_'.$index); } /** @@ -297,13 +302,14 @@ public function display($name, array $context = array()) /** * Loads a template by name. * - * @param string $name The template name + * @param string $name The template name + * @param integer $index The index if it is an embedded template * * @return Twig_TemplateInterface A template instance representing the given template name */ - public function loadTemplate($name) + public function loadTemplate($name, $index = null) { - $cls = $this->getTemplateClass($name); + $cls = $this->getTemplateClass($name, $index); if (isset($this->loadedTemplates[$cls])) { return $this->loadedTemplates[$cls]; @@ -311,10 +317,10 @@ public function loadTemplate($name) if (!class_exists($cls, false)) { if (false === $cache = $this->getCacheFilename($name)) { - eval('?>'.$this->compileSource($this->loader->getSource($name), $name)); + eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name)); } else { if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) { - $this->writeCacheFile($cache, $this->compileSource($this->loader->getSource($name), $name)); + $this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name)); } require_once $cache; @@ -349,7 +355,7 @@ public function isTemplateFresh($name, $time) } } - return $this->loader->isFresh($name, $time); + return $this->getLoader()->isFresh($name, $time); } public function resolveTemplate($names) @@ -546,6 +552,10 @@ public function setLoader(Twig_LoaderInterface $loader) */ public function getLoader() { + if (null === $this->loader) { + throw new LogicException('You must set a loader first.'); + } + return $this->loader; } @@ -556,7 +566,7 @@ public function getLoader() */ public function setCharset($charset) { - $this->charset = $charset; + $this->charset = strtoupper($charset); } /** @@ -616,16 +626,28 @@ public function getExtension($name) */ public function addExtension(Twig_ExtensionInterface $extension) { + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName())); + } + $this->extensions[$extension->getName()] = $extension; } /** * Removes an extension by name. * + * This method is deprecated and you should not use it. + * * @param string $name The extension name + * + * @deprecated since 1.12 (to be removed in 2.0) */ public function removeExtension($name) { + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name)); + } + unset($this->extensions[$name]); } @@ -658,40 +680,44 @@ public function getExtensions() */ public function addTokenParser(Twig_TokenParserInterface $parser) { - $this->staging['token_parsers'][] = $parser; + if ($this->extensionInitialized) { + throw new LogicException('Unable to add a token parser as extensions have already been initialized.'); + } + + $this->staging->addTokenParser($parser); } /** * Gets the registered Token Parsers. * - * @return Twig_TokenParserInterface[] An array of Twig_TokenParserInterface instances + * @return Twig_TokenParserBrokerInterface A broker containing token parsers */ public function getTokenParsers() { - if (null === $this->parsers) { - $this->parsers = new Twig_TokenParserBroker(); + if (!$this->extensionInitialized) { + $this->initExtensions(); + } - if (isset($this->staging['token_parsers'])) { - foreach ($this->staging['token_parsers'] as $parser) { - $this->parsers->addTokenParser($parser); - } - } + return $this->parsers; + } - foreach ($this->getExtensions() as $extension) { - $parsers = $extension->getTokenParsers(); - foreach($parsers as $parser) { - if ($parser instanceof Twig_TokenParserInterface) { - $this->parsers->addTokenParser($parser); - } else if ($parser instanceof Twig_TokenParserBrokerInterface) { - $this->parsers->addTokenParserBroker($parser); - } else { - throw new Twig_Error_Runtime('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances'); - } - } + /** + * Gets registered tags. + * + * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes. + * + * @return Twig_TokenParserInterface[] An array of Twig_TokenParserInterface instances + */ + public function getTags() + { + $tags = array(); + foreach ($this->getTokenParsers()->getParsers() as $parser) { + if ($parser instanceof Twig_TokenParserInterface) { + $tags[$parser->getTag()] = $parser; } } - return $this->parsers; + return $tags; } /** @@ -701,7 +727,11 @@ public function getTokenParsers() */ public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) { - $this->staging['visitors'][] = $visitor; + if ($this->extensionInitialized) { + throw new LogicException('Unable to add a node visitor as extensions have already been initialized.', $extension->getName()); + } + + $this->staging->addNodeVisitor($visitor); } /** @@ -711,11 +741,8 @@ public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) */ public function getNodeVisitors() { - if (null === $this->visitors) { - $this->visitors = isset($this->staging['visitors']) ? $this->staging['visitors'] : array(); - foreach ($this->getExtensions() as $extension) { - $this->visitors = array_merge($this->visitors, $extension->getNodeVisitors()); - } + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->visitors; @@ -724,12 +751,25 @@ public function getNodeVisitors() /** * Registers a Filter. * - * @param string $name The filter name - * @param Twig_FilterInterface $visitor A Twig_FilterInterface instance + * @param string|Twig_SimpleFilter $name The filter name or a Twig_SimpleFilter instance + * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance */ - public function addFilter($name, Twig_FilterInterface $filter) + public function addFilter($name, $filter = null) { - $this->staging['filters'][$name] = $filter; + if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) { + throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter'); + } + + if ($name instanceof Twig_SimpleFilter) { + $filter = $name; + $name = $filter->getName(); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addFilter($name, $filter); } /** @@ -740,18 +780,31 @@ public function addFilter($name, Twig_FilterInterface $filter) * * @param string $name The filter name * - * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exists + * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist */ public function getFilter($name) { - if (null === $this->filters) { - $this->getFilters(); + if (!$this->extensionInitialized) { + $this->initExtensions(); } if (isset($this->filters[$name])) { return $this->filters[$name]; } + foreach ($this->filters as $pattern => $filter) { + $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); + + if ($count) { + if (preg_match('#^'.$pattern.'$#', $name, $matches)) { + array_shift($matches); + $filter->setArguments($matches); + + return $filter; + } + } + } + foreach ($this->filterCallbacks as $callback) { if (false !== $filter = call_user_func($callback, $name)) { return $filter; @@ -769,15 +822,16 @@ public function registerUndefinedFilterCallback($callable) /** * Gets the registered Filters. * + * Be warned that this method cannot return filters defined with registerUndefinedFunctionCallback. + * * @return Twig_FilterInterface[] An array of Twig_FilterInterface instances + * + * @see registerUndefinedFilterCallback */ public function getFilters() { - if (null === $this->filters) { - $this->filters = isset($this->staging['filters']) ? $this->staging['filters'] : array(); - foreach ($this->getExtensions() as $extension) { - $this->filters = array_merge($this->filters, $extension->getFilters()); - } + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->filters; @@ -786,12 +840,25 @@ public function getFilters() /** * Registers a Test. * - * @param string $name The test name - * @param Twig_TestInterface $visitor A Twig_TestInterface instance + * @param string|Twig_SimpleTest $name The test name or a Twig_SimpleTest instance + * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance */ - public function addTest($name, Twig_TestInterface $test) + public function addTest($name, $test = null) { - $this->staging['tests'][$name] = $test; + if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) { + throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest'); + } + + if ($name instanceof Twig_SimpleTest) { + $test = $name; + $name = $test->getName(); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addTest($name, $test); } /** @@ -801,25 +868,55 @@ public function addTest($name, Twig_TestInterface $test) */ public function getTests() { - if (null === $this->tests) { - $this->tests = isset($this->staging['tests']) ? $this->staging['tests'] : array(); - foreach ($this->getExtensions() as $extension) { - $this->tests = array_merge($this->tests, $extension->getTests()); - } + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->tests; } + /** + * Gets a test by name. + * + * @param string $name The test name + * + * @return Twig_Test|false A Twig_Test instance or false if the test does not exist + */ + public function getTest($name) + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + if (isset($this->tests[$name])) { + return $this->tests[$name]; + } + + return false; + } + /** * Registers a Function. * - * @param string $name The function name - * @param Twig_FunctionInterface $function A Twig_FunctionInterface instance + * @param string|Twig_SimpleFunction $name The function name or a Twig_SimpleFunction instance + * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance */ - public function addFunction($name, Twig_FunctionInterface $function) + public function addFunction($name, $function = null) { - $this->staging['functions'][$name] = $function; + if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) { + throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction'); + } + + if ($name instanceof Twig_SimpleFunction) { + $function = $name; + $name = $function->getName(); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addFunction($name, $function); } /** @@ -830,18 +927,31 @@ public function addFunction($name, Twig_FunctionInterface $function) * * @param string $name function name * - * @return Twig_Function|false A Twig_Function instance or false if the function does not exists + * @return Twig_Function|false A Twig_Function instance or false if the function does not exist */ public function getFunction($name) { - if (null === $this->functions) { - $this->getFunctions(); + if (!$this->extensionInitialized) { + $this->initExtensions(); } if (isset($this->functions[$name])) { return $this->functions[$name]; } + foreach ($this->functions as $pattern => $function) { + $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); + + if ($count) { + if (preg_match('#^'.$pattern.'$#', $name, $matches)) { + array_shift($matches); + $function->setArguments($matches); + + return $function; + } + } + } + foreach ($this->functionCallbacks as $callback) { if (false !== $function = call_user_func($callback, $name)) { return $function; @@ -856,13 +966,19 @@ public function registerUndefinedFunctionCallback($callable) $this->functionCallbacks[] = $callable; } + /** + * Gets registered functions. + * + * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback. + * + * @return Twig_FunctionInterface[] An array of Twig_FunctionInterface instances + * + * @see registerUndefinedFunctionCallback + */ public function getFunctions() { - if (null === $this->functions) { - $this->functions = isset($this->staging['functions']) ? $this->staging['functions'] : array(); - foreach ($this->getExtensions() as $extension) { - $this->functions = array_merge($this->functions, $extension->getFunctions()); - } + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->functions; @@ -871,12 +987,32 @@ public function getFunctions() /** * Registers a Global. * + * New globals can be added before compiling or rendering a template; + * but after, you can only update existing globals. + * * @param string $name The global name * @param mixed $value The global value */ public function addGlobal($name, $value) { - $this->staging['globals'][$name] = $value; + if ($this->extensionInitialized || $this->runtimeInitialized) { + if (null === $this->globals) { + $this->globals = $this->initGlobals(); + } + + /* This condition must be uncommented in Twig 2.0 + if (!array_key_exists($name, $this->globals)) { + throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + } + */ + } + + if ($this->extensionInitialized || $this->runtimeInitialized) { + // update the value + $this->globals[$name] = $value; + } else { + $this->staging->addGlobal($name, $value); + } } /** @@ -886,16 +1022,37 @@ public function addGlobal($name, $value) */ public function getGlobals() { + if (!$this->runtimeInitialized && !$this->extensionInitialized) { + return $this->initGlobals(); + } + if (null === $this->globals) { - $this->globals = isset($this->staging['globals']) ? $this->staging['globals'] : array(); - foreach ($this->getExtensions() as $extension) { - $this->globals = array_merge($this->globals, $extension->getGlobals()); - } + $this->globals = $this->initGlobals(); } return $this->globals; } + /** + * Merges a context with the defined globals. + * + * @param array $context An array representing the context + * + * @return array The context merged with the globals + */ + public function mergeGlobals(array $context) + { + // we don't use array_merge as the context being generally + // bigger than globals, this code is faster. + foreach ($this->getGlobals() as $key => $value) { + if (!array_key_exists($key, $context)) { + $context[$key] = $value; + } + } + + return $context; + } + /** * Gets the registered unary Operators. * @@ -903,8 +1060,8 @@ public function getGlobals() */ public function getUnaryOperators() { - if (null === $this->unaryOperators) { - $this->initOperators(); + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->unaryOperators; @@ -917,24 +1074,121 @@ public function getUnaryOperators() */ public function getBinaryOperators() { - if (null === $this->binaryOperators) { - $this->initOperators(); + if (!$this->extensionInitialized) { + $this->initExtensions(); } return $this->binaryOperators; } - protected function initOperators() + public function computeAlternatives($name, $items) + { + $alternatives = array(); + foreach ($items as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = $lev; + } + } + asort($alternatives); + + return array_keys($alternatives); + } + + protected function initGlobals() + { + $globals = array(); + foreach ($this->extensions as $extension) { + $extGlob = $extension->getGlobals(); + if (!is_array($extGlob)) { + throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension))); + } + + $globals[] = $extGlob; + } + + $globals[] = $this->staging->getGlobals(); + + return call_user_func_array('array_merge', $globals); + } + + protected function initExtensions() { + if ($this->extensionInitialized) { + return; + } + + $this->extensionInitialized = true; + $this->parsers = new Twig_TokenParserBroker(); + $this->filters = array(); + $this->functions = array(); + $this->tests = array(); + $this->visitors = array(); $this->unaryOperators = array(); $this->binaryOperators = array(); - foreach ($this->getExtensions() as $extension) { - $operators = $extension->getOperators(); - if (!$operators) { - continue; + foreach ($this->extensions as $extension) { + $this->initExtension($extension); + } + $this->initExtension($this->staging); + } + + protected function initExtension(Twig_ExtensionInterface $extension) + { + // filters + foreach ($extension->getFilters() as $name => $filter) { + if ($name instanceof Twig_SimpleFilter) { + $filter = $name; + $name = $filter->getName(); + } elseif ($filter instanceof Twig_SimpleFilter) { + $name = $filter->getName(); + } + + $this->filters[$name] = $filter; + } + + // functions + foreach ($extension->getFunctions() as $name => $function) { + if ($name instanceof Twig_SimpleFunction) { + $function = $name; + $name = $function->getName(); + } elseif ($function instanceof Twig_SimpleFunction) { + $name = $function->getName(); } + $this->functions[$name] = $function; + } + + // tests + foreach ($extension->getTests() as $name => $test) { + if ($name instanceof Twig_SimpleTest) { + $test = $name; + $name = $test->getName(); + } elseif ($test instanceof Twig_SimpleTest) { + $name = $test->getName(); + } + + $this->tests[$name] = $test; + } + + // token parsers + foreach ($extension->getTokenParsers() as $parser) { + if ($parser instanceof Twig_TokenParserInterface) { + $this->parsers->addTokenParser($parser); + } elseif ($parser instanceof Twig_TokenParserBrokerInterface) { + $this->parsers->addTokenParserBroker($parser); + } else { + throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances'); + } + } + + // node visitors + foreach ($extension->getNodeVisitors() as $visitor) { + $this->visitors[] = $visitor; + } + + // operators + if ($operators = $extension->getOperators()) { if (2 !== count($operators)) { throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension))); } @@ -946,20 +1200,25 @@ protected function initOperators() protected function writeCacheFile($file, $content) { - if (!is_dir(dirname($file))) { - mkdir(dirname($file), 0777, true); + $dir = dirname($file); + if (!is_dir($dir)) { + if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new RuntimeException(sprintf("Unable to create the cache directory (%s).", $dir)); + } + } elseif (!is_writable($dir)) { + throw new RuntimeException(sprintf("Unable to write in the cache directory (%s).", $dir)); } $tmpFile = tempnam(dirname($file), basename($file)); if (false !== @file_put_contents($tmpFile, $content)) { // rename does not work on Win32 before 5.2.6 if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) { - chmod($file, 0644); + @chmod($file, 0666 & ~umask()); return; } } - throw new Twig_Error_Runtime(sprintf('Failed to write cache file "%s".', $file)); + throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file)); } } diff --git a/app/parsers/Twig/Error.php b/app/parsers/Twig/Error.php old mode 100755 new mode 100644 index 8c1c54b7..72d91a98 --- a/app/parsers/Twig/Error.php +++ b/app/parsers/Twig/Error.php @@ -12,8 +12,24 @@ /** * Twig base exception. * - * @package twig - * @author Fabien Potencier + * This exception class and its children must only be used when + * an error occurs during the loading of a template, when a syntax error + * is detected in a template, or when rendering a template. Other + * errors must use regular PHP exception classes (like when the template + * cache directory is not writable for instance). + * + * To help debugging template issues, this class tracks the original template + * name and line where the error occurred. + * + * Whenever possible, you must set these information (original template name + * and line number) yourself by passing them to the constructor. If some or all + * these information are not available from where you throw the exception, then + * this class will guess them automatically (when the line number is set to -1 + * and/or the filename is set to null). As this is a costly operation, this + * can be disabled by passing false for both the filename and the line number + * when creating a new instance of this class. + * + * @author Fabien Potencier */ class Twig_Error extends Exception { @@ -25,6 +41,15 @@ class Twig_Error extends Exception /** * Constructor. * + * Set both the line number and the filename to false to + * disable automatic guessing of the original template name + * and line number. + * + * Set the line number to -1 to enable its automatic guessing. + * Set the filename to null to enable its automatic guessing. + * + * By default, automatic guessing is enabled. + * * @param string $message The error message * @param integer $lineno The template line where the error occurred * @param string $filename The template file name where the error occurred @@ -32,22 +57,23 @@ class Twig_Error extends Exception */ public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null) { - if (-1 === $lineno || null === $filename) { - list($lineno, $filename) = $this->findTemplateInfo(null !== $previous ? $previous : $this, $lineno, $filename); + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->previous = $previous; + parent::__construct(''); + } else { + parent::__construct('', 0, $previous); } $this->lineno = $lineno; $this->filename = $filename; + + if (-1 === $this->lineno || null === $this->filename) { + $this->guessTemplateInfo(); + } + $this->rawMessage = $message; $this->updateRepr(); - - if (version_compare(PHP_VERSION, '5.3.0', '<')) { - $this->previous = $previous; - parent::__construct($this->message); - } else { - parent::__construct($this->message, 0, $previous); - } } /** @@ -104,13 +130,21 @@ public function setTemplateLine($lineno) $this->updateRepr(); } + public function guess() + { + $this->guessTemplateInfo(); + $this->updateRepr(); + } + /** * For PHP < 5.3.0, provides access to the getPrevious() method. * - * @param string $method The method name - * @param array $arguments The parameters to be passed to the method + * @param string $method The method name + * @param array $arguments The parameters to be passed to the method * * @return Exception The previous exception or null + * + * @throws BadMethodCallException */ public function __call($method, $arguments) { @@ -131,11 +165,16 @@ protected function updateRepr() $dot = true; } - if (null !== $this->filename) { - $this->message .= sprintf(' in %s', is_string($this->filename) ? '"'.$this->filename.'"' : json_encode($this->filename)); + if ($this->filename) { + if (is_string($this->filename) || (is_object($this->filename) && method_exists($this->filename, '__toString'))) { + $filename = sprintf('"%s"', $this->filename); + } else { + $filename = json_encode($this->filename); + } + $this->message .= sprintf(' in %s', $filename); } - if ($this->lineno >= 0) { + if ($this->lineno && $this->lineno >= 0) { $this->message .= sprintf(' at line %d', $this->lineno); } @@ -144,52 +183,57 @@ protected function updateRepr() } } - protected function findTemplateInfo(Exception $e, $currentLine, $currentFile) + protected function guessTemplateInfo() { - if (!function_exists('token_get_all')) { - return array($currentLine, $currentFile); + $template = null; + + if (version_compare(phpversion(), '5.3.6', '>=')) { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); + } else { + $backtrace = debug_backtrace(); } - $traces = $e->getTrace(); - foreach ($traces as $i => $trace) { - if (!isset($trace['class']) || 'Twig_Template' === $trace['class']) { - continue; + foreach ($backtrace as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof Twig_Template && 'Twig_Template' !== get_class($trace['object'])) { + if (null === $this->filename || $this->filename == $trace['object']->getTemplateName()) { + $template = $trace['object']; + } } + } - $r = new ReflectionClass($trace['class']); - if (!$r->implementsInterface('Twig_TemplateInterface')) { - continue; - } + // update template filename + if (null !== $template && null === $this->filename) { + $this->filename = $template->getTemplateName(); + } - if (!is_file($r->getFilename())) { - // probably an eval()'d code - return array($currentLine, $currentFile); - } + if (null === $template || $this->lineno > -1) { + return; + } - if (0 === $i) { - $line = $e->getLine(); - } else { - $line = isset($traces[$i - 1]['line']) ? $traces[$i - 1]['line'] : -log(0); - } + $r = new ReflectionObject($template); + $file = $r->getFileName(); - $tokens = token_get_all(file_get_contents($r->getFilename())); - $templateline = -1; - $template = null; - foreach ($tokens as $token) { - if (isset($token[2]) && $token[2] >= $line) { - return array($templateline, $template); + $exceptions = array($e = $this); + while (($e instanceof self || method_exists($e, 'getPrevious')) && $e = $e->getPrevious()) { + $exceptions[] = $e; + } + + while ($e = array_pop($exceptions)) { + $traces = $e->getTrace(); + while ($trace = array_shift($traces)) { + if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { + continue; } - if (T_COMMENT === $token[0] && null === $template && preg_match('#/\* +(.+) +\*/#', $token[1], $match)) { - $template = $match[1]; - } elseif (T_COMMENT === $token[0] && preg_match('#^//\s*line (\d+)\s*$#', $token[1], $match)) { - $templateline = $match[1]; + foreach ($template->getDebugInfo() as $codeLine => $templateLine) { + if ($codeLine <= $trace['line']) { + // update template line + $this->lineno = $templateLine; + + return; + } } } - - return array($currentLine, $template); } - - return array($currentLine, $currentFile); } } diff --git a/app/parsers/Twig/Error/Loader.php b/app/parsers/Twig/Error/Loader.php old mode 100755 new mode 100644 index 418a7760..68efb574 --- a/app/parsers/Twig/Error/Loader.php +++ b/app/parsers/Twig/Error/Loader.php @@ -12,9 +12,20 @@ /** * Exception thrown when an error occurs during template loading. * - * @package twig - * @author Fabien Potencier + * Automatic template information guessing is always turned off as + * if a template cannot be loaded, there is nothing to guess. + * However, when a template is loaded from another one, then, we need + * to find the current context and this is automatically done by + * Twig_Template::displayWithErrorHandling(). + * + * This strategy makes Twig_Environment::resolveTemplate() much faster. + * + * @author Fabien Potencier */ class Twig_Error_Loader extends Twig_Error { + public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, false, false, $previous); + } } diff --git a/app/parsers/Twig/Error/Runtime.php b/app/parsers/Twig/Error/Runtime.php old mode 100755 new mode 100644 index 8a387fa8..8b6ceddb --- a/app/parsers/Twig/Error/Runtime.php +++ b/app/parsers/Twig/Error/Runtime.php @@ -13,8 +13,7 @@ /** * Exception thrown when an error occurs at runtime. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Error_Runtime extends Twig_Error { diff --git a/app/parsers/Twig/Error/Syntax.php b/app/parsers/Twig/Error/Syntax.php old mode 100755 new mode 100644 index a2650c36..0f5c5792 --- a/app/parsers/Twig/Error/Syntax.php +++ b/app/parsers/Twig/Error/Syntax.php @@ -13,8 +13,7 @@ /** * Exception thrown when a syntax error occurs during lexing or parsing of a template. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Error_Syntax extends Twig_Error { diff --git a/app/parsers/Twig/ExistsLoaderInterface.php b/app/parsers/Twig/ExistsLoaderInterface.php new file mode 100644 index 00000000..ce434765 --- /dev/null +++ b/app/parsers/Twig/ExistsLoaderInterface.php @@ -0,0 +1,28 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_ExistsLoaderInterface +{ + /** + * Check if we have the source code of a template, given its name. + * + * @param string $name The name of the template to check if we can load + * + * @return boolean If the template source code is handled by this loader or not + */ + public function exists($name); +} diff --git a/app/parsers/Twig/ExpressionParser.php b/app/parsers/Twig/ExpressionParser.php old mode 100755 new mode 100644 index 33cc0dca..9cf19344 --- a/app/parsers/Twig/ExpressionParser.php +++ b/app/parsers/Twig/ExpressionParser.php @@ -18,8 +18,7 @@ * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm * @see http://en.wikipedia.org/wiki/Operator-precedence_parser * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_ExpressionParser { @@ -89,9 +88,19 @@ protected function parseConditionalExpression($expr) { while ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '?')) { $this->parser->getStream()->next(); - $expr2 = $this->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'The ternary operator must have a default value'); - $expr3 = $this->parseExpression(); + if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) { + $expr2 = $this->parseExpression(); + if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) { + $this->parser->getStream()->next(); + $expr3 = $this->parseExpression(); + } else { + $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine()); + } + } else { + $this->parser->getStream()->next(); + $expr2 = $expr; + $expr3 = $this->parseExpression(); + } $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); } @@ -143,31 +152,67 @@ public function parsePrimaryExpression() break; case Twig_Token::NUMBER_TYPE: - case Twig_Token::STRING_TYPE: $this->parser->getStream()->next(); $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); break; + case Twig_Token::STRING_TYPE: + case Twig_Token::INTERPOLATION_START_TYPE: + $node = $this->parseStringExpression(); + break; + default: if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) { $node = $this->parseArrayExpression(); } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseHashExpression(); } else { - throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine()); + throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine(), $this->parser->getFilename()); } } return $this->parsePostfixExpression($node); } + public function parseStringExpression() + { + $stream = $this->parser->getStream(); + + $nodes = array(); + // a string cannot be followed by another string in a single expression + $nextCanBeString = true; + while (true) { + if ($stream->test(Twig_Token::STRING_TYPE) && $nextCanBeString) { + $token = $stream->next(); + $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + $nextCanBeString = false; + } elseif ($stream->test(Twig_Token::INTERPOLATION_START_TYPE)) { + $stream->next(); + $nodes[] = $this->parseExpression(); + $stream->expect(Twig_Token::INTERPOLATION_END_TYPE); + $nextCanBeString = true; + } else { + break; + } + } + + $expr = array_shift($nodes); + foreach ($nodes as $node) { + $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine()); + } + + return $expr; + } + public function parseArrayExpression() { $stream = $this->parser->getStream(); $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); - $elements = array(); + + $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); + $first = true; while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { - if (!empty($elements)) { + if (!$first) { $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); // trailing ,? @@ -175,21 +220,24 @@ public function parseArrayExpression() break; } } + $first = false; - $elements[] = $this->parseExpression(); + $node->addElement($this->parseExpression()); } $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); - return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine()); + return $node; } public function parseHashExpression() { $stream = $this->parser->getStream(); $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); - $elements = array(); + + $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); + $first = true; while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) { - if (!empty($elements)) { + if (!$first) { $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); // trailing ,? @@ -197,19 +245,33 @@ public function parseHashExpression() break; } } - - if (!$stream->test(Twig_Token::STRING_TYPE) && !$stream->test(Twig_Token::NUMBER_TYPE)) { + $first = false; + + // a hash key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($stream->test(Twig_Token::STRING_TYPE) || $stream->test(Twig_Token::NAME_TYPE) || $stream->test(Twig_Token::NUMBER_TYPE)) { + $token = $stream->next(); + $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $key = $this->parseExpression(); + } else { $current = $stream->getCurrent(); - throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string or a number (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine()); + + throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine(), $this->parser->getFilename()); } - $key = $stream->next()->getValue(); $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); - $elements[$key] = $this->parseExpression(); + $value = $this->parseExpression(); + + $node->addElement($value, $key); } $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); - return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine()); + return $node; } public function parsePostfixExpression($node) @@ -234,32 +296,42 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { - $args = $this->parseArguments(); switch ($name) { case 'parent': + $args = $this->parseArguments(); if (!count($this->parser->getBlockStack())) { - throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line); + throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename()); } if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line); + throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename()); } return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line); case 'block': - return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line); + return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line); case 'attribute': + $args = $this->parseArguments(); if (count($args) < 2) { - throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attribute)', $line); + throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename()); } return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line); default: - if (null !== $alias = $this->parser->getImportedFunction($name)) { - return new Twig_Node_Expression_GetAttr($alias['node'], new Twig_Node_Expression_Constant($alias['name'], $line), $args, Twig_TemplateInterface::METHOD_CALL, $line); + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + $arguments = new Twig_Node_Expression_Array(array(), $line); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } + + $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line); + $node->setAttribute('safe', true); + + return $node; } - $class = $this->getFunctionNodeClass($name); + $args = $this->parseArguments(true); + $class = $this->getFunctionNodeClass($name, $line); return new $class($name, $args, $line); } @@ -267,12 +339,13 @@ public function getFunctionNode($name, $line) public function parseSubscriptExpression($node) { - $token = $this->parser->getStream()->next(); + $stream = $this->parser->getStream(); + $token = $stream->next(); $lineno = $token->getLine(); - $arguments = new Twig_Node(); + $arguments = new Twig_Node_Expression_Array(array(), $lineno); $type = Twig_TemplateInterface::ANY_CALL; if ($token->getValue() == '.') { - $token = $this->parser->getStream()->next(); + $token = $stream->next(); if ( $token->getType() == Twig_Token::NAME_TYPE || @@ -282,20 +355,60 @@ public function parseSubscriptExpression($node) ) { $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno); - if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { $type = Twig_TemplateInterface::METHOD_CALL; - $arguments = $this->parseArguments(); - } else { - $arguments = new Twig_Node(); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } } } else { - throw new Twig_Error_Syntax('Expected name or number', $lineno); + throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename()); + } + + if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { + if (!$arg instanceof Twig_Node_Expression_Constant) { + throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename()); + } + + $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno); + $node->setAttribute('safe', true); + + return $node; } } else { $type = Twig_TemplateInterface::ARRAY_CALL; - $arg = $this->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ']'); + // slice? + $slice = false; + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $arg = new Twig_Node_Expression_Constant(0, $token->getLine()); + } else { + $arg = $this->parseExpression(); + } + + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $stream->next(); + } + + if ($slice) { + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { + $length = new Twig_Node_Expression_Constant(null, $token->getLine()); + } else { + $length = $this->parseExpression(); + } + + $class = $this->getFilterNodeClass('slice', $token->getLine()); + $arguments = new Twig_Node(array($arg, $length)); + $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine()); + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); } return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno); @@ -317,10 +430,10 @@ public function parseFilterExpressionRaw($node, $tag = null) if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) { $arguments = new Twig_Node(); } else { - $arguments = $this->parseArguments(); + $arguments = $this->parseArguments(true); } - $class = $this->getFilterNodeClass($name->getAttribute('value')); + $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); $node = new $class($node, $name, $arguments, $token->getLine(), $tag); @@ -334,17 +447,62 @@ public function parseFilterExpressionRaw($node, $tag = null) return $node; } - public function parseArguments() + /** + * Parses arguments. + * + * @param Boolean $namedArguments Whether to allow named arguments or not + * @param Boolean $definition Whether we are parsing arguments for a function definition + */ + public function parseArguments($namedArguments = false, $definition = false) { $args = array(); $stream = $this->parser->getStream(); - $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis'); + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) { if (!empty($args)) { $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); } - $args[] = $this->parseExpression(); + + if ($definition) { + $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name'); + $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine()); + } else { + $value = $this->parseExpression(); + } + + $name = null; + if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) { + $token = $stream->next(); + if (!$value instanceof Twig_Node_Expression_Name) { + throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename()); + } + $name = $value->getAttribute('name'); + + if ($definition) { + $value = $this->parsePrimaryExpression(); + + if (!$this->checkConstantExpression($value)) { + throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename()); + } + } else { + $value = $this->parseExpression(); + } + } + + if ($definition) { + if (null === $name) { + $name = $value->getAttribute('name'); + $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine()); + } + $args[$name] = $value; + } else { + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } } $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); @@ -357,7 +515,7 @@ public function parseAssignmentExpression() while (true) { $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to'); if (in_array($token->getValue(), array('true', 'false', 'none'))) { - throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine()); + throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename()); } $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine()); @@ -384,23 +542,59 @@ public function parseMultitargetExpression() return new Twig_Node($targets); } - protected function getFunctionNodeClass($name) + protected function getFunctionNodeClass($name, $line) { - $functionMap = $this->parser->getEnvironment()->getFunctions(); - if (isset($functionMap[$name]) && $functionMap[$name] instanceof Twig_Filter_Node) { - return $functionMap[$name]->getClass(); + $env = $this->parser->getEnvironment(); + + if (false === $function = $env->getFunction($name)) { + $message = sprintf('The function "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename()); } - return 'Twig_Node_Expression_Function'; + if ($function instanceof Twig_SimpleFunction) { + return $function->getNodeClass(); + } + + return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function'; } - protected function getFilterNodeClass($name) + protected function getFilterNodeClass($name, $line) { - $filterMap = $this->parser->getEnvironment()->getFilters(); - if (isset($filterMap[$name]) && $filterMap[$name] instanceof Twig_Filter_Node) { - return $filterMap[$name]->getClass(); + $env = $this->parser->getEnvironment(); + + if (false === $filter = $env->getFilter($name)) { + $message = sprintf('The filter "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename()); + } + + if ($filter instanceof Twig_SimpleFilter) { + return $filter->getNodeClass(); + } + + return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter'; + } + + // checks that the node only contains "constant" elements + protected function checkConstantExpression(Twig_NodeInterface $node) + { + if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } } - return 'Twig_Node_Expression_Filter'; + return true; } } diff --git a/app/parsers/Twig/Extension.php b/app/parsers/Twig/Extension.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Extension/Core.php b/app/parsers/Twig/Extension/Core.php old mode 100755 new mode 100644 index d9e6cdf8..e68687b4 --- a/app/parsers/Twig/Extension/Core.php +++ b/app/parsers/Twig/Extension/Core.php @@ -14,6 +14,83 @@ */ class Twig_Extension_Core extends Twig_Extension { + protected $dateFormats = array('F j, Y H:i', '%d days'); + protected $numberFormat = array(0, '.', ','); + protected $timezone = null; + + /** + * Sets the default format to be used by the date filter. + * + * @param string $format The default date format string + * @param string $dateIntervalFormat The default date interval format string + */ + public function setDateFormat($format = null, $dateIntervalFormat = null) + { + if (null !== $format) { + $this->dateFormats[0] = $format; + } + + if (null !== $dateIntervalFormat) { + $this->dateFormats[1] = $dateIntervalFormat; + } + } + + /** + * Gets the default format to be used by the date filter. + * + * @return array The default date format string and the default date interval format string + */ + public function getDateFormat() + { + return $this->dateFormats; + } + + /** + * Sets the default timezone to be used by the date filter. + * + * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); + } + + /** + * Gets the default timezone to be used by the date filter. + * + * @return DateTimeZone The default timezone currently in use + */ + public function getTimezone() + { + if (null === $this->timezone) { + $this->timezone = new DateTimeZone(date_default_timezone_get()); + } + + return $this->timezone; + } + + /** + * Sets the default format to be used by the number_format filter. + * + * @param integer $decimal The number of decimal places to use. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + */ + public function setNumberFormat($decimal, $decimalPoint, $thousandSep) + { + $this->numberFormat = array($decimal, $decimalPoint, $thousandSep); + } + + /** + * Get the default format used by the number_format filter. + * + * @return array The arguments for number_format() + */ + public function getNumberFormat() + { + return $this->numberFormat; + } + /** * Returns the token parser instance to add to the existing list. * @@ -34,6 +111,9 @@ public function getTokenParsers() new Twig_TokenParser_From(), new Twig_TokenParser_Set(), new Twig_TokenParser_Spaceless(), + new Twig_TokenParser_Flush(), + new Twig_TokenParser_Do(), + new Twig_TokenParser_Embed(), ); } @@ -46,43 +126,53 @@ public function getFilters() { $filters = array( // formatting filters - 'date' => new Twig_Filter_Function('twig_date_format_filter'), - 'format' => new Twig_Filter_Function('sprintf'), - 'replace' => new Twig_Filter_Function('strtr'), + new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('format', 'sprintf'), + new Twig_SimpleFilter('replace', 'strtr'), + new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('abs', 'abs'), // encoding - 'url_encode' => new Twig_Filter_Function('twig_urlencode_filter'), - 'json_encode' => new Twig_Filter_Function('twig_jsonencode_filter'), - 'convert_encoding' => new Twig_Filter_Function('twig_convert_encoding'), + new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'), + new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'), + new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'), // string filters - 'title' => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)), - 'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)), - 'upper' => new Twig_Filter_Function('strtoupper'), - 'lower' => new Twig_Filter_Function('strtolower'), - 'striptags' => new Twig_Filter_Function('strip_tags'), + new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('upper', 'strtoupper'), + new Twig_SimpleFilter('lower', 'strtolower'), + new Twig_SimpleFilter('striptags', 'strip_tags'), + new Twig_SimpleFilter('trim', 'trim'), + new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))), // array helpers - 'join' => new Twig_Filter_Function('twig_join_filter'), - 'reverse' => new Twig_Filter_Function('twig_reverse_filter'), - 'length' => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)), - 'sort' => new Twig_Filter_Function('twig_sort_filter'), - 'merge' => new Twig_Filter_Function('twig_array_merge'), + new Twig_SimpleFilter('join', 'twig_join_filter'), + new Twig_SimpleFilter('split', 'twig_split_filter'), + new Twig_SimpleFilter('sort', 'twig_sort_filter'), + new Twig_SimpleFilter('merge', 'twig_array_merge'), + new Twig_SimpleFilter('batch', 'twig_array_batch'), + + // string/array filters + new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)), + new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)), + new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)), // iteration and runtime - 'default' => new Twig_Filter_Node('Twig_Node_Expression_Filter_Default'), - '_default' => new Twig_Filter_Function('_twig_default_filter'), - - 'keys' => new Twig_Filter_Function('twig_get_array_keys_filter'), + new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')), + new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'), // escaping - 'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), - 'e' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), ); if (function_exists('mb_get_info')) { - $filters['upper'] = new Twig_Filter_Function('twig_upper_filter', array('needs_environment' => true)); - $filters['lower'] = new Twig_Filter_Function('twig_lower_filter', array('needs_environment' => true)); + $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true)); + $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true)); } return $filters; @@ -96,29 +186,33 @@ public function getFilters() public function getFunctions() { return array( - 'range' => new Twig_Function_Function('range'), - 'constant' => new Twig_Function_Function('constant'), - 'cycle' => new Twig_Function_Function('twig_cycle'), + new Twig_SimpleFunction('range', 'range'), + new Twig_SimpleFunction('constant', 'twig_constant'), + new Twig_SimpleFunction('cycle', 'twig_cycle'), + new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)), + new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)), + new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true, 'is_safe' => array('all'))), ); } /** - * Returns a list of filters to add to the existing list. + * Returns a list of tests to add to the existing list. * - * @return array An array of filters + * @return array An array of tests */ public function getTests() { return array( - 'even' => new Twig_Test_Node('Twig_Node_Expression_Test_Even'), - 'odd' => new Twig_Test_Node('Twig_Node_Expression_Test_Odd'), - 'defined' => new Twig_Test_Node('Twig_Node_Expression_Test_Defined'), - 'sameas' => new Twig_Test_Node('Twig_Node_Expression_Test_Sameas'), - 'none' => new Twig_Test_Node('Twig_Node_Expression_Test_Null'), - 'null' => new Twig_Test_Node('Twig_Node_Expression_Test_Null'), - 'divisibleby' => new Twig_Test_Node('Twig_Node_Expression_Test_Divisibleby'), - 'constant' => new Twig_Test_Node('Twig_Node_Expression_Test_Constant'), - 'empty' => new Twig_Test_Function('twig_test_empty'), + new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')), + new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')), + new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')), + new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')), + new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')), + new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')), + new Twig_SimpleTest('empty', 'twig_test_empty'), + new Twig_SimpleTest('iterable', 'twig_test_iterable'), ); } @@ -132,15 +226,15 @@ public function getOperators() return array( array( 'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'), - '-' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Neg'), - '+' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Pos'), + '-' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'), + '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'), ), array( - 'b-and' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-xor' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-or' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), @@ -175,22 +269,32 @@ public function parseTestExpression(Twig_Parser $parser, $node) $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); $arguments = null; if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { - $arguments = $parser->getExpressionParser()->parseArguments(); + $arguments = $parser->getExpressionParser()->parseArguments(true); } - $class = $this->getTestNodeClass($parser->getEnvironment(), $name); + $class = $this->getTestNodeClass($parser, $name, $node->getLine()); return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine()); } - protected function getTestNodeClass(Twig_Environment $env, $name) + protected function getTestNodeClass(Twig_Parser $parser, $name, $line) { + $env = $parser->getEnvironment(); $testMap = $env->getTests(); - if (isset($testMap[$name]) && $testMap[$name] instanceof Twig_Test_Node) { - return $testMap[$name]->getClass(); + if (!isset($testMap[$name])) { + $message = sprintf('The test "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getTests()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $parser->getFilename()); } - return 'Twig_Node_Expression_Test'; + if ($testMap[$name] instanceof Twig_SimpleTest) { + return $testMap[$name]->getNodeClass(); + } + + return $testMap[$name] instanceof Twig_Test_Node ? $testMap[$name]->getClass() : 'Twig_Node_Expression_Test'; } /** @@ -207,18 +311,77 @@ public function getName() /** * Cycles over a value. * - * @param ArrayAccess|array $values An array or an ArrayAccess instance - * @param integer $i The cycle value + * @param ArrayAccess|array $values An array or an ArrayAccess instance + * @param integer $position The cycle position * * @return string The next value in the cycle */ -function twig_cycle($values, $i) +function twig_cycle($values, $position) { if (!is_array($values) && !$values instanceof ArrayAccess) { return $values; } - return $values[$i % count($values)]; + return $values[$position % count($values)]; +} + +/** + * Returns a random value depending on the supplied parameter type: + * - a random item from a Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter + * + * @param Twig_Environment $env A Twig_Environment instance + * @param Traversable|array|integer|string $values The values to pick a random item from + * + * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is). + * + * @return mixed A random value from the given sequence + */ +function twig_random(Twig_Environment $env, $values = null) +{ + if (null === $values) { + return mt_rand(); + } + + if (is_int($values) || is_float($values)) { + return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values); + } + + if ($values instanceof Traversable) { + $values = iterator_to_array($values); + } elseif (is_string($values)) { + if ('' === $values) { + return ''; + } + if (null !== $charset = $env->getCharset()) { + if ('UTF-8' != $charset) { + $values = twig_convert_encoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + } + } + } else { + return $values[mt_rand(0, strlen($values) - 1)]; + } + } + + if (!is_array($values)) { + return $values; + } + + if (0 === count($values)) { + throw new Twig_Error_Runtime('The random function cannot pick from an empty array.'); + } + + return $values[array_rand($values, 1)]; } /** @@ -228,45 +391,143 @@ function twig_cycle($values, $i) * {{ post.published_at|date("m/d/Y") }} * * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|DateInterval|string $date A date + * @param string $format A format + * @param DateTimeZone|string $timezone A timezone + * + * @return string The formatted date + */ +function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null) +{ + if (null === $format) { + $formats = $env->getExtension('core')->getDateFormat(); + $format = $date instanceof DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof DateInterval) { + return $date->format($format); + } + + return twig_date_converter($env, $date, $timezone)->format($format); +} + +/** + * Returns a new date object modified + * + *
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|string $date A date + * @param string $modifier A modifier string + * + * @return DateTime A new date object + */ +function twig_date_modify_filter(Twig_Environment $env, $date, $modifier) +{ + $date = twig_date_converter($env, $date, false); + $date->modify($modifier); + + return $date; +} + +/** + * Converts an input to a DateTime instance. + * + *
+ *    {% if date(user.created_at) < date('+2days') %}
+ *      {# do something #}
+ *    {% endif %}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance * @param DateTime|string $date A date - * @param string $format A format * @param DateTimeZone|string $timezone A timezone * - * @return string The formatter date + * @return DateTime A DateTime instance */ -function twig_date_format_filter($date, $format = 'F j, Y H:i', $timezone = null) +function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null) { - if (!$date instanceof DateTime && !$date instanceof DateInterval) { - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new DateTime('@'.$date); - $date->setTimezone(new DateTimeZone(date_default_timezone_get())); - } else { - $date = new DateTime($date); - } + // determine the timezone + if (!$timezone) { + $defaultTimezone = $env->getExtension('core')->getTimezone(); + } elseif (!$timezone instanceof DateTimeZone) { + $defaultTimezone = new DateTimeZone($timezone); + } else { + $defaultTimezone = $timezone; } - if (null !== $timezone) { - if (!$timezone instanceof DateTimeZone) { - $timezone = new DateTimeZone($timezone); + if ($date instanceof DateTime) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); } - $date->setTimezone($timezone); + return $date; + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = '@'.$date; + } + + $date = new DateTime($date, $defaultTimezone); + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); } - return $date->format($format); + return $date; } /** - * URL encodes a string. + * Number format filter. * - * @param string $url A URL - * @param bool $raw true to use rawurlencode() instead of urlencode + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $number A float/int/string of the number to format + * @param integer $decimal The number of decimal points to display. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + * + * @return string The formatted number + */ +function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + $defaults = $env->getExtension('core')->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } + + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); +} + +/** + * URL encodes a string as a path segment or an array as a query string. + * + * @param string|array $url A URL or an array of query parameters + * @param bool $raw true to use rawurlencode() instead of urlencode * * @return string The URL encoded value */ function twig_urlencode_filter($url, $raw = false) { + if (is_array($url)) { + return http_build_query($url, '', '&'); + } + if ($raw) { return rawurlencode($url); } @@ -276,7 +537,7 @@ function twig_urlencode_filter($url, $raw = false) if (version_compare(PHP_VERSION, '5.3.0', '<')) { /** - * JSON encodes a PHP variable. + * JSON encodes a variable. * * @param mixed $value The value to encode. * @param integer $options Not used on PHP 5.2.x @@ -295,7 +556,7 @@ function twig_jsonencode_filter($value, $options = 0) } } else { /** - * JSON encodes a PHP variable. + * JSON encodes a variable. * * @param mixed $value The value to encode. * @param integer $options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT @@ -340,12 +601,72 @@ function _twig_markup2string(&$value) function twig_array_merge($arr1, $arr2) { if (!is_array($arr1) || !is_array($arr2)) { - throw new Twig_Error_Runtime('The merge filter only work with arrays or hashes.'); + throw new Twig_Error_Runtime('The merge filter only works with arrays or hashes.'); } return array_merge($arr1, $arr2); } +/** + * Slices a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * @param integer $start Start of the slice + * @param integer $length Size of the slice + * @param Boolean $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + */ +function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + if ($item instanceof Traversable) { + $item = iterator_to_array($item, false); + } + + if (is_array($item)) { + return array_slice($item, $start, $length, $preserveKeys); + } + + $item = (string) $item; + + if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) { + return mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset); + } + + return null === $length ? substr($item, $start) : substr($item, $start, $length); +} + +/** + * Returns the first element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The first element of the item + */ +function twig_first(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, 0, 1, false); + + return is_string($elements) ? $elements[0] : current($elements); +} + +/** + * Returns the last element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The last element of the item + */ +function twig_last(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, -1, 1, false); + + return is_string($elements) ? $elements[0] : current($elements); +} + /** * Joins the values to a string. * @@ -366,9 +687,45 @@ function twig_array_merge($arr1, $arr2) */ function twig_join_filter($value, $glue = '') { + if ($value instanceof Traversable) { + $value = iterator_to_array($value, false); + } + return implode($glue, (array) $value); } +/** + * Splits the string into an array. + * + *
+ *  {{ "one,two,three"|split(',') }}
+ *  {# returns [one, two, three] #}
+ *
+ *  {{ "one,two,three,four,five"|split(',', 3) }}
+ *  {# returns [one, two, "three,four,five"] #}
+ *
+ *  {{ "123"|split('') }}
+ *  {# returns [1, 2, 3] #}
+ *
+ *  {{ "aabbcc"|split('', 2) }}
+ *  {# returns [aa, bb, cc] #}
+ * 
+ * + * @param string $value A string + * @param string $delimiter The delimiter + * @param integer $limit The limit + * + * @return array The split string as an array + */ +function twig_split_filter($value, $delimiter, $limit = null) +{ + if (empty($delimiter)) { + return str_split($value, null === $limit ? 1 : $limit); + } + + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); +} + // The '_default' filter is used internally to avoid using the ternary operator // which costs a lot for big contexts (before PHP 5.4). So, on average, // a function call is cheaper. @@ -376,9 +733,9 @@ function _twig_default_filter($value, $default = '') { if (twig_test_empty($value)) { return $default; - } else { - return $value; } + + return $value; } /** @@ -410,23 +767,43 @@ function twig_get_array_keys_filter($array) } /** - * Reverses an array. + * Reverses a variable. * - * @param array|Traversable $array An array or a Traversable instance + * @param Twig_Environment $env A Twig_Environment instance + * @param array|Traversable|string $item An array, a Traversable instance, or a string + * @param Boolean $preserveKeys Whether to preserve key or not * - * return array The array reversed + * @return mixed The reversed input */ -function twig_reverse_filter($array) +function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false) { - if (is_object($array) && $array instanceof Traversable) { - return array_reverse(iterator_to_array($array)); + if (is_object($item) && $item instanceof Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); } - if (!is_array($array)) { - return array(); + if (is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + if (null !== $charset = $env->getCharset()) { + $string = (string) $item; + + if ('UTF-8' != $charset) { + $item = twig_convert_encoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $item, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; } - return array_reverse($array); + return strrev((string) $item); } /** @@ -445,14 +822,15 @@ function twig_sort_filter($array) function twig_in_filter($value, $compare) { if (is_array($compare)) { - return in_array($value, $compare); + return in_array($value, $compare, is_object($value)); } elseif (is_string($compare)) { - if (!strlen((string) $value)) { + if (!strlen($value)) { return empty($compare); } + return false !== strpos($compare, (string) $value); - } elseif (is_object($compare) && $compare instanceof Traversable) { - return in_array($value, iterator_to_array($compare, false)); + } elseif ($compare instanceof Traversable) { + return in_array($value, iterator_to_array($compare, false), is_object($value)); } return false; @@ -463,27 +841,68 @@ function twig_in_filter($value, $compare) * * @param Twig_Environment $env A Twig_Environment instance * @param string $string The value to be escaped - * @param string $type The escaping strategy + * @param string $strategy The escaping strategy * @param string $charset The charset * @param Boolean $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) */ -function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $charset = null, $autoescape = false) +function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) { - if ($autoescape && is_object($string) && $string instanceof Twig_Markup) { + if ($autoescape && $string instanceof Twig_Markup) { return $string; } - if (!is_string($string) && !(is_object($string) && method_exists($string, '__toString'))) { - return $string; + if (!is_string($string)) { + if (is_object($string) && method_exists($string, '__toString')) { + $string = (string) $string; + } else { + return $string; + } } if (null === $charset) { $charset = $env->getCharset(); } - $string = (string) $string; + switch ($strategy) { + case 'html': + // see http://php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = array( + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ); + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return twig_convert_encoding($string, $charset, 'UTF-8'); - switch ($type) { case 'js': // escape all non-alphanumeric characters // into their \xHH or \uHHHH representations @@ -491,50 +910,63 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $cha $string = twig_convert_encoding($string, 'UTF-8', $charset); } - if (null === $string = preg_replace_callback('#[^\p{L}\p{N} ]#u', '_twig_escape_js_callback', $string)) { + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); } + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string); + if ('UTF-8' != $charset) { $string = twig_convert_encoding($string, $charset, 'UTF-8'); } return $string; - case 'html': - // see http://php.net/htmlspecialchars + case 'css': + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping types. - static $htmlspecialcharsCharsets = array( - 'iso-8859-1' => true, 'iso8859-1' => true, - 'iso-8859-15' => true, 'iso8859-15' => true, - 'utf-8' => true, - 'cp866' => true, 'ibm866' => true, '866' => true, - 'cp1251' => true, 'windows-1251' => true, 'win-1251' => true, - '1251' => true, - 'cp1252' => true, 'windows-1252' => true, '1252' => true, - 'koi8-r' => true, 'koi8-ru' => true, 'koi8r' => true, - 'big5' => true, '950' => true, - 'gb2312' => true, '936' => true, - 'big5-hkscs' => true, - 'shift_jis' => true, 'sjis' => true, '932' => true, - 'euc-jp' => true, 'eucjp' => true, - 'iso8859-5' => true, 'iso-8859-5' => true, 'macroman' => true, - ); + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } - if (isset($htmlspecialcharsCharsets[strtolower($charset)])) { - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); } - $string = twig_convert_encoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + return $string; - return twig_convert_encoding($string, $charset, 'UTF-8'); + case 'html_attr': + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'url': + // hackish test to avoid version_compare that is much slower, this works unless PHP releases a 5.10.* + // at that point however PHP 5.2.* support can be removed + if (PHP_VERSION < '5.3.0') { + return str_replace('%7E', '~', rawurlencode($string)); + } + + return rawurlencode($string); default: - throw new Twig_Error_Runtime(sprintf('Invalid escape type "%s".', $type)); + throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: html, js, url, css, and html_attr).', $strategy)); } } @@ -544,25 +976,23 @@ function twig_escape_filter_is_safe(Twig_Node $filterArgs) foreach ($filterArgs as $arg) { if ($arg instanceof Twig_Node_Expression_Constant) { return array($arg->getAttribute('value')); - } else { - return array(); } - break; + return array(); } return array('html'); } -if (function_exists('iconv')) { +if (function_exists('mb_convert_encoding')) { function twig_convert_encoding($string, $to, $from) { - return iconv($from, $to, $string); + return mb_convert_encoding($string, $to, $from); } -} elseif (function_exists('mb_convert_encoding')) { +} elseif (function_exists('iconv')) { function twig_convert_encoding($string, $to, $from) { - return mb_convert_encoding($string, $to, $from); + return iconv($from, $to, $string); } } else { function twig_convert_encoding($string, $to, $from) @@ -577,22 +1007,98 @@ function _twig_escape_js_callback($matches) // \xHH if (!isset($char[1])) { - return '\\x'.substr('00'.bin2hex($char), -2); + return '\\x'.strtoupper(substr('00'.bin2hex($char), -2)); } // \uHHHH - $char = _twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); - return '\\u'.substr('0000'.bin2hex($char), -4); + return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4)); +} + +function _twig_escape_css_callback($matches) +{ + $char = $matches[0]; + + // \xHH + if (!isset($char[1])) { + $hex = ltrim(strtoupper(bin2hex($char)), '0'); + if (0 === strlen($hex)) { + $hex = '0'; + } + + return '\\'.$hex.' '; + } + + // \uHHHH + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + + return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' '; +} + +/** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +function _twig_escape_html_attr_callback($matches) +{ + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = array( + 34 => 'quot', /* quotation mark */ + 38 => 'amp', /* ampersand */ + 60 => 'lt', /* less-than sign */ + 62 => 'gt', /* greater-than sign */ + ); + + $chr = $matches[0]; + $ord = ord($chr); + + /** + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) { + return '�'; + } + + /** + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (strlen($chr) == 1) { + $hex = strtoupper(substr('00'.bin2hex($chr), -2)); + } else { + $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8'); + $hex = strtoupper(substr('0000'.bin2hex($chr), -4)); + } + + $int = hexdec($hex); + if (array_key_exists($int, $entityMap)) { + return sprintf('&%s;', $entityMap[$int]); + } + + /** + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + + return sprintf('&#x%s;', $hex); } // add multibyte extensions if possible if (function_exists('mb_get_info')) { /** - * Returns the length of a PHP variable. + * Returns the length of a variable. * * @param Twig_Environment $env A Twig_Environment instance - * @param mixed $thing A PHP variable + * @param mixed $thing A variable * * @return integer The length of the value */ @@ -671,13 +1177,12 @@ function twig_capitalize_string_filter(Twig_Environment $env, $string) } } // and byte fallback -else -{ +else { /** - * Returns the length of a PHP variable. + * Returns the length of a variable. * * @param Twig_Environment $env A Twig_Environment instance - * @param mixed $thing A PHP variable + * @param mixed $thing A variable * * @return integer The length of the value */ @@ -716,11 +1221,11 @@ function twig_capitalize_string_filter(Twig_Environment $env, $string) /* used internally */ function twig_ensure_traversable($seq) { - if (is_array($seq) || (is_object($seq) && $seq instanceof Traversable)) { + if ($seq instanceof Traversable || is_array($seq)) { return $seq; - } else { - return array(); } + + return array(); } /** @@ -733,7 +1238,7 @@ function twig_ensure_traversable($seq) * {% endif %} * * - * @param mixed $value A PHP variable + * @param mixed $value A variable * * @return Boolean true if the value is empty, false otherwise */ @@ -742,5 +1247,109 @@ function twig_test_empty($value) if ($value instanceof Countable) { return 0 == count($value); } - return false === $value || (empty($value) && '0' != $value); + + return '' === $value || false === $value || null === $value || array() === $value; +} + +/** + * Checks if a variable is traversable. + * + *
+ * {# evaluates to true if the foo variable is an array or a traversable object #}
+ * {% if foo is traversable %}
+ *     {# ... #}
+ * {% endif %}
+ * 
+ * + * @param mixed $value A variable + * + * @return Boolean true if the value is traversable + */ +function twig_test_iterable($value) +{ + return $value instanceof Traversable || is_array($value); +} + +/** + * Renders a template. + * + * @param string $template The template to render + * @param array $variables The variables to pass to the template + * @param Boolean $with_context Whether to pass the current context variables or not + * @param Boolean $ignore_missing Whether to ignore missing templates or not + * @param Boolean $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + */ +function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false) +{ + if ($withContext) { + $variables = array_merge($context, $variables); + } + + if ($isSandboxed = $sandboxed && $env->hasExtension('sandbox')) { + $sandbox = $env->getExtension('sandbox'); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + } + + try { + return $env->resolveTemplate($template)->render($variables); + } catch (Twig_Error_Loader $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } +} + +/** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param null|object $object The object to get the constant from + * + * @return string + */ +function twig_constant($constant, $object = null) +{ + if (null !== $object) { + $constant = get_class($object).'::'.$constant; + } + + return constant($constant); +} + +/** + * Batches item. + * + * @param array $items An array of items + * @param integer $size The size of the batch + * @param string $fill A string to fill missing items + * + * @return array + */ +function twig_array_batch($items, $size, $fill = null) +{ + if ($items instanceof Traversable) { + $items = iterator_to_array($items, false); + } + + $size = ceil($size); + + $result = array_chunk($items, $size, true); + + if (null !== $fill) { + $last = count($result) - 1; + $result[$last] = array_merge( + $result[$last], + array_fill(0, $size - count($result[$last]), $fill) + ); + } + + return $result; } diff --git a/app/parsers/Twig/Extension/Debug.php b/app/parsers/Twig/Extension/Debug.php new file mode 100644 index 00000000..e3a85bfe --- /dev/null +++ b/app/parsers/Twig/Extension/Debug.php @@ -0,0 +1,71 @@ + $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)), + ); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'debug'; + } +} + +function twig_var_dump(Twig_Environment $env, $context) +{ + if (!$env->isDebug()) { + return; + } + + ob_start(); + + $count = func_num_args(); + if (2 === $count) { + $vars = array(); + foreach ($context as $key => $value) { + if (!$value instanceof Twig_Template) { + $vars[$key] = $value; + } + } + + var_dump($vars); + } else { + for ($i = 2; $i < $count; $i++) { + var_dump(func_get_arg($i)); + } + } + + return ob_get_clean(); +} diff --git a/app/parsers/Twig/Extension/Escaper.php b/app/parsers/Twig/Extension/Escaper.php old mode 100755 new mode 100644 index 43ae1113..c9a7f68e --- a/app/parsers/Twig/Extension/Escaper.php +++ b/app/parsers/Twig/Extension/Escaper.php @@ -10,11 +10,11 @@ */ class Twig_Extension_Escaper extends Twig_Extension { - protected $autoescape; + protected $defaultStrategy; - public function __construct($autoescape = true) + public function __construct($defaultStrategy = 'html') { - $this->autoescape = $autoescape; + $this->setDefaultStrategy($defaultStrategy); } /** @@ -45,13 +45,44 @@ public function getNodeVisitors() public function getFilters() { return array( - 'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe' => array('all'))), + new Twig_SimpleFilter('raw', 'twig_raw_filter', array('is_safe' => array('all'))), ); } - public function isGlobal() + /** + * Sets the default strategy to use when not defined by the user. + * + * The strategy can be a valid PHP callback that takes the template + * "filename" as an argument and returns the strategy to use. + * + * @param mixed $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy) + { + // for BC + if (true === $defaultStrategy) { + $defaultStrategy = 'html'; + } + + $this->defaultStrategy = $defaultStrategy; + } + + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $filename The template "filename" + * + * @return string The default strategy to use for the template + */ + public function getDefaultStrategy($filename) { - return $this->autoescape; + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!is_string($this->defaultStrategy) && is_callable($this->defaultStrategy)) { + return call_user_func($this->defaultStrategy, $filename); + } + + return $this->defaultStrategy; } /** @@ -74,4 +105,3 @@ function twig_raw_filter($string) { return $string; } - diff --git a/app/parsers/Twig/Extension/Optimizer.php b/app/parsers/Twig/Extension/Optimizer.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Extension/Sandbox.php b/app/parsers/Twig/Extension/Sandbox.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Extension/Staging.php b/app/parsers/Twig/Extension/Staging.php new file mode 100644 index 00000000..8ab0f459 --- /dev/null +++ b/app/parsers/Twig/Extension/Staging.php @@ -0,0 +1,113 @@ + + */ +class Twig_Extension_Staging extends Twig_Extension +{ + protected $functions = array(); + protected $filters = array(); + protected $visitors = array(); + protected $tokenParsers = array(); + protected $globals = array(); + protected $tests = array(); + + public function addFunction($name, $function) + { + $this->functions[$name] = $function; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return $this->functions; + } + + public function addFilter($name, $filter) + { + $this->filters[$name] = $filter; + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return $this->filters; + } + + public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) + { + $this->visitors[] = $visitor; + } + + /** + * {@inheritdoc} + */ + public function getNodeVisitors() + { + return $this->visitors; + } + + public function addTokenParser(Twig_TokenParserInterface $parser) + { + $this->tokenParsers[] = $parser; + } + + /** + * {@inheritdoc} + */ + public function getTokenParsers() + { + return $this->tokenParsers; + } + + public function addGlobal($name, $value) + { + $this->globals[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getGlobals() + { + return $this->globals; + } + + public function addTest($name, $test) + { + $this->tests[$name] = $test; + } + + /** + * {@inheritdoc} + */ + public function getTests() + { + return $this->tests; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'staging'; + } +} diff --git a/app/parsers/Twig/Extension/StringLoader.php b/app/parsers/Twig/Extension/StringLoader.php new file mode 100644 index 00000000..20f3f994 --- /dev/null +++ b/app/parsers/Twig/Extension/StringLoader.php @@ -0,0 +1,64 @@ + true)), + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'string_loader'; + } +} + +/** + * Loads a template from a string. + * + *
+ * {{ include(template_from_string("Hello {{ name }}")) }}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $template A template as a string + * + * @return Twig_Template A Twig_Template instance + */ +function twig_template_from_string(Twig_Environment $env, $template) +{ + static $loader; + + if (null === $loader) { + $loader = new Twig_Loader_String(); + } + + $current = $env->getLoader(); + $env->setLoader($loader); + try { + $template = $env->loadTemplate($template); + } catch (Exception $e) { + $env->setLoader($current); + + throw $e; + } + $env->setLoader($current); + + return $template; +} diff --git a/app/parsers/Twig/ExtensionInterface.php b/app/parsers/Twig/ExtensionInterface.php old mode 100755 new mode 100644 index 0bfed88c..f189e9d9 --- a/app/parsers/Twig/ExtensionInterface.php +++ b/app/parsers/Twig/ExtensionInterface.php @@ -12,8 +12,7 @@ /** * Interface implemented by extension classes. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_ExtensionInterface { @@ -24,61 +23,61 @@ interface Twig_ExtensionInterface * * @param Twig_Environment $environment The current Twig_Environment instance */ - function initRuntime(Twig_Environment $environment); + public function initRuntime(Twig_Environment $environment); /** * Returns the token parser instances to add to the existing list. * * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances */ - function getTokenParsers(); + public function getTokenParsers(); /** * Returns the node visitor instances to add to the existing list. * * @return array An array of Twig_NodeVisitorInterface instances */ - function getNodeVisitors(); + public function getNodeVisitors(); /** * Returns a list of filters to add to the existing list. * * @return array An array of filters */ - function getFilters(); + public function getFilters(); /** * Returns a list of tests to add to the existing list. * * @return array An array of tests */ - function getTests(); + public function getTests(); /** * Returns a list of functions to add to the existing list. * * @return array An array of functions */ - function getFunctions(); + public function getFunctions(); /** * Returns a list of operators to add to the existing list. * * @return array An array of operators */ - function getOperators(); + public function getOperators(); /** * Returns a list of global variables to add to the existing list. * * @return array An array of global variables */ - function getGlobals(); + public function getGlobals(); /** * Returns the name of the extension. * * @return string The extension name */ - function getName(); + public function getName(); } diff --git a/app/parsers/Twig/Filter.php b/app/parsers/Twig/Filter.php old mode 100755 new mode 100644 index 9595a1a8..5cfbb662 --- a/app/parsers/Twig/Filter.php +++ b/app/parsers/Twig/Filter.php @@ -12,12 +12,15 @@ /** * Represents a template filter. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -abstract class Twig_Filter implements Twig_FilterInterface +abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface { protected $options; + protected $arguments = array(); public function __construct(array $options = array()) { @@ -25,9 +28,21 @@ public function __construct(array $options = array()) 'needs_environment' => false, 'needs_context' => false, 'pre_escape' => null, + 'preserves_safety' => null, + 'callable' => null, ), $options); } + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + public function needsEnvironment() { return $this->options['needs_environment']; @@ -47,12 +62,20 @@ public function getSafe(Twig_Node $filterArgs) if (isset($this->options['is_safe_callback'])) { return call_user_func($this->options['is_safe_callback'], $filterArgs); } + } - return array(); + public function getPreservesSafety() + { + return $this->options['preserves_safety']; } public function getPreEscape() { return $this->options['pre_escape']; } + + public function getCallable() + { + return $this->options['callable']; + } } diff --git a/app/parsers/Twig/Filter/Function.php b/app/parsers/Twig/Filter/Function.php old mode 100755 new mode 100644 index 1de078b2..ad374a55 --- a/app/parsers/Twig/Filter/Function.php +++ b/app/parsers/Twig/Filter/Function.php @@ -12,8 +12,10 @@ /** * Represents a function template filter. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Function extends Twig_Filter { @@ -21,6 +23,8 @@ class Twig_Filter_Function extends Twig_Filter public function __construct($function, array $options = array()) { + $options['callable'] = $function; + parent::__construct($options); $this->function = $function; diff --git a/app/parsers/Twig/Filter/Method.php b/app/parsers/Twig/Filter/Method.php old mode 100755 new mode 100644 index d831e0f2..63c8c3be --- a/app/parsers/Twig/Filter/Method.php +++ b/app/parsers/Twig/Filter/Method.php @@ -12,15 +12,20 @@ /** * Represents a method template filter. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Method extends Twig_Filter { - protected $extension, $method; + protected $extension; + protected $method; public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + parent::__construct($options); $this->extension = $extension; diff --git a/app/parsers/Twig/Filter/Node.php b/app/parsers/Twig/Filter/Node.php old mode 100755 new mode 100644 index 7481c05c..8744c5e0 --- a/app/parsers/Twig/Filter/Node.php +++ b/app/parsers/Twig/Filter/Node.php @@ -12,8 +12,10 @@ /** * Represents a template filter as a node. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Node extends Twig_Filter { diff --git a/app/parsers/Twig/FilterCallableInterface.php b/app/parsers/Twig/FilterCallableInterface.php new file mode 100644 index 00000000..145534df --- /dev/null +++ b/app/parsers/Twig/FilterCallableInterface.php @@ -0,0 +1,23 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FilterCallableInterface +{ + public function getCallable(); +} diff --git a/app/parsers/Twig/FilterInterface.php b/app/parsers/Twig/FilterInterface.php old mode 100755 new mode 100644 index 4ac19ceb..5319ecc9 --- a/app/parsers/Twig/FilterInterface.php +++ b/app/parsers/Twig/FilterInterface.php @@ -12,8 +12,10 @@ /** * Represents a template filter. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FilterInterface { @@ -22,13 +24,19 @@ interface Twig_FilterInterface * * @return string The PHP code for the filter */ - function compile(); + public function compile(); + + public function needsEnvironment(); + + public function needsContext(); + + public function getSafe(Twig_Node $filterArgs); - function needsEnvironment(); + public function getPreservesSafety(); - function needsContext(); + public function getPreEscape(); - function getSafe(Twig_Node $filterArgs); + public function setArguments($arguments); - function getPreEscape(); + public function getArguments(); } diff --git a/app/parsers/Twig/Function.php b/app/parsers/Twig/Function.php old mode 100755 new mode 100644 index 1197924a..b5ffb2b0 --- a/app/parsers/Twig/Function.php +++ b/app/parsers/Twig/Function.php @@ -12,21 +12,35 @@ /** * Represents a template function. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFunction instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -abstract class Twig_Function implements Twig_FunctionInterface +abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface { protected $options; + protected $arguments = array(); public function __construct(array $options = array()) { $this->options = array_merge(array( 'needs_environment' => false, 'needs_context' => false, + 'callable' => null, ), $options); } + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + public function needsEnvironment() { return $this->options['needs_environment']; @@ -49,4 +63,9 @@ public function getSafe(Twig_Node $functionArgs) return array(); } + + public function getCallable() + { + return $this->options['callable']; + } } diff --git a/app/parsers/Twig/Function/Function.php b/app/parsers/Twig/Function/Function.php old mode 100755 new mode 100644 index 3237d8c5..d1e1b96a --- a/app/parsers/Twig/Function/Function.php +++ b/app/parsers/Twig/Function/Function.php @@ -13,8 +13,10 @@ /** * Represents a function template function. * - * @package twig - * @author Arnaud Le Blanc + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Function_Function extends Twig_Function { @@ -22,6 +24,8 @@ class Twig_Function_Function extends Twig_Function public function __construct($function, array $options = array()) { + $options['callable'] = $function; + parent::__construct($options); $this->function = $function; diff --git a/app/parsers/Twig/Function/Method.php b/app/parsers/Twig/Function/Method.php old mode 100755 new mode 100644 index 7328566e..67039a95 --- a/app/parsers/Twig/Function/Method.php +++ b/app/parsers/Twig/Function/Method.php @@ -13,15 +13,20 @@ /** * Represents a method template function. * - * @package twig - * @author Arnaud Le Blanc + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Function_Method extends Twig_Function { - protected $extension, $method; + protected $extension; + protected $method; public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + parent::__construct($options); $this->extension = $extension; diff --git a/app/parsers/Twig/Function/Node.php b/app/parsers/Twig/Function/Node.php old mode 100755 new mode 100644 index a687a849..06a0d0db --- a/app/parsers/Twig/Function/Node.php +++ b/app/parsers/Twig/Function/Node.php @@ -12,10 +12,12 @@ /** * Represents a template function as a node. * - * @package twig - * @author Fabien Potencier + * Use Twig_SimpleFunction instead. + * + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -class Twig_Function_Node extends Twig_Filter +class Twig_Function_Node extends Twig_Function { protected $class; diff --git a/app/parsers/Twig/FunctionCallableInterface.php b/app/parsers/Twig/FunctionCallableInterface.php new file mode 100644 index 00000000..0aab4f5e --- /dev/null +++ b/app/parsers/Twig/FunctionCallableInterface.php @@ -0,0 +1,23 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FunctionCallableInterface +{ + public function getCallable(); +} diff --git a/app/parsers/Twig/FunctionInterface.php b/app/parsers/Twig/FunctionInterface.php old mode 100755 new mode 100644 index ccc9fd93..67f4f89c --- a/app/parsers/Twig/FunctionInterface.php +++ b/app/parsers/Twig/FunctionInterface.php @@ -13,8 +13,10 @@ /** * Represents a template function. * - * @package twig - * @author Arnaud Le Blanc + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FunctionInterface { @@ -23,11 +25,15 @@ interface Twig_FunctionInterface * * @return string The PHP code for the function */ - function compile(); + public function compile(); + + public function needsEnvironment(); + + public function needsContext(); - function needsEnvironment(); + public function getSafe(Twig_Node $filterArgs); - function needsContext(); + public function setArguments($arguments); - function getSafe(Twig_Node $filterArgs); + public function getArguments(); } diff --git a/app/parsers/Twig/Lexer.php b/app/parsers/Twig/Lexer.php old mode 100755 new mode 100644 index 6c75d45e..000b038e --- a/app/parsers/Twig/Lexer.php +++ b/app/parsers/Twig/Lexer.php @@ -13,8 +13,7 @@ /** * Lexes a template string. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Lexer implements Twig_LexerInterface { @@ -24,20 +23,28 @@ class Twig_Lexer implements Twig_LexerInterface protected $lineno; protected $end; protected $state; + protected $states; protected $brackets; - protected $env; protected $filename; protected $options; - - const STATE_DATA = 0; - const STATE_BLOCK = 1; - const STATE_VAR = 2; - - const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; - const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A'; - const REGEX_STRING = '/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; - const PUNCTUATION = '()[]{}?:.,|'; + protected $regexes; + protected $position; + protected $positions; + protected $currentVarBlockLine; + + const STATE_DATA = 0; + const STATE_BLOCK = 1; + const STATE_VAR = 2; + const STATE_STRING = 3; + const STATE_INTERPOLATION = 4; + + const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; + const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A'; + const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + const REGEX_DQ_STRING_DELIM = '/"/A'; + const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + const PUNCTUATION = '()[]{}?:.,|'; public function __construct(Twig_Environment $env, array $options = array()) { @@ -48,23 +55,28 @@ public function __construct(Twig_Environment $env, array $options = array()) 'tag_block' => array('{%', '%}'), 'tag_variable' => array('{{', '}}'), 'whitespace_trim' => '-', + 'interpolation' => array('#{', '}'), ), $options); - $this->options['lex_var_regex'] = '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A'; - $this->options['lex_block_regex'] = '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A'; - $this->options['lex_raw_data_regex'] = '/'.preg_quote($this->options['tag_block'][0], '/').'\s*endraw\s*'.preg_quote($this->options['tag_block'][1], '/').'/s'; - $this->options['operator_regex'] = $this->getOperatorRegex(); - $this->options['lex_comment_regex'] = '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s'; - $this->options['lex_block_raw_regex'] = '/\s*raw\s*'.preg_quote($this->options['tag_block'][1], '/').'/As'; - $this->options['lex_block_line_regex'] = '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As'; - $this->options['lex_tokens_start_regex'] = '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s'; + $this->regexes = array( + 'lex_var' => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A', + 'lex_block' => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A', + 'lex_raw_data' => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*(?:end%s)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s', + 'operator' => $this->getOperatorRegex(), + 'lex_comment' => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s', + 'lex_block_raw' => '/\s*(raw|verbatim)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As', + 'lex_block_line' => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', + 'lex_tokens_start' => '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s', + 'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A', + 'interpolation_end' => '/\s*'.preg_quote($this->options['interpolation'][1], '/').'/A', + ); } /** * Tokenizes a source code. * - * @param string $code The source code - * @param string $filename A unique identifier for the source code + * @param string $code The source code + * @param string $filename A unique identifier for the source code * * @return Twig_TokenStream A token stream instance */ @@ -82,11 +94,12 @@ public function tokenize($code, $filename = null) $this->end = strlen($this->code); $this->tokens = array(); $this->state = self::STATE_DATA; + $this->states = array(); $this->brackets = array(); $this->position = -1; // find all token starts in one go - preg_match_all($this->options['lex_tokens_start_regex'], $this->code, $matches, PREG_OFFSET_CAPTURE); + preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, PREG_OFFSET_CAPTURE); $this->positions = $matches; while ($this->cursor < $this->end) { @@ -104,6 +117,14 @@ public function tokenize($code, $filename = null) case self::STATE_VAR: $this->lexVar(); break; + + case self::STATE_STRING: + $this->lexString(); + break; + + case self::STATE_INTERPOLATION: + $this->lexInterpolation(); + break; } } @@ -127,6 +148,7 @@ protected function lexData() if ($this->position == count($this->positions[0]) - 1) { $this->pushToken(Twig_Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->cursor = $this->end; + return; } @@ -154,34 +176,34 @@ protected function lexData() case $this->options['tag_block'][0]: // raw data? - if (preg_match($this->options['lex_block_raw_regex'], $this->code, $match, null, $this->cursor)) { + if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) { $this->moveCursor($match[0]); - $this->lexRawData(); - $this->state = self::STATE_DATA; + $this->lexRawData($match[1]); // {% line \d+ %} - } else if (preg_match($this->options['lex_block_line_regex'], $this->code, $match, null, $this->cursor)) { + } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) { $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; - $this->state = self::STATE_DATA; } else { $this->pushToken(Twig_Token::BLOCK_START_TYPE); - $this->state = self::STATE_BLOCK; + $this->pushState(self::STATE_BLOCK); + $this->currentVarBlockLine = $this->lineno; } break; case $this->options['tag_variable'][0]: $this->pushToken(Twig_Token::VAR_START_TYPE); - $this->state = self::STATE_VAR; + $this->pushState(self::STATE_VAR); + $this->currentVarBlockLine = $this->lineno; break; } } protected function lexBlock() { - if (empty($this->brackets) && preg_match($this->options['lex_block_regex'], $this->code, $match, null, $this->cursor)) { + if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); - $this->state = self::STATE_DATA; + $this->popState(); } else { $this->lexExpression(); } @@ -189,10 +211,10 @@ protected function lexBlock() protected function lexVar() { - if (empty($this->brackets) && preg_match($this->options['lex_var_regex'], $this->code, $match, null, $this->cursor)) { + if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::VAR_END_TYPE); $this->moveCursor($match[0]); - $this->state = self::STATE_DATA; + $this->popState(); } else { $this->lexExpression(); } @@ -205,12 +227,12 @@ protected function lexExpression() $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable')); + throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable'), $this->currentVarBlockLine, $this->filename); } } // operators - if (preg_match($this->options['operator_regex'], $this->code, $match, null, $this->cursor)) { + if (preg_match($this->regexes['operator'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::OPERATOR_TYPE, $match[0]); $this->moveCursor($match[0]); } @@ -221,7 +243,11 @@ protected function lexExpression() } // numbers elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor)) { - $this->pushToken(Twig_Token::NUMBER_TYPE, ctype_digit($match[0]) ? (int) $match[0] : (float) $match[0]); + $number = (float) $match[0]; // floats + if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { + $number = (int) $match[0]; // integers lower than the maximum + } + $this->pushToken(Twig_Token::NUMBER_TYPE, $number); $this->moveCursor($match[0]); } // punctuation @@ -250,31 +276,80 @@ protected function lexExpression() $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1))); $this->moveCursor($match[0]); } + // opening double quoted string + elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array('"', $this->lineno); + $this->pushState(self::STATE_STRING); + $this->moveCursor($match[0]); + } // unlexable else { throw new Twig_Error_Syntax(sprintf('Unexpected character "%s"', $this->code[$this->cursor]), $this->lineno, $this->filename); } } - protected function lexRawData() + protected function lexRawData($tag) { - if (!preg_match($this->options['lex_raw_data_regex'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { - throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "block"')); + if (!preg_match(str_replace('%s', $tag, $this->regexes['lex_raw_data']), $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s" block', $tag), $this->lineno, $this->filename); } + $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); - $this->pushToken(Twig_Token::TEXT_TYPE, $text); $this->moveCursor($text.$match[0][0]); + + if (false !== strpos($match[1][0], $this->options['whitespace_trim'])) { + $text = rtrim($text); + } + + $this->pushToken(Twig_Token::TEXT_TYPE, $text); } protected function lexComment() { - if (!preg_match($this->options['lex_comment_regex'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + if (!preg_match($this->regexes['lex_comment'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { throw new Twig_Error_Syntax('Unclosed comment', $this->lineno, $this->filename); } $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); } + protected function lexString() + { + if (preg_match($this->regexes['interpolation_start'], $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array($this->options['interpolation'][0], $this->lineno); + $this->pushToken(Twig_Token::INTERPOLATION_START_TYPE); + $this->moveCursor($match[0]); + $this->pushState(self::STATE_INTERPOLATION); + + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, null, $this->cursor) && strlen($match[0]) > 0) { + $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes($match[0])); + $this->moveCursor($match[0]); + + } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + + list($expect, $lineno) = array_pop($this->brackets); + if ($this->code[$this->cursor] != '"') { + throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename); + } + + $this->popState(); + ++$this->cursor; + } + } + + protected function lexInterpolation() + { + $bracket = end($this->brackets); + if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, null, $this->cursor)) { + array_pop($this->brackets); + $this->pushToken(Twig_Token::INTERPOLATION_END_TYPE); + $this->moveCursor($match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + protected function pushToken($type, $value = '') { // do not push empty text tokens @@ -307,7 +382,7 @@ protected function getOperatorRegex() // an operator that ends with a character must be followed by // a whitespace or a parenthesis if (ctype_alpha($operator[$length - 1])) { - $regex[] = preg_quote($operator, '/').'(?=[ ()])'; + $regex[] = preg_quote($operator, '/').'(?=[\s()])'; } else { $regex[] = preg_quote($operator, '/'); } @@ -315,4 +390,19 @@ protected function getOperatorRegex() return '/'.implode('|', $regex).'/A'; } + + protected function pushState($state) + { + $this->states[] = $this->state; + $this->state = $state; + } + + protected function popState() + { + if (0 === count($this->states)) { + throw new Exception('Cannot pop state without a previous state'); + } + + $this->state = array_pop($this->states); + } } diff --git a/app/parsers/Twig/LexerInterface.php b/app/parsers/Twig/LexerInterface.php old mode 100755 new mode 100644 index 02233849..4b83f81b --- a/app/parsers/Twig/LexerInterface.php +++ b/app/parsers/Twig/LexerInterface.php @@ -12,18 +12,18 @@ /** * Interface implemented by lexer classes. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_LexerInterface { /** * Tokenizes a source code. * - * @param string $code The source code - * @param string $filename A unique identifier for the source code + * @param string $code The source code + * @param string $filename A unique identifier for the source code * * @return Twig_TokenStream A token stream instance */ - function tokenize($code, $filename = null); + public function tokenize($code, $filename = null); } diff --git a/app/parsers/Twig/Loader/Array.php b/app/parsers/Twig/Loader/Array.php old mode 100755 new mode 100644 index 962d64cb..89087aea --- a/app/parsers/Twig/Loader/Array.php +++ b/app/parsers/Twig/Loader/Array.php @@ -17,10 +17,9 @@ * source code of the template). If you don't want to see your cache grows out of * control, you need to take care of clearing the old cache file by yourself. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ -class Twig_Loader_Array implements Twig_LoaderInterface +class Twig_Loader_Array implements Twig_LoaderInterface, Twig_ExistsLoaderInterface { protected $templates; @@ -47,18 +46,15 @@ public function __construct(array $templates) */ public function setTemplate($name, $template) { - $this->templates[$name] = $template; + $this->templates[(string) $name] = $template; } /** - * Gets the source code of a template, given its name. - * - * @param string $name The name of the template to load - * - * @return string The template source code + * {@inheritdoc} */ public function getSource($name) { + $name = (string) $name; if (!isset($this->templates[$name])) { throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); } @@ -67,14 +63,19 @@ public function getSource($name) } /** - * Gets the cache key to use for the cache for a given template name. - * - * @param string $name The name of the template to load - * - * @return string The cache key + * {@inheritdoc} + */ + public function exists($name) + { + return isset($this->templates[(string) $name]); + } + + /** + * {@inheritdoc} */ public function getCacheKey($name) { + $name = (string) $name; if (!isset($this->templates[$name])) { throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); } @@ -83,13 +84,11 @@ public function getCacheKey($name) } /** - * Returns true if the template is still fresh. - * - * @param string $name The template name - * @param timestamp $time The last modification time of the cached template + * {@inheritdoc} */ public function isFresh($name, $time) { + $name = (string) $name; if (!isset($this->templates[$name])) { throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); } diff --git a/app/parsers/Twig/Loader/Chain.php b/app/parsers/Twig/Loader/Chain.php old mode 100755 new mode 100644 index 48dd8b84..1f1cf065 --- a/app/parsers/Twig/Loader/Chain.php +++ b/app/parsers/Twig/Loader/Chain.php @@ -12,11 +12,11 @@ /** * Loads templates from other loaders. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ -class Twig_Loader_Chain implements Twig_LoaderInterface +class Twig_Loader_Chain implements Twig_LoaderInterface, Twig_ExistsLoaderInterface { + private $hasSourceCache = array(); protected $loaders; /** @@ -40,61 +40,100 @@ public function __construct(array $loaders = array()) public function addLoader(Twig_LoaderInterface $loader) { $this->loaders[] = $loader; + $this->hasSourceCache = array(); } /** - * Gets the source code of a template, given its name. - * - * @param string $name The name of the template to load - * - * @return string The template source code + * {@inheritdoc} */ public function getSource($name) { + $exceptions = array(); foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + try { return $loader->getSource($name); } catch (Twig_Error_Loader $e) { + $exceptions[] = $e->getMessage(); } } - throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(', ', $exceptions))); } /** - * Gets the cache key to use for the cache for a given template name. - * - * @param string $name The name of the template to load - * - * @return string The cache key + * {@inheritdoc} + */ + public function exists($name) + { + $name = (string) $name; + + if (isset($this->hasSourceCache[$name])) { + return $this->hasSourceCache[$name]; + } + + foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface) { + if ($loader->exists($name)) { + return $this->hasSourceCache[$name] = true; + } + + continue; + } + + try { + $loader->getSource($name); + + return $this->hasSourceCache[$name] = true; + } catch (Twig_Error_Loader $e) { + } + } + + return $this->hasSourceCache[$name] = false; + } + + /** + * {@inheritdoc} */ public function getCacheKey($name) { + $exceptions = array(); foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + try { return $loader->getCacheKey($name); } catch (Twig_Error_Loader $e) { + $exceptions[] = get_class($loader).': '.$e->getMessage(); } } - throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(' ', $exceptions))); } /** - * Returns true if the template is still fresh. - * - * @param string $name The template name - * @param timestamp $time The last modification time of the cached template + * {@inheritdoc} */ public function isFresh($name, $time) { + $exceptions = array(); foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + try { return $loader->isFresh($name, $time); } catch (Twig_Error_Loader $e) { + $exceptions[] = get_class($loader).': '.$e->getMessage(); } } - throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(' ', $exceptions))); } } diff --git a/app/parsers/Twig/Loader/Filesystem.php b/app/parsers/Twig/Loader/Filesystem.php old mode 100755 new mode 100644 index be348aa3..f9211cbd --- a/app/parsers/Twig/Loader/Filesystem.php +++ b/app/parsers/Twig/Loader/Filesystem.php @@ -12,10 +12,9 @@ /** * Loads template from the filesystem. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ -class Twig_Loader_Filesystem implements Twig_LoaderInterface +class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface { protected $paths; protected $cache; @@ -25,44 +24,64 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface * * @param string|array $paths A path or an array of paths where to look for templates */ - public function __construct($paths) + public function __construct($paths = array()) { - $this->setPaths($paths); + if ($paths) { + $this->setPaths($paths); + } } /** * Returns the paths to the templates. * + * @param string $namespace A path namespace + * * @return array The array of paths where to look for templates */ - public function getPaths() + public function getPaths($namespace = '__main__') + { + return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array(); + } + + /** + * Returns the path namespaces. + * + * The "__main__" namespace is always defined. + * + * @return array The array of defined namespaces + */ + public function getNamespaces() { - return $this->paths; + return array_keys($this->paths); } /** * Sets the paths where templates are stored. * - * @param string|array $paths A path or an array of paths where to look for templates + * @param string|array $paths A path or an array of paths where to look for templates + * @param string $namespace A path namespace */ - public function setPaths($paths) + public function setPaths($paths, $namespace = '__main__') { if (!is_array($paths)) { $paths = array($paths); } - $this->paths = array(); + $this->paths[$namespace] = array(); foreach ($paths as $path) { - $this->addPath($path); + $this->addPath($path, $namespace); } } /** * Adds a path where templates are stored. * - * @param string $path A path where to look for templates + * @param string $path A path where to look for templates + * @param string $namespace A path name + * + * @throws Twig_Error_Loader */ - public function addPath($path) + public function addPath($path, $namespace = '__main__') { // invalidate the cache $this->cache = array(); @@ -71,15 +90,37 @@ public function addPath($path) throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path)); } - $this->paths[] = $path; + $this->paths[$namespace][] = rtrim($path, '/\\'); } /** - * Gets the source code of a template, given its name. + * Prepends a path where templates are stored. * - * @param string $name The name of the template to load + * @param string $path A path where to look for templates + * @param string $namespace A path name * - * @return string The template source code + * @throws Twig_Error_Loader + */ + public function prependPath($path, $namespace = '__main__') + { + // invalidate the cache + $this->cache = array(); + + if (!is_dir($path)) { + throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path)); + } + + $path = rtrim($path, '/\\'); + + if (!isset($this->paths[$namespace])) { + $this->paths[$namespace][] = $path; + } else { + array_unshift($this->paths[$namespace], $path); + } + } + + /** + * {@inheritdoc} */ public function getSource($name) { @@ -87,11 +128,7 @@ public function getSource($name) } /** - * Gets the cache key to use for the cache for a given template name. - * - * @param string $name The name of the template to load - * - * @return string The cache key + * {@inheritdoc} */ public function getCacheKey($name) { @@ -99,18 +136,36 @@ public function getCacheKey($name) } /** - * Returns true if the template is still fresh. - * - * @param string $name The template name - * @param timestamp $time The last modification time of the cached template + * {@inheritdoc} + */ + public function exists($name) + { + $name = (string) $name; + if (isset($this->cache[$name])) { + return true; + } + + try { + $this->findTemplate($name); + + return true; + } catch (Twig_Error_Loader $exception) { + return false; + } + } + + /** + * {@inheritdoc} */ public function isFresh($name, $time) { - return filemtime($this->findTemplate($name)) < $time; + return filemtime($this->findTemplate($name)) <= $time; } protected function findTemplate($name) { + $name = (string) $name; + // normalize name $name = preg_replace('#/{2,}#', '/', strtr($name, '\\', '/')); @@ -120,13 +175,28 @@ protected function findTemplate($name) $this->validateName($name); - foreach ($this->paths as $path) { + $namespace = '__main__'; + if (isset($name[0]) && '@' == $name[0]) { + if (false === $pos = strpos($name, '/')) { + throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + } + + $namespace = substr($name, 1, $pos - 1); + + $name = substr($name, $pos + 1); + } + + if (!isset($this->paths[$namespace])) { + throw new Twig_Error_Loader(sprintf('There are no registered paths for namespace "%s".', $namespace)); + } + + foreach ($this->paths[$namespace] as $path) { if (is_file($path.'/'.$name)) { return $this->cache[$name] = $path.'/'.$name; } } - throw new Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths))); + throw new Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]))); } protected function validateName($name) @@ -135,6 +205,7 @@ protected function validateName($name) throw new Twig_Error_Loader('A template name cannot contain NUL bytes.'); } + $name = ltrim($name, '/'); $parts = explode('/', $name); $level = 0; foreach ($parts as $part) { diff --git a/app/parsers/Twig/Loader/String.php b/app/parsers/Twig/Loader/String.php old mode 100755 new mode 100644 index 26eb0096..8ad9856c --- a/app/parsers/Twig/Loader/String.php +++ b/app/parsers/Twig/Loader/String.php @@ -12,22 +12,21 @@ /** * Loads a template from a string. * + * This loader should only be used for unit testing as it has many limitations + * (for instance, the include or extends tag does not make any sense for a string + * loader). + * * When using this loader with a cache mechanism, you should know that a new cache * key is generated each time a template content "changes" (the cache key being the * source code of the template). If you don't want to see your cache grows out of * control, you need to take care of clearing the old cache file by yourself. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ -class Twig_Loader_String implements Twig_LoaderInterface +class Twig_Loader_String implements Twig_LoaderInterface, Twig_ExistsLoaderInterface { /** - * Gets the source code of a template, given its name. - * - * @param string $name The name of the template to load - * - * @return string The template source code + * {@inheritdoc} */ public function getSource($name) { @@ -35,11 +34,15 @@ public function getSource($name) } /** - * Gets the cache key to use for the cache for a given template name. - * - * @param string $name The name of the template to load - * - * @return string The cache key + * {@inheritdoc} + */ + public function exists($name) + { + return true; + } + + /** + * {@inheritdoc} */ public function getCacheKey($name) { @@ -47,10 +50,7 @@ public function getCacheKey($name) } /** - * Returns true if the template is still fresh. - * - * @param string $name The template name - * @param timestamp $time The last modification time of the cached template + * {@inheritdoc} */ public function isFresh($name, $time) { diff --git a/app/parsers/Twig/LoaderInterface.php b/app/parsers/Twig/LoaderInterface.php old mode 100755 new mode 100644 index f0bd3a5b..927786d1 --- a/app/parsers/Twig/LoaderInterface.php +++ b/app/parsers/Twig/LoaderInterface.php @@ -12,34 +12,41 @@ /** * Interface all loaders must implement. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_LoaderInterface { /** * Gets the source code of a template, given its name. * - * @param string $name The name of the template to load + * @param string $name The name of the template to load * * @return string The template source code + * + * @throws Twig_Error_Loader When $name is not found */ - function getSource($name); + public function getSource($name); /** * Gets the cache key to use for the cache for a given template name. * - * @param string $name The name of the template to load + * @param string $name The name of the template to load * * @return string The cache key + * + * @throws Twig_Error_Loader When $name is not found */ - function getCacheKey($name); + public function getCacheKey($name); /** * Returns true if the template is still fresh. * * @param string $name The template name * @param timestamp $time The last modification time of the cached template + * + * @return Boolean true if the template is fresh, false otherwise + * + * @throws Twig_Error_Loader When $name is not found */ - function isFresh($name, $time); + public function isFresh($name, $time); } diff --git a/app/parsers/Twig/Markup.php b/app/parsers/Twig/Markup.php old mode 100755 new mode 100644 index c1a1469c..69871fcb --- a/app/parsers/Twig/Markup.php +++ b/app/parsers/Twig/Markup.php @@ -12,20 +12,26 @@ /** * Marks a content as safe. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ -class Twig_Markup +class Twig_Markup implements Countable { protected $content; + protected $charset; - public function __construct($content) + public function __construct($content, $charset) { $this->content = (string) $content; + $this->charset = $charset; } public function __toString() { return $this->content; } + + public function count() + { + return function_exists('mb_get_info') ? mb_strlen($this->content, $this->charset) : strlen($this->content); + } } diff --git a/app/parsers/Twig/Node.php b/app/parsers/Twig/Node.php old mode 100755 new mode 100644 index 651ffc4d..931b4635 --- a/app/parsers/Twig/Node.php +++ b/app/parsers/Twig/Node.php @@ -13,8 +13,7 @@ /** * Represents a node in the AST. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node implements Twig_NodeInterface { @@ -134,12 +133,12 @@ public function hasAttribute($name) * * @param string The attribute name * - * @return mixed The attribute value + * @return mixed The attribute value */ public function getAttribute($name) { if (!array_key_exists($name, $this->attributes)) { - throw new Twig_Error_Runtime(sprintf('Attribute "%s" does not exist for Node "%s".', $name, get_class($this))); + throw new LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, get_class($this))); } return $this->attributes[$name]; @@ -188,7 +187,7 @@ public function hasNode($name) public function getNode($name) { if (!array_key_exists($name, $this->nodes)) { - throw new Twig_Error_Runtime(sprintf('Node "%s" does not exist for Node "%s".', $name, get_class($this))); + throw new LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, get_class($this))); } return $this->nodes[$name]; diff --git a/app/parsers/Twig/Node/AutoEscape.php b/app/parsers/Twig/Node/AutoEscape.php old mode 100755 new mode 100644 index a0c2ee6d..8f190e0b --- a/app/parsers/Twig/Node/AutoEscape.php +++ b/app/parsers/Twig/Node/AutoEscape.php @@ -18,8 +18,7 @@ * * If autoescaping is disabled, then the value is false. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_AutoEscape extends Twig_Node { diff --git a/app/parsers/Twig/Node/Block.php b/app/parsers/Twig/Node/Block.php old mode 100755 new mode 100644 index 5548ad06..50eb67ed --- a/app/parsers/Twig/Node/Block.php +++ b/app/parsers/Twig/Node/Block.php @@ -13,8 +13,7 @@ /** * Represents a block node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Block extends Twig_Node { diff --git a/app/parsers/Twig/Node/BlockReference.php b/app/parsers/Twig/Node/BlockReference.php old mode 100755 new mode 100644 index 53f6287c..013e369e --- a/app/parsers/Twig/Node/BlockReference.php +++ b/app/parsers/Twig/Node/BlockReference.php @@ -13,8 +13,7 @@ /** * Represents a block call node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_BlockReference extends Twig_Node implements Twig_NodeOutputInterface { diff --git a/app/parsers/Twig/Node/Body.php b/app/parsers/Twig/Node/Body.php old mode 100755 new mode 100644 index f72bf506..3ffb1342 --- a/app/parsers/Twig/Node/Body.php +++ b/app/parsers/Twig/Node/Body.php @@ -12,8 +12,7 @@ /** * Represents a body node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Body extends Twig_Node { diff --git a/app/parsers/Twig/Node/Do.php b/app/parsers/Twig/Node/Do.php new file mode 100644 index 00000000..c528066b --- /dev/null +++ b/app/parsers/Twig/Node/Do.php @@ -0,0 +1,38 @@ + + */ +class Twig_Node_Do extends Twig_Node +{ + public function __construct(Twig_Node_Expression $expr, $lineno, $tag = null) + { + parent::__construct(array('expr' => $expr), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/app/parsers/Twig/Node/Embed.php b/app/parsers/Twig/Node/Embed.php new file mode 100644 index 00000000..4c9456dc --- /dev/null +++ b/app/parsers/Twig/Node/Embed.php @@ -0,0 +1,38 @@ + + */ +class Twig_Node_Embed extends Twig_Node_Include +{ + // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) + public function __construct($filename, $index, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) + { + parent::__construct(new Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + + $this->setAttribute('filename', $filename); + $this->setAttribute('index', $index); + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { + $compiler + ->write("\$this->env->loadTemplate(") + ->string($this->getAttribute('filename')) + ->raw(', ') + ->string($this->getAttribute('index')) + ->raw(")") + ; + } +} diff --git a/app/parsers/Twig/Node/Expression.php b/app/parsers/Twig/Node/Expression.php old mode 100755 new mode 100644 index 13b170e5..a7382e7d --- a/app/parsers/Twig/Node/Expression.php +++ b/app/parsers/Twig/Node/Expression.php @@ -13,8 +13,7 @@ /** * Abstract class for all nodes that represents an expression. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ abstract class Twig_Node_Expression extends Twig_Node { diff --git a/app/parsers/Twig/Node/Expression/Array.php b/app/parsers/Twig/Node/Expression/Array.php old mode 100755 new mode 100644 index 2d860823..1da785fe --- a/app/parsers/Twig/Node/Expression/Array.php +++ b/app/parsers/Twig/Node/Expression/Array.php @@ -10,9 +10,54 @@ */ class Twig_Node_Expression_Array extends Twig_Node_Expression { + protected $index; + public function __construct(array $elements, $lineno) { parent::__construct($elements, array(), $lineno); + + $this->index = -1; + foreach ($this->getKeyValuePairs() as $pair) { + if ($pair['key'] instanceof Twig_Node_Expression_Constant && ctype_digit((string) $pair['key']->getAttribute('value')) && $pair['key']->getAttribute('value') > $this->index) { + $this->index = $pair['key']->getAttribute('value'); + } + } + } + + public function getKeyValuePairs() + { + $pairs = array(); + + foreach (array_chunk($this->nodes, 2) as $pair) { + $pairs[] = array( + 'key' => $pair[0], + 'value' => $pair[1], + ); + } + + return $pairs; + } + + public function hasElement(Twig_Node_Expression $key) + { + foreach ($this->getKeyValuePairs() as $pair) { + // we compare the string representation of the keys + // to avoid comparing the line numbers which are not relevant here. + if ((string) $key == (string) $pair['key']) { + return true; + } + } + + return false; + } + + public function addElement(Twig_Node_Expression $value, Twig_Node_Expression $key = null) + { + if (null === $key) { + $key = new Twig_Node_Expression_Constant(++$this->index, $value->getLine()); + } + + array_push($this->nodes, $key, $value); } /** @@ -24,16 +69,16 @@ public function compile(Twig_Compiler $compiler) { $compiler->raw('array('); $first = true; - foreach ($this->nodes as $name => $node) { + foreach ($this->getKeyValuePairs() as $pair) { if (!$first) { $compiler->raw(', '); } $first = false; $compiler - ->repr($name) + ->subcompile($pair['key']) ->raw(' => ') - ->subcompile($node) + ->subcompile($pair['value']) ; } $compiler->raw(')'); diff --git a/app/parsers/Twig/Node/Expression/AssignName.php b/app/parsers/Twig/Node/Expression/AssignName.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary.php b/app/parsers/Twig/Node/Expression/Binary.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Add.php b/app/parsers/Twig/Node/Expression/Binary/Add.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/And.php b/app/parsers/Twig/Node/Expression/Binary/And.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/BitwiseAnd.php b/app/parsers/Twig/Node/Expression/Binary/BitwiseAnd.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/BitwiseOr.php b/app/parsers/Twig/Node/Expression/Binary/BitwiseOr.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/BitwiseXor.php b/app/parsers/Twig/Node/Expression/Binary/BitwiseXor.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Concat.php b/app/parsers/Twig/Node/Expression/Binary/Concat.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Div.php b/app/parsers/Twig/Node/Expression/Binary/Div.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Equal.php b/app/parsers/Twig/Node/Expression/Binary/Equal.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/FloorDiv.php b/app/parsers/Twig/Node/Expression/Binary/FloorDiv.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Greater.php b/app/parsers/Twig/Node/Expression/Binary/Greater.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/GreaterEqual.php b/app/parsers/Twig/Node/Expression/Binary/GreaterEqual.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/In.php b/app/parsers/Twig/Node/Expression/Binary/In.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Less.php b/app/parsers/Twig/Node/Expression/Binary/Less.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/LessEqual.php b/app/parsers/Twig/Node/Expression/Binary/LessEqual.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Mod.php b/app/parsers/Twig/Node/Expression/Binary/Mod.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Mul.php b/app/parsers/Twig/Node/Expression/Binary/Mul.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/NotEqual.php b/app/parsers/Twig/Node/Expression/Binary/NotEqual.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/NotIn.php b/app/parsers/Twig/Node/Expression/Binary/NotIn.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Or.php b/app/parsers/Twig/Node/Expression/Binary/Or.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Power.php b/app/parsers/Twig/Node/Expression/Binary/Power.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Range.php b/app/parsers/Twig/Node/Expression/Binary/Range.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Binary/Sub.php b/app/parsers/Twig/Node/Expression/Binary/Sub.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/BlockReference.php b/app/parsers/Twig/Node/Expression/BlockReference.php old mode 100755 new mode 100644 index 174d9097..647196eb --- a/app/parsers/Twig/Node/Expression/BlockReference.php +++ b/app/parsers/Twig/Node/Expression/BlockReference.php @@ -13,8 +13,7 @@ /** * Represents a block call node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_BlockReference extends Twig_Node_Expression { diff --git a/app/parsers/Twig/Node/Expression/Call.php b/app/parsers/Twig/Node/Expression/Call.php new file mode 100644 index 00000000..ec76cc6e --- /dev/null +++ b/app/parsers/Twig/Node/Expression/Call.php @@ -0,0 +1,189 @@ +getAttribute('callable'); causes the error ... + // + // An exception has been thrown during the compilation of a template + // ("Attribute "callable" does not exist for Node + // "Twig_Node_Expression_Function".") + // + // ... to be thrown. Original line is ... + // + // $callable = $this->getAttribute('callable'); + // + $callable = true; + + $closingParenthesis = false; + if ($callable) { + if (is_string($callable)) { + $compiler->raw($callable); + } elseif (is_array($callable) && $callable[0] instanceof Twig_ExtensionInterface) { + $compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', $callable[0]->getName(), $callable[1])); + } else { + $type = ucfirst($this->getAttribute('type')); + $compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', $type, $this->getAttribute('name'))); + $closingParenthesis = true; + } + } else { + $compiler->raw($this->getAttribute('thing')->compile()); + } + + $this->compileArguments($compiler); + + if ($closingParenthesis) { + $compiler->raw(')'); + } + } + + protected function compileArguments(Twig_Compiler $compiler) + { + $compiler->raw('('); + + $first = true; + + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + $compiler->raw('$this->env'); + $first = false; + } + + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$context'); + $first = false; + } + + if ($this->hasAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->string($argument); + $first = false; + } + } + + if ($this->hasNode('node')) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($this->getNode('node')); + $first = false; + } + + if ($this->hasNode('arguments') && null !== $this->getNode('arguments')) { + $callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null; + + $arguments = $this->getArguments($callable, $this->getNode('arguments')); + + foreach ($arguments as $node) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($node); + $first = false; + } + } + + $compiler->raw(')'); + } + + protected function getArguments($callable, $arguments) + { + $parameters = array(); + $named = false; + foreach ($arguments as $name => $node) { + if (!is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } elseif ($named) { + throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name'))); + } + + $parameters[$name] = $node; + } + + if (!$named) { + return $parameters; + } + + if (!$callable) { + throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name'))); + } + + // manage named arguments + if (is_array($callable)) { + $r = new ReflectionMethod($callable[0], $callable[1]); + } elseif (is_object($callable) && !$callable instanceof Closure) { + $r = new ReflectionObject($callable); + $r = $r->getMethod('__invoke'); + } else { + $r = new ReflectionFunction($callable); + } + + $definition = $r->getParameters(); + if ($this->hasNode('node')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + array_shift($definition); + } + if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + array_shift($definition); + } + } + + $arguments = array(); + $pos = 0; + foreach ($definition as $param) { + $name = $this->normalizeName($param->name); + + if (array_key_exists($name, $parameters)) { + if (array_key_exists($pos, $parameters)) { + throw new Twig_Error_Syntax(sprintf('Arguments "%s" is defined twice for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + } elseif (array_key_exists($pos, $parameters)) { + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + ++$pos; + } elseif ($param->isDefaultValueAvailable()) { + $arguments[] = new Twig_Node_Expression_Constant($param->getDefaultValue(), -1); + } elseif ($param->isOptional()) { + break; + } else { + throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + } + + foreach (array_keys($parameters) as $name) { + throw new Twig_Error_Syntax(sprintf('Unknown argument "%s" for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + + return $arguments; + } + + protected function normalizeName($name) + { + return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name)); + } +} diff --git a/app/parsers/Twig/Node/Expression/Conditional.php b/app/parsers/Twig/Node/Expression/Conditional.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Constant.php b/app/parsers/Twig/Node/Expression/Constant.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/ExtensionReference.php b/app/parsers/Twig/Node/Expression/ExtensionReference.php old mode 100755 new mode 100644 index cb4efad0..00ac6701 --- a/app/parsers/Twig/Node/Expression/ExtensionReference.php +++ b/app/parsers/Twig/Node/Expression/ExtensionReference.php @@ -12,8 +12,7 @@ /** * Represents an extension call node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_ExtensionReference extends Twig_Node_Expression { diff --git a/app/parsers/Twig/Node/Expression/Filter.php b/app/parsers/Twig/Node/Expression/Filter.php old mode 100755 new mode 100644 index bff1a676..207b062a --- a/app/parsers/Twig/Node/Expression/Filter.php +++ b/app/parsers/Twig/Node/Expression/Filter.php @@ -9,7 +9,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -class Twig_Node_Expression_Filter extends Twig_Node_Expression +class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call { public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null) { @@ -19,29 +19,18 @@ public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Const public function compile(Twig_Compiler $compiler) { $name = $this->getNode('filter')->getAttribute('value'); - if (false === $filter = $compiler->getEnvironment()->getFilter($name)) { - throw new Twig_Error_Syntax(sprintf('The filter "%s" does not exist', $name), $this->getLine()); - } - - $this->compileFilter($compiler, $filter); - } - - protected function compileFilter(Twig_Compiler $compiler, Twig_FilterInterface $filter) - { - $compiler - ->raw($filter->compile().'(') - ->raw($filter->needsEnvironment() ? '$this->env, ' : '') - ->raw($filter->needsContext() ? '$context, ' : '') - ->subcompile($this->getNode('node')) - ; + $filter = $compiler->getEnvironment()->getFilter($name); - foreach ($this->getNode('arguments') as $node) { - $compiler - ->raw(', ') - ->subcompile($node) - ; + $this->setAttribute('name', $name); + $this->setAttribute('type', 'filter'); + $this->setAttribute('thing', $filter); + $this->setAttribute('needs_environment', $filter->needsEnvironment()); + $this->setAttribute('needs_context', $filter->needsContext()); + $this->setAttribute('arguments', $filter->getArguments()); + if ($filter instanceof Twig_FilterCallableInterface || $filter instanceof Twig_SimpleFilter) { + $this->setAttribute('callable', $filter->getCallable()); } - $compiler->raw(')'); + $this->compileCallable($compiler); } } diff --git a/app/parsers/Twig/Node/Expression/Filter/Default.php b/app/parsers/Twig/Node/Expression/Filter/Default.php old mode 100755 new mode 100644 index 1cb33428..1827c888 --- a/app/parsers/Twig/Node/Expression/Filter/Default.php +++ b/app/parsers/Twig/Node/Expression/Filter/Default.php @@ -16,14 +16,13 @@ * {{ var.foo|default('foo item on var is not defined') }} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Filter_Default extends Twig_Node_Expression_Filter { public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null) { - $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('_default', $node->getLine()), $arguments, $node->getLine()); + $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('default', $node->getLine()), $arguments, $node->getLine()); if ('default' === $filterName->getAttribute('value') && ($node instanceof Twig_Node_Expression_Name || $node instanceof Twig_Node_Expression_GetAttr)) { $test = new Twig_Node_Expression_Test_Defined(clone $node, 'defined', new Twig_Node(), $node->getLine()); diff --git a/app/parsers/Twig/Node/Expression/Function.php b/app/parsers/Twig/Node/Expression/Function.php old mode 100755 new mode 100644 index 3f457199..3e1f6b55 --- a/app/parsers/Twig/Node/Expression/Function.php +++ b/app/parsers/Twig/Node/Expression/Function.php @@ -8,7 +8,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -class Twig_Node_Expression_Function extends Twig_Node_Expression +class Twig_Node_Expression_Function extends Twig_Node_Expression_Call { public function __construct($name, Twig_NodeInterface $arguments, $lineno) { @@ -17,33 +17,19 @@ public function __construct($name, Twig_NodeInterface $arguments, $lineno) public function compile(Twig_Compiler $compiler) { - $function = $compiler->getEnvironment()->getFunction($this->getAttribute('name')); - if (false === $function) { - throw new Twig_Error_Syntax(sprintf('The function "%s" does not exist', $this->getAttribute('name')), $this->getLine()); - } - - $compiler - ->raw($function->compile().'(') - ->raw($function->needsEnvironment() ? '$this->env' : '') - ; - - if ($function->needsContext()) { - $compiler->raw($function->needsEnvironment() ? ', $context' : '$context'); - } + $name = $this->getAttribute('name'); + $function = $compiler->getEnvironment()->getFunction($name); - $first = true; - foreach ($this->getNode('arguments') as $node) { - if (!$first) { - $compiler->raw(', '); - } else { - if ($function->needsEnvironment() || $function->needsContext()) { - $compiler->raw(', '); - } - $first = false; - } - $compiler->subcompile($node); + $this->setAttribute('name', $name); + $this->setAttribute('type', 'function'); + $this->setAttribute('thing', $function); + $this->setAttribute('needs_environment', $function->needsEnvironment()); + $this->setAttribute('needs_context', $function->needsContext()); + $this->setAttribute('arguments', $function->getArguments()); + if ($function instanceof Twig_FunctionCallableInterface || $function instanceof Twig_SimpleFunction) { + $this->setAttribute('callable', $function->getCallable()); } - $compiler->raw(')'); + $this->compileCallable($compiler); } } diff --git a/app/parsers/Twig/Node/Expression/GetAttr.php b/app/parsers/Twig/Node/Expression/GetAttr.php old mode 100755 new mode 100644 index e6f5ba23..81a9b137 --- a/app/parsers/Twig/Node/Expression/GetAttr.php +++ b/app/parsers/Twig/Node/Expression/GetAttr.php @@ -11,14 +11,14 @@ */ class Twig_Node_Expression_GetAttr extends Twig_Node_Expression { - public function __construct(Twig_Node_Expression $node, Twig_Node_Expression $attribute, Twig_NodeInterface $arguments, $type, $lineno) + public function __construct(Twig_Node_Expression $node, Twig_Node_Expression $attribute, Twig_Node_Expression_Array $arguments, $type, $lineno) { - parent::__construct(array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), array('type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false), $lineno); + parent::__construct(array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), array('type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'disable_c_ext' => false), $lineno); } public function compile(Twig_Compiler $compiler) { - if (function_exists('twig_template_get_attributes')) { + if (function_exists('twig_template_get_attributes') && !$this->getAttribute('disable_c_ext')) { $compiler->raw('twig_template_get_attributes($this, '); } else { $compiler->raw('$this->getAttribute('); @@ -33,16 +33,7 @@ public function compile(Twig_Compiler $compiler) $compiler->raw(', ')->subcompile($this->getNode('attribute')); if (count($this->getNode('arguments')) || Twig_TemplateInterface::ANY_CALL !== $this->getAttribute('type') || $this->getAttribute('is_defined_test') || $this->getAttribute('ignore_strict_check')) { - $compiler->raw(', array('); - - foreach ($this->getNode('arguments') as $node) { - $compiler - ->subcompile($node) - ->raw(', ') - ; - } - - $compiler->raw(')'); + $compiler->raw(', ')->subcompile($this->getNode('arguments')); if (Twig_TemplateInterface::ANY_CALL !== $this->getAttribute('type') || $this->getAttribute('is_defined_test') || $this->getAttribute('ignore_strict_check')) { $compiler->raw(', ')->repr($this->getAttribute('type')); diff --git a/app/parsers/Twig/Node/Expression/MethodCall.php b/app/parsers/Twig/Node/Expression/MethodCall.php new file mode 100644 index 00000000..620b02bf --- /dev/null +++ b/app/parsers/Twig/Node/Expression/MethodCall.php @@ -0,0 +1,41 @@ + $node, 'arguments' => $arguments), array('method' => $method, 'safe' => false), $lineno); + + if ($node instanceof Twig_Node_Expression_Name) { + $node->setAttribute('always_defined', true); + } + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->subcompile($this->getNode('node')) + ->raw('->') + ->raw($this->getAttribute('method')) + ->raw('(') + ; + $first = true; + foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + $compiler->subcompile($pair['value']); + } + $compiler->raw(')'); + } +} diff --git a/app/parsers/Twig/Node/Expression/Name.php b/app/parsers/Twig/Node/Expression/Name.php old mode 100755 new mode 100644 index 4b8d541f..3b8fae01 --- a/app/parsers/Twig/Node/Expression/Name.php +++ b/app/parsers/Twig/Node/Expression/Name.php @@ -19,7 +19,7 @@ class Twig_Node_Expression_Name extends Twig_Node_Expression public function __construct($name, $lineno) { - parent::__construct(array(), array('name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false), $lineno); + parent::__construct(array(), array('name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false), $lineno); } public function compile(Twig_Compiler $compiler) @@ -34,16 +34,31 @@ public function compile(Twig_Compiler $compiler) } } elseif ($this->isSpecial()) { $compiler->raw($this->specialVars[$name]); + } elseif ($this->getAttribute('always_defined')) { + $compiler + ->raw('$context[') + ->string($name) + ->raw(']') + ; } else { - if (version_compare(phpversion(), '5.4.0RC1', '>=') && ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables())) { + // remove the non-PHP 5.4 version when PHP 5.3 support is dropped + // as the non-optimized version is just a workaround for slow ternary operator + // when the context has a lot of variables + if (version_compare(phpversion(), '5.4.0RC1', '>=')) { // PHP 5.4 ternary operator performance was optimized $compiler ->raw('(isset($context[') ->string($name) ->raw(']) ? $context[') ->string($name) - ->raw('] : null)') + ->raw('] : ') ; + + if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) { + $compiler->raw('null)'); + } else { + $compiler->raw('$this->getContext($context, ')->string($name)->raw('))'); + } } else { $compiler ->raw('$this->getContext($context, ') diff --git a/app/parsers/Twig/Node/Expression/Parent.php b/app/parsers/Twig/Node/Expression/Parent.php old mode 100755 new mode 100644 index ea973494..dcf618c0 --- a/app/parsers/Twig/Node/Expression/Parent.php +++ b/app/parsers/Twig/Node/Expression/Parent.php @@ -13,8 +13,7 @@ /** * Represents a parent node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Parent extends Twig_Node_Expression { diff --git a/app/parsers/Twig/Node/Expression/TempName.php b/app/parsers/Twig/Node/Expression/TempName.php old mode 100755 new mode 100644 index eea9d472..e6b058e8 --- a/app/parsers/Twig/Node/Expression/TempName.php +++ b/app/parsers/Twig/Node/Expression/TempName.php @@ -17,6 +17,10 @@ public function __construct($name, $lineno) public function compile(Twig_Compiler $compiler) { - $compiler->raw('$_')->raw($this->getAttribute('name'))->raw('_'); + $compiler + ->raw('$_') + ->raw($this->getAttribute('name')) + ->raw('_') + ; } } diff --git a/app/parsers/Twig/Node/Expression/Test.php b/app/parsers/Twig/Node/Expression/Test.php old mode 100755 new mode 100644 index 088f60e4..639f501a --- a/app/parsers/Twig/Node/Expression/Test.php +++ b/app/parsers/Twig/Node/Expression/Test.php @@ -8,7 +8,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -class Twig_Node_Expression_Test extends Twig_Node_Expression +class Twig_Node_Expression_Test extends Twig_Node_Expression_Call { public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno) { @@ -17,32 +17,16 @@ public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface public function compile(Twig_Compiler $compiler) { - $testMap = $compiler->getEnvironment()->getTests(); - if (!isset($testMap[$this->getAttribute('name')])) { - throw new Twig_Error_Syntax(sprintf('The test "%s" does not exist', $this->getAttribute('name')), $this->getLine()); - } - $name = $this->getAttribute('name'); - $node = $this->getNode('node'); - - $compiler - ->raw($testMap[$name]->compile().'(') - ->subcompile($node) - ; - - if (null !== $this->getNode('arguments')) { - $compiler->raw(', '); - - $max = count($this->getNode('arguments')) - 1; - foreach ($this->getNode('arguments') as $i => $arg) { - $compiler->subcompile($arg); + $test = $compiler->getEnvironment()->getTest($name); - if ($i != $max) { - $compiler->raw(', '); - } - } + $this->setAttribute('name', $name); + $this->setAttribute('type', 'test'); + $this->setAttribute('thing', $test); + if ($test instanceof Twig_TestCallableInterface || $test instanceof Twig_SimpleTest) { + $this->setAttribute('callable', $test->getCallable()); } - $compiler->raw(')'); + $this->compileCallable($compiler); } } diff --git a/app/parsers/Twig/Node/Expression/Test/Constant.php b/app/parsers/Twig/Node/Expression/Test/Constant.php old mode 100755 new mode 100644 index 6e6b6fd7..de55f5f5 --- a/app/parsers/Twig/Node/Expression/Test/Constant.php +++ b/app/parsers/Twig/Node/Expression/Test/Constant.php @@ -18,8 +18,7 @@ * {% endif %} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Constant extends Twig_Node_Expression_Test { @@ -29,6 +28,17 @@ public function compile(Twig_Compiler $compiler) ->raw('(') ->subcompile($this->getNode('node')) ->raw(' === constant(') + ; + + if ($this->getNode('arguments')->hasNode(1)) { + $compiler + ->raw('get_class(') + ->subcompile($this->getNode('arguments')->getNode(1)) + ->raw(')."::".') + ; + } + + $compiler ->subcompile($this->getNode('arguments')->getNode(0)) ->raw('))') ; diff --git a/app/parsers/Twig/Node/Expression/Test/Defined.php b/app/parsers/Twig/Node/Expression/Test/Defined.php old mode 100755 new mode 100644 index e7c68280..247b2e23 --- a/app/parsers/Twig/Node/Expression/Test/Defined.php +++ b/app/parsers/Twig/Node/Expression/Test/Defined.php @@ -19,8 +19,7 @@ * {% endif %} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Test/Divisibleby.php b/app/parsers/Twig/Node/Expression/Test/Divisibleby.php old mode 100755 new mode 100644 index 05563d57..0aceb530 --- a/app/parsers/Twig/Node/Expression/Test/Divisibleby.php +++ b/app/parsers/Twig/Node/Expression/Test/Divisibleby.php @@ -16,8 +16,7 @@ * {% if loop.index is divisibleby(3) %} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Divisibleby extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Test/Even.php b/app/parsers/Twig/Node/Expression/Test/Even.php old mode 100755 new mode 100644 index 08e6d829..d7853e89 --- a/app/parsers/Twig/Node/Expression/Test/Even.php +++ b/app/parsers/Twig/Node/Expression/Test/Even.php @@ -16,8 +16,7 @@ * {{ var is even }} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Even extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Test/Null.php b/app/parsers/Twig/Node/Expression/Test/Null.php old mode 100755 new mode 100644 index 55061db3..1c83825a --- a/app/parsers/Twig/Node/Expression/Test/Null.php +++ b/app/parsers/Twig/Node/Expression/Test/Null.php @@ -16,8 +16,7 @@ * {{ var is none }} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Null extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Test/Odd.php b/app/parsers/Twig/Node/Expression/Test/Odd.php old mode 100755 new mode 100644 index 5fecebcb..421c19e8 --- a/app/parsers/Twig/Node/Expression/Test/Odd.php +++ b/app/parsers/Twig/Node/Expression/Test/Odd.php @@ -16,8 +16,7 @@ * {{ var is odd }} * * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Test/Sameas.php b/app/parsers/Twig/Node/Expression/Test/Sameas.php old mode 100755 new mode 100644 index 8639b965..b48905ee --- a/app/parsers/Twig/Node/Expression/Test/Sameas.php +++ b/app/parsers/Twig/Node/Expression/Test/Sameas.php @@ -12,8 +12,7 @@ /** * Checks if a variable is the same as another one (=== in PHP). * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Expression_Test_Sameas extends Twig_Node_Expression_Test { diff --git a/app/parsers/Twig/Node/Expression/Unary.php b/app/parsers/Twig/Node/Expression/Unary.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Unary/Neg.php b/app/parsers/Twig/Node/Expression/Unary/Neg.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Unary/Not.php b/app/parsers/Twig/Node/Expression/Unary/Not.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Expression/Unary/Pos.php b/app/parsers/Twig/Node/Expression/Unary/Pos.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Flush.php b/app/parsers/Twig/Node/Flush.php new file mode 100644 index 00000000..0467ddce --- /dev/null +++ b/app/parsers/Twig/Node/Flush.php @@ -0,0 +1,36 @@ + + */ +class Twig_Node_Flush extends Twig_Node +{ + public function __construct($lineno, $tag) + { + parent::__construct(array(), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("flush();\n") + ; + } +} diff --git a/app/parsers/Twig/Node/For.php b/app/parsers/Twig/Node/For.php old mode 100755 new mode 100644 index c8565f14..d1ff371d --- a/app/parsers/Twig/Node/For.php +++ b/app/parsers/Twig/Node/For.php @@ -13,8 +13,7 @@ /** * Represents a for node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_For extends Twig_Node { @@ -22,7 +21,7 @@ class Twig_Node_For extends Twig_Node public function __construct(Twig_Node_Expression_AssignName $keyTarget, Twig_Node_Expression_AssignName $valueTarget, Twig_Node_Expression $seq, Twig_Node_Expression $ifexpr = null, Twig_NodeInterface $body, Twig_NodeInterface $else = null, $lineno, $tag = null) { - $body->setNode('_for_loop', $this->loop = new Twig_Node_ForLoop($lineno, $tag)); + $body = new Twig_Node(array($body, $this->loop = new Twig_Node_ForLoop($lineno, $tag))); if (null !== $ifexpr) { $body = new Twig_Node_If(new Twig_Node(array($ifexpr, $body)), null, $lineno, $tag); @@ -108,6 +107,6 @@ public function compile(Twig_Compiler $compiler) $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n"); // keep the values set in the inner context for variables defined in the outer context - $compiler->write("\$context = array_merge(\$_parent, array_intersect_key(\$context, \$_parent));\n"); + $compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n"); } } diff --git a/app/parsers/Twig/Node/ForLoop.php b/app/parsers/Twig/Node/ForLoop.php old mode 100755 new mode 100644 index 38f2e852..b8841583 --- a/app/parsers/Twig/Node/ForLoop.php +++ b/app/parsers/Twig/Node/ForLoop.php @@ -12,8 +12,7 @@ /** * Internal node used by the for node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_ForLoop extends Twig_Node { diff --git a/app/parsers/Twig/Node/If.php b/app/parsers/Twig/Node/If.php old mode 100755 new mode 100644 index aa12efbe..4296a8d6 --- a/app/parsers/Twig/Node/If.php +++ b/app/parsers/Twig/Node/If.php @@ -13,8 +13,7 @@ /** * Represents an if node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_If extends Twig_Node { diff --git a/app/parsers/Twig/Node/Import.php b/app/parsers/Twig/Node/Import.php old mode 100755 new mode 100644 index a327411d..99efc091 --- a/app/parsers/Twig/Node/Import.php +++ b/app/parsers/Twig/Node/Import.php @@ -12,8 +12,7 @@ /** * Represents an import node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Import extends Twig_Node { diff --git a/app/parsers/Twig/Node/Include.php b/app/parsers/Twig/Node/Include.php old mode 100755 new mode 100644 index 467749b5..ed4a3751 --- a/app/parsers/Twig/Node/Include.php +++ b/app/parsers/Twig/Node/Include.php @@ -13,8 +13,7 @@ /** * Represents an include node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Include extends Twig_Node implements Twig_NodeOutputInterface { @@ -39,21 +38,46 @@ public function compile(Twig_Compiler $compiler) ; } + $this->addGetTemplate($compiler); + + $compiler->raw('->display('); + + $this->addTemplateArguments($compiler); + + $compiler->raw(");\n"); + + if ($this->getAttribute('ignore_missing')) { + $compiler + ->outdent() + ->write("} catch (Twig_Error_Loader \$e) {\n") + ->indent() + ->write("// ignore missing template\n") + ->outdent() + ->write("}\n\n") + ; + } + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { if ($this->getNode('expr') instanceof Twig_Node_Expression_Constant) { $compiler ->write("\$this->env->loadTemplate(") ->subcompile($this->getNode('expr')) - ->raw(")->display(") + ->raw(")") ; } else { $compiler ->write("\$template = \$this->env->resolveTemplate(") ->subcompile($this->getNode('expr')) ->raw(");\n") - ->write('$template->display(') + ->write('$template') ; } + } + protected function addTemplateArguments(Twig_Compiler $compiler) + { if (false === $this->getAttribute('only')) { if (null === $this->getNode('variables')) { $compiler->raw('$context'); @@ -71,18 +95,5 @@ public function compile(Twig_Compiler $compiler) $compiler->subcompile($this->getNode('variables')); } } - - $compiler->raw(");\n"); - - if ($this->getAttribute('ignore_missing')) { - $compiler - ->outdent() - ->write("} catch (Twig_Error_Loader \$e) {\n") - ->indent() - ->write("// ignore missing template\n") - ->outdent() - ->write("}\n\n") - ; - } } } diff --git a/app/parsers/Twig/Node/Macro.php b/app/parsers/Twig/Node/Macro.php old mode 100755 new mode 100644 index be747266..89910618 --- a/app/parsers/Twig/Node/Macro.php +++ b/app/parsers/Twig/Node/Macro.php @@ -12,8 +12,7 @@ /** * Represents a macro node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Macro extends Twig_Node { @@ -29,14 +28,27 @@ public function __construct($name, Twig_NodeInterface $body, Twig_NodeInterface */ public function compile(Twig_Compiler $compiler) { - $arguments = array(); - foreach ($this->getNode('arguments') as $argument) { - $arguments[] = '$'.$argument->getAttribute('name').' = null'; + $compiler + ->addDebugInfo($this) + ->write(sprintf("public function get%s(", $this->getAttribute('name'))) + ; + + $count = count($this->getNode('arguments')); + $pos = 0; + foreach ($this->getNode('arguments') as $name => $default) { + $compiler + ->raw('$_'.$name.' = ') + ->subcompile($default) + ; + + if (++$pos < $count) { + $compiler->raw(', '); + } } $compiler - ->addDebugInfo($this) - ->write(sprintf("public function get%s(%s)\n", $this->getAttribute('name'), implode(', ', $arguments)), "{\n") + ->raw(")\n") + ->write("{\n") ->indent() ; @@ -44,15 +56,15 @@ public function compile(Twig_Compiler $compiler) $compiler->write("\$context = \$this->env->getGlobals();\n\n"); } else { $compiler - ->write("\$context = array_merge(\$this->env->getGlobals(), array(\n") + ->write("\$context = \$this->env->mergeGlobals(array(\n") ->indent() ; - foreach ($this->getNode('arguments') as $argument) { + foreach ($this->getNode('arguments') as $name => $default) { $compiler ->write('') - ->string($argument->getAttribute('name')) - ->raw(' => $'.$argument->getAttribute('name')) + ->string($name) + ->raw(' => $_'.$name) ->raw(",\n") ; } @@ -64,18 +76,19 @@ public function compile(Twig_Compiler $compiler) } $compiler + ->write("\$blocks = array();\n\n") ->write("ob_start();\n") ->write("try {\n") ->indent() ->subcompile($this->getNode('body')) ->outdent() - ->write("} catch(Exception \$e) {\n") + ->write("} catch (Exception \$e) {\n") ->indent() ->write("ob_end_clean();\n\n") ->write("throw \$e;\n") ->outdent() ->write("}\n\n") - ->write("return ob_get_clean();\n") + ->write("return ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());\n") ->outdent() ->write("}\n\n") ; diff --git a/app/parsers/Twig/Node/Module.php b/app/parsers/Twig/Node/Module.php old mode 100755 new mode 100644 index 7321e063..585048b8 --- a/app/parsers/Twig/Node/Module.php +++ b/app/parsers/Twig/Node/Module.php @@ -13,14 +13,19 @@ /** * Represents a module node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Module extends Twig_Node { - public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $filename) + public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $embeddedTemplates, $filename) { - parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits), array('filename' => $filename), 1); + // embedded templates are set as attributes so that they are only visited once by the visitors + parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits), array('filename' => $filename, 'index' => null, 'embedded_templates' => $embeddedTemplates), 1); + } + + public function setIndex($index) + { + $this->setAttribute('index', $index); } /** @@ -31,13 +36,21 @@ public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $pare public function compile(Twig_Compiler $compiler) { $this->compileTemplate($compiler); + + foreach ($this->getAttribute('embedded_templates') as $template) { + $compiler->subcompile($template); + } } protected function compileTemplate(Twig_Compiler $compiler) { + if (!$this->getAttribute('index')) { + $compiler->write('compileClassHeader($compiler); - if (count($this->getNode('blocks')) || count($this->getNode('traits'))) { + if (count($this->getNode('blocks')) || count($this->getNode('traits')) || null === $this->getNode('parent') || $this->getNode('parent') instanceof Twig_Node_Expression_Constant) { $this->compileConstructor($compiler); } @@ -57,29 +70,31 @@ protected function compileTemplate(Twig_Compiler $compiler) $this->compileIsTraitable($compiler); + $this->compileDebugInfo($compiler); + $this->compileClassFooter($compiler); } protected function compileGetParent(Twig_Compiler $compiler) { + if (null === $this->getNode('parent')) { + return; + } + $compiler ->write("protected function doGetParent(array \$context)\n", "{\n") ->indent() ->write("return ") ; - if (null === $this->getNode('parent')) { - $compiler->raw("false"); + if ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $compiler->subcompile($this->getNode('parent')); } else { - if ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { - $compiler->subcompile($this->getNode('parent')); - } else { - $compiler - ->raw("\$this->env->resolveTemplate(") - ->subcompile($this->getNode('parent')) - ->raw(")") - ; - } + $compiler + ->raw("\$this->env->resolveTemplate(") + ->subcompile($this->getNode('parent')) + ->raw(")") + ; } $compiler @@ -94,17 +109,22 @@ protected function compileDisplayBody(Twig_Compiler $compiler) $compiler->subcompile($this->getNode('body')); if (null !== $this->getNode('parent')) { - $compiler->write("\$this->getParent(\$context)->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); + if ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $compiler->write("\$this->parent"); + } else { + $compiler->write("\$this->getParent(\$context)"); + } + $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); } } protected function compileClassHeader(Twig_Compiler $compiler) { $compiler - ->write("write("\n\n") // if the filename contains */, add a blank to avoid a PHP parse error ->write("/* ".str_replace('*/', '* /', $this->getAttribute('filename'))." */\n") - ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getAttribute('filename'))) + ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getAttribute('filename'), $this->getAttribute('index'))) ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass())) ->write("{\n") ->indent() @@ -119,6 +139,17 @@ protected function compileConstructor(Twig_Compiler $compiler) ->write("parent::__construct(\$env);\n\n") ; + // parent + if (null === $this->getNode('parent')) { + $compiler->write("\$this->parent = false;\n\n"); + } elseif ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $compiler + ->write("\$this->parent = \$this->env->loadTemplate(") + ->subcompile($this->getNode('parent')) + ->raw(");\n\n") + ; + } + $countTraits = count($this->getNode('traits')); if ($countTraits) { // traits @@ -150,22 +181,28 @@ protected function compileConstructor(Twig_Compiler $compiler) } } - $compiler - ->write("\$this->traits = array_merge(\n") - ->indent() - ; + if ($countTraits > 1) { + $compiler + ->write("\$this->traits = array_merge(\n") + ->indent() + ; + + for ($i = 0; $i < $countTraits; $i++) { + $compiler + ->write(sprintf("\$_trait_%s_blocks".($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ; + } - for ($i = 0; $i < $countTraits; $i++) { $compiler - ->write(sprintf("\$_trait_%s_blocks".($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ->outdent() + ->write(");\n\n") + ; + } else { + $compiler + ->write("\$this->traits = \$_trait_0_blocks;\n\n") ; } - $compiler - ->outdent() - ->write(");\n\n") - ; - $compiler ->write("\$this->blocks = array_merge(\n") ->indent() @@ -285,16 +322,31 @@ protected function compileIsTraitable(Twig_Compiler $compiler) } } + if ($traitable) { + return; + } + $compiler ->write("public function isTraitable()\n", "{\n") ->indent() ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) ->outdent() + ->write("}\n\n") + ; + } + + protected function compileDebugInfo(Twig_Compiler $compiler) + { + $compiler + ->write("public function getDebugInfo()\n", "{\n") + ->indent() + ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->outdent() ->write("}\n") ; } - public function compileLoadTemplate(Twig_Compiler $compiler, $node, $var) + protected function compileLoadTemplate(Twig_Compiler $compiler, $node, $var) { if ($node instanceof Twig_Node_Expression_Constant) { $compiler diff --git a/app/parsers/Twig/Node/Print.php b/app/parsers/Twig/Node/Print.php old mode 100755 new mode 100644 index 766725ff..b0c41d1d --- a/app/parsers/Twig/Node/Print.php +++ b/app/parsers/Twig/Node/Print.php @@ -13,8 +13,7 @@ /** * Represents a node that outputs an expression. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Print extends Twig_Node implements Twig_NodeOutputInterface { diff --git a/app/parsers/Twig/Node/Sandbox.php b/app/parsers/Twig/Node/Sandbox.php old mode 100755 new mode 100644 index cbfcb411..8cf3ed44 --- a/app/parsers/Twig/Node/Sandbox.php +++ b/app/parsers/Twig/Node/Sandbox.php @@ -12,8 +12,7 @@ /** * Represents a sandbox node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Sandbox extends Twig_Node { diff --git a/app/parsers/Twig/Node/SandboxedModule.php b/app/parsers/Twig/Node/SandboxedModule.php old mode 100755 new mode 100644 index 36d9f198..be1f5daa --- a/app/parsers/Twig/Node/SandboxedModule.php +++ b/app/parsers/Twig/Node/SandboxedModule.php @@ -13,8 +13,7 @@ /** * Represents a module node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_SandboxedModule extends Twig_Node_Module { @@ -24,7 +23,9 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module public function __construct(Twig_Node_Module $node, array $usedFilters, array $usedTags, array $usedFunctions) { - parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag()); + parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getAttribute('embedded_templates'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag()); + + $this->setAttribute('index', $node->getAttribute('index')); $this->usedFilters = $usedFilters; $this->usedTags = $usedTags; @@ -33,9 +34,7 @@ public function __construct(Twig_Node_Module $node, array $usedFilters, array $u protected function compileDisplayBody(Twig_Compiler $compiler) { - if (null === $this->getNode('parent')) { - $compiler->write("\$this->checkSecurity();\n"); - } + $compiler->write("\$this->checkSecurity();\n"); parent::compileDisplayBody($compiler); } @@ -45,7 +44,7 @@ protected function compileDisplayFooter(Twig_Compiler $compiler) parent::compileDisplayFooter($compiler); $compiler - ->write("protected function checkSecurity() {\n") + ->write("protected function checkSecurity()\n", "{\n") ->indent() ->write("\$this->env->getExtension('sandbox')->checkSecurity(\n") ->indent() @@ -54,16 +53,6 @@ protected function compileDisplayFooter(Twig_Compiler $compiler) ->write(!$this->usedFunctions ? "array()\n" : "array('".implode('\', \'', $this->usedFunctions)."')\n") ->outdent() ->write(");\n") - ; - - if (null !== $this->getNode('parent')) { - $compiler - ->raw("\n") - ->write("\$this->parent->checkSecurity();\n") - ; - } - - $compiler ->outdent() ->write("}\n\n") ; diff --git a/app/parsers/Twig/Node/SandboxedPrint.php b/app/parsers/Twig/Node/SandboxedPrint.php old mode 100755 new mode 100644 index 77730d8c..73dfaa96 --- a/app/parsers/Twig/Node/SandboxedPrint.php +++ b/app/parsers/Twig/Node/SandboxedPrint.php @@ -17,8 +17,7 @@ * and if the sandbox is enabled, we need to check that the __toString() * method is allowed if 'article' is an object. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_SandboxedPrint extends Twig_Node_Print { diff --git a/app/parsers/Twig/Node/Set.php b/app/parsers/Twig/Node/Set.php old mode 100755 new mode 100644 index 9913664f..4c9c16ce --- a/app/parsers/Twig/Node/Set.php +++ b/app/parsers/Twig/Node/Set.php @@ -12,8 +12,7 @@ /** * Represents a set node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Set extends Twig_Node { @@ -67,7 +66,7 @@ public function compile(Twig_Compiler $compiler) $compiler->subcompile($this->getNode('names'), false); if ($this->getAttribute('capture')) { - $compiler->raw(" = new Twig_Markup(ob_get_clean())"); + $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset())"); } } @@ -87,9 +86,9 @@ public function compile(Twig_Compiler $compiler) } else { if ($this->getAttribute('safe')) { $compiler - ->raw("new Twig_Markup(") + ->raw("('' === \$tmp = ") ->subcompile($this->getNode('values')) - ->raw(")") + ->raw(") ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset())") ; } else { $compiler->subcompile($this->getNode('values')); diff --git a/app/parsers/Twig/Node/SetTemp.php b/app/parsers/Twig/Node/SetTemp.php old mode 100755 new mode 100644 diff --git a/app/parsers/Twig/Node/Spaceless.php b/app/parsers/Twig/Node/Spaceless.php old mode 100755 new mode 100644 index 46013466..7555fa0f --- a/app/parsers/Twig/Node/Spaceless.php +++ b/app/parsers/Twig/Node/Spaceless.php @@ -14,8 +14,7 @@ * * It removes spaces between HTML tags. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Spaceless extends Twig_Node { diff --git a/app/parsers/Twig/Node/Text.php b/app/parsers/Twig/Node/Text.php old mode 100755 new mode 100644 index 0c1c0928..21bdcea1 --- a/app/parsers/Twig/Node/Text.php +++ b/app/parsers/Twig/Node/Text.php @@ -13,8 +13,7 @@ /** * Represents a text node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Node_Text extends Twig_Node implements Twig_NodeOutputInterface { diff --git a/app/parsers/Twig/NodeInterface.php b/app/parsers/Twig/NodeInterface.php old mode 100755 new mode 100644 index 29a84b03..f0ef7258 --- a/app/parsers/Twig/NodeInterface.php +++ b/app/parsers/Twig/NodeInterface.php @@ -12,8 +12,8 @@ /** * Represents a node in the AST. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_NodeInterface extends Countable, IteratorAggregate { @@ -22,9 +22,9 @@ interface Twig_NodeInterface extends Countable, IteratorAggregate * * @param Twig_Compiler A Twig_Compiler instance */ - function compile(Twig_Compiler $compiler); + public function compile(Twig_Compiler $compiler); - function getLine(); + public function getLine(); - function getNodeTag(); + public function getNodeTag(); } diff --git a/app/parsers/Twig/NodeOutputInterface.php b/app/parsers/Twig/NodeOutputInterface.php old mode 100755 new mode 100644 index 71839569..22172c09 --- a/app/parsers/Twig/NodeOutputInterface.php +++ b/app/parsers/Twig/NodeOutputInterface.php @@ -12,8 +12,7 @@ /** * Represents a displayable node in the AST. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_NodeOutputInterface { diff --git a/app/parsers/Twig/NodeTraverser.php b/app/parsers/Twig/NodeTraverser.php old mode 100755 new mode 100644 index 1e82b032..28cba1ad --- a/app/parsers/Twig/NodeTraverser.php +++ b/app/parsers/Twig/NodeTraverser.php @@ -14,8 +14,7 @@ * * It visits all nodes and their children and call the given visitor for each. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_NodeTraverser { diff --git a/app/parsers/Twig/NodeVisitor/Escaper.php b/app/parsers/Twig/NodeVisitor/Escaper.php old mode 100755 new mode 100644 index d848f808..cc4b3d71 --- a/app/parsers/Twig/NodeVisitor/Escaper.php +++ b/app/parsers/Twig/NodeVisitor/Escaper.php @@ -12,18 +12,18 @@ /** * Twig_NodeVisitor_Escaper implements output escaping. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface { protected $statusStack = array(); protected $blocks = array(); - protected $safeAnalysis; protected $traverser; + protected $defaultStrategy = false; + protected $safeVars = array(); - function __construct() + public function __construct() { $this->safeAnalysis = new Twig_NodeVisitor_SafeAnalysis(); } @@ -34,14 +34,21 @@ function __construct() * @param Twig_NodeInterface $node The node to visit * @param Twig_Environment $env The Twig environment instance * - * @param Twig_NodeInterface The modified node + * @return Twig_NodeInterface The modified node */ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) { - if ($node instanceof Twig_Node_AutoEscape) { + if ($node instanceof Twig_Node_Module) { + if ($env->hasExtension('escaper') && $defaultStrategy = $env->getExtension('escaper')->getDefaultStrategy($node->getAttribute('filename'))) { + $this->defaultStrategy = $defaultStrategy; + } + $this->safeVars = array(); + } elseif ($node instanceof Twig_Node_AutoEscape) { $this->statusStack[] = $node->getAttribute('value'); } elseif ($node instanceof Twig_Node_Block) { $this->statusStack[] = isset($this->blocks[$node->getAttribute('name')]) ? $this->blocks[$node->getAttribute('name')] : $this->needEscaping($env); + } elseif ($node instanceof Twig_Node_Import) { + $this->safeVars[] = $node->getNode('var')->getAttribute('name'); } return $node; @@ -53,11 +60,14 @@ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) * @param Twig_NodeInterface $node The node to visit * @param Twig_Environment $env The Twig environment instance * - * @param Twig_NodeInterface The modified node + * @return Twig_NodeInterface The modified node */ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) { - if ($node instanceof Twig_Node_Expression_Filter) { + if ($node instanceof Twig_Node_Module) { + $this->defaultStrategy = false; + $this->safeVars = array(); + } elseif ($node instanceof Twig_Node_Expression_Filter) { return $this->preEscapeFilterNode($node, $env); } elseif ($node instanceof Twig_Node_Print) { return $this->escapePrintNode($node, $env, $this->needEscaping($env)); @@ -96,22 +106,18 @@ protected function preEscapeFilterNode(Twig_Node_Expression_Filter $filter, Twig { $name = $filter->getNode('filter')->getAttribute('value'); - if (false !== $f = $env->getFilter($name)) { - $type = $f->getPreEscape(); - if (null === $type) { - return $filter; - } - - $node = $filter->getNode('node'); - if ($this->isSafeFor($type, $node, $env)) { - return $filter; - } - - $filter->setNode('node', $this->getEscaperFilter($type, $node)); + $type = $env->getFilter($name)->getPreEscape(); + if (null === $type) { + return $filter; + } + $node = $filter->getNode('node'); + if ($this->isSafeFor($type, $node, $env)) { return $filter; } + $filter->setNode('node', $this->getEscaperFilter($type, $node)); + return $filter; } @@ -123,6 +129,9 @@ protected function isSafeFor($type, Twig_NodeInterface $expression, $env) if (null === $this->traverser) { $this->traverser = new Twig_NodeTraverser($env, array($this->safeAnalysis)); } + + $this->safeAnalysis->setSafeVars($this->safeVars); + $this->traverser->traverse($expression); $safe = $this->safeAnalysis->getSafe($expression); } @@ -136,11 +145,7 @@ protected function needEscaping(Twig_Environment $env) return $this->statusStack[count($this->statusStack) - 1]; } - if ($env->hasExtension('escaper') && $env->getExtension('escaper')->isGlobal()) { - return 'html'; - } - - return false; + return $this->defaultStrategy ? $this->defaultStrategy : false; } protected function getEscaperFilter($type, Twig_NodeInterface $node) @@ -148,6 +153,7 @@ protected function getEscaperFilter($type, Twig_NodeInterface $node) $line = $node->getLine(); $name = new Twig_Node_Expression_Constant('escape', $line); $args = new Twig_Node(array(new Twig_Node_Expression_Constant((string) $type, $line), new Twig_Node_Expression_Constant(null, $line), new Twig_Node_Expression_Constant(true, $line))); + return new Twig_Node_Expression_Filter($node, $name, $args, $line); } diff --git a/app/parsers/Twig/NodeVisitor/Optimizer.php b/app/parsers/Twig/NodeVisitor/Optimizer.php old mode 100755 new mode 100644 index cbc61fc3..a254def7 --- a/app/parsers/Twig/NodeVisitor/Optimizer.php +++ b/app/parsers/Twig/NodeVisitor/Optimizer.php @@ -17,8 +17,7 @@ * You can configure which optimizations you want to activate via the * optimizer mode. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface { @@ -56,7 +55,7 @@ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) $this->enterOptimizeFor($node, $env); } - if (!version_compare(phpversion(), '5.4.0RC1', '>=') && self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) { + if (!version_compare(phpversion(), '5.4.0RC1', '>=') && self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) { if ($this->inABody) { if (!$node instanceof Twig_Node_Expression) { if (get_class($node) !== 'Twig_Node') { diff --git a/app/parsers/Twig/NodeVisitor/SafeAnalysis.php b/app/parsers/Twig/NodeVisitor/SafeAnalysis.php old mode 100755 new mode 100644 index 5961ba29..c4bbd812 --- a/app/parsers/Twig/NodeVisitor/SafeAnalysis.php +++ b/app/parsers/Twig/NodeVisitor/SafeAnalysis.php @@ -3,27 +3,33 @@ class Twig_NodeVisitor_SafeAnalysis implements Twig_NodeVisitorInterface { protected $data = array(); + protected $safeVars = array(); + + public function setSafeVars($safeVars) + { + $this->safeVars = $safeVars; + } public function getSafe(Twig_NodeInterface $node) { $hash = spl_object_hash($node); if (isset($this->data[$hash])) { - foreach($this->data[$hash] as $bucket) { + foreach ($this->data[$hash] as $bucket) { if ($bucket['key'] === $node) { return $bucket['value']; } } } - return null; } protected function setSafe(Twig_NodeInterface $node, array $safe) { $hash = spl_object_hash($node); if (isset($this->data[$hash])) { - foreach($this->data[$hash] as &$bucket) { + foreach ($this->data[$hash] as &$bucket) { if ($bucket['key'] === $node) { $bucket['value'] = $safe; + return; } } @@ -59,7 +65,11 @@ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) $name = $node->getNode('filter')->getAttribute('value'); $args = $node->getNode('arguments'); if (false !== $filter = $env->getFilter($name)) { - $this->setSafe($node, $filter->getSafe($args)); + $safe = $filter->getSafe($args); + if (null === $safe) { + $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); + } + $this->setSafe($node, $safe); } else { $this->setSafe($node, array()); } @@ -73,6 +83,20 @@ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) } else { $this->setSafe($node, array()); } + } elseif ($node instanceof Twig_Node_Expression_MethodCall) { + if ($node->getAttribute('safe')) { + $this->setSafe($node, array('all')); + } else { + $this->setSafe($node, array()); + } + } elseif ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name) { + $name = $node->getNode('node')->getAttribute('name'); + // attributes on template instances are safe + if ('_self' == $name || in_array($name, $this->safeVars)) { + $this->setSafe($node, array('all')); + } else { + $this->setSafe($node, array()); + } } else { $this->setSafe($node, array()); } diff --git a/app/parsers/Twig/NodeVisitor/Sandbox.php b/app/parsers/Twig/NodeVisitor/Sandbox.php old mode 100755 new mode 100644 index 356b661f..fb27045b --- a/app/parsers/Twig/NodeVisitor/Sandbox.php +++ b/app/parsers/Twig/NodeVisitor/Sandbox.php @@ -12,8 +12,7 @@ /** * Twig_NodeVisitor_Sandbox implements sandboxing. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_NodeVisitor_Sandbox implements Twig_NodeVisitorInterface { @@ -28,7 +27,7 @@ class Twig_NodeVisitor_Sandbox implements Twig_NodeVisitorInterface * @param Twig_NodeInterface $node The node to visit * @param Twig_Environment $env The Twig environment instance * - * @param Twig_NodeInterface The modified node + * @return Twig_NodeInterface The modified node */ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) { @@ -70,7 +69,7 @@ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) * @param Twig_NodeInterface $node The node to visit * @param Twig_Environment $env The Twig environment instance * - * @param Twig_NodeInterface The modified node + * @return Twig_NodeInterface The modified node */ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) { diff --git a/app/parsers/Twig/NodeVisitorInterface.php b/app/parsers/Twig/NodeVisitorInterface.php old mode 100755 new mode 100644 index e0123b5d..f33c13fc --- a/app/parsers/Twig/NodeVisitorInterface.php +++ b/app/parsers/Twig/NodeVisitorInterface.php @@ -12,8 +12,7 @@ /** * Twig_NodeVisitorInterface is the interface the all node visitor classes must implement. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_NodeVisitorInterface { @@ -25,7 +24,7 @@ interface Twig_NodeVisitorInterface * * @return Twig_NodeInterface The modified node */ - function enterNode(Twig_NodeInterface $node, Twig_Environment $env); + public function enterNode(Twig_NodeInterface $node, Twig_Environment $env); /** * Called after child nodes are visited. @@ -33,9 +32,9 @@ function enterNode(Twig_NodeInterface $node, Twig_Environment $env); * @param Twig_NodeInterface $node The node to visit * @param Twig_Environment $env The Twig environment instance * - * @return Twig_NodeInterface The modified node + * @return Twig_NodeInterface|false The modified node or false if the node must be removed */ - function leaveNode(Twig_NodeInterface $node, Twig_Environment $env); + public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env); /** * Returns the priority for this visitor. @@ -44,5 +43,5 @@ function leaveNode(Twig_NodeInterface $node, Twig_Environment $env); * * @return integer The priority level */ - function getPriority(); + public function getPriority(); } diff --git a/app/parsers/Twig/Parser.php b/app/parsers/Twig/Parser.php old mode 100755 new mode 100644 index 8fcf9c47..958e46b3 --- a/app/parsers/Twig/Parser.php +++ b/app/parsers/Twig/Parser.php @@ -13,11 +13,11 @@ /** * Default parser implementation. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Parser implements Twig_ParserInterface { + protected $stack = array(); protected $stream; protected $parent; protected $handlers; @@ -28,9 +28,9 @@ class Twig_Parser implements Twig_ParserInterface protected $macros; protected $env; protected $reservedMacroNames; - protected $importedFunctions; - protected $tmpVarCount; + protected $importedSymbols; protected $traits; + protected $embeddedTemplates = array(); /** * Constructor. @@ -49,26 +49,38 @@ public function getEnvironment() public function getVarName() { - return sprintf('__internal_%s_%d', substr($this->env->getTemplateClass($this->stream->getFilename()), strlen($this->env->getTemplateClassPrefix())), ++$this->tmpVarCount); + return sprintf('__internal_%s', hash('sha1', uniqid(mt_rand(), true), false)); + } + + public function getFilename() + { + return $this->stream->getFilename(); } /** * Converts a token stream to a node tree. * - * @param Twig_TokenStream $stream A token stream instance + * @param Twig_TokenStream $stream A token stream instance * * @return Twig_Node_Module A node tree */ - public function parse(Twig_TokenStream $stream) + public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = false) { - $this->tmpVarCount = 0; + // push all variables into the stack to keep the current state of the parser + $vars = get_object_vars($this); + unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser']); + $this->stack[] = $vars; // tag handlers - $this->handlers = $this->env->getTokenParsers(); - $this->handlers->setParser($this); + if (null === $this->handlers) { + $this->handlers = $this->env->getTokenParsers(); + $this->handlers->setParser($this); + } // node visitors - $this->visitors = $this->env->getNodeVisitors(); + if (null === $this->visitors) { + $this->visitors = $this->env->getNodeVisitors(); + } if (null === $this->expressionParser) { $this->expressionParser = new Twig_ExpressionParser($this, $this->env->getUnaryOperators(), $this->env->getBinaryOperators()); @@ -80,10 +92,11 @@ public function parse(Twig_TokenStream $stream) $this->macros = array(); $this->traits = array(); $this->blockStack = array(); - $this->importedFunctions = array(array()); + $this->importedSymbols = array(array()); + $this->embeddedTemplates = array(); try { - $body = $this->subparse(null); + $body = $this->subparse($test, $dropNeedle); if (null !== $this->parent) { if (null === $body = $this->filterBodyNodes($body)) { @@ -91,18 +104,29 @@ public function parse(Twig_TokenStream $stream) } } } catch (Twig_Error_Syntax $e) { - if (null === $e->getTemplateFile()) { - $e->setTemplateFile($this->stream->getFilename()); + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getFilename()); + } + + if (!$e->getTemplateLine()) { + $e->setTemplateLine($this->stream->getCurrent()->getLine()); } throw $e; } - $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->stream->getFilename()); + $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->embeddedTemplates, $this->getFilename()); $traverser = new Twig_NodeTraverser($this->env, $this->visitors); - return $traverser->traverse($node); + $node = $traverser->traverse($node); + + // restore previous stack so previous parse() call can resume working + foreach (array_pop($this->stack) as $key => $val) { + $this->$key = $val; + } + + return $node; } public function subparse($test, $dropNeedle = false) @@ -128,7 +152,7 @@ public function subparse($test, $dropNeedle = false) $token = $this->getCurrentToken(); if ($token->getType() !== Twig_Token::NAME_TYPE) { - throw new Twig_Error_Syntax('A block must start with a tag name', $token->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax('A block must start with a tag name', $token->getLine(), $this->getFilename()); } if (null !== $test && call_user_func($test, $token)) { @@ -146,10 +170,20 @@ public function subparse($test, $dropNeedle = false) $subparser = $this->handlers->getTokenParser($token->getValue()); if (null === $subparser) { if (null !== $test) { - throw new Twig_Error_Syntax(sprintf('Unexpected tag name "%s" (expecting closing tag for the "%s" tag defined near line %s)', $token->getValue(), $test[0]->getTag(), $lineno), $token->getLine(), $this->stream->getFilename()); + $error = sprintf('Unexpected tag name "%s"', $token->getValue()); + if (is_array($test) && isset($test[0]) && $test[0] instanceof Twig_TokenParserInterface) { + $error .= sprintf(' (expecting closing tag for the "%s" tag defined near line %s)', $test[0]->getTag(), $lineno); + } + + throw new Twig_Error_Syntax($error, $token->getLine(), $this->getFilename()); + } + + $message = sprintf('Unknown tag name "%s"', $token->getValue()); + if ($alternatives = $this->env->computeAlternatives($token->getValue(), array_keys($this->env->getTags()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); } - throw new Twig_Error_Syntax(sprintf('Unknown tag name "%s"', $token->getValue()), $token->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax($message, $token->getLine(), $this->getFilename()); } $this->stream->next(); @@ -161,7 +195,7 @@ public function subparse($test, $dropNeedle = false) break; default: - throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', -1, $this->stream->getFilename()); + throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', 0, $this->getFilename()); } } @@ -207,9 +241,14 @@ public function hasBlock($name) return isset($this->blocks[$name]); } + public function getBlock($name) + { + return $this->blocks[$name]; + } + public function setBlock($name, $value) { - $this->blocks[$name] = new Twig_Node_Body(array($value)); + $this->blocks[$name] = new Twig_Node_Body(array($value), array(), $value->getLine()); } public function hasMacro($name) @@ -228,7 +267,7 @@ public function setMacro($name, Twig_Node_Macro $node) } if (in_array($name, $this->reservedMacroNames)) { - throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword', $name), $node->getLine()); + throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword', $name), $node->getLine(), $this->getFilename()); } $this->macros[$name] = $node; @@ -244,33 +283,40 @@ public function hasTraits() return count($this->traits) > 0; } - public function addImportedFunction($alias, $name, Twig_Node_Expression $node) + public function embedTemplate(Twig_Node_Module $template) { - $this->importedFunctions[0][$alias] = array('name' => $name, 'node' => $node); + $template->setIndex(mt_rand()); + + $this->embeddedTemplates[] = $template; } - public function getImportedFunction($alias) + public function addImportedSymbol($type, $alias, $name = null, Twig_Node_Expression $node = null) { - foreach ($this->importedFunctions as $functions) { - if (isset($functions[$alias])) { - return $functions[$alias]; + $this->importedSymbols[0][$type][$alias] = array('name' => $name, 'node' => $node); + } + + public function getImportedSymbol($type, $alias) + { + foreach ($this->importedSymbols as $functions) { + if (isset($functions[$type][$alias])) { + return $functions[$type][$alias]; } } } public function isMainScope() { - return 1 === count($this->importedFunctions); + return 1 === count($this->importedSymbols); } public function pushLocalScope() { - array_unshift($this->importedFunctions, array()); + array_unshift($this->importedSymbols, array()); } public function popLocalScope() { - array_shift($this->importedFunctions); + array_shift($this->importedSymbols); } /** @@ -322,10 +368,10 @@ protected function filterBodyNodes(Twig_NodeInterface $node) (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface) ) { if (false !== strpos((string) $node, chr(0xEF).chr(0xBB).chr(0xBF))) { - throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->stream->getFilename()); - } else { - throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->getFilename()); } + + throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->getFilename()); } // bypass "set" nodes as they "capture" the output diff --git a/app/parsers/Twig/ParserInterface.php b/app/parsers/Twig/ParserInterface.php old mode 100755 new mode 100644 index c7a34418..f0d79009 --- a/app/parsers/Twig/ParserInterface.php +++ b/app/parsers/Twig/ParserInterface.php @@ -12,17 +12,17 @@ /** * Interface implemented by parser classes. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_ParserInterface { /** * Converts a token stream to a node tree. * - * @param Twig_TokenStream $stream A token stream instance + * @param Twig_TokenStream $stream A token stream instance * * @return Twig_Node_Module A node tree */ - function parse(Twig_TokenStream $code); + public function parse(Twig_TokenStream $stream); } diff --git a/app/parsers/Twig/Sandbox/SecurityError.php b/app/parsers/Twig/Sandbox/SecurityError.php old mode 100755 new mode 100644 index debabb79..015bfaea --- a/app/parsers/Twig/Sandbox/SecurityError.php +++ b/app/parsers/Twig/Sandbox/SecurityError.php @@ -12,8 +12,7 @@ /** * Exception thrown when a security error occurs at runtime. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Sandbox_SecurityError extends Twig_Error { diff --git a/app/parsers/Twig/Sandbox/SecurityPolicy.php b/app/parsers/Twig/Sandbox/SecurityPolicy.php old mode 100755 new mode 100644 index ba912ef4..66ee2332 --- a/app/parsers/Twig/Sandbox/SecurityPolicy.php +++ b/app/parsers/Twig/Sandbox/SecurityPolicy.php @@ -12,8 +12,7 @@ /** * Represents a security policy which need to be enforced when sandbox mode is enabled. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Sandbox_SecurityPolicy implements Twig_Sandbox_SecurityPolicyInterface { diff --git a/app/parsers/Twig/Sandbox/SecurityPolicyInterface.php b/app/parsers/Twig/Sandbox/SecurityPolicyInterface.php old mode 100755 new mode 100644 index d5015aff..6ab48e3c --- a/app/parsers/Twig/Sandbox/SecurityPolicyInterface.php +++ b/app/parsers/Twig/Sandbox/SecurityPolicyInterface.php @@ -12,14 +12,13 @@ /** * Interfaces that all security policy classes must implements. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_Sandbox_SecurityPolicyInterface { - function checkSecurity($tags, $filters, $functions); + public function checkSecurity($tags, $filters, $functions); - function checkMethodAllowed($obj, $method); + public function checkMethodAllowed($obj, $method); - function checkPropertyAllowed($obj, $method); + public function checkPropertyAllowed($obj, $method); } diff --git a/app/parsers/Twig/SimpleFilter.php b/app/parsers/Twig/SimpleFilter.php new file mode 100644 index 00000000..d35c5633 --- /dev/null +++ b/app/parsers/Twig/SimpleFilter.php @@ -0,0 +1,94 @@ + + */ +class Twig_SimpleFilter +{ + protected $name; + protected $callable; + protected $options; + protected $arguments = array(); + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'is_safe' => null, + 'is_safe_callback' => null, + 'pre_escape' => null, + 'preserves_safety' => null, + 'node_class' => 'Twig_Node_Expression_Filter', + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $filterArgs) + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return call_user_func($this->options['is_safe_callback'], $filterArgs); + } + } + + public function getPreservesSafety() + { + return $this->options['preserves_safety']; + } + + public function getPreEscape() + { + return $this->options['pre_escape']; + } +} diff --git a/app/parsers/Twig/SimpleFunction.php b/app/parsers/Twig/SimpleFunction.php new file mode 100644 index 00000000..8ef6aca2 --- /dev/null +++ b/app/parsers/Twig/SimpleFunction.php @@ -0,0 +1,84 @@ + + */ +class Twig_SimpleFunction +{ + protected $name; + protected $callable; + protected $options; + protected $arguments = array(); + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'is_safe' => null, + 'is_safe_callback' => null, + 'node_class' => 'Twig_Node_Expression_Function', + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $functionArgs) + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return call_user_func($this->options['is_safe_callback'], $functionArgs); + } + + return array(); + } +} diff --git a/app/parsers/Twig/SimpleTest.php b/app/parsers/Twig/SimpleTest.php new file mode 100644 index 00000000..225459c9 --- /dev/null +++ b/app/parsers/Twig/SimpleTest.php @@ -0,0 +1,46 @@ + + */ +class Twig_SimpleTest +{ + protected $name; + protected $callable; + protected $options; + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'node_class' => 'Twig_Node_Expression_Test', + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } +} diff --git a/app/parsers/Twig/Template.php b/app/parsers/Twig/Template.php old mode 100755 new mode 100644 index ec54b084..26c00b4a --- a/app/parsers/Twig/Template.php +++ b/app/parsers/Twig/Template.php @@ -13,13 +13,13 @@ /** * Default base class for compiled templates. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ abstract class Twig_Template implements Twig_TemplateInterface { - static protected $cache = array(); + protected static $cache = array(); + protected $parent; protected $parents; protected $env; protected $blocks; @@ -62,6 +62,10 @@ public function getEnvironment() */ public function getParent(array $context) { + if (null !== $this->parent) { + return $this->parent; + } + $parent = $this->doGetParent($context); if (false === $parent) { return false; @@ -76,7 +80,15 @@ public function getParent(array $context) return $this->parents[$parent]; } - abstract protected function doGetParent(array $context); + protected function doGetParent(array $context) + { + return false; + } + + public function isTraitable() + { + return true; + } /** * Displays a parent block. @@ -90,6 +102,8 @@ abstract protected function doGetParent(array $context); */ public function displayParentBlock($name, array $context, array $blocks = array()) { + $name = (string) $name; + if (isset($this->traits[$name])) { $this->traits[$name][0]->displayBlock($name, $context, $blocks); } elseif (false !== $parent = $this->getParent($context)) { @@ -111,6 +125,8 @@ public function displayParentBlock($name, array $context, array $blocks = array( */ public function displayBlock($name, array $context, array $blocks = array()) { + $name = (string) $name; + if (isset($blocks[$name])) { $b = $blocks; unset($b[$name]); @@ -181,7 +197,7 @@ public function renderBlock($name, array $context, array $blocks = array()) */ public function hasBlock($name) { - return isset($this->blocks[$name]); + return isset($this->blocks[(string) $name]); } /** @@ -219,21 +235,7 @@ public function getBlocks() */ public function display(array $context, array $blocks = array()) { - // we don't use array_merge as the context being generally - // bigger than globals, this code is faster. - foreach ($this->env->getGlobals() as $key => $value) { - if (!array_key_exists($key, $context)) { - $context[$key] = $value; - } - } - - try { - $this->doDisplay($context, $blocks); - } catch (Twig_Error $e) { - throw $e; - } catch (Exception $e) { - throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, null, $e); - } + $this->displayWithErrorHandling($this->env->mergeGlobals($context), $blocks); } /** @@ -256,6 +258,28 @@ public function render(array $context) return ob_get_clean(); } + protected function displayWithErrorHandling(array $context, array $blocks = array()) + { + try { + $this->doDisplay($context, $blocks); + } catch (Twig_Error $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getTemplateName()); + } + + // this is mostly useful for Twig_Error_Loader exceptions + // see Twig_Error_Loader + if (false === $e->getTemplateLine()) { + $e->setTemplateLine(-1); + $e->guess(); + } + + throw $e; + } catch (Exception $e) { + throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, null, $e); + } + } + /** * Auto-generated method to display the template with the given context. * @@ -267,6 +291,14 @@ abstract protected function doDisplay(array $context, array $blocks = array()); /** * Returns a variable from the context. * + * This method is for internal use only and should never be called + * directly. + * + * This method should not be overridden in a sub-class as this is an + * implementation detail that has been introduced to optimize variable + * access for versions of PHP before 5.4. This is not a way to override + * the way to get a variable value. + * * @param array $context The context * @param string $item The variable to return from the context * @param Boolean $ignoreStrictCheck Whether to ignore the strict variable check or not @@ -275,14 +307,14 @@ abstract protected function doDisplay(array $context, array $blocks = array()); * * @throws Twig_Error_Runtime if the variable does not exist and Twig is running in strict mode */ - protected function getContext($context, $item, $ignoreStrictCheck = false) + final protected function getContext($context, $item, $ignoreStrictCheck = false) { if (!array_key_exists($item, $context)) { if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { return null; } - throw new Twig_Error_Runtime(sprintf('Variable "%s" does not exist', $item)); + throw new Twig_Error_Runtime(sprintf('Variable "%s" does not exist', $item), -1, $this->getTemplateName()); } return $context[$item]; @@ -304,25 +336,26 @@ protected function getContext($context, $item, $ignoreStrictCheck = false) */ protected function getAttribute($object, $item, array $arguments = array(), $type = Twig_TemplateInterface::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) { - $item = (string) $item; // Stacey-specific // If the attribute is a string path, then we need to pass it through the asset factory to create its page variables - if (is_string($object)) $object =& AssetFactory::get($object);; + if (is_string($object)) $object =& AssetFactory::get($object); // array if (Twig_TemplateInterface::METHOD_CALL !== $type) { - if ((is_array($object) && array_key_exists($item, $object)) - || ($object instanceof ArrayAccess && isset($object[$item])) + $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; + + if ((is_array($object) && array_key_exists($arrayItem, $object)) + || ($object instanceof ArrayAccess && isset($object[$arrayItem])) ) { if ($isDefinedTest) { return true; } - return $object[$item]; + return $object[$arrayItem]; } - if (Twig_TemplateInterface::ARRAY_CALL === $type) { + if (Twig_TemplateInterface::ARRAY_CALL === $type || !is_object($object)) { if ($isDefinedTest) { return false; } @@ -332,10 +365,13 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ } if (is_object($object)) { - throw new Twig_Error_Runtime(sprintf('Key "%s" in object (with ArrayAccess) of type "%s" does not exist', $item, get_class($object))); - // array + throw new Twig_Error_Runtime(sprintf('Key "%s" in object (with ArrayAccess) of type "%s" does not exist', $arrayItem, get_class($object)), -1, $this->getTemplateName()); + } elseif (is_array($object)) { + throw new Twig_Error_Runtime(sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))), -1, $this->getTemplateName()); + } elseif (Twig_TemplateInterface::ARRAY_CALL === $type) { + throw new Twig_Error_Runtime(sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); } else { - throw new Twig_Error_Runtime(sprintf('Key "%s" for array with keys "%s" does not exist', $item, implode(', ', array_keys($object)))); + throw new Twig_Error_Runtime(sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); } } } @@ -349,22 +385,14 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ return null; } - throw new Twig_Error_Runtime(sprintf('Item "%s" for "%s" does not exist', $item, is_array($object) ? 'Array' : $object)); + throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); } $class = get_class($object); // object property if (Twig_TemplateInterface::METHOD_CALL !== $type) { - /* apparently, this is not needed as this is already covered by the array_key_exists() call below - if (!isset(self::$cache[$class]['properties'])) { - foreach (get_object_vars($object) as $k => $v) { - self::$cache[$class]['properties'][$k] = true; - } - } - */ - - if (isset($object->$item) || array_key_exists($item, $object)) { + if (isset($object->$item) || array_key_exists((string) $item, $object)) { if ($isDefinedTest) { return true; } @@ -384,13 +412,13 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ $lcItem = strtolower($item); if (isset(self::$cache[$class]['methods'][$lcItem])) { - $method = $item; + $method = (string) $item; } elseif (isset(self::$cache[$class]['methods']['get'.$lcItem])) { $method = 'get'.$item; } elseif (isset(self::$cache[$class]['methods']['is'.$lcItem])) { $method = 'is'.$item; } elseif (isset(self::$cache[$class]['methods']['__call'])) { - $method = $item; + $method = (string) $item; } else { if ($isDefinedTest) { return false; @@ -400,7 +428,7 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ return null; } - throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object))); + throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object)), -1, $this->getTemplateName()); } if ($isDefinedTest) { @@ -413,8 +441,10 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ $ret = call_user_func_array(array($object, $method), $arguments); + // useful when calling a template method from a template + // this is not supported but unfortunately heavily used in the Symfony profiler if ($object instanceof Twig_TemplateInterface) { - return new Twig_Markup($ret); + return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset()); } return $ret; @@ -423,7 +453,7 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ /** * This method is only useful when testing Twig. Do not use it. */ - static public function clearCache() + public static function clearCache() { self::$cache = array(); } diff --git a/app/parsers/Twig/TemplateInterface.php b/app/parsers/Twig/TemplateInterface.php old mode 100755 new mode 100644 index 08da1163..879f503e --- a/app/parsers/Twig/TemplateInterface.php +++ b/app/parsers/Twig/TemplateInterface.php @@ -12,8 +12,8 @@ /** * Interface implemented by all compiled templates. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_TemplateInterface { @@ -28,7 +28,7 @@ interface Twig_TemplateInterface * * @return string The rendered template */ - function render(array $context); + public function render(array $context); /** * Displays the template with the given context. @@ -36,12 +36,12 @@ function render(array $context); * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template */ - function display(array $context, array $blocks = array()); + public function display(array $context, array $blocks = array()); /** * Returns the bound environment for this template. * * @return Twig_Environment The current environment */ - function getEnvironment(); + public function getEnvironment(); } diff --git a/app/parsers/Twig/Test.php b/app/parsers/Twig/Test.php new file mode 100644 index 00000000..3baff885 --- /dev/null +++ b/app/parsers/Twig/Test.php @@ -0,0 +1,34 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface +{ + protected $options; + protected $arguments = array(); + + public function __construct(array $options = array()) + { + $this->options = array_merge(array( + 'callable' => null, + ), $options); + } + + public function getCallable() + { + return $this->options['callable']; + } +} diff --git a/app/parsers/Twig/Test/Function.php b/app/parsers/Twig/Test/Function.php old mode 100755 new mode 100644 index 1240a0f1..4be6b9b9 --- a/app/parsers/Twig/Test/Function.php +++ b/app/parsers/Twig/Test/Function.php @@ -12,15 +12,19 @@ /** * Represents a function template test. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -class Twig_Test_Function implements Twig_TestInterface +class Twig_Test_Function extends Twig_Test { protected $function; - public function __construct($function) + public function __construct($function, array $options = array()) { + $options['callable'] = $function; + + parent::__construct($options); + $this->function = $function; } diff --git a/app/parsers/Twig/Test/IntegrationTestCase.php b/app/parsers/Twig/Test/IntegrationTestCase.php new file mode 100644 index 00000000..724f0941 --- /dev/null +++ b/app/parsers/Twig/Test/IntegrationTestCase.php @@ -0,0 +1,154 @@ + + * @author Karma Dordrak + */ +abstract class Twig_Test_IntegrationTestCase extends PHPUnit_Framework_TestCase +{ + abstract protected function getExtensions(); + abstract protected function getFixturesDir(); + + /** + * @dataProvider getTests + */ + public function testIntegration($file, $message, $condition, $templates, $exception, $outputs) + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); + } + + public function getTests() + { + $fixturesDir = realpath($this->getFixturesDir()); + $tests = array(); + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + continue; + } + + $test = file_get_contents($file->getRealpath()); + + if (preg_match('/ + --TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $templates = $this->parseTemplates($match[3]); + $exception = $match[5]; + $outputs = array(array(null, $match[4], null, '')); + } elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $templates = $this->parseTemplates($match[3]); + $exception = false; + preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, PREG_SET_ORDER); + } else { + throw new InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + } + + $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs); + } + + return $tests; + } + + protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs) + { + if ($condition) { + eval('$ret = '.$condition.';'); + if (!$ret) { + $this->markTestSkipped($condition); + } + } + + $loader = new Twig_Loader_Array($templates); + + foreach ($outputs as $match) { + $config = array_merge(array( + 'cache' => false, + 'strict_variables' => true, + ), $match[2] ? eval($match[2].';') : array()); + $twig = new Twig_Environment($loader, $config); + $twig->addGlobal('global', 'global'); + foreach ($this->getExtensions() as $extension) { + $twig->addExtension($extension); + } + + try { + $template = $twig->loadTemplate('index.twig'); + } catch (Exception $e) { + if (false !== $exception) { + $this->assertEquals(trim($exception), trim(sprintf('%s: %s', get_class($e), $e->getMessage()))); + + return; + } + + if ($e instanceof Twig_Error_Syntax) { + $e->setTemplateFile($file); + + throw $e; + } + + throw new Twig_Error(sprintf('%s: %s', get_class($e), $e->getMessage()), -1, $file, $e); + } + + try { + $output = trim($template->render(eval($match[1].';')), "\n "); + } catch (Exception $e) { + if (false !== $exception) { + $this->assertEquals(trim($exception), trim(sprintf('%s: %s', get_class($e), $e->getMessage()))); + + return; + } + + if ($e instanceof Twig_Error_Syntax) { + $e->setTemplateFile($file); + } else { + $e = new Twig_Error(sprintf('%s: %s', get_class($e), $e->getMessage()), -1, $file, $e); + } + + $output = trim(sprintf('%s: %s', get_class($e), $e->getMessage())); + } + + if (false !== $exception) { + list($class, ) = explode(':', $exception); + $this->assertThat(NULL, new PHPUnit_Framework_Constraint_Exception($class)); + } + + $expected = trim($match[3], "\n "); + + if ($expected != $output) { + echo 'Compiled template that failed:'; + + foreach (array_keys($templates) as $name) { + echo "Template: $name\n"; + $source = $loader->getSource($name); + echo $twig->compile($twig->parse($twig->tokenize($source, $name))); + } + } + $this->assertEquals($expected, $output, $message.' (in '.$file.')'); + } + } + + protected static function parseTemplates($test) + { + $templates = array(); + preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $templates[($match[1] ? $match[1] : 'index.twig')] = $match[2]; + } + + return $templates; + } +} diff --git a/app/parsers/Twig/Test/Method.php b/app/parsers/Twig/Test/Method.php old mode 100755 new mode 100644 index a3b39483..17c6c041 --- a/app/parsers/Twig/Test/Method.php +++ b/app/parsers/Twig/Test/Method.php @@ -12,15 +12,20 @@ /** * Represents a method template test. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -class Twig_Test_Method implements Twig_TestInterface +class Twig_Test_Method extends Twig_Test { - protected $extension, $method; + protected $extension; + protected $method; - public function __construct(Twig_ExtensionInterface $extension, $method) + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + + parent::__construct($options); + $this->extension = $extension; $this->method = $method; } diff --git a/app/parsers/Twig/Test/Node.php b/app/parsers/Twig/Test/Node.php old mode 100755 new mode 100644 index 47a978e3..c832a57b --- a/app/parsers/Twig/Test/Node.php +++ b/app/parsers/Twig/Test/Node.php @@ -12,15 +12,17 @@ /** * Represents a template test as a Node. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ -class Twig_Test_Node implements Twig_TestInterface +class Twig_Test_Node extends Twig_Test { protected $class; - public function __construct($class) + public function __construct($class, array $options = array()) { + parent::__construct($options); + $this->class = $class; } diff --git a/app/parsers/Twig/Test/NodeTestCase.php b/app/parsers/Twig/Test/NodeTestCase.php new file mode 100644 index 00000000..b15c85ff --- /dev/null +++ b/app/parsers/Twig/Test/NodeTestCase.php @@ -0,0 +1,58 @@ +assertNodeCompilation($source, $node, $environment); + } + + public function assertNodeCompilation($source, Twig_Node $node, Twig_Environment $environment = null) + { + $compiler = $this->getCompiler($environment); + $compiler->compile($node); + + $this->assertEquals($source, trim($compiler->getSource())); + } + + protected function getCompiler(Twig_Environment $environment = null) + { + return new Twig_Compiler(null === $environment ? $this->getEnvironment() : $environment); + } + + protected function getEnvironment() + { + return new Twig_Environment(); + } + + protected function getVariableGetter($name) + { + if (version_compare(phpversion(), '5.4.0RC1', '>=')) { + return sprintf('(isset($context["%s"]) ? $context["%s"] : null)', $name, $name); + } + + return sprintf('$this->getContext($context, "%s")', $name); + } + + protected function getAttributeGetter() + { + if (function_exists('twig_template_get_attributes')) { + return 'twig_template_get_attributes($this, '; + } + + return '$this->getAttribute('; + } +} diff --git a/app/parsers/Twig/TestCallableInterface.php b/app/parsers/Twig/TestCallableInterface.php new file mode 100644 index 00000000..0db43682 --- /dev/null +++ b/app/parsers/Twig/TestCallableInterface.php @@ -0,0 +1,21 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_TestCallableInterface +{ + public function getCallable(); +} diff --git a/app/parsers/Twig/TestInterface.php b/app/parsers/Twig/TestInterface.php old mode 100755 new mode 100644 index c2ff7258..30d8a2c4 --- a/app/parsers/Twig/TestInterface.php +++ b/app/parsers/Twig/TestInterface.php @@ -12,8 +12,8 @@ /** * Represents a template test. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_TestInterface { @@ -22,5 +22,5 @@ interface Twig_TestInterface * * @return string The PHP code for the test */ - function compile(); + public function compile(); } diff --git a/app/parsers/Twig/Token.php b/app/parsers/Twig/Token.php old mode 100755 new mode 100644 index 79a10030..bbca90db --- a/app/parsers/Twig/Token.php +++ b/app/parsers/Twig/Token.php @@ -13,8 +13,7 @@ /** * Represents a Token. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_Token { @@ -22,17 +21,19 @@ class Twig_Token protected $type; protected $lineno; - const EOF_TYPE = -1; - const TEXT_TYPE = 0; - const BLOCK_START_TYPE = 1; - const VAR_START_TYPE = 2; - const BLOCK_END_TYPE = 3; - const VAR_END_TYPE = 4; - const NAME_TYPE = 5; - const NUMBER_TYPE = 6; - const STRING_TYPE = 7; - const OPERATOR_TYPE = 8; - const PUNCTUATION_TYPE = 9; + const EOF_TYPE = -1; + const TEXT_TYPE = 0; + const BLOCK_START_TYPE = 1; + const VAR_START_TYPE = 2; + const BLOCK_END_TYPE = 3; + const VAR_END_TYPE = 4; + const NAME_TYPE = 5; + const NUMBER_TYPE = 6; + const STRING_TYPE = 7; + const OPERATOR_TYPE = 8; + const PUNCTUATION_TYPE = 9; + const INTERPOLATION_START_TYPE = 10; + const INTERPOLATION_END_TYPE = 11; /** * Constructor. @@ -120,10 +121,11 @@ public function getValue() * * @param integer $type The type as an integer * @param Boolean $short Whether to return a short representation or not + * @param integer $line The code line * * @return string The string representation */ - static public function typeToString($type, $short = false, $line = -1) + public static function typeToString($type, $short = false, $line = -1) { switch ($type) { case self::EOF_TYPE: @@ -159,8 +161,14 @@ static public function typeToString($type, $short = false, $line = -1) case self::PUNCTUATION_TYPE: $name = 'PUNCTUATION_TYPE'; break; + case self::INTERPOLATION_START_TYPE: + $name = 'INTERPOLATION_START_TYPE'; + break; + case self::INTERPOLATION_END_TYPE: + $name = 'INTERPOLATION_END_TYPE'; + break; default: - throw new Twig_Error_Syntax(sprintf('Token of type "%s" does not exist.', $type), $line); + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); } return $short ? $name : 'Twig_Token::'.$name; @@ -169,12 +177,12 @@ static public function typeToString($type, $short = false, $line = -1) /** * Returns the english representation of a given type. * - * @param integer $type The type as an integer - * @param Boolean $short Whether to return a short representation or not + * @param integer $type The type as an integer + * @param integer $line The code line * * @return string The string representation */ - static public function typeToEnglish($type, $line = -1) + public static function typeToEnglish($type, $line = -1) { switch ($type) { case self::EOF_TYPE: @@ -199,8 +207,12 @@ static public function typeToEnglish($type, $line = -1) return 'operator'; case self::PUNCTUATION_TYPE: return 'punctuation'; + case self::INTERPOLATION_START_TYPE: + return 'begin of string interpolation'; + case self::INTERPOLATION_END_TYPE: + return 'end of string interpolation'; default: - throw new Twig_Error_Syntax(sprintf('Token of type "%s" does not exist.', $type), $line); + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); } } } diff --git a/app/parsers/Twig/TokenParser.php b/app/parsers/Twig/TokenParser.php old mode 100755 new mode 100644 index 6c9e6935..decebd5e --- a/app/parsers/Twig/TokenParser.php +++ b/app/parsers/Twig/TokenParser.php @@ -12,11 +12,13 @@ /** * Base class for all token parsers. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ abstract class Twig_TokenParser implements Twig_TokenParserInterface { + /** + * @var Twig_Parser + */ protected $parser; /** diff --git a/app/parsers/Twig/TokenParser/AutoEscape.php b/app/parsers/Twig/TokenParser/AutoEscape.php old mode 100755 new mode 100644 index 3b4b96e3..27560288 --- a/app/parsers/Twig/TokenParser/AutoEscape.php +++ b/app/parsers/Twig/TokenParser/AutoEscape.php @@ -39,23 +39,35 @@ class Twig_TokenParser_AutoEscape extends Twig_TokenParser public function parse(Twig_Token $token) { $lineno = $token->getLine(); - $value = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue(); - if (!in_array($value, array('true', 'false'))) { - throw new Twig_Error_Syntax("Autoescape value must be 'true' or 'false'", $lineno); - } - $value = 'true' === $value ? 'html' : false; + $stream = $this->parser->getStream(); + + if ($stream->test(Twig_Token::BLOCK_END_TYPE)) { + $value = 'html'; + } else { + $expr = $this->parser->getExpressionParser()->parseExpression(); + if (!$expr instanceof Twig_Node_Expression_Constant) { + throw new Twig_Error_Syntax('An escaping strategy must be a string or a Boolean.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + $value = $expr->getAttribute('value'); - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE)) { - if (false === $value) { - throw new Twig_Error_Syntax('Unexpected escaping strategy as you set autoescaping to false.', $lineno); + $compat = true === $value || false === $value; + + if (true === $value) { + $value = 'html'; } - $value = $this->parser->getStream()->next()->getValue(); + if ($compat && $stream->test(Twig_Token::NAME_TYPE)) { + if (false === $value) { + throw new Twig_Error_Syntax('Unexpected escaping strategy as you set autoescaping to false.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + + $value = $stream->next()->getValue(); + } } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); return new Twig_Node_AutoEscape($value, $body, $lineno, $this->getTag()); } @@ -68,7 +80,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Block.php b/app/parsers/Twig/TokenParser/Block.php old mode 100755 new mode 100644 index b31f7ed6..a2e017f3 --- a/app/parsers/Twig/TokenParser/Block.php +++ b/app/parsers/Twig/TokenParser/Block.php @@ -35,8 +35,9 @@ public function parse(Twig_Token $token) $stream = $this->parser->getStream(); $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); if ($this->parser->hasBlock($name)) { - throw new Twig_Error_Syntax("The block '$name' has already been defined", $lineno); + throw new Twig_Error_Syntax(sprintf("The block '$name' has already been defined line %d", $this->parser->getBlock($name)->getLine()), $stream->getCurrent()->getLine(), $stream->getFilename()); } + $this->parser->setBlock($name, $block = new Twig_Node_Block($name, new Twig_Node(array()), $lineno)); $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); @@ -48,7 +49,7 @@ public function parse(Twig_Token $token) $value = $stream->next()->getValue(); if ($value != $name) { - throw new Twig_Error_Syntax(sprintf("Expected endblock for block '$name' (but %s given)", $value), $lineno); + throw new Twig_Error_Syntax(sprintf("Expected endblock for block '$name' (but %s given)", $value), $stream->getCurrent()->getLine(), $stream->getFilename()); } } } else { @@ -58,8 +59,7 @@ public function parse(Twig_Token $token) } $stream->expect(Twig_Token::BLOCK_END_TYPE); - $block = new Twig_Node_Block($name, $body, $lineno); - $this->parser->setBlock($name, $block); + $block->setNode('body', $body); $this->parser->popBlockStack(); $this->parser->popLocalScope(); @@ -74,7 +74,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Do.php b/app/parsers/Twig/TokenParser/Do.php new file mode 100644 index 00000000..f50939dd --- /dev/null +++ b/app/parsers/Twig/TokenParser/Do.php @@ -0,0 +1,42 @@ +parser->getExpressionParser()->parseExpression(); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Do($expr, $token->getLine(), $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'do'; + } +} diff --git a/app/parsers/Twig/TokenParser/Embed.php b/app/parsers/Twig/TokenParser/Embed.php new file mode 100644 index 00000000..69cb5f35 --- /dev/null +++ b/app/parsers/Twig/TokenParser/Embed.php @@ -0,0 +1,66 @@ +parser->getStream(); + + $parent = $this->parser->getExpressionParser()->parseExpression(); + + list($variables, $only, $ignoreMissing) = $this->parseArguments(); + + // inject a fake parent to make the parent() function work + $stream->injectTokens(array( + new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', $token->getLine()), + new Twig_Token(Twig_Token::NAME_TYPE, 'extends', $token->getLine()), + new Twig_Token(Twig_Token::STRING_TYPE, '__parent__', $token->getLine()), + new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $token->getLine()), + )); + + $module = $this->parser->parse($stream, array($this, 'decideBlockEnd'), true); + + // override the parent with the correct one + $module->setNode('parent', $parent); + + $this->parser->embedTemplate($module); + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Embed($module->getAttribute('filename'), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endembed'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'embed'; + } +} diff --git a/app/parsers/Twig/TokenParser/Extends.php b/app/parsers/Twig/TokenParser/Extends.php old mode 100755 new mode 100644 index 67eacda0..f5ecee21 --- a/app/parsers/Twig/TokenParser/Extends.php +++ b/app/parsers/Twig/TokenParser/Extends.php @@ -29,23 +29,21 @@ class Twig_TokenParser_Extends extends Twig_TokenParser public function parse(Twig_Token $token) { if (!$this->parser->isMainScope()) { - throw new Twig_Error_Syntax('Cannot extend from a block', $token->getLine()); + throw new Twig_Error_Syntax('Cannot extend from a block', $token->getLine(), $this->parser->getFilename()); } if (null !== $this->parser->getParent()) { - throw new Twig_Error_Syntax('Multiple extends tags are forbidden', $token->getLine()); + throw new Twig_Error_Syntax('Multiple extends tags are forbidden', $token->getLine(), $this->parser->getFilename()); } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); - - return null; } /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Filter.php b/app/parsers/Twig/TokenParser/Filter.php old mode 100755 new mode 100644 index 0969ab1e..2b97475a --- a/app/parsers/Twig/TokenParser/Filter.php +++ b/app/parsers/Twig/TokenParser/Filter.php @@ -52,7 +52,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Flush.php b/app/parsers/Twig/TokenParser/Flush.php new file mode 100644 index 00000000..4e15e785 --- /dev/null +++ b/app/parsers/Twig/TokenParser/Flush.php @@ -0,0 +1,42 @@ +parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Flush($token->getLine(), $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'flush'; + } +} diff --git a/app/parsers/Twig/TokenParser/For.php b/app/parsers/Twig/TokenParser/For.php old mode 100755 new mode 100644 index 39755a49..98a6d079 --- a/app/parsers/Twig/TokenParser/For.php +++ b/app/parsers/Twig/TokenParser/For.php @@ -33,25 +33,26 @@ class Twig_TokenParser_For extends Twig_TokenParser public function parse(Twig_Token $token) { $lineno = $token->getLine(); + $stream = $this->parser->getStream(); $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); - $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, 'in'); + $stream->expect(Twig_Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); $ifexpr = null; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'if')) { - $this->parser->getStream()->next(); + if ($stream->test(Twig_Token::NAME_TYPE, 'if')) { + $stream->next(); $ifexpr = $this->parser->getExpressionParser()->parseExpression(); } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideForFork')); - if ($this->parser->getStream()->next()->getValue() == 'else') { - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + if ($stream->next()->getValue() == 'else') { + $stream->expect(Twig_Token::BLOCK_END_TYPE); $else = $this->parser->subparse(array($this, 'decideForEnd'), true); } else { $else = null; } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); if (count($targets) > 1) { $keyTarget = $targets->getNode(0); @@ -64,6 +65,11 @@ public function parse(Twig_Token $token) $valueTarget = new Twig_Node_Expression_AssignName($valueTarget->getAttribute('name'), $valueTarget->getLine()); } + if ($ifexpr) { + $this->checkLoopUsageCondition($stream, $ifexpr); + $this->checkLoopUsageBody($stream, $body); + } + return new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, $lineno, $this->getTag()); } @@ -77,10 +83,51 @@ public function decideForEnd(Twig_Token $token) return $token->test('endfor'); } + // the loop variable cannot be used in the condition + protected function checkLoopUsageCondition(Twig_TokenStream $stream, Twig_NodeInterface $node) + { + if ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name && 'loop' == $node->getNode('node')->getAttribute('name')) { + throw new Twig_Error_Syntax('The "loop" variable cannot be used in a looping condition', $node->getLine(), $stream->getFilename()); + } + + foreach ($node as $n) { + if (!$n) { + continue; + } + + $this->checkLoopUsageCondition($stream, $n); + } + } + + // check usage of non-defined loop-items + // it does not catch all problems (for instance when a for is included into another or when the variable is used in an include) + protected function checkLoopUsageBody(Twig_TokenStream $stream, Twig_NodeInterface $node) + { + if ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name && 'loop' == $node->getNode('node')->getAttribute('name')) { + $attribute = $node->getNode('attribute'); + if ($attribute instanceof Twig_Node_Expression_Constant && in_array($attribute->getAttribute('value'), array('length', 'revindex0', 'revindex', 'last'))) { + throw new Twig_Error_Syntax(sprintf('The "loop.%s" variable is not defined when looping with a condition', $attribute->getAttribute('value')), $node->getLine(), $stream->getFilename()); + } + } + + // should check for parent.loop.XXX usage + if ($node instanceof Twig_Node_For) { + return; + } + + foreach ($node as $n) { + if (!$n) { + continue; + } + + $this->checkLoopUsageBody($stream, $n); + } + } + /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/From.php b/app/parsers/Twig/TokenParser/From.php old mode 100755 new mode 100644 index 87aceb4d..a54054db --- a/app/parsers/Twig/TokenParser/From.php +++ b/app/parsers/Twig/TokenParser/From.php @@ -55,8 +55,8 @@ public function parse(Twig_Token $token) $node = new Twig_Node_Import($macro, new Twig_Node_Expression_AssignName($this->parser->getVarName(), $token->getLine()), $token->getLine(), $this->getTag()); - foreach($targets as $name => $alias) { - $this->parser->addImportedFunction($alias, $name, $node->getNode('var')); + foreach ($targets as $name => $alias) { + $this->parser->addImportedSymbol('function', $alias, 'get'.$name, $node->getNode('var')); } return $node; @@ -65,7 +65,7 @@ public function parse(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/If.php b/app/parsers/Twig/TokenParser/If.php old mode 100755 new mode 100644 index 65a1a8b2..3d7d1f51 --- a/app/parsers/Twig/TokenParser/If.php +++ b/app/parsers/Twig/TokenParser/If.php @@ -36,22 +36,23 @@ public function parse(Twig_Token $token) { $lineno = $token->getLine(); $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideIfFork')); $tests = array($expr, $body); $else = null; $end = false; while (!$end) { - switch ($this->parser->getStream()->next()->getValue()) { + switch ($stream->next()->getValue()) { case 'else': - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $else = $this->parser->subparse(array($this, 'decideIfEnd')); break; case 'elseif': $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideIfFork')); $tests[] = $expr; $tests[] = $body; @@ -62,11 +63,11 @@ public function parse(Twig_Token $token) break; default: - throw new Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d)', $lineno), -1); + throw new Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d)', $lineno), $stream->getCurrent()->getLine(), $stream->getFilename()); } } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); return new Twig_Node_If(new Twig_Node($tests), $else, $lineno, $this->getTag()); } @@ -84,7 +85,7 @@ public function decideIfEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Import.php b/app/parsers/Twig/TokenParser/Import.php old mode 100755 new mode 100644 index d0a88cde..e7050c70 --- a/app/parsers/Twig/TokenParser/Import.php +++ b/app/parsers/Twig/TokenParser/Import.php @@ -32,13 +32,15 @@ public function parse(Twig_Token $token) $var = new Twig_Node_Expression_AssignName($this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue(), $token->getLine()); $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $this->parser->addImportedSymbol('template', $var->getAttribute('name')); + return new Twig_Node_Import($macro, $var, $token->getLine(), $this->getTag()); } /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Include.php b/app/parsers/Twig/TokenParser/Include.php old mode 100755 new mode 100644 index 54154559..4a317868 --- a/app/parsers/Twig/TokenParser/Include.php +++ b/app/parsers/Twig/TokenParser/Include.php @@ -32,37 +32,46 @@ public function parse(Twig_Token $token) { $expr = $this->parser->getExpressionParser()->parseExpression(); + list($variables, $only, $ignoreMissing) = $this->parseArguments(); + + return new Twig_Node_Include($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + protected function parseArguments() + { + $stream = $this->parser->getStream(); + $ignoreMissing = false; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'ignore')) { - $this->parser->getStream()->next(); - $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, 'missing'); + if ($stream->test(Twig_Token::NAME_TYPE, 'ignore')) { + $stream->next(); + $stream->expect(Twig_Token::NAME_TYPE, 'missing'); $ignoreMissing = true; } $variables = null; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'with')) { - $this->parser->getStream()->next(); + if ($stream->test(Twig_Token::NAME_TYPE, 'with')) { + $stream->next(); $variables = $this->parser->getExpressionParser()->parseExpression(); } $only = false; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'only')) { - $this->parser->getStream()->next(); + if ($stream->test(Twig_Token::NAME_TYPE, 'only')) { + $stream->next(); $only = true; } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); - return new Twig_Node_Include($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return array($variables, $only, $ignoreMissing); } /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Macro.php b/app/parsers/Twig/TokenParser/Macro.php old mode 100755 new mode 100644 index 64f2ea5e..82b4fa6d --- a/app/parsers/Twig/TokenParser/Macro.php +++ b/app/parsers/Twig/TokenParser/Macro.php @@ -30,26 +30,25 @@ class Twig_TokenParser_Macro extends Twig_TokenParser public function parse(Twig_Token $token) { $lineno = $token->getLine(); - $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); - $arguments = $this->parser->getExpressionParser()->parseArguments(); + $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE)) { - $value = $this->parser->getStream()->next()->getValue(); + if ($stream->test(Twig_Token::NAME_TYPE)) { + $value = $stream->next()->getValue(); if ($value != $name) { - throw new Twig_Error_Syntax(sprintf("Expected endmacro for macro '$name' (but %s given)", $value), $lineno); + throw new Twig_Error_Syntax(sprintf("Expected endmacro for macro '$name' (but %s given)", $value), $stream->getCurrent()->getLine(), $stream->getFilename()); } } $this->parser->popLocalScope(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->setMacro($name, new Twig_Node_Macro($name, new Twig_Node_Body(array($body)), $arguments, $lineno, $this->getTag())); - - return null; } public function decideBlockEnd(Twig_Token $token) @@ -60,7 +59,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Sandbox.php b/app/parsers/Twig/TokenParser/Sandbox.php old mode 100755 new mode 100644 index 62e7f8f7..9457325a --- a/app/parsers/Twig/TokenParser/Sandbox.php +++ b/app/parsers/Twig/TokenParser/Sandbox.php @@ -35,6 +35,19 @@ public function parse(Twig_Token $token) $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + // in a sandbox tag, only include tags are allowed + if (!$body instanceof Twig_Node_Include) { + foreach ($body as $node) { + if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) { + continue; + } + + if (!$node instanceof Twig_Node_Include) { + throw new Twig_Error_Syntax('Only "include" tags are allowed within a "sandbox" section', $node->getLine(), $this->parser->getFilename()); + } + } + } + return new Twig_Node_Sandbox($body, $token->getLine(), $this->getTag()); } @@ -46,7 +59,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Set.php b/app/parsers/Twig/TokenParser/Set.php old mode 100755 new mode 100644 index 489e1d30..70e0b41b --- a/app/parsers/Twig/TokenParser/Set.php +++ b/app/parsers/Twig/TokenParser/Set.php @@ -49,13 +49,13 @@ public function parse(Twig_Token $token) $stream->expect(Twig_Token::BLOCK_END_TYPE); if (count($names) !== count($values)) { - throw new Twig_Error_Syntax("When using set, you must have the same number of variables and assignements.", $lineno); + throw new Twig_Error_Syntax("When using set, you must have the same number of variables and assignments.", $stream->getCurrent()->getLine(), $stream->getFilename()); } } else { $capture = true; if (count($names) > 1) { - throw new Twig_Error_Syntax("When using set with a block, you cannot have a multi-target.", $lineno); + throw new Twig_Error_Syntax("When using set with a block, you cannot have a multi-target.", $stream->getCurrent()->getLine(), $stream->getFilename()); } $stream->expect(Twig_Token::BLOCK_END_TYPE); @@ -75,7 +75,7 @@ public function decideBlockEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Spaceless.php b/app/parsers/Twig/TokenParser/Spaceless.php old mode 100755 new mode 100644 index aa7ffbc4..1e3fa8f3 --- a/app/parsers/Twig/TokenParser/Spaceless.php +++ b/app/parsers/Twig/TokenParser/Spaceless.php @@ -50,7 +50,7 @@ public function decideSpacelessEnd(Twig_Token $token) /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParser/Use.php b/app/parsers/Twig/TokenParser/Use.php old mode 100755 new mode 100644 index 16c47e3e..bc0e09ef --- a/app/parsers/Twig/TokenParser/Use.php +++ b/app/parsers/Twig/TokenParser/Use.php @@ -35,13 +35,12 @@ class Twig_TokenParser_Use extends Twig_TokenParser public function parse(Twig_Token $token) { $template = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); if (!$template instanceof Twig_Node_Expression_Constant) { - throw new Twig_Error_Syntax('The template references in a "use" statement must be a string.', $token->getLine()); + throw new Twig_Error_Syntax('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getFilename()); } - $stream = $this->parser->getStream(); - $targets = array(); if ($stream->test('with')) { $stream->next(); @@ -69,14 +68,12 @@ public function parse(Twig_Token $token) $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->addTrait(new Twig_Node(array('template' => $template, 'targets' => new Twig_Node($targets)))); - - return null; } /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ public function getTag() { diff --git a/app/parsers/Twig/TokenParserBroker.php b/app/parsers/Twig/TokenParserBroker.php old mode 100755 new mode 100644 index 4b2ff410..ec3fba67 --- a/app/parsers/Twig/TokenParserBroker.php +++ b/app/parsers/Twig/TokenParserBroker.php @@ -13,8 +13,8 @@ /** * Default implementation of a token parser broker. * - * @package twig - * @author Arnaud Le Blanc + * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_TokenParserBroker implements Twig_TokenParserBrokerInterface { @@ -32,13 +32,13 @@ public function __construct($parsers = array(), $brokers = array()) { foreach ($parsers as $parser) { if (!$parser instanceof Twig_TokenParserInterface) { - throw new Twig_Error('$parsers must a an array of Twig_TokenParserInterface'); + throw new LogicException('$parsers must a an array of Twig_TokenParserInterface'); } $this->parsers[$parser->getTag()] = $parser; } foreach ($brokers as $broker) { if (!$broker instanceof Twig_TokenParserBrokerInterface) { - throw new Twig_Error('$brokers must a an array of Twig_TokenParserBrokerInterface'); + throw new LogicException('$brokers must a an array of Twig_TokenParserBrokerInterface'); } $this->brokers[] = $broker; } @@ -54,6 +54,19 @@ public function addTokenParser(Twig_TokenParserInterface $parser) $this->parsers[$parser->getTag()] = $parser; } + /** + * Removes a TokenParser. + * + * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance + */ + public function removeTokenParser(Twig_TokenParserInterface $parser) + { + $name = $parser->getTag(); + if (isset($this->parsers[$name]) && $parser === $this->parsers[$name]) { + unset($this->parsers[$name]); + } + } + /** * Adds a TokenParserBroker. * @@ -64,6 +77,18 @@ public function addTokenParserBroker(Twig_TokenParserBroker $broker) $this->brokers[] = $broker; } + /** + * Removes a TokenParserBroker. + * + * @param Twig_TokenParserBroker $broker A Twig_TokenParserBroker instance + */ + public function removeTokenParserBroker(Twig_TokenParserBroker $broker) + { + if (false !== $pos = array_search($broker, $this->brokers)) { + unset($this->brokers[$pos]); + } + } + /** * Gets a suitable TokenParser for a tag. * @@ -86,7 +111,6 @@ public function getTokenParser($tag) } $broker = prev($this->brokers); } - return null; } public function getParsers() diff --git a/app/parsers/Twig/TokenParserBrokerInterface.php b/app/parsers/Twig/TokenParserBrokerInterface.php old mode 100755 new mode 100644 index 3ce8ca26..3f006e33 --- a/app/parsers/Twig/TokenParserBrokerInterface.php +++ b/app/parsers/Twig/TokenParserBrokerInterface.php @@ -15,31 +15,31 @@ * * Token parser brokers allows to implement custom logic in the process of resolving a token parser for a given tag name. * - * @package twig - * @author Arnaud Le Blanc + * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_TokenParserBrokerInterface { /** * Gets a TokenParser suitable for a tag. * - * @param string $tag A tag name + * @param string $tag A tag name * * @return null|Twig_TokenParserInterface A Twig_TokenParserInterface or null if no suitable TokenParser was found */ - function getTokenParser($tag); + public function getTokenParser($tag); /** * Calls Twig_TokenParserInterface::setParser on all parsers the implementation knows of. * * @param Twig_ParserInterface $parser A Twig_ParserInterface interface */ - function setParser(Twig_ParserInterface $parser); + public function setParser(Twig_ParserInterface $parser); /** * Gets the Twig_ParserInterface. * - * @return null|Twig_ParserInterface A Twig_ParserInterface instance of null + * @return null|Twig_ParserInterface A Twig_ParserInterface instance or null */ - function getParser(); + public function getParser(); } diff --git a/app/parsers/Twig/TokenParserInterface.php b/app/parsers/Twig/TokenParserInterface.php old mode 100755 new mode 100644 index 114a939e..bbde7714 --- a/app/parsers/Twig/TokenParserInterface.php +++ b/app/parsers/Twig/TokenParserInterface.php @@ -12,8 +12,7 @@ /** * Interface implemented by token parsers. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ interface Twig_TokenParserInterface { @@ -22,7 +21,7 @@ interface Twig_TokenParserInterface * * @param $parser A Twig_Parser instance */ - function setParser(Twig_Parser $parser); + public function setParser(Twig_Parser $parser); /** * Parses a token and returns a node. @@ -31,12 +30,12 @@ function setParser(Twig_Parser $parser); * * @return Twig_NodeInterface A Twig_NodeInterface instance */ - function parse(Twig_Token $token); + public function parse(Twig_Token $token); /** * Gets the tag name associated with this token parser. * - * @param string The tag name + * @return string The tag name */ - function getTag(); + public function getTag(); } diff --git a/app/parsers/Twig/TokenStream.php b/app/parsers/Twig/TokenStream.php old mode 100755 new mode 100644 index a2002b42..a78189f6 --- a/app/parsers/Twig/TokenStream.php +++ b/app/parsers/Twig/TokenStream.php @@ -13,8 +13,7 @@ /** * Represents a token stream. * - * @package twig - * @author Fabien Potencier + * @author Fabien Potencier */ class Twig_TokenStream { @@ -45,6 +44,11 @@ public function __toString() return implode("\n", $this->tokens); } + public function injectTokens(array $tokens) + { + $this->tokens = array_merge(array_slice($this->tokens, 0, $this->current), $tokens, array_slice($this->tokens, $this->current)); + } + /** * Sets the pointer to the next token and returns the old one. * @@ -53,7 +57,7 @@ public function __toString() public function next() { if (!isset($this->tokens[++$this->current])) { - throw new Twig_Error_Syntax('Unexpected end of template', -1, $this->filename); + throw new Twig_Error_Syntax('Unexpected end of template', $this->tokens[$this->current - 1]->getLine(), $this->filename); } return $this->tokens[$this->current - 1]; @@ -92,7 +96,7 @@ public function expect($type, $value = null, $message = null) public function look($number = 1) { if (!isset($this->tokens[$this->current + $number])) { - throw new Twig_Error_Syntax('Unexpected end of template', -1, $this->filename); + throw new Twig_Error_Syntax('Unexpected end of template', $this->tokens[$this->current + $number - 1]->getLine(), $this->filename); } return $this->tokens[$this->current + $number]; diff --git a/app/parsers/markdown-parser.inc.php b/app/parsers/markdown-parser.inc.php old mode 100755 new mode 100644 index af634c27..53f8a2e8 --- a/app/parsers/markdown-parser.inc.php +++ b/app/parsers/markdown-parser.inc.php @@ -18,6 +18,10 @@ # # +# Require Stacey configs +require_once(dirname(dirname(dirname(__FILE__))).'/extensions/config.php'); + + define( 'MARKDOWN_VERSION', "1.0.1n" ); # Sat 10 Oct 2009 define( 'MARKDOWNEXTRA_VERSION', "1.2.4" ); # Sat 10 Oct 2009 diff --git a/app/parsers/slir/croppers/centered.class.php b/app/parsers/slir/croppers/centered.class.php index 6925f7d4..80b05184 100644 --- a/app/parsers/slir/croppers/centered.class.php +++ b/app/parsers/slir/croppers/centered.class.php @@ -1,113 +1,113 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @subpackage Croppers - */ - -/* $Id: centered.class.php 116 2010-12-21 15:46:25Z joe.lencioni $ */ - -require_once 'slircropper.interface.php'; - -/** - * Centered SLIR cropper - * - * Calculates the crop offset anchored in the center of the image - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 09:46:25 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 116 $ - * @package SLIR - * @subpackage Croppers - */ -class SLIRCropperCentered implements SLIRCropper -{ - /** - * Determines if the top and bottom need to be cropped - * - * @since 2.0 - * @param SLIRImage $image - * @return boolean - */ - private function shouldCropTopAndBottom(SLIRImage $image) - { - if ($image->cropRatio() > $image->ratio()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @param SLIRImage $image - * @return integer - */ - public function getCropY(SLIRImage $image) - { - return round(($image->height - $image->cropHeight) / 2); - } - - /** - * @since 2.0 - * @param SLIRImage $image - * @return integer - */ - public function getCropX(SLIRImage $image) - { - return round(($image->width - $image->cropWidth) / 2); - } - - /** - * Calculates the crop offset anchored in the center of the image - * - * @since 2.0 - * @param SLIRImage $image - * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped - */ - public function getCrop(SLIRImage $image) - { - // Determine crop offset - $crop = array( - 'x' => 0, - 'y' => 0, - ); - - if ($this->shouldCropTopAndBottom($image)) - { - // Image is too tall so we will crop the top and bottom - $crop['y'] = $this->getCropY($image); - } - else - { - // Image is too wide so we will crop off the left and right sides - $crop['x'] = $this->getCropX($image); - } - - return $crop; - } +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @subpackage Croppers + */ + +/* $Id: centered.class.php 116 2010-12-21 15:46:25Z joe.lencioni $ */ + +require_once 'slircropper.interface.php'; + +/** + * Centered SLIR cropper + * + * Calculates the crop offset anchored in the center of the image + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 09:46:25 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 116 $ + * @package SLIR + * @subpackage Croppers + */ +class SLIRCropperCentered implements SLIRCropper +{ + /** + * Determines if the top and bottom need to be cropped + * + * @since 2.0 + * @param SLIRImage $image + * @return boolean + */ + private function shouldCropTopAndBottom(SLIRImage $image) + { + if ($image->cropRatio() > $image->ratio()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @param SLIRImage $image + * @return integer + */ + public function getCropY(SLIRImage $image) + { + return round(($image->height - $image->cropHeight) / 2); + } + + /** + * @since 2.0 + * @param SLIRImage $image + * @return integer + */ + public function getCropX(SLIRImage $image) + { + return round(($image->width - $image->cropWidth) / 2); + } + + /** + * Calculates the crop offset anchored in the center of the image + * + * @since 2.0 + * @param SLIRImage $image + * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped + */ + public function getCrop(SLIRImage $image) + { + // Determine crop offset + $crop = array( + 'x' => 0, + 'y' => 0, + ); + + if ($this->shouldCropTopAndBottom($image)) + { + // Image is too tall so we will crop the top and bottom + $crop['y'] = $this->getCropY($image); + } + else + { + // Image is too wide so we will crop off the left and right sides + $crop['x'] = $this->getCropX($image); + } + + return $crop; + } } \ No newline at end of file diff --git a/app/parsers/slir/croppers/face.class.php b/app/parsers/slir/croppers/face.class.php index 15c0a15e..a2d94e7a 100644 --- a/app/parsers/slir/croppers/face.class.php +++ b/app/parsers/slir/croppers/face.class.php @@ -1,223 +1,223 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @subpackage Croppers - */ - -/* $Id: face.class.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ - -require_once 'slircropper.interface.php'; - -/** - * Face SLIR cropper - * - * Calculates the crop offset using face detection - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 113 $ - * @package SLIR - * @subpackage Croppers - */ -class SLIRCropperFace implements SLIRCropper -{ - /** - * Determines if the top and bottom need to be cropped - * - * @since 2.0 - * @param SLIRImage $image - * @return boolean - */ - private function shouldCropTopAndBottom(SLIRImage $image) - { - if ($image->cropRatio() > $image->ratio()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Calculates the crop offset using face detection - * - * @since 2.0 - * @param SLIRImage $image - * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped - */ - private function getCrop(SLIRImage $image) - { - // This is way too slow to not have apc caching face detection data for us - if (!function_exists('apc_fetch')) - { - return NULL; - } - - $key = 'slirface_' . md5($image->path); - $cached = (function_exists('apc_fetch')) ? apc_fetch($key) : FALSE; - - if ($cached === FALSE) - { - require_once '../facedetector/facedetector.class.php'; - $detector = new SLIRFaceDetector(); - - // Make the image smaller for face detection so it will work faster - // @todo this should be done before resizing - $a = $image->width * $image->height; - $smallerA = pow(80, 2); - $ratio = sqrt($smallerA) / sqrt($a); - - if ($ratio < 1) - { - $smallerW = $image->width * $ratio; - $smallerH = $image->height * $ratio; - $smaller = imagecreatetruecolor($smallerW, $smallerH); - imagecopyresampled($smaller, $image->image, 0, 0, 0, 0, $smallerW, $smallerH, $image->width, $image->height); - } - else - { - $smaller = $image->image; - $smallerW = $image->width; - } - - // convert to grayscale - //imagefilter($smaller, IMG_FILTER_GRAYSCALE); - - // for some reason, this grayscale conversion causes the final image to be in - // grayscale if the size is small. but why? - - // load up our detection data - $cascade = json_decode(file_get_contents(SLIR_DOCUMENT_ROOT . SLIR_DIR . '/facedetector/face.json'), TRUE); - - // detect faces - $faces = $detector->detect_objects($smaller, $cascade, 5, 1); - if (function_exists('apc_store')) - { - apc_store($key, array('width' => $smallerW, 'faces' => $faces)); - } - } - else // Face detection data was cached - { - $faces = $cached['faces']; - $ratio = $cached['width'] / $image->width; - } - - if (count($faces) > 0) - { - $confidenceThreshold = 10; - - if ($this->shouldCropTopAndBottom($image)) - { - - /* // this outlines the faces in red - $color = imagecolorallocate($image->image, 255, 0, 0); //red - foreach($faces as $face) - { - if ($face['confidence'] > $confidenceThreshold) - { - $face['x'] /= $ratio; - $face['y'] /= $ratio; - $face['height'] /= $ratio; - $face['width'] /= $ratio; - imagerectangle($image->image, $face['x'], $face['y'], $face['x']+$face['width'], $face['y']+ $face['height'], $color); - } - } - - header('Content-type: image/jpeg'); - imagejpeg($image->image); - exit(); - */ - - // @todo extract this into its own function (and generalize it for top/bottom cropping as well as left/right cropping) - $highest = NULL; - $lowest = NULL; - foreach($faces as $face) - { - if ($face['confidence'] > $confidenceThreshold) - { - $face['x'] /= $ratio; - $face['y'] /= $ratio; - $face['height'] /= $ratio; - $face['width'] /= $ratio; - - if ($highest === NULL || $face['y'] < $highest) - { - $highest = $face['y']; - } - if ($lowest === NULL || $face['y'] + $face['height'] > $lowest) - { - $lowest = $face['y'] + $face['height']; - } - } - - if ($highest !== NULL && $lowest !== NULL) - { - $midpoint = $highest + (($lowest - $highest) / 2); - return min($image->height - $image->cropHeight, max(0, $midpoint - ($image->cropHeight / 2))); - } - } - - } - else - { - $leftest = NULL; - $rightest = NULL; - foreach($faces as $face) - { - if ($face['confidence'] > $confidenceThreshold) - { - $face['x'] /= $ratio; - $face['y'] /= $ratio; - $face['height'] /= $ratio; - $face['width'] /= $ratio; - - if ($leftest === NULL || $face['x'] < $leftest) - { - $leftest = $face['x']; - } - if ($rightest === NULL || $face['x'] + $face['width'] > $rightest) - { - $rightest = $face['x'] + $face['width']; - } - } - - - if ($leftest !== NULL && $rightest !== NULL) - { - $midpoint = $leftest + (($rightest - $leftest) / 2); - return min($image->width - $image->cropWidth, max(0, $midpoint - ($image->cropWidth / 2))); - } - } - } - } - else - { - // @todo fallback to another cropper - return NULL; - } - } +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @subpackage Croppers + */ + +/* $Id: face.class.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ + +require_once 'slircropper.interface.php'; + +/** + * Face SLIR cropper + * + * Calculates the crop offset using face detection + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 113 $ + * @package SLIR + * @subpackage Croppers + */ +class SLIRCropperFace implements SLIRCropper +{ + /** + * Determines if the top and bottom need to be cropped + * + * @since 2.0 + * @param SLIRImage $image + * @return boolean + */ + private function shouldCropTopAndBottom(SLIRImage $image) + { + if ($image->cropRatio() > $image->ratio()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Calculates the crop offset using face detection + * + * @since 2.0 + * @param SLIRImage $image + * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped + */ + private function getCrop(SLIRImage $image) + { + // This is way too slow to not have apc caching face detection data for us + if (!function_exists('apc_fetch')) + { + return NULL; + } + + $key = 'slirface_' . md5($image->path); + $cached = (function_exists('apc_fetch')) ? apc_fetch($key) : FALSE; + + if ($cached === FALSE) + { + require_once '../facedetector/facedetector.class.php'; + $detector = new SLIRFaceDetector(); + + // Make the image smaller for face detection so it will work faster + // @todo this should be done before resizing + $a = $image->width * $image->height; + $smallerA = pow(80, 2); + $ratio = sqrt($smallerA) / sqrt($a); + + if ($ratio < 1) + { + $smallerW = $image->width * $ratio; + $smallerH = $image->height * $ratio; + $smaller = imagecreatetruecolor($smallerW, $smallerH); + imagecopyresampled($smaller, $image->image, 0, 0, 0, 0, $smallerW, $smallerH, $image->width, $image->height); + } + else + { + $smaller = $image->image; + $smallerW = $image->width; + } + + // convert to grayscale + //imagefilter($smaller, IMG_FILTER_GRAYSCALE); + + // for some reason, this grayscale conversion causes the final image to be in + // grayscale if the size is small. but why? + + // load up our detection data + $cascade = json_decode(file_get_contents(SLIR_DOCUMENT_ROOT . SLIR_DIR . '/facedetector/face.json'), TRUE); + + // detect faces + $faces = $detector->detect_objects($smaller, $cascade, 5, 1); + if (function_exists('apc_store')) + { + apc_store($key, array('width' => $smallerW, 'faces' => $faces)); + } + } + else // Face detection data was cached + { + $faces = $cached['faces']; + $ratio = $cached['width'] / $image->width; + } + + if (count($faces) > 0) + { + $confidenceThreshold = 10; + + if ($this->shouldCropTopAndBottom($image)) + { + + /* // this outlines the faces in red + $color = imagecolorallocate($image->image, 255, 0, 0); //red + foreach($faces as $face) + { + if ($face['confidence'] > $confidenceThreshold) + { + $face['x'] /= $ratio; + $face['y'] /= $ratio; + $face['height'] /= $ratio; + $face['width'] /= $ratio; + imagerectangle($image->image, $face['x'], $face['y'], $face['x']+$face['width'], $face['y']+ $face['height'], $color); + } + } + + header('Content-type: image/jpeg'); + imagejpeg($image->image); + exit(); + */ + + // @todo extract this into its own function (and generalize it for top/bottom cropping as well as left/right cropping) + $highest = NULL; + $lowest = NULL; + foreach($faces as $face) + { + if ($face['confidence'] > $confidenceThreshold) + { + $face['x'] /= $ratio; + $face['y'] /= $ratio; + $face['height'] /= $ratio; + $face['width'] /= $ratio; + + if ($highest === NULL || $face['y'] < $highest) + { + $highest = $face['y']; + } + if ($lowest === NULL || $face['y'] + $face['height'] > $lowest) + { + $lowest = $face['y'] + $face['height']; + } + } + + if ($highest !== NULL && $lowest !== NULL) + { + $midpoint = $highest + (($lowest - $highest) / 2); + return min($image->height - $image->cropHeight, max(0, $midpoint - ($image->cropHeight / 2))); + } + } + + } + else + { + $leftest = NULL; + $rightest = NULL; + foreach($faces as $face) + { + if ($face['confidence'] > $confidenceThreshold) + { + $face['x'] /= $ratio; + $face['y'] /= $ratio; + $face['height'] /= $ratio; + $face['width'] /= $ratio; + + if ($leftest === NULL || $face['x'] < $leftest) + { + $leftest = $face['x']; + } + if ($rightest === NULL || $face['x'] + $face['width'] > $rightest) + { + $rightest = $face['x'] + $face['width']; + } + } + + + if ($leftest !== NULL && $rightest !== NULL) + { + $midpoint = $leftest + (($rightest - $leftest) / 2); + return min($image->width - $image->cropWidth, max(0, $midpoint - ($image->cropWidth / 2))); + } + } + } + } + else + { + // @todo fallback to another cropper + return NULL; + } + } } \ No newline at end of file diff --git a/app/parsers/slir/croppers/slircropper.interface.php b/app/parsers/slir/croppers/slircropper.interface.php index 41c55804..06b6fd94 100644 --- a/app/parsers/slir/croppers/slircropper.interface.php +++ b/app/parsers/slir/croppers/slircropper.interface.php @@ -1,48 +1,48 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @subpackage Croppers - */ - -/* $Id: slircropper.interface.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ - -/** - * SLIR cropper interface - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 113 $ - * @package SLIR - * @subpackage Croppers - */ -interface SLIRCropper -{ - /** - * @since 2.0 - * @param SLIRImage $image - * @return array Associative array with the keys of x, y, width, and height that specify the box that should be cropped - */ - public function getCrop(SLIRImage $image); +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @subpackage Croppers + */ + +/* $Id: slircropper.interface.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ + +/** + * SLIR cropper interface + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 113 $ + * @package SLIR + * @subpackage Croppers + */ +interface SLIRCropper +{ + /** + * @since 2.0 + * @param SLIRImage $image + * @return array Associative array with the keys of x, y, width, and height that specify the box that should be cropped + */ + public function getCrop(SLIRImage $image); } \ No newline at end of file diff --git a/app/parsers/slir/croppers/smart.class.php b/app/parsers/slir/croppers/smart.class.php index 240bc9ca..bc0f09b3 100644 --- a/app/parsers/slir/croppers/smart.class.php +++ b/app/parsers/slir/croppers/smart.class.php @@ -1,816 +1,816 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @subpackage Croppers - */ - -/* $Id: smart.class.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ - -require_once 'slircropper.interface.php'; - -/** - * Smart SLIR cropper - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 113 $ - * @package SLIR - * @subpackage Croppers - */ -class SLIRCropperSmart implements SLIRCropper -{ - /** - * Determines if the top and bottom need to be cropped - * - * @since 2.0 - * @param SLIRImage $image - * @return boolean - */ - private function shouldCropTopAndBottom(SLIRImage $image) - { - if ($image->cropRatio() > $image->ratio()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Determines the optimal number of rows in from the top or left to crop - * the source image - * - * @since 2.0 - * @param SLIRImage $image - * @return integer|boolean - */ - private function cropSmartOffsetRows(SLIRImage $image) - { - // @todo Change this method to resize image, determine offset, and then extrapolate the actual offset based on the image size difference. Then we can cache the offset in APC (all just like we are doing for face detection) - - if ($this->shouldCropTopAndBottom($image)) - { - $length = $image->cropHeight; - $lengthB = $image->cropWidth; - $originalLength = $image->height; - } - else - { - $length = $image->cropWidth; - $lengthB = $image->cropHeight; - $originalLength = $image->width; - } - - // To smart crop an image, we need to calculate the difference between - // each pixel in each row and its adjacent pixels. Add these up to - // determine how interesting each row is. Based on how interesting each - // row is, we can determine whether or not to discard it. We start with - // the closest row and the farthest row and then move on from there. - - // All colors in the image will be stored in the global colors array. - // This array will also include information about each pixel's - // interestingness. - // - // For example (rough representation): - // - // $colors = array( - // x1 => array( - // x1y1 => array( - // 'lab' => array(l, a, b), - // 'dE' => array(TL, TC, TR, LC, LR, BL, BC, BR), - // 'i' => computedInterestingness - // ), - // x1y2 => array( ... ), - // ... - // ), - // x2 => array( ... ), - // ... - // ); - global $colors; - $colors = array(); - - // Offset will remember how far in from each side we are in the - // cropping game - $offset = array( - 'near' => 0, - 'far' => 0, - ); - - $rowsToCrop = $originalLength - $length; - - // $pixelStep will sacrifice accuracy for memory and speed. Essentially - // it acts as a spot-checker and scales with the size of the cropped area - $pixelStep = round(sqrt($rowsToCrop * $lengthB) / 10); - - // We won't save much speed if the pixelStep is between 4 and 1 because - // we still need to sample adjacent pixels - if ($pixelStep < 4) - { - $pixelStep = 1; - } - - $tolerance = 0.5; - $upperTol = 1 + $tolerance; - $lowerTol = 1 / $upperTol; - - // Fight the near and far rows. The stronger will remain standing. - $returningChampion = NULL; - $ratio = 1; - for ($rowsCropped = 0; $rowsCropped < $rowsToCrop; ++$rowsCropped) - { - $a = $this->rowInterestingness($image, $offset['near'], $pixelStep, $originalLength); - $b = $this->rowInterestingness($image, $originalLength - $offset['far'] - 1, $pixelStep, $originalLength); - - if ($a == 0 && $b == 0) - { - $ratio = 1; - } - else if ($b == 0) - { - $ratio = 1 + $a; - } - else - { - $ratio = $a / $b; - } - - if ($ratio > $upperTol) - { - ++$offset['far']; - - // Fightback. Winning side gets to go backwards through fallen rows - // to see if they are stronger - if ($returningChampion == 'near') - { - $offset['near'] -= ($offset['near'] > 0) ? 1 : 0; - } - else - { - $returningChampion = 'near'; - } - } - else if ($ratio < $lowerTol) - { - ++$offset['near']; - - if ($returningChampion == 'far') - { - $offset['far'] -= ($offset['far'] > 0) ? 1 : 0; - } - else - { - $returningChampion = 'far'; - } - } - else - { - // There is no strong winner, so discard rows from the side that - // has lost the fewest so far. Essentially this is a draw. - if ($offset['near'] > $offset['far']) - { - ++$offset['far']; - } - else // Discard near - { - ++$offset['near']; - } - - // No fightback for draws - $returningChampion = NULL; - } // if - - } // for - - // Bounceback for potentially important details on the edge. - // This may possibly be better if the winning side fights a hard final - // push multiple-rows-at-stake battle where it stands the chance to gain - // ground. - if ($ratio > (1 + ($tolerance * 1.25))) - { - $offset['near'] -= round($length * .03); - } - else if ($ratio < (1 / (1 + ($tolerance * 1.25)))) - { - $offset['near'] += round($length * .03); - } - - return min($rowsToCrop, max(0, $offset['near'])); - } - - /** - * Calculate the interestingness value of a row of pixels - * - * @since 2.0 - * @param SLIRImage $image - * @param integer $row - * @param integer $pixelStep Number of pixels to jump after each step when comparing interestingness - * @param integer $originalLength Number of rows in the original image - * @return float - */ - private function rowInterestingness(SLIRImage $image, $row, $pixelStep, $originalLength) - { - $interestingness = 0; - $max = 0; - - if ($this->shouldCropTopAndBottom($image)) - { - for ($totalPixels = 0; $totalPixels < $image->width; $totalPixels += $pixelStep) - { - $i = $this->pixelInterestingness($image, $totalPixels, $row); - - // Content at the very edge of an image tends to be less interesting than - // content toward the center, so we give it a little extra push away from the edge - //$i += min($row, $originalLength - $row, $originalLength * .04); - - $max = max($i, $max); - $interestingness += $i; - } - } - else - { - for ($totalPixels = 0; $totalPixels < $image->height; $totalPixels += $pixelStep) - { - $i = $this->pixelInterestingness($image, $row, $totalPixels); - - // Content at the very edge of an image tends to be less interesting than - // content toward the center, so we give it a little extra push away from the edge - //$i += min($row, $originalLength - $row, $originalLength * .04); - - $max = max($i, $max); - $interestingness += $i; - } - } - - return $interestingness + (($max - ($interestingness / ($totalPixels / $pixelStep))) * ($totalPixels / $pixelStep)); - } - - /** - * Get the interestingness value of a pixel - * - * @since 2.0 - * @param SLIRImage $image - * @param integer $x x-axis position of pixel to calculate - * @param integer $y y-axis position of pixel to calculate - * @return float - */ - private function pixelInterestingness(SLIRImage $image, $x, $y) - { - global $colors; - - if (!isset($colors[$x][$y]['i'])) - { - // Ensure this pixel's color information has already been loaded - $this->loadPixelInfo($image, $x, $y); - - // Calculate each neighboring pixel's Delta E in relation to this - // pixel - $this->calculateDeltas($image, $x, $y); - - // Calculate the interestingness of this pixel based on neighboring - // pixels' Delta E in relation to this pixel - $this->calculateInterestingness($x, $y); - } // if - - return $colors[$x][$y]['i']; - } - - /** - * Load the color information of the requested pixel into the $colors array - * - * @since 2.0 - * @param SLIRImage $image - * @param integer $x x-axis position of pixel to calculate - * @param integer $y y-axis position of pixel to calculate - * @return boolean - */ - private function loadPixelInfo(SLIRImage $image, $x, $y) - { - if ($x < 0 || $x >= $image->width - || $y < 0 || $y >= $image->height) - { - return FALSE; - } - - global $colors; - - if (!isset($colors[$x])) - { - $colors[$x] = array(); - } - - if (!isset($colors[$x][$y])) - { - $colors[$x][$y] = array(); - } - - if (!isset($colors[$x][$y]['i']) && !isset($colors[$x][$y]['lab'])) - { - $colors[$x][$y]['lab'] = $this->evaluateColor(imagecolorat($image->image, $x, $y)); - } - - return TRUE; - } - - /** - * Calculates each adjacent pixel's Delta E in relation to the pixel requested - * - * @since 2.0 - * @param SLIRImage $image - * @param integer $x x-axis position of pixel to calculate - * @param integer $y y-axis position of pixel to calculate - * @return boolean - */ - private function calculateDeltas(SLIRImage $image, $x, $y) - { - // Calculate each adjacent pixel's Delta E in relation to the current - // pixel (top left, top center, top right, center left, center right, - // bottom left, bottom center, and bottom right) - - global $colors; - - if (!isset($colors[$x][$y]['dE']['d-1-1'])) - { - $this->calculateDelta($image, $x, $y, -1, -1); - } - if (!isset($colors[$x][$y]['dE']['d0-1'])) - { - $this->calculateDelta($image, $x, $y, 0, -1); - } - if (!isset($colors[$x][$y]['dE']['d1-1'])) - { - $this->calculateDelta($image, $x, $y, 1, -1); - } - if (!isset($colors[$x][$y]['dE']['d-10'])) - { - $this->calculateDelta($image, $x, $y, -1, 0); - } - if (!isset($colors[$x][$y]['dE']['d10'])) - { - $this->calculateDelta($image, $x, $y, 1, 0); - } - if (!isset($colors[$x][$y]['dE']['d-11'])) - { - $this->calculateDelta($image, $x, $y, -1, 1); - } - if (!isset($colors[$x][$y]['dE']['d01'])) - { - $this->calculateDelta($image, $x, $y, 0, 1); - } - if (!isset($colors[$x][$y]['dE']['d11'])) - { - $this->calculateDelta($image, $x, $y, 1, 1); - } - - return TRUE; - } - - /** - * Calculates and stores requested pixel's Delta E in relation to comparison pixel - * - * @since 2.0 - * @param SLIRImage $image - * @param integer $x1 x-axis position of pixel to calculate - * @param integer $y1 y-axis position of pixel to calculate - * @param integer $xMove number of pixels to move on the x-axis to find comparison pixel - * @param integer $yMove number of pixels to move on the y-axis to find comparison pixel - * @return boolean - */ - private function calculateDelta(SLIRImage $image, $x1, $y1, $xMove, $yMove) - { - $x2 = $x1 + $xMove; - $y2 = $y1 + $yMove; - - // Pixel is outside of the image, so we cant't calculate the Delta E - if ($x2 < 0 || $x2 >= $image->width - || $y2 < 0 || $y2 >= $image->height) - { - return NULL; - } - - global $colors; - - if (!isset($colors[$x1][$y1]['lab'])) - { - $this->loadPixelInfo($image, $x1, $y1); - } - if (!isset($colors[$x2][$y2]['lab'])) - { - $this->loadPixelInfo($image, $x2, $y2); - } - - $delta = $this->deltaE($colors[$x1][$y1]['lab'], $colors[$x2][$y2]['lab']); - - $colors[$x1][$y1]['dE']["d$xMove$yMove"] = $delta; - - $x2Move = $xMove * -1; - $y2Move = $yMove * -1; - $colors[$x2][$y2]['dE']["d$x2Move$y2Move"] =& $colors[$x1][$y1]['dE']["d$xMove$yMove"]; - - return TRUE; - } - - /** - * Calculates and stores a pixel's overall interestingness value - * - * @since 2.0 - * @param integer $x x-axis position of pixel to calculate - * @param integer $y y-axis position of pixel to calculate - * @return boolean - */ - private function calculateInterestingness($x, $y) - { - global $colors; - - // The interestingness is the average of the pixel's Delta E values - $colors[$x][$y]['i'] = array_sum($colors[$x][$y]['dE']) - / count(array_filter($colors[$x][$y]['dE'], 'is_numeric')); - - return TRUE; - } - - /** - * @since 2.0 - * @param integer $int - * @return array - */ - private function evaluateColor($int) - { - $rgb = $this->colorIndexToRGB($int); - $xyz = $this->RGBtoXYZ($rgb); - $lab = $this->XYZtoHunterLab($xyz); - - return $lab; - } - - /** - * @since 2.0 - * @param integer $int - * @return array - */ - private function colorIndexToRGB($int) - { - $a = (255 - (($int >> 24) & 0xFF)) / 255; - $r = (($int >> 16) & 0xFF) * $a; - $g = (($int >> 8) & 0xFF) * $a; - $b = ($int & 0xFF) * $a; - return array('r' => $r, 'g' => $g, 'b' => $b); - } - - /** - * @since 2.0 - * @param array $rgb - * @return array XYZ - * @link http://easyrgb.com/index.php?X=MATH&H=02#text2 - */ - private function RGBtoXYZ($rgb) - { - $r = $rgb['r'] / 255; - $g = $rgb['g'] / 255; - $b = $rgb['b'] / 255; - - if ($r > 0.04045) - { - $r = pow((($r + 0.055) / 1.055), 2.4); - } - else - { - $r = $r / 12.92; - } - - if ($g > 0.04045) - { - $g = pow((($g + 0.055) / 1.055), 2.4); - } - else - { - $g = $g / 12.92; - } - - if ($b > 0.04045) - { - $b = pow((($b + 0.055) / 1.055), 2.4); - } - else - { - $b = $b / 12.92; - } - - $r *= 100; - $g *= 100; - $b *= 100; - - //Observer. = 2Β°, Illuminant = D65 - $x = $r * 0.4124 + $g * 0.3576 + $b * 0.1805; - $y = $r * 0.2126 + $g * 0.7152 + $b * 0.0722; - $z = $r * 0.0193 + $g * 0.1192 + $b * 0.9505; - - return array('x' => $x, 'y' => $y, 'z' => $z); - } - - /** - * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5 - */ - private function XYZtoHunterLab($xyz) - { - if ($xyz['y'] == 0) - { - return array('l' => 0, 'a' => 0, 'b' => 0); - } - - $l = 10 * sqrt($xyz['y']); - $a = 17.5 * ( ( ( 1.02 * $xyz['x'] ) - $xyz['y']) / sqrt( $xyz['y'] ) ); - $b = 7 * ( ( $xyz['y'] - ( 0.847 * $xyz['z'] ) ) / sqrt( $xyz['y'] ) ); - - return array('l' => $l, 'a' => $a, 'b' => $b); - } - - /** - * Converts a color from RGB colorspace to CIE-L*ab colorspace - * @since 2.0 - * @param array $xyz - * @return array LAB - * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5 - */ - private function XYZtoCIELAB($xyz) - { - $refX = 100; - $refY = 100; - $refZ = 100; - - $X = $xyz['x'] / $refX; - $Y = $xyz['y'] / $refY; - $Z = $xyz['z'] / $refZ; - - if ( $X > 0.008856 ) - { - $X = pow($X, 1/3); - } - else - { - $X = ( 7.787 * $X ) + ( 16 / 116 ); - } - - if ( $Y > 0.008856 ) - { - $Y = pow($Y, 1/3); - } - else - { - $Y = ( 7.787 * $Y ) + ( 16 / 116 ); - } - - if ( $Z > 0.008856 ) - { - $Z = pow($Z, 1/3); - } - else - { - $Z = ( 7.787 * $Z ) + ( 16 / 116 ); - } - - $l = ( 116 * $Y ) - 16; - $a = 500 * ( $X - $Y ); - $b = 200 * ( $Y - $Z ); - - return array('l' => $l, 'a' => $a, 'b' => $b); - } - - private function deltaE($lab1, $lab2) - { - return sqrt( ( pow( $lab1['l'] - $lab2['l'], 2 ) ) - + ( pow( $lab1['a'] - $lab2['a'], 2 ) ) - + ( pow( $lab1['b'] - $lab2['b'], 2 ) ) ); - } - - /** - * Compute the Delta E 2000 value of two colors in the LAB colorspace - * - * @link http://en.wikipedia.org/wiki/Color_difference#CIEDE2000 - * @link http://easyrgb.com/index.php?X=DELT&H=05#text5 - * @since 2.0 - * @param array $lab1 LAB color array - * @param array $lab2 LAB color array - * @return float - */ - private function deltaE2000($lab1, $lab2) - { - $weightL = 1; // Lightness - $weightC = 1; // Chroma - $weightH = 1; // Hue - - $xC1 = sqrt( $lab1['a'] * $lab1['a'] + $lab1['b'] * $lab1['b'] ); - $xC2 = sqrt( $lab2['a'] * $lab2['a'] + $lab2['b'] * $lab2['b'] ); - $xCX = ( $xC1 + $xC2 ) / 2; - $xGX = 0.5 * ( 1 - sqrt( ( pow($xCX, 7) ) / ( ( pow($xCX, 7) ) + ( pow(25, 7) ) ) ) ); - $xNN = ( 1 + $xGX ) * $lab1['a']; - $xC1 = sqrt( $xNN * $xNN + $lab1['b'] * $lab1['b'] ); - $xH1 = $this->LABtoHue( $xNN, $lab1['b'] ); - $xNN = ( 1 + $xGX ) * $lab2['a']; - $xC2 = sqrt( $xNN * $xNN + $lab2['b'] * $lab2['b'] ); - $xH2 = $this->LABtoHue( $xNN, $lab2['b'] ); - $xDL = $lab2['l'] - $lab1['l']; - $xDC = $xC2 - $xC1; - - if ( ( $xC1 * $xC2 ) == 0 ) - { - $xDH = 0; - } - else - { - $xNN = round( $xH2 - $xH1, 12 ); - if ( abs( $xNN ) <= 180 ) - { - $xDH = $xH2 - $xH1; - } - else - { - if ( $xNN > 180 ) - { - $xDH = $xH2 - $xH1 - 360; - } - else - { - $xDH = $xH2 - $xH1 + 360; - } - } // if - } // if - - $xDH = 2 * sqrt( $xC1 * $xC2 ) * sin( rad2deg( $xDH / 2 ) ); - $xLX = ( $lab1['l'] + $lab2['l'] ) / 2; - $xCY = ( $xC1 + $xC2 ) / 2; - - if ( ( $xC1 * $xC2 ) == 0 ) - { - $xHX = $xH1 + $xH2; - } - else - { - $xNN = abs( round( $xH1 - $xH2, 12 ) ); - if ( $xNN > 180 ) - { - if ( ( $xH2 + $xH1 ) < 360 ) - { - $xHX = $xH1 + $xH2 + 360; - } - else - { - $xHX = $xH1 + $xH2 - 360; - } - } - else - { - $xHX = $xH1 + $xH2; - } // if - $xHX /= 2; - } // if - - $xTX = 1 - 0.17 * cos( rad2deg( $xHX - 30 ) ) - + 0.24 * cos( rad2deg( 2 * $xHX ) ) - + 0.32 * cos( rad2deg( 3 * $xHX + 6 ) ) - - 0.20 * cos( rad2deg( 4 * $xHX - 63 ) ); - - $xPH = 30 * exp( - ( ( $xHX - 275 ) / 25 ) * ( ( $xHX - 275 ) / 25 ) ); - $xRC = 2 * sqrt( ( pow($xCY, 7) ) / ( ( pow($xCY, 7) ) + ( pow(25, 7) ) ) ); - $xSL = 1 + ( ( 0.015 * ( ( $xLX - 50 ) * ( $xLX - 50 ) ) ) - / sqrt( 20 + ( ( $xLX - 50 ) * ( $xLX - 50 ) ) ) ); - $xSC = 1 + 0.045 * $xCY; - $xSH = 1 + 0.015 * $xCY * $xTX; - $xRT = - sin( rad2deg( 2 * $xPH ) ) * $xRC; - $xDL = $xDL / $weightL * $xSL; - $xDC = $xDC / $weightC * $xSC; - $xDH = $xDH / $weightH * $xSH; - - $delta = sqrt( pow($xDL, 2) + pow($xDC, 2) + pow($xDH, 2) + $xRT * $xDC * $xDH ); - return (is_nan($delta)) ? 1 : $delta / 100; - } - - /** - * Compute the Delta CMC value of two colors in the LAB colorspace - * - * @since 2.0 - * @param array $lab1 LAB color array - * @param array $lab2 LAB color array - * @return float - * @link http://easyrgb.com/index.php?X=DELT&H=06#text6 - */ - private function deltaCMC($lab1, $lab2) - { - // if $weightL is 2 and $weightC is 1, it means that the lightness - // will contribute half as much importance to the delta as the chroma - $weightL = 2; // Lightness - $weightC = 1; // Chroma - - $xC1 = sqrt( ( pow($lab1['a'], 2) ) + ( pow($lab1['b'], 2) ) ); - $xC2 = sqrt( ( pow($lab2['a'], 2) ) + ( pow($lab2['b'], 2) ) ); - $xff = sqrt( ( pow($xC1, 4) ) / ( ( pow($xC1, 4) ) + 1900 ) ); - $xH1 = $this->LABtoHue( $lab1['a'], $lab1['b'] ); - - if ( $xH1 < 164 || $xH1 > 345 ) - { - $xTT = 0.36 + abs( 0.4 * cos( deg2rad( 35 + $xH1 ) ) ); - } - else - { - $xTT = 0.56 + abs( 0.2 * cos( deg2rad( 168 + $xH1 ) ) ); - } - - if ( $lab1['l'] < 16 ) - { - $xSL = 0.511; - } - else - { - $xSL = ( 0.040975 * $lab1['l'] ) / ( 1 + ( 0.01765 * $lab1['l'] ) ); - } - - $xSC = ( ( 0.0638 * $xC1 ) / ( 1 + ( 0.0131 * $xC1 ) ) ) + 0.638; - $xSH = ( ( $xff * $xTT ) + 1 - $xff ) * $xSC; - $xDH = sqrt( pow( $lab2['a'] - $lab1['a'], 2 ) + pow( $lab2['b'] - $lab1['b'], 2 ) - pow( $xC2 - $xC1, 2 ) ); - $xSL = ( $lab2['l'] - $lab1['l'] ) / $weightL * $xSL; - $xSC = ( $xC2 - $xC1 ) / $weightC * $xSC; - $xSH = $xDH / $xSH; - - $delta = sqrt( pow($xSL, 2) + pow($xSC, 2) + pow($xSH, 2) ); - return (is_nan($delta)) ? 1 : $delta; - } - - /** - * @since 2.0 - * @param integer $a - * @param integer $b - * @return CIE-HΒ° value - */ - private function LABtoHue($a, $b) - { - $bias = 0; - - if ($a >= 0 && $b == 0) return 0; - if ($a < 0 && $b == 0) return 180; - if ($a == 0 && $b > 0) return 90; - if ($a == 0 && $b < 0) return 270; - if ($a > 0 && $b > 0) $bias = 0; - if ($a < 0 ) $bias = 180; - if ($a > 0 && $b < 0) $bias = 360; - - return (rad2deg(atan($b / $a)) + $bias); - } - - /** - * Calculates the crop offset using an algorithm that tries to determine - * the most interesting portion of the image to keep. - * - * @since 2.0 - * @param SLIRImage $image - * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped - */ - public function getCrop(SLIRImage $image) - { - // Try contrast detection - $o = $this->cropSmartOffsetRows($image); - - $crop = array( - 'x' => 0, - 'y' => 0, - ); - - if ($o === FALSE) - { - return TRUE; - } - else if ($this->shouldCropTopAndBottom($image)) - { - $crop['y'] = $o; - } - else - { - $crop['x'] = $o; - } - - return $crop; - } - +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @subpackage Croppers + */ + +/* $Id: smart.class.php 113 2010-12-21 15:21:32Z joe.lencioni $ */ + +require_once 'slircropper.interface.php'; + +/** + * Smart SLIR cropper + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 09:21:32 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 113 $ + * @package SLIR + * @subpackage Croppers + */ +class SLIRCropperSmart implements SLIRCropper +{ + /** + * Determines if the top and bottom need to be cropped + * + * @since 2.0 + * @param SLIRImage $image + * @return boolean + */ + private function shouldCropTopAndBottom(SLIRImage $image) + { + if ($image->cropRatio() > $image->ratio()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Determines the optimal number of rows in from the top or left to crop + * the source image + * + * @since 2.0 + * @param SLIRImage $image + * @return integer|boolean + */ + private function cropSmartOffsetRows(SLIRImage $image) + { + // @todo Change this method to resize image, determine offset, and then extrapolate the actual offset based on the image size difference. Then we can cache the offset in APC (all just like we are doing for face detection) + + if ($this->shouldCropTopAndBottom($image)) + { + $length = $image->cropHeight; + $lengthB = $image->cropWidth; + $originalLength = $image->height; + } + else + { + $length = $image->cropWidth; + $lengthB = $image->cropHeight; + $originalLength = $image->width; + } + + // To smart crop an image, we need to calculate the difference between + // each pixel in each row and its adjacent pixels. Add these up to + // determine how interesting each row is. Based on how interesting each + // row is, we can determine whether or not to discard it. We start with + // the closest row and the farthest row and then move on from there. + + // All colors in the image will be stored in the global colors array. + // This array will also include information about each pixel's + // interestingness. + // + // For example (rough representation): + // + // $colors = array( + // x1 => array( + // x1y1 => array( + // 'lab' => array(l, a, b), + // 'dE' => array(TL, TC, TR, LC, LR, BL, BC, BR), + // 'i' => computedInterestingness + // ), + // x1y2 => array( ... ), + // ... + // ), + // x2 => array( ... ), + // ... + // ); + global $colors; + $colors = array(); + + // Offset will remember how far in from each side we are in the + // cropping game + $offset = array( + 'near' => 0, + 'far' => 0, + ); + + $rowsToCrop = $originalLength - $length; + + // $pixelStep will sacrifice accuracy for memory and speed. Essentially + // it acts as a spot-checker and scales with the size of the cropped area + $pixelStep = round(sqrt($rowsToCrop * $lengthB) / 10); + + // We won't save much speed if the pixelStep is between 4 and 1 because + // we still need to sample adjacent pixels + if ($pixelStep < 4) + { + $pixelStep = 1; + } + + $tolerance = 0.5; + $upperTol = 1 + $tolerance; + $lowerTol = 1 / $upperTol; + + // Fight the near and far rows. The stronger will remain standing. + $returningChampion = NULL; + $ratio = 1; + for ($rowsCropped = 0; $rowsCropped < $rowsToCrop; ++$rowsCropped) + { + $a = $this->rowInterestingness($image, $offset['near'], $pixelStep, $originalLength); + $b = $this->rowInterestingness($image, $originalLength - $offset['far'] - 1, $pixelStep, $originalLength); + + if ($a == 0 && $b == 0) + { + $ratio = 1; + } + else if ($b == 0) + { + $ratio = 1 + $a; + } + else + { + $ratio = $a / $b; + } + + if ($ratio > $upperTol) + { + ++$offset['far']; + + // Fightback. Winning side gets to go backwards through fallen rows + // to see if they are stronger + if ($returningChampion == 'near') + { + $offset['near'] -= ($offset['near'] > 0) ? 1 : 0; + } + else + { + $returningChampion = 'near'; + } + } + else if ($ratio < $lowerTol) + { + ++$offset['near']; + + if ($returningChampion == 'far') + { + $offset['far'] -= ($offset['far'] > 0) ? 1 : 0; + } + else + { + $returningChampion = 'far'; + } + } + else + { + // There is no strong winner, so discard rows from the side that + // has lost the fewest so far. Essentially this is a draw. + if ($offset['near'] > $offset['far']) + { + ++$offset['far']; + } + else // Discard near + { + ++$offset['near']; + } + + // No fightback for draws + $returningChampion = NULL; + } // if + + } // for + + // Bounceback for potentially important details on the edge. + // This may possibly be better if the winning side fights a hard final + // push multiple-rows-at-stake battle where it stands the chance to gain + // ground. + if ($ratio > (1 + ($tolerance * 1.25))) + { + $offset['near'] -= round($length * .03); + } + else if ($ratio < (1 / (1 + ($tolerance * 1.25)))) + { + $offset['near'] += round($length * .03); + } + + return min($rowsToCrop, max(0, $offset['near'])); + } + + /** + * Calculate the interestingness value of a row of pixels + * + * @since 2.0 + * @param SLIRImage $image + * @param integer $row + * @param integer $pixelStep Number of pixels to jump after each step when comparing interestingness + * @param integer $originalLength Number of rows in the original image + * @return float + */ + private function rowInterestingness(SLIRImage $image, $row, $pixelStep, $originalLength) + { + $interestingness = 0; + $max = 0; + + if ($this->shouldCropTopAndBottom($image)) + { + for ($totalPixels = 0; $totalPixels < $image->width; $totalPixels += $pixelStep) + { + $i = $this->pixelInterestingness($image, $totalPixels, $row); + + // Content at the very edge of an image tends to be less interesting than + // content toward the center, so we give it a little extra push away from the edge + //$i += min($row, $originalLength - $row, $originalLength * .04); + + $max = max($i, $max); + $interestingness += $i; + } + } + else + { + for ($totalPixels = 0; $totalPixels < $image->height; $totalPixels += $pixelStep) + { + $i = $this->pixelInterestingness($image, $row, $totalPixels); + + // Content at the very edge of an image tends to be less interesting than + // content toward the center, so we give it a little extra push away from the edge + //$i += min($row, $originalLength - $row, $originalLength * .04); + + $max = max($i, $max); + $interestingness += $i; + } + } + + return $interestingness + (($max - ($interestingness / ($totalPixels / $pixelStep))) * ($totalPixels / $pixelStep)); + } + + /** + * Get the interestingness value of a pixel + * + * @since 2.0 + * @param SLIRImage $image + * @param integer $x x-axis position of pixel to calculate + * @param integer $y y-axis position of pixel to calculate + * @return float + */ + private function pixelInterestingness(SLIRImage $image, $x, $y) + { + global $colors; + + if (!isset($colors[$x][$y]['i'])) + { + // Ensure this pixel's color information has already been loaded + $this->loadPixelInfo($image, $x, $y); + + // Calculate each neighboring pixel's Delta E in relation to this + // pixel + $this->calculateDeltas($image, $x, $y); + + // Calculate the interestingness of this pixel based on neighboring + // pixels' Delta E in relation to this pixel + $this->calculateInterestingness($x, $y); + } // if + + return $colors[$x][$y]['i']; + } + + /** + * Load the color information of the requested pixel into the $colors array + * + * @since 2.0 + * @param SLIRImage $image + * @param integer $x x-axis position of pixel to calculate + * @param integer $y y-axis position of pixel to calculate + * @return boolean + */ + private function loadPixelInfo(SLIRImage $image, $x, $y) + { + if ($x < 0 || $x >= $image->width + || $y < 0 || $y >= $image->height) + { + return FALSE; + } + + global $colors; + + if (!isset($colors[$x])) + { + $colors[$x] = array(); + } + + if (!isset($colors[$x][$y])) + { + $colors[$x][$y] = array(); + } + + if (!isset($colors[$x][$y]['i']) && !isset($colors[$x][$y]['lab'])) + { + $colors[$x][$y]['lab'] = $this->evaluateColor(imagecolorat($image->image, $x, $y)); + } + + return TRUE; + } + + /** + * Calculates each adjacent pixel's Delta E in relation to the pixel requested + * + * @since 2.0 + * @param SLIRImage $image + * @param integer $x x-axis position of pixel to calculate + * @param integer $y y-axis position of pixel to calculate + * @return boolean + */ + private function calculateDeltas(SLIRImage $image, $x, $y) + { + // Calculate each adjacent pixel's Delta E in relation to the current + // pixel (top left, top center, top right, center left, center right, + // bottom left, bottom center, and bottom right) + + global $colors; + + if (!isset($colors[$x][$y]['dE']['d-1-1'])) + { + $this->calculateDelta($image, $x, $y, -1, -1); + } + if (!isset($colors[$x][$y]['dE']['d0-1'])) + { + $this->calculateDelta($image, $x, $y, 0, -1); + } + if (!isset($colors[$x][$y]['dE']['d1-1'])) + { + $this->calculateDelta($image, $x, $y, 1, -1); + } + if (!isset($colors[$x][$y]['dE']['d-10'])) + { + $this->calculateDelta($image, $x, $y, -1, 0); + } + if (!isset($colors[$x][$y]['dE']['d10'])) + { + $this->calculateDelta($image, $x, $y, 1, 0); + } + if (!isset($colors[$x][$y]['dE']['d-11'])) + { + $this->calculateDelta($image, $x, $y, -1, 1); + } + if (!isset($colors[$x][$y]['dE']['d01'])) + { + $this->calculateDelta($image, $x, $y, 0, 1); + } + if (!isset($colors[$x][$y]['dE']['d11'])) + { + $this->calculateDelta($image, $x, $y, 1, 1); + } + + return TRUE; + } + + /** + * Calculates and stores requested pixel's Delta E in relation to comparison pixel + * + * @since 2.0 + * @param SLIRImage $image + * @param integer $x1 x-axis position of pixel to calculate + * @param integer $y1 y-axis position of pixel to calculate + * @param integer $xMove number of pixels to move on the x-axis to find comparison pixel + * @param integer $yMove number of pixels to move on the y-axis to find comparison pixel + * @return boolean + */ + private function calculateDelta(SLIRImage $image, $x1, $y1, $xMove, $yMove) + { + $x2 = $x1 + $xMove; + $y2 = $y1 + $yMove; + + // Pixel is outside of the image, so we cant't calculate the Delta E + if ($x2 < 0 || $x2 >= $image->width + || $y2 < 0 || $y2 >= $image->height) + { + return NULL; + } + + global $colors; + + if (!isset($colors[$x1][$y1]['lab'])) + { + $this->loadPixelInfo($image, $x1, $y1); + } + if (!isset($colors[$x2][$y2]['lab'])) + { + $this->loadPixelInfo($image, $x2, $y2); + } + + $delta = $this->deltaE($colors[$x1][$y1]['lab'], $colors[$x2][$y2]['lab']); + + $colors[$x1][$y1]['dE']["d$xMove$yMove"] = $delta; + + $x2Move = $xMove * -1; + $y2Move = $yMove * -1; + $colors[$x2][$y2]['dE']["d$x2Move$y2Move"] =& $colors[$x1][$y1]['dE']["d$xMove$yMove"]; + + return TRUE; + } + + /** + * Calculates and stores a pixel's overall interestingness value + * + * @since 2.0 + * @param integer $x x-axis position of pixel to calculate + * @param integer $y y-axis position of pixel to calculate + * @return boolean + */ + private function calculateInterestingness($x, $y) + { + global $colors; + + // The interestingness is the average of the pixel's Delta E values + $colors[$x][$y]['i'] = array_sum($colors[$x][$y]['dE']) + / count(array_filter($colors[$x][$y]['dE'], 'is_numeric')); + + return TRUE; + } + + /** + * @since 2.0 + * @param integer $int + * @return array + */ + private function evaluateColor($int) + { + $rgb = $this->colorIndexToRGB($int); + $xyz = $this->RGBtoXYZ($rgb); + $lab = $this->XYZtoHunterLab($xyz); + + return $lab; + } + + /** + * @since 2.0 + * @param integer $int + * @return array + */ + private function colorIndexToRGB($int) + { + $a = (255 - (($int >> 24) & 0xFF)) / 255; + $r = (($int >> 16) & 0xFF) * $a; + $g = (($int >> 8) & 0xFF) * $a; + $b = ($int & 0xFF) * $a; + return array('r' => $r, 'g' => $g, 'b' => $b); + } + + /** + * @since 2.0 + * @param array $rgb + * @return array XYZ + * @link http://easyrgb.com/index.php?X=MATH&H=02#text2 + */ + private function RGBtoXYZ($rgb) + { + $r = $rgb['r'] / 255; + $g = $rgb['g'] / 255; + $b = $rgb['b'] / 255; + + if ($r > 0.04045) + { + $r = pow((($r + 0.055) / 1.055), 2.4); + } + else + { + $r = $r / 12.92; + } + + if ($g > 0.04045) + { + $g = pow((($g + 0.055) / 1.055), 2.4); + } + else + { + $g = $g / 12.92; + } + + if ($b > 0.04045) + { + $b = pow((($b + 0.055) / 1.055), 2.4); + } + else + { + $b = $b / 12.92; + } + + $r *= 100; + $g *= 100; + $b *= 100; + + //Observer. = 2Β°, Illuminant = D65 + $x = $r * 0.4124 + $g * 0.3576 + $b * 0.1805; + $y = $r * 0.2126 + $g * 0.7152 + $b * 0.0722; + $z = $r * 0.0193 + $g * 0.1192 + $b * 0.9505; + + return array('x' => $x, 'y' => $y, 'z' => $z); + } + + /** + * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5 + */ + private function XYZtoHunterLab($xyz) + { + if ($xyz['y'] == 0) + { + return array('l' => 0, 'a' => 0, 'b' => 0); + } + + $l = 10 * sqrt($xyz['y']); + $a = 17.5 * ( ( ( 1.02 * $xyz['x'] ) - $xyz['y']) / sqrt( $xyz['y'] ) ); + $b = 7 * ( ( $xyz['y'] - ( 0.847 * $xyz['z'] ) ) / sqrt( $xyz['y'] ) ); + + return array('l' => $l, 'a' => $a, 'b' => $b); + } + + /** + * Converts a color from RGB colorspace to CIE-L*ab colorspace + * @since 2.0 + * @param array $xyz + * @return array LAB + * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5 + */ + private function XYZtoCIELAB($xyz) + { + $refX = 100; + $refY = 100; + $refZ = 100; + + $X = $xyz['x'] / $refX; + $Y = $xyz['y'] / $refY; + $Z = $xyz['z'] / $refZ; + + if ( $X > 0.008856 ) + { + $X = pow($X, 1/3); + } + else + { + $X = ( 7.787 * $X ) + ( 16 / 116 ); + } + + if ( $Y > 0.008856 ) + { + $Y = pow($Y, 1/3); + } + else + { + $Y = ( 7.787 * $Y ) + ( 16 / 116 ); + } + + if ( $Z > 0.008856 ) + { + $Z = pow($Z, 1/3); + } + else + { + $Z = ( 7.787 * $Z ) + ( 16 / 116 ); + } + + $l = ( 116 * $Y ) - 16; + $a = 500 * ( $X - $Y ); + $b = 200 * ( $Y - $Z ); + + return array('l' => $l, 'a' => $a, 'b' => $b); + } + + private function deltaE($lab1, $lab2) + { + return sqrt( ( pow( $lab1['l'] - $lab2['l'], 2 ) ) + + ( pow( $lab1['a'] - $lab2['a'], 2 ) ) + + ( pow( $lab1['b'] - $lab2['b'], 2 ) ) ); + } + + /** + * Compute the Delta E 2000 value of two colors in the LAB colorspace + * + * @link http://en.wikipedia.org/wiki/Color_difference#CIEDE2000 + * @link http://easyrgb.com/index.php?X=DELT&H=05#text5 + * @since 2.0 + * @param array $lab1 LAB color array + * @param array $lab2 LAB color array + * @return float + */ + private function deltaE2000($lab1, $lab2) + { + $weightL = 1; // Lightness + $weightC = 1; // Chroma + $weightH = 1; // Hue + + $xC1 = sqrt( $lab1['a'] * $lab1['a'] + $lab1['b'] * $lab1['b'] ); + $xC2 = sqrt( $lab2['a'] * $lab2['a'] + $lab2['b'] * $lab2['b'] ); + $xCX = ( $xC1 + $xC2 ) / 2; + $xGX = 0.5 * ( 1 - sqrt( ( pow($xCX, 7) ) / ( ( pow($xCX, 7) ) + ( pow(25, 7) ) ) ) ); + $xNN = ( 1 + $xGX ) * $lab1['a']; + $xC1 = sqrt( $xNN * $xNN + $lab1['b'] * $lab1['b'] ); + $xH1 = $this->LABtoHue( $xNN, $lab1['b'] ); + $xNN = ( 1 + $xGX ) * $lab2['a']; + $xC2 = sqrt( $xNN * $xNN + $lab2['b'] * $lab2['b'] ); + $xH2 = $this->LABtoHue( $xNN, $lab2['b'] ); + $xDL = $lab2['l'] - $lab1['l']; + $xDC = $xC2 - $xC1; + + if ( ( $xC1 * $xC2 ) == 0 ) + { + $xDH = 0; + } + else + { + $xNN = round( $xH2 - $xH1, 12 ); + if ( abs( $xNN ) <= 180 ) + { + $xDH = $xH2 - $xH1; + } + else + { + if ( $xNN > 180 ) + { + $xDH = $xH2 - $xH1 - 360; + } + else + { + $xDH = $xH2 - $xH1 + 360; + } + } // if + } // if + + $xDH = 2 * sqrt( $xC1 * $xC2 ) * sin( rad2deg( $xDH / 2 ) ); + $xLX = ( $lab1['l'] + $lab2['l'] ) / 2; + $xCY = ( $xC1 + $xC2 ) / 2; + + if ( ( $xC1 * $xC2 ) == 0 ) + { + $xHX = $xH1 + $xH2; + } + else + { + $xNN = abs( round( $xH1 - $xH2, 12 ) ); + if ( $xNN > 180 ) + { + if ( ( $xH2 + $xH1 ) < 360 ) + { + $xHX = $xH1 + $xH2 + 360; + } + else + { + $xHX = $xH1 + $xH2 - 360; + } + } + else + { + $xHX = $xH1 + $xH2; + } // if + $xHX /= 2; + } // if + + $xTX = 1 - 0.17 * cos( rad2deg( $xHX - 30 ) ) + + 0.24 * cos( rad2deg( 2 * $xHX ) ) + + 0.32 * cos( rad2deg( 3 * $xHX + 6 ) ) + - 0.20 * cos( rad2deg( 4 * $xHX - 63 ) ); + + $xPH = 30 * exp( - ( ( $xHX - 275 ) / 25 ) * ( ( $xHX - 275 ) / 25 ) ); + $xRC = 2 * sqrt( ( pow($xCY, 7) ) / ( ( pow($xCY, 7) ) + ( pow(25, 7) ) ) ); + $xSL = 1 + ( ( 0.015 * ( ( $xLX - 50 ) * ( $xLX - 50 ) ) ) + / sqrt( 20 + ( ( $xLX - 50 ) * ( $xLX - 50 ) ) ) ); + $xSC = 1 + 0.045 * $xCY; + $xSH = 1 + 0.015 * $xCY * $xTX; + $xRT = - sin( rad2deg( 2 * $xPH ) ) * $xRC; + $xDL = $xDL / $weightL * $xSL; + $xDC = $xDC / $weightC * $xSC; + $xDH = $xDH / $weightH * $xSH; + + $delta = sqrt( pow($xDL, 2) + pow($xDC, 2) + pow($xDH, 2) + $xRT * $xDC * $xDH ); + return (is_nan($delta)) ? 1 : $delta / 100; + } + + /** + * Compute the Delta CMC value of two colors in the LAB colorspace + * + * @since 2.0 + * @param array $lab1 LAB color array + * @param array $lab2 LAB color array + * @return float + * @link http://easyrgb.com/index.php?X=DELT&H=06#text6 + */ + private function deltaCMC($lab1, $lab2) + { + // if $weightL is 2 and $weightC is 1, it means that the lightness + // will contribute half as much importance to the delta as the chroma + $weightL = 2; // Lightness + $weightC = 1; // Chroma + + $xC1 = sqrt( ( pow($lab1['a'], 2) ) + ( pow($lab1['b'], 2) ) ); + $xC2 = sqrt( ( pow($lab2['a'], 2) ) + ( pow($lab2['b'], 2) ) ); + $xff = sqrt( ( pow($xC1, 4) ) / ( ( pow($xC1, 4) ) + 1900 ) ); + $xH1 = $this->LABtoHue( $lab1['a'], $lab1['b'] ); + + if ( $xH1 < 164 || $xH1 > 345 ) + { + $xTT = 0.36 + abs( 0.4 * cos( deg2rad( 35 + $xH1 ) ) ); + } + else + { + $xTT = 0.56 + abs( 0.2 * cos( deg2rad( 168 + $xH1 ) ) ); + } + + if ( $lab1['l'] < 16 ) + { + $xSL = 0.511; + } + else + { + $xSL = ( 0.040975 * $lab1['l'] ) / ( 1 + ( 0.01765 * $lab1['l'] ) ); + } + + $xSC = ( ( 0.0638 * $xC1 ) / ( 1 + ( 0.0131 * $xC1 ) ) ) + 0.638; + $xSH = ( ( $xff * $xTT ) + 1 - $xff ) * $xSC; + $xDH = sqrt( pow( $lab2['a'] - $lab1['a'], 2 ) + pow( $lab2['b'] - $lab1['b'], 2 ) - pow( $xC2 - $xC1, 2 ) ); + $xSL = ( $lab2['l'] - $lab1['l'] ) / $weightL * $xSL; + $xSC = ( $xC2 - $xC1 ) / $weightC * $xSC; + $xSH = $xDH / $xSH; + + $delta = sqrt( pow($xSL, 2) + pow($xSC, 2) + pow($xSH, 2) ); + return (is_nan($delta)) ? 1 : $delta; + } + + /** + * @since 2.0 + * @param integer $a + * @param integer $b + * @return CIE-HΒ° value + */ + private function LABtoHue($a, $b) + { + $bias = 0; + + if ($a >= 0 && $b == 0) return 0; + if ($a < 0 && $b == 0) return 180; + if ($a == 0 && $b > 0) return 90; + if ($a == 0 && $b < 0) return 270; + if ($a > 0 && $b > 0) $bias = 0; + if ($a < 0 ) $bias = 180; + if ($a > 0 && $b < 0) $bias = 360; + + return (rad2deg(atan($b / $a)) + $bias); + } + + /** + * Calculates the crop offset using an algorithm that tries to determine + * the most interesting portion of the image to keep. + * + * @since 2.0 + * @param SLIRImage $image + * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped + */ + public function getCrop(SLIRImage $image) + { + // Try contrast detection + $o = $this->cropSmartOffsetRows($image); + + $crop = array( + 'x' => 0, + 'y' => 0, + ); + + if ($o === FALSE) + { + return TRUE; + } + else if ($this->shouldCropTopAndBottom($image)) + { + $crop['y'] = $o; + } + else + { + $crop['x'] = $o; + } + + return $crop; + } + } \ No newline at end of file diff --git a/app/parsers/slir/croppers/topcentered.class.php b/app/parsers/slir/croppers/topcentered.class.php index 55c40ffa..907ced34 100644 --- a/app/parsers/slir/croppers/topcentered.class.php +++ b/app/parsers/slir/croppers/topcentered.class.php @@ -1,55 +1,55 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @subpackage Croppers - */ - -/* $Id: topcentered.class.php 119 2010-12-21 16:04:47Z joe.lencioni $ */ - -require_once 'centered.class.php'; - -/** - * Top/centered SLIR cropper - * - * Calculates the crop offset anchored in the top of the image if the top and bottom are being cropped, or the center of the image if the left and right are being cropped - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 10:04:47 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 119 $ - * @package SLIR - * @subpackage Croppers - */ -class SLIRCropperTopcentered extends SLIRCropperCentered -{ - /** - * @since 2.0 - * @param SLIRImage $image - * @return integer - */ - public function getCropY(SLIRImage $image) - { - return 0; - } +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @subpackage Croppers + */ + +/* $Id: topcentered.class.php 119 2010-12-21 16:04:47Z joe.lencioni $ */ + +require_once 'centered.class.php'; + +/** + * Top/centered SLIR cropper + * + * Calculates the crop offset anchored in the top of the image if the top and bottom are being cropped, or the center of the image if the left and right are being cropped + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 10:04:47 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 119 $ + * @package SLIR + * @subpackage Croppers + */ +class SLIRCropperTopcentered extends SLIRCropperCentered +{ + /** + * @since 2.0 + * @param SLIRImage $image + * @return integer + */ + public function getCropY(SLIRImage $image) + { + return 0; + } } \ No newline at end of file diff --git a/app/parsers/slir/facedetector/facedetector.class.php b/app/parsers/slir/facedetector/facedetector.class.php index 3667a95e..2233698e 100644 --- a/app/parsers/slir/facedetector/facedetector.class.php +++ b/app/parsers/slir/facedetector/facedetector.class.php @@ -1,547 +1,547 @@ -. - * - * @copyright Copyright Β© 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: facedetector.class.php 112 2010-12-20 21:13:56Z joe.lencioni $ */ - -/** - * Face detector class - * - * This code was originally written by Liu Liu in JavaScript - * and was ported to PHP by Joe Lencioni - * - * @link https://github.com/liuliu/ccv - * - * @since 2.0 - * @author Liu Liu - * @author Joe Lencioni - * $Date: 2010-12-20 15:13:56 -0600 (Mon, 20 Dec 2010) $ - * @version $Revision: 112 $ - * @package SLIR - */ -class SLIRFaceDetector -{ - - /** - * @return void - */ - public function __construct() - { - } - - /** - * @param array $seq - * @return array - */ - protected function array_group($seq) - { - $i = NULL; - $j = NULL; - $node = array(); // array_fill(0, count($seq), NULL); - - for ($i = 0; $i < count($seq); ++$i) - { - $node[$i] = array( - 'parent' => -1, - 'element' => $seq[$i], - 'rank' => 0, - ); - } - - for ($i = 0; $i < count($seq); ++$i) - { - if (!$node[$i]['element']) - { - continue; - } - - $root = $i; - while ($node[$root]['parent'] != -1) - { - $root = $node[$root]['parent']; - } - - for ($j = 0; $j < count($seq); ++$j) - { - if ($i != $j && $node[$j]['element'] && $this->gfunc($node[$i]['element'], $node[$j]['element'])) - { - $root2 = $j; - - while ($node[$root2]['parent'] != -1) - { - $root2 = $node[$root2]['parent']; - } - - if ($root2 != $root) - { - if ($node[$root]['rank'] > $node[$root2]['rank']) - { - $node[$root2]['parent'] = $root; - } - else - { - $node[$root]['parent'] = $root2; - if ($node[$root]['rank'] == $node[$root2]['rank']) - { - ++$node[$root2]['rank']; - } - $root = $root2; - } - - // Compress path from node2 to the root: - $temp = NULL; - $node2 = $j; - - while ($node[$node2]['parent'] != -1) - { - $temp = $node2; - $node2 = $node[$node2]['parent']; - $node[$temp]['parent'] = $root; - } - - // Compress path from node to the root: - $node2 = $i; - while ($node[$node2]['parent'] != -1) - { - $temp = $node2; - $node2 = $node[$node2]['parent']; - $node[$temp]['parent'] = $root; - } - } // if - } - } // for - } // for - - $idx = array(); //array_fill(0, count($seq), NULL); - $class_idx = 0; - - for ($i = 0; $i < count($seq); ++$i) - { - $j = -1; - $node1 = $i; - if ($node[$node1]['element']) - { - while ($node[$node1]['parent'] != -1) - { - $node1 = $node[$node1]['parent']; - } - - if ($node[$node1]['rank'] >= 0) - { - // JS: ~class_idx++; - $node[$node1]['rank'] = (0 - $class_idx) + 1; - ++$class_idx; - } - - $j = (0 - $node[$node1]['rank']) + 1; // JS: ~node[node1].rank; - } - $idx[$i] = $j; - } - - return array('index' => $idx, 'cat' => $class_idx); - } - - /** - * @param array $r1 - * @param array $r2 - * @return boolean - */ - protected function gfunc($r1, $r2) - { - $distance = floor($r1['width'] * 0.25 + 0.5); - return $r2['x'] <= $r1['x'] + $distance && - $r2['x'] >= $r1['x'] - $distance && - $r2['y'] <= $r1['y'] + $distance && - $r2['y'] >= $r1['y'] - $distance && - $r2['width'] <= floor($r1['width'] * 1.5 + 0.5) && - floor($r2['width'] * 1.5 + 0.5) >= $r1['width']; - } - - /** - * @param resource $canvas GD Image resource - * @param array $cascade - * @param integer $interval - * @param integer $min_neighbors - * @return array - */ - public function detect_objects($canvas, $cascade, $interval, $min_neighbors) - { - $scale = pow(2, 1 / ($interval + 1)); - $next = $interval + 1; - $scale_upto = floor(log(min($cascade['width'], $cascade['height'])) / log($scale)); - $pyr = array_fill(0, ($scale_upto + $next * 2) * 4, NULL); - $pyr[0] = $canvas; - $i = $j = $k = $x = $y = $q = NULL; - - $baseWidth = imagesx($pyr[0]); - $baseHeight = imagesy($pyr[0]); - - for ($i = 1; $i <= $interval; ++$i) - { - $newWidth = floor($baseWidth / pow($scale, $i)); - $newHeight = floor($baseHeight / pow($scale, $i)); - $pyr[$i * 4] = imagecreatetruecolor($newWidth, $newHeight); - imagecopyresampled($pyr[$i * 4], $pyr[0], 0, 0, 0, 0, $newWidth, $newHeight, $baseWidth, $baseHeight); - } - - for ($i = $next; $i < $scale_upto + $next * 2; ++$i) - { - $baseImage = $pyr[$i * 4 - $next * 4]; - $baseWidth = imagesx($baseImage); - $baseHeight = imagesy($baseImage); - $newWidth = max(1, floor($baseWidth / 2)); - $newHeight = max(1, floor($baseHeight / 2)); - $pyr[$i * 4] = imagecreatetruecolor($newWidth, $newHeight); - imagecopyresampled($pyr[$i * 4], $baseImage, 0, 0, 0, 0, $newWidth, $newHeight, $baseWidth, $baseHeight); - } - - for ($i = $next * 2; $i < $scale_upto + $next * 2; ++$i) - { - $baseImage = $pyr[$i * 4 - $next * 4]; - $baseWidth = imagesx($baseImage); - $baseHeight = imagesy($baseImage); - $newWidth = max(1, floor($baseWidth / 2)); - $newHeight = max(1, floor($baseHeight / 2)); - - $pyr[$i * 4 + 1] = imagecreatetruecolor($newWidth, $newHeight); - imagecopyresampled($pyr[$i * 4 + 1], $baseImage, 0, 0, 1, 0, $newWidth-2, $newHeight, $baseWidth-1, $baseHeight); - - $pyr[$i * 4 + 2] = imagecreatetruecolor($newWidth, $newHeight); - imagecopyresampled($pyr[$i * 4 + 2], $baseImage, 0, 0, 0, 1, $newWidth, $newHeight-2, $baseWidth, $baseHeight-1); - - $pyr[$i * 4 + 3] = imagecreatetruecolor($newWidth, $newHeight); - imagecopyresampled($pyr[$i * 4 + 3], $baseImage, 0, 0, 1, 1, $newWidth-2, $newHeight-2, $baseWidth-1, $baseHeight-1); - } - - for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) - { - $cascade['stage_classifier'][$j]['orig_feature'] = $cascade['stage_classifier'][$j]['feature']; - } - - $scale_x = 1; - $scale_y = 1; - $dx = array(0, 1, 0, 1); - $dy = array(0, 0, 1, 1); - $seq = array(); - - for ($i = 0; $i < $scale_upto; ++$i) - { - $qw = imagesx($pyr[$i * 4 + $next * 8]) - floor($cascade['width'] / 4); - $qh = imagesy($pyr[$i * 4 + $next * 8]) - floor($cascade['height'] / 4); - $step = array( - imagesx($pyr[$i * 4]) * 4, - imagesx($pyr[$i * 4 + $next * 4]) * 4, - imagesx($pyr[$i * 4 + $next * 8]) * 4, - ); - - $paddings = array( - imagesx($pyr[$i * 4]) * 16 - $qw * 16, - imagesx($pyr[$i * 4 + $next * 4]) * 8 - $qw * 8, - imagesx($pyr[$i * 4 + $next * 8]) * 4 - $qw * 4, - ); - - for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) - { - $orig_feature = $cascade['stage_classifier'][$j]['orig_feature']; - $feature = array_fill(0, $cascade['stage_classifier'][$j]['count'], NULL); - - for ($k = 0; $k < $cascade['stage_classifier'][$j]['count']; ++$k) - { - $feature[$k] = array( - 'size' => $orig_feature[$k]['size'], - 'px' => array_fill(0, $orig_feature[$k]['size'], NULL), - 'pz' => array_fill(0, $orig_feature[$k]['size'], NULL), - 'nx' => array_fill(0, $orig_feature[$k]['size'], NULL), - 'nz' => array_fill(0, $orig_feature[$k]['size'], NULL), - ); - - for ($q = 0; $q < $orig_feature[$k]['size']; ++$q) - { - if ($orig_feature[$k]['pz'][$q] === -1 || $orig_feature[$k]['nz'][$q] === -1) - { - continue; - } - - $feature[$k]['px'][$q] = $orig_feature[$k]['px'][$q] * 4 + $orig_feature[$k]['py'][$q] * $step[$orig_feature[$k]['pz'][$q]]; - $feature[$k]['pz'][$q] = $orig_feature[$k]['pz'][$q]; - $feature[$k]['nx'][$q] = $orig_feature[$k]['nx'][$q] * 4 + $orig_feature[$k]['ny'][$q] * $step[$orig_feature[$k]['nz'][$q]]; - $feature[$k]['nz'][$q] = $orig_feature[$k]['nz'][$q]; - } - } - - $cascade['stage_classifier'][$j]['feature'] = $feature; - } - - - for ($q = 0; $q < 4; ++$q) - { - $u8 = array( - $pyr[$i * 4], - $pyr[$i * 4 + $next * 4], - $pyr[$i * 4 + $next * 8 + $q], - ); - $u8w = array( - imagesx($u8[0]), - imagesx($u8[1]), - imagesx($u8[2]), - ); - $u8o = array( - $dx[$q] * 8 + $dy[$q] * $u8w[0] * 8, - $dx[$q] * 4 + $dy[$q] * $u8w[1] * 4, - 0, - ); - - // Color cache saves time - $colors = array(array(), array(), array()); - - for ($y = 0; $y < $qh; ++$y) - { - for ($x = 0; $x < $qw; ++$x) - { - $sum = 0; - $flag = TRUE; - - for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) - { - $sum = 0; - $alpha = $cascade['stage_classifier'][$j]['alpha']; - $feature = $cascade['stage_classifier'][$j]['feature']; - - for ($k = 0; $k < $cascade['stage_classifier'][$j]['count']; ++$k) - { - $feature_k = $feature[$k]; - $p = NULL; - - $pos = ($u8o[$feature_k['pz'][0]] + $feature_k['px'][0]) / 4; - - if (!isset($colors[$feature_k['pz'][0]][$pos])) - { - $posx = $pos % $u8w[$feature_k['pz'][0]]; - $posy = floor($pos / $u8w[$feature_k['pz'][0]]); - $colors[$feature_k['pz'][0]][$pos] = imagecolorat($u8[$feature_k['pz'][0]], $posx, $posy); - } - $pmin = $colors[$feature_k['pz'][0]][$pos]; - - - $n = NULL; - $pos = ($u8o[$feature_k['nz'][0]] + $feature_k['nx'][0]) / 4; - - if (!isset($colors[$feature_k['nz'][0]][$pos])) - { - $posx = $pos % $u8w[$feature_k['nz'][0]]; - $posy = floor($pos / $u8w[$feature_k['nz'][0]]); - $colors[$feature_k['nz'][0]][$pos] = imagecolorat($u8[$feature_k['nz'][0]], $posx, $posy); - } - $nmax = $colors[$feature_k['nz'][0]][$pos]; - - if ($pmin <= $nmax) - { - $sum += $alpha[$k * 2]; - } - else - { - $f = NULL; - $shortcut = TRUE; - - for ($f = 0; $f < $feature_k['size']; ++$f) - { - if ($feature_k['pz'][$f] >= 0 && $feature_k['pz'][$f] !== NULL) - { - $pos = ($u8o[$feature_k['pz'][$f]] + $feature_k['px'][$f]) / 4; - if (!isset($colors[$feature_k['pz'][$f]][$pos])) - { - $posx = $pos % $u8w[$feature_k['pz'][$f]]; - $posy = floor($pos / $u8w[$feature_k['pz'][$f]]); - $colors[$feature_k['pz'][$f]][$pos] = imagecolorat($u8[$feature_k['pz'][$f]], $posx, $posy); - } - $p = $colors[$feature_k['pz'][$f]][$pos]; - - if ($p < $pmin) - { - if ($p <= $nmax) - { - $shortcut = FALSE; - break; - } - $pmin = $p; - } - } - - if ($feature_k['nz'][$f] >= 0 && $feature_k['nz'][$f]) - { - $pos = ($u8o[$feature_k['nz'][$f]] + $feature_k['nx'][$f]) / 4; - - if (!isset($colors[$feature_k['nz'][$f]][$pos])) - { - $posx = $pos % $u8w[$feature_k['nz'][$f]]; - $posy = floor($pos / $u8w[$feature_k['nz'][$f]]); - $colors[$feature_k['nz'][$f]][$pos] = imagecolorat($u8[$feature_k['nz'][$f]], $posx, $posy); - } - $n = $colors[$feature_k['nz'][$f]][$pos]; - - if ($n > $nmax) - { - if ($pmin <= $n) - { - $shortcut = FALSE; - break; - } - $nmax = $n; - } - } - } - - $sum += ($shortcut) ? $alpha[$k * 2 + 1] : $alpha[$k * 2]; - } - } - - if ($sum < $cascade['stage_classifier'][$j]['threshold']) - { - $flag = FALSE; - break; - } - } - - if ($flag) - { - $seq[] = array( - 'x' => ($x * 4 + $dx[$q] * 2) * $scale_x, - 'y' => ($y * 4 + $dy[$q] * 2) * $scale_y, - 'width' => $cascade['width'] * $scale_x, - 'height' => $cascade['height'] * $scale_y, - 'neighbor' => 1, - 'confidence' => $sum, - ); - } - $u8o[0] += 16; - $u8o[1] += 8; - $u8o[2] += 4; - } - $u8o[0] += $paddings[0]; - $u8o[1] += $paddings[1]; - $u8o[2] += $paddings[2]; - } - } - $scale_x *= $scale; - $scale_y *= $scale; - - } // for scale_upto - - for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) - { - $cascade['stage_classifier'][$j]['feature'] = $cascade['stage_classifier'][$j]['orig_feature']; - } - - if (!($min_neighbors > 0)) - { - return $seq; - } - else - { - $result = $this->array_group($seq); - - $ncomp = $result['cat']; - $idx_seq = $result['index']; - $comps = array_fill(0, $ncomp + 1, array( - 'neighbors' => 0, - 'x' => 0, - 'y' => 0, - 'width' => 0, - 'height' => 0, - 'confidence' => 0, - )); - - - // count number of neighbors - for ($i = 0; $i < count($seq); ++$i) - { - $r1 = $seq[$i]; - $idx = $idx_seq[$i]; - - if ($comps[$idx]['neighbors'] == 0) - { - $comps[$idx]['confidence'] = $r1['confidence']; - } - - ++$comps[$idx]['neighbors']; - - $comps[$idx]['x'] += $r1['x']; - $comps[$idx]['y'] += $r1['y']; - $comps[$idx]['width'] += $r1['width']; - $comps[$idx]['height'] += $r1['height']; - $comps[$idx]['confidence'] = max($comps[$idx]['confidence'], $r1['confidence']); - } - - $seq2 = array(); - // calculate average bounding box - for ($i = 0; $i < $ncomp; ++$i) - { - $n = $comps[$i]['neighbors']; - if ($n >= $min_neighbors) - { - $seq2[] = array( - 'x' => ($comps[$i]['x'] * 2 + $n) / (2 * $n), - 'y' => ($comps[$i]['y'] * 2 + $n) / (2 * $n), - 'width' => ($comps[$i]['width'] * 2 + $n) / (2 * $n), - 'height' => ($comps[$i]['height'] * 2 + $n) / (2 * $n), - 'neighbors' => $comps[$i]['neighbors'], - 'confidence' => $comps[$i]['confidence'] - ); - } - } - - $result_seq = array(); - // filter out small face rectangles inside large face rectangles - for ($i = 0; $i < count($seq2); ++$i) - { - $r1 = $seq2[$i]; - $flag = TRUE; - - for ($j = 0; $j < count($seq2); ++$j) - { - $r2 = $seq2[$j]; - $distance = floor($r2['width'] * 0.25 + 0.5); - - if ($i != $j && - $r1['x'] >= $r2['x'] - $distance && - $r1['y'] >= $r2['y'] - $distance && - $r1['x'] + $r1['width'] <= $r2['x'] + $r2['width'] + $distance && - $r1['y'] + $r1['height'] <= $r2['y'] + $r2['height'] + $distance && - ($r2['neighbors'] > max(3, $r1['neighbors']) || $r1['neighbors'] < 3)) - { - $flag = FALSE; - break; - } - } - - if ($flag) - { - $result_seq[] = $r1; - } - } // for - return $result_seq; - } // else - } // detect_objects() +. + * + * @copyright Copyright Β© 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: facedetector.class.php 112 2010-12-20 21:13:56Z joe.lencioni $ */ + +/** + * Face detector class + * + * This code was originally written by Liu Liu in JavaScript + * and was ported to PHP by Joe Lencioni + * + * @link https://github.com/liuliu/ccv + * + * @since 2.0 + * @author Liu Liu + * @author Joe Lencioni + * $Date: 2010-12-20 15:13:56 -0600 (Mon, 20 Dec 2010) $ + * @version $Revision: 112 $ + * @package SLIR + */ +class SLIRFaceDetector +{ + + /** + * @return void + */ + public function __construct() + { + } + + /** + * @param array $seq + * @return array + */ + protected function array_group($seq) + { + $i = NULL; + $j = NULL; + $node = array(); // array_fill(0, count($seq), NULL); + + for ($i = 0; $i < count($seq); ++$i) + { + $node[$i] = array( + 'parent' => -1, + 'element' => $seq[$i], + 'rank' => 0, + ); + } + + for ($i = 0; $i < count($seq); ++$i) + { + if (!$node[$i]['element']) + { + continue; + } + + $root = $i; + while ($node[$root]['parent'] != -1) + { + $root = $node[$root]['parent']; + } + + for ($j = 0; $j < count($seq); ++$j) + { + if ($i != $j && $node[$j]['element'] && $this->gfunc($node[$i]['element'], $node[$j]['element'])) + { + $root2 = $j; + + while ($node[$root2]['parent'] != -1) + { + $root2 = $node[$root2]['parent']; + } + + if ($root2 != $root) + { + if ($node[$root]['rank'] > $node[$root2]['rank']) + { + $node[$root2]['parent'] = $root; + } + else + { + $node[$root]['parent'] = $root2; + if ($node[$root]['rank'] == $node[$root2]['rank']) + { + ++$node[$root2]['rank']; + } + $root = $root2; + } + + // Compress path from node2 to the root: + $temp = NULL; + $node2 = $j; + + while ($node[$node2]['parent'] != -1) + { + $temp = $node2; + $node2 = $node[$node2]['parent']; + $node[$temp]['parent'] = $root; + } + + // Compress path from node to the root: + $node2 = $i; + while ($node[$node2]['parent'] != -1) + { + $temp = $node2; + $node2 = $node[$node2]['parent']; + $node[$temp]['parent'] = $root; + } + } // if + } + } // for + } // for + + $idx = array(); //array_fill(0, count($seq), NULL); + $class_idx = 0; + + for ($i = 0; $i < count($seq); ++$i) + { + $j = -1; + $node1 = $i; + if ($node[$node1]['element']) + { + while ($node[$node1]['parent'] != -1) + { + $node1 = $node[$node1]['parent']; + } + + if ($node[$node1]['rank'] >= 0) + { + // JS: ~class_idx++; + $node[$node1]['rank'] = (0 - $class_idx) + 1; + ++$class_idx; + } + + $j = (0 - $node[$node1]['rank']) + 1; // JS: ~node[node1].rank; + } + $idx[$i] = $j; + } + + return array('index' => $idx, 'cat' => $class_idx); + } + + /** + * @param array $r1 + * @param array $r2 + * @return boolean + */ + protected function gfunc($r1, $r2) + { + $distance = floor($r1['width'] * 0.25 + 0.5); + return $r2['x'] <= $r1['x'] + $distance && + $r2['x'] >= $r1['x'] - $distance && + $r2['y'] <= $r1['y'] + $distance && + $r2['y'] >= $r1['y'] - $distance && + $r2['width'] <= floor($r1['width'] * 1.5 + 0.5) && + floor($r2['width'] * 1.5 + 0.5) >= $r1['width']; + } + + /** + * @param resource $canvas GD Image resource + * @param array $cascade + * @param integer $interval + * @param integer $min_neighbors + * @return array + */ + public function detect_objects($canvas, $cascade, $interval, $min_neighbors) + { + $scale = pow(2, 1 / ($interval + 1)); + $next = $interval + 1; + $scale_upto = floor(log(min($cascade['width'], $cascade['height'])) / log($scale)); + $pyr = array_fill(0, ($scale_upto + $next * 2) * 4, NULL); + $pyr[0] = $canvas; + $i = $j = $k = $x = $y = $q = NULL; + + $baseWidth = imagesx($pyr[0]); + $baseHeight = imagesy($pyr[0]); + + for ($i = 1; $i <= $interval; ++$i) + { + $newWidth = floor($baseWidth / pow($scale, $i)); + $newHeight = floor($baseHeight / pow($scale, $i)); + $pyr[$i * 4] = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($pyr[$i * 4], $pyr[0], 0, 0, 0, 0, $newWidth, $newHeight, $baseWidth, $baseHeight); + } + + for ($i = $next; $i < $scale_upto + $next * 2; ++$i) + { + $baseImage = $pyr[$i * 4 - $next * 4]; + $baseWidth = imagesx($baseImage); + $baseHeight = imagesy($baseImage); + $newWidth = max(1, floor($baseWidth / 2)); + $newHeight = max(1, floor($baseHeight / 2)); + $pyr[$i * 4] = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($pyr[$i * 4], $baseImage, 0, 0, 0, 0, $newWidth, $newHeight, $baseWidth, $baseHeight); + } + + for ($i = $next * 2; $i < $scale_upto + $next * 2; ++$i) + { + $baseImage = $pyr[$i * 4 - $next * 4]; + $baseWidth = imagesx($baseImage); + $baseHeight = imagesy($baseImage); + $newWidth = max(1, floor($baseWidth / 2)); + $newHeight = max(1, floor($baseHeight / 2)); + + $pyr[$i * 4 + 1] = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($pyr[$i * 4 + 1], $baseImage, 0, 0, 1, 0, $newWidth-2, $newHeight, $baseWidth-1, $baseHeight); + + $pyr[$i * 4 + 2] = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($pyr[$i * 4 + 2], $baseImage, 0, 0, 0, 1, $newWidth, $newHeight-2, $baseWidth, $baseHeight-1); + + $pyr[$i * 4 + 3] = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($pyr[$i * 4 + 3], $baseImage, 0, 0, 1, 1, $newWidth-2, $newHeight-2, $baseWidth-1, $baseHeight-1); + } + + for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) + { + $cascade['stage_classifier'][$j]['orig_feature'] = $cascade['stage_classifier'][$j]['feature']; + } + + $scale_x = 1; + $scale_y = 1; + $dx = array(0, 1, 0, 1); + $dy = array(0, 0, 1, 1); + $seq = array(); + + for ($i = 0; $i < $scale_upto; ++$i) + { + $qw = imagesx($pyr[$i * 4 + $next * 8]) - floor($cascade['width'] / 4); + $qh = imagesy($pyr[$i * 4 + $next * 8]) - floor($cascade['height'] / 4); + $step = array( + imagesx($pyr[$i * 4]) * 4, + imagesx($pyr[$i * 4 + $next * 4]) * 4, + imagesx($pyr[$i * 4 + $next * 8]) * 4, + ); + + $paddings = array( + imagesx($pyr[$i * 4]) * 16 - $qw * 16, + imagesx($pyr[$i * 4 + $next * 4]) * 8 - $qw * 8, + imagesx($pyr[$i * 4 + $next * 8]) * 4 - $qw * 4, + ); + + for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) + { + $orig_feature = $cascade['stage_classifier'][$j]['orig_feature']; + $feature = array_fill(0, $cascade['stage_classifier'][$j]['count'], NULL); + + for ($k = 0; $k < $cascade['stage_classifier'][$j]['count']; ++$k) + { + $feature[$k] = array( + 'size' => $orig_feature[$k]['size'], + 'px' => array_fill(0, $orig_feature[$k]['size'], NULL), + 'pz' => array_fill(0, $orig_feature[$k]['size'], NULL), + 'nx' => array_fill(0, $orig_feature[$k]['size'], NULL), + 'nz' => array_fill(0, $orig_feature[$k]['size'], NULL), + ); + + for ($q = 0; $q < $orig_feature[$k]['size']; ++$q) + { + if ($orig_feature[$k]['pz'][$q] === -1 || $orig_feature[$k]['nz'][$q] === -1) + { + continue; + } + + $feature[$k]['px'][$q] = $orig_feature[$k]['px'][$q] * 4 + $orig_feature[$k]['py'][$q] * $step[$orig_feature[$k]['pz'][$q]]; + $feature[$k]['pz'][$q] = $orig_feature[$k]['pz'][$q]; + $feature[$k]['nx'][$q] = $orig_feature[$k]['nx'][$q] * 4 + $orig_feature[$k]['ny'][$q] * $step[$orig_feature[$k]['nz'][$q]]; + $feature[$k]['nz'][$q] = $orig_feature[$k]['nz'][$q]; + } + } + + $cascade['stage_classifier'][$j]['feature'] = $feature; + } + + + for ($q = 0; $q < 4; ++$q) + { + $u8 = array( + $pyr[$i * 4], + $pyr[$i * 4 + $next * 4], + $pyr[$i * 4 + $next * 8 + $q], + ); + $u8w = array( + imagesx($u8[0]), + imagesx($u8[1]), + imagesx($u8[2]), + ); + $u8o = array( + $dx[$q] * 8 + $dy[$q] * $u8w[0] * 8, + $dx[$q] * 4 + $dy[$q] * $u8w[1] * 4, + 0, + ); + + // Color cache saves time + $colors = array(array(), array(), array()); + + for ($y = 0; $y < $qh; ++$y) + { + for ($x = 0; $x < $qw; ++$x) + { + $sum = 0; + $flag = TRUE; + + for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) + { + $sum = 0; + $alpha = $cascade['stage_classifier'][$j]['alpha']; + $feature = $cascade['stage_classifier'][$j]['feature']; + + for ($k = 0; $k < $cascade['stage_classifier'][$j]['count']; ++$k) + { + $feature_k = $feature[$k]; + $p = NULL; + + $pos = ($u8o[$feature_k['pz'][0]] + $feature_k['px'][0]) / 4; + + if (!isset($colors[$feature_k['pz'][0]][$pos])) + { + $posx = $pos % $u8w[$feature_k['pz'][0]]; + $posy = floor($pos / $u8w[$feature_k['pz'][0]]); + $colors[$feature_k['pz'][0]][$pos] = imagecolorat($u8[$feature_k['pz'][0]], $posx, $posy); + } + $pmin = $colors[$feature_k['pz'][0]][$pos]; + + + $n = NULL; + $pos = ($u8o[$feature_k['nz'][0]] + $feature_k['nx'][0]) / 4; + + if (!isset($colors[$feature_k['nz'][0]][$pos])) + { + $posx = $pos % $u8w[$feature_k['nz'][0]]; + $posy = floor($pos / $u8w[$feature_k['nz'][0]]); + $colors[$feature_k['nz'][0]][$pos] = imagecolorat($u8[$feature_k['nz'][0]], $posx, $posy); + } + $nmax = $colors[$feature_k['nz'][0]][$pos]; + + if ($pmin <= $nmax) + { + $sum += $alpha[$k * 2]; + } + else + { + $f = NULL; + $shortcut = TRUE; + + for ($f = 0; $f < $feature_k['size']; ++$f) + { + if ($feature_k['pz'][$f] >= 0 && $feature_k['pz'][$f] !== NULL) + { + $pos = ($u8o[$feature_k['pz'][$f]] + $feature_k['px'][$f]) / 4; + if (!isset($colors[$feature_k['pz'][$f]][$pos])) + { + $posx = $pos % $u8w[$feature_k['pz'][$f]]; + $posy = floor($pos / $u8w[$feature_k['pz'][$f]]); + $colors[$feature_k['pz'][$f]][$pos] = imagecolorat($u8[$feature_k['pz'][$f]], $posx, $posy); + } + $p = $colors[$feature_k['pz'][$f]][$pos]; + + if ($p < $pmin) + { + if ($p <= $nmax) + { + $shortcut = FALSE; + break; + } + $pmin = $p; + } + } + + if ($feature_k['nz'][$f] >= 0 && $feature_k['nz'][$f]) + { + $pos = ($u8o[$feature_k['nz'][$f]] + $feature_k['nx'][$f]) / 4; + + if (!isset($colors[$feature_k['nz'][$f]][$pos])) + { + $posx = $pos % $u8w[$feature_k['nz'][$f]]; + $posy = floor($pos / $u8w[$feature_k['nz'][$f]]); + $colors[$feature_k['nz'][$f]][$pos] = imagecolorat($u8[$feature_k['nz'][$f]], $posx, $posy); + } + $n = $colors[$feature_k['nz'][$f]][$pos]; + + if ($n > $nmax) + { + if ($pmin <= $n) + { + $shortcut = FALSE; + break; + } + $nmax = $n; + } + } + } + + $sum += ($shortcut) ? $alpha[$k * 2 + 1] : $alpha[$k * 2]; + } + } + + if ($sum < $cascade['stage_classifier'][$j]['threshold']) + { + $flag = FALSE; + break; + } + } + + if ($flag) + { + $seq[] = array( + 'x' => ($x * 4 + $dx[$q] * 2) * $scale_x, + 'y' => ($y * 4 + $dy[$q] * 2) * $scale_y, + 'width' => $cascade['width'] * $scale_x, + 'height' => $cascade['height'] * $scale_y, + 'neighbor' => 1, + 'confidence' => $sum, + ); + } + $u8o[0] += 16; + $u8o[1] += 8; + $u8o[2] += 4; + } + $u8o[0] += $paddings[0]; + $u8o[1] += $paddings[1]; + $u8o[2] += $paddings[2]; + } + } + $scale_x *= $scale; + $scale_y *= $scale; + + } // for scale_upto + + for ($j = 0; $j < count($cascade['stage_classifier']); ++$j) + { + $cascade['stage_classifier'][$j]['feature'] = $cascade['stage_classifier'][$j]['orig_feature']; + } + + if (!($min_neighbors > 0)) + { + return $seq; + } + else + { + $result = $this->array_group($seq); + + $ncomp = $result['cat']; + $idx_seq = $result['index']; + $comps = array_fill(0, $ncomp + 1, array( + 'neighbors' => 0, + 'x' => 0, + 'y' => 0, + 'width' => 0, + 'height' => 0, + 'confidence' => 0, + )); + + + // count number of neighbors + for ($i = 0; $i < count($seq); ++$i) + { + $r1 = $seq[$i]; + $idx = $idx_seq[$i]; + + if ($comps[$idx]['neighbors'] == 0) + { + $comps[$idx]['confidence'] = $r1['confidence']; + } + + ++$comps[$idx]['neighbors']; + + $comps[$idx]['x'] += $r1['x']; + $comps[$idx]['y'] += $r1['y']; + $comps[$idx]['width'] += $r1['width']; + $comps[$idx]['height'] += $r1['height']; + $comps[$idx]['confidence'] = max($comps[$idx]['confidence'], $r1['confidence']); + } + + $seq2 = array(); + // calculate average bounding box + for ($i = 0; $i < $ncomp; ++$i) + { + $n = $comps[$i]['neighbors']; + if ($n >= $min_neighbors) + { + $seq2[] = array( + 'x' => ($comps[$i]['x'] * 2 + $n) / (2 * $n), + 'y' => ($comps[$i]['y'] * 2 + $n) / (2 * $n), + 'width' => ($comps[$i]['width'] * 2 + $n) / (2 * $n), + 'height' => ($comps[$i]['height'] * 2 + $n) / (2 * $n), + 'neighbors' => $comps[$i]['neighbors'], + 'confidence' => $comps[$i]['confidence'] + ); + } + } + + $result_seq = array(); + // filter out small face rectangles inside large face rectangles + for ($i = 0; $i < count($seq2); ++$i) + { + $r1 = $seq2[$i]; + $flag = TRUE; + + for ($j = 0; $j < count($seq2); ++$j) + { + $r2 = $seq2[$j]; + $distance = floor($r2['width'] * 0.25 + 0.5); + + if ($i != $j && + $r1['x'] >= $r2['x'] - $distance && + $r1['y'] >= $r2['y'] - $distance && + $r1['x'] + $r1['width'] <= $r2['x'] + $r2['width'] + $distance && + $r1['y'] + $r1['height'] <= $r2['y'] + $r2['height'] + $distance && + ($r2['neighbors'] > max(3, $r1['neighbors']) || $r1['neighbors'] < 3)) + { + $flag = FALSE; + break; + } + } + + if ($flag) + { + $result_seq[] = $r1; + } + } // for + return $result_seq; + } // else + } // detect_objects() } \ No newline at end of file diff --git a/app/parsers/slir/index.php b/app/parsers/slir/index.php index 371deabc..e4a2946c 100644 --- a/app/parsers/slir/index.php +++ b/app/parsers/slir/index.php @@ -1,37 +1,37 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - * @date $Date: 2010-11-11 12:36:47 -0600 (Thu, 11 Nov 2010) $ - * @version $Revision: 107 $ - */ - - /* $Id: index.php 107 2010-11-11 18:36:47Z joe.lencioni $ */ - -// define('SLIR_CONFIG_FILENAME', 'slir-config-alternate.php'); -function __autoload($className) -{ - require_once strtolower($className) . '.class.php'; -} - +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + * @date $Date: 2010-11-11 12:36:47 -0600 (Thu, 11 Nov 2010) $ + * @version $Revision: 107 $ + */ + + /* $Id: index.php 107 2010-11-11 18:36:47Z joe.lencioni $ */ + +// define('SLIR_CONFIG_FILENAME', 'slir-config-alternate.php'); +function __autoload($className) +{ + require_once strtolower($className) . '.class.php'; +} + new SLIR(); \ No newline at end of file diff --git a/app/parsers/slir/slir.class.php b/app/parsers/slir/slir.class.php index 9fc4f52a..7f37f974 100644 --- a/app/parsers/slir/slir.class.php +++ b/app/parsers/slir/slir.class.php @@ -1,1508 +1,1508 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slir.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ - -/** - * SLIR (Smart Lencioni Image Resizer) - * Resizes images, intelligently sharpens, crops based on width:height ratios, - * color fills transparent GIFs and PNGs, and caches variations for optimal - * performance. - * - * I love to hear when my work is being used, so if you decide to use this, - * feel encouraged to send me an email. I would appreciate it if you would - * include a link on your site back to Shifting Pixel (either the SLIR page or - * shiftingpixel.com), but don’t worry about including a big link on each page - * if you don’t want to–one will do just nicely. Feel free to contact me to - * discuss any specifics (joe@shiftingpixel.com). - * - * REQUIREMENTS: - * - PHP 5.1.0+ - * - GD - * - * RECOMMENDED: - * - mod_rewrite - * - * USAGE: - * To use, place an img tag with the src pointing to the path of SLIR (typically - * "/slir/") followed by the parameters, followed by the path to the source - * image to resize. All parameters follow the pattern of a one-letter code and - * then the parameter value: - * - Maximum width = w - * - Maximum height = h - * - Crop ratio = c - * - Quality = q - * - Background fill color = b - * - Progressive = p - * - * Note: filenames that include special characters must be URL-encoded (e.g. - * plus sign, +, should be encoded as %2B) in order for SLIR to recognize them - * properly. This can be accomplished by passing your filenames through PHP's - * rawurlencode() or urlencode() function. - * - * EXAMPLES: - * - * Resizing a JPEG to a max width of 100 pixels and a max height of 100 pixels: - * Don't forget your alt
- * text - * - * Resizing and cropping a JPEG into a square: - * Don't forget
- * your alt text - * - * Resizing a JPEG without interlacing (for use in Flash): - * Don't forget your alt
- * text - * - * Matting a PNG with #990000: - * Don't forget your alt
- * text - * - * Without mod_rewrite (not recommended) - * Don't forget your alt text - * - * @author Joe Lencioni - * $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ - * @version $Revision: 129 $ - * @package SLIR - * - * @uses PEL - * - * @todo lock files when writing? - * @todo Prevent SLIR from calling itself - * @todo Percentage resizing? - * @todo Animated GIF resizing? - * @todo Seam carving? - * @todo Crop zoom? - * @todo Crop offsets? - * @todo Remote image fetching? - * @todo Alternative support for ImageMagick? - * @todo Prevent files in cache from being read directly? - * @todo split directory initialization into a separate - * install/upgrade script with friendly error messages, an opportunity to give a - * tip, and a button that tells me they are using it on their site if they like - * @todo document new code - * @todo clean up new code - */ -class SLIR -{ - /** - * @since 2.0 - * @var string - */ - const VERSION = '2.0b4'; - - /** - * @since 2.0 - * @var string - */ - const CROP_CLASS_CENTERED = 'centered'; - - /** - * @since 2.0 - * @var string - */ - const CROP_CLASS_TOP_CENTERED = 'topcentered'; - - /** - * @since 2.0 - * @var string - */ - const CROP_CLASS_SMART = 'smart'; - - /** - * @since 2.0 - * @var string - */ - const CROP_CLASS_FACE = 'face'; - - /** - * Request object - * - * @since 2.0 - * @uses SLIRRequest - * @var object - */ - private $request; - - /** - * Source image object - * - * @since 2.0 - * @uses SLIRImage - * @var object - */ - private $source; - - /** - * Rendered image object - * - * @since 2.0 - * @uses SLIRImage - * @var object - */ - private $rendered; - - /** - * Whether or not the cache has already been initialized - * - * @since 2.0 - * @var boolean - */ - private $isCacheInitialized = FALSE; - - /** - * The magic starts here - * - * @since 2.0 - */ - final public function __construct() - { - // This helps prevent unnecessary warnings (which messes up images) - // on servers that are set to display E_STRICT errors. - $this->disableStrictErrorReporting(); - - // Prevents ob_start('ob_gzhandler') in auto_prepend files from messing - // up SLIR's output. - $this->escapeOutputBuffering(); - - $this->getConfig(); - - $this->initializeGarbageCollection(); - - $this->request = new SLIRRequest(); - - // Check the cache based on the request URI - if (SLIRConfig::$useRequestCache === TRUE && $this->isRequestCached()) - { - $this->serveRequestCachedImage(); - } - - // Set up our error handler after the request cache to help keep - // everything humming along nicely - require 'slirexception.class.php'; - set_error_handler(array('SLIRException', 'error')); - - // Set all parameters for resizing - $this->setParameters(); - - // See if there is anything we actually need to do - if ($this->isSourceImageDesired()) - { - $this->serveSourceImage(); - } - - // Determine rendered dimensions - $this->setRenderedProperties(); - - // Check the cache based on the properties of the rendered image - if (!$this->isRenderedCached() || !$this->serveRenderedCachedImage()) - { - // Image is not cached in any way, so we need to render the image, - // cache it, and serve it up to the client - $this->render(); - $this->serveRenderedImage(); - } // if - } - - /** - * Disables E_STRICT error reporting - * - * @since 2.0 - * @return integer - */ - private function disableStrictErrorReporting() - { - return error_reporting(error_reporting() & ~E_STRICT); - } - - /** - * Escapes from output buffering. - * - * @since 2.0 - * @return void - */ - private function escapeOutputBuffering() - { - while ($level = ob_get_level()) - { - ob_end_clean(); - - if ($level == ob_get_level()) // On some setups, ob_get_level() will return a 1 instead of a 0 when there are no more buffers - { - return; - } - } - } - - /** - * Determines if the garbage collector should run for this request. - * - * @since 2.0 - * @return boolean - */ - private function garbageCollectionShouldRun() - { - if (rand(1, SLIRConfig::$garbageCollectDivisor) <= SLIRConfig::$garbageCollectProbability) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Checks to see if the garbage collector should be initialized, and if it should, initializes it. - * - * @since 2.0 - * @return void - */ - private function initializeGarbageCollection() - { - if ($this->garbageCollectionShouldRun()) - { - // Register this as a shutdown function so the additional processing time - // will not affect the speed of the request - register_shutdown_function(array($this, 'collectGarbage')); - } - } - - /** - * Deletes stale files from a directory. - * - * Used by the garbage collector to keep the cache directories from overflowing. - * - * @param string $path Directory to delete stale files from - */ - private function deleteStaleFilesFromDirectory($path, $useAccessedTime = TRUE) - { - $now = time(); - $dir = new DirectoryIterator($path); - - if ($useAccessedTime === TRUE) - { - $function = 'getATime'; - } - else - { - $function = 'getCTime'; - } - - foreach ($dir as $file) - { - if (!$file->isDot() && ($now - $file->$function()) > SLIRConfig::$garbageCollectFileCacheMaxLifetime) - { - unlink($file->getPathName()); - } - } - } - - /** - * Garbage collector - * - * Clears out old files from the cache - * - * @since 2.0 - * @return void - */ - public function collectGarbage() - { - $this->deleteStaleFilesFromDirectory($this->getRequestCacheDir(), FALSE); - $this->deleteStaleFilesFromDirectory($this->getRenderedCacheDir()); - } - - /** - * Includes the configuration file. - * - * If the configuration file cannot be included, this will throw an error that will hopefully explain what needs to be done. - * - * @since 2.0 - * @return void - */ - private function getConfig() - { - if (file_exists(self::configFilename())) - { - require self::configFilename(); - } - else if (file_exists('slirconfig-sample.class.php')) - { - if (copy('slirconfig-sample.class.php', self::configFilename())) - { - require self::configFilename(); - } - else - { - throw new SLIRException('Could not load configuration file. ' - . 'Please copy "slirconfig-sample.class.php" to ' - . '"' . self::configFilename() . '".'); - } - } - else - { - throw new SLIRException('Could not find "' . self::configFilename() . '" or ' - . '"slirconfig-sample.class.php"'); - } // if - } - - /** - * Returns the configuration filename. Allows the developer to specify an alternate configuration file. - * - * @since 2.0 - * @return string - */ - private function configFilename() - { - if (defined('SLIR_CONFIG_FILENAME')) - { - return SLIR_CONFIG_FILENAME; - } - else - { - return 'slirconfig.class.php'; - } - } - - /** - * Sets up parameters for image resizing - * - * @since 2.0 - * @return void - */ - private function setParameters() - { - $this->source = new SLIRImage(); - $this->source->path = $this->request->path; - - // If either a max width or max height are not specified or larger than - // the source image we default to the dimension of the source image so - // they do not become constraints on our resized image. - if (!$this->request->width || $this->request->width > $this->source->width) - { - $this->request->width = $this->source->width; - } - - if (!$this->request->height || $this->request->height > $this->source->height) - { - $this->request->height = $this->source->height; - } - } - - /** - * Allocates memory for the request. - * - * Tries to dynamically guess how much memory will be needed for the request based on the dimensions of the source image. - * - * @since 2.0 - * @return void - */ - private function allocateMemory() - { - // Multiply width * height * 5 bytes - $estimatedMemory = $this->source->width * $this->source->height * 5; - - // Convert memory to Megabytes and add 15 in order to allow some slack - $estimatedMemory = round(($estimatedMemory / 1024) / 1024, 0) + 15; - - $v = ini_set('memory_limit', min($estimatedMemory, SLIRConfig::$maxMemoryToAllocate) . 'M'); - } - - /** - * Renders requested changes to the image - * - * @since 2.0 - * @return void - */ - private function render() - { - $this->allocateMemory(); - - $this->source->createImageFromFile(); - - $this->rendered->createBlankImage(); - $this->rendered->background($this->isBackgroundFillOn()); - - $this->copySourceToRendered(); - $this->rendered->setPath($this->source->path, FALSE); - $this->source->destroyImage(); - - $this->rendered->crop($this->isBackgroundFillOn()); - $this->rendered->sharpen($this->calculateSharpnessFactor()); - $this->rendered->interlace(); - } - - /** - * Copies the source image to the rendered image, resizing (resampling) it if resizing is requested - * - * @since 2.0 - * @return void - */ - private function copySourceToRendered() - { - // Resample the original image into the resized canvas we set up earlier - if ($this->source->width != $this->rendered->width || $this->source->height != $this->rendered->height) - { - ImageCopyResampled( - $this->rendered->image, - $this->source->image, - 0, - 0, - 0, - 0, - $this->rendered->width, - $this->rendered->height, - $this->source->width, - $this->source->height - ); - } - else // No resizing is needed, so make a clean copy - { - ImageCopy( - $this->rendered->image, - $this->source->image, - 0, - 0, - 0, - 0, - $this->source->width, - $this->source->height - ); - } // if - } - - /** - * Calculates how much to sharpen the image based on the difference in dimensions of the source image and the rendered image - * - * @since 2.0 - * @return integer Sharpness factor - */ - private function calculateSharpnessFactor() - { - return $this->calculateASharpnessFactor($this->source->area(), $this->rendered->area()); - } - - /** - * Calculates sharpness factor to be used to sharpen an image based on the - * area of the source image and the area of the destination image - * - * @since 2.0 - * @author Ryan Rud - * @link http://adryrun.com - * - * @param integer $sourceArea Area of source image - * @param integer $destinationArea Area of destination image - * @return integer Sharpness factor - */ - private function calculateASharpnessFactor($sourceArea, $destinationArea) - { - $final = sqrt($destinationArea) * (750.0 / sqrt($sourceArea)); - $a = 52; - $b = -0.27810650887573124; - $c = .00047337278106508946; - - $result = $a + $b * $final + $c * $final * $final; - - return max(round($result), 0); - } - - /** - * Copies IPTC data from the source image to the cached file - * - * @since 2.0 - * @param string $cacheFilePath - * @return boolean - */ - private function copyIPTC($cacheFilePath) - { - $data = ''; - - $iptc = $this->source->iptc; - - // Originating program - $iptc['2#065'] = array('Smart Lencioni Image Resizer'); - - // Program version - $iptc['2#070'] = array(SLIR::VERSION); - - foreach($iptc as $tag => $iptcData) - { - $tag = substr($tag, 2); - $data .= $this->makeIPTCTag(2, $tag, $iptcData[0]); - } - - // Embed the IPTC data - return iptcembed($data, $cacheFilePath); - } - - /** - * @since 2.0 - * @author Thies C. Arntzen - */ - final function makeIPTCTag($rec, $data, $value) - { - $length = strlen($value); - $retval = chr(0x1C) . chr($rec) . chr($data); - - if ($length < 0x8000) - { - $retval .= chr($length >> 8) . chr($length & 0xFF); - } - else - { - $retval .= chr(0x80) . - chr(0x04) . - chr(($length >> 24) & 0xFF) . - chr(($length >> 16) & 0xFF) . - chr(($length >> 8) & 0xFF) . - chr($length & 0xFF); - } - - return $retval . $value; - } - - /** - * Checks parameters against the image's attributes and determines whether - * anything needs to be changed or if we simply need to serve up the source - * image - * - * @since 2.0 - * @return boolean - * @todo Add check for JPEGs and progressiveness - */ - private function isSourceImageDesired() - { - if ($this->isWidthDifferent() - || $this->isHeightDifferent() - || $this->isBackgroundFillOn() - || $this->isQualityOn() - || $this->isCroppingNeeded() - ) - { - return FALSE; - } - else - { - return TRUE; - } - } - - /** - * Determines if the requested width is different than the width of the source image - * - * @since 2.0 - * @return boolean - */ - private function isWidthDifferent() - { - if ($this->request->width !== NULL && $this->request->width < $this->source->width) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Determines if the requested height is different than the height of the source image - * - * @since 2.0 - * @return boolean - */ - private function isHeightDifferent() - { - if ($this->request->height !== NULL && $this->request->height < $this->source->height) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Determines if a background fill has been requested and if the image is able to have transparency (not for JPEG files) - * - * @since 2.0 - * @return boolean - */ - private function isBackgroundFillOn() - { - if ($this->request->isBackground() && $this->source->isAbleToHaveTransparency()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Determines if the user included image quality in the request - * - * @since 2.0 - * @return boolean - */ - private function isQualityOn() - { - return $this->request->isQuality(); - } - - /** - * Determines if the image should be cropped based on the requested crop ratio and the width:height ratio of the source image - * - * @since 2.0 - * @return boolean - */ - private function isCroppingNeeded() - { - if ($this->request->isCropping() && $this->request->cropRatio['ratio'] != $this->source->ratio()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Computes and sets properties of the rendered image, such as the actual - * width, height, and quality - * - * @since 2.0 - */ - private function setRenderedProperties() - { - $this->rendered = new SLIRImage(); - - // Set default properties of the rendered image - $this->rendered->width = $this->source->width; - $this->rendered->height = $this->source->height; - - // Cropping - /* - To determine the width and height of the rendered image, the following - should occur. - - If cropping an image is required, we need to: - 1. Compute the dimensions of the source image after cropping before - resizing. - 2. Compute the dimensions of the resized image before cropping. One of - these dimensions may be greater than maxWidth or maxHeight because - they are based on the dimensions of the final rendered image, which - will be cropped to fit within the specified maximum dimensions. - 3. Compute the dimensions of the resized image after cropping. These - must both be less than or equal to maxWidth and maxHeight. - 4. Then when rendering, the image needs to be resized, crop offsets - need to be computed based on the desired method (smart or centered), - and the image needs to be cropped to the specified dimensions. - - If cropping an image is not required, we need to compute the dimensions - of the image without cropping. These must both be less than or equal to - maxWidth and maxHeight. - */ - if ($this->isCroppingNeeded()) - { - // Determine the dimensions of the source image after cropping and - // before resizing - - if ($this->request->cropRatio['ratio'] > $this->source->ratio()) - { - // Image is too tall so we will crop the top and bottom - $this->source->cropHeight = $this->source->width / $this->request->cropRatio['ratio']; - $this->source->cropWidth = $this->source->width; - } - else - { - // Image is too wide so we will crop off the left and right sides - $this->source->cropWidth = $this->source->height * $this->request->cropRatio['ratio']; - $this->source->cropHeight = $this->source->height; - } // if - - $this->source->cropper = $this->request->cropper; - $this->rendered->cropper = $this->source->cropper; - } // if - - if ($this->shouldResizeBasedOnWidth()) - { - $this->rendered->height = round($this->resizeWidthFactor() * $this->source->height); - $this->rendered->width = round($this->resizeWidthFactor() * $this->source->width); - - // Determine dimensions after cropping - if ($this->isCroppingNeeded()) - { - $this->rendered->cropHeight = round($this->resizeWidthFactor() * $this->source->cropHeight); - $this->rendered->cropWidth = round($this->resizeWidthFactor() * $this->source->cropWidth); - } // if - } - else if ($this->shouldResizeBasedOnHeight()) - { - $this->rendered->width = round($this->resizeHeightFactor() * $this->source->width); - $this->rendered->height = round($this->resizeHeightFactor() * $this->source->height); - - // Determine dimensions after cropping - if ($this->isCroppingNeeded()) - { - $this->rendered->cropHeight = round($this->resizeHeightFactor() * $this->source->cropHeight); - $this->rendered->cropWidth = round($this->resizeHeightFactor() * $this->source->cropWidth); - } // if - } - else if ($this->isCroppingNeeded()) // No resizing is needed but we still need to crop - { - $ratio = ($this->resizeUncroppedWidthFactor() > $this->resizeUncroppedHeightFactor()) - ? $this->resizeUncroppedWidthFactor() : $this->resizeUncroppedHeightFactor(); - - $this->rendered->width = round($ratio * $this->source->width); - $this->rendered->height = round($ratio * $this->source->height); - - $this->rendered->cropWidth = round($ratio * $this->source->cropWidth); - $this->rendered->cropHeight = round($ratio * $this->source->cropHeight); - } // if - - // Determine the quality of the output image - $this->rendered->quality = ($this->request->quality !== NULL) - ? $this->request->quality : SLIRConfig::$defaultQuality; - - // Set up the appropriate image handling parameters based on the original - // image's mime type - // @todo some of this code should be moved to the SLIRImage class - $this->rendered->mime = $this->source->mime; - if ($this->source->isGIF()) - { - // We need to convert GIFs to PNGs - $this->rendered->mime = 'image/png'; - $this->rendered->progressive = FALSE; - - // We are converting the GIF to a PNG, and PNG needs a - // compression level of 0 (no compression) through 9 - $this->rendered->quality = round(10 - ($this->rendered->quality / 10)); - } - else if ($this->source->isPNG()) - { - $this->rendered->progressive = FALSE; - - // PNG needs a compression level of 0 (no compression) through 9 - $this->rendered->quality = round(10 - ($this->rendered->quality / 10)); - } - else if ($this->source->isJPEG()) - { - $this->rendered->progressive = ($this->request->progressive !== NULL) - ? $this->request->progressive : SLIRConfig::$defaultProgressiveJPEG; - $this->rendered->background = NULL; - } - else - { - throw new SLIRException("Unable to determine type of source image"); - } // if - - if ($this->isBackgroundFillOn()) - { - $this->rendered->background = $this->request->background; - } - } - - /** - * Detemrines if the image should be resized based on its width (i.e. the width is the constraining dimension for this request) - * - * @since 2.0 - * @return boolean - */ - private function shouldResizeBasedOnWidth() - { - if (floor($this->resizeWidthFactor() * $this->source->height) <= $this->request->height) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * Detemrines if the image should be resized based on its height (i.e. the height is the constraining dimension for this request) - * @since 2.0 - * @return boolean - */ - private function shouldResizeBasedOnHeight() - { - if (floor($this->resizeHeightFactor() * $this->source->width) <= $this->request->width) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return float - */ - private function resizeWidthFactor() - { - if ($this->source->cropWidth !== NULL) - { - return $this->resizeCroppedWidthFactor(); - } - else - { - return $this->resizeUncroppedWidthFactor(); - } - } - - /** - * @since 2.0 - * @return float - */ - private function resizeUncroppedWidthFactor() - { - return $this->request->width / $this->source->width; - } - - /** - * @since 2.0 - * @return float - */ - private function resizeCroppedWidthFactor() - { - return $this->request->width / $this->source->cropWidth; - } - - /** - * @since 2.0 - * @return float - */ - private function resizeHeightFactor() - { - if ($this->source->cropHeight !== NULL) - { - return $this->resizeCroppedHeightFactor(); - } - else - { - return $this->resizeUncroppedHeightFactor(); - } - } - - /** - * @since 2.0 - * @return float - */ - private function resizeUncroppedHeightFactor() - { - return $this->request->height / $this->source->height; - } - - /** - * @since 2.0 - * @return float - */ - private function resizeCroppedHeightFactor() - { - return $this->request->height / $this->source->cropHeight; - } - - /** - * Determines if the rendered file is in the rendered cache - * - * @since 2.0 - * @return boolean - */ - private function isRenderedCached() - { - return $this->isCached($this->renderedCacheFilePath()); - } - - /** - * Determines if the request is symlinked to the rendered file - * - * @since 2.0 - * @return boolean - */ - private function isRequestCached() - { - return $this->isCached($this->requestCacheFilePath()); - } - - /** - * Determines if a given file exists in the cache - * - * @since 2.0 - * @param string $cacheFilePath - * @return boolean - */ - private function isCached($cacheFilePath) - { - if (!file_exists($cacheFilePath)) - { - return FALSE; - } - - $cacheModified = filemtime($cacheFilePath); - - if (!$cacheModified) - { - return FALSE; - } - - $imageModified = filectime($this->request->fullPath()); - - if ($imageModified >= $cacheModified) - { - return FALSE; - } - else - { - return TRUE; - } - } - - /** - * @since 2.0 - * @return string - */ - private function getRenderedCacheDir() - { - return SLIRConfig::$cacheDir . '/rendered'; - } - - /** - * @since 2.0 - * @return string - */ - private function renderedCacheFilePath() - { - return $this->getRenderedCacheDir() . $this->renderedCacheFilename(); - } - - /** - * @since 2.0 - * @return string - */ - private function renderedCacheFilename() - { - return '/' . md5($this->request->fullPath() . serialize($this->rendered->cacheParameters())); - } - - /** - * @since 2.0 - * @return string - */ - private function requestCacheFilename() - { - return '/' . md5($_SERVER['HTTP_HOST'] . '/' . $this->requestURI() . '/' . SLIRConfig::$defaultCropper); - } - - /** - * @since 2.0 - * @return string - */ - private function requestURI() - { - if (SLIRConfig::$forceQueryString === TRUE) - { - return $_SERVER['SCRIPT_NAME'] . '?' . http_build_query($_GET); - } - else - { - return $_SERVER['REQUEST_URI']; - } - } - - /** - * @since 2.0 - * @return string - */ - private function getRequestCacheDir() - { - return SLIRConfig::$cacheDir . '/request'; - } - - /** - * @since 2.0 - * @return string - */ - private function requestCacheFilePath() - { - return $this->getRequestCacheDir() . $this->requestCacheFilename(); - } - - /** - * Write an image to the cache - * - * @since 2.0 - * @param string $imageData - * @return boolean - */ - private function cache() - { - $this->cacheRendered(); - - if (SLIRConfig::$useRequestCache === TRUE) - { - return $this->cacheRequest($this->rendered->data, TRUE); - } - else - { - return TRUE; - } - } - - /** - * Write an image to the cache based on the properties of the rendered image - * - * @since 2.0 - * @return boolean - */ - private function cacheRendered() - { - $this->rendered->data = $this->cacheFile( - $this->renderedCacheFilePath(), - $this->rendered->data, - TRUE - ); - - return TRUE; - } - - /** - * Write an image to the cache based on the request URI - * - * @since 2.0 - * @param string $imageData - * @param boolean $copyEXIF - * @return string - */ - private function cacheRequest($imageData, $copyEXIF = TRUE) - { - return $this->cacheFile( - $this->requestCacheFilePath(), - $imageData, - $copyEXIF, - $this->renderedCacheFilePath() - ); - } - - /** - * Write an image to the cache based on the properties of the rendered image - * - * @since 2.0 - * @param string $cacheFilePath - * @param string $imageData - * @param boolean $copyEXIF - * @param string $symlinkToPath - * @return string|boolean - */ - private function cacheFile($cacheFilePath, $imageData, $copyEXIF = TRUE, $symlinkToPath = NULL) - { - $this->initializeCache(); - - // Try to create just a symlink to minimize disk space - if ($symlinkToPath && function_exists('symlink') && (file_exists($cacheFilePath) || symlink('../'.$symlinkToPath, $cacheFilePath))) - { - return TRUE; - } - - // Create the file - if (!file_put_contents($cacheFilePath, $imageData)) - { - return FALSE; - } - - if (SLIRConfig::$copyEXIF == TRUE && $copyEXIF && $this->source->isJPEG()) - { - // Copy IPTC data - if (isset($this->source->iptc) && !$this->copyIPTC($cacheFilePath)) - { - return FALSE; - } - - // Copy EXIF data - $imageData = $this->copyEXIF($cacheFilePath); - } // if - - return $imageData; - } - - /** - * Copy the source image's EXIF information to the new file in the cache - * - * @since 2.0 - * @uses PEL - * @param string $cacheFilePath - * @return mixed string contents of image on success, FALSE on failure - */ - private function copyEXIF($cacheFilePath) - { - // Make sure to suppress strict warning thrown by PEL - @require_once dirname(__FILE__) . '/pel-0.9.2/src/PelJpeg.php'; - - $jpeg = new PelJpeg($this->source->fullPath()); - $exif = $jpeg->getExif(); - - if ($exif) - { - $jpeg = new PelJpeg($cacheFilePath); - $jpeg->setExif($exif); - $imageData = $jpeg->getBytes(); - if (!file_put_contents($cacheFilePath, $imageData)) - { - return FALSE; - } - - return $imageData; - } // if - - return file_get_contents($cacheFilePath); - } - - /** - * Makes sure the cache directory exists, is readable, and is writable - * - * @since 2.0 - * @return boolean - */ - private function initializeCache() - { - if ($this->isCacheInitialized) - { - return TRUE; - } - - $this->initializeDirectory(SLIRConfig::$cacheDir); - $this->initializeDirectory(SLIRConfig::$cacheDir . '/rendered', FALSE); - $this->initializeDirectory(SLIRConfig::$cacheDir . '/request', FALSE); - - $this->isCacheInitialized = TRUE; - return TRUE; - } - - /** - * @since 2.0 - * @param string $path Directory to initialize - * @param boolean $verifyReadWriteability - * @return boolean - */ - private function initializeDirectory($path, $verifyReadWriteability = TRUE, $test = FALSE) - { - if (!file_exists($path)) - { - if (!@mkdir($path, 0755, TRUE)) - { - header('HTTP/1.1 500 Internal Server Error'); - throw new SLIRException("Directory ($path) does not exist and was unable to be created. Please create the directory."); - } - } - - if (!$verifyReadWriteability) - return TRUE; - - // Make sure we can read and write the cache directory - if (!is_readable($path)) - { - header('HTTP/1.1 500 Internal Server Error'); - throw new SLIRException("Directory ($path) is not readable"); - } - else if (!is_writable($path)) - { - header('HTTP/1.1 500 Internal Server Error'); - throw new SLIRException("Directory ($path) is not writable"); - } - - return TRUE; - } - - /** - * Serves the unmodified source image - * - * @since 2.0 - * @return void - */ - private function serveSourceImage() - { - $this->serveFile( - $this->source->fullPath(), - NULL, - NULL, - NULL, - $this->source->mime, - 'source' - ); - - exit(); - } - - /** - * Serves the image from the cache based on the properties of the rendered - * image - * - * @since 2.0 - * @return void - */ - private function serveRenderedCachedImage() - { - return $this->serveCachedImage($this->renderedCacheFilePath(), 'rendered'); - } - - /** - * Serves the image from the cache based on the request URI - * - * @since 2.0 - * @return void - */ - private function serveRequestCachedImage() - { - return $this->serveCachedImage($this->requestCacheFilePath(), 'request'); - } - - /** - * Serves the image from the cache - * - * @since 2.0 - * @param string $cacheFilePath - * @param string $cacheType Can be 'request' or 'image' - * @return void - */ - private function serveCachedImage($cacheFilePath, $cacheType) - { - // Serve the image - $data = $this->serveFile( - $cacheFilePath, - NULL, - NULL, - NULL, - NULL, - "$cacheType cache" - ); - - // If we are serving from the rendered cache, create a symlink in the - // request cache to the rendered file - if ($cacheType != 'request') - { - $this->cacheRequest($data, FALSE); - } - - exit(); - } - - /** - * Determines the mime type of an image - * - * @since 2.0 - * @param string $path - * @return string - */ - private function mimeType($path) - { - $info = getimagesize($path); - return $info['mime']; - } - - /** - * Serves the rendered image - * - * @since 2.0 - * @return void - */ - private function serveRenderedImage() - { - // Cache the image - $this->cache(); - - // Serve the file - $this->serveFile( - NULL, - $this->rendered->data, - gmdate('U'), - $this->rendered->fileSize(), - $this->rendered->mime, - 'rendered' - ); - - // Clean up memory - $this->rendered->destroyImage(); - - exit(); - } - - /** - * Serves a file - * - * @since 2.0 - * @param string $imagePath Path to file to serve - * @param string $data Data of file to serve - * @param integer $lastModified Timestamp of when the file was last modified - * @param string $mimeType - * @param string $SLIRheader - * @return string Image data - */ - private function serveFile($imagePath, $data, $lastModified, $length, $mimeType, $SLIRHeader) - { - if ($imagePath != NULL) - { - if ($lastModified == NULL) - { - $lastModified = filemtime($imagePath); - } - if ($length == NULL) - { - $length = filesize($imagePath); - } - if ($mimeType == NULL) - { - $mimeType = $this->mimeType($imagePath); - } - } - else if ($length == NULL) - { - $length = strlen($data); - } // if - - // Serve the headers - $this->serveHeaders( - $this->lastModified($lastModified), - $mimeType, - $length, - $SLIRHeader - ); - - // Read the image data into memory if we need to - if ($data == NULL) - { - $data = file_get_contents($imagePath); - } - - // Send the image to the browser in bite-sized chunks - $chunkSize = 1024 * 8; - $fp = fopen('php://memory', 'r+b'); - fwrite($fp, $data); - rewind($fp); - while (!feof($fp)) - { - echo fread($fp, $chunkSize); - flush(); - } // while - fclose($fp); - - return $data; - } - - /** - * Serves headers for file for optimal browser caching - * - * @since 2.0 - * @param string $lastModified Time when file was last modified in 'D, d M Y H:i:s' format - * @param string $mimeType - * @param integer $fileSize - * @param string $SLIRHeader - * @return void - */ - private function serveHeaders($lastModified, $mimeType, $fileSize, $SLIRHeader) - { - header("Last-Modified: $lastModified"); - header("Content-Type: $mimeType"); - header("Content-Length: $fileSize"); - - // Lets us easily know whether the image was rendered from scratch, - // from the cache, or served directly from the source image - header("Content-SLIR: $SLIRHeader"); - - // Keep in browser cache how long? - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + SLIRConfig::$browserCacheTTL) . ' GMT'); - - // Public in the Cache-Control lets proxies know that it is okay to - // cache this content. If this is being served over HTTPS, there may be - // sensitive content and therefore should probably not be cached by - // proxy servers. - header('Cache-Control: max-age=' . SLIRConfig::$browserCacheTTL . ', public'); - - $this->doConditionalGet($lastModified); - - // The "Connection: close" header allows us to serve the file and let - // the browser finish processing the script so we can do extra work - // without making the user wait. This header must come last or the file - // size will not properly work for images in the browser's cache - //header('Connection: close'); - } - - /** - * Converts a UNIX timestamp into the format needed for the Last-Modified - * header - * - * @since 2.0 - * @param integer $timestamp - * @return string - */ - private function lastModified($timestamp) - { - return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT'; - } - - /** - * Checks the to see if the file is different than the browser's cache - * - * @since 2.0 - * @param string $lastModified - * @return void - */ - private function doConditionalGet($lastModified) - { - $ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) ? - stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : - FALSE; - - if (!$ifModifiedSince || $ifModifiedSince != $lastModified) - { - return; - } - - // Nothing has changed since their last request - serve a 304 and exit - header('HTTP/1.1 304 Not Modified'); - - // Serve a "Connection: close" header here in case there are any - // shutdown functions that have been registered with - // register_shutdown_function() - header('Connection: close'); - - exit(); - } - -} // class SLIR - -// old pond -// a frog jumps -// the sound of water - +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slir.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ + +/** + * SLIR (Smart Lencioni Image Resizer) + * Resizes images, intelligently sharpens, crops based on width:height ratios, + * color fills transparent GIFs and PNGs, and caches variations for optimal + * performance. + * + * I love to hear when my work is being used, so if you decide to use this, + * feel encouraged to send me an email. I would appreciate it if you would + * include a link on your site back to Shifting Pixel (either the SLIR page or + * shiftingpixel.com), but don’t worry about including a big link on each page + * if you don’t want to–one will do just nicely. Feel free to contact me to + * discuss any specifics (joe@shiftingpixel.com). + * + * REQUIREMENTS: + * - PHP 5.1.0+ + * - GD + * + * RECOMMENDED: + * - mod_rewrite + * + * USAGE: + * To use, place an img tag with the src pointing to the path of SLIR (typically + * "/slir/") followed by the parameters, followed by the path to the source + * image to resize. All parameters follow the pattern of a one-letter code and + * then the parameter value: + * - Maximum width = w + * - Maximum height = h + * - Crop ratio = c + * - Quality = q + * - Background fill color = b + * - Progressive = p + * + * Note: filenames that include special characters must be URL-encoded (e.g. + * plus sign, +, should be encoded as %2B) in order for SLIR to recognize them + * properly. This can be accomplished by passing your filenames through PHP's + * rawurlencode() or urlencode() function. + * + * EXAMPLES: + * + * Resizing a JPEG to a max width of 100 pixels and a max height of 100 pixels: + * Don't forget your alt
+ * text + * + * Resizing and cropping a JPEG into a square: + * Don't forget
+ * your alt text + * + * Resizing a JPEG without interlacing (for use in Flash): + * Don't forget your alt
+ * text + * + * Matting a PNG with #990000: + * Don't forget your alt
+ * text + * + * Without mod_rewrite (not recommended) + * Don't forget your alt text + * + * @author Joe Lencioni + * $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ + * @version $Revision: 129 $ + * @package SLIR + * + * @uses PEL + * + * @todo lock files when writing? + * @todo Prevent SLIR from calling itself + * @todo Percentage resizing? + * @todo Animated GIF resizing? + * @todo Seam carving? + * @todo Crop zoom? + * @todo Crop offsets? + * @todo Remote image fetching? + * @todo Alternative support for ImageMagick? + * @todo Prevent files in cache from being read directly? + * @todo split directory initialization into a separate + * install/upgrade script with friendly error messages, an opportunity to give a + * tip, and a button that tells me they are using it on their site if they like + * @todo document new code + * @todo clean up new code + */ +class SLIR +{ + /** + * @since 2.0 + * @var string + */ + const VERSION = '2.0b4'; + + /** + * @since 2.0 + * @var string + */ + const CROP_CLASS_CENTERED = 'centered'; + + /** + * @since 2.0 + * @var string + */ + const CROP_CLASS_TOP_CENTERED = 'topcentered'; + + /** + * @since 2.0 + * @var string + */ + const CROP_CLASS_SMART = 'smart'; + + /** + * @since 2.0 + * @var string + */ + const CROP_CLASS_FACE = 'face'; + + /** + * Request object + * + * @since 2.0 + * @uses SLIRRequest + * @var object + */ + private $request; + + /** + * Source image object + * + * @since 2.0 + * @uses SLIRImage + * @var object + */ + private $source; + + /** + * Rendered image object + * + * @since 2.0 + * @uses SLIRImage + * @var object + */ + private $rendered; + + /** + * Whether or not the cache has already been initialized + * + * @since 2.0 + * @var boolean + */ + private $isCacheInitialized = FALSE; + + /** + * The magic starts here + * + * @since 2.0 + */ + final public function __construct() + { + // This helps prevent unnecessary warnings (which messes up images) + // on servers that are set to display E_STRICT errors. + $this->disableStrictErrorReporting(); + + // Prevents ob_start('ob_gzhandler') in auto_prepend files from messing + // up SLIR's output. + $this->escapeOutputBuffering(); + + $this->getConfig(); + + $this->initializeGarbageCollection(); + + $this->request = new SLIRRequest(); + + // Check the cache based on the request URI + if (SLIRConfig::$useRequestCache === TRUE && $this->isRequestCached()) + { + $this->serveRequestCachedImage(); + } + + // Set up our error handler after the request cache to help keep + // everything humming along nicely + require 'slirexception.class.php'; + set_error_handler(array('SLIRException', 'error')); + + // Set all parameters for resizing + $this->setParameters(); + + // See if there is anything we actually need to do + if ($this->isSourceImageDesired()) + { + $this->serveSourceImage(); + } + + // Determine rendered dimensions + $this->setRenderedProperties(); + + // Check the cache based on the properties of the rendered image + if (!$this->isRenderedCached() || !$this->serveRenderedCachedImage()) + { + // Image is not cached in any way, so we need to render the image, + // cache it, and serve it up to the client + $this->render(); + $this->serveRenderedImage(); + } // if + } + + /** + * Disables E_STRICT error reporting + * + * @since 2.0 + * @return integer + */ + private function disableStrictErrorReporting() + { + return error_reporting(error_reporting() & ~E_STRICT); + } + + /** + * Escapes from output buffering. + * + * @since 2.0 + * @return void + */ + private function escapeOutputBuffering() + { + while ($level = ob_get_level()) + { + ob_end_clean(); + + if ($level == ob_get_level()) // On some setups, ob_get_level() will return a 1 instead of a 0 when there are no more buffers + { + return; + } + } + } + + /** + * Determines if the garbage collector should run for this request. + * + * @since 2.0 + * @return boolean + */ + private function garbageCollectionShouldRun() + { + if (rand(1, SLIRConfig::$garbageCollectDivisor) <= SLIRConfig::$garbageCollectProbability) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Checks to see if the garbage collector should be initialized, and if it should, initializes it. + * + * @since 2.0 + * @return void + */ + private function initializeGarbageCollection() + { + if ($this->garbageCollectionShouldRun()) + { + // Register this as a shutdown function so the additional processing time + // will not affect the speed of the request + register_shutdown_function(array($this, 'collectGarbage')); + } + } + + /** + * Deletes stale files from a directory. + * + * Used by the garbage collector to keep the cache directories from overflowing. + * + * @param string $path Directory to delete stale files from + */ + private function deleteStaleFilesFromDirectory($path, $useAccessedTime = TRUE) + { + $now = time(); + $dir = new DirectoryIterator($path); + + if ($useAccessedTime === TRUE) + { + $function = 'getATime'; + } + else + { + $function = 'getCTime'; + } + + foreach ($dir as $file) + { + if (!$file->isDot() && ($now - $file->$function()) > SLIRConfig::$garbageCollectFileCacheMaxLifetime) + { + unlink($file->getPathName()); + } + } + } + + /** + * Garbage collector + * + * Clears out old files from the cache + * + * @since 2.0 + * @return void + */ + public function collectGarbage() + { + $this->deleteStaleFilesFromDirectory($this->getRequestCacheDir(), FALSE); + $this->deleteStaleFilesFromDirectory($this->getRenderedCacheDir()); + } + + /** + * Includes the configuration file. + * + * If the configuration file cannot be included, this will throw an error that will hopefully explain what needs to be done. + * + * @since 2.0 + * @return void + */ + private function getConfig() + { + if (file_exists(self::configFilename())) + { + require self::configFilename(); + } + else if (file_exists('slirconfig-sample.class.php')) + { + if (copy('slirconfig-sample.class.php', self::configFilename())) + { + require self::configFilename(); + } + else + { + throw new SLIRException('Could not load configuration file. ' + . 'Please copy "slirconfig-sample.class.php" to ' + . '"' . self::configFilename() . '".'); + } + } + else + { + throw new SLIRException('Could not find "' . self::configFilename() . '" or ' + . '"slirconfig-sample.class.php"'); + } // if + } + + /** + * Returns the configuration filename. Allows the developer to specify an alternate configuration file. + * + * @since 2.0 + * @return string + */ + private function configFilename() + { + if (defined('SLIR_CONFIG_FILENAME')) + { + return SLIR_CONFIG_FILENAME; + } + else + { + return 'slirconfig.class.php'; + } + } + + /** + * Sets up parameters for image resizing + * + * @since 2.0 + * @return void + */ + private function setParameters() + { + $this->source = new SLIRImage(); + $this->source->path = $this->request->path; + + // If either a max width or max height are not specified or larger than + // the source image we default to the dimension of the source image so + // they do not become constraints on our resized image. + if (!$this->request->width || $this->request->width > $this->source->width) + { + $this->request->width = $this->source->width; + } + + if (!$this->request->height || $this->request->height > $this->source->height) + { + $this->request->height = $this->source->height; + } + } + + /** + * Allocates memory for the request. + * + * Tries to dynamically guess how much memory will be needed for the request based on the dimensions of the source image. + * + * @since 2.0 + * @return void + */ + private function allocateMemory() + { + // Multiply width * height * 5 bytes + $estimatedMemory = $this->source->width * $this->source->height * 5; + + // Convert memory to Megabytes and add 15 in order to allow some slack + $estimatedMemory = round(($estimatedMemory / 1024) / 1024, 0) + 15; + + $v = ini_set('memory_limit', min($estimatedMemory, SLIRConfig::$maxMemoryToAllocate) . 'M'); + } + + /** + * Renders requested changes to the image + * + * @since 2.0 + * @return void + */ + private function render() + { + $this->allocateMemory(); + + $this->source->createImageFromFile(); + + $this->rendered->createBlankImage(); + $this->rendered->background($this->isBackgroundFillOn()); + + $this->copySourceToRendered(); + $this->rendered->setPath($this->source->path, FALSE); + $this->source->destroyImage(); + + $this->rendered->crop($this->isBackgroundFillOn()); + $this->rendered->sharpen($this->calculateSharpnessFactor()); + $this->rendered->interlace(); + } + + /** + * Copies the source image to the rendered image, resizing (resampling) it if resizing is requested + * + * @since 2.0 + * @return void + */ + private function copySourceToRendered() + { + // Resample the original image into the resized canvas we set up earlier + if ($this->source->width != $this->rendered->width || $this->source->height != $this->rendered->height) + { + ImageCopyResampled( + $this->rendered->image, + $this->source->image, + 0, + 0, + 0, + 0, + $this->rendered->width, + $this->rendered->height, + $this->source->width, + $this->source->height + ); + } + else // No resizing is needed, so make a clean copy + { + ImageCopy( + $this->rendered->image, + $this->source->image, + 0, + 0, + 0, + 0, + $this->source->width, + $this->source->height + ); + } // if + } + + /** + * Calculates how much to sharpen the image based on the difference in dimensions of the source image and the rendered image + * + * @since 2.0 + * @return integer Sharpness factor + */ + private function calculateSharpnessFactor() + { + return $this->calculateASharpnessFactor($this->source->area(), $this->rendered->area()); + } + + /** + * Calculates sharpness factor to be used to sharpen an image based on the + * area of the source image and the area of the destination image + * + * @since 2.0 + * @author Ryan Rud + * @link http://adryrun.com + * + * @param integer $sourceArea Area of source image + * @param integer $destinationArea Area of destination image + * @return integer Sharpness factor + */ + private function calculateASharpnessFactor($sourceArea, $destinationArea) + { + $final = sqrt($destinationArea) * (750.0 / sqrt($sourceArea)); + $a = 52; + $b = -0.27810650887573124; + $c = .00047337278106508946; + + $result = $a + $b * $final + $c * $final * $final; + + return max(round($result), 0); + } + + /** + * Copies IPTC data from the source image to the cached file + * + * @since 2.0 + * @param string $cacheFilePath + * @return boolean + */ + private function copyIPTC($cacheFilePath) + { + $data = ''; + + $iptc = $this->source->iptc; + + // Originating program + $iptc['2#065'] = array('Smart Lencioni Image Resizer'); + + // Program version + $iptc['2#070'] = array(SLIR::VERSION); + + foreach($iptc as $tag => $iptcData) + { + $tag = substr($tag, 2); + $data .= $this->makeIPTCTag(2, $tag, $iptcData[0]); + } + + // Embed the IPTC data + return iptcembed($data, $cacheFilePath); + } + + /** + * @since 2.0 + * @author Thies C. Arntzen + */ + final function makeIPTCTag($rec, $data, $value) + { + $length = strlen($value); + $retval = chr(0x1C) . chr($rec) . chr($data); + + if ($length < 0x8000) + { + $retval .= chr($length >> 8) . chr($length & 0xFF); + } + else + { + $retval .= chr(0x80) . + chr(0x04) . + chr(($length >> 24) & 0xFF) . + chr(($length >> 16) & 0xFF) . + chr(($length >> 8) & 0xFF) . + chr($length & 0xFF); + } + + return $retval . $value; + } + + /** + * Checks parameters against the image's attributes and determines whether + * anything needs to be changed or if we simply need to serve up the source + * image + * + * @since 2.0 + * @return boolean + * @todo Add check for JPEGs and progressiveness + */ + private function isSourceImageDesired() + { + if ($this->isWidthDifferent() + || $this->isHeightDifferent() + || $this->isBackgroundFillOn() + || $this->isQualityOn() + || $this->isCroppingNeeded() + ) + { + return FALSE; + } + else + { + return TRUE; + } + } + + /** + * Determines if the requested width is different than the width of the source image + * + * @since 2.0 + * @return boolean + */ + private function isWidthDifferent() + { + if ($this->request->width !== NULL && $this->request->width < $this->source->width) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Determines if the requested height is different than the height of the source image + * + * @since 2.0 + * @return boolean + */ + private function isHeightDifferent() + { + if ($this->request->height !== NULL && $this->request->height < $this->source->height) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Determines if a background fill has been requested and if the image is able to have transparency (not for JPEG files) + * + * @since 2.0 + * @return boolean + */ + private function isBackgroundFillOn() + { + if ($this->request->isBackground() && $this->source->isAbleToHaveTransparency()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Determines if the user included image quality in the request + * + * @since 2.0 + * @return boolean + */ + private function isQualityOn() + { + return $this->request->isQuality(); + } + + /** + * Determines if the image should be cropped based on the requested crop ratio and the width:height ratio of the source image + * + * @since 2.0 + * @return boolean + */ + private function isCroppingNeeded() + { + if ($this->request->isCropping() && $this->request->cropRatio['ratio'] != $this->source->ratio()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Computes and sets properties of the rendered image, such as the actual + * width, height, and quality + * + * @since 2.0 + */ + private function setRenderedProperties() + { + $this->rendered = new SLIRImage(); + + // Set default properties of the rendered image + $this->rendered->width = $this->source->width; + $this->rendered->height = $this->source->height; + + // Cropping + /* + To determine the width and height of the rendered image, the following + should occur. + + If cropping an image is required, we need to: + 1. Compute the dimensions of the source image after cropping before + resizing. + 2. Compute the dimensions of the resized image before cropping. One of + these dimensions may be greater than maxWidth or maxHeight because + they are based on the dimensions of the final rendered image, which + will be cropped to fit within the specified maximum dimensions. + 3. Compute the dimensions of the resized image after cropping. These + must both be less than or equal to maxWidth and maxHeight. + 4. Then when rendering, the image needs to be resized, crop offsets + need to be computed based on the desired method (smart or centered), + and the image needs to be cropped to the specified dimensions. + + If cropping an image is not required, we need to compute the dimensions + of the image without cropping. These must both be less than or equal to + maxWidth and maxHeight. + */ + if ($this->isCroppingNeeded()) + { + // Determine the dimensions of the source image after cropping and + // before resizing + + if ($this->request->cropRatio['ratio'] > $this->source->ratio()) + { + // Image is too tall so we will crop the top and bottom + $this->source->cropHeight = $this->source->width / $this->request->cropRatio['ratio']; + $this->source->cropWidth = $this->source->width; + } + else + { + // Image is too wide so we will crop off the left and right sides + $this->source->cropWidth = $this->source->height * $this->request->cropRatio['ratio']; + $this->source->cropHeight = $this->source->height; + } // if + + $this->source->cropper = $this->request->cropper; + $this->rendered->cropper = $this->source->cropper; + } // if + + if ($this->shouldResizeBasedOnWidth()) + { + $this->rendered->height = round($this->resizeWidthFactor() * $this->source->height); + $this->rendered->width = round($this->resizeWidthFactor() * $this->source->width); + + // Determine dimensions after cropping + if ($this->isCroppingNeeded()) + { + $this->rendered->cropHeight = round($this->resizeWidthFactor() * $this->source->cropHeight); + $this->rendered->cropWidth = round($this->resizeWidthFactor() * $this->source->cropWidth); + } // if + } + else if ($this->shouldResizeBasedOnHeight()) + { + $this->rendered->width = round($this->resizeHeightFactor() * $this->source->width); + $this->rendered->height = round($this->resizeHeightFactor() * $this->source->height); + + // Determine dimensions after cropping + if ($this->isCroppingNeeded()) + { + $this->rendered->cropHeight = round($this->resizeHeightFactor() * $this->source->cropHeight); + $this->rendered->cropWidth = round($this->resizeHeightFactor() * $this->source->cropWidth); + } // if + } + else if ($this->isCroppingNeeded()) // No resizing is needed but we still need to crop + { + $ratio = ($this->resizeUncroppedWidthFactor() > $this->resizeUncroppedHeightFactor()) + ? $this->resizeUncroppedWidthFactor() : $this->resizeUncroppedHeightFactor(); + + $this->rendered->width = round($ratio * $this->source->width); + $this->rendered->height = round($ratio * $this->source->height); + + $this->rendered->cropWidth = round($ratio * $this->source->cropWidth); + $this->rendered->cropHeight = round($ratio * $this->source->cropHeight); + } // if + + // Determine the quality of the output image + $this->rendered->quality = ($this->request->quality !== NULL) + ? $this->request->quality : SLIRConfig::$defaultQuality; + + // Set up the appropriate image handling parameters based on the original + // image's mime type + // @todo some of this code should be moved to the SLIRImage class + $this->rendered->mime = $this->source->mime; + if ($this->source->isGIF()) + { + // We need to convert GIFs to PNGs + $this->rendered->mime = 'image/png'; + $this->rendered->progressive = FALSE; + + // We are converting the GIF to a PNG, and PNG needs a + // compression level of 0 (no compression) through 9 + $this->rendered->quality = round(10 - ($this->rendered->quality / 10)); + } + else if ($this->source->isPNG()) + { + $this->rendered->progressive = FALSE; + + // PNG needs a compression level of 0 (no compression) through 9 + $this->rendered->quality = round(10 - ($this->rendered->quality / 10)); + } + else if ($this->source->isJPEG()) + { + $this->rendered->progressive = ($this->request->progressive !== NULL) + ? $this->request->progressive : SLIRConfig::$defaultProgressiveJPEG; + $this->rendered->background = NULL; + } + else + { + throw new SLIRException("Unable to determine type of source image"); + } // if + + if ($this->isBackgroundFillOn()) + { + $this->rendered->background = $this->request->background; + } + } + + /** + * Detemrines if the image should be resized based on its width (i.e. the width is the constraining dimension for this request) + * + * @since 2.0 + * @return boolean + */ + private function shouldResizeBasedOnWidth() + { + if (floor($this->resizeWidthFactor() * $this->source->height) <= $this->request->height) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Detemrines if the image should be resized based on its height (i.e. the height is the constraining dimension for this request) + * @since 2.0 + * @return boolean + */ + private function shouldResizeBasedOnHeight() + { + if (floor($this->resizeHeightFactor() * $this->source->width) <= $this->request->width) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return float + */ + private function resizeWidthFactor() + { + if ($this->source->cropWidth !== NULL) + { + return $this->resizeCroppedWidthFactor(); + } + else + { + return $this->resizeUncroppedWidthFactor(); + } + } + + /** + * @since 2.0 + * @return float + */ + private function resizeUncroppedWidthFactor() + { + return $this->request->width / $this->source->width; + } + + /** + * @since 2.0 + * @return float + */ + private function resizeCroppedWidthFactor() + { + return $this->request->width / $this->source->cropWidth; + } + + /** + * @since 2.0 + * @return float + */ + private function resizeHeightFactor() + { + if ($this->source->cropHeight !== NULL) + { + return $this->resizeCroppedHeightFactor(); + } + else + { + return $this->resizeUncroppedHeightFactor(); + } + } + + /** + * @since 2.0 + * @return float + */ + private function resizeUncroppedHeightFactor() + { + return $this->request->height / $this->source->height; + } + + /** + * @since 2.0 + * @return float + */ + private function resizeCroppedHeightFactor() + { + return $this->request->height / $this->source->cropHeight; + } + + /** + * Determines if the rendered file is in the rendered cache + * + * @since 2.0 + * @return boolean + */ + private function isRenderedCached() + { + return $this->isCached($this->renderedCacheFilePath()); + } + + /** + * Determines if the request is symlinked to the rendered file + * + * @since 2.0 + * @return boolean + */ + private function isRequestCached() + { + return $this->isCached($this->requestCacheFilePath()); + } + + /** + * Determines if a given file exists in the cache + * + * @since 2.0 + * @param string $cacheFilePath + * @return boolean + */ + private function isCached($cacheFilePath) + { + if (!file_exists($cacheFilePath)) + { + return FALSE; + } + + $cacheModified = filemtime($cacheFilePath); + + if (!$cacheModified) + { + return FALSE; + } + + $imageModified = filectime($this->request->fullPath()); + + if ($imageModified >= $cacheModified) + { + return FALSE; + } + else + { + return TRUE; + } + } + + /** + * @since 2.0 + * @return string + */ + private function getRenderedCacheDir() + { + return SLIRConfig::$cacheDir . '/rendered'; + } + + /** + * @since 2.0 + * @return string + */ + private function renderedCacheFilePath() + { + return $this->getRenderedCacheDir() . $this->renderedCacheFilename(); + } + + /** + * @since 2.0 + * @return string + */ + private function renderedCacheFilename() + { + return '/' . md5($this->request->fullPath() . serialize($this->rendered->cacheParameters())); + } + + /** + * @since 2.0 + * @return string + */ + private function requestCacheFilename() + { + return '/' . md5($_SERVER['HTTP_HOST'] . '/' . $this->requestURI() . '/' . SLIRConfig::$defaultCropper); + } + + /** + * @since 2.0 + * @return string + */ + private function requestURI() + { + if (SLIRConfig::$forceQueryString === TRUE) + { + return $_SERVER['SCRIPT_NAME'] . '?' . http_build_query($_GET); + } + else + { + return $_SERVER['REQUEST_URI']; + } + } + + /** + * @since 2.0 + * @return string + */ + private function getRequestCacheDir() + { + return SLIRConfig::$cacheDir . '/request'; + } + + /** + * @since 2.0 + * @return string + */ + private function requestCacheFilePath() + { + return $this->getRequestCacheDir() . $this->requestCacheFilename(); + } + + /** + * Write an image to the cache + * + * @since 2.0 + * @param string $imageData + * @return boolean + */ + private function cache() + { + $this->cacheRendered(); + + if (SLIRConfig::$useRequestCache === TRUE) + { + return $this->cacheRequest($this->rendered->data, TRUE); + } + else + { + return TRUE; + } + } + + /** + * Write an image to the cache based on the properties of the rendered image + * + * @since 2.0 + * @return boolean + */ + private function cacheRendered() + { + $this->rendered->data = $this->cacheFile( + $this->renderedCacheFilePath(), + $this->rendered->data, + TRUE + ); + + return TRUE; + } + + /** + * Write an image to the cache based on the request URI + * + * @since 2.0 + * @param string $imageData + * @param boolean $copyEXIF + * @return string + */ + private function cacheRequest($imageData, $copyEXIF = TRUE) + { + return $this->cacheFile( + $this->requestCacheFilePath(), + $imageData, + $copyEXIF, + $this->renderedCacheFilePath() + ); + } + + /** + * Write an image to the cache based on the properties of the rendered image + * + * @since 2.0 + * @param string $cacheFilePath + * @param string $imageData + * @param boolean $copyEXIF + * @param string $symlinkToPath + * @return string|boolean + */ + private function cacheFile($cacheFilePath, $imageData, $copyEXIF = TRUE, $symlinkToPath = NULL) + { + $this->initializeCache(); + + // Try to create just a symlink to minimize disk space + if ($symlinkToPath && function_exists('symlink') && (file_exists($cacheFilePath) || symlink('../'.$symlinkToPath, $cacheFilePath))) + { + return TRUE; + } + + // Create the file + if (!file_put_contents($cacheFilePath, $imageData)) + { + return FALSE; + } + + if (SLIRConfig::$copyEXIF == TRUE && $copyEXIF && $this->source->isJPEG()) + { + // Copy IPTC data + if (isset($this->source->iptc) && !$this->copyIPTC($cacheFilePath)) + { + return FALSE; + } + + // Copy EXIF data + $imageData = $this->copyEXIF($cacheFilePath); + } // if + + return $imageData; + } + + /** + * Copy the source image's EXIF information to the new file in the cache + * + * @since 2.0 + * @uses PEL + * @param string $cacheFilePath + * @return mixed string contents of image on success, FALSE on failure + */ + private function copyEXIF($cacheFilePath) + { + // Make sure to suppress strict warning thrown by PEL + @require_once dirname(__FILE__) . '/pel-0.9.2/src/PelJpeg.php'; + + $jpeg = new PelJpeg($this->source->fullPath()); + $exif = $jpeg->getExif(); + + if ($exif) + { + $jpeg = new PelJpeg($cacheFilePath); + $jpeg->setExif($exif); + $imageData = $jpeg->getBytes(); + if (!file_put_contents($cacheFilePath, $imageData)) + { + return FALSE; + } + + return $imageData; + } // if + + return file_get_contents($cacheFilePath); + } + + /** + * Makes sure the cache directory exists, is readable, and is writable + * + * @since 2.0 + * @return boolean + */ + private function initializeCache() + { + if ($this->isCacheInitialized) + { + return TRUE; + } + + $this->initializeDirectory(SLIRConfig::$cacheDir); + $this->initializeDirectory(SLIRConfig::$cacheDir . '/rendered', FALSE); + $this->initializeDirectory(SLIRConfig::$cacheDir . '/request', FALSE); + + $this->isCacheInitialized = TRUE; + return TRUE; + } + + /** + * @since 2.0 + * @param string $path Directory to initialize + * @param boolean $verifyReadWriteability + * @return boolean + */ + private function initializeDirectory($path, $verifyReadWriteability = TRUE, $test = FALSE) + { + if (!file_exists($path)) + { + if (!@mkdir($path, 0755, TRUE)) + { + header('HTTP/1.1 500 Internal Server Error'); + throw new SLIRException("Directory ($path) does not exist and was unable to be created. Please create the directory."); + } + } + + if (!$verifyReadWriteability) + return TRUE; + + // Make sure we can read and write the cache directory + if (!is_readable($path)) + { + header('HTTP/1.1 500 Internal Server Error'); + throw new SLIRException("Directory ($path) is not readable"); + } + else if (!is_writable($path)) + { + header('HTTP/1.1 500 Internal Server Error'); + throw new SLIRException("Directory ($path) is not writable"); + } + + return TRUE; + } + + /** + * Serves the unmodified source image + * + * @since 2.0 + * @return void + */ + private function serveSourceImage() + { + $this->serveFile( + $this->source->fullPath(), + NULL, + NULL, + NULL, + $this->source->mime, + 'source' + ); + + exit(); + } + + /** + * Serves the image from the cache based on the properties of the rendered + * image + * + * @since 2.0 + * @return void + */ + private function serveRenderedCachedImage() + { + return $this->serveCachedImage($this->renderedCacheFilePath(), 'rendered'); + } + + /** + * Serves the image from the cache based on the request URI + * + * @since 2.0 + * @return void + */ + private function serveRequestCachedImage() + { + return $this->serveCachedImage($this->requestCacheFilePath(), 'request'); + } + + /** + * Serves the image from the cache + * + * @since 2.0 + * @param string $cacheFilePath + * @param string $cacheType Can be 'request' or 'image' + * @return void + */ + private function serveCachedImage($cacheFilePath, $cacheType) + { + // Serve the image + $data = $this->serveFile( + $cacheFilePath, + NULL, + NULL, + NULL, + NULL, + "$cacheType cache" + ); + + // If we are serving from the rendered cache, create a symlink in the + // request cache to the rendered file + if ($cacheType != 'request') + { + $this->cacheRequest($data, FALSE); + } + + exit(); + } + + /** + * Determines the mime type of an image + * + * @since 2.0 + * @param string $path + * @return string + */ + private function mimeType($path) + { + $info = getimagesize($path); + return $info['mime']; + } + + /** + * Serves the rendered image + * + * @since 2.0 + * @return void + */ + private function serveRenderedImage() + { + // Cache the image + $this->cache(); + + // Serve the file + $this->serveFile( + NULL, + $this->rendered->data, + gmdate('U'), + $this->rendered->fileSize(), + $this->rendered->mime, + 'rendered' + ); + + // Clean up memory + $this->rendered->destroyImage(); + + exit(); + } + + /** + * Serves a file + * + * @since 2.0 + * @param string $imagePath Path to file to serve + * @param string $data Data of file to serve + * @param integer $lastModified Timestamp of when the file was last modified + * @param string $mimeType + * @param string $SLIRheader + * @return string Image data + */ + private function serveFile($imagePath, $data, $lastModified, $length, $mimeType, $SLIRHeader) + { + if ($imagePath != NULL) + { + if ($lastModified == NULL) + { + $lastModified = filemtime($imagePath); + } + if ($length == NULL) + { + $length = filesize($imagePath); + } + if ($mimeType == NULL) + { + $mimeType = $this->mimeType($imagePath); + } + } + else if ($length == NULL) + { + $length = strlen($data); + } // if + + // Serve the headers + $this->serveHeaders( + $this->lastModified($lastModified), + $mimeType, + $length, + $SLIRHeader + ); + + // Read the image data into memory if we need to + if ($data == NULL) + { + $data = file_get_contents($imagePath); + } + + // Send the image to the browser in bite-sized chunks + $chunkSize = 1024 * 8; + $fp = fopen('php://memory', 'r+b'); + fwrite($fp, $data); + rewind($fp); + while (!feof($fp)) + { + echo fread($fp, $chunkSize); + flush(); + } // while + fclose($fp); + + return $data; + } + + /** + * Serves headers for file for optimal browser caching + * + * @since 2.0 + * @param string $lastModified Time when file was last modified in 'D, d M Y H:i:s' format + * @param string $mimeType + * @param integer $fileSize + * @param string $SLIRHeader + * @return void + */ + private function serveHeaders($lastModified, $mimeType, $fileSize, $SLIRHeader) + { + header("Last-Modified: $lastModified"); + header("Content-Type: $mimeType"); + header("Content-Length: $fileSize"); + + // Lets us easily know whether the image was rendered from scratch, + // from the cache, or served directly from the source image + header("Content-SLIR: $SLIRHeader"); + + // Keep in browser cache how long? + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + SLIRConfig::$browserCacheTTL) . ' GMT'); + + // Public in the Cache-Control lets proxies know that it is okay to + // cache this content. If this is being served over HTTPS, there may be + // sensitive content and therefore should probably not be cached by + // proxy servers. + header('Cache-Control: max-age=' . SLIRConfig::$browserCacheTTL . ', public'); + + $this->doConditionalGet($lastModified); + + // The "Connection: close" header allows us to serve the file and let + // the browser finish processing the script so we can do extra work + // without making the user wait. This header must come last or the file + // size will not properly work for images in the browser's cache + //header('Connection: close'); + } + + /** + * Converts a UNIX timestamp into the format needed for the Last-Modified + * header + * + * @since 2.0 + * @param integer $timestamp + * @return string + */ + private function lastModified($timestamp) + { + return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT'; + } + + /** + * Checks the to see if the file is different than the browser's cache + * + * @since 2.0 + * @param string $lastModified + * @return void + */ + private function doConditionalGet($lastModified) + { + $ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) ? + stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : + FALSE; + + if (!$ifModifiedSince || $ifModifiedSince != $lastModified) + { + return; + } + + // Nothing has changed since their last request - serve a 304 and exit + header('HTTP/1.1 304 Not Modified'); + + // Serve a "Connection: close" header here in case there are any + // shutdown functions that have been registered with + // register_shutdown_function() + header('Connection: close'); + + exit(); + } + +} // class SLIR + +// old pond +// a frog jumps +// the sound of water + // —Matsuo Basho \ No newline at end of file diff --git a/app/parsers/slir/slirconfig.class.php b/app/parsers/slir/slirconfig.class.php index 0c6a12d1..ee9b3ff1 100644 --- a/app/parsers/slir/slirconfig.class.php +++ b/app/parsers/slir/slirconfig.class.php @@ -1,55 +1,55 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slirconfig-sample.class.php 123 2010-12-21 18:58:03Z joe.lencioni $ */ - -require_once 'slirconfigdefaults.class.php'; -require_once '../../../extensions/config.php'; - -/** - * SLIR Config Class - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-21 12:58:03 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 123 $ - * @package SLIR - */ -class SLIRConfig extends SLIRConfigDefaults -{ - // override configuration values here - - public static $SLIRDir = 'render'; - - public static function init() - { - self::$cacheDir = '../../../'.Config::$cache_folder.'/images'; - self::$documentRoot = '../../..'; - // This must be the last line of this function - parent::init(); - } -} - -SLIRConfig::init(); +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slirconfig-sample.class.php 123 2010-12-21 18:58:03Z joe.lencioni $ */ + +require_once 'slirconfigdefaults.class.php'; +require_once '../../../extensions/config.php'; + +/** + * SLIR Config Class + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-21 12:58:03 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 123 $ + * @package SLIR + */ +class SLIRConfig extends SLIRConfigDefaults +{ + // override configuration values here + + public static $SLIRDir = 'render'; + + public static function init() + { + self::$cacheDir = '../../../'.Config::$cache_folder.'/images'; + self::$documentRoot = '../../..'; + // This must be the last line of this function + parent::init(); + } +} + +SLIRConfig::init(); diff --git a/app/parsers/slir/slirconfigdefaults.class.php b/app/parsers/slir/slirconfigdefaults.class.php index 03fc0ece..6a592086 100644 --- a/app/parsers/slir/slirconfigdefaults.class.php +++ b/app/parsers/slir/slirconfigdefaults.class.php @@ -1,228 +1,228 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slirconfigdefaults.class.php 126 2010-12-22 18:43:22Z joe.lencioni $ */ - -/** - * SLIR Config Class - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-22 12:43:22 -0600 (Wed, 22 Dec 2010) $ - * @version $Revision: 126 $ - * @package SLIR - */ -class SLIRConfigDefaults -{ - /** - * How long (in seconds) the web browser should use its cached copy of the image - * before checking with the server for a new version - * - * @since 2.0 - * @var integer - */ - public static $browserCacheTTL = 604800; // 7 * 24 * 60 * 60 - - /** - * Whether we should use the faster, symlink-based request cache as a first - * line cache - * - * @since 2.0 - * @var boolean - */ - public static $useRequestCache = TRUE; - - /** - * Whether EXIF information should be copied from the source image - * - * @since 2.0 - * @var boolean - */ - public static $copyEXIF = FALSE; - - /** - * How much memory (in megabytes) SLIR is allowed to allocate for memory-intensive processes such as rendering - * - * @since 2.0 - * @var integer - */ - public static $maxMemoryToAllocate = 100; - - /** - * Default quality setting to use if quality is not specified in the request. - * Ranges from 0 (worst quality, smaller file) to 100 (best quality, largest - * filesize). - * - * @since 2.0 - * @var integer - */ - public static $defaultQuality = 80; - - /** - * Default crop mode setting to use if crop mode is not specified in the request. - * - * Possible values are: - * SLIR::CROP_CLASS_CENTERED - * SLIR::CROP_CLASS_TOP_CENTERED - * SLIR::CROP_CLASS_SMART - * SLIR::CROP_CLASS_FACE (not finished) - * - * @since 2.0 - * @var string - */ - public static $defaultCropper = SLIR::CROP_CLASS_CENTERED; - - /** - * Default setting for whether JPEGs should be progressive JPEGs (interlaced) - * or not. - * - * @since 2.0 - * @var boolean - */ - public static $defaultProgressiveJPEG = TRUE; - - /** - * Whether SLIR should log errors - * - * @since 2.0 - * @var boolean - */ - public static $logErrors = TRUE; - - /** - * Whether SLIR should generate and output images from error messages - * - * @since 2.0 - * @var boolean - */ - public static $errorImages = TRUE; - - /** - * Absolute path to the web root (location of files when visiting - * http://domainname.com/) (no trailing slash) - * - * @since 2.0 - * @var string - */ - public static $documentRoot = NULL; - - /** - * Path to SLIR (no trailing slash) - * - * @since 2.0 - * @var string - */ - public static $SLIRDir = NULL; - - /** - * Name of directory to store cached files in (no trailing slash) - * - * @since 2.0 - * @var string - */ - public static $cacheDirName = '/cache'; - - /** - * Absolute path to cache directory. This directory must be world-readable, - * writable by the web server, and must end with SLIR_CACHE_DIR_NAME (no - * trailing slash). Ideally, this should be located outside of the web tree. - * - * @var string - */ - public static $cacheDir = NULL; - - /** - * Path to the error log file. Needs to be writable by the web server. Ideally, - * this should be located outside of the web tree. - * - * @since 2.0 - * @var string - */ - public static $errorLogPath = NULL; - - /** - * If TRUE, forces SLIR to always use the query string for parameters instead - * of mod_rewrite. - * - * @since 2.0 - * @var boolean - */ - public static $forceQueryString = FALSE; - - /** - * In conjunction with $garbageCollectDivisor is used to manage probability that the garbage collection routine is started. - * - * @since 2.0 - * @var integer - */ - public static $garbageCollectProbability = 1; - - /** - * Coupled with $garbageCollectProbability defines the probability that the garbage collection process is started on every request. - * - * The probability is calculated by using $garbageCollectProbability/$garbageCollectDivisor, e.g. 1/100 means there is a 1% chance that the garbage collection process starts on each request. - * - * @since 2.0 - * @var integer - */ - public static $garbageCollectDivisor = 200; - - /** - * Specifies the number of seconds after which data will be seen as 'garbage' and potentially cleaned up (deleted from the cache). - * - * @since 2.0 - * @var integer - */ - public static $garbageCollectFileCacheMaxLifetime = 86400; // 1 day = 1 * 24 * 60 * 60 - - /** - * Initialize variables that require some dynamic processing. - * - * @since 2.0 - * @return void - */ - public static function init() - { - if (self::$documentRoot === NULL) - { - self::$documentRoot = preg_replace('/\/$/', '', $_SERVER['DOCUMENT_ROOT']); - } - - if (self::$SLIRDir === NULL) - { - self::$SLIRDir = dirname($_SERVER['SCRIPT_NAME']); - } - - if (self::$cacheDir === NULL) - { - self::$cacheDir = self::$documentRoot . self::$SLIRDir . self::$cacheDirName; - } - - if (self::$errorLogPath === NULL) - { - self::$documentRoot . self::$SLIRDir . '/slir-error-log'; - } - } +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slirconfigdefaults.class.php 126 2010-12-22 18:43:22Z joe.lencioni $ */ + +/** + * SLIR Config Class + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-22 12:43:22 -0600 (Wed, 22 Dec 2010) $ + * @version $Revision: 126 $ + * @package SLIR + */ +class SLIRConfigDefaults +{ + /** + * How long (in seconds) the web browser should use its cached copy of the image + * before checking with the server for a new version + * + * @since 2.0 + * @var integer + */ + public static $browserCacheTTL = 604800; // 7 * 24 * 60 * 60 + + /** + * Whether we should use the faster, symlink-based request cache as a first + * line cache + * + * @since 2.0 + * @var boolean + */ + public static $useRequestCache = TRUE; + + /** + * Whether EXIF information should be copied from the source image + * + * @since 2.0 + * @var boolean + */ + public static $copyEXIF = FALSE; + + /** + * How much memory (in megabytes) SLIR is allowed to allocate for memory-intensive processes such as rendering + * + * @since 2.0 + * @var integer + */ + public static $maxMemoryToAllocate = 100; + + /** + * Default quality setting to use if quality is not specified in the request. + * Ranges from 0 (worst quality, smaller file) to 100 (best quality, largest + * filesize). + * + * @since 2.0 + * @var integer + */ + public static $defaultQuality = 80; + + /** + * Default crop mode setting to use if crop mode is not specified in the request. + * + * Possible values are: + * SLIR::CROP_CLASS_CENTERED + * SLIR::CROP_CLASS_TOP_CENTERED + * SLIR::CROP_CLASS_SMART + * SLIR::CROP_CLASS_FACE (not finished) + * + * @since 2.0 + * @var string + */ + public static $defaultCropper = SLIR::CROP_CLASS_CENTERED; + + /** + * Default setting for whether JPEGs should be progressive JPEGs (interlaced) + * or not. + * + * @since 2.0 + * @var boolean + */ + public static $defaultProgressiveJPEG = TRUE; + + /** + * Whether SLIR should log errors + * + * @since 2.0 + * @var boolean + */ + public static $logErrors = TRUE; + + /** + * Whether SLIR should generate and output images from error messages + * + * @since 2.0 + * @var boolean + */ + public static $errorImages = TRUE; + + /** + * Absolute path to the web root (location of files when visiting + * http://domainname.com/) (no trailing slash) + * + * @since 2.0 + * @var string + */ + public static $documentRoot = NULL; + + /** + * Path to SLIR (no trailing slash) + * + * @since 2.0 + * @var string + */ + public static $SLIRDir = NULL; + + /** + * Name of directory to store cached files in (no trailing slash) + * + * @since 2.0 + * @var string + */ + public static $cacheDirName = '/cache'; + + /** + * Absolute path to cache directory. This directory must be world-readable, + * writable by the web server, and must end with SLIR_CACHE_DIR_NAME (no + * trailing slash). Ideally, this should be located outside of the web tree. + * + * @var string + */ + public static $cacheDir = NULL; + + /** + * Path to the error log file. Needs to be writable by the web server. Ideally, + * this should be located outside of the web tree. + * + * @since 2.0 + * @var string + */ + public static $errorLogPath = NULL; + + /** + * If TRUE, forces SLIR to always use the query string for parameters instead + * of mod_rewrite. + * + * @since 2.0 + * @var boolean + */ + public static $forceQueryString = FALSE; + + /** + * In conjunction with $garbageCollectDivisor is used to manage probability that the garbage collection routine is started. + * + * @since 2.0 + * @var integer + */ + public static $garbageCollectProbability = 1; + + /** + * Coupled with $garbageCollectProbability defines the probability that the garbage collection process is started on every request. + * + * The probability is calculated by using $garbageCollectProbability/$garbageCollectDivisor, e.g. 1/100 means there is a 1% chance that the garbage collection process starts on each request. + * + * @since 2.0 + * @var integer + */ + public static $garbageCollectDivisor = 200; + + /** + * Specifies the number of seconds after which data will be seen as 'garbage' and potentially cleaned up (deleted from the cache). + * + * @since 2.0 + * @var integer + */ + public static $garbageCollectFileCacheMaxLifetime = 86400; // 1 day = 1 * 24 * 60 * 60 + + /** + * Initialize variables that require some dynamic processing. + * + * @since 2.0 + * @return void + */ + public static function init() + { + if (self::$documentRoot === NULL) + { + self::$documentRoot = preg_replace('/\/$/', '', $_SERVER['DOCUMENT_ROOT']); + } + + if (self::$SLIRDir === NULL) + { + self::$SLIRDir = dirname($_SERVER['SCRIPT_NAME']); + } + + if (self::$cacheDir === NULL) + { + self::$cacheDir = self::$documentRoot . self::$SLIRDir . self::$cacheDirName; + } + + if (self::$errorLogPath === NULL) + { + self::$documentRoot . self::$SLIRDir . '/slir-error-log'; + } + } } \ No newline at end of file diff --git a/app/parsers/slir/slirexception.class.php b/app/parsers/slir/slirexception.class.php index cabb1fff..7b232fc5 100644 --- a/app/parsers/slir/slirexception.class.php +++ b/app/parsers/slir/slirexception.class.php @@ -1,246 +1,246 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slirexception.class.php 123 2010-12-21 18:58:03Z joe.lencioni $ */ - -/** - * Exception and error handler - * - * @since 2.0 - * @author Joe Lencioni - * @date $Date: 2010-12-21 12:58:03 -0600 (Tue, 21 Dec 2010) $ - * @version $Revision: 123 $ - * @package SLIR - */ -class SLIRException extends Exception -{ - /** - * Max number of characters to wrap error message at - * - * @since 2.0 - * @var integer - */ - const WRAP_AT = 65; - - /** - * Text size to use in imagestring(). Possible values are 1, 2, 3, 4, or 5 - * - * @since 2.0 - * @var integer - */ - const TEXT_SIZE = 4; - - /** - * Height of one line of text, in pixels - * - * @since 2.0 - * @var integer - */ - const LINE_HEIGHT = 16; - - /** - * Width of one character of text, in pixels - * - * @since 2.0 - * @var integer - */ - const CHAR_WIDTH = 8; - - /** - * @since 2.0 - * @param Exception $exception - * @param string $explanationText - */ - public function __construct($exception, $explanationText = NULL) - { - parent::__construct($exception); - - /* @todo fix this - if (defined('SLIR_ERROR_LOG_PATH') && (!defined('SLIR_LOG_ERRORS') || SLIR_LOG_ERRORS !== FALSE)) - { - $log = $this->log(); - if (!$log) - { - $explanationText .= "\n\nAlso could not log error to file. " - . 'Please create a file at \'' . SLIR_ERROR_LOG_PATH . '\' ' - . 'and give the web server permissions to write to it.'; - } - } // if - */ - - if (SLIRConfig::$errorImages === TRUE) - { - $this->errorImage($explanationText); - } - else - { - $this->errorText($explanationText); - } - } - - /** - * Determines if a constant is not defined - * - * @since 2.0 - * @param string $constantName - * @return boolean - */ - public function isNotDefined($constantName) - { - return (!defined($constantName)); - } - - /** - * Logs the error to a file - * - * @since 2.0 - */ - private function log() - { - $userAgent = (isset($_SERVER['HTTP_USER_AGENT'])) ? " {$_SERVER['HTTP_USER_AGENT']}" : ''; - $referrer = (isset($_SERVER['HTTP_REFERER'])) ? "Referrer: {$_SERVER['HTTP_REFERER']}\n\n" : ''; - $request = (isset($_SERVER['REQUEST_URI'])) ? "Request: {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}\n" : ''; - - $message = "\n[" . @gmdate('D M d H:i:s Y') . '] [' - . $_SERVER['REMOTE_ADDR'] . $userAgent . '] ' . $this->getMessage() - . "\n\n" . $referrer . $request . $this->getTraceAsString() . "\n"; - - return @error_log($message, 3, SLIRConfig::$errorLogPath); - } - - /** - * @since 2.0 - * @param string $explanationText - * @return string Error message - */ - private function errorMessage($explanationText = NULL) - { - $text = $this->getMessage(); - if ($explanationText) - { - $text .= "\n\n$explanationText"; - } - - return $text; - } - - /** - * Create and output an image with an error message - * - * @since 2.0 - * @param string $explanationText - */ - private function errorImage($explanationText = NULL) - { - $text = $this->errorMessage($explanationText); - - $text = wordwrap($text, SLIRException::WRAP_AT); - $text = explode("\n", $text); - - // determine width - $characters = 0; - foreach($text as $line) - { - if (($temp = strlen($line)) > $characters) - { - $characters = $temp; - } - } // foreach - - // set up the image - $image = imagecreatetruecolor( - $characters * SLIRException::CHAR_WIDTH, - count($text) * SLIRException::LINE_HEIGHT - ); - $white = imagecolorallocate($image, 255, 255, 255); - imagefill($image, 0, 0, $white); - - // set text color - $textColor = imagecolorallocate($image, 200, 0, 0); // red - - // write the text to the image - $i = 0; - foreach($text as $line) - { - imagestring( - $image, - SLIRException::TEXT_SIZE, - 0, - $i * SLIRException::LINE_HEIGHT, - $line, - $textColor - ); - ++$i; - } - - // output the image - header('Content-type: image/png'); - imagepng($image); - - // clean up for memory - imagedestroy($image); - } - - /** - * @since 2.0 - * @param string $explanationText - */ - private function errorText($explanationText = NULL) - { - echo nl2br($this->errorMessage($explanationText) . "\n"); - } - - /** - * Error handler - * - * @since 2.0 - * @param integer $errno Level of the error raised - * @param string $errstr Error message - * @param string $errfile Filename that the error was raised in - * @param integer $errline Line number the error was raised at, - * @param array $errcontext Points to the active symbol table at the point the error occurred - */ - public static function error($errno, $errstr, $errfile = NULL, $errline = NULL, $errcontext = array()) - { - // if error has been supressed with an @ - if (error_reporting() == 0) - { - return; - } - - $message = $errno . ' ' .$errstr; - if ($errfile !== NULL) - { - $message .= "\n\nFile: $errfile"; - if ($errline !== NULL) - { - $message .= "\nLine $errline"; - } - } - - throw new SLIRException($message); - } +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slirexception.class.php 123 2010-12-21 18:58:03Z joe.lencioni $ */ + +/** + * Exception and error handler + * + * @since 2.0 + * @author Joe Lencioni + * @date $Date: 2010-12-21 12:58:03 -0600 (Tue, 21 Dec 2010) $ + * @version $Revision: 123 $ + * @package SLIR + */ +class SLIRException extends Exception +{ + /** + * Max number of characters to wrap error message at + * + * @since 2.0 + * @var integer + */ + const WRAP_AT = 65; + + /** + * Text size to use in imagestring(). Possible values are 1, 2, 3, 4, or 5 + * + * @since 2.0 + * @var integer + */ + const TEXT_SIZE = 4; + + /** + * Height of one line of text, in pixels + * + * @since 2.0 + * @var integer + */ + const LINE_HEIGHT = 16; + + /** + * Width of one character of text, in pixels + * + * @since 2.0 + * @var integer + */ + const CHAR_WIDTH = 8; + + /** + * @since 2.0 + * @param Exception $exception + * @param string $explanationText + */ + public function __construct($exception, $explanationText = NULL) + { + parent::__construct($exception); + + /* @todo fix this + if (defined('SLIR_ERROR_LOG_PATH') && (!defined('SLIR_LOG_ERRORS') || SLIR_LOG_ERRORS !== FALSE)) + { + $log = $this->log(); + if (!$log) + { + $explanationText .= "\n\nAlso could not log error to file. " + . 'Please create a file at \'' . SLIR_ERROR_LOG_PATH . '\' ' + . 'and give the web server permissions to write to it.'; + } + } // if + */ + + if (SLIRConfig::$errorImages === TRUE) + { + $this->errorImage($explanationText); + } + else + { + $this->errorText($explanationText); + } + } + + /** + * Determines if a constant is not defined + * + * @since 2.0 + * @param string $constantName + * @return boolean + */ + public function isNotDefined($constantName) + { + return (!defined($constantName)); + } + + /** + * Logs the error to a file + * + * @since 2.0 + */ + private function log() + { + $userAgent = (isset($_SERVER['HTTP_USER_AGENT'])) ? " {$_SERVER['HTTP_USER_AGENT']}" : ''; + $referrer = (isset($_SERVER['HTTP_REFERER'])) ? "Referrer: {$_SERVER['HTTP_REFERER']}\n\n" : ''; + $request = (isset($_SERVER['REQUEST_URI'])) ? "Request: {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}\n" : ''; + + $message = "\n[" . @gmdate('D M d H:i:s Y') . '] [' + . $_SERVER['REMOTE_ADDR'] . $userAgent . '] ' . $this->getMessage() + . "\n\n" . $referrer . $request . $this->getTraceAsString() . "\n"; + + return @error_log($message, 3, SLIRConfig::$errorLogPath); + } + + /** + * @since 2.0 + * @param string $explanationText + * @return string Error message + */ + private function errorMessage($explanationText = NULL) + { + $text = $this->getMessage(); + if ($explanationText) + { + $text .= "\n\n$explanationText"; + } + + return $text; + } + + /** + * Create and output an image with an error message + * + * @since 2.0 + * @param string $explanationText + */ + private function errorImage($explanationText = NULL) + { + $text = $this->errorMessage($explanationText); + + $text = wordwrap($text, SLIRException::WRAP_AT); + $text = explode("\n", $text); + + // determine width + $characters = 0; + foreach($text as $line) + { + if (($temp = strlen($line)) > $characters) + { + $characters = $temp; + } + } // foreach + + // set up the image + $image = imagecreatetruecolor( + $characters * SLIRException::CHAR_WIDTH, + count($text) * SLIRException::LINE_HEIGHT + ); + $white = imagecolorallocate($image, 255, 255, 255); + imagefill($image, 0, 0, $white); + + // set text color + $textColor = imagecolorallocate($image, 200, 0, 0); // red + + // write the text to the image + $i = 0; + foreach($text as $line) + { + imagestring( + $image, + SLIRException::TEXT_SIZE, + 0, + $i * SLIRException::LINE_HEIGHT, + $line, + $textColor + ); + ++$i; + } + + // output the image + header('Content-type: image/png'); + imagepng($image); + + // clean up for memory + imagedestroy($image); + } + + /** + * @since 2.0 + * @param string $explanationText + */ + private function errorText($explanationText = NULL) + { + echo nl2br($this->errorMessage($explanationText) . "\n"); + } + + /** + * Error handler + * + * @since 2.0 + * @param integer $errno Level of the error raised + * @param string $errstr Error message + * @param string $errfile Filename that the error was raised in + * @param integer $errline Line number the error was raised at, + * @param array $errcontext Points to the active symbol table at the point the error occurred + */ + public static function error($errno, $errstr, $errfile = NULL, $errline = NULL, $errcontext = array()) + { + // if error has been supressed with an @ + if (error_reporting() == 0) + { + return; + } + + $message = $errno . ' ' .$errstr; + if ($errfile !== NULL) + { + $message .= "\n\nFile: $errfile"; + if ($errline !== NULL) + { + $message .= "\nLine $errline"; + } + } + + throw new SLIRException($message); + } } // SLIRException \ No newline at end of file diff --git a/app/parsers/slir/slirimage.class.php b/app/parsers/slir/slirimage.class.php index 80041406..d85b3cb7 100644 --- a/app/parsers/slir/slirimage.class.php +++ b/app/parsers/slir/slirimage.class.php @@ -1,729 +1,729 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slirimage.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ - -/** - * SLIR image class - * - * @since 2.0 - * @author Joe Lencioni - * $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ - * @version $Revision: 129 $ - * @package SLIR - */ -class SLIRImage -{ - /** - * Path to this image file - * @var string - * @since 2.0 - */ - private $path; - - /** - * Image data - * @var string - * @since 2.0 - */ - private $data; - - /** - * Image identifier - * @var resource - * @since 2.0 - */ - private $image; - - /** - * MIME type of this image - * @var string - * @since 2.0 - */ - private $mime; - - /** - * Width of image in pixels - * @var integer - * @since 2.0 - */ - private $width; - - /** - * Height of image in pixels - * @var integer - * @since 2.0 - */ - private $height; - - /** - * Width of cropped image in pixels - * @var integer - * @since 2.0 - */ - private $cropWidth; - - /** - * Height of cropped image in pixels - * @var integer - * @since 2.0 - */ - private $cropHeight; - - /** - * Name of the cropper to use - * @var string - * @since 2.0 - */ - private $cropper; - - /** - * IPTC data embedded in image - * @var array - * @since 2.0 - */ - private $iptc; - - /** - * Quality of image - * @var integer - * @since 2.0 - */ - private $quality; - - /** - * Whether or not progressive JPEG output is turned on - * @var boolean - * @since 2.0 - */ - private $progressive; - - /** - * Color to fill background of transparent PNGs and GIFs - * @var string - * @since 2.0 - */ - public $background; - - /** - * @since 2.0 - */ - final public function __construct() - { - } - - /** - * @param string $name - * @param mixed $value - * @since 2.0 - */ - final public function __set($name, $value) - { - switch ($name) - { - case 'path': - $this->setPath($value); - break; - - default: - if (property_exists($this, $name)) - { - $this->$name = $value; - } - break; - } // switch - } - - /** - * @since 2.0 - */ - final public function __get($name) - { - switch($name) - { - case 'data': - if ($this->data === NULL) - { - $this->data = $this->getData(); - } - return $this->data; - break; - - default: - if (property_exists($this, $name)) - { - return $this->$name; - } - break; - } - } - - /** - * @param string $path - * @param boolean $loadImage - * @since 2.0 - */ - public function setPath($path, $loadImage = TRUE) - { - $this->path = $path; - - if ($loadImage === TRUE) - { - // Set the image info (width, height, mime type, etc.) - $this->setImageInfoFromFile(); - - // Make sure the file is actually an image - if (!$this->isImage()) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Requested file is not an ' - . 'accepted image type: ' . $this->fullPath()); - } // if - } - } - - /** - * @return float - * @since 2.0 - */ - final public function ratio() - { - return $this->width / $this->height; - } - - /** - * @return float - * @since 2.0 - */ - final public function cropRatio() - { - if ($this->cropHeight != 0) - { - return $this->cropWidth / $this->cropHeight; - } - else - { - return 0; - } - } - - /** - * @return integer - * @since 2.0 - */ - final public function area() - { - return $this->width * $this->height; - } - - /** - * @return string - * @since 2.0 - */ - final public function fullPath() - { - return SLIRConfig::$documentRoot . $this->path; - } - - /** - * Checks the mime type to see if it is an image - * - * @since 2.0 - * @return boolean - */ - final public function isImage() - { - if (substr($this->mime, 0, 6) == 'image/') - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @param string $type Can be 'JPEG', 'GIF', or 'PNG' - * @return boolean - */ - final public function isOfType($type = 'JPEG') - { - $method = "is$type"; - if (method_exists($this, $method) && isset($imageArray['mime'])) - { - return $this->$method(); - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isJPEG() - { - if ($this->mime == 'image/jpeg') - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isGIF() - { - if ($this->mime == 'image/gif') - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isPNG() - { - if (in_array($this->mime, array('image/png', 'image/x-png'))) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isAbleToHaveTransparency() - { - if ($this->isPNG() || $this->isGIF()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - private function isCroppingNeeded() - { - if ($this->cropWidth !== NULL && $this->cropHeight != NULL - && ($this->cropWidth < $this->width || $this->cropHeight < $this->height) - ) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - private function isSharpeningDesired() - { - if ($this->isJPEG()) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - */ - private function setImageInfoFromFile() - { - $info = $this->getImageInfoFromFile(); - - $this->mime = $info['mime']; - $this->width = $info['width']; - $this->height = $info['height']; - if (isset($info['iptc'])) - { - $this->iptc = $info['iptc']; - } - } - - /** - * Retrieves information about the image such as width, height, and IPTC info - * - * @since 2.0 - * @return array - */ - private function getImageInfoFromFile() - { - $info = getimagesize($this->fullPath(), $extraInfo); - - if ($info == FALSE) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('getimagesize failed (source file may not ' - . 'be an image): ' . $this->fullPath()); - } - - $info['width'] =& $info[0]; - $info['height'] =& $info[1]; - - // IPTC - if(is_array($extraInfo) && isset($extraInfo['APP13'])) - { - $info['iptc'] = iptcparse($extraInfo['APP13']); - } - - return $info; - } - - /** - * @since 2.0 - */ - final public function createBlankImage() - { - $this->image = imagecreatetruecolor($this->width, $this->height); - } - - /** - * @since 2.0 - */ - final public function createImageFromFile() - { - if ($this->isJPEG()) - { - $this->image = ImageCreateFromJpeg($this->fullPath()); - } - else if ($this->isGIF()) - { - $this->image = ImageCreateFromGif($this->fullPath()); - } - else if ($this->isPNG()) - { - $this->image = ImageCreateFromPng($this->fullPath()); - } - } - - /** - * Turns on transparency for image if no background fill color is - * specified, otherwise, fills background with specified color - * - * @param boolean $isBackgroundFillOn - * @since 2.0 - */ - final public function background($isBackgroundFillOn, $image = NULL) - { - if (!$this->isAbleToHaveTransparency()) - { - return; - } - - if ($image === NULL) - { - $image = $this->image; - } - - if (!$isBackgroundFillOn) - { - // If this is a GIF or a PNG, we need to set up transparency - $this->transparency($image); - } - else - { - // Fill the background with the specified color for matting purposes - $this->fillBackground($image); - } // if - } - - /** - * @since 2.0 - */ - private function transparency($image) - { - imagealphablending($image, FALSE); - imagesavealpha($image, TRUE); - } - - /** - * @since 2.0 - */ - private function fillBackground($image) - { - $background = imagecolorallocate( - $image, - hexdec($this->background[0].$this->background[1]), - hexdec($this->background[2].$this->background[3]), - hexdec($this->background[4].$this->background[5]) - ); - - imagefilledrectangle($image, 0, 0, $this->width, $this->height, $background); - } - - /** - * @since 2.0 - */ - final public function interlace() - { - if ($this->progressive) - { - imageinterlace($this->image, 1); - } - } - - /** - * Gets the name of the class that will be used to determine the crop offset for the image - * - * @since 2.0 - * @param string $className Name of the cropper class name to get - * @return string - */ - private function getCropperClassName($className = NULL) - { - if ($className !== NULL) - { - return $className; - } - else if ($this->cropper !== NULL) - { - return $this->cropper; - } - else - { - return SLIRConfig::$defaultCropper; - } - } - - /** - * Gets the class that will be used to determine the crop offset for the image - * - * @since 2.0 - * @param string $className Name of the cropper class to get - * @return SLIRCropper - */ - final public function getCropperClass($className = NULL) - { - $cropClass = strtolower($this->getCropperClassName($className)); - $fileName = "croppers/$cropClass.class.php"; - $class = 'SLIRCropper' . ucfirst($cropClass); - require_once $fileName; - return new $class(); - } - - /** - * Crops the image - * - * @since 2.0 - * @param boolean $isBackgroundFillOn - * @return boolean - * @todo improve cropping method preference (smart or centered) - */ - final public function crop($isBackgroundFillOn) - { - if (!$this->isCroppingNeeded()) - { - return TRUE; - } - - $cropper = $this->getCropperClass(); - $offset = $cropper->getCrop($this); - return $this->cropImage($offset['x'], $offset['y'], $isBackgroundFillOn); - } - - /** - * Performs the actual cropping of the image - * - * @since 2.0 - * @param integer $leftOffset Number of pixels from the left side of the image to crop in - * @param integer $topOffset Number of pixels from the top side of the image to crop in - * @param boolean $isBackgroundFillOn - * @return boolean - */ - private function cropImage($leftOffset, $topOffset, $isBackgroundFillOn) - { - // Set up a blank canvas for our cropped image (destination) - $cropped = imagecreatetruecolor( - $this->cropWidth, - $this->cropHeight - ); - - $this->background($isBackgroundFillOn, $cropped); - - // Copy rendered image to cropped image - ImageCopy( - $cropped, - $this->image, - 0, - 0, - $leftOffset, - $topOffset, - $this->width, - $this->height - ); - - // Replace pre-cropped image with cropped image - imagedestroy($this->image); - $this->image = $cropped; - unset($cropped); - - return TRUE; - } - - /** - * Sharpens the image - * - * @param integer $sharpness - * @since 2.0 - */ - final public function sharpen($sharpness) - { - if ($this->isSharpeningDesired()) - { - imageconvolution( - $this->image, - $this->sharpenMatrix($sharpness), - $sharpness, - 0 - ); - } - } - - /** - * @param integer $sharpness - * @return array - * @since 2.0 - */ - private function sharpenMatrix($sharpness) - { - return array( - array(-1, -2, -1), - array(-2, $sharpness + 12, -2), - array(-1, -2, -1) - ); - } - - /** - * @since 2.0 - * @return array - */ - final public function cacheParameters() - { - return array( - 'path' => $this->fullPath(), - 'width' => $this->width, - 'height' => $this->height, - 'cropWidth' => $this->cropWidth, - 'cropHeight' => $this->cropHeight, - 'iptc' => $this->iptc, - 'quality' => $this->quality, - 'progressive' => $this->progressive, - 'background' => $this->background, - 'cropper' => $this->getCropperClassName(), - ); - } - - /** - * @since 2.0 - * @return string - */ - private function getData() - { - ob_start(NULL); - if (!$this->output()) - { - return FALSE; - } - $data = ob_get_contents(); - ob_end_clean(); - - return $data; - } - - /** - * @since 2.0 - * @return boolean - */ - private function output($filename = NULL) - { - if ($this->isJPEG()) - { - return imagejpeg($this->image, $filename, $this->quality); - } - else if ($this->isPNG()) - { - return imagepng($this->image, $filename, $this->quality); - } - else if ($this->isGIF()) - { - return imagegif($this->image, $filename, $this->quality); - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return integer - */ - final public function fileSize() - { - return strlen($this->data); - } - - /** - * @since 2.0 - * @return boolean - */ - final public function destroyImage() - { - return imagedestroy($this->image); - } - +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slirimage.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ + +/** + * SLIR image class + * + * @since 2.0 + * @author Joe Lencioni + * $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ + * @version $Revision: 129 $ + * @package SLIR + */ +class SLIRImage +{ + /** + * Path to this image file + * @var string + * @since 2.0 + */ + private $path; + + /** + * Image data + * @var string + * @since 2.0 + */ + private $data; + + /** + * Image identifier + * @var resource + * @since 2.0 + */ + private $image; + + /** + * MIME type of this image + * @var string + * @since 2.0 + */ + private $mime; + + /** + * Width of image in pixels + * @var integer + * @since 2.0 + */ + private $width; + + /** + * Height of image in pixels + * @var integer + * @since 2.0 + */ + private $height; + + /** + * Width of cropped image in pixels + * @var integer + * @since 2.0 + */ + private $cropWidth; + + /** + * Height of cropped image in pixels + * @var integer + * @since 2.0 + */ + private $cropHeight; + + /** + * Name of the cropper to use + * @var string + * @since 2.0 + */ + private $cropper; + + /** + * IPTC data embedded in image + * @var array + * @since 2.0 + */ + private $iptc; + + /** + * Quality of image + * @var integer + * @since 2.0 + */ + private $quality; + + /** + * Whether or not progressive JPEG output is turned on + * @var boolean + * @since 2.0 + */ + private $progressive; + + /** + * Color to fill background of transparent PNGs and GIFs + * @var string + * @since 2.0 + */ + public $background; + + /** + * @since 2.0 + */ + final public function __construct() + { + } + + /** + * @param string $name + * @param mixed $value + * @since 2.0 + */ + final public function __set($name, $value) + { + switch ($name) + { + case 'path': + $this->setPath($value); + break; + + default: + if (property_exists($this, $name)) + { + $this->$name = $value; + } + break; + } // switch + } + + /** + * @since 2.0 + */ + final public function __get($name) + { + switch($name) + { + case 'data': + if ($this->data === NULL) + { + $this->data = $this->getData(); + } + return $this->data; + break; + + default: + if (property_exists($this, $name)) + { + return $this->$name; + } + break; + } + } + + /** + * @param string $path + * @param boolean $loadImage + * @since 2.0 + */ + public function setPath($path, $loadImage = TRUE) + { + $this->path = $path; + + if ($loadImage === TRUE) + { + // Set the image info (width, height, mime type, etc.) + $this->setImageInfoFromFile(); + + // Make sure the file is actually an image + if (!$this->isImage()) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Requested file is not an ' + . 'accepted image type: ' . $this->fullPath()); + } // if + } + } + + /** + * @return float + * @since 2.0 + */ + final public function ratio() + { + return $this->width / $this->height; + } + + /** + * @return float + * @since 2.0 + */ + final public function cropRatio() + { + if ($this->cropHeight != 0) + { + return $this->cropWidth / $this->cropHeight; + } + else + { + return 0; + } + } + + /** + * @return integer + * @since 2.0 + */ + final public function area() + { + return $this->width * $this->height; + } + + /** + * @return string + * @since 2.0 + */ + final public function fullPath() + { + return SLIRConfig::$documentRoot . $this->path; + } + + /** + * Checks the mime type to see if it is an image + * + * @since 2.0 + * @return boolean + */ + final public function isImage() + { + if (substr($this->mime, 0, 6) == 'image/') + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @param string $type Can be 'JPEG', 'GIF', or 'PNG' + * @return boolean + */ + final public function isOfType($type = 'JPEG') + { + $method = "is$type"; + if (method_exists($this, $method) && isset($imageArray['mime'])) + { + return $this->$method(); + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isJPEG() + { + if ($this->mime == 'image/jpeg') + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isGIF() + { + if ($this->mime == 'image/gif') + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isPNG() + { + if (in_array($this->mime, array('image/png', 'image/x-png'))) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isAbleToHaveTransparency() + { + if ($this->isPNG() || $this->isGIF()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + private function isCroppingNeeded() + { + if ($this->cropWidth !== NULL && $this->cropHeight != NULL + && ($this->cropWidth < $this->width || $this->cropHeight < $this->height) + ) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + private function isSharpeningDesired() + { + if ($this->isJPEG()) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + */ + private function setImageInfoFromFile() + { + $info = $this->getImageInfoFromFile(); + + $this->mime = $info['mime']; + $this->width = $info['width']; + $this->height = $info['height']; + if (isset($info['iptc'])) + { + $this->iptc = $info['iptc']; + } + } + + /** + * Retrieves information about the image such as width, height, and IPTC info + * + * @since 2.0 + * @return array + */ + private function getImageInfoFromFile() + { + $info = getimagesize($this->fullPath(), $extraInfo); + + if ($info == FALSE) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('getimagesize failed (source file may not ' + . 'be an image): ' . $this->fullPath()); + } + + $info['width'] =& $info[0]; + $info['height'] =& $info[1]; + + // IPTC + if(is_array($extraInfo) && isset($extraInfo['APP13'])) + { + $info['iptc'] = iptcparse($extraInfo['APP13']); + } + + return $info; + } + + /** + * @since 2.0 + */ + final public function createBlankImage() + { + $this->image = imagecreatetruecolor($this->width, $this->height); + } + + /** + * @since 2.0 + */ + final public function createImageFromFile() + { + if ($this->isJPEG()) + { + $this->image = ImageCreateFromJpeg($this->fullPath()); + } + else if ($this->isGIF()) + { + $this->image = ImageCreateFromGif($this->fullPath()); + } + else if ($this->isPNG()) + { + $this->image = ImageCreateFromPng($this->fullPath()); + } + } + + /** + * Turns on transparency for image if no background fill color is + * specified, otherwise, fills background with specified color + * + * @param boolean $isBackgroundFillOn + * @since 2.0 + */ + final public function background($isBackgroundFillOn, $image = NULL) + { + if (!$this->isAbleToHaveTransparency()) + { + return; + } + + if ($image === NULL) + { + $image = $this->image; + } + + if (!$isBackgroundFillOn) + { + // If this is a GIF or a PNG, we need to set up transparency + $this->transparency($image); + } + else + { + // Fill the background with the specified color for matting purposes + $this->fillBackground($image); + } // if + } + + /** + * @since 2.0 + */ + private function transparency($image) + { + imagealphablending($image, FALSE); + imagesavealpha($image, TRUE); + } + + /** + * @since 2.0 + */ + private function fillBackground($image) + { + $background = imagecolorallocate( + $image, + hexdec($this->background[0].$this->background[1]), + hexdec($this->background[2].$this->background[3]), + hexdec($this->background[4].$this->background[5]) + ); + + imagefilledrectangle($image, 0, 0, $this->width, $this->height, $background); + } + + /** + * @since 2.0 + */ + final public function interlace() + { + if ($this->progressive) + { + imageinterlace($this->image, 1); + } + } + + /** + * Gets the name of the class that will be used to determine the crop offset for the image + * + * @since 2.0 + * @param string $className Name of the cropper class name to get + * @return string + */ + private function getCropperClassName($className = NULL) + { + if ($className !== NULL) + { + return $className; + } + else if ($this->cropper !== NULL) + { + return $this->cropper; + } + else + { + return SLIRConfig::$defaultCropper; + } + } + + /** + * Gets the class that will be used to determine the crop offset for the image + * + * @since 2.0 + * @param string $className Name of the cropper class to get + * @return SLIRCropper + */ + final public function getCropperClass($className = NULL) + { + $cropClass = strtolower($this->getCropperClassName($className)); + $fileName = "croppers/$cropClass.class.php"; + $class = 'SLIRCropper' . ucfirst($cropClass); + require_once $fileName; + return new $class(); + } + + /** + * Crops the image + * + * @since 2.0 + * @param boolean $isBackgroundFillOn + * @return boolean + * @todo improve cropping method preference (smart or centered) + */ + final public function crop($isBackgroundFillOn) + { + if (!$this->isCroppingNeeded()) + { + return TRUE; + } + + $cropper = $this->getCropperClass(); + $offset = $cropper->getCrop($this); + return $this->cropImage($offset['x'], $offset['y'], $isBackgroundFillOn); + } + + /** + * Performs the actual cropping of the image + * + * @since 2.0 + * @param integer $leftOffset Number of pixels from the left side of the image to crop in + * @param integer $topOffset Number of pixels from the top side of the image to crop in + * @param boolean $isBackgroundFillOn + * @return boolean + */ + private function cropImage($leftOffset, $topOffset, $isBackgroundFillOn) + { + // Set up a blank canvas for our cropped image (destination) + $cropped = imagecreatetruecolor( + $this->cropWidth, + $this->cropHeight + ); + + $this->background($isBackgroundFillOn, $cropped); + + // Copy rendered image to cropped image + ImageCopy( + $cropped, + $this->image, + 0, + 0, + $leftOffset, + $topOffset, + $this->width, + $this->height + ); + + // Replace pre-cropped image with cropped image + imagedestroy($this->image); + $this->image = $cropped; + unset($cropped); + + return TRUE; + } + + /** + * Sharpens the image + * + * @param integer $sharpness + * @since 2.0 + */ + final public function sharpen($sharpness) + { + if ($this->isSharpeningDesired()) + { + imageconvolution( + $this->image, + $this->sharpenMatrix($sharpness), + $sharpness, + 0 + ); + } + } + + /** + * @param integer $sharpness + * @return array + * @since 2.0 + */ + private function sharpenMatrix($sharpness) + { + return array( + array(-1, -2, -1), + array(-2, $sharpness + 12, -2), + array(-1, -2, -1) + ); + } + + /** + * @since 2.0 + * @return array + */ + final public function cacheParameters() + { + return array( + 'path' => $this->fullPath(), + 'width' => $this->width, + 'height' => $this->height, + 'cropWidth' => $this->cropWidth, + 'cropHeight' => $this->cropHeight, + 'iptc' => $this->iptc, + 'quality' => $this->quality, + 'progressive' => $this->progressive, + 'background' => $this->background, + 'cropper' => $this->getCropperClassName(), + ); + } + + /** + * @since 2.0 + * @return string + */ + private function getData() + { + ob_start(NULL); + if (!$this->output()) + { + return FALSE; + } + $data = ob_get_contents(); + ob_end_clean(); + + return $data; + } + + /** + * @since 2.0 + * @return boolean + */ + private function output($filename = NULL) + { + if ($this->isJPEG()) + { + return imagejpeg($this->image, $filename, $this->quality); + } + else if ($this->isPNG()) + { + return imagepng($this->image, $filename, $this->quality); + } + else if ($this->isGIF()) + { + return imagegif($this->image, $filename, $this->quality); + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return integer + */ + final public function fileSize() + { + return strlen($this->data); + } + + /** + * @since 2.0 + * @return boolean + */ + final public function destroyImage() + { + return imagedestroy($this->image); + } + } \ No newline at end of file diff --git a/app/parsers/slir/slirrequest.class.php b/app/parsers/slir/slirrequest.class.php index 7a3f12d5..dc7e0c3a 100644 --- a/app/parsers/slir/slirrequest.class.php +++ b/app/parsers/slir/slirrequest.class.php @@ -1,536 +1,536 @@ -. - * - * @copyright Copyright © 2010, Joe Lencioni - * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public - * License version 3 (GPLv3) - * @since 2.0 - * @package SLIR - */ - -/* $Id: slirrequest.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ - -/** - * SLIR request class - * - * @since 2.0 - * @author Joe Lencioni - * @date $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ - * @version $Revision: 129 $ - * @package SLIR - */ -class SLIRRequest -{ - - const CROP_RATIO_DELIMITERS = ':.'; - - /** - * Path to image - * - * @since 2.0 - * @var string - */ - private $path; - - /** - * Maximum width for resized image, in pixels - * - * @since 2.0 - * @var integer - */ - private $width; - - /** - * Maximum height for resized image, in pixels - * - * @since 2.0 - * @var integer - */ - private $height; - - /** - * Ratio of width:height to crop image to. - * - * For example, if a square shape is desired, the crop ratio should be "1:1" - * or if a long rectangle is desired, the crop ratio could be "4:1". Stored - * as an associative array with keys being 'width' and 'height'. - * - * @since 2.0 - * @var array - */ - private $cropRatio; - - /** - * Name of the cropper to use, e.g. 'centered' or 'smart' - * - * @since 2.0 - * @var string - */ - private $cropper; - - /** - * Quality of rendered image - * - * @since 2.0 - * @var integer - */ - private $quality; - - /** - * Whether or not progressive JPEG output is turned on - * @var boolean - * @since 2.0 - */ - private $progressive; - - /** - * Color to fill background of transparent PNGs and GIFs - * @var string - * @since 2.0 - */ - private $background; - - /** - * @since 2.0 - */ - final public function __construct() - { - $params = $this->getParameters(); - - // Set image path first - if (isset($params['i']) && $params['i'] != '' && $params['i'] != '/') - { - $this->__set('i', $params['i']); - unset($params['i']); - } - else - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Source image was not specified.'); - } // if - - // Set the rest of the parameters - foreach($params as $name => $value) - { - $this->__set($name, $value); - } // foreach - } - - /** - * @since 2.0 - * @return void - */ - final public function __set($name, $value) - { - switch($name) - { - case 'i': - case 'image': - case 'imagePath': - case 'path': - $this->setPath($value); - break; - - case 'w': - case 'width': - $this->setWidth($value); - break; - - case 'h': - case 'height': - $this->setHeight($value); - break; - - case 'q': - case 'quality': - $this->setQuality($value); - break; - - case 'p': - case 'progressive': - $this->setProgressive($value); - break; - - case 'b'; - case 'backgroundFillColor': - $this->setBackgroundFillColor($value); - break; - - case 'c': - case 'cropRatio': - $this->setCrop($value); - break; - } // switch - } - - /** - * @since 2.0 - * @return mixed - */ - final public function __get($name) - { - return $this->$name; - } - - /** - * @since 2.0 - * @return void - */ - private function setWidth($value) - { - $this->width = (int) $value; - } - - /** - * @since 2.0 - * @return void - */ - private function setHeight($value) - { - $this->height = (int) $value; - } - - /** - * @since 2.0 - * @return void - */ - private function setQuality($value) - { - $this->quality = $value; - if ($this->quality < 0 || $this->quality > 100) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Quality must be between 0 and 100: ' . $this->quality); - } - } - - /** - * @param string $value - * @return void - */ - private function setProgressive($value) - { - $this->progressive = (bool) $value; - } - - /** - * @param string $value - * @return void - */ - private function setBackgroundFillColor($value) - { - $this->background = preg_replace('/[^0-9a-fA-F]/', '', $value); - - if(strlen($this->background) == 3) - { - $this->background = $this->background[0] - .$this->background[0] - .$this->background[1] - .$this->background[1] - .$this->background[2] - .$this->background[2]; - } - else if (strlen($this->background) != 6) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Background fill color must be in ' - .'hexadecimal format, longhand or shorthand: ' - . $this->background); - } // if - } - - /** - * @param string $value - * @return void - */ - private function setCrop($value) - { - $delimiters = preg_quote(self::CROP_RATIO_DELIMITERS); - $ratio = preg_split("/[$delimiters]/", (string) urldecode($value)); - if (count($ratio) >= 2) - { - if ((float) $ratio[0] == 0 || (float) $ratio[1] == 0) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Crop ratio must not contain a zero: ' . (string) $value); - } - - $this->cropRatio = array( - 'width' => (float) $ratio[0], - 'height' => (float) $ratio[1], - 'ratio' => (float) $ratio[0] / (float) $ratio[1] - ); - - // If there was a third part, that is the cropper being specified - if (count($ratio) >= 3) - { - $this->cropper = (string) $ratio[2]; - } - } - else - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Crop ratio must be in width:height' - . ' format: ' . (string) $value); - } // if - } - - /** - * Determines the parameters to use for resizing - * - * @since 2.0 - * @return array - */ - private function getParameters() - { - if (!$this->isUsingQueryString()) // Using the mod_rewrite version - { - return $this->getParametersFromPath(); - } - else // Using the query string version - { - return $_GET; - } - } - - /** - * For requests that are using the mod_rewrite syntax - * - * @since 2.0 - * @return array - */ - private function getParametersFromPath() - { - $params = array(); - - // The parameters should be the first set of characters after the - // SLIR path - $request = preg_replace('`.*?' . preg_quote(SLIRConfig::$SLIRDir) . '`', '', (string) $_SERVER['REQUEST_URI']); - $request = explode('/', trim($request, '/')); - - if (count($request) < 2) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Not enough parameters were given.', 'Available parameters: -w = Maximum width -h = Maximum height -c = Crop ratio (width:height) -q = Quality (0-100) -b = Background fill color (RRGGBB or RGB) -p = Progressive (0 or 1) - -Example usage: -Don\'t forget '
-.'your alt text!' - ); - - } // if - - // The parameters are separated by hyphens - $rawParams = array_filter(explode('-', array_shift($request))); - - // The image path should be all of the remaining values in the array - $params['i'] = implode('/', $request); - - foreach ($rawParams as $rawParam) - { - // The name of each parameter should be the first character of the - // parameter string - $name = $rawParam[0]; - // The value of each parameter should be the remaining characters of - // the parameter string - $value = substr($rawParam, 1, strlen($rawParam) - 1); - - $params[$name] = $value; - } // foreach - - $params = array_filter($params); - - return $params; - } - - /** - * Determines if the request is using the mod_rewrite version or the query - * string version - * - * @since 2.0 - * @return boolean - */ - private function isUsingQueryString() - { - if (SLIRConfig::$forceQueryString === TRUE) - { - return TRUE; - } - else if (isset($_SERVER['QUERY_STRING']) - && trim($_SERVER['QUERY_STRING']) != '' - && count(array_intersect(array('i', 'w', 'h', 'q', 'c', 'b'), array_keys($_GET))) - ) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @param string $path - */ - private function setPath($path) - { - $this->path = $this->localizePath((string) urldecode($path)); - - // Make sure the image path is secure - if (!$this->isPathSecure()) - { - header('HTTP/1.1 400 Bad Request'); - throw new SLIRException('Image path may not contain ":", ".' - . '.", "<", or ">"'); - } - // Make sure the image file exists - else if (!$this->pathExists()) - { - header('HTTP/1.1 404 Not Found'); - throw new SLIRException('Image does not exist: ' . $this->fullPath()); - } - } - - /** - * Strips the domain and query string from the path if either is there - * @since 2.0 - * @return string - */ - private function localizePath($path) - { - return '/content/' . trim($this->stripQueryString($this->stripProtocolAndDomain($path)), '/'); - } - - /** - * Strips the protocol and domain from the path if it is there - * @since 2.0 - * @return string - */ - private function stripProtocolAndDomain($path) - { - return preg_replace('/^(?:s?f|ht)tps?:\/\/[^\/]+/i', '', $path); - } - - /** - * Strips the query string from the path if it is there - * @since 2.0 - * @return string - */ - private function stripQueryString($path) - { - return preg_replace('/\?.*/', '', $path); - } - - /** - * Checks to see if the path is secure - * - * For security, directories may not contain ':' and images may not contain - * '..', '<', or '>'. - * - * @since 2.0 - * @return boolean - */ - private function isPathSecure() - { - if (strpos(dirname($this->path), ':') || preg_match('/(\.\.|<|>)/', $this->path)) - { - return FALSE; - } - else - { - return TRUE; - } - } - - /** - * Determines if the path exists - * - * @since 2.0 - * @return boolean - */ - private function pathExists() - { - return is_file($this->fullPath()); - } - - /** - * @return string - * @since 2.0 - */ - final public function fullPath() - { - return SLIRConfig::$documentRoot . $this->path; - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isBackground() - { - if ($this->background !== NULL) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isQuality() - { - if ($this->quality !== NULL) - { - return TRUE; - } - else - { - return FALSE; - } - } - - /** - * @since 2.0 - * @return boolean - */ - final public function isCropping() - { - if ($this->cropRatio['width'] !== NULL && $this->cropRatio['height'] !== NULL) - { - return TRUE; - } - else - { - return FALSE; - } - } - +. + * + * @copyright Copyright © 2010, Joe Lencioni + * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public + * License version 3 (GPLv3) + * @since 2.0 + * @package SLIR + */ + +/* $Id: slirrequest.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */ + +/** + * SLIR request class + * + * @since 2.0 + * @author Joe Lencioni + * @date $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $ + * @version $Revision: 129 $ + * @package SLIR + */ +class SLIRRequest +{ + + const CROP_RATIO_DELIMITERS = ':.'; + + /** + * Path to image + * + * @since 2.0 + * @var string + */ + private $path; + + /** + * Maximum width for resized image, in pixels + * + * @since 2.0 + * @var integer + */ + private $width; + + /** + * Maximum height for resized image, in pixels + * + * @since 2.0 + * @var integer + */ + private $height; + + /** + * Ratio of width:height to crop image to. + * + * For example, if a square shape is desired, the crop ratio should be "1:1" + * or if a long rectangle is desired, the crop ratio could be "4:1". Stored + * as an associative array with keys being 'width' and 'height'. + * + * @since 2.0 + * @var array + */ + private $cropRatio; + + /** + * Name of the cropper to use, e.g. 'centered' or 'smart' + * + * @since 2.0 + * @var string + */ + private $cropper; + + /** + * Quality of rendered image + * + * @since 2.0 + * @var integer + */ + private $quality; + + /** + * Whether or not progressive JPEG output is turned on + * @var boolean + * @since 2.0 + */ + private $progressive; + + /** + * Color to fill background of transparent PNGs and GIFs + * @var string + * @since 2.0 + */ + private $background; + + /** + * @since 2.0 + */ + final public function __construct() + { + $params = $this->getParameters(); + + // Set image path first + if (isset($params['i']) && $params['i'] != '' && $params['i'] != '/') + { + $this->__set('i', $params['i']); + unset($params['i']); + } + else + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Source image was not specified.'); + } // if + + // Set the rest of the parameters + foreach($params as $name => $value) + { + $this->__set($name, $value); + } // foreach + } + + /** + * @since 2.0 + * @return void + */ + final public function __set($name, $value) + { + switch($name) + { + case 'i': + case 'image': + case 'imagePath': + case 'path': + $this->setPath($value); + break; + + case 'w': + case 'width': + $this->setWidth($value); + break; + + case 'h': + case 'height': + $this->setHeight($value); + break; + + case 'q': + case 'quality': + $this->setQuality($value); + break; + + case 'p': + case 'progressive': + $this->setProgressive($value); + break; + + case 'b'; + case 'backgroundFillColor': + $this->setBackgroundFillColor($value); + break; + + case 'c': + case 'cropRatio': + $this->setCrop($value); + break; + } // switch + } + + /** + * @since 2.0 + * @return mixed + */ + final public function __get($name) + { + return $this->$name; + } + + /** + * @since 2.0 + * @return void + */ + private function setWidth($value) + { + $this->width = (int) $value; + } + + /** + * @since 2.0 + * @return void + */ + private function setHeight($value) + { + $this->height = (int) $value; + } + + /** + * @since 2.0 + * @return void + */ + private function setQuality($value) + { + $this->quality = $value; + if ($this->quality < 0 || $this->quality > 100) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Quality must be between 0 and 100: ' . $this->quality); + } + } + + /** + * @param string $value + * @return void + */ + private function setProgressive($value) + { + $this->progressive = (bool) $value; + } + + /** + * @param string $value + * @return void + */ + private function setBackgroundFillColor($value) + { + $this->background = preg_replace('/[^0-9a-fA-F]/', '', $value); + + if(strlen($this->background) == 3) + { + $this->background = $this->background[0] + .$this->background[0] + .$this->background[1] + .$this->background[1] + .$this->background[2] + .$this->background[2]; + } + else if (strlen($this->background) != 6) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Background fill color must be in ' + .'hexadecimal format, longhand or shorthand: ' + . $this->background); + } // if + } + + /** + * @param string $value + * @return void + */ + private function setCrop($value) + { + $delimiters = preg_quote(self::CROP_RATIO_DELIMITERS); + $ratio = preg_split("/[$delimiters]/", (string) urldecode($value)); + if (count($ratio) >= 2) + { + if ((float) $ratio[0] == 0 || (float) $ratio[1] == 0) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Crop ratio must not contain a zero: ' . (string) $value); + } + + $this->cropRatio = array( + 'width' => (float) $ratio[0], + 'height' => (float) $ratio[1], + 'ratio' => (float) $ratio[0] / (float) $ratio[1] + ); + + // If there was a third part, that is the cropper being specified + if (count($ratio) >= 3) + { + $this->cropper = (string) $ratio[2]; + } + } + else + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Crop ratio must be in width:height' + . ' format: ' . (string) $value); + } // if + } + + /** + * Determines the parameters to use for resizing + * + * @since 2.0 + * @return array + */ + private function getParameters() + { + if (!$this->isUsingQueryString()) // Using the mod_rewrite version + { + return $this->getParametersFromPath(); + } + else // Using the query string version + { + return $_GET; + } + } + + /** + * For requests that are using the mod_rewrite syntax + * + * @since 2.0 + * @return array + */ + private function getParametersFromPath() + { + $params = array(); + + // The parameters should be the first set of characters after the + // SLIR path + $request = preg_replace('`.*?' . preg_quote(SLIRConfig::$SLIRDir) . '`', '', (string) $_SERVER['REQUEST_URI']); + $request = explode('/', trim($request, '/')); + + if (count($request) < 2) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Not enough parameters were given.', 'Available parameters: +w = Maximum width +h = Maximum height +c = Crop ratio (width:height) +q = Quality (0-100) +b = Background fill color (RRGGBB or RGB) +p = Progressive (0 or 1) + +Example usage: +Don\'t forget '
+.'your alt text!' + ); + + } // if + + // The parameters are separated by hyphens + $rawParams = array_filter(explode('-', array_shift($request))); + + // The image path should be all of the remaining values in the array + $params['i'] = implode('/', $request); + + foreach ($rawParams as $rawParam) + { + // The name of each parameter should be the first character of the + // parameter string + $name = $rawParam[0]; + // The value of each parameter should be the remaining characters of + // the parameter string + $value = substr($rawParam, 1, strlen($rawParam) - 1); + + $params[$name] = $value; + } // foreach + + $params = array_filter($params); + + return $params; + } + + /** + * Determines if the request is using the mod_rewrite version or the query + * string version + * + * @since 2.0 + * @return boolean + */ + private function isUsingQueryString() + { + if (SLIRConfig::$forceQueryString === TRUE) + { + return TRUE; + } + else if (isset($_SERVER['QUERY_STRING']) + && trim($_SERVER['QUERY_STRING']) != '' + && count(array_intersect(array('i', 'w', 'h', 'q', 'c', 'b'), array_keys($_GET))) + ) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @param string $path + */ + private function setPath($path) + { + $this->path = $this->localizePath((string) urldecode($path)); + + // Make sure the image path is secure + if (!$this->isPathSecure()) + { + header('HTTP/1.1 400 Bad Request'); + throw new SLIRException('Image path may not contain ":", ".' + . '.", "<", or ">"'); + } + // Make sure the image file exists + else if (!$this->pathExists()) + { + header('HTTP/1.1 404 Not Found'); + throw new SLIRException('Image does not exist: ' . $this->fullPath()); + } + } + + /** + * Strips the domain and query string from the path if either is there + * @since 2.0 + * @return string + */ + private function localizePath($path) + { + return '/content/' . trim($this->stripQueryString($this->stripProtocolAndDomain($path)), '/'); + } + + /** + * Strips the protocol and domain from the path if it is there + * @since 2.0 + * @return string + */ + private function stripProtocolAndDomain($path) + { + return preg_replace('/^(?:s?f|ht)tps?:\/\/[^\/]+/i', '', $path); + } + + /** + * Strips the query string from the path if it is there + * @since 2.0 + * @return string + */ + private function stripQueryString($path) + { + return preg_replace('/\?.*/', '', $path); + } + + /** + * Checks to see if the path is secure + * + * For security, directories may not contain ':' and images may not contain + * '..', '<', or '>'. + * + * @since 2.0 + * @return boolean + */ + private function isPathSecure() + { + if (strpos(dirname($this->path), ':') || preg_match('/(\.\.|<|>)/', $this->path)) + { + return FALSE; + } + else + { + return TRUE; + } + } + + /** + * Determines if the path exists + * + * @since 2.0 + * @return boolean + */ + private function pathExists() + { + return is_file($this->fullPath()); + } + + /** + * @return string + * @since 2.0 + */ + final public function fullPath() + { + return SLIRConfig::$documentRoot . $this->path; + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isBackground() + { + if ($this->background !== NULL) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isQuality() + { + if ($this->quality !== NULL) + { + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * @since 2.0 + * @return boolean + */ + final public function isCropping() + { + if ($this->cropRatio['width'] !== NULL && $this->cropRatio['height'] !== NULL) + { + return TRUE; + } + else + { + return FALSE; + } + } + } \ No newline at end of file diff --git a/app/parsers/yaml/sfYaml.php b/app/parsers/yaml/sfYaml.php old mode 100755 new mode 100644 diff --git a/app/parsers/yaml/sfYamlDumper.php b/app/parsers/yaml/sfYamlDumper.php old mode 100755 new mode 100644 diff --git a/app/parsers/yaml/sfYamlInline.php b/app/parsers/yaml/sfYamlInline.php old mode 100755 new mode 100644 diff --git a/app/parsers/yaml/sfYamlParser.php b/app/parsers/yaml/sfYamlParser.php old mode 100755 new mode 100644