Skip to content

Commit 7423da4

Browse files
committed
PHPC-2159: Consider enums and traits in BSON decoding instantiatable checks
Prohibit uninstantiatable classes in type maps, and ignore them when processing __pclass fields. Utilize zend_get_object_type_case from PHP 8.2 for more helpful error messages in php_phongo_bson_state_fetch_class.
1 parent 889d536 commit 7423da4

8 files changed

+382
-106
lines changed

src/phongo_bson.c

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
#include "bson/bson.h"
1818

1919
#include <php.h>
20+
#if PHP_VERSION_ID >= 80100
21+
#include <Zend/zend_enum.h>
22+
#endif
2023
#include <Zend/zend_interfaces.h>
2124

2225
#include "php_array_api.h"
@@ -28,9 +31,6 @@
2831
#undef MONGOC_LOG_DOMAIN
2932
#define MONGOC_LOG_DOMAIN "PHONGO-BSON"
3033

31-
#define PHONGO_IS_CLASS_INSTANTIATABLE(ce) \
32-
(!(ce->ce_flags & (ZEND_ACC_INTERFACE | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)))
33-
3434
#define PHONGO_BSON_STATE_ZCHILD(state) (&((php_phongo_bson_state*) (state))->zchild)
3535

3636
#define PHONGO_FIELD_PATH_EXPANSION 8
@@ -39,6 +39,21 @@
3939
static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data);
4040
static bool php_phongo_bson_visit_array(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_array, void* data);
4141

42+
static inline bool phongo_is_class_instantiatable(const zend_class_entry* ce)
43+
{
44+
if (ce->ce_flags & (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS | ZEND_ACC_INTERFACE | ZEND_ACC_TRAIT)) {
45+
return false;
46+
}
47+
48+
#if PHP_VERSION_ID >= 80100
49+
if (ce->ce_flags & ZEND_ACC_ENUM) {
50+
return false;
51+
}
52+
#endif /* PHP_VERSION_ID < 80100 */
53+
54+
return true;
55+
}
56+
4257
/* Path builder */
4358
char* php_phongo_field_path_as_string(php_phongo_field_path* field_path)
4459
{
@@ -267,7 +282,7 @@ static bool php_phongo_bson_visit_binary(const bson_iter_t* iter ARG_UNUSED, con
267282
zend_class_entry* found_ce = zend_fetch_class(zs_classname, ZEND_FETCH_CLASS_AUTO | ZEND_FETCH_CLASS_SILENT);
268283
zend_string_release(zs_classname);
269284

270-
if (found_ce && PHONGO_IS_CLASS_INSTANTIATABLE(found_ce) && instanceof_function(found_ce, php_phongo_persistable_ce)) {
285+
if (found_ce && phongo_is_class_instantiatable(found_ce) && instanceof_function(found_ce, php_phongo_persistable_ce)) {
271286
((php_phongo_bson_state*) data)->odm = found_ce;
272287
}
273288
}
@@ -1239,8 +1254,8 @@ static zend_class_entry* php_phongo_bson_state_fetch_class(const char* classname
12391254

12401255
if (!found_ce) {
12411256
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Class %s does not exist", classname);
1242-
} else if (!PHONGO_IS_CLASS_INSTANTIATABLE(found_ce)) {
1243-
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Class %s is not instantiatable", classname);
1257+
} else if (!phongo_is_class_instantiatable(found_ce)) {
1258+
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "%s %s is not instantiatable", zend_get_object_type_uc(found_ce), classname);
12441259
} else if (!instanceof_function(found_ce, interface_ce)) {
12451260
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Class %s does not implement %s", classname, ZSTR_VAL(interface_ce->name));
12461261
} else {
@@ -1276,6 +1291,7 @@ static bool php_phongo_bson_state_parse_type(zval* options, const char* name, ph
12761291
if ((element->ce = php_phongo_bson_state_fetch_class(classname, classname_len, php_phongo_unserializable_ce))) {
12771292
element->type = PHONGO_TYPEMAP_CLASS;
12781293
} else {
1294+
/* Exception already thrown */
12791295
retval = false;
12801296
}
12811297
}

tests/bson/bson-toPHP-003.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ Class MyClass does not implement MongoDB\BSON\Unserializable
268268
=== IS NOT A CONCRETE CLASS ===
269269

270270
{ "foo": "yes" }
271-
Class MongoDB\BSON\Unserializable is not instantiatable
271+
Interface MongoDB\BSON\Unserializable is not instantiatable
272272

273273

274274
=== IS NOT A CONCRETE CLASS VIA PCLASS ===

tests/bson/bson-toPHP-013.phpt

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
--TEST--
2+
Uninstantiatable classes are ignored when processing __pclass
3+
--FILE--
4+
<?php
5+
6+
require_once __DIR__ . '/../utils/basic.inc';
7+
8+
abstract class MyAbstractDocument implements MongoDB\BSON\Unserializable {}
9+
10+
class MyDocument {}
11+
12+
trait MyTrait {}
13+
14+
$classes = [
15+
'MissingClass',
16+
MyAbstractDocument::class,
17+
MyDocument::class,
18+
MyTrait::class,
19+
MongoDB\BSON\Persistable::class,
20+
];
21+
22+
foreach ($classes as $class) {
23+
var_dump(toPHP(fromJSON(sprintf('{"x": {"__pclass": {"$binary": "%s", "$type": "80"}, "y": 1}}', base64_encode($class)))));
24+
}
25+
26+
?>
27+
===DONE===
28+
<?php exit(0); ?>
29+
--EXPECTF--
30+
object(stdClass)#%d (%d) {
31+
["x"]=>
32+
object(stdClass)#%d (%d) {
33+
["__pclass"]=>
34+
object(MongoDB\BSON\Binary)#%d (%d) {
35+
["data"]=>
36+
string(12) "MissingClass"
37+
["type"]=>
38+
int(128)
39+
}
40+
["y"]=>
41+
int(1)
42+
}
43+
}
44+
object(stdClass)#%d (%d) {
45+
["x"]=>
46+
object(stdClass)#%d (%d) {
47+
["__pclass"]=>
48+
object(MongoDB\BSON\Binary)#%d (%d) {
49+
["data"]=>
50+
string(18) "MyAbstractDocument"
51+
["type"]=>
52+
int(128)
53+
}
54+
["y"]=>
55+
int(1)
56+
}
57+
}
58+
object(stdClass)#%d (%d) {
59+
["x"]=>
60+
object(stdClass)#%d (%d) {
61+
["__pclass"]=>
62+
object(MongoDB\BSON\Binary)#%d (%d) {
63+
["data"]=>
64+
string(10) "MyDocument"
65+
["type"]=>
66+
int(128)
67+
}
68+
["y"]=>
69+
int(1)
70+
}
71+
}
72+
object(stdClass)#%d (%d) {
73+
["x"]=>
74+
object(stdClass)#%d (%d) {
75+
["__pclass"]=>
76+
object(MongoDB\BSON\Binary)#%d (%d) {
77+
["data"]=>
78+
string(7) "MyTrait"
79+
["type"]=>
80+
int(128)
81+
}
82+
["y"]=>
83+
int(1)
84+
}
85+
}
86+
object(stdClass)#%d (%d) {
87+
["x"]=>
88+
object(stdClass)#%d (%d) {
89+
["__pclass"]=>
90+
object(MongoDB\BSON\Binary)#%d (%d) {
91+
["data"]=>
92+
string(24) "MongoDB\BSON\Persistable"
93+
["type"]=>
94+
int(128)
95+
}
96+
["y"]=>
97+
int(1)
98+
}
99+
}
100+
===DONE===

tests/bson/bson-toPHP-014.phpt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
--TEST--
2+
Uninstantiatable classes are ignored when processing __pclass (enums)
3+
--SKIPIF--
4+
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
5+
<?php skip_if_php_version('<', '8.1.0'); ?>
6+
--FILE--
7+
<?php
8+
9+
require_once __DIR__ . '/../utils/basic.inc';
10+
11+
enum MyEnum
12+
{
13+
case A;
14+
}
15+
16+
enum MyBackedEnum: int
17+
{
18+
case A = 1;
19+
}
20+
21+
/* Note: the following BSON data corresponds to what might be produced by enums
22+
* implementing Persistable. Although that is now prohibited in the driver, it
23+
* could have been produced by an earlier version. */
24+
var_dump(toPHP(fromJSON(sprintf('{"x": {"__pclass": {"$binary": "%s", "$type": "80"}, "name": "A"}}', base64_encode(MyEnum::class)))));
25+
26+
var_dump(toPHP(fromJSON(sprintf('{"x": {"__pclass": {"$binary": "%s", "$type": "80"}, "name": "A", "value": 1}}', base64_encode(MyBackedEnum::class)))));
27+
28+
?>
29+
===DONE===
30+
<?php exit(0); ?>
31+
--EXPECTF--
32+
object(stdClass)#%d (%d) {
33+
["x"]=>
34+
object(stdClass)#%d (%d) {
35+
["__pclass"]=>
36+
object(MongoDB\BSON\Binary)#%d (%d) {
37+
["data"]=>
38+
string(6) "MyEnum"
39+
["type"]=>
40+
int(128)
41+
}
42+
["name"]=>
43+
string(1) "A"
44+
}
45+
}
46+
object(stdClass)#%d (%d) {
47+
["x"]=>
48+
object(stdClass)#%d (%d) {
49+
["__pclass"]=>
50+
object(MongoDB\BSON\Binary)#%d (%d) {
51+
["data"]=>
52+
string(12) "MyBackedEnum"
53+
["type"]=>
54+
int(128)
55+
}
56+
["name"]=>
57+
string(1) "A"
58+
["value"]=>
59+
int(1)
60+
}
61+
}
62+
===DONE===

tests/bson/bson-toPHP_error-001.phpt

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
MongoDB\BSON\toPHP(): Type classes must be instantiatable and implement Unserializable
2+
MongoDB\BSON\toPHP(): Type map classes must be instantiatable and implement Unserializable
33
--FILE--
44
<?php
55

@@ -9,32 +9,30 @@ abstract class MyAbstractDocument implements MongoDB\BSON\Unserializable {}
99

1010
class MyDocument {}
1111

12-
$types = [
13-
'array',
14-
'document',
15-
'root',
16-
];
12+
trait MyTrait {}
1713

1814
$classes = [
1915
'MissingClass',
20-
'MyAbstractDocument',
21-
'MyDocument',
22-
'MongoDB\BSON\Unserializable',
16+
MyAbstractDocument::class,
17+
MyDocument::class,
18+
MyTrait::class,
19+
MongoDB\BSON\Unserializable::class,
2320
];
2421

25-
$bson = pack('Vx', 5); // Empty document
26-
27-
foreach ($types as $type) {
28-
foreach ($classes as $class) {
29-
$typeMap = [$type => $class];
22+
foreach ($classes as $class) {
23+
$typeMaps = [
24+
['array' => $class],
25+
['document' => $class],
26+
['root' => $class],
27+
['fieldPaths' => ['x' => $class]],
28+
];
3029

30+
foreach ($typeMaps as $typeMap) {
3131
printf("Test typeMap: %s\n", json_encode($typeMap));
3232

33-
echo throws(function() use ($bson, $typeMap) {
34-
toPHP($bson, $typeMap);
35-
}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n";
36-
37-
echo "\n";
33+
echo throws(function() use ($typeMap) {
34+
toPHP(fromJSON('{}'), $typeMap);
35+
}, MongoDB\Driver\Exception\InvalidArgumentException::class), "\n\n";
3836
}
3937
}
4038

@@ -46,48 +44,80 @@ Test typeMap: {"array":"MissingClass"}
4644
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
4745
Class MissingClass does not exist
4846

49-
Test typeMap: {"array":"MyAbstractDocument"}
47+
Test typeMap: {"document":"MissingClass"}
5048
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
51-
Class MyAbstractDocument is not instantiatable
49+
Class MissingClass does not exist
5250

53-
Test typeMap: {"array":"MyDocument"}
51+
Test typeMap: {"root":"MissingClass"}
5452
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
55-
Class MyDocument does not implement MongoDB\BSON\Unserializable
53+
Class MissingClass does not exist
5654

57-
Test typeMap: {"array":"MongoDB\\BSON\\Unserializable"}
55+
Test typeMap: {"fieldPaths":{"x":"MissingClass"}}
5856
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
59-
Class MongoDB\BSON\Unserializable is not instantiatable
57+
Class MissingClass does not exist
6058

61-
Test typeMap: {"document":"MissingClass"}
59+
Test typeMap: {"array":"MyAbstractDocument"}
6260
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
63-
Class MissingClass does not exist
61+
Class MyAbstractDocument is not instantiatable
6462

6563
Test typeMap: {"document":"MyAbstractDocument"}
6664
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
6765
Class MyAbstractDocument is not instantiatable
6866

69-
Test typeMap: {"document":"MyDocument"}
67+
Test typeMap: {"root":"MyAbstractDocument"}
7068
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
71-
Class MyDocument does not implement MongoDB\BSON\Unserializable
69+
Class MyAbstractDocument is not instantiatable
7270

73-
Test typeMap: {"document":"MongoDB\\BSON\\Unserializable"}
71+
Test typeMap: {"fieldPaths":{"x":"MyAbstractDocument"}}
7472
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
75-
Class MongoDB\BSON\Unserializable is not instantiatable
73+
Class MyAbstractDocument is not instantiatable
7674

77-
Test typeMap: {"root":"MissingClass"}
75+
Test typeMap: {"array":"MyDocument"}
7876
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
79-
Class MissingClass does not exist
77+
Class MyDocument does not implement MongoDB\BSON\Unserializable
8078

81-
Test typeMap: {"root":"MyAbstractDocument"}
79+
Test typeMap: {"document":"MyDocument"}
8280
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
83-
Class MyAbstractDocument is not instantiatable
81+
Class MyDocument does not implement MongoDB\BSON\Unserializable
8482

8583
Test typeMap: {"root":"MyDocument"}
8684
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
8785
Class MyDocument does not implement MongoDB\BSON\Unserializable
8886

87+
Test typeMap: {"fieldPaths":{"x":"MyDocument"}}
88+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
89+
Class MyDocument does not implement MongoDB\BSON\Unserializable
90+
91+
Test typeMap: {"array":"MyTrait"}
92+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
93+
Trait MyTrait is not instantiatable
94+
95+
Test typeMap: {"document":"MyTrait"}
96+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
97+
Trait MyTrait is not instantiatable
98+
99+
Test typeMap: {"root":"MyTrait"}
100+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
101+
Trait MyTrait is not instantiatable
102+
103+
Test typeMap: {"fieldPaths":{"x":"MyTrait"}}
104+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
105+
Trait MyTrait is not instantiatable
106+
107+
Test typeMap: {"array":"MongoDB\\BSON\\Unserializable"}
108+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
109+
Interface MongoDB\BSON\Unserializable is not instantiatable
110+
111+
Test typeMap: {"document":"MongoDB\\BSON\\Unserializable"}
112+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
113+
Interface MongoDB\BSON\Unserializable is not instantiatable
114+
89115
Test typeMap: {"root":"MongoDB\\BSON\\Unserializable"}
90116
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
91-
Class MongoDB\BSON\Unserializable is not instantiatable
117+
Interface MongoDB\BSON\Unserializable is not instantiatable
118+
119+
Test typeMap: {"fieldPaths":{"x":"MongoDB\\BSON\\Unserializable"}}
120+
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
121+
Interface MongoDB\BSON\Unserializable is not instantiatable
92122

93123
===DONE===

0 commit comments

Comments
 (0)