diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index bb27b6e63f8de..c3705396ea712 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -738,6 +738,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */ zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0); return 0; } + if ((flags & ZEND_ACC_LOCKED) && (new_flag & ZEND_ACC_LOCKED)) { + zend_throw_exception(zend_ce_compile_error, "Multiple locked modifiers are not allowed", 0); + return 0; + } if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) { zend_throw_exception(zend_ce_compile_error, "Cannot use the final modifier on an abstract class", 0); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 82d70fd4b026d..553dcf16378d9 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -226,7 +226,7 @@ typedef struct _zend_oparray_context { /* Top-level class or function declaration | | | */ #define ZEND_ACC_TOP_LEVEL (1 << 9) /* X | X | | */ /* | | | */ -/* Class Flags (unused: 16...) | | | */ +/* Class Flags (unused: 17...) | | | */ /* =========== | | | */ /* | | | */ /* Special class types | | | */ @@ -266,6 +266,9 @@ typedef struct _zend_oparray_context { /* Children must reuse parent get_iterator() | | | */ #define ZEND_ACC_REUSE_GET_ITERATOR (1 << 17) /* X | | | */ /* | | | */ +/* Class is declared "locked" | | | */ +#define ZEND_ACC_LOCKED (1 << 18) /* X | | | */ +/* | | | */ /* Function Flags (unused: 28...30) | | | */ /* ============== | | | */ /* | | | */ diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 8e0728f4b559f..6022e090fa224 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -83,7 +83,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %left T_ELSEIF %left T_ELSE %left T_ENDIF -%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC +%right T_STATIC T_ABSTRACT T_FINAL T_LOCKED T_PRIVATE T_PROTECTED T_PUBLIC %token T_LNUMBER "integer number (T_LNUMBER)" %token T_DNUMBER "floating-point number (T_DNUMBER)" @@ -178,6 +178,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_STATIC "static (T_STATIC)" %token T_ABSTRACT "abstract (T_ABSTRACT)" %token T_FINAL "final (T_FINAL)" +%token T_LOCKED "locked (T_LOCKED)" %token T_PRIVATE "private (T_PRIVATE)" %token T_PROTECTED "protected (T_PROTECTED)" %token T_PUBLIC "public (T_PUBLIC)" @@ -278,7 +279,7 @@ reserved_non_modifiers: semi_reserved: reserved_non_modifiers - | T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC + | T_STATIC | T_ABSTRACT | T_FINAL | T_LOCKED | T_PRIVATE | T_PROTECTED | T_PUBLIC ; identifier: @@ -518,6 +519,7 @@ class_modifiers: class_modifier: T_ABSTRACT { $$ = ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; } | T_FINAL { $$ = ZEND_ACC_FINAL; } + | T_LOCKED { $$ = ZEND_ACC_LOCKED; } ; trait_declaration_statement: diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index e93431de24f0b..28e91c800724f 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1561,6 +1561,10 @@ NEWLINE ("\r"|"\n"|"\r\n") RETURN_TOKEN(T_FINAL); } +"locked" { + RETURN_TOKEN(T_LOCKED); +} + "private" { RETURN_TOKEN(T_PRIVATE); } diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 8a75c51de6579..b4c0cb603ebfa 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -779,6 +779,8 @@ ZEND_API zval *zend_std_read_property(zval *object, zval *member, int type, void zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(name)); + } else if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) { + zend_throw_error(NULL, "Cannot access undefined property $%s on locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name)); } else { zend_error(E_NOTICE,"Undefined property: %s::$%s", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name)); } @@ -883,6 +885,11 @@ ZEND_API zval *zend_std_write_property(zval *object, zval *member, zval *value, ZVAL_COPY_VALUE(variable_ptr, value); } else { + if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) { + zend_throw_error(NULL, "Cannot write undefined property $%s on locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name)); + variable_ptr = &EG(error_zval); + goto exit; + } if (!zobj->properties) { rebuild_object_properties(zobj); } @@ -1075,6 +1082,11 @@ ZEND_API void zend_std_unset_property(zval *object, zval *member, void **cache_s property_offset = zend_get_property_offset(zobj->ce, name, (zobj->ce->__unset != NULL), cache_slot, &prop_info); if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) { + if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) { + zend_throw_error(NULL, "Cannot unset property $%s of locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name)); + goto exit; + } + zval *slot = OBJ_PROP(zobj, property_offset); if (Z_TYPE_P(slot) != IS_UNDEF) { @@ -1093,6 +1105,11 @@ ZEND_API void zend_std_unset_property(zval *object, zval *member, void **cache_s } } else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset)) && EXPECTED(zobj->properties != NULL)) { + if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) { + zend_throw_error(NULL, "Cannot unset property $%s of locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name)); + goto exit; + } + if (UNEXPECTED(GC_REFCOUNT(zobj->properties) > 1)) { if (EXPECTED(!(GC_FLAGS(zobj->properties) & IS_ARRAY_IMMUTABLE))) { GC_DELREF(zobj->properties); diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 4c6f6f9aa48b9..703a471413474 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -325,6 +325,9 @@ static void _class_string(smart_str *str, zend_class_entry *ce, zval *obj, char if (ce->ce_flags & ZEND_ACC_FINAL) { smart_str_append_printf(str, "final "); } + if (ce->ce_flags & ZEND_ACC_LOCKED) { + smart_str_append_printf(str, "locked "); + } smart_str_append_printf(str, "class "); } smart_str_append_printf(str, "%s", ZSTR_VAL(ce->name)); @@ -1478,6 +1481,9 @@ ZEND_METHOD(reflection, getModifierNames) if (modifiers & ZEND_ACC_FINAL) { add_next_index_stringl(return_value, "final", sizeof("final")-1); } + if (modifiers & ZEND_ACC_LOCKED) { + add_next_index_stringl(return_value, "locked", sizeof("locked")-1); + } /* These are mutually exclusive */ switch (modifiers & ZEND_ACC_PPP_MASK) { @@ -4579,6 +4585,14 @@ ZEND_METHOD(reflection_class, isAbstract) } /* }}} */ +/* {{{ proto public bool ReflectionClass::isLocked() + Returns whether this class is locked */ +ZEND_METHOD(reflection_class, isLocked) +{ + _class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_LOCKED); +} +/* }}} */ + /* {{{ proto public int ReflectionClass::getModifiers() Returns a bitfield of the access modifiers for this class */ ZEND_METHOD(reflection_class, getModifiers) @@ -4586,7 +4600,8 @@ ZEND_METHOD(reflection_class, getModifiers) reflection_object *intern; zend_class_entry *ce; uint32_t keep_flags = ZEND_ACC_FINAL - | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; + | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS + | ZEND_ACC_LOCKED; if (zend_parse_parameters_none() == FAILURE) { return; @@ -6513,6 +6528,7 @@ static const zend_function_entry reflection_class_functions[] = { ZEND_ME(reflection_class, isTrait, arginfo_reflection__void, 0) ZEND_ME(reflection_class, isAbstract, arginfo_reflection__void, 0) ZEND_ME(reflection_class, isFinal, arginfo_reflection__void, 0) + ZEND_ME(reflection_class, isLocked, arginfo_reflection__void, 0) ZEND_ME(reflection_class, getModifiers, arginfo_reflection__void, 0) ZEND_ME(reflection_class, isInstance, arginfo_reflection_class_isInstance, 0) ZEND_ME(reflection_class, newInstance, arginfo_reflection_class_newInstance, 0) @@ -6846,6 +6862,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS); REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS); REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL); + REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_LOCKED", ZEND_ACC_LOCKED); INIT_CLASS_ENTRY(_reflection_entry, "ReflectionObject", reflection_object_functions); reflection_init_class_handlers(&_reflection_entry); diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index e97af14110be6..0ea70cbad33bf 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -11,10 +11,11 @@ echo $rc; --EXPECT-- Class [ class ReflectionClass implements Reflector ] { - - Constants [3] { + - Constants [4] { Constant [ public int IS_IMPLICIT_ABSTRACT ] { 16 } Constant [ public int IS_EXPLICIT_ABSTRACT ] { 64 } Constant [ public int IS_FINAL ] { 32 } + Constant [ public int IS_LOCKED ] { 262144 } } - Static properties [0] { @@ -34,7 +35,7 @@ Class [ class ReflectionClass implements Reflector ] { Property [ public $name ] } - - Methods [53] { + - Methods [54] { Method [ final private method __clone ] { - Parameters [0] { @@ -249,6 +250,12 @@ Class [ class ReflectionClass implements Reflector ] { } } + Method [ public method isLocked ] { + + - Parameters [0] { + } + } + Method [ public method getModifiers ] { - Parameters [0] { diff --git a/tests/classes/class_locked_derived.phpt b/tests/classes/class_locked_derived.phpt new file mode 100644 index 0000000000000..2b929b8c0f55f --- /dev/null +++ b/tests/classes/class_locked_derived.phpt @@ -0,0 +1,40 @@ +--TEST-- +A sub-class of a locked class is not locked unless explciitly marked +--FILE-- +definedProp = "OK\n"; +echo $t->definedProp; +unset($t->definedProp); + +$t->nonExistentProp = "Also OK\n"; +echo $t->nonExistentProp; +unset($t->nonExistentProp); + + +$t = new LockedDerived(); + +$t->definedProp = "OK\n"; +$t->nonExistentProp = "Not OK\n"; + + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- +OK +Also OK + +Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/class_locked_get.phpt b/tests/classes/class_locked_get.phpt new file mode 100644 index 0000000000000..6e0716744d8f3 --- /dev/null +++ b/tests/classes/class_locked_get.phpt @@ -0,0 +1,25 @@ +--TEST-- +A locked class prevents undefined properties being accessed +--FILE-- +definedProp = "OK\n"; +echo $t->definedProp; + +echo $t->nonExistentProp; + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- +OK + +Fatal error: Uncaught Error: Cannot access undefined property %s on locked class %s in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/class_locked_get_overload.phpt b/tests/classes/class_locked_get_overload.phpt new file mode 100644 index 0000000000000..8b679032ea165 --- /dev/null +++ b/tests/classes/class_locked_get_overload.phpt @@ -0,0 +1,30 @@ +--TEST-- +A locked class may still have __get overload +--FILE-- +definedProp = "OK\n"; +echo $t->definedProp; + +echo $t->privateProp; +echo $t->nonExistentProp; + +echo "Done\n"; +?> +--EXPECT-- +OK +__get called for privateProp +__get called for nonExistentProp +Done + diff --git a/tests/classes/class_locked_private.phpt b/tests/classes/class_locked_private.phpt new file mode 100644 index 0000000000000..a6042cfb1db2a --- /dev/null +++ b/tests/classes/class_locked_private.phpt @@ -0,0 +1,29 @@ +--TEST-- +A locked class cannot set undefined properties on its own instances +--FILE-- +definedProp = "OK\n"; + echo $this->definedProp; + + $this->nonExistentProp = "Not OK\n"; + } +} + +$t = new testClass(); +$t->test(); + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- +OK + +Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d +Stack trace: +#0 %s(%d): TestClass->test() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/class_locked_set.phpt b/tests/classes/class_locked_set.phpt new file mode 100644 index 0000000000000..cf1c1aa947aa5 --- /dev/null +++ b/tests/classes/class_locked_set.phpt @@ -0,0 +1,25 @@ +--TEST-- +A locked class prevents undefined properties being set +--FILE-- +definedProp = "OK\n"; +echo $t->definedProp; + +$t->nonExistentProp = "Not OK\n"; + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- +OK + +Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/class_locked_set_overload.phpt b/tests/classes/class_locked_set_overload.phpt new file mode 100644 index 0000000000000..ddb62dbbdffc8 --- /dev/null +++ b/tests/classes/class_locked_set_overload.phpt @@ -0,0 +1,30 @@ +--TEST-- +A locked class may still have __set overload +--FILE-- +definedProp = "OK\n"; +echo $t->definedProp; + +$t->privateProp = "call magic"; +$t->nonExistentProp = "call magic again"; + +echo "Done\n"; +?> +--EXPECT-- +OK +__set called for privateProp with 'call magic' +__set called for nonExistentProp with 'call magic again' +Done + diff --git a/tests/classes/class_locked_unset.phpt b/tests/classes/class_locked_unset.phpt new file mode 100644 index 0000000000000..0b177ec8a1ce2 --- /dev/null +++ b/tests/classes/class_locked_unset.phpt @@ -0,0 +1,22 @@ +--TEST-- +A locked class prevents defined properties being unset +--FILE-- +definedProp = "OK\n"; +unset($t->definedProp); + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- + +Fatal error: Uncaught Error: Cannot unset property %s of locked class %s in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/class_locked_unset_overload.phpt b/tests/classes/class_locked_unset_overload.phpt new file mode 100644 index 0000000000000..cbebe9b5d9dfc --- /dev/null +++ b/tests/classes/class_locked_unset_overload.phpt @@ -0,0 +1,32 @@ +--TEST-- +A locked class with __unset overload will only error for public properties +--FILE-- +nonExistentProp); +unset($t->privateProp); + +$t->definedProp = "OK\n"; +unset($t->definedProp); + +echo "Done\n"; // shouldn't be displayed +?> +--EXPECTF-- +__unset called for nonExistentProp +__unset called for privateProp + +Fatal error: Uncaught Error: Cannot unset property %s of locked class %s in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d