Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions ext/reflection/php_reflection.c
Original file line number Diff line number Diff line change
Expand Up @@ -6601,6 +6601,215 @@ 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;
}

*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;
zend_object *obj = NULL;

ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STR_OR_NULL(scope_name)
Z_PARAM_OPTIONAL
Z_PARAM_OBJ_OR_NULL(obj)
ZEND_PARSE_PARAMETERS_END();

GET_REFLECTION_OBJECT_PTR(ref);

zend_property_info *prop = ref->prop;
if (prop && obj) {
if (prop->flags & ZEND_ACC_STATIC) {
_DO_THROW("null is expected as object argument for static properties");
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();
}
prop = reflection_property_get_effective_prop(ref, intern->ce, obj);
}

zend_class_entry *ce = obj ? obj->ce : intern->ce;
if (!prop) {
if (obj && obj->properties && zend_hash_find_ptr(obj->properties, ref->unmangled_name)) {
RETURN_TRUE;
}
handle_magic_get:
if (ce->__get) {
if (obj && ce->__isset) {
uint32_t *guard = zend_get_property_guard(obj, ref->unmangled_name);
if (!((*guard) & ZEND_GUARD_PROPERTY_ISSET)) {
GC_ADDREF(obj);
*guard |= ZEND_GUARD_PROPERTY_ISSET;
zval member;
ZVAL_STR(&member, ref->unmangled_name);
zend_call_known_instance_method_with_1_params(ce->__isset, obj, return_value, &member);
*guard &= ~ZEND_GUARD_PROPERTY_ISSET;
OBJ_RELEASE(obj);
return;
}
}
RETURN_TRUE;
}
RETURN_FALSE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, if the object is lazy it may need to be initialized, and obj->properties must be checked again:

Suggested change
RETURN_FALSE;
if (zend_lazy_object_must_init(obj)) {
obj = zend_lazy_object_init(obj);
if (!obj) {
RETURN_THROWS();
}
if (obj->properties && zend_hash_find_ptr(obj->properties, ref->unmangled_name)) {
RETURN_TRUE;
}
}
RETURN_FALSE;

The initialized object will have neither __get or __isset since the uninitialized one didn't, so we don't need to check these again.

Test case:

#[AllowDynamicProperties]
class A {
    public function __construct() {
        $this->prop = 1;
    }
}

$rc = new ReflectionClass(A::class);
$obj = $rc->newLazyProxy(fn() => new A());

$rp = new ReflectionProperty(new A, 'prop');
var_dump($rp->isReadable(null, $obj)); // should be true

}

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)) {
if (!(prop->flags & ZEND_ACC_STATIC)) {
goto handle_magic_get;
}
RETURN_FALSE;
}

if (prop->flags & ZEND_ACC_VIRTUAL) {
ZEND_ASSERT(prop->hooks);
if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) {
RETURN_FALSE;
}
} else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) {
zval *prop_val = OBJ_PROP(obj, prop->offset);
if (Z_TYPE_P(prop_val) == IS_UNDEF) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object may need to be initialized if the prop is undef:

Suggested change
if (Z_TYPE_P(prop_val) == IS_UNDEF) {
if (Z_TYPE_P(prop_val) == IS_UNDEF) {
if (zend_lazy_object_must_init(obj) && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_LAZY)) {
if (!(obj = zend_lazy_object_init(obj)) {
RETURN_THROW();
}
prop_val = OBJ_PROP(obj, prop->offset);

Test case:

class A {
    public int $prop;
    public function __construct() {
        $this->prop = 1;
    }
}

$rc = new ReflectionClass(A::class);
$obj = $rc->newLazyProxy(fn() => new A());

$rp = new ReflectionProperty(A::class, 'prop');
var_dump($rp->isReadable(null, $obj)); // should be true

if (!(Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) {
goto handle_magic_get;
}
RETURN_FALSE;
}
} else if (prop->flags & ZEND_ACC_STATIC) {
if (ce->default_static_members_count && !CE_STATIC_MEMBERS(ce)) {
zend_class_init_statics(ce);
}
zval *prop_val = CE_STATIC_MEMBERS(ce) + prop->offset;
RETURN_BOOL(!Z_ISUNDEF_P(prop_val));
}

RETURN_TRUE;
}

ZEND_METHOD(ReflectionProperty, isWritable)
{
reflection_object *intern;
property_reference *ref;
zend_string *scope_name;
zend_object *obj = NULL;

ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STR_OR_NULL(scope_name)
Z_PARAM_OPTIONAL
Z_PARAM_OBJ_OR_NULL(obj)
ZEND_PARSE_PARAMETERS_END();

GET_REFLECTION_OBJECT_PTR(ref);

zend_property_info *prop = ref->prop;
if (prop && obj) {
if (prop->flags & ZEND_ACC_STATIC) {
_DO_THROW("null is expected as object argument for static properties");
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();
}
prop = reflection_property_get_effective_prop(ref, intern->ce, obj);
}

zend_class_entry *ce = obj ? obj->ce : intern->ce;
if (!prop) {
if (!(ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) {
RETURN_TRUE;
}
/* This path is effectively unreachable, but theoretically possible for
* two internal classes where ZEND_ACC_NO_DYNAMIC_PROPERTIES is only
* added to the subclass, in which case a ReflectionProperty can be
* constructed on the parent class, and then tested on the subclass. */
handle_magic_set:
RETURN_BOOL(ce->__set);
}

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)) {
if (!(prop->flags & ZEND_ACC_STATIC)) {
goto handle_magic_set;
}
RETURN_FALSE;
}
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;
}
} else if (obj && (prop->flags & ZEND_ACC_READONLY)) {
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

RETURN_FALSE;
}
}

RETURN_TRUE;
}

/* {{{ Constructor. Throws an Exception in case the given extension does not exist */
ZEND_METHOD(ReflectionExtension, __construct)
{
Expand Down
4 changes: 4 additions & 0 deletions ext/reflection/php_reflection.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,10 @@ public function hasHook(PropertyHookType $type): bool {}
public function getHook(PropertyHookType $type): ?ReflectionMethod {}

public function isFinal(): bool {}

public function isReadable(?string $scope, ?object $object = null): bool {}

public function isWritable(?string $scope, ?object $object = null): bool {}
}

/** @not-serializable */
Expand Down
13 changes: 12 additions & 1 deletion ext/reflection/php_reflection_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions ext/reflection/php_reflection_decl.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Test ReflectionProperty::isReadable() dynamic
--FILE--
<?php

#[AllowDynamicProperties]
class A {}

$a = new A;

$a->a = 'a';
$r = new ReflectionProperty($a, 'a');

var_dump($r->isReadable(null, $a));
unset($a->a);
var_dump($r->isReadable(null, $a));

$a = new A;
var_dump($r->isReadable(null, $a));

var_dump($r->isReadable(null, null));

?>
--EXPECT--
bool(true)
bool(false)
bool(false)
bool(false)
39 changes: 39 additions & 0 deletions ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Test ReflectionProperty::isReadable() hooks
--FILE--
<?php

class A {
public $a { get => $this->a; }
public $b { get => 42; }
public $c { set => $value; }
public $d { set {} }
public $e { get => $this->e; set => $value; }
public $f { get {} set {} }
}

function test($scope) {
$rc = new ReflectionClass(A::class);
foreach ($rc->getProperties() as $rp) {
echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': ';
var_dump($rp->isReadable($scope, null));
}
}

test('A');
test(null);

?>
--EXPECT--
a from A: bool(true)
b from A: bool(true)
c from A: bool(true)
d from A: bool(false)
e from A: bool(true)
f from A: bool(true)
a from global: bool(true)
b from global: bool(true)
c from global: bool(true)
d from global: bool(false)
e from global: bool(true)
f from global: bool(true)
Loading
Loading