Provide type information to Visualforce rules
Addresses the general issue raised in https://github.com/pmd/pmd/issues/1092 This commit removes false positives from expressions in apex tags. The specific use case raised in 1092 isn't reproducible and represents a false negative that will be fixed separately. The existing Visualforce rules don't have any information about the data types referenced in the Visualforce page. This results in false positives when attempting to identify expressions that are vulnerable to XSS attacks. The rules should not warn about XSS attacks when the expression refers to a type such as Integer or Boolean. The VfExpressionTypeVisitor visits the Visualforce page and extracts the datatypes from Salesforce metadata. Data type information can come from either Apex classes or Object Fields. The Salesforce metadata is generally located in a sibling directory of the Visualforce directory. By default the code looks in directories relative to the Visualforce file to find the metadata. The conventional locations for the metadata are "../classes" and "../objects", the user can override this default with other directories if required.
This commit is contained in:
parent
47d3e1b9f9
commit
ddf55c7f81
@ -77,6 +77,12 @@
|
||||
<artifactId>pmd-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-apex</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import net.sourceforge.pmd.lang.LanguageRegistry;
|
||||
import net.sourceforge.pmd.lang.LanguageVersion;
|
||||
import net.sourceforge.pmd.lang.Parser;
|
||||
import net.sourceforge.pmd.lang.ParserOptions;
|
||||
import net.sourceforge.pmd.lang.apex.ApexLanguageModule;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
|
||||
import apex.jorje.semantic.symbol.type.BasicType;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Responsible for storing a mapping of Apex Class properties that can be referenced from Visualforce to the type of the
|
||||
* property.
|
||||
*/
|
||||
class ApexClassPropertyTypes {
|
||||
private static final Logger LOGGER = Logger.getLogger(ApexClassPropertyTypes.class.getName());
|
||||
private static final String APEX_CLASS_FILE_SUFFIX = ".cls";
|
||||
|
||||
private final ConcurrentHashMap<String, ExpressionType> variableNameToVariableType;
|
||||
private final Set<String> variableNameProcessed;
|
||||
|
||||
ApexClassPropertyTypes() {
|
||||
this.variableNameToVariableType = new ConcurrentHashMap<>();
|
||||
this.variableNameProcessed = Sets.newConcurrentHashSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks in {@code apexDirectories} for an Apex property identified by {@code expression}.
|
||||
*
|
||||
* @return the ExpressionType for the property represented by {@code expression} or null if not found.
|
||||
*/
|
||||
public ExpressionType getVariableType(String expression, String vfFileName, List<String> apexDirectories) {
|
||||
String lowerExpression = expression.toLowerCase(Locale.ROOT);
|
||||
if (variableNameToVariableType.containsKey(lowerExpression)) {
|
||||
// The expression has been previously retrieved
|
||||
return variableNameToVariableType.get(lowerExpression);
|
||||
} else if (variableNameProcessed.contains(lowerExpression)) {
|
||||
// The expression has been previously requested, but was not found
|
||||
return null;
|
||||
} else {
|
||||
String[] parts = expression.split("\\.");
|
||||
if (parts.length >= 2) {
|
||||
// Load the class and parse it
|
||||
String className = parts[0];
|
||||
|
||||
Path vfFilePath = Paths.get(vfFileName);
|
||||
for (String apexDirectory : apexDirectories) {
|
||||
Path candidateDirectory;
|
||||
if (Paths.get(apexDirectory).isAbsolute()) {
|
||||
candidateDirectory = Paths.get(apexDirectory);
|
||||
} else {
|
||||
candidateDirectory = vfFilePath.getParent().resolve(apexDirectory);
|
||||
}
|
||||
|
||||
Path apexFilePath = candidateDirectory.resolve(className + APEX_CLASS_FILE_SUFFIX);
|
||||
if (Files.exists(apexFilePath) && Files.isRegularFile(apexFilePath)) {
|
||||
Parser parser = getApexParser();
|
||||
try (BufferedReader reader = Files.newBufferedReader(apexFilePath, StandardCharsets.UTF_8)) {
|
||||
Node node = parser.parse(apexFilePath.toString(), reader);
|
||||
ApexClassPropertyTypesVisitor visitor = new ApexClassPropertyTypesVisitor();
|
||||
visitor.visit((ApexNode<?>) node, null);
|
||||
for (Pair<String, BasicType> variable : visitor.getVariables()) {
|
||||
setVariableType(variable.getKey(), variable.getValue());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (variableNameToVariableType.containsKey(lowerExpression)) {
|
||||
// Break out of the loop if a variable was found
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
variableNameProcessed.add(lowerExpression);
|
||||
} else {
|
||||
throw new RuntimeException("Malformed expression: " + expression);
|
||||
}
|
||||
}
|
||||
|
||||
return variableNameToVariableType.get(lowerExpression);
|
||||
}
|
||||
|
||||
private void setVariableType(String name, BasicType basicType) {
|
||||
ExpressionType expressionType = ExpressionType.fromBasicType(basicType);
|
||||
ExpressionType previousType = variableNameToVariableType.put(name.toLowerCase(Locale.ROOT), expressionType);
|
||||
if (previousType != null && !previousType.equals(expressionType)) {
|
||||
// It is possible to have a property and method with different types that appear the same to this code. An
|
||||
// example is an Apex class with a property "public String Foo {get; set;}" and a method of
|
||||
// "Integer getFoo() { return 1; }". In this case set the value as Unknown because we can't be sure which it
|
||||
// is. This code could be more complex in an attempt to determine if all the types are safe from escaping,
|
||||
// but we will allow a false positive in order to let the user know that the code could be refactored to be
|
||||
// more clear.
|
||||
variableNameToVariableType.put(name.toLowerCase(Locale.ROOT), ExpressionType.Unknown);
|
||||
LOGGER.warning("Conflicting types for "
|
||||
+ name
|
||||
+ ". CurrentType="
|
||||
+ expressionType
|
||||
+ ", PreviousType="
|
||||
+ previousType);
|
||||
}
|
||||
}
|
||||
|
||||
private Parser getApexParser() {
|
||||
LanguageVersion languageVersion = LanguageRegistry.getLanguage(ApexLanguageModule.NAME).getDefaultVersion();
|
||||
ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions();
|
||||
return languageVersion.getLanguageVersionHandler().getParser(parserOptions);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTUserClass;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter;
|
||||
|
||||
import apex.jorje.semantic.symbol.member.method.Generated;
|
||||
import apex.jorje.semantic.symbol.member.method.MethodInfo;
|
||||
import apex.jorje.semantic.symbol.type.BasicType;
|
||||
|
||||
/**
|
||||
* Visits an Apex class to determine a mapping of referenceable expressions to expression type.
|
||||
*/
|
||||
final class ApexClassPropertyTypesVisitor extends ApexParserVisitorAdapter {
|
||||
|
||||
/**
|
||||
* Prefix for standard bean type getters, i.e. getFoo
|
||||
*/
|
||||
private static final String BEAN_GETTER_PREFIX = "get";
|
||||
/**
|
||||
* This is the prefix assigned to automatic get/set properties such as String myProp { get; set; }
|
||||
*/
|
||||
private static final String PROPERTY_PREFIX_ACCESSOR = "__sfdc_";
|
||||
|
||||
private static final String RETURN_TYPE_VOID = "void";
|
||||
|
||||
/**
|
||||
* Pairs of (variableName, expressionType)
|
||||
*/
|
||||
private final List<Pair<String, BasicType>> variables;
|
||||
|
||||
ApexClassPropertyTypesVisitor() {
|
||||
this.variables = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<Pair<String, BasicType>> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the return type of the method in {@link #variables} if the method is referenceable from a
|
||||
* Visualforce page.
|
||||
*/
|
||||
@Override
|
||||
public Object visit(ASTMethod node, Object data) {
|
||||
MethodInfo mi = node.getNode().getMethodInfo();
|
||||
if (mi.getParameterTypes().isEmpty()
|
||||
&& isVisibleToVisualForce(node)
|
||||
&& !RETURN_TYPE_VOID.equalsIgnoreCase(mi.getReturnType().getApexName())
|
||||
&& (mi.getGenerated().equals(Generated.USER) || mi.isPropertyAccessor())) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<ASTUserClass> parents = node.getParentsOfType(ASTUserClass.class);
|
||||
Collections.reverse(parents);
|
||||
for (ASTUserClass parent : parents) {
|
||||
sb.append(parent.getImage()).append(".");
|
||||
}
|
||||
String name = node.getImage();
|
||||
for (String prefix : new String[]{BEAN_GETTER_PREFIX, PROPERTY_PREFIX_ACCESSOR}) {
|
||||
if (name.startsWith(prefix)) {
|
||||
name = name.substring(prefix.length());
|
||||
}
|
||||
}
|
||||
sb.append(name);
|
||||
|
||||
variables.add(Pair.of(sb.toString(), mi.getReturnType().getBasicType()));
|
||||
}
|
||||
return super.visit((ApexNode<?>) node, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to filter out methods that aren't visible to the Visualforce page.
|
||||
*
|
||||
* @return true if the method is visible to Visualforce.
|
||||
*/
|
||||
private boolean isVisibleToVisualForce(ASTMethod node) {
|
||||
ASTModifierNode modifier = node.getFirstChildOfType(ASTModifierNode.class);
|
||||
return modifier.isGlobal() | modifier.isPublic();
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import apex.jorje.semantic.symbol.type.BasicType;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Represents all data types that can be referenced from a Visualforce page. This enum consolidates the data types
|
||||
* available to CustomFields and Apex. It uses the naming convention of CustomFields.
|
||||
*
|
||||
* See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_field_types.htm#meta_type_fieldtype
|
||||
*/
|
||||
public enum ExpressionType {
|
||||
AutoNumber(false),
|
||||
Checkbox(false, BasicType.BOOLEAN),
|
||||
Currency(false, BasicType.CURRENCY),
|
||||
Date(false, BasicType.DATE),
|
||||
DateTime(false, BasicType.DATE_TIME),
|
||||
Email(false),
|
||||
EncryptedText(true),
|
||||
ExternalLookup(true),
|
||||
File(false),
|
||||
Hierarchy(false),
|
||||
Html(true),
|
||||
IndirectLookup(false),
|
||||
Location(false),
|
||||
LongTextArea(true),
|
||||
Lookup(false, BasicType.ID),
|
||||
MasterDetail(false),
|
||||
MetadataRelationship(false),
|
||||
MultiselectPicklist(true),
|
||||
Note(true),
|
||||
Number(false, BasicType.DECIMAL, BasicType.DOUBLE, BasicType.INTEGER, BasicType.LONG),
|
||||
Percent(false),
|
||||
Phone(false),
|
||||
Picklist(true),
|
||||
Summary(false),
|
||||
Text(true, BasicType.STRING),
|
||||
TextArea(true),
|
||||
Time(false, BasicType.TIME),
|
||||
Url(false),
|
||||
Unknown(true);
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ExpressionType.class.getName());
|
||||
|
||||
|
||||
/**
|
||||
* True if this field is an XSS risk
|
||||
*/
|
||||
public final boolean requiresEscaping;
|
||||
|
||||
/**
|
||||
* The set of {@link BasicType}s that map to this type. Multiple types can map to a single instance of this enum.
|
||||
*/
|
||||
private final Set<BasicType> basicTypes;
|
||||
|
||||
/**
|
||||
* A case insensitive map of the enum name to its instance. The case metadata is not guaranteed to have the correct
|
||||
* case.
|
||||
*/
|
||||
private static final Map<String, ExpressionType> CASE_INSENSITIVE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Map of BasicType to ExpressionType. Multiple BasicTypes may map to one ExrpessionType.
|
||||
*/
|
||||
private static final Map<BasicType, ExpressionType> BASIC_TYPE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
for (ExpressionType expressionType : ExpressionType.values()) {
|
||||
CASE_INSENSITIVE_MAP.put(expressionType.name().toLowerCase(Locale.ROOT), expressionType);
|
||||
for (BasicType basicType : expressionType.basicTypes) {
|
||||
BASIC_TYPE_MAP.put(basicType, expressionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to correct instance, returns {@code Unknown} if the value can't be mapped.
|
||||
*/
|
||||
public static ExpressionType fromString(String value) {
|
||||
value = Strings.nullToEmpty(value);
|
||||
ExpressionType expressionType = CASE_INSENSITIVE_MAP.get(value.toLowerCase(Locale.ROOT));
|
||||
|
||||
if (expressionType == null) {
|
||||
expressionType = ExpressionType.Unknown;
|
||||
LOGGER.fine("Unable to determine ExpressionType of " + value);
|
||||
}
|
||||
|
||||
return expressionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to correct instance, returns {@code Unknown} if the value can't be mapped.
|
||||
*/
|
||||
public static ExpressionType fromBasicType(BasicType value) {
|
||||
ExpressionType expressionType = value != null ? BASIC_TYPE_MAP.get(value) : null;
|
||||
|
||||
if (expressionType == null) {
|
||||
expressionType = ExpressionType.Unknown;
|
||||
LOGGER.fine("Unable to determine ExpressionType of " + value);
|
||||
}
|
||||
|
||||
return expressionType;
|
||||
}
|
||||
|
||||
ExpressionType(boolean requiresEscaping) {
|
||||
this(requiresEscaping, null);
|
||||
}
|
||||
|
||||
ExpressionType(boolean requiresEscaping, BasicType...basicTypes) {
|
||||
this.requiresEscaping = requiresEscaping;
|
||||
this.basicTypes = Sets.newConcurrentHashSet();
|
||||
if (basicTypes != null) {
|
||||
this.basicTypes.addAll(Arrays.asList(basicTypes));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Responsible for storing a mapping of Fields that can be referenced from Visualforce to the type of the field.
|
||||
*/
|
||||
class ObjectFieldTypes {
|
||||
private static final Logger LOGGER = Logger.getLogger(ObjectFieldTypes.class.getName());
|
||||
|
||||
public static final String CUSTOM_OBJECT_SUFFIX = "__c";
|
||||
private static final String FIELDS_DIRECTORY = "fields";
|
||||
private static final String MDAPI_OBJECT_FILE_SUFFIX = ".object";
|
||||
private static final String SFDX_FIELD_FILE_SUFFIX = ".field-meta.xml";
|
||||
|
||||
private static final ImmutableMap<String, ExpressionType> STANDARD_FIELD_TYPES =
|
||||
ImmutableMap.<String, ExpressionType>builder()
|
||||
.put("createdbyid", ExpressionType.Lookup)
|
||||
.put("createddate", ExpressionType.DateTime)
|
||||
.put("id", ExpressionType.Lookup)
|
||||
.put("isdeleted", ExpressionType.Checkbox)
|
||||
.put("lastmodifiedbyid", ExpressionType.Lookup)
|
||||
.put("lastmodifieddate", ExpressionType.DateTime)
|
||||
.put("systemmodstamp", ExpressionType.DateTime)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Cache of lowercase variable names to the variable type declared in the field's metadata file.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, ExpressionType> variableNameToVariableType;
|
||||
|
||||
/**
|
||||
* Keep track of which variables were already processed. Avoid processing if a page repeatedly asks for an entry
|
||||
* which we haven't previously found.
|
||||
*/
|
||||
private final Set<String> variableNameProcessed;
|
||||
|
||||
/**
|
||||
* Keep track of which ".object" files have already been processed. All fields are processed at once. If an object
|
||||
* file has been processed
|
||||
*/
|
||||
private final Set<String> objectFileProcessed;
|
||||
|
||||
// XML Parsing objects
|
||||
private final DocumentBuilder documentBuilder;
|
||||
private final XPathExpression customObjectFieldsExpression;
|
||||
private final XPathExpression customFieldFullNameExpression;
|
||||
private final XPathExpression customFieldTypeExpression;
|
||||
private final XPathExpression sfdxCustomFieldFullNameExpression;
|
||||
private final XPathExpression sfdxCustomFieldTypeExpression;
|
||||
|
||||
ObjectFieldTypes() {
|
||||
this.variableNameToVariableType = new ConcurrentHashMap<>();
|
||||
this.variableNameProcessed = Sets.newConcurrentHashSet();
|
||||
this.objectFileProcessed = Sets.newConcurrentHashSet();
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
documentBuilderFactory.setNamespaceAware(false);
|
||||
documentBuilderFactory.setValidating(false);
|
||||
documentBuilderFactory.setIgnoringComments(true);
|
||||
documentBuilderFactory.setIgnoringElementContentWhitespace(true);
|
||||
documentBuilderFactory.setExpandEntityReferences(false);
|
||||
documentBuilderFactory.setCoalescing(false);
|
||||
documentBuilderFactory.setXIncludeAware(false);
|
||||
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
this.customObjectFieldsExpression = xPath.compile("/CustomObject/fields");
|
||||
this.customFieldFullNameExpression = xPath.compile("fullName/text()");
|
||||
this.customFieldTypeExpression = xPath.compile("type/text()");
|
||||
this.sfdxCustomFieldFullNameExpression = xPath.compile("/CustomField/fullName/text()");
|
||||
this.sfdxCustomFieldTypeExpression = xPath.compile("/CustomField/type/text()");
|
||||
} catch (XPathExpressionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks in {@code objectsDirectories} for a custom field identified by {@code expression}.
|
||||
*
|
||||
* @return the ExpressionType for the field represented by {@code expression} or null the custom field isn't found.
|
||||
*/
|
||||
public ExpressionType getVariableType(String expression, String vfFileName, List<String> objectsDirectories) {
|
||||
String lowerExpression = expression.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (variableNameToVariableType.containsKey(lowerExpression)) {
|
||||
// The expression has been previously retrieved
|
||||
return variableNameToVariableType.get(lowerExpression);
|
||||
} else if (variableNameProcessed.contains(lowerExpression)) {
|
||||
// The expression has been previously requested, but was not found
|
||||
return null;
|
||||
} else {
|
||||
// The expression should be in the form <objectName>.<fieldName>
|
||||
String[] parts = expression.split("\\.");
|
||||
if (parts.length == 1) {
|
||||
throw new RuntimeException("Malformed identifier: " + expression);
|
||||
} else if (parts.length == 2) {
|
||||
String objectName = parts[0];
|
||||
String fieldName = parts[1];
|
||||
|
||||
addStandardFields(objectName);
|
||||
|
||||
// Attempt to find a metadata file that contains the custom field. The information will be located in a
|
||||
// file located at <objectDirectory>/<objectName>.object or in an file located at
|
||||
// <objectDirectory>/<objectName>/fields/<fieldName>.field-meta.xml. The list of object directories
|
||||
// defaults to the [<vfFileName>/../objects] but can be overridden by the user.
|
||||
Path vfFilePath = Paths.get(vfFileName);
|
||||
for (String objectsDirectory : objectsDirectories) {
|
||||
Path candidateDirectory;
|
||||
if (Paths.get(objectsDirectory).isAbsolute()) {
|
||||
candidateDirectory = Paths.get(objectsDirectory);
|
||||
} else {
|
||||
candidateDirectory = vfFilePath.getParent().resolve(objectsDirectory);
|
||||
}
|
||||
|
||||
Path sfdxCustomFieldPath = getSfdxCustomFieldPath(candidateDirectory, objectName, fieldName);
|
||||
if (sfdxCustomFieldPath != null) {
|
||||
// SFDX Format
|
||||
parseSfdxCustomField(objectName, sfdxCustomFieldPath);
|
||||
} else {
|
||||
// MDAPI Format
|
||||
String fileName = objectName + MDAPI_OBJECT_FILE_SUFFIX;
|
||||
Path mdapiPath = candidateDirectory.resolve(fileName);
|
||||
if (Files.exists(mdapiPath) && Files.isRegularFile(mdapiPath)) {
|
||||
parseMdapiCustomObject(mdapiPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (variableNameToVariableType.containsKey(lowerExpression)) {
|
||||
// Break out of the loop if a variable was found
|
||||
break;
|
||||
}
|
||||
}
|
||||
variableNameProcessed.add(lowerExpression);
|
||||
} else {
|
||||
// TODO: Support cross object relationships, these are expressions that contain "__r"
|
||||
LOGGER.fine("Expression does not have two parts: " + expression);
|
||||
}
|
||||
}
|
||||
|
||||
return variableNameToVariableType.get(lowerExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sfdx projects decompose custom fields into individual files. This method will return the individual file that
|
||||
* corresponds to <objectName>.<fieldName> if it exists.
|
||||
*
|
||||
* @return path to the metadata file for the Custom Field or null if not found
|
||||
*/
|
||||
private Path getSfdxCustomFieldPath(Path objectsDirectory, String objectName, String fieldName) {
|
||||
Path fieldsDirectoryPath = Paths.get(objectsDirectory.toString(), objectName, FIELDS_DIRECTORY);
|
||||
if (Files.exists(fieldsDirectoryPath) && Files.isDirectory(fieldsDirectoryPath)) {
|
||||
Path sfdxFieldPath = Paths.get(fieldsDirectoryPath.toString(), fieldName + SFDX_FIELD_FILE_SUFFIX);
|
||||
if (Files.exists(sfdxFieldPath) && Files.isRegularFile(sfdxFieldPath)) {
|
||||
return sfdxFieldPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the type of the custom field.
|
||||
*/
|
||||
private void parseSfdxCustomField(String customObjectName, Path sfdxCustomFieldPath) {
|
||||
try {
|
||||
Document document = documentBuilder.parse(sfdxCustomFieldPath.toFile());
|
||||
Node fullNameNode = (Node) sfdxCustomFieldFullNameExpression.evaluate(document, XPathConstants.NODE);
|
||||
Node typeNode = (Node) sfdxCustomFieldTypeExpression.evaluate(document, XPathConstants.NODE);
|
||||
String type = typeNode.getNodeValue();
|
||||
ExpressionType expressionType = ExpressionType.fromString(type);
|
||||
|
||||
String key = customObjectName + "." + fullNameNode.getNodeValue();
|
||||
setVariableType(key, expressionType);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the custom object path and determine the type of all of its custom fields.
|
||||
*/
|
||||
private void parseMdapiCustomObject(Path mdapiObjectFile) {
|
||||
String fileName = mdapiObjectFile.getFileName().toString();
|
||||
|
||||
String customObjectName = fileName.substring(0, fileName.lastIndexOf(MDAPI_OBJECT_FILE_SUFFIX));
|
||||
if (!objectFileProcessed.contains(customObjectName)) {
|
||||
try {
|
||||
Document document = documentBuilder.parse(mdapiObjectFile.toFile());
|
||||
NodeList fieldsNodes = (NodeList) customObjectFieldsExpression.evaluate(document, XPathConstants.NODESET);
|
||||
for (int i = 0; i < fieldsNodes.getLength(); i++) {
|
||||
Node fieldsNode = fieldsNodes.item(i);
|
||||
Node fullNameNode = (Node) customFieldFullNameExpression.evaluate(fieldsNode, XPathConstants.NODE);
|
||||
if (fullNameNode == null) {
|
||||
throw new RuntimeException("fullName evaluate failed for " + customObjectName + " " + fieldsNode.getTextContent());
|
||||
}
|
||||
String name = fullNameNode.getNodeValue();
|
||||
if (endsWithIgnoreCase(name, CUSTOM_OBJECT_SUFFIX)) {
|
||||
Node typeNode = (Node) customFieldTypeExpression.evaluate(fieldsNode, XPathConstants.NODE);
|
||||
if (typeNode == null) {
|
||||
throw new RuntimeException("type evaluate failed for object=" + customObjectName + ", field=" + name + " " + fieldsNode.getTextContent());
|
||||
}
|
||||
String type = typeNode.getNodeValue();
|
||||
ExpressionType expressionType = ExpressionType.fromString(type);
|
||||
String key = customObjectName + "." + fullNameNode.getNodeValue();
|
||||
setVariableType(key, expressionType);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
objectFileProcessed.add(customObjectName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the set of standard fields which aren't present in the metadata file, but may be refernced from the
|
||||
* visualforce page.
|
||||
*/
|
||||
private void addStandardFields(String customObjectName) {
|
||||
for (Map.Entry<String, ExpressionType> entry : STANDARD_FIELD_TYPES.entrySet()) {
|
||||
setVariableType(customObjectName + "." + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Null safe endsWithIgnoreCase
|
||||
*/
|
||||
private boolean endsWithIgnoreCase(String str, String suffix) {
|
||||
return str != null && str.toLowerCase(Locale.ROOT).endsWith(suffix.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private void setVariableType(String name, ExpressionType expressionType) {
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
ExpressionType previousType = variableNameToVariableType.put(name, expressionType);
|
||||
if (previousType != null && !previousType.equals(expressionType)) {
|
||||
// It should not be possible ot have conflicting types for CustomFields
|
||||
throw new RuntimeException("Conflicting types for "
|
||||
+ name
|
||||
+ ". CurrentType="
|
||||
+ expressionType
|
||||
+ ", PreviousType="
|
||||
+ previousType);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTAttribute;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTAttributeValue;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTDotExpression;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTElExpression;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTElement;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTExpression;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTIdentifier;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTText;
|
||||
import net.sourceforge.pmd.lang.vf.ast.VfParserVisitorAdapter;
|
||||
|
||||
/**
|
||||
* Visits {@link ASTElExpression} nodes and stores type information for all {@link ASTIdentifier} child nodes.
|
||||
*/
|
||||
public class VfExpressionTypeVisitor extends VfParserVisitorAdapter {
|
||||
private static final Logger LOGGER = Logger.getLogger(VfExpressionTypeVisitor.class.getName());
|
||||
|
||||
private static final String APEX_PAGE = "apex:page";
|
||||
private static final String CONTROLLER_ATTRIBUTE = "controller";
|
||||
private static final String STANDARD_CONTROLLER_ATTRIBUTE = "standardcontroller";
|
||||
private static final String EXTENSIONS_ATTRIBUTE = "extensions";
|
||||
|
||||
private ApexClassPropertyTypes apexClassPropertyTypes;
|
||||
private ObjectFieldTypes objectFieldTypes;
|
||||
private final String fileName;
|
||||
private String standardControllerName;
|
||||
private final IdentityHashMap<ASTIdentifier, ExpressionType> expressionTypes;
|
||||
|
||||
/**
|
||||
* List of all Apex Class names that the VF page might refer to. These values come from either the
|
||||
* {@code controller} or {@code extensions} attribute.
|
||||
*/
|
||||
private final List<String> apexClassNames;
|
||||
private final List<String> apexDirectories;
|
||||
private final List<String> objectsDirectories;
|
||||
|
||||
public VfExpressionTypeVisitor(String fileName, List<String> apexDirectories, List<String> objectsDirectories) {
|
||||
this.fileName = fileName;
|
||||
this.apexDirectories = apexDirectories;
|
||||
this.objectsDirectories = objectsDirectories;
|
||||
this.apexClassPropertyTypes = new ApexClassPropertyTypes();
|
||||
this.objectFieldTypes = new ObjectFieldTypes();
|
||||
this.apexClassNames = new ArrayList<>();
|
||||
this.expressionTypes = new IdentityHashMap<>();
|
||||
}
|
||||
|
||||
public IdentityHashMap<ASTIdentifier, ExpressionType> getExpressionTypes() {
|
||||
return this.expressionTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather names of Controller, Extensions, and StandardController. Each of these may contain the identifier
|
||||
* referenced from the Visualforce page.
|
||||
*/
|
||||
@Override
|
||||
public Object visit(ASTElement node, Object data) {
|
||||
if (APEX_PAGE.equalsIgnoreCase(node.getName())) {
|
||||
List<ASTAttribute> attribs = node.findChildrenOfType(ASTAttribute.class);
|
||||
|
||||
for (ASTAttribute attr : attribs) {
|
||||
String lowerAttr = attr.getName().toLowerCase(Locale.ROOT);
|
||||
if (CONTROLLER_ATTRIBUTE.equals(lowerAttr)) {
|
||||
// Controller Name should always take precedence
|
||||
apexClassNames.add(0, attr.getFirstChildOfType(ASTAttributeValue.class)
|
||||
.getFirstChildOfType(ASTText.class).getImage());
|
||||
break;
|
||||
} else if (STANDARD_CONTROLLER_ATTRIBUTE.equals(lowerAttr)) {
|
||||
standardControllerName = attr.getFirstChildOfType(ASTAttributeValue.class)
|
||||
.getFirstChildOfType(ASTText.class).getImage().toLowerCase(Locale.ROOT);
|
||||
} else if (EXTENSIONS_ATTRIBUTE.equalsIgnoreCase(lowerAttr)) {
|
||||
for (String extension : attr.getFirstChildOfType(ASTAttributeValue.class)
|
||||
.getFirstChildOfType(ASTText.class).getImage().split(",")) {
|
||||
apexClassNames.add(extension.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all {@link ASTIdentifier} child nodes of {@code node} and attempt to resolve their type. The order of
|
||||
* precedence is Controller, Extensions, StandardController.
|
||||
*/
|
||||
@Override
|
||||
public Object visit(ASTElExpression node, Object data) {
|
||||
for (Map.Entry<ASTIdentifier, String> entry : getExpressionIdentifierNames(node).entrySet()) {
|
||||
String name = entry.getValue();
|
||||
ExpressionType type = null;
|
||||
String[] parts = name.split("\\.");
|
||||
|
||||
// Apex extensions take precedence over Standard controllers.
|
||||
// The example below will display "Name From Inner Class" instead of the Account name
|
||||
// public class AccountExtension {
|
||||
// public AccountExtension(ApexPages.StandardController controller) {
|
||||
// }
|
||||
//
|
||||
// public InnerClass getAccount() {
|
||||
// return new InnerClass();
|
||||
// }
|
||||
//
|
||||
// public class InnerClass {
|
||||
// public String getName() {
|
||||
// return 'Name From Inner Class';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//<apex:page standardController="Account" extensions="AccountExtension">
|
||||
// <apex:outputText value="{!Account.Name}" escape="false"/>
|
||||
//</apex:page>
|
||||
|
||||
// Try to find the identifier in an Apex class
|
||||
for (String apexClassName : apexClassNames) {
|
||||
String fullName = apexClassName + "." + name;
|
||||
type = apexClassPropertyTypes.getVariableType(fullName, fileName, apexDirectories);
|
||||
if (type != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the identifier in a CustomField if it wasn't found in an Apex class and the identifier corresponds
|
||||
// to the StandardController.
|
||||
if (type == null) {
|
||||
if (parts.length >= 2 && standardControllerName != null && standardControllerName.equalsIgnoreCase(parts[0])) {
|
||||
type = objectFieldTypes.getVariableType(name, fileName, objectsDirectories);
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
expressionTypes.put(entry.getKey(), type);
|
||||
} else {
|
||||
LOGGER.fine("Unable to determine type for: " + name);
|
||||
}
|
||||
}
|
||||
return super.visit(node, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the expression returning all of the identifiers in that expression mapped to its string represenation.
|
||||
* An {@code ASTElExpression} can contain multiple {@code ASTExpressions} in cases of logical operators.
|
||||
*/
|
||||
private Map<ASTIdentifier, String> getExpressionIdentifierNames(ASTElExpression elExpression) {
|
||||
Map<ASTIdentifier, String> identifierToName = new IdentityHashMap<>();
|
||||
|
||||
for (ASTExpression expression : elExpression.findChildrenOfType(ASTExpression.class)) {
|
||||
for (ASTIdentifier identifier : expression.findChildrenOfType(ASTIdentifier.class)) {
|
||||
StringBuilder sb = new StringBuilder(identifier.getImage());
|
||||
|
||||
for (ASTDotExpression dotExpression : expression.findChildrenOfType(ASTDotExpression.class)) {
|
||||
sb.append(".");
|
||||
|
||||
List<ASTIdentifier> childIdentifiers = dotExpression.findChildrenOfType(ASTIdentifier.class);
|
||||
if (childIdentifiers.isEmpty()) {
|
||||
continue;
|
||||
} else if (childIdentifiers.size() > 1) {
|
||||
// The grammar guarantees tha there should be at most 1 child identifier
|
||||
// <EXP_DOT> (Identifier() | Literal() )
|
||||
throw new RuntimeException("Unexpected number of childIdentifiers: size=" + childIdentifiers.size());
|
||||
}
|
||||
|
||||
ASTIdentifier childIdentifier = childIdentifiers.get(0);
|
||||
sb.append(childIdentifier.getImage());
|
||||
}
|
||||
|
||||
identifierToName.put(identifier, sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return identifierToName;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf.rule.security;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.sourceforge.pmd.RuleContext;
|
||||
import net.sourceforge.pmd.lang.vf.ExpressionType;
|
||||
import net.sourceforge.pmd.lang.vf.VfExpressionTypeVisitor;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTCompilationUnit;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTIdentifier;
|
||||
import net.sourceforge.pmd.lang.vf.rule.AbstractVfRule;
|
||||
import net.sourceforge.pmd.properties.PropertyDescriptor;
|
||||
import net.sourceforge.pmd.properties.PropertyFactory;
|
||||
|
||||
/**
|
||||
* Represents a rule where the {@link net.sourceforge.pmd.lang.vf.ast.ASTIdentifier} nodes are enhanced with the
|
||||
* node's {@link ExpressionType}. This is achieved by processing metadata files referenced by the Visualforce page.
|
||||
*/
|
||||
class AbstractVfTypedElExpressionRule extends AbstractVfRule {
|
||||
/**
|
||||
* Directory that contains Apex classes that may be referenced from a Visualforce page.
|
||||
*/
|
||||
private static final PropertyDescriptor<List<String>> APEX_DIRECTORIES_DESCRIPTOR =
|
||||
PropertyFactory.stringListProperty("apexDirectories")
|
||||
.desc("Location of Apex Class directories. Absolute or relative to the Visualforce directory")
|
||||
.defaultValue(Collections.singletonList(".." + File.separator + "classes"))
|
||||
.delim(',')
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Directory that contains Object definitions that may be referenced from a Visualforce page.
|
||||
*/
|
||||
private static final PropertyDescriptor<List<String>> OBJECTS_DIRECTORIES_DESCRIPTOR =
|
||||
PropertyFactory.stringListProperty("objectsDirectories")
|
||||
.desc("Location of CustomObject directories. Absolute or relative to the Visualforce directory")
|
||||
.defaultValue(Collections.singletonList(".." + File.separator + "objects"))
|
||||
.delim(',')
|
||||
.build();
|
||||
|
||||
private Map<ASTIdentifier, ExpressionType> expressionTypes;
|
||||
|
||||
AbstractVfTypedElExpressionRule() {
|
||||
definePropertyDescriptor(APEX_DIRECTORIES_DESCRIPTOR);
|
||||
definePropertyDescriptor(OBJECTS_DIRECTORIES_DESCRIPTOR);
|
||||
}
|
||||
|
||||
public ExpressionType getExpressionType(ASTIdentifier node) {
|
||||
return expressionTypes.get(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(RuleContext ctx) {
|
||||
this.expressionTypes = Collections.synchronizedMap(new IdentityHashMap<ASTIdentifier, ExpressionType>());
|
||||
super.start(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke {@link VfExpressionTypeVisitor#visit(ASTCompilationUnit, Object)} to identify Visualforce expression's and
|
||||
* their types.
|
||||
*/
|
||||
@Override
|
||||
public Object visit(ASTCompilationUnit node, Object data) {
|
||||
List<String> apexDirectories = getProperty(APEX_DIRECTORIES_DESCRIPTOR);
|
||||
List<String> objectsDirectories = getProperty(OBJECTS_DIRECTORIES_DESCRIPTOR);
|
||||
|
||||
// The visitor will only find information if there are directories to look in. This allows users to disable the
|
||||
// visitor in the unlikely scenario that they want to.
|
||||
if (!apexDirectories.isEmpty() || !objectsDirectories.isEmpty()) {
|
||||
RuleContext ctx = (RuleContext) data;
|
||||
File file = ctx.getSourceCodeFile();
|
||||
if (file != null) {
|
||||
VfExpressionTypeVisitor visitor = new VfExpressionTypeVisitor(file.getAbsolutePath(),
|
||||
apexDirectories,
|
||||
objectsDirectories);
|
||||
visitor.visit(node, data);
|
||||
this.expressionTypes.putAll(visitor.getExpressionTypes());
|
||||
}
|
||||
}
|
||||
|
||||
return super.visit(node, data);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
import net.sourceforge.pmd.lang.vf.ExpressionType;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTArguments;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTAttribute;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTContent;
|
||||
@ -25,13 +26,12 @@ import net.sourceforge.pmd.lang.vf.ast.ASTLiteral;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTNegationExpression;
|
||||
import net.sourceforge.pmd.lang.vf.ast.ASTText;
|
||||
import net.sourceforge.pmd.lang.vf.ast.AbstractVFNode;
|
||||
import net.sourceforge.pmd.lang.vf.rule.AbstractVfRule;
|
||||
|
||||
/**
|
||||
* @author sergey.gorbaty February 2017
|
||||
*
|
||||
*/
|
||||
public class VfUnescapeElRule extends AbstractVfRule {
|
||||
public class VfUnescapeElRule extends AbstractVfTypedElExpressionRule {
|
||||
private static final String A_CONST = "a";
|
||||
private static final String APEXIFRAME_CONST = "apex:iframe";
|
||||
private static final String IFRAME_CONST = "iframe";
|
||||
@ -53,7 +53,6 @@ public class VfUnescapeElRule extends AbstractVfRule {
|
||||
@Override
|
||||
public Object visit(ASTHtmlScript node, Object data) {
|
||||
checkIfCorrectlyEscaped(node, data);
|
||||
|
||||
return super.visit(node, data);
|
||||
}
|
||||
|
||||
@ -412,6 +411,11 @@ public class VfUnescapeElRule extends AbstractVfRule {
|
||||
final List<ASTIdentifier> ids = expr.findChildrenOfType(ASTIdentifier.class);
|
||||
|
||||
for (final ASTIdentifier id : ids) {
|
||||
ExpressionType expressionType = getExpressionType(id);
|
||||
if (expressionType != null && !expressionType.requiresEscaping) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isEscaped = false;
|
||||
|
||||
for (Escaping e : escapes) {
|
||||
|
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ApexClassPropertyTypesTest {
|
||||
@Test
|
||||
public void testApexClassIsProperlyParsed() {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "ApexController.page"))
|
||||
.toAbsolutePath();
|
||||
String vfFileName = vfPagePath.toString();
|
||||
|
||||
// Intentionally use the wrong case for property names to ensure that they can be found. The Apex class name
|
||||
// must have the correct case since it is used to lookup the file. The Apex class name is guaranteed to be correct
|
||||
// in the Visualforce page, but the property names are not
|
||||
ApexClassPropertyTypes apexClassPropertyTypes = new ApexClassPropertyTypes();
|
||||
assertEquals(ExpressionType.Lookup,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.accOuntIdProp", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertEquals(ExpressionType.Lookup,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.AcCountId", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertEquals(ExpressionType.Text,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.AcCountname", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
|
||||
// InnerController
|
||||
assertEquals("The class should be parsed to Unknown. It's not a valid expression on its own.",
|
||||
ExpressionType.Unknown,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.innErController", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertEquals(ExpressionType.Lookup,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.innErController.innErAccountIdProp",
|
||||
vfFileName, VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertEquals(ExpressionType.Lookup,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.innErController.innErAccountid",
|
||||
vfFileName, VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertEquals(ExpressionType.Text,
|
||||
apexClassPropertyTypes.getVariableType("ApexController.innErController.innErAccountnAme",
|
||||
vfFileName, VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
|
||||
assertNull("Invalid class should return null",
|
||||
apexClassPropertyTypes.getVariableType("unknownclass.invalidProperty", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
assertNull("Invalid class property should return null",
|
||||
apexClassPropertyTypes.getVariableType("ApexController.invalidProperty", vfFileName,
|
||||
VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
}
|
||||
|
||||
/**
|
||||
* It is possible to have a property and method with different types that resolve to the same Visualforce
|
||||
* expression. An example is an Apex class with a property "public String Foo {get; set;}" and a method of
|
||||
* "Integer getFoo() { return 1; }". These properties should map to {@link ExpressionType#Unknown}.
|
||||
*/
|
||||
@Test
|
||||
public void testConflictingPropertyTypesMapsToUnknown() {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "ApexController.page"))
|
||||
.toAbsolutePath();
|
||||
String vfFileName = vfPagePath.toString();
|
||||
ApexClassPropertyTypes apexClassPropertyTypes = new ApexClassPropertyTypes();
|
||||
assertEquals(ExpressionType.Unknown,
|
||||
apexClassPropertyTypes.getVariableType("ApexWithConflictingPropertyTypes.ConflictingProp",
|
||||
vfFileName, VFTestContstants.RELATIVE_APEX_DIRECTORIES));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidDirectoryDoesNotCauseAnException() {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "ApexController.page"))
|
||||
.toAbsolutePath();
|
||||
String vfFileName = vfPagePath.toString();
|
||||
|
||||
List<String> paths = Arrays.asList(Paths.get("..", "classes-does-not-exist").toString());
|
||||
ApexClassPropertyTypes apexClassPropertyTypes = new ApexClassPropertyTypes();
|
||||
assertNull(apexClassPropertyTypes.getVariableType("ApexController.accOuntIdProp", vfFileName, paths));
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.Test;
|
||||
|
||||
import net.sourceforge.pmd.lang.LanguageRegistry;
|
||||
import net.sourceforge.pmd.lang.LanguageVersion;
|
||||
import net.sourceforge.pmd.lang.Parser;
|
||||
import net.sourceforge.pmd.lang.ParserOptions;
|
||||
import net.sourceforge.pmd.lang.apex.ApexLanguageModule;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
|
||||
import apex.jorje.semantic.symbol.type.BasicType;
|
||||
|
||||
public class ApexClassPropertyTypesVisitorTest {
|
||||
@Test
|
||||
public void testApexClassIsProperlyParsed() throws IOException {
|
||||
LanguageVersion languageVersion = LanguageRegistry.getLanguage(ApexLanguageModule.NAME).getDefaultVersion();
|
||||
ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions();
|
||||
Parser parser = languageVersion.getLanguageVersionHandler().getParser(parserOptions);
|
||||
|
||||
Path apexPath = VFTestContstants.SFDX_PATH.resolve(Paths.get("classes", "ApexController.cls")).toAbsolutePath();
|
||||
ApexClassPropertyTypesVisitor visitor = new ApexClassPropertyTypesVisitor();
|
||||
try (BufferedReader reader = Files.newBufferedReader(apexPath, StandardCharsets.UTF_8)) {
|
||||
Node node = parser.parse(apexPath.toString(), reader);
|
||||
assertNotNull(node);
|
||||
visitor.visit((ApexNode<?>) node, null);
|
||||
}
|
||||
|
||||
List<Pair<String, BasicType>> variables = visitor.getVariables();
|
||||
assertEquals(7, variables.size());
|
||||
Map<String, BasicType> variableNameToVariableType = new Hashtable<>();
|
||||
for (Pair<String, BasicType> variable : variables) {
|
||||
// Map the values and ensure there were no duplicates
|
||||
BasicType previous = variableNameToVariableType.put(variable.getKey(), variable.getValue());
|
||||
assertNull(variable.getKey(), previous);
|
||||
}
|
||||
|
||||
assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.AccountIdProp"));
|
||||
assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.AccountId"));
|
||||
assertEquals(BasicType.STRING, variableNameToVariableType.get("ApexController.AccountName"));
|
||||
assertEquals(BasicType.APEX_OBJECT, variableNameToVariableType.get("ApexController.InnerController"));
|
||||
assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.InnerController.InnerAccountIdProp"));
|
||||
assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.InnerController.InnerAccountId"));
|
||||
assertEquals(BasicType.STRING, variableNameToVariableType.get("ApexController.InnerController.InnerAccountName"));
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import apex.jorje.semantic.symbol.type.BasicType;
|
||||
|
||||
public class ExpressionTypeTest {
|
||||
@Test
|
||||
public void testFromString() {
|
||||
assertEquals(ExpressionType.AutoNumber, ExpressionType.fromString("AutoNumber"));
|
||||
assertEquals(ExpressionType.AutoNumber, ExpressionType.fromString("autonumber"));
|
||||
assertEquals(ExpressionType.Unknown, ExpressionType.fromString(""));
|
||||
assertEquals(ExpressionType.Unknown, ExpressionType.fromString(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromBasicType() {
|
||||
assertEquals(ExpressionType.Checkbox, ExpressionType.fromBasicType(BasicType.BOOLEAN));
|
||||
assertEquals(ExpressionType.Number, ExpressionType.fromBasicType(BasicType.DECIMAL));
|
||||
assertEquals(ExpressionType.Number, ExpressionType.fromBasicType(BasicType.DOUBLE));
|
||||
assertEquals(ExpressionType.Unknown, ExpressionType.fromBasicType(BasicType.APEX_OBJECT));
|
||||
assertEquals(ExpressionType.Unknown, ExpressionType.fromBasicType(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiresEncoding() {
|
||||
assertFalse(ExpressionType.AutoNumber.requiresEscaping);
|
||||
assertTrue(ExpressionType.Text.requiresEscaping);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ObjectFieldTypesTest {
|
||||
|
||||
/**
|
||||
* Verify that CustomFields stored in sfdx project format are correctly parsed
|
||||
*/
|
||||
@Test
|
||||
public void testSfdxAccountIsProperlyParsed() {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "StandarAccountPage.page"))
|
||||
.toAbsolutePath();
|
||||
|
||||
ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes();
|
||||
validateSfdxAccount(objectFieldTypes, vfPagePath, VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that CustomFields stored in mdapi format are correctly parsed
|
||||
*/
|
||||
@Test
|
||||
public void testMdapiAccountIsProperlyParsed() {
|
||||
Path vfPagePath = VFTestContstants.MDAPI_PATH.resolve(Paths.get("pages", "StandarAccountPage.page"))
|
||||
.toAbsolutePath();
|
||||
|
||||
ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes();
|
||||
validateMDAPIAccount(objectFieldTypes, vfPagePath, VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that fields are found across multiple directories
|
||||
*/
|
||||
@Test
|
||||
public void testFieldsAreFoundInMultipleDirectories() {
|
||||
ObjectFieldTypes objectFieldTypes;
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "StandarAccountPage.page"))
|
||||
.toAbsolutePath();
|
||||
|
||||
List<String> paths = Arrays.asList(VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES.get(0),
|
||||
VFTestContstants.ABSOLUTE_MDAPI_OBJECTS_DIRECTORIES.get(0));
|
||||
objectFieldTypes = new ObjectFieldTypes();
|
||||
validateSfdxAccount(objectFieldTypes, vfPagePath, paths);
|
||||
validateMDAPIAccount(objectFieldTypes, vfPagePath, paths);
|
||||
|
||||
Collections.reverse(paths);
|
||||
objectFieldTypes = new ObjectFieldTypes();
|
||||
validateSfdxAccount(objectFieldTypes, vfPagePath, paths);
|
||||
validateMDAPIAccount(objectFieldTypes, vfPagePath, paths);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidDirectoryDoesNotCauseAnException() {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "StandarAccountPage.page"))
|
||||
.toAbsolutePath();
|
||||
String vfFileName = vfPagePath.toString();
|
||||
|
||||
List<String> paths = Arrays.asList(Paths.get("..", "objects-does-not-exist").toString());
|
||||
ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes();
|
||||
assertNull(objectFieldTypes.getVariableType("Account.DoesNotExist__c", vfFileName, paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the expected results when the Account Fields are stored in decomposed sfdx format
|
||||
*/
|
||||
private void validateSfdxAccount(ObjectFieldTypes objectFieldTypes, Path vfPagePath, List<String> paths) {
|
||||
String vfFileName = vfPagePath.toString();
|
||||
|
||||
assertEquals(ExpressionType.Checkbox,
|
||||
objectFieldTypes.getVariableType("Account.Checkbox__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.DateTime,
|
||||
objectFieldTypes.getVariableType("Account.DateTime__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.LongTextArea,
|
||||
objectFieldTypes.getVariableType("Account.LongTextArea__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.Picklist,
|
||||
objectFieldTypes.getVariableType("Account.Picklist__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.Text,
|
||||
objectFieldTypes.getVariableType("Account.Text__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.TextArea,
|
||||
objectFieldTypes.getVariableType("Account.TextArea__c", vfFileName, paths));
|
||||
assertNull(objectFieldTypes.getVariableType("Account.DoesNotExist__c", vfFileName, paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the expected results when the Account Fields are stored in a single file MDAPI format
|
||||
*/
|
||||
private void validateMDAPIAccount(ObjectFieldTypes objectFieldTypes, Path vfPagePath, List<String> paths) {
|
||||
String vfFileName = vfPagePath.toString();
|
||||
|
||||
assertEquals(ExpressionType.Checkbox,
|
||||
objectFieldTypes.getVariableType("Account.MDCheckbox__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.DateTime,
|
||||
objectFieldTypes.getVariableType("Account.MDDateTime__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.LongTextArea,
|
||||
objectFieldTypes.getVariableType("Account.MDLongTextArea__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.Picklist,
|
||||
objectFieldTypes.getVariableType("Account.MDPicklist__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.Text,
|
||||
objectFieldTypes.getVariableType("Account.MDText__c", vfFileName, paths));
|
||||
assertEquals(ExpressionType.TextArea,
|
||||
objectFieldTypes.getVariableType("Account.MDTextArea__c", vfFileName, paths));
|
||||
assertNull(objectFieldTypes.getVariableType("Account.DoesNotExist__c", vfFileName, paths));
|
||||
}
|
||||
}
|
@ -5,7 +5,13 @@
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import net.sourceforge.pmd.AbstractRuleSetFactoryTest;
|
||||
import net.sourceforge.pmd.lang.apex.rule.ApexXPathRule;
|
||||
|
||||
public class RuleSetFactoryTest extends AbstractRuleSetFactoryTest {
|
||||
// no additional tests
|
||||
public RuleSetFactoryTest() {
|
||||
super();
|
||||
// Copied from net.sourceforge.pmd.lang.apex.RuleSetFactoryTest
|
||||
// Apex rules are found in the classpath because this module has a dependency on pmd-apex
|
||||
validXPathClassNames.add(ApexXPathRule.class.getName());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.vf;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class VFTestContstants {
|
||||
private static final Path ROOT_PATH = Paths.get("src", "test", "resources", "net", "sourceforge",
|
||||
"pmd", "lang", "vf").toAbsolutePath();
|
||||
|
||||
public static final Path SFDX_PATH = ROOT_PATH.resolve("metadata-sfdx");
|
||||
|
||||
public static final Path MDAPI_PATH = ROOT_PATH.resolve("metadata-mdapi");
|
||||
public static final List<String> ABSOLUTE_MDAPI_OBJECTS_DIRECTORIES =
|
||||
Collections.singletonList(MDAPI_PATH.resolve("objects").toAbsolutePath().toString());
|
||||
|
||||
public static final List<String> RELATIVE_APEX_DIRECTORIES = Collections.singletonList(".." + File.separator + "classes");
|
||||
public static final List<String> RELATIVE_OBJECTS_DIRECTORIES = Collections.singletonList(".." + File.separator + "objects");
|
||||
}
|
@ -4,8 +4,162 @@
|
||||
|
||||
package net.sourceforge.pmd.lang.vf.rule.security;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import net.sourceforge.pmd.PMD;
|
||||
import net.sourceforge.pmd.PMDException;
|
||||
import net.sourceforge.pmd.Report;
|
||||
import net.sourceforge.pmd.Rule;
|
||||
import net.sourceforge.pmd.RuleContext;
|
||||
import net.sourceforge.pmd.RuleSet;
|
||||
import net.sourceforge.pmd.RuleSets;
|
||||
import net.sourceforge.pmd.RuleViolation;
|
||||
import net.sourceforge.pmd.RulesetsFactoryUtils;
|
||||
import net.sourceforge.pmd.lang.LanguageRegistry;
|
||||
import net.sourceforge.pmd.lang.LanguageVersion;
|
||||
import net.sourceforge.pmd.lang.Parser;
|
||||
import net.sourceforge.pmd.lang.ParserOptions;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
import net.sourceforge.pmd.lang.vf.VFTestContstants;
|
||||
import net.sourceforge.pmd.lang.vf.VfLanguageModule;
|
||||
import net.sourceforge.pmd.properties.PropertyDescriptor;
|
||||
import net.sourceforge.pmd.testframework.PmdRuleTst;
|
||||
|
||||
public class VfUnescapeElTest extends PmdRuleTst {
|
||||
// no additional unit tests
|
||||
public static final String EXPECTED_RULE_MESSAGE = "Avoid unescaped user controlled content in EL";
|
||||
|
||||
/**
|
||||
* Verify that CustomFields stored in sfdx project format are correctly parsed
|
||||
*/
|
||||
@Test
|
||||
public void testSfdxCustomFields() throws IOException, PMDException {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "StandardAccount.page")).toAbsolutePath();
|
||||
|
||||
Report report = runRule(vfPagePath, VFTestContstants.RELATIVE_APEX_DIRECTORIES,
|
||||
VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
List<RuleViolation> ruleViolations = report.getViolations();
|
||||
assertEquals(6, ruleViolations.size());
|
||||
int firstLineWithErrors = 7;
|
||||
for (int i = 0; i < ruleViolations.size(); i++) {
|
||||
RuleViolation ruleViolation = ruleViolations.get(i);
|
||||
assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription());
|
||||
assertEquals(firstLineWithErrors + i, ruleViolation.getBeginLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that CustomFields stored in mdapi format are correctly parsed
|
||||
*/
|
||||
@Test
|
||||
public void testMdapiCustomFields() throws IOException, PMDException {
|
||||
Path vfPagePath = VFTestContstants.MDAPI_PATH.resolve(Paths.get("pages", "StandardAccount.page")).toAbsolutePath();
|
||||
|
||||
Report report = runRule(vfPagePath, VFTestContstants.RELATIVE_APEX_DIRECTORIES,
|
||||
VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
List<RuleViolation> ruleViolations = report.getViolations();
|
||||
assertEquals(6, ruleViolations.size());
|
||||
int firstLineWithErrors = 8;
|
||||
for (int i = 0; i < ruleViolations.size(); i++) {
|
||||
RuleViolation ruleViolation = ruleViolations.get(i);
|
||||
assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription());
|
||||
assertEquals(firstLineWithErrors + i, ruleViolation.getBeginLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a page with a single Apex controller
|
||||
*/
|
||||
@Test
|
||||
public void testApexController() throws IOException, PMDException {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "ApexController.page")).toAbsolutePath();
|
||||
|
||||
Report report = runRule(vfPagePath, VFTestContstants.RELATIVE_APEX_DIRECTORIES,
|
||||
VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
List<RuleViolation> ruleViolations = report.getViolations();
|
||||
assertEquals(2, ruleViolations.size());
|
||||
int firstLineWithErrors = 9;
|
||||
for (int i = 0; i < ruleViolations.size(); i++) {
|
||||
// There should start at line 9
|
||||
RuleViolation ruleViolation = ruleViolations.get(i);
|
||||
assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription());
|
||||
assertEquals(firstLineWithErrors + i, ruleViolation.getBeginLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a page with a standard controller and two Apex extensions
|
||||
*/
|
||||
@Test
|
||||
public void testExtensions() throws IOException, PMDException {
|
||||
Path vfPagePath = VFTestContstants.SFDX_PATH.resolve(Paths.get("pages", "StandardAccountWithExtensions.page")).toAbsolutePath();
|
||||
|
||||
Report report = runRule(vfPagePath, VFTestContstants.RELATIVE_APEX_DIRECTORIES,
|
||||
VFTestContstants.RELATIVE_OBJECTS_DIRECTORIES);
|
||||
List<RuleViolation> ruleViolations = report.getViolations();
|
||||
assertEquals(8, ruleViolations.size());
|
||||
int firstLineWithErrors = 9;
|
||||
for (int i = 0; i < ruleViolations.size(); i++) {
|
||||
RuleViolation ruleViolation = ruleViolations.get(i);
|
||||
assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription());
|
||||
assertEquals(firstLineWithErrors + i, ruleViolation.getBeginLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a rule against a Visualforce page on the file system. This code is based on
|
||||
* {@link net.sourceforge.pmd.testframework.RuleTst#runTestFromString(String, Rule, Report, LanguageVersion, boolean)}
|
||||
*/
|
||||
private Report runRule(Path vfPagePath, List<String> apexDirectories, List<String> objectsDirectories) throws FileNotFoundException, PMDException {
|
||||
LanguageVersion languageVersion = LanguageRegistry.getLanguage(VfLanguageModule.NAME).getDefaultVersion();
|
||||
ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions();
|
||||
Parser parser = languageVersion.getLanguageVersionHandler().getParser(parserOptions);
|
||||
|
||||
Node node = parser.parse(vfPagePath.toString(), new FileReader(vfPagePath.toFile()));
|
||||
assertNotNull(node);
|
||||
|
||||
// BEGIN Based on RuleTst class
|
||||
PMD p = new PMD();
|
||||
p.getConfiguration().setDefaultLanguageVersion(languageVersion);
|
||||
p.getConfiguration().setIgnoreIncrementalAnalysis(true);
|
||||
// simple class loader, that doesn't delegate to parent.
|
||||
// this allows us in the tests to simulate PMD run without
|
||||
// auxclasspath, not even the classes from the test dependencies
|
||||
// will be found.
|
||||
p.getConfiguration().setClassLoader(new ClassLoader() {
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.startsWith("java.") || name.startsWith("javax.")) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
});
|
||||
|
||||
Rule rule = findRule("category/vf/security.xml", "VfUnescapeEl");
|
||||
PropertyDescriptor apexPropertyDescriptor = rule.getPropertyDescriptor("apexDirectories");
|
||||
PropertyDescriptor objectPropertyDescriptor = rule.getPropertyDescriptor("objectsDirectories");
|
||||
rule.setProperty(apexPropertyDescriptor, apexDirectories);
|
||||
rule.setProperty(objectPropertyDescriptor, objectsDirectories);
|
||||
Report report = new Report();
|
||||
RuleContext ctx = new RuleContext();
|
||||
ctx.setReport(report);
|
||||
ctx.setSourceCodeFile(vfPagePath.toFile());
|
||||
ctx.setLanguageVersion(languageVersion);
|
||||
ctx.setIgnoreExceptions(false);
|
||||
RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule);
|
||||
p.getSourceCodeProcessor().processSourceCode(new FileReader(vfPagePath.toFile()), new RuleSets(rules), ctx);
|
||||
// END Based on RuleTst class
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
||||
<apex:page standardController="Account">
|
||||
<!-- Safe -->
|
||||
<apex:outputText value="{!Account.CreatedDate}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDCheckbox__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDDateTime__c}" escape="false"/>
|
||||
<!-- Extra Line to make the lines different than the SFDX file -->
|
||||
<!-- Unsafe -->
|
||||
<apex:outputText value="{!Account.Name}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDText__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDTextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDLongTextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.MDPicklist__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.UnknownField__c}" escape="false"/> <!-- This field doesn't exist on Account -->
|
||||
</apex:page>
|
@ -0,0 +1,39 @@
|
||||
public class ApexController {
|
||||
public Id AccountIdProp { get; set; }
|
||||
|
||||
public ApexController() {
|
||||
acc = [SELECT Id, Name, Site FROM Account
|
||||
WHERE Id = :ApexPages.currentPage().getParameters().get('id')];
|
||||
this.AccountIdProp = acc.Id;
|
||||
}
|
||||
|
||||
public Id getAccountId() {
|
||||
return acc.id;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return acc.name;
|
||||
}
|
||||
|
||||
public InnerController getInnerController() {
|
||||
return new InnerController(this);
|
||||
}
|
||||
|
||||
public class InnerController {
|
||||
private ApexController parent;
|
||||
public Id InnerAccountIdProp { get; set; }
|
||||
|
||||
public InnerController(ApexController parent) {
|
||||
this.parent = parent;
|
||||
this.InnerAccountIdProp = parent.AccountIdProp;
|
||||
}
|
||||
|
||||
public Id getInnerAccountId() {
|
||||
return 'Inner: ' + parent.acc.id;
|
||||
}
|
||||
|
||||
public String getInnerAccountName() {
|
||||
return 'Inner: ' + parent.acc.name;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
public class ApexExtension1 {
|
||||
public String StringFromExtension1 {get; set;}
|
||||
public Id IdFromExtension1 {get; set;}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
public class ApexExtension2 {
|
||||
public String StringFromExtension2 {get; set;}
|
||||
public Id IdFromExtension2 {get; set;}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
public class ApexWithConflictingPropertyTypes {
|
||||
public String ConflictingProp { get; set; }
|
||||
|
||||
public Integer getConflictingProp() {
|
||||
return '';
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Checkbox__c</fullName>
|
||||
<defaultValue>false</defaultValue>
|
||||
<externalId>false</externalId>
|
||||
<label>A Checkbox</label>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>Checkbox</type>
|
||||
</CustomField>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>DateTime__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>A DateTime</label>
|
||||
<required>false</required>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>DateTime</type>
|
||||
</CustomField>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>LongTextArea__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>A LongTextArea</label>
|
||||
<length>32768</length>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>LongTextArea</type>
|
||||
<visibleLines>3</visibleLines>
|
||||
</CustomField>
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Picklist__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>A Picklist</label>
|
||||
<required>false</required>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>Picklist</type>
|
||||
<valueSet>
|
||||
<valueSetDefinition>
|
||||
<sorted>false</sorted>
|
||||
<value>
|
||||
<fullName>Value1</fullName>
|
||||
<default>false</default>
|
||||
<label>Value2</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Value2</fullName>
|
||||
<default>false</default>
|
||||
<label>Value2</label>
|
||||
</value>
|
||||
</valueSetDefinition>
|
||||
</valueSet>
|
||||
</CustomField>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>TextArea__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>A TextArea</label>
|
||||
<required>false</required>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>TextArea</type>
|
||||
</CustomField>
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Text__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>A Text</label>
|
||||
<length>255</length>
|
||||
<required>false</required>
|
||||
<trackFeedHistory>false</trackFeedHistory>
|
||||
<type>Text</type>
|
||||
<unique>false</unique>
|
||||
</CustomField>
|
@ -0,0 +1,11 @@
|
||||
<apex:page controller="ApexController" tabStyle="Account">
|
||||
<!-- Safe -->
|
||||
<apex:outputText value="{!AccountIdProp}" escape="false" />
|
||||
<apex:outputText value="{!AccountId}" escape="false" />
|
||||
<apex:outputText value="{!InnerController.InnerAccountId}" escape="false" />
|
||||
<apex:outputText value="{!InnerController.InnerAccountIdProp}" escape="false" />
|
||||
|
||||
<!-- Unsafe -->
|
||||
<apex:outputText value="{!AccountName}" escape="false" />
|
||||
<apex:outputText value="{!InnerController.InnerAccountName}" escape="false" />
|
||||
</apex:page>
|
@ -0,0 +1,13 @@
|
||||
<apex:page standardController="Account">
|
||||
<!-- Safe -->
|
||||
<apex:outputText value="{!Account.CreatedDate}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Checkbox__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.DateTime__c}" escape="false"/>
|
||||
<!-- Unsafe -->
|
||||
<apex:outputText value="{!Account.Name}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Text__c}" escape="false"/>
|
||||
<td><apex:outputText value="{!Account.TextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.LongTextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Picklist__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.UnknownField__c}" escape="false"/> <!-- This field doesn't exist on Account -->
|
||||
</apex:page>
|
@ -0,0 +1,17 @@
|
||||
<apex:page standardController="Account" extensions="ApexExtension1,ApexExtension2">
|
||||
<!-- Safe -->
|
||||
<apex:outputText value="{!Account.CreatedDate}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Checkbox__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.DateTime__c}" escape="false"/>
|
||||
<apex:outputText value="{!IdFromExtension1}" escape="false"/>
|
||||
<apex:outputText value="{!IdFromExtension2}" escape="false"/>
|
||||
<!-- Unsafe -->
|
||||
<apex:outputText value="{!Account.Name}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Text__c}" escape="false"/>
|
||||
<td><apex:outputText value="{!Account.TextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.LongTextArea__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.Picklist__c}" escape="false"/>
|
||||
<apex:outputText value="{!Account.UnknownField__c}" escape="false"/> <!-- This field doesn't exist on Account -->
|
||||
<apex:outputText value="{!Account.StringFromExtension1}" escape="false"/>
|
||||
<apex:outputText value="{!Account.StringFromExtension2}" escape="false"/>
|
||||
</apex:page>
|
Loading…
x
Reference in New Issue
Block a user