flow-development-collection icon indicating copy to clipboard operation
flow-development-collection copied to clipboard

class(.*\\SpecialController) leads to ProxyBuilding of TYPO3Fluid\\Fluid\\Core\\Variables\\StandardVariableProvider

Open fcool opened this issue 4 years ago • 15 comments

Description

If you use a matcher in your Policy.yaml like that:

privilegeTargets:
  Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege:
    MyPrivilegeName:
      matcher: "class(.*\\StartController)"

Flow will build a proxy for TYPO3Fluid\\Fluid\\Core\\Variables\\StandardVariableProvider which then does not match TYPO3Fluid\\Fluid\\Core\\Variables\\VariableProviderInterface because of the constructor definition, resulting in a PHP Fatal error.

Expected behavior

While we probably cannot change the "awkward" decision of TYPO3Fluid we probably should not all of sudden build proxy for every class, if we stumble over such an expression.

I'm still unsure, where the problem exactly arises but believe it comes from here: \Neos\Flow\Aop\Pointcut\PointcutClassNameFilter::reduceTargetClassNames but I cannot get the big picture together.

Flow: 6.3

fcool avatar Aug 31 '21 17:08 fcool

The proxy building shouldn't be affected by your policy, i.e. proxies are always created unless a class has a Proxy(false) annotation or isn't a class. => Flow won't create a proxy for VariableProviderInterface.

resulting in a PHP Fatal error.

Could you share that?

bwaidelich avatar Sep 01 '21 08:09 bwaidelich

The error?

[Tue Aug 31 19:21:55.347686 2021] [proxy_fcgi:error] [pid 30257:tid 140693842413120] [client 127.0.0.1:44376] AH01071: Got error 'PHP message: PHP Fatal error:  Declaration of TYPO3Fluid\\Fluid\\Core\\Variables\\StandardVariableProvider::__construct() must be compatible with TYPO3Fluid\\Fluid\\Core\\Variables\\VariableProviderInterface::__construct(array $variables = Array) in /projectpath/Data/Temporary/Development/Cache/Code/Flow_Object_Classes/TYPO3Fluid_Fluid_Core_Variables_StandardVariableProvider.php on line 410'

Coming when renderin any Fluid template.

And I'm absolutely with you. It should not, but it does.

fcool avatar Sep 01 '21 08:09 fcool

Ah, the problem is that the interface declares a constructor (which is considered bad style btw) in this case. That and the fact that apparently Flow changes the constructor for the StandardVariableProvider somehow. Is it possible that your privilege matches that class somehow?

I'm not sure if class(.*\\StartController) does what it is expected to do. Btw what is it expected to do? Match all classes that happen to contain the string "\StartController" in their fully qualified name?

bwaidelich avatar Sep 01 '21 08:09 bwaidelich

The privilege matches, even if it definitely should not. And the matches function returns false for this specific class name, too.

But if this matcher is set, the modified constructor is built. If it is not, it is not.

And yes, the "intention" is, to match anything ending with "\StartContoller" so \Acme\Package\StartController but not \Acme\Package\SpecialStartController

It is quite "easy" to fix the problem by adjusting the matcher. But I guess the framework should not trigger this unexpected behavior nevertheless.

fcool avatar Sep 01 '21 08:09 fcool

It happens with

privilegeTargets:
  Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege:
    MyPrivilegeName:
      matcher: "class(.*StartController)"

too.

That is why I suspecting the reduceTargetClassNames as guilty. Because it searches for expressions beginning with . (and some other chars) and returns the unmodified ClassNameIndex in such a case.

fcool avatar Sep 01 '21 08:09 fcool

(thinking of it it comes to my mind, that it maybe should be possible to copy the original constructor signatur with all arguments being optional and not changing anything else, allowing to advice classes EVEN if they implements "bad" interfaces. But that does not mean, that I consider this behavior here as at least "unexpected")

fcool avatar Sep 01 '21 08:09 fcool

OK, I can reproduce this: With the privilege as described above an invalid proxy class for the StandardVariableProvider class is generated. And, interestingly, proxies for 90 additional classes (in a 5.3 Neos app) - even though I don't have a single StartController.

The (simplified) proxy code that is generated: https://3v4l.org/qk9DT

So I think the bug is two fold:

  • The matcher seems to be misinterpreted and matches too many classes
  • We don't support proxying of classes that implement an interface with a constructor declaration

it maybe should be possible to copy the original constructor signatur with all arguments being optional and not changing anything else

Yes, that seems like a good idea

bwaidelich avatar Sep 01 '21 08:09 bwaidelich

Related thread on discuss -> https://discuss.neos.io/t/flow-generates-faulty-proxy-classes/5592/11

sorenmalling avatar Sep 01 '21 12:09 sorenmalling

@bwaidelich thanks for pointing me to this issue. Just to link it here, this issue is related to this one: https://github.com/neos/neos-development-collection/issues/3418 - though I just realize I obviuosly posted that one in the wrong repository...

ifx-cw avatar Sep 01 '21 12:09 ifx-cw

https://github.com/neos/flow-development-collection/issues/2554 I moved the issue to the flow-development-collection and I agree with Bastian's assessment

PS: I would suggest to focus this issue on the "The matcher seems to be misinterpreted and matches too many classes" and the other one regarding proxying of constructors with arguments/interfaces

albe avatar Sep 01 '21 14:09 albe

I tried to address the proxy building issue with #2555

Shall we convert this report to one covering the other issue (a pointcut expression of class(.*\\StartController) should not target the StandardVariableProvider class)?

bwaidelich avatar Sep 03 '21 08:09 bwaidelich

Unfortunately the issue still remains, whenever using a pointcut expression like class(.*\\....) a proxy class is built for every class in the system (!)

I was able to track this down to PointcutClassNameFilter:: reduceTargetClassNames() This piece of code is more than 10 years old(!) and it was introduced in order to speed up proxy class building by skipping all classes that don't match the specified prefix.

I wonder why we can't filter the class names against the whole regex, but I'm sure there is a reason for that

bwaidelich avatar Jul 14 '22 12:07 bwaidelich

@bwaidelich could it be that it'S as simple as \Neos\Flow\Aop\AspectContainer::reduceTargetClassNames to me it looks like it unions all classes that are affected by one of the methods in the aspect, so if just one doesn't restrict the classnames in the pointcut, that would mean all classes are affected, and the AOP ProxyBuilder then unions all class names of all AspectContainers, so I have the feeling if just one aspect has an unrestricted pointcut everything will be affected, BUT shouldn't Fluid as a third party package be excluded from proxy building (yes it shoudl but the fluid adaptor includes them in settings because I guess that was necessary at some point)

kitsunet avatar Jul 14 '22 12:07 kitsunet

Right

a proxy class is built for every class in the system

was not precise. Only for classes for which object management is enabled (and yes, it seems for Fluid this is the case)

I have the feeling if just one aspect has an unrestricted pointcut everything will be affected

The thing is: It doesn't have to be class(.*) as in: "match all classes". But even if it is class(.*\\SomeClass), a proxy is built because the regex doesn't match => the class is not filtered

bwaidelich avatar Jul 14 '22 12:07 bwaidelich

Unfortunately the issue still remains, whenever using a pointcut expression like class(.*\\....) a proxy class is built for every class in the system (!)

I was able to track this down to PointcutClassNameFilter:: reduceTargetClassNames() This piece of code is more than 10 years old(!) and it was introduced in order to speed up proxy class building by skipping all classes that don't match the specified prefix.

if (!preg_match('/^([^\.\(\)\{\}\[\]\?\+\$\!\|]+)/', $this->originalExpressionString, $matches)) {

😨 this regex is a little bit hellish and even after reading it twice I'm not sure what it's supposed to do. Maybe because it's a NOT matches something starting with NOT these, mostly unnecessarily escaped, characters. Am I right in that this check will just make any expression that doesn't start with a character, hence a Namespace/Classname, not filter anything out? Still, AFAIK this is only an optimizing pre-filtering done to reduce the amount of class names that the actual matches() method has to be run against. So even if that just passes along all classes, the matches() method should finally disregard all classes that do not match the regex .*\\Acme, so why isn't it?

https://3v4l.org/ARMDP#v7.4.30

albe avatar Jul 16 '22 17:07 albe

As this bug just hit me too, I also found an older related bug report which I assume could be closed: #1011

In case someone googles the error, I received this one:

Cannot build proxy for class "TYPO3Fluid\Fluid\Core\Variables\ChainedVariableProvider" because it implements interface "TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface" that defines a constructor with arguments.

gjwnc avatar Jan 20 '23 10:01 gjwnc

I took some time today trying to reproduce this behavior through tests, but was not successful yet. My current take is that the bug must lie in the optimization code (reduceTargetClassNames) as you already pointed out.

Whoever tries to fix this next: try to create a unit test containing the mentioned filter and target class and see it fail.

robertlemke avatar May 31 '23 13:05 robertlemke