• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 916

29 Oct 2026 02:41PM UTC coverage: 80.083% (+0.007%) from 80.076%
916

push

github

web-flow
[core] Disallow external entity expansions/DTD in XML parsing (#7261)

- centralized all the factory creators in XmlUtil
- disable DTD parsing by default to prevent XXE attacks

20596 of 26716 branches covered (77.09%)

Branch coverage included in aggregate %.

49 of 54 new or added lines in 11 files covered. (90.74%)

2 existing lines in 2 files now uncovered.

43994 of 53938 relevant lines covered (81.56%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.08
/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/visualforce/ast/ObjectFieldTypes.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.visualforce.ast;
6

7
import java.io.IOException;
8
import java.lang.reflect.Field;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.nio.file.Paths;
12
import java.util.Collections;
13
import java.util.HashMap;
14
import java.util.HashSet;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Set;
19
import java.util.function.Function;
20
import java.util.stream.Collectors;
21
import javax.xml.parsers.DocumentBuilder;
22
import javax.xml.parsers.DocumentBuilderFactory;
23
import javax.xml.parsers.ParserConfigurationException;
24
import javax.xml.xpath.XPath;
25
import javax.xml.xpath.XPathConstants;
26
import javax.xml.xpath.XPathExpression;
27
import javax.xml.xpath.XPathExpressionException;
28
import javax.xml.xpath.XPathFactory;
29

30
import org.apache.commons.lang3.exception.ContextedRuntimeException;
31
import org.slf4j.Logger;
32
import org.slf4j.LoggerFactory;
33
import org.w3c.dom.Document;
34
import org.w3c.dom.Node;
35
import org.w3c.dom.NodeList;
36
import org.xml.sax.SAXException;
37

38
import net.sourceforge.pmd.lang.visualforce.DataType;
39
import net.sourceforge.pmd.util.internal.xml.XmlUtil;
40

41
import com.google.common.reflect.ClassPath;
42

43
/**
44
 * Responsible for storing a mapping of Fields that can be referenced from Visualforce to the type of the field.
45
 *
46
 * <p>SFDX and MDAPI project formats are supported.
47
 * @see <a href="https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm">Salesforce DX Project Structure and Source Format</a>
48
 */
49
class ObjectFieldTypes extends SalesforceFieldTypes {
50
    private static final Logger LOG = LoggerFactory.getLogger(ObjectFieldTypes.class);
1 ✔
51

52
    public static final String CUSTOM_OBJECT_SUFFIX = "__c";
53
    private static final String FIELDS_DIRECTORY = "fields";
54
    private static final String MDAPI_OBJECT_FILE_SUFFIX = ".object";
55
    private static final String SFDX_FIELD_FILE_SUFFIX = ".field-meta.xml";
56

57
    private static final Map<String, DataType> SYSTEM_FIELDS;
58
    private static final Map<String, ClassPath.ClassInfo> SOBJECTS;
59

60
    static {
61
        // see https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/system_fields.htm
62
        SYSTEM_FIELDS = new HashMap<>();
1 ✔
63
        SYSTEM_FIELDS.put("id", DataType.LOOKUP);
1 ✔
64
        SYSTEM_FIELDS.put("isdeleted", DataType.CHECKBOX);
1 ✔
65
        SYSTEM_FIELDS.put("createdbyid", DataType.LOOKUP);
1 ✔
66
        SYSTEM_FIELDS.put("createddate", DataType.DATE_TIME);
1 ✔
67
        SYSTEM_FIELDS.put("lastmodifiedbyid", DataType.LOOKUP);
1 ✔
68
        SYSTEM_FIELDS.put("lastmodifieddate", DataType.DATE_TIME);
1 ✔
69
        SYSTEM_FIELDS.put("systemmodstamp", DataType.DATE_TIME);
1 ✔
70
        // name is not defined as systemfield, but might occur frequently
71
        SYSTEM_FIELDS.put("name", DataType.TEXT);
1 ✔
72

73
        try {
74
            // see https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_list.htm
75
            // and https://github.com/apex-dev-tools/sobject-types
76
            SOBJECTS = Collections.unmodifiableMap(
1 ✔
77
                    ClassPath.from(ClassLoader.getSystemClassLoader())
1 ✔
78
                            .getTopLevelClasses(io.github.apexdevtools.sobjecttypes.Account.class.getPackage().getName())
1 ✔
79
                            .stream()
1 ✔
80
                            .collect(Collectors.toMap(c -> c.getSimpleName().toLowerCase(Locale.ROOT),
1 ✔
81
                                    Function.identity()))
1 ✔
82
            );
83
        } catch (IOException e) {
×
84
            throw new RuntimeException(e);
×
85
        }
1 ✔
86
    }
1 ✔
87

88
    /**
89
     * Keep track of which ".object" files have already been processed. All fields are processed at once. If an object
90
     * file has been processed
91
     */
92
    private final Set<String> objectFileProcessed;
93

94
    // XML Parsing objects
95
    private final DocumentBuilder documentBuilder;
96
    private final XPathExpression customObjectFieldsExpression;
97
    private final XPathExpression customFieldFullNameExpression;
98
    private final XPathExpression customFieldTypeExpression;
99
    private final XPathExpression sfdxCustomFieldFullNameExpression;
100
    private final XPathExpression sfdxCustomFieldTypeExpression;
101

102
    ObjectFieldTypes() {
1 ✔
103
        this.objectFileProcessed = new HashSet<>();
1 ✔
104

105
        try {
106
            DocumentBuilderFactory documentBuilderFactory = XmlUtil.newDocumentBuilderFactory();
1 ✔
107
            documentBuilderFactory.setNamespaceAware(false);
1 ✔
108
            documentBuilderFactory.setValidating(false);
1 ✔
109
            documentBuilderFactory.setIgnoringComments(true);
1 ✔
110
            documentBuilderFactory.setIgnoringElementContentWhitespace(true);
1 ✔
111
            documentBuilderFactory.setCoalescing(false);
1 ✔
112
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
1 ✔
UNCOV
113
        } catch (ParserConfigurationException e) {
×
114
            throw new RuntimeException(e);
×
115
        }
1 ✔
116

117
        try {
118
            XPath xPath = XPathFactory.newInstance().newXPath();
1 ✔
119
            this.customObjectFieldsExpression = xPath.compile("/CustomObject/fields");
1 ✔
120
            this.customFieldFullNameExpression = xPath.compile("fullName/text()");
1 ✔
121
            this.customFieldTypeExpression = xPath.compile("type/text()");
1 ✔
122
            this.sfdxCustomFieldFullNameExpression = xPath.compile("/CustomField/fullName/text()");
1 ✔
123
            this.sfdxCustomFieldTypeExpression = xPath.compile("/CustomField/type/text()");
1 ✔
124
        } catch (XPathExpressionException e) {
×
125
            throw new RuntimeException(e);
×
126
        }
1 ✔
127
    }
1 ✔
128

129
    /**
130
     * Looks in {@code objectsDirectories} for a custom field identified by {@code expression}.
131
     */
132
    @Override
133
    protected void findDataType(String expression, List<Path> objectsDirectories) {
134
        // The expression should be in the form <objectName>.<fieldName>
135
        String[] parts = expression.split("\\.");
1 ✔
136
        if (parts.length == 1) {
1 !
137
            throw new RuntimeException("Malformed identifier: " + expression);
×
138
        } else if (parts.length == 2) {
1 !
139
            String objectName = parts[0];
1 ✔
140
            String fieldName = parts[1];
1 ✔
141

142
            addSystemFields(objectName);
1 ✔
143

144
            // Attempt to find a metadata file that contains the custom field. The information will be located in a
145
            // file located at <objectDirectory>/<objectName>.object or in an file located at
146
            // <objectDirectory>/<objectName>/fields/<fieldName>.field-meta.xml. The list of object directories
147
            // defaults to the [<vfFileName>/../objects] but can be overridden by the user.
148
            for (Path objectsDirectory : objectsDirectories) {
1 ✔
149
                Path sfdxCustomFieldPath = getSfdxCustomFieldPath(objectsDirectory, objectName, fieldName);
1 ✔
150
                if (sfdxCustomFieldPath != null) {
1 ✔
151
                    // SFDX Format
152
                    parseSfdxCustomField(objectName, sfdxCustomFieldPath);
1 ✔
153
                } else {
154
                    // MDAPI Format
155
                    String fileName = objectName + MDAPI_OBJECT_FILE_SUFFIX;
1 ✔
156
                    Path mdapiPath = objectsDirectory.resolve(fileName);
1 ✔
157
                    if (Files.exists(mdapiPath) && Files.isRegularFile(mdapiPath)) {
1 !
158
                        parseMdapiCustomObject(mdapiPath);
1 ✔
159
                    }
160
                }
161

162
                if (containsExpression(expression)) {
1 ✔
163
                    // Break out of the loop if a variable was found
164
                    break;
1 ✔
165
                }
166
            }
1 ✔
167
        } else {
1 ✔
168
            // TODO: Support cross object relationships, these are expressions that contain "__r"
169
            LOG.debug("Expression does not have two parts: {}", expression);
×
170
        }
171
    }
1 ✔
172

173
    /**
174
     * Sfdx projects decompose custom fields into individual files. This method will return the individual file that
175
     * corresponds to &lt;objectName&gt;.&lt;fieldName&gt; if it exists.
176
     *
177
     * <p>Note: these metadata files are created not only for custom fields, but also for standard fields that
178
     * are used within the project. The metadata of these standard fields (fields of standard objects) provide
179
     * less explicit information. E.g. the type is not available the metadata file for these standard fields.
180
     *
181
     * @return path to the metadata file for the Custom Field or null if not found
182
     */
183
    private Path getSfdxCustomFieldPath(Path objectsDirectory, String objectName, String fieldName) {
184
        Path fieldsDirectoryPath = Paths.get(objectsDirectory.toString(), objectName, FIELDS_DIRECTORY);
1 ✔
185
        if (Files.exists(fieldsDirectoryPath) && Files.isDirectory(fieldsDirectoryPath)) {
1 !
186
            Path sfdxFieldPath = Paths.get(fieldsDirectoryPath.toString(), fieldName + SFDX_FIELD_FILE_SUFFIX);
1 ✔
187
            if (Files.exists(sfdxFieldPath) && Files.isRegularFile(sfdxFieldPath)) {
1 !
188
                return sfdxFieldPath;
1 ✔
189
            }
190
        }
191
        return null;
1 ✔
192
    }
193

194
    /**
195
     * Determine the type of the custom field.
196
     */
197
    private void parseSfdxCustomField(String customObjectName, Path sfdxCustomFieldPath) {
198
        try {
199
            Document document = documentBuilder.parse(sfdxCustomFieldPath.toFile());
1 ✔
200
            Node fullNameNode = (Node) sfdxCustomFieldFullNameExpression.evaluate(document, XPathConstants.NODE);
1 ✔
201
            Node typeNode = (Node) sfdxCustomFieldTypeExpression.evaluate(document, XPathConstants.NODE);
1 ✔
202

203
            DataType dataType = null;
1 ✔
204

205
            if (typeNode != null) {
1 ✔
206
                // custom field with a defined type
207
                String type = typeNode.getNodeValue();
1 ✔
208
                dataType = DataType.fromString(type);
1 ✔
209
            } else {
1 ✔
210
                // maybe a field from a standard object - the type is then not explicitly in field-meta.xml provided
211
                ClassPath.ClassInfo classInfo = SOBJECTS.get(customObjectName.toLowerCase(Locale.ROOT));
1 ✔
212
                if (classInfo != null) {
1 !
213
                    Field[] fields = classInfo.load().getFields();
1 ✔
214
                    for (Field f : fields) {
1 !
215
                        if (f.getName().equalsIgnoreCase(fullNameNode.getNodeValue())) {
1 ✔
216
                            dataType = DataType.fromTypeName(f.getType().getSimpleName());
1 ✔
217
                            break;
1 ✔
218
                        }
219
                    }
220
                    if (dataType == null) {
1 !
221
                        LOG.warn("Couldn't determine data type of customObjectName={} from {}", customObjectName, sfdxCustomFieldPath);
×
222
                        dataType = DataType.UNKNOWN;
×
223
                    }
224
                } else {
1 ✔
225
                    LOG.warn("Couldn't determine data type of customObjectName={} - no sobject definition found", customObjectName);
×
226
                    dataType = DataType.UNKNOWN;
×
227
                }
228
            }
229

230
            String key = customObjectName + "." + fullNameNode.getNodeValue();
1 ✔
231
            putDataType(key, dataType);
1 ✔
232
        } catch (IOException | SAXException | XPathExpressionException e) {
×
233
            throw new ContextedRuntimeException(e)
×
234
                    .addContextValue("customObjectName", customObjectName)
×
235
                    .addContextValue("sfdxCustomFieldPath", sfdxCustomFieldPath);
×
236
        }
1 ✔
237
    }
1 ✔
238

239
    /**
240
     * Parse the custom object path and determine the type of all of its custom fields.
241
     */
242
    private void parseMdapiCustomObject(Path mdapiObjectFile) {
243
        String fileName = mdapiObjectFile.getFileName().toString();
1 ✔
244

245
        String customObjectName = fileName.substring(0, fileName.lastIndexOf(MDAPI_OBJECT_FILE_SUFFIX));
1 ✔
246
        if (!objectFileProcessed.contains(customObjectName)) {
1 ✔
247
            try {
248
                Document document = documentBuilder.parse(mdapiObjectFile.toFile());
1 ✔
249
                NodeList fieldsNodes = (NodeList) customObjectFieldsExpression.evaluate(document, XPathConstants.NODESET);
1 ✔
250
                for (int i = 0; i < fieldsNodes.getLength(); i++) {
1 ✔
251
                    Node fieldsNode = fieldsNodes.item(i);
1 ✔
252
                    Node fullNameNode = (Node) customFieldFullNameExpression.evaluate(fieldsNode, XPathConstants.NODE);
1 ✔
253
                    if (fullNameNode == null) {
1 !
254
                        throw new RuntimeException("fullName evaluate failed for " + customObjectName + " " + fieldsNode.getTextContent());
×
255
                    }
256
                    String name = fullNameNode.getNodeValue();
1 ✔
257
                    if (endsWithIgnoreCase(name, CUSTOM_OBJECT_SUFFIX)) {
1 ✔
258
                        Node typeNode = (Node) customFieldTypeExpression.evaluate(fieldsNode, XPathConstants.NODE);
1 ✔
259
                        if (typeNode == null) {
1 !
260
                            throw new RuntimeException("type evaluate failed for object=" + customObjectName + ", field=" + name + " " + fieldsNode.getTextContent());
×
261
                        }
262
                        String type = typeNode.getNodeValue();
1 ✔
263
                        DataType dataType = DataType.fromString(type);
1 ✔
264
                        String key = customObjectName + "." + fullNameNode.getNodeValue();
1 ✔
265
                        putDataType(key, dataType);
1 ✔
266
                    }
267
                }
268
            } catch (IOException | SAXException | XPathExpressionException e) {
×
269
                throw new ContextedRuntimeException(e)
×
270
                        .addContextValue("customObjectName", customObjectName)
×
271
                        .addContextValue("mdapiObjectFile", mdapiObjectFile);
×
272
            }
1 ✔
273
            objectFileProcessed.add(customObjectName);
1 ✔
274
        }
275
    }
1 ✔
276

277
    /**
278
     * Add the set of system fields which aren't present in the metadata file, but may be referenced from the
279
     * visualforce page.
280
     *
281
     * @see <a href="https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/system_fields.htm">Overview of Salesforce Objects and Fields / System Fields</a>
282
     */
283
    private void addSystemFields(String customObjectName) {
284
        for (Map.Entry<String, DataType> entry : SYSTEM_FIELDS.entrySet()) {
1 ✔
285
            putDataType(customObjectName + "." + entry.getKey(), entry.getValue());
1 ✔
286
        }
1 ✔
287
    }
1 ✔
288

289
    /**
290
     * Null safe endsWithIgnoreCase
291
     */
292
    private boolean endsWithIgnoreCase(String str, String suffix) {
293
        return str != null && str.toLowerCase(Locale.ROOT).endsWith(suffix.toLowerCase(Locale.ROOT));
1 !
294
    }
295

296
    @Override
297
    protected DataType putDataType(String name, DataType dataType) {
298
        DataType previousType = super.putDataType(name, dataType);
1 ✔
299
        if (previousType != null && previousType != dataType) {
1 !
300
            // It should not be possible to have conflicting types for CustomFields
301
            throw new RuntimeException("Conflicting types for "
×
302
                    + name
303
                    + ". CurrentType="
304
                    + dataType
305
                    + ", PreviousType="
306
                    + previousType);
307
        }
308
        return previousType;
1 ✔
309
    }
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc