Skip to content

Commit 8a3c604

Browse files
committed
add support for isGranted in Twig and php on security.yaml, Voter::voteOnAttribute and Voter::supports #431
1 parent cf34961 commit 8a3c604

File tree

11 files changed

+604
-0
lines changed

11 files changed

+604
-0
lines changed

META-INF/plugin.xml

+1
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@
582582
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.dic.registrar.DicGotoCompletionRegistrar"/>
583583
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.completion.yaml.YamlGotoCompletionRegistrar"/>
584584
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.translation.ValidatorTranslationGotoCompletionRegistrar"/>
585+
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.security.VoterGotoCompletionRegistrar"/>
585586

586587
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.JsonFileIndexTwigNamespaces"/>
587588
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.ConfigAddPathTwigNamespaces"/>

src/fr/adrienbrault/idea/symfony2plugin/codeInsight/utils/GotoCompletionUtil.java

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.intellij.psi.xml.XmlAttributeValue;
66
import com.intellij.psi.xml.XmlText;
77
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
8+
import com.jetbrains.twig.TwigTokenTypes;
89
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionContributor;
910
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
1011
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
@@ -56,6 +57,9 @@ public static String getTextValueForElement(@NotNull PsiElement psiElement) {
5657
} else if(parent instanceof YAMLScalar) {
5758
// foo: foo, foo: 'foo', foo: "foo"
5859
value = ((YAMLScalar) parent).getTextValue();
60+
} else if(psiElement.getNode().getElementType() == TwigTokenTypes.STRING_TEXT) {
61+
// twig: 'foobar'
62+
value = psiElement.getText();
5963
}
6064

6165
if(StringUtils.isBlank(value)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package fr.adrienbrault.idea.symfony2plugin.security;
2+
3+
import com.intellij.codeInsight.lookup.LookupElement;
4+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
5+
import com.intellij.patterns.PlatformPatterns;
6+
import com.intellij.psi.PsiElement;
7+
import com.jetbrains.php.lang.PhpLanguage;
8+
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
9+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
10+
import fr.adrienbrault.idea.symfony2plugin.TwigHelper;
11+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
12+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
13+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
14+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.utils.GotoCompletionUtil;
15+
import fr.adrienbrault.idea.symfony2plugin.security.utils.VoterUtil;
16+
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
17+
import org.apache.commons.lang.StringUtils;
18+
import org.jetbrains.annotations.NotNull;
19+
20+
import java.util.ArrayList;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
24+
/**
25+
* @author Daniel Espendiller <daniel@espendiller.net>
26+
*/
27+
public class VoterGotoCompletionRegistrar implements GotoCompletionRegistrar {
28+
@Override
29+
public void register(GotoCompletionRegistrarParameter registrar) {
30+
// {% is_granted('foobar') %}
31+
registrar.register(
32+
TwigHelper.getPrintBlockOrTagFunctionPattern("is_granted"), MyVisitorGotoCompletionProvider::new
33+
);
34+
35+
// Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface::isGranted %}
36+
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
37+
PsiElement context = psiElement.getContext();
38+
if (!(context instanceof StringLiteralExpression)) {
39+
return null;
40+
}
41+
42+
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.StringParameterRecursiveMatcher(context, 0)
43+
.withSignature("Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller", "isGranted")
44+
.withSignature("Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller", "denyAccessUnlessGranted")
45+
.withSignature("Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface", "isGranted")
46+
.match();
47+
48+
if(methodMatchParameter != null) {
49+
return new MyVisitorGotoCompletionProvider(psiElement);
50+
}
51+
52+
// ['foobar']
53+
MethodMatcher.MethodMatchParameter arrayMatchParameter = new MethodMatcher.ArrayParameterMatcher(context, 0)
54+
.withSignature("Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller", "isGranted")
55+
.withSignature("Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller", "denyAccessUnlessGranted")
56+
.withSignature("Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface", "isGranted")
57+
.match();
58+
59+
if(arrayMatchParameter != null) {
60+
return new MyVisitorGotoCompletionProvider(psiElement);
61+
}
62+
63+
return null;
64+
});
65+
}
66+
67+
private static class MyVisitorGotoCompletionProvider extends GotoCompletionProvider {
68+
MyVisitorGotoCompletionProvider(PsiElement psiElement) {
69+
super(psiElement);
70+
}
71+
72+
@NotNull
73+
@Override
74+
public Collection<LookupElement> getLookupElements() {
75+
Collection<LookupElement> lookupElements = new ArrayList<>();
76+
77+
VoterUtil.visitAttribute(getProject(), pair ->
78+
lookupElements.add(LookupElementBuilder.create(pair.getFirst()).withIcon(Symfony2Icons.SYMFONY))
79+
);
80+
81+
return lookupElements;
82+
}
83+
84+
@NotNull
85+
@Override
86+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
87+
String text = GotoCompletionUtil.getTextValueForElement(element);
88+
if(StringUtils.isBlank(text)) {
89+
return Collections.emptyList();
90+
}
91+
92+
VoterUtil.TargetPairConsumer foo = new VoterUtil.TargetPairConsumer(text);
93+
VoterUtil.visitAttribute(getProject(), foo);
94+
return foo.getValues();
95+
}
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package fr.adrienbrault.idea.symfony2plugin.security.utils;
2+
3+
import com.intellij.openapi.project.Project;
4+
import com.intellij.openapi.util.Pair;
5+
import com.intellij.psi.PsiElement;
6+
import com.intellij.psi.PsiFile;
7+
import com.intellij.psi.PsiReference;
8+
import com.intellij.psi.search.FilenameIndex;
9+
import com.intellij.psi.search.GlobalSearchScope;
10+
import com.intellij.psi.tree.IElementType;
11+
import com.intellij.psi.util.PsiTreeUtil;
12+
import com.intellij.util.CommonProcessors;
13+
import com.jetbrains.php.PhpIndex;
14+
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
15+
import com.jetbrains.php.lang.parser.PhpElementTypes;
16+
import com.jetbrains.php.lang.psi.PhpPsiUtil;
17+
import com.jetbrains.php.lang.psi.elements.*;
18+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
19+
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
20+
import org.apache.commons.lang.StringUtils;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.yaml.YAMLFileType;
23+
import org.jetbrains.yaml.YAMLUtil;
24+
import org.jetbrains.yaml.psi.*;
25+
import org.jetbrains.yaml.psi.impl.YAMLHashImpl;
26+
27+
import java.util.HashSet;
28+
import java.util.Set;
29+
import java.util.function.Consumer;
30+
31+
/**
32+
* @author Daniel Espendiller <daniel@espendiller.net>
33+
*/
34+
public class VoterUtil {
35+
36+
public static void visitAttribute(@NotNull Project project, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
37+
for (PhpClass phpClass : PhpIndex.getInstance(project).getAllSubclasses("Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter")) {
38+
Method supports = phpClass.findMethodByName("supports");
39+
if(supports != null) {
40+
visitAttribute(supports, consumer);
41+
}
42+
43+
Method voteOnAttribute = phpClass.findMethodByName("voteOnAttribute");
44+
if(voteOnAttribute != null) {
45+
visitAttribute(voteOnAttribute, consumer);
46+
}
47+
}
48+
49+
for (PsiFile psiFile : FilenameIndex.getFilesByName(project, "security.yml", GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), YAMLFileType.YML))) {
50+
if(!(psiFile instanceof YAMLFile)) {
51+
continue;
52+
}
53+
54+
YAMLKeyValue roleHierarchy = YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, "security", "role_hierarchy");
55+
if(roleHierarchy != null) {
56+
YAMLValue value = roleHierarchy.getValue();
57+
if(!(value instanceof YAMLMapping)) {
58+
continue;
59+
}
60+
61+
for (YAMLPsiElement yamlPsiElement : value.getYAMLElements()) {
62+
if(!(yamlPsiElement instanceof YAMLKeyValue)) {
63+
continue;
64+
}
65+
66+
String keyText = ((YAMLKeyValue) yamlPsiElement).getKeyText();
67+
if(StringUtils.isNotBlank(keyText)) {
68+
consumer.accept(Pair.create(keyText, yamlPsiElement));
69+
}
70+
71+
YAMLValue yamlValue = ((YAMLKeyValue) yamlPsiElement).getValue();
72+
if(yamlValue instanceof YAMLSequence) {
73+
for (String item : YamlHelper.getYamlArrayValuesAsString((YAMLSequence) yamlValue)) {
74+
consumer.accept(Pair.create(item, yamlValue));
75+
}
76+
}
77+
}
78+
}
79+
80+
YAMLKeyValue accessControl = YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, "security", "access_control");
81+
if(accessControl != null) {
82+
YAMLValue value = accessControl.getValue();
83+
if(!(value instanceof YAMLSequence)) {
84+
continue;
85+
}
86+
87+
for (YAMLPsiElement yamlPsiElement : value.getYAMLElements()) {
88+
if(!(yamlPsiElement instanceof YAMLSequenceItem)) {
89+
continue;
90+
}
91+
92+
YAMLValue value1 = ((YAMLSequenceItem) yamlPsiElement).getValue();
93+
if(!(value1 instanceof YAMLMapping)) {
94+
continue;
95+
}
96+
97+
YAMLKeyValue roles = ((YAMLHashImpl) value1).getKeyValueByKey("roles");
98+
if(roles == null) {
99+
continue;
100+
}
101+
102+
YAMLValue value2 = roles.getValue();
103+
if(value2 instanceof YAMLScalar) {
104+
// roles: FOOBAR
105+
String textValue = ((YAMLScalar) value2).getTextValue();
106+
if(StringUtils.isNotBlank(textValue)) {
107+
consumer.accept(Pair.create(textValue, value2));
108+
}
109+
} else if(value2 instanceof YAMLSequence) {
110+
// roles: [FOOBAR, FOOBAR_1]
111+
for (String item : YamlHelper.getYamlArrayValuesAsString((YAMLSequence) value2)) {
112+
consumer.accept(Pair.create(item, value2));
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
private static void visitAttribute(@NotNull Method method, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
121+
Parameter[] parameters = method.getParameters();
122+
if(parameters.length == 0) {
123+
return;
124+
}
125+
126+
PhpPsiUtil.hasReferencesInSearchScope(
127+
method.getUseScope(),
128+
parameters[0],
129+
new MyAttributePsiReferenceFindProcessor(consumer)
130+
);
131+
}
132+
133+
public static class StringPairConsumer implements Consumer<Pair<String, PsiElement>> {
134+
private Set<String> values = new HashSet<>();
135+
136+
@Override
137+
public void accept(Pair<String, PsiElement> pair) {
138+
values.add(pair.getFirst());
139+
}
140+
141+
@NotNull
142+
public Set<String> getValues() {
143+
return values;
144+
}
145+
}
146+
147+
public static class TargetPairConsumer implements Consumer<Pair<String, PsiElement>> {
148+
@NotNull
149+
private final String filterValue;
150+
151+
@NotNull
152+
private Set<PsiElement> values = new HashSet<>();
153+
154+
public TargetPairConsumer(@NotNull String filterValue) {
155+
this.filterValue = filterValue;
156+
}
157+
158+
@Override
159+
public void accept(Pair<String, PsiElement> pair) {
160+
if(pair.getFirst().equalsIgnoreCase(filterValue)) {
161+
values.add(pair.getSecond());
162+
}
163+
}
164+
165+
@NotNull
166+
public Set<PsiElement> getValues() {
167+
return values;
168+
}
169+
}
170+
171+
/**
172+
* Find security roles on Voter implementation and security roles in Yaml
173+
*/
174+
private static class MyAttributePsiReferenceFindProcessor extends CommonProcessors.FindProcessor<PsiReference> {
175+
private final Consumer<Pair<String, PsiElement>> consumer;
176+
177+
MyAttributePsiReferenceFindProcessor(Consumer<Pair<String, PsiElement>> consumer) {
178+
this.consumer = consumer;
179+
}
180+
181+
@Override
182+
protected boolean accept(PsiReference psiReference) {
183+
PsiElement resolve = psiReference.getElement();
184+
185+
PsiElement parent = resolve.getParent();
186+
if(parent instanceof BinaryExpression) {
187+
// 'VALUE' == $var
188+
PsiElement rightElement = PsiTreeUtil.prevVisibleLeaf(resolve);
189+
if(rightElement != null) {
190+
IElementType node = rightElement.getNode().getElementType();
191+
if(isIfOperand(node)) {
192+
PsiElement leftOperand = ((BinaryExpression) parent).getLeftOperand();
193+
String stringValue = PhpElementsUtil.getStringValue(leftOperand);
194+
if(StringUtils.isNotBlank(stringValue)) {
195+
consumer.accept(Pair.create(stringValue, leftOperand));
196+
}
197+
}
198+
}
199+
200+
// $var == 'VALUE'
201+
PsiElement leftElement = PsiTreeUtil.nextVisibleLeaf(resolve);
202+
if(leftElement != null) {
203+
IElementType node = leftElement.getNode().getElementType();
204+
if(isIfOperand(node)) {
205+
PsiElement rightOperand = ((BinaryExpression) parent).getRightOperand();
206+
String stringValue = PhpElementsUtil.getStringValue(rightOperand);
207+
if(StringUtils.isNotBlank(stringValue)) {
208+
consumer.accept(Pair.create(stringValue, rightOperand));
209+
}
210+
}
211+
}
212+
} else if(parent instanceof ParameterList) {
213+
PsiElement functionCall = parent.getParent();
214+
if(functionCall instanceof FunctionReference && "in_array".equalsIgnoreCase(((FunctionReference) functionCall).getName())) {
215+
PsiElement[] functionParameter = ((ParameterList) parent).getParameters();
216+
if(functionParameter.length > 1 && functionParameter[1] instanceof ArrayCreationExpression) {
217+
//functionParameter[1].getNode().findChildByType()
218+
PsiElement[] psiElements = PsiTreeUtil.collectElements(functionParameter[1], psiElement -> psiElement.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE);
219+
for (PsiElement psiElement : psiElements) {
220+
PsiElement firstChild = psiElement.getFirstChild();
221+
String stringValue = PhpElementsUtil.getStringValue(firstChild);
222+
if(StringUtils.isNotBlank(stringValue)) {
223+
consumer.accept(Pair.create(stringValue, firstChild));
224+
}
225+
}
226+
}
227+
}
228+
} else if(parent instanceof PhpSwitch) {
229+
// case "foobar"
230+
for (PhpCase phpCase : ((PhpSwitch) parent).getAllCases()) {
231+
PhpPsiElement condition = phpCase.getCondition();
232+
String stringValue = PhpElementsUtil.getStringValue(condition);
233+
if(StringUtils.isNotBlank(stringValue)) {
234+
consumer.accept(Pair.create(stringValue, condition));
235+
}
236+
}
237+
}
238+
239+
return false;
240+
}
241+
242+
/**
243+
* null == null, null != null, null === null
244+
*/
245+
private boolean isIfOperand(@NotNull IElementType node) {
246+
return
247+
node == PhpTokenTypes.opIDENTICAL ||
248+
node == PhpTokenTypes.opEQUAL ||
249+
node == PhpTokenTypes.opNOT_EQUAL ||
250+
node == PhpTokenTypes.opNOT_IDENTICAL
251+
;
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)