From 47d23c6780c7c20c4e33c5e78899f09d1038a1dd Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 4 Oct 2024 00:59:24 +0200 Subject: [PATCH] Implement ReflectionProperty::is{Readable,Writable}() Fixes GH-15309 Fixes GH-16175 --- ext/reflection/php_reflection.c | 141 ++++++++++++++++++ ext/reflection/php_reflection.stub.php | 4 + ext/reflection/php_reflection_arginfo.h | 15 +- .../tests/ReflectionProperty_isReadable.phpt | 84 +++++++++++ .../tests/ReflectionProperty_isWritable.phpt | 90 +++++++++++ 5 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 64418bfd513b9..9cb33c3cec1f1 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6617,6 +6617,147 @@ ZEND_METHOD(ReflectionProperty, isFinal) _property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_FINAL); } +static zend_result get_ce_from_scope_name(zend_class_entry **scope, zend_string *scope_name, zend_execute_data *execute_data) +{ + if (!scope_name) { + *scope = NULL; + return SUCCESS; + } + if (zend_string_equals(scope_name, ZSTR_KNOWN(ZEND_STR_STATIC))) { + *scope = EX(prev_execute_data)->func->common.scope; + return SUCCESS; + } + + *scope = zend_lookup_class(scope_name); + if (!*scope) { + zend_throw_error(NULL, "Class \"%s\" not found", ZSTR_VAL(scope_name)); + return FAILURE; + } + return SUCCESS; +} + +static zend_always_inline uint32_t set_visibility_to_visibility(uint32_t set_visibility) +{ + switch (set_visibility) { + case ZEND_ACC_PUBLIC_SET: + return ZEND_ACC_PUBLIC; + case ZEND_ACC_PROTECTED_SET: + return ZEND_ACC_PROTECTED; + case ZEND_ACC_PRIVATE_SET: + return ZEND_ACC_PRIVATE; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} + +static bool check_visibility(uint32_t visibility, zend_class_entry *ce, zend_class_entry *scope) +{ + if (!(visibility & ZEND_ACC_PUBLIC) && (scope != ce)) { + if (!scope) { + return false; + } + if (visibility & ZEND_ACC_PRIVATE) { + return false; + } + ZEND_ASSERT(visibility & ZEND_ACC_PROTECTED); + if (!instanceof_function(scope, ce) && !instanceof_function(ce, scope)) { + return false; + } + } + return true; +} + +ZEND_METHOD(ReflectionProperty, isReadable) +{ + reflection_object *intern; + property_reference *ref; + zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_STR_OR_NULL(scope_name) + ZEND_PARSE_PARAMETERS_END(); + + GET_REFLECTION_OBJECT_PTR(ref); + zend_property_info *prop = ref->prop; + if (!prop) { + _DO_THROW("May not use isReadable on dynamic properties"); + RETURN_THROWS(); + } + + zend_class_entry *scope; + if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { + RETURN_THROWS(); + } + + if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { + RETURN_FALSE; + } + + if (prop->flags & ZEND_ACC_VIRTUAL) { + ZEND_ASSERT(prop->hooks); + if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) { + RETURN_FALSE; + } + } + + RETURN_TRUE; +} + +ZEND_METHOD(ReflectionProperty, isWritable) +{ + reflection_object *intern; + property_reference *ref; + zend_object *obj; + zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJ(obj) + Z_PARAM_OPTIONAL + Z_PARAM_STR_OR_NULL(scope_name) + ZEND_PARSE_PARAMETERS_END(); + + GET_REFLECTION_OBJECT_PTR(ref); + zend_property_info *prop = ref->prop; + if (!prop) { + _DO_THROW("May not use isReadable on dynamic properties"); + RETURN_THROWS(); + } + + zend_class_entry *scope; + if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { + RETURN_THROWS(); + } + if (!instanceof_function(obj->ce, prop->ce)) { + _DO_THROW("Given object is not an instance of the class this property was declared in"); + RETURN_THROWS(); + } + + uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK; + if (!set_visibility) { + set_visibility = zend_visibility_to_set_visibility(prop->flags & ZEND_ACC_PPP_MASK); + } + if (!check_visibility(set_visibility_to_visibility(set_visibility), prop->ce, scope)) { + RETURN_FALSE; + } + + if (prop->flags & ZEND_ACC_VIRTUAL) { + ZEND_ASSERT(prop->hooks); + if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) { + RETURN_FALSE; + } + } + + if (prop->flags & ZEND_ACC_READONLY) { + ZEND_ASSERT(prop->offset != ZEND_VIRTUAL_PROPERTY_OFFSET); + zval *prop_val = OBJ_PROP(obj, prop->offset); + if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) { + RETURN_FALSE; + } + } + + RETURN_TRUE; +} + /* {{{ Constructor. Throws an Exception in case the given extension does not exist */ ZEND_METHOD(ReflectionExtension, __construct) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 28a79feee7542..d650f6130f423 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -569,6 +569,10 @@ public function hasHook(PropertyHookType $type): bool {} public function getHook(PropertyHookType $type): ?ReflectionMethod {} public function isFinal(): bool {} + + public function isReadable(?string $scope = 'static'): bool {} + + public function isWritable(object $object, ?string $scope = 'static'): bool {} } /** @not-serializable */ diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 1807416a0c85c..d12ef8819a2bb 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 261798b92d4eac170538185ced1068bc72705385 */ + * Stub hash: b515f1f5b0cae320d03f036b40fc0767c18216eb */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) @@ -462,6 +462,15 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionProperty_isFinal arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionProperty_isReadable, 0, 0, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, scope, IS_STRING, 1, "\'static\'") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionProperty_isWritable, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, scope, IS_STRING, 1, "\'static\'") +ZEND_END_ARG_INFO() + #define arginfo_class_ReflectionClassConstant___clone arginfo_class_ReflectionFunctionAbstract___clone ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionClassConstant___construct, 0, 0, 2) @@ -871,6 +880,8 @@ ZEND_METHOD(ReflectionProperty, getHooks); ZEND_METHOD(ReflectionProperty, hasHook); ZEND_METHOD(ReflectionProperty, getHook); ZEND_METHOD(ReflectionProperty, isFinal); +ZEND_METHOD(ReflectionProperty, isReadable); +ZEND_METHOD(ReflectionProperty, isWritable); ZEND_METHOD(ReflectionClassConstant, __construct); ZEND_METHOD(ReflectionClassConstant, __toString); ZEND_METHOD(ReflectionClassConstant, getName); @@ -1168,6 +1179,8 @@ static const zend_function_entry class_ReflectionProperty_methods[] = { ZEND_ME(ReflectionProperty, hasHook, arginfo_class_ReflectionProperty_hasHook, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, getHook, arginfo_class_ReflectionProperty_getHook, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isFinal, arginfo_class_ReflectionProperty_isFinal, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isReadable, arginfo_class_ReflectionProperty_isReadable, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isWritable, arginfo_class_ReflectionProperty_isWritable, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/reflection/tests/ReflectionProperty_isReadable.phpt b/ext/reflection/tests/ReflectionProperty_isReadable.phpt new file mode 100644 index 0000000000000..ff83c8f824392 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable.phpt @@ -0,0 +1,84 @@ +--TEST-- +Test ReflectionProperty::isReadable() +--FILE-- + 42; } + public $f { set {} } +} + +class C extends B {} + +$test = static function ($scope) { + $rc = new ReflectionClass(B::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isReadable($scope)); + } +}; + +foreach (['A', 'B', 'C'] as $scope) { + $test($scope); + $test->bindTo(null, $scope)('static'); +} + +$test(null); +$test->bindTo(null, null)('static'); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(false) +d from A: bool(true) +e from A: bool(true) +f from A: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from B: bool(true) +b from B: bool(true) +c from B: bool(true) +d from B: bool(true) +e from B: bool(true) +f from B: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(true) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from C: bool(true) +b from C: bool(true) +c from C: bool(false) +d from C: bool(true) +e from C: bool(true) +f from C: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(true) +e from global: bool(true) +f from global: bool(false) +a from static: bool(true) +b from static: bool(false) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable.phpt b/ext/reflection/tests/ReflectionProperty_isWritable.phpt new file mode 100644 index 0000000000000..b99782c793875 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable.phpt @@ -0,0 +1,90 @@ +--TEST-- +Test ReflectionProperty::isWritable() +--FILE-- + 42; } + public $f { set {} } + public readonly int $g; + public private(set) int $h; + + public function setG($g) { + $this->g = $g; + } + + public function __clone() { + $rp = new ReflectionProperty(A::class, 'g'); + echo $rp->getName() . ' from static (initialized, clone): '; + var_dump($rp->isWritable($this)); + } +} + +$test = static function ($scope) { + $rc = new ReflectionClass(A::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isWritable(new A(), $scope)); + + if ($rp->name == 'g') { + $a = new A(); + $a->setG(42); + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ' (initialized): '; + var_dump($rp->isWritable($a, $scope)); + clone $a; + } + } +}; + +$test('A'); +$test->bindTo(null, 'A')('static'); + +$test(null); +$test->bindTo(null, null)('static'); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(true) +d from A: bool(true) +e from A: bool(false) +f from A: bool(true) +g from A: bool(true) +g from A (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from A: bool(true) +a from static: bool(true) +b from static: bool(true) +c from static: bool(true) +d from static: bool(true) +e from static: bool(false) +f from static: bool(true) +g from static: bool(true) +g from static (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from static: bool(true) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(false) +e from global: bool(false) +f from global: bool(true) +g from global: bool(false) +g from global (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from global: bool(false) +a from static: bool(true) +b from static: bool(false) +c from static: bool(false) +d from static: bool(false) +e from static: bool(false) +f from static: bool(true) +g from static: bool(false) +g from static (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from static: bool(false)