diff --git a/ext/twig/twig.c b/ext/twig/twig.c index 5c482bedb7c..49cc07d1eea 100644 --- a/ext/twig/twig.c +++ b/ext/twig/twig.c @@ -175,6 +175,36 @@ char *TWIG_STRTOLOWER(const char *str, int str_len) return item_dup; } +char *TWIG_DECAMELIZE(const char *str) +{ + int i, chars = 0, underscored = 0; + char prev, current; + int str_len = strlen(str); + char *retval = emalloc(2*str_len+1); + + prev = str[0]; + for(i = 1; i < str_len; i++) { + current = str[i]; + if (islower(current) && isupper(prev) && i > 1 && !underscored) { + retval[chars++] = '_'; + } + underscored = 0; + retval[chars++] = tolower(prev); + if (isupper(current) && (islower(prev) || isdigit(prev))) { + retval[chars++] = '_'; + underscored = 1; + } else if (isupper(current) && prev == '_') { + underscored = 1; + } + prev = current; + } + + retval[chars++] = prev; + retval[chars++] = '\0'; + erealloc(retval, chars); + return retval; +} + zval *TWIG_CALL_USER_FUNC_ARRAY(zval *object, char *function, zval *arguments TSRMLS_DC) { zend_fcall_info fci; @@ -665,9 +695,10 @@ static char *TWIG_GET_CLASS_NAME(zval *object TSRMLS_DC) static int twig_add_method_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key) { + int from; zval *retval; - char *item; - size_t item_len; + char *method, *lMethod; + size_t method_len; zend_function *mptr = (zend_function *) pDest; APPLY_TSRMLS_FETCH(); @@ -677,11 +708,22 @@ static int twig_add_method_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, v retval = va_arg(args, zval*); - item_len = strlen(mptr->common.function_name); - item = estrndup(mptr->common.function_name, item_len); - php_strtolower(item, item_len); - - add_assoc_stringl_ex(retval, item, item_len+1, item, item_len, 0); + method_len = strlen(mptr->common.function_name); + method = estrndup(mptr->common.function_name, method_len); + lMethod = TWIG_STRTOLOWER(method, method_len); + + add_assoc_string(retval, lMethod, method, 1); + add_assoc_string(retval, TWIG_DECAMELIZE(method), method, 1); + + if (method_len > 3 && 0 == strncmp("get", lMethod, 3)) { + from = lMethod[3] == '_' ? 4 : 3; + add_assoc_string(retval, estrndup(lMethod+from, method_len-from), method, 1); + add_assoc_string(retval, TWIG_DECAMELIZE(estrndup(method+from, method_len-from)), method, 1); + } else if (method_len > 2 && 0 == strncmp("is", lMethod, 2)) { + from = lMethod[2] == '_' ? 3 : 2; + add_assoc_string(retval, estrndup(lMethod+from, method_len-from), method, 1); + add_assoc_string(retval, TWIG_DECAMELIZE(estrndup(method+from, method_len-from)), method, 1); + } return 0; } @@ -691,10 +733,11 @@ static int twig_add_property_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, zend_class_entry *ce; zval *retval; char *class_name, *prop_name; + zend_property_info *pptr = (zend_property_info *) pDest; APPLY_TSRMLS_FETCH(); - if (!(pptr->flags & ZEND_ACC_PUBLIC) || (pptr->flags & ZEND_ACC_STATIC)) { + if ((pptr->flags & ZEND_ACC_PRIVATE) || (pptr->flags & ZEND_ACC_STATIC)) { return 0; } @@ -708,6 +751,7 @@ static int twig_add_property_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, #endif add_assoc_string(retval, prop_name, prop_name, 1); + add_assoc_string(retval, TWIG_DECAMELIZE(prop_name), prop_name, 1); return 0; } @@ -727,6 +771,7 @@ static void twig_add_class_to_cache(zval *cache, zval *object, char *class_name array_init(class_properties); // add all methods to self::cache[$class]['methods'] zend_hash_apply_with_arguments(&class_ce->function_table APPLY_TSRMLS_CC, twig_add_method_to_class, 1, class_methods); + // add all properties to self::cache[$class]['properties'] zend_hash_apply_with_arguments(&class_ce->properties_info APPLY_TSRMLS_CC, twig_add_property_to_class, 2, &class_ce, class_properties); add_assoc_zval(class_info, "methods", class_methods); @@ -771,14 +816,46 @@ PHP_FUNCTION(twig_template_get_attributes) type = "any"; } +/* + if (is_object($object)) { + $class = get_class($object); + if (!isset(self::$cache[$class])) { + self::$cache[$class] = $this->getCacheForClass($class); + } + } +*/ + if (Z_TYPE_P(object) == IS_OBJECT) { + class_name = TWIG_GET_CLASS_NAME(object TSRMLS_CC); + tmp_self_cache = TWIG_GET_STATIC_PROPERTY(template, "cache" TSRMLS_CC); + tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC); + + if (!tmp_class) { + twig_add_class_to_cache(tmp_self_cache, object, class_name TSRMLS_CC); + tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC); + } + efree(class_name); + } + /* // array if (Twig_Template::METHOD_CALL !== $type) { $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; + $hasArrayItem = false; + + if (is_array($object) && array_key_exists($arrayItem, $object)) { + $hasArrayItem = true; + } elseif ($object instanceof ArrayAccess) { + if (isset($object[$arrayItem])) { + $hasArrayItem = true; + } elseif (isset(self::$cache[$class]['properties'][$arrayItem]) + && isset($object[self::$cache[$class]['properties'][$arrayItem]]) + ) { + $arrayItem = self::$cache[$class]['properties'][$arrayItem]; + $hasArrayItem = true; + } + } - if ((is_array($object) && array_key_exists($arrayItem, $object)) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { + if ($hasArrayItem) { if ($isDefinedTest) { return true; } @@ -789,15 +866,29 @@ PHP_FUNCTION(twig_template_get_attributes) if (strcmp("method", type) != 0) { - if ((TWIG_ARRAY_KEY_EXISTS(object, zitem)) - || (TWIG_INSTANCE_OF(object, zend_ce_arrayaccess TSRMLS_CC) && TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, zitem TSRMLS_CC)) - ) { + zval *tmp_properties, *tmp_item = NULL; + + if (TWIG_ARRAY_KEY_EXISTS(object, zitem)) { + tmp_item = zitem; + } else if (TWIG_INSTANCE_OF(object, zend_ce_arrayaccess TSRMLS_CC)) { + if (TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, zitem TSRMLS_CC)) { + tmp_item = zitem; + } else { + tmp_properties = TWIG_GET_ARRAY_ELEMENT(tmp_class, "properties", strlen("properties") TSRMLS_CC); + if ((tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC)) + && !TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, tmp_item TSRMLS_CC) + ) { + tmp_item = NULL; + } + } + } + if (tmp_item) { if (isDefinedTest) { RETURN_TRUE; } - ret = TWIG_GET_ARRAY_ELEMENT_ZVAL(object, zitem TSRMLS_CC); + ret = TWIG_GET_ARRAY_ELEMENT_ZVAL(object, tmp_item TSRMLS_CC); if (!ret) { ret = &EG(uninitialized_zval); @@ -814,7 +905,7 @@ PHP_FUNCTION(twig_template_get_attributes) return false; } if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { - return null; + return; } */ if (strcmp("array", type) == 0 || Z_TYPE_P(object) != IS_OBJECT) { @@ -882,8 +973,9 @@ PHP_FUNCTION(twig_template_get_attributes) } /* if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { - return null; + return; } + throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); } */ @@ -901,102 +993,85 @@ PHP_FUNCTION(twig_template_get_attributes) return; } -/* - $class = get_class($object); -*/ - - class_name = TWIG_GET_CLASS_NAME(object TSRMLS_CC); - tmp_self_cache = TWIG_GET_STATIC_PROPERTY(template, "cache" TSRMLS_CC); - tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC); - - if (!tmp_class) { - twig_add_class_to_cache(tmp_self_cache, object, class_name TSRMLS_CC); - tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC); - } - efree(class_name); /* // object property if (Twig_Template::METHOD_CALL !== $type) { + $property = null; + if (isset($object->$item) || array_key_exists((string) $item, $object)) { + $property = $item; + } elseif (isset(self::$cache[$class]['properties'][$item]) + && isset($object->{self::$cache[$class]['properties'][$item]}) + ) { + $property = self::$cache[$class]['properties'][$item]; + } + + if (null !== $property) { if ($isDefinedTest) { return true; } if ($this->env->hasExtension('sandbox')) { - $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item); + $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $property); } - return $object->$item; + return $object->$property; } } */ if (strcmp("method", type) != 0) { - zval *tmp_properties, *tmp_item; + zval *tmp_properties, *tmp_item = NULL; tmp_properties = TWIG_GET_ARRAY_ELEMENT(tmp_class, "properties", strlen("properties") TSRMLS_CC); - tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC); - if (tmp_item || TWIG_HAS_PROPERTY(object, zitem TSRMLS_CC) || TWIG_HAS_DYNAMIC_PROPERTY(object, item, item_len TSRMLS_CC)) { + if (TWIG_HAS_PROPERTY(object, zitem TSRMLS_CC) || TWIG_HAS_DYNAMIC_PROPERTY(object, item, item_len TSRMLS_CC)) { + tmp_item = zitem; + } else if (tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC)) { + if (!TWIG_HAS_PROPERTY(object, tmp_item TSRMLS_CC) && !TWIG_HAS_DYNAMIC_PROPERTY(object, Z_STRVAL_P(tmp_item), strlen(tmp_item) TSRMLS_CC)) { + tmp_item = NULL; + } + } + + if (tmp_item) { if (isDefinedTest) { RETURN_TRUE; } if (TWIG_CALL_SB(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "hasExtension", "sandbox" TSRMLS_CC)) { - TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkPropertyAllowed", object, zitem TSRMLS_CC); + TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkPropertyAllowed", object, tmp_item TSRMLS_CC); } if (EG(exception)) { return; } - ret = TWIG_PROPERTY(object, zitem TSRMLS_CC); + ret = TWIG_PROPERTY(object, tmp_item TSRMLS_CC); RETURN_ZVAL(ret, 1, 0); } } /* // object method - if (!isset(self::$cache[$class]['methods'])) { - self::$cache[$class]['methods'] = array_change_key_case(array_flip(get_class_methods($object))); - } - $call = false; - $lcItem = strtolower($item); - if (isset(self::$cache[$class]['methods'][$lcItem])) { - $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; + if (isset(self::$cache[$class]['methods'][$item])) { + $method = self::$cache[$class]['methods'][$item]; + } elseif (($lcItem = strtolower($item)) && isset(self::$cache[$class]['methods'][$lcItem])) { + $method = self::$cache[$class]['methods'][$lcItem]; } elseif (isset(self::$cache[$class]['methods']['__call'])) { $method = (string) $item; $call = true; */ { int call = 0; - char *lcItem = TWIG_STRTOLOWER(item, item_len); - int lcItem_length; - char *method = NULL; - char *tmp_method_name_get; - char *tmp_method_name_is; + zval *method; zval *zmethod; zval *tmp_methods; - lcItem_length = strlen(lcItem); - tmp_method_name_get = emalloc(4 + lcItem_length); - tmp_method_name_is = emalloc(3 + lcItem_length); - - sprintf(tmp_method_name_get, "get%s", lcItem); - sprintf(tmp_method_name_is, "is%s", lcItem); - tmp_methods = TWIG_GET_ARRAY_ELEMENT(tmp_class, "methods", strlen("methods") TSRMLS_CC); - if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, lcItem, lcItem_length TSRMLS_CC)) { - method = item; - } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, tmp_method_name_get, lcItem_length + 3 TSRMLS_CC)) { - method = tmp_method_name_get; - } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, tmp_method_name_is, lcItem_length + 2 TSRMLS_CC)) { - method = tmp_method_name_is; - } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, "__call", 6 TSRMLS_CC)) { - method = item; + method = TWIG_GET_ARRAY_ELEMENT(tmp_methods, TWIG_STRTOLOWER(item, item_len), item_len TSRMLS_CC); + + if (!method && TWIG_GET_ARRAY_ELEMENT(tmp_methods, "__call", 6 TSRMLS_CC)) { + MAKE_STD_ZVAL(method); + ZVAL_STRING(method, item, 1); call = 1; /* } else { @@ -1005,7 +1080,7 @@ PHP_FUNCTION(twig_template_get_attributes) } if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { - return null; + return; } throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object)), -1, $this->getTemplateName()); @@ -1015,11 +1090,7 @@ PHP_FUNCTION(twig_template_get_attributes) return true; } */ - } else { - efree(tmp_method_name_get); - efree(tmp_method_name_is); - efree(lcItem); - + } else if (!method) { if (isDefinedTest) { RETURN_FALSE; } @@ -1031,9 +1102,6 @@ PHP_FUNCTION(twig_template_get_attributes) } if (isDefinedTest) { - efree(tmp_method_name_get); - efree(tmp_method_name_is); - efree(lcItem); RETURN_TRUE; } /* @@ -1042,14 +1110,11 @@ PHP_FUNCTION(twig_template_get_attributes) } */ MAKE_STD_ZVAL(zmethod); - ZVAL_STRING(zmethod, method, 1); + ZVAL_ZVAL(zmethod, method, 1, 0); if (TWIG_CALL_SB(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "hasExtension", "sandbox" TSRMLS_CC)) { TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkMethodAllowed", object, zmethod TSRMLS_CC); } if (EG(exception)) { - efree(tmp_method_name_get); - efree(tmp_method_name_is); - efree(lcItem); zval_ptr_dtor(&zmethod); return; } @@ -1060,12 +1125,12 @@ PHP_FUNCTION(twig_template_get_attributes) $ret = call_user_func_array(array($object, $method), $arguments); } catch (BadMethodCallException $e) { if ($call && ($ignoreStrictCheck || !$this->env->isStrictVariables())) { - return null; + return; } throw $e; } */ - ret = TWIG_CALL_USER_FUNC_ARRAY(object, method, arguments TSRMLS_CC); + ret = TWIG_CALL_USER_FUNC_ARRAY(object, Z_STRVAL_P(method), arguments TSRMLS_CC); if (EG(exception) && TWIG_INSTANCE_OF(EG(exception), spl_ce_BadMethodCallException TSRMLS_CC)) { if (ignoreStrictCheck || !TWIG_CALL_BOOLEAN(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "isStrictVariables" TSRMLS_CC)) { zend_clear_exception(TSRMLS_C); @@ -1073,9 +1138,6 @@ PHP_FUNCTION(twig_template_get_attributes) } } free_ret = 1; - efree(tmp_method_name_get); - efree(tmp_method_name_is); - efree(lcItem); zval_ptr_dtor(&zmethod); } /* diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 70d19e740b5..34916cc010e 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -361,13 +361,32 @@ final protected function getContext($context, $item, $ignoreStrictCheck = false) */ protected function getAttribute($object, $item, array $arguments = array(), $type = Twig_Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) { + if (is_object($object)) { + $class = get_class($object); + if (!isset(self::$cache[$class])) { + self::$cache[$class] = $this->getCacheForClass($class); + } + } + // array if (Twig_Template::METHOD_CALL !== $type) { $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; + $hasArrayItem = false; + + if (is_array($object) && array_key_exists($arrayItem, $object)) { + $hasArrayItem = true; + } elseif ($object instanceof ArrayAccess) { + if (isset($object[$arrayItem])) { + $hasArrayItem = true; + } elseif (isset(self::$cache[$class]['properties'][$arrayItem]) + && isset($object[self::$cache[$class]['properties'][$arrayItem]]) + ) { + $arrayItem = self::$cache[$class]['properties'][$arrayItem]; + $hasArrayItem = true; + } + } - if ((is_array($object) && array_key_exists($arrayItem, $object)) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { + if ($hasArrayItem) { if ($isDefinedTest) { return true; } @@ -418,34 +437,37 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ // object property if (Twig_Template::METHOD_CALL !== $type) { + $property = null; + if (isset($object->$item) || array_key_exists((string) $item, $object)) { + $property = $item; + } elseif (isset(self::$cache[$class]['properties'][$item]) + && isset($object->{self::$cache[$class]['properties'][$item]}) + ) { + $property = self::$cache[$class]['properties'][$item]; + } + + if (null !== $property) { if ($isDefinedTest) { return true; } if ($this->env->hasExtension('sandbox')) { - $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item); + $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $property); } - return $object->$item; + return $object->$property; } } $class = get_class($object); // object method - if (!isset(self::$cache[$class]['methods'])) { - self::$cache[$class]['methods'] = array_change_key_case(array_flip(get_class_methods($object))); - } - $call = false; - $lcItem = strtolower($item); - if (isset(self::$cache[$class]['methods'][$lcItem])) { - $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; + if (isset(self::$cache[$class]['methods'][$item])) { + $method = self::$cache[$class]['methods'][$item]; + } elseif (($lcItem = strtolower($item)) && isset(self::$cache[$class]['methods'][$lcItem])) { + $method = self::$cache[$class]['methods'][$lcItem]; } elseif (isset(self::$cache[$class]['methods']['__call'])) { $method = (string) $item; $call = true; @@ -488,4 +510,44 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ return $ret; } + + /** + * Returns the key-value pairs for all public methods and public properties of a class. + * + * The key is normalized name of the method/property and the value is the real name of the method/property. + * + * @param string $class The class name + * + * @return array + */ + protected function getCacheForClass($class) + { + $cache = array('methods' => array(), 'properties' => array()); + + $methods = get_class_methods($class); + if (!empty($methods)) { + $cache['methods'] = array_combine($methods, $methods); + $keys = array_merge(preg_replace('/^(?:get|is)_?(.++)$/i', '\\1', $methods), $methods); + $keys = array_merge(preg_replace('/((?<=[a-z]|\d)[A-Z]|(?getProperties(ReflectionProperty::IS_PROTECTED) as $property) { + $properties[] = $property->getName(); + } + } + + if (!empty($properties)) { + $properties = array_combine($properties, $properties); + $cache['properties'] = array_flip(preg_replace('/((?<=[a-z]|\d)[A-Z]|(?$name); + } + + public function __get($name) + { + return $this->$name; + } +} + class Twig_TemplateMagicPropertyObjectWithException { public function __isset($key) @@ -564,6 +608,29 @@ public function __construct() } } +class Twig_TemplatePropertyArrayAccess implements ArrayAccess +{ + protected $camelCase = 'camelCase'; + + public function offsetExists($offset) + { + return isset($this->$offset); + } + + public function offsetGet($offset) + { + return $this->$offset; + } + + public function offsetSet($offset, $value) + { + } + + public function offsetUnset($offset) + { + } +} + class Twig_TemplateMethodObject { public function getDefined() @@ -625,6 +692,24 @@ private function getC() { return 'c'; } + + private $camelCase = 'camelcase_prop'; + public function getCamelCase() + { + return 'camelcase'; + } + + public function GetHTTPResponseCode() + { + return 'httpresponsecode'; + } + + public function get_http2_response() + { + return 'http2_response'; + } + + public $responseCode = 'responseCode'; } class Twig_TemplateMagicMethodObject