2
2
3
3
namespace mglaman \PHPStanDrupal \Rules \Drupal ;
4
4
5
+ use Drupal \Core \Render \Element \RenderCallbackInterface ;
5
6
use Drupal \Core \Render \PlaceholderGenerator ;
6
7
use Drupal \Core \Render \Renderer ;
8
+ use Drupal \Core \Security \Attribute \TrustedCallback ;
9
+ use Drupal \Core \Security \TrustedCallbackInterface ;
7
10
use mglaman \PHPStanDrupal \Drupal \ServiceMap ;
8
11
use PhpParser \Node ;
9
12
use PhpParser \Node \Name ;
10
13
use PHPStan \Analyser \Scope ;
14
+ use PHPStan \Reflection \ClassReflection ;
11
15
use PHPStan \Reflection \ReflectionProvider ;
12
16
use PHPStan \Rules \Rule ;
13
- use PHPStan \Rules \RuleError ;
14
17
use PHPStan \Rules \RuleErrorBuilder ;
18
+ use PHPStan \TrinaryLogic ;
15
19
use PHPStan \Type \ClosureType ;
16
20
use PHPStan \Type \Constant \ConstantArrayType ;
17
21
use PHPStan \Type \Constant \ConstantIntegerType ;
20
24
use PHPStan \Type \IntersectionType ;
21
25
use PHPStan \Type \ObjectType ;
22
26
use PHPStan \Type \StaticType ;
23
- use PHPStan \Type \ThisType ;
24
27
use PHPStan \Type \Type ;
25
28
use PHPStan \Type \UnionType ;
26
29
use PHPStan \Type \VerbosityLevel ;
27
30
28
31
final class RenderCallbackRule implements Rule
29
32
{
30
33
31
- protected ReflectionProvider $ reflectionProvider ;
34
+ private ReflectionProvider $ reflectionProvider ;
32
35
33
- protected ServiceMap $ serviceMap ;
36
+ private ServiceMap $ serviceMap ;
37
+
38
+ private array $ supportedKeys = [
39
+ '#pre_render ' ,
40
+ '#post_render ' ,
41
+ '#access_callback ' ,
42
+ '#lazy_builder ' ,
43
+ ];
34
44
35
45
public function __construct (ReflectionProvider $ reflectionProvider , ServiceMap $ serviceMap )
36
46
{
@@ -51,20 +61,18 @@ public function processNode(Node $node, Scope $scope): array
51
61
return [];
52
62
}
53
63
54
- // @todo this should be 3 rules.
55
64
// @see https://www.drupal.org/node/2966725
56
- $ keysToCheck = ['#pre_render ' , '#post_render ' , '#access_callback ' , '#lazy_builder ' ];
57
- $ keySearch = array_search ($ key ->value , $ keysToCheck , true );
65
+ $ keySearch = array_search ($ key ->value , $ this ->supportedKeys , true );
58
66
if ($ keySearch === false ) {
59
67
return [];
60
68
}
61
- $ keyChecked = $ keysToCheck [$ keySearch ];
62
-
69
+ $ keyChecked = $ this ->supportedKeys [$ keySearch ];
63
70
$ value = $ node ->value ;
64
71
65
- $ errors = [];
72
+ if ($ keyChecked === '#access_callback ' ) {
73
+ return $ this ->doProcessNode ($ node ->value , $ scope , $ keyChecked , 0 );
74
+ }
66
75
67
- // @todo Move into its own rule.
68
76
if ($ keyChecked === '#lazy_builder ' ) {
69
77
if ($ scope ->isInClass ()) {
70
78
$ classReflection = $ scope ->getClassReflection ();
@@ -73,15 +81,13 @@ public function processNode(Node $node, Scope $scope): array
73
81
// PHPStan 1.6, nodes do not track their parent/next/prev which
74
82
// saves a lot of memory. But makes it harder to detect if we're
75
83
// in a call to array_intersect_key. This is an easier workaround.
76
- $ allowedTypes = [
77
- PlaceholderGenerator::class,
78
- Renderer::class,
79
- 'Drupal\Tests\Core\Render\RendererPlaceholdersTest ' ,
80
- ];
81
- foreach ($ allowedTypes as $ allowedType ) {
82
- if ($ classType ->isInstanceOf ($ allowedType )->yes ()) {
83
- return [];
84
- }
84
+ $ allowedTypes = new UnionType ([
85
+ new ObjectType (PlaceholderGenerator::class),
86
+ new ObjectType (Renderer::class),
87
+ new ObjectType ('Drupal\Tests\Core\Render\RendererPlaceholdersTest ' ),
88
+ ]);
89
+ if ($ allowedTypes ->isSuperTypeOf ($ classType )->yes ()) {
90
+ return [];
85
91
}
86
92
}
87
93
@@ -98,107 +104,131 @@ public function processNode(Node $node, Scope $scope): array
98
104
return [];
99
105
}
100
106
// @todo take $value->items[1] and validate parameters against the callback.
101
- $ errors [] = $ this ->doProcessNode ($ value ->items [0 ]->value , $ scope , $ keyChecked , 0 );
102
- } elseif ($ keyChecked === '#access_callback ' ) {
103
- // @todo move into its own rule.
104
- $ errors [] = $ this ->doProcessNode ($ value , $ scope , $ keyChecked , 0 );
105
- } else {
106
- // @todo keep here.
107
- if (!$ value instanceof Node \Expr \Array_) {
108
- return [
109
- RuleErrorBuilder::message (sprintf ('The "%s" render array value expects an array of callbacks. ' , $ keyChecked ))
110
- ->line ($ node ->getLine ())->build ()
111
- ];
112
- }
113
- if (count ($ value ->items ) === 0 ) {
114
- return [];
115
- }
116
- foreach ($ value ->items as $ pos => $ item ) {
117
- if (!$ item instanceof Node \Expr \ArrayItem) {
118
- continue ;
119
- }
120
- $ errors [] = $ this ->doProcessNode ($ item ->value , $ scope , $ keyChecked , $ pos );
107
+ return $ this ->doProcessNode ($ value ->items [0 ]->value , $ scope , $ keyChecked , 0 );
108
+ }
109
+
110
+ if (!$ value instanceof Node \Expr \Array_) {
111
+ return [
112
+ RuleErrorBuilder::message (sprintf ('The "%s" render array value expects an array of callbacks. ' , $ keyChecked ))
113
+ ->line ($ node ->getLine ())->build ()
114
+ ];
115
+ }
116
+ if (count ($ value ->items ) === 0 ) {
117
+ return [];
118
+ }
119
+ $ errors = [];
120
+ foreach ($ value ->items as $ pos => $ item ) {
121
+ if (!$ item instanceof Node \Expr \ArrayItem) {
122
+ continue ;
121
123
}
124
+ $ errors [] = $ this ->doProcessNode ($ item ->value , $ scope , $ keyChecked , $ pos );
122
125
}
123
- return array_filter ( $ errors );
126
+ return array_merge (... $ errors );
124
127
}
125
128
126
- private function doProcessNode (Node \Expr $ node , Scope $ scope , string $ keyChecked , int $ pos ): ?RuleError
129
+ /**
130
+ @return (string|\PHPStan\Rules\RuleError)[] errors
131
+ */
132
+ private function doProcessNode (Node \Expr $ node , Scope $ scope , string $ keyChecked , int $ pos ): array
127
133
{
128
134
$ trustedCallbackType = new UnionType ([
129
- new ObjectType (' Drupal\Core\Security\ TrustedCallbackInterface' ),
130
- new ObjectType (' Drupal\Core\Render\Element\ RenderCallbackInterface' ),
135
+ new ObjectType (TrustedCallbackInterface::class ),
136
+ new ObjectType (RenderCallbackInterface::class ),
131
137
]);
132
138
139
+ $ errors = [];
133
140
$ errorLine = $ node ->getLine ();
134
141
$ type = $ this ->getType ($ node , $ scope );
135
142
136
- if ($ type instanceof ConstantStringType ) {
137
- if (!$ type ->isCallable ()->yes ()) {
138
- return RuleErrorBuilder::message (
139
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
143
+ foreach ($ type-> getConstantStrings () as $ constantStringType ) {
144
+ if (!$ constantStringType ->isCallable ()->yes ()) {
145
+ $ errors [] = RuleErrorBuilder::message (
146
+ sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ constantStringType ->describe (VerbosityLevel::value ()), $ pos )
140
147
)->line ($ errorLine )->build ();
141
- }
142
- // We can determine if the callback is callable through the type system. However, we cannot determine
143
- // if it is just a function or a static class call (MyClass::staticFunc).
144
- if ($ this ->reflectionProvider ->hasFunction (new Name ($ type ->getValue ()), null )) {
145
- return RuleErrorBuilder::message (
146
- sprintf ("%s callback %s at key '%s' is not trusted. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
148
+ } elseif ($ this ->reflectionProvider ->hasFunction (new Name ($ constantStringType ->getValue ()), null )) {
149
+ // We can determine if the callback is callable through the type system. However, we cannot determine
150
+ // if it is just a function or a static class call (MyClass::staticFunc).
151
+ $ errors [] = RuleErrorBuilder::message (
152
+ sprintf ("%s callback %s at key '%s' is not trusted. " , $ keyChecked , $ constantStringType ->describe (VerbosityLevel::value ()), $ pos )
147
153
)->line ($ errorLine )
148
154
->tip ('Change record: https://www.drupal.org/node/2966725. ' )
149
155
->build ();
150
- }
151
-
152
- if (!$ trustedCallbackType ->isSuperTypeOf ($ type )->yes ()) {
153
- return RuleErrorBuilder::message (
154
- sprintf ("%s callback class %s at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
156
+ } elseif (!$ trustedCallbackType ->isSuperTypeOf ($ type )->yes ()) {
157
+ $ errors [] = RuleErrorBuilder::message (
158
+ sprintf ("%s callback class %s at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " , $ keyChecked , $ constantStringType ->describe (VerbosityLevel::value ()), $ pos )
155
159
)->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/2966725. ' )->build ();
156
160
}
157
- } elseif ($ type instanceof ConstantArrayType) {
158
- if (!$ type ->isCallable ()->yes ()) {
159
- return RuleErrorBuilder::message (
160
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
161
+ }
162
+
163
+ foreach ($ type ->getConstantArrays () as $ constantArrayType ) {
164
+ if (!$ constantArrayType ->isCallable ()->yes ()) {
165
+ $ errors [] = RuleErrorBuilder::message (
166
+ sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ constantArrayType ->describe (VerbosityLevel::value ()), $ pos )
161
167
)->line ($ errorLine )->build ();
168
+ continue ;
162
169
}
163
- $ typeAndMethodNames = $ type ->findTypeAndMethodNames ();
170
+ $ typeAndMethodNames = $ constantArrayType ->findTypeAndMethodNames ();
164
171
if ($ typeAndMethodNames === []) {
165
- throw new \ PHPStan \ ShouldNotHappenException () ;
172
+ continue ;
166
173
}
167
174
168
175
foreach ($ typeAndMethodNames as $ typeAndMethodName ) {
169
- if (!$ trustedCallbackType ->isSuperTypeOf ($ typeAndMethodName ->getType ())->yes ()) {
170
- return RuleErrorBuilder::message (
171
- sprintf ("%s callback class '%s' at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " , $ keyChecked , $ typeAndMethodName ->getType ()->describe (VerbosityLevel::value ()), $ pos )
172
- )->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/2966725. ' )->build ();
173
- }
174
- }
175
- } elseif ($ type instanceof ClosureType) {
176
- if ($ scope ->isInClass ()) {
177
- $ classReflection = $ scope ->getClassReflection ();
178
- $ classType = new ObjectType ($ classReflection ->getName ());
179
- $ formType = new ObjectType ('\Drupal\Core\Form\FormInterface ' );
180
- if ($ formType ->isSuperTypeOf ($ classType )->yes ()) {
181
- return RuleErrorBuilder::message (
182
- sprintf ("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed. " , $ keyChecked , $ pos )
183
- )->line ($ errorLine )->build ();
176
+ $ isTrustedCallbackAttribute = TrinaryLogic::createNo ()->lazyOr (
177
+ $ typeAndMethodName ->getType ()->getObjectClassReflections (),
178
+ function (ClassReflection $ reflection ) use ($ typeAndMethodName ) {
179
+ if (!class_exists (TrustedCallback::class)) {
180
+ return TrinaryLogic::createNo ();
181
+ }
182
+ $ hasAttribute = $ reflection ->getNativeReflection ()
183
+ ->getMethod ($ typeAndMethodName ->getMethod ())
184
+ ->getAttributes (TrustedCallback::class);
185
+ return TrinaryLogic::createFromBoolean (count ($ hasAttribute ) > 0 );
186
+ }
187
+ );
188
+
189
+ $ isTrustedCallbackInterfaceType = $ trustedCallbackType ->isSuperTypeOf ($ typeAndMethodName ->getType ())->yes ();
190
+ if (!$ isTrustedCallbackInterfaceType && !$ isTrustedCallbackAttribute ->yes ()) {
191
+ if (class_exists (TrustedCallback::class)) {
192
+ $ errors [] = RuleErrorBuilder::message (
193
+ sprintf (
194
+ "%s callback method '%s' at key '%s' does not implement attribute \Drupal\Core\Security\Attribute\TrustedCallback. " ,
195
+ $ keyChecked ,
196
+ $ constantArrayType ->describe (VerbosityLevel::value ()),
197
+ $ pos
198
+ )
199
+ )->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/3349470 ' )->build ();
200
+ } else {
201
+ $ errors [] = RuleErrorBuilder::message (
202
+ sprintf (
203
+ "%s callback class '%s' at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " ,
204
+ $ keyChecked ,
205
+ $ typeAndMethodName ->getType ()->describe (VerbosityLevel::value ()),
206
+ $ pos
207
+ )
208
+ )->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/2966725. ' )->build ();
209
+ }
184
210
}
185
211
}
186
- } elseif ($ type instanceof ThisType) {
187
- if (!$ type ->isCallable ()->yes ()) {
188
- return RuleErrorBuilder::message (
189
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
212
+ }
213
+ // @todo move to its own rule for 1.2.0, FormClosureSerializationRule.
214
+ if (($ type instanceof ClosureType) && $ scope ->isInClass ()) {
215
+ $ classReflection = $ scope ->getClassReflection ();
216
+ $ classType = new ObjectType ($ classReflection ->getName ());
217
+ $ formType = new ObjectType ('\Drupal\Core\Form\FormInterface ' );
218
+ if ($ formType ->isSuperTypeOf ($ classType )->yes ()) {
219
+ $ errors [] = RuleErrorBuilder::message (
220
+ sprintf ("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed. " , $ keyChecked , $ pos )
190
221
)->line ($ errorLine )->build ();
191
222
}
192
- } elseif ($ type ->isCallable ()->yes ()) {
193
- // If the value has been marked as callable or callable-string, we cannot resolve the callable, trust it.
194
- return null ;
195
- } else {
196
- return RuleErrorBuilder::message (
223
+ }
224
+
225
+ if (count ($ errors ) === 0 && !$ type ->isCallable ()->yes ()) {
226
+ $ errors [] = RuleErrorBuilder::message (
197
227
sprintf ("%s value '%s' at key '%s' is invalid. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
198
228
)->line ($ errorLine )->build ();
199
229
}
200
230
201
- return null ;
231
+ return $ errors ;
202
232
}
203
233
204
234
// @todo move to a helper, as Drupal uses `service:method` references a lot.
0 commit comments