package fr.adrienbrault.idea.symfony2plugin.security.utils;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.yaml.YAMLFileType;
import org.jetbrains.yaml.YAMLUtil;
import org.jetbrains.yaml.psi.*;
import org.jetbrains.yaml.psi.impl.YAMLHashImpl;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class VoterUtil {
public static void visitAttribute(@NotNull Project project, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
for (PhpClass phpClass : PhpIndex.getInstance(project).getAllSubclasses("Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter")) {
Method supports = phpClass.findMethodByName("supports");
if(supports != null) {
visitAttribute(supports, consumer);
}
Method voteOnAttribute = phpClass.findMethodByName("voteOnAttribute");
if(voteOnAttribute != null) {
visitAttribute(voteOnAttribute, consumer);
}
}
for (PhpClass phpClass : PhpIndex.getInstance(project).getAllSubclasses("Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface")) {
Method vote = phpClass.findMethodByName("vote");
if(vote != null) {
visitAttributeForeach(vote, consumer);
}
}
for (PsiFile psiFile : FilenameIndex.getFilesByName(project, "security.yml", GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), YAMLFileType.YML))) {
if(!(psiFile instanceof YAMLFile)) {
continue;
}
YAMLKeyValue roleHierarchy = YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, "security", "role_hierarchy");
if(roleHierarchy != null) {
YAMLValue value = roleHierarchy.getValue();
if(!(value instanceof YAMLMapping)) {
continue;
}
for (YAMLPsiElement yamlPsiElement : value.getYAMLElements()) {
if(!(yamlPsiElement instanceof YAMLKeyValue)) {
continue;
}
String keyText = ((YAMLKeyValue) yamlPsiElement).getKeyText();
if(StringUtils.isNotBlank(keyText)) {
consumer.accept(Pair.create(keyText, yamlPsiElement));
}
YAMLValue yamlValue = ((YAMLKeyValue) yamlPsiElement).getValue();
if(yamlValue instanceof YAMLSequence) {
for (String item : YamlHelper.getYamlArrayValuesAsString((YAMLSequence) yamlValue)) {
consumer.accept(Pair.create(item, yamlValue));
}
}
}
}
YAMLKeyValue accessControl = YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, "security", "access_control");
if(accessControl != null) {
YAMLValue value = accessControl.getValue();
if(!(value instanceof YAMLSequence)) {
continue;
}
for (YAMLPsiElement yamlPsiElement : value.getYAMLElements()) {
if(!(yamlPsiElement instanceof YAMLSequenceItem)) {
continue;
}
YAMLValue value1 = ((YAMLSequenceItem) yamlPsiElement).getValue();
if(!(value1 instanceof YAMLMapping)) {
continue;
}
YAMLKeyValue roles = ((YAMLHashImpl) value1).getKeyValueByKey("roles");
if(roles == null) {
continue;
}
YAMLValue value2 = roles.getValue();
if(value2 instanceof YAMLScalar) {
// roles: FOOBAR
String textValue = ((YAMLScalar) value2).getTextValue();
if(StringUtils.isNotBlank(textValue)) {
consumer.accept(Pair.create(textValue, value2));
}
} else if(value2 instanceof YAMLSequence) {
// roles: [FOOBAR, FOOBAR_1]
for (String item : YamlHelper.getYamlArrayValuesAsString((YAMLSequence) value2)) {
consumer.accept(Pair.create(item, value2));
}
}
}
}
}
}
private static void visitAttributeForeach(@NotNull Method method, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
Parameter[] parameters = method.getParameters();
if(parameters.length < 3) {
return;
}
for (Variable variable : PhpElementsUtil.getVariablesInScope(method, parameters[2])) {
// foreach ($attributes as $attribute)
PsiElement psiElement = PsiTreeUtil.nextVisibleLeaf(variable);
if(psiElement != null && psiElement.getNode().getElementType() == PhpTokenTypes.kwAS) {
PsiElement parent = variable.getParent();
if(!(parent instanceof ForeachStatement)) {
continue;
}
PhpPsiElement variableDecl = variable.getNextPsiSibling();
if(variableDecl instanceof Variable) {
for (Variable variable1 : PhpElementsUtil.getVariablesInScope(parent, (Variable) variableDecl)) {
visitVariable(variable1, consumer);
}
}
}
// in_array('foobar', $attributes)
PsiElement parameterList = variable.getParent();
if(parameterList instanceof ParameterList && PsiElementUtils.getParameterIndexValue(variable) == 1) {
PsiElement functionCall = parameterList.getParent();
if(functionCall instanceof FunctionReference && "in_array".equalsIgnoreCase(((FunctionReference) functionCall).getName())) {
PsiElement[] functionParameter = ((ParameterList) parameterList).getParameters();
if(functionParameter.length > 0) {
String stringValue = PhpElementsUtil.getStringValue(functionParameter[0]);
if(stringValue != null && StringUtils.isNotBlank(stringValue)) {
consumer.accept(Pair.create(stringValue, functionParameter[0]));
}
}
}
}
}
}
private static void visitAttribute(@NotNull Method method, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
Parameter[] parameters = method.getParameters();
if(parameters.length == 0) {
return;
}
for (Variable variable : PhpElementsUtil.getVariablesInScope(method, parameters[0])) {
visitVariable(variable, consumer);
}
}
public static class StringPairConsumer implements Consumer<Pair<String, PsiElement>> {
private Set<String> values = new HashSet<>();
@Override
public void accept(Pair<String, PsiElement> pair) {
values.add(pair.getFirst());
}
@NotNull
public Set<String> getValues() {
return values;
}
}
public static class TargetPairConsumer implements Consumer<Pair<String, PsiElement>> {
@NotNull
private final String filterValue;
@NotNull
private Set<PsiElement> values = new HashSet<>();
public TargetPairConsumer(@NotNull String filterValue) {
this.filterValue = filterValue;
}
@Override
public void accept(Pair<String, PsiElement> pair) {
if(pair.getFirst().equalsIgnoreCase(filterValue)) {
values.add(pair.getSecond());
}
}
@NotNull
public Set<PsiElement> getValues() {
return values;
}
}
/**
* Find security roles on Voter implementation and security roles in Yaml
*/
private static void visitVariable(@NotNull Variable resolve, @NotNull Consumer<Pair<String, PsiElement>> consumer) {
PsiElement parent = resolve.getParent();
if(parent instanceof BinaryExpression) {
// 'VALUE' == $var
PsiElement rightElement = PsiTreeUtil.prevVisibleLeaf(resolve);
if(rightElement != null) {
IElementType node = rightElement.getNode().getElementType();
if(isIfOperand(node)) {
PsiElement leftOperand = ((BinaryExpression) parent).getLeftOperand();
String stringValue = PhpElementsUtil.getStringValue(leftOperand);
if(StringUtils.isNotBlank(stringValue)) {
consumer.accept(Pair.create(stringValue, leftOperand));
}
}
}
// $var == 'VALUE'
PsiElement leftElement = PsiTreeUtil.nextVisibleLeaf(resolve);
if(leftElement != null) {
IElementType node = leftElement.getNode().getElementType();
if(isIfOperand(node)) {
PsiElement rightOperand = ((BinaryExpression) parent).getRightOperand();
String stringValue = PhpElementsUtil.getStringValue(rightOperand);
if(StringUtils.isNotBlank(stringValue)) {
consumer.accept(Pair.create(stringValue, rightOperand));
}
}
}
} else if(parent instanceof ParameterList && PsiElementUtils.getParameterIndexValue(resolve) == 0) {
// in_array($caret, X)
PsiElement functionCall = parent.getParent();
if(functionCall instanceof FunctionReference && "in_array".equalsIgnoreCase(((FunctionReference) functionCall).getName())) {
PsiElement[] functionParameter = ((ParameterList) parent).getParameters();
if(functionParameter.length > 1) {
if(functionParameter[1] instanceof ArrayCreationExpression) {
// in_array($x, ['FOOBAR'])
PsiElement[] psiElements = PsiTreeUtil.collectElements(functionParameter[1], psiElement -> psiElement.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE);
for (PsiElement psiElement : psiElements) {
PsiElement firstChild = psiElement.getFirstChild();
String stringValue = PhpElementsUtil.getStringValue(firstChild);
if(StringUtils.isNotBlank(stringValue)) {
consumer.accept(Pair.create(stringValue, firstChild));
}
}
} else if(functionParameter[1] instanceof MemberReference) {
// in_array($attribute, self::FOO);
// in_array($attribute, $this->foo);
PsiElement PsiReference = ((PsiReference) functionParameter[1]).resolve();
if(PsiReference instanceof Field) {
PsiElement defaultValue = ((Field) PsiReference).getDefaultValue();
if(defaultValue instanceof ArrayCreationExpression) {
for (String s : PhpElementsUtil.getArrayValuesAsString((ArrayCreationExpression) defaultValue)) {
consumer.accept(Pair.create(s, defaultValue));
}
}
}
}
}
}
} else if(parent instanceof PhpSwitch) {
// case "foobar":
for (PhpCase phpCase : ((PhpSwitch) parent).getAllCases()) {
PhpPsiElement condition = phpCase.getCondition();
String stringValue = PhpElementsUtil.getStringValue(condition);
if(StringUtils.isNotBlank(stringValue)) {
consumer.accept(Pair.create(stringValue, condition));
}
}
}
}
/**
* null == null, null != null, null === null
*/
private static boolean isIfOperand(@NotNull IElementType node) {
return
node == PhpTokenTypes.opIDENTICAL ||
node == PhpTokenTypes.opEQUAL ||
node == PhpTokenTypes.opNOT_EQUAL ||
node == PhpTokenTypes.opNOT_IDENTICAL
;
}
}