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
162 changes: 162 additions & 0 deletions ext/reflection/php_reflection.c
Original file line number Diff line number Diff line change
Expand Up @@ -6656,6 +6656,168 @@ 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_object *obj = NULL;
zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC);

ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_OBJ_OR_NULL(obj)
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();
}

if (obj) {
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 *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;
}
}

if (obj) {
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;
}

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

ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_OBJ_OR_NULL(obj)
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 isWritable on dynamic properties");
RETURN_THROWS();
}

if (obj) {
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 *scope;
if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) {
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 (obj && (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)
{
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 @@ -571,6 +571,10 @@ public function hasHook(PropertyHookType $type): bool {}
public function getHook(PropertyHookType $type): ?ReflectionMethod {}

public function isFinal(): bool {}

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

public function isWritable(?object $object = null, ?string $scope = 'static'): 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.

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

class A {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I would have added some tests for traits. For example, a test ensuring that protected properties from a shared trait are not accessible (https://3v4l.org/hM5TM). Also a trait must not be accepted as a scope.

This comment was marked as duplicate.


class B extends A {
public $a;
protected $b;
private $c;
public protected(set) int $d;
public $e { get => 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(null, $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)
90 changes: 90 additions & 0 deletions ext/reflection/tests/ReflectionProperty_isWritable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
--TEST--
Test ReflectionProperty::isWritable()
--FILE--
<?php

class A {
public $a;
protected $b;
private $c;
public protected(set) int $d;
public $e { get => 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)