From b003f82d5cb75354dd92674c703f6122b785ef7e Mon Sep 17 00:00:00 2001 From: w0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Fri, 3 Oct 2025 08:47:39 -0700 Subject: [PATCH 1/6] =?UTF-8?q?openapi:=20generate=20XML=20request=20bodie?= =?UTF-8?q?s,=20wire=20into=20converter,=20add=20tests;=E2=80=A6=20?= =?UTF-8?q?=E2=80=A6=20misc=20UI=20&=20spider=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement DOM-based XML body generation in BodyGenerator Add BodyGenerator.generateXml(MediaType) and generateXml(Schema) Prefer media-type examples; otherwise generate XML from Schema Support common xml.* metadata: xml.name, xml.attribute, xml.wrapped, xml.namespace and xml.prefix Handle primitives, arrays (wrapped / unwrapped), object properties, additionalProperties (serialized as ), BinarySchema placeholder, and basic composed-schema handling (oneOf/anyOf/allOf merge) Emit generator error messages on failures (preserve existing error collection) Wire XML generation into request conversion RequestModelConverter: detect application/xml, text/xml and application/*+xml, prefer exact application/xml and call BodyGenerator.generateXml(...) instead of logging unsupported-content Tests and integration Add/tighten unit tests in BodyGeneratorXmlUnitTest that parse generated XML and assert structure (elements, attributes, namespaces, counts) Update v3 BodyGeneratorUnitTest where necessary Add integration test OpenApiIntegrationXmlTest and test resource openapi_xml_integration.yaml to verify generated XML bodies and removal of the previous unsupported-content message Miscellaneous improvements / cleanup ImportDialog: minor formatting, switch validation to use java.net.URI to avoid deprecated URL-based parsing and avoid deprecated constructors SpiderDialog & UrlCanonicalizer: minor whitespace/formatting fixes and replace deprecated URL(String,...) usage with URI construction where appropriate (avoid deprecated constructors) Small refactors and formatting adjustments across changed files --- .../zap/extension/openapi/ImportDialog.java | 30 +- .../swagger/RequestModelConverter.java | 35 +- .../openapi/generators/BodyGenerator.java | 427 +++++++++++++++++- .../automation/OpenApiIntegrationXmlTest.java | 92 ++++ .../generators/BodyGeneratorXmlUnitTest.java | 266 +++++++++++ .../openapi/v3/BodyGeneratorUnitTest.java | 20 +- .../openapi/v3/openapi_xml_integration.yaml | 34 ++ .../zaproxy/addon/spider/SpiderDialog.java | 34 +- .../addon/spider/UrlCanonicalizer.java | 117 +++-- 9 files changed, 934 insertions(+), 121 deletions(-) create mode 100644 addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/automation/OpenApiIntegrationXmlTest.java create mode 100644 addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/generators/BodyGeneratorXmlUnitTest.java create mode 100644 addOns/openapi/src/test/resources/org/zaproxy/zap/extension/openapi/v3/openapi_xml_integration.yaml diff --git a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java index 808e4b00231..82a6ec5730b 100644 --- a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java +++ b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java @@ -23,9 +23,7 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.io.File; -import java.net.MalformedURLException; import java.net.URISyntaxException; -import java.net.URL; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFileChooser; @@ -36,7 +34,6 @@ import javax.swing.JPopupMenu; import javax.swing.JProgressBar; import javax.swing.JTextField; -import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; @@ -333,16 +330,23 @@ private boolean importDefinition() { } try { - new URL(definitionLocation).toURI(); - var uri = new URI(definitionLocation, true); - return extOpenApi.importOpenApiDefinition( - uri, - getTargetField().getText(), - true, - getSelectedContextId(), - getSelectedUser()) - == null; - } catch (URIException | MalformedURLException | URISyntaxException ignored) { + // Validate the definitionLocation as a URI without using the deprecated + // URL(String) constructor. Parse once with java.net.URI then build the + // apache httpclient URI from the ASCII/escaped form if the URI is absolute. + java.net.URI juri = new java.net.URI(definitionLocation); + if (juri.isAbsolute()) { + org.apache.commons.httpclient.URI uri = + new org.apache.commons.httpclient.URI(juri.toASCIIString(), true); + return extOpenApi.importOpenApiDefinition( + uri, + getTargetField().getText(), + true, + getSelectedContextId(), + getSelectedUser()) + == null; + } + // Not absolute -> treat as a file path (fall through) + } catch (URIException | URISyntaxException ignored) { // Not a valid URI, try to import as a file } catch (InvalidUrlException e) { ThreadUtils.invokeAndWaitHandled( diff --git a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/converter/swagger/RequestModelConverter.java b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/converter/swagger/RequestModelConverter.java index ef8d2c31a43..468b0e30941 100644 --- a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/converter/swagger/RequestModelConverter.java +++ b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/converter/swagger/RequestModelConverter.java @@ -26,7 +26,6 @@ import io.swagger.v3.oas.models.parameters.RequestBody; import java.util.List; import java.util.Map; -import org.parosproxy.paros.Constant; import org.parosproxy.paros.network.HttpHeaderField; import org.zaproxy.zap.extension.openapi.generators.Generators; import org.zaproxy.zap.extension.openapi.generators.HeadersGenerator; @@ -85,12 +84,34 @@ private String generateBody() { return generators.getBodyGenerator().generateMultiPart(schema, encoding); } - if (content.containsKey(CONTENT_APPLICATION_XML)) { - generators.addErrorMessage( - Constant.messages.getString( - "openapi.unsupportedcontent", - operation.getOperationId(), - CONTENT_APPLICATION_XML)); + // handle XML media types (application/xml, text/xml, application/*+xml) + if (content.containsKey(CONTENT_APPLICATION_XML) + || content.containsKey("text/xml") + || content.keySet().stream().anyMatch(k -> k != null && k.contains("+xml"))) { + // prefer exact application/xml entry if present + io.swagger.v3.oas.models.media.MediaType mediaType = null; + if (content.containsKey(CONTENT_APPLICATION_XML)) { + mediaType = content.get(CONTENT_APPLICATION_XML); + } else if (content.containsKey("text/xml")) { + mediaType = content.get("text/xml"); + } else { + // pick the first +xml media type + String key = + content.keySet().stream() + .filter(k -> k != null && k.contains("+xml")) + .findFirst() + .orElse(null); + if (key != null) { + mediaType = content.get(key); + } + } + if (mediaType != null) { + String xml = generators.getBodyGenerator().generateXml(mediaType); + if (xml == null) { + return ""; + } + return xml; + } return ""; } diff --git a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/generators/BodyGenerator.java b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/generators/BodyGenerator.java index 10c4f85a71d..40af380a16a 100644 --- a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/generators/BodyGenerator.java +++ b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/generators/BodyGenerator.java @@ -30,6 +30,7 @@ import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.XML; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -42,6 +43,16 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +// Use fully-qualified org.w3c.dom types to avoid name collisions with local Element class import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -64,7 +75,373 @@ public BodyGenerator(Generators generators) { this.dataGenerator = generators.getDataGenerator(); } - private enum Element { + public String generateXml(MediaType mediaType) { + String exampleBody = extractExampleBody(mediaType); + if (exampleBody != null) { + return exampleBody; + } + if (mediaType == null || mediaType.getSchema() == null) { + return ""; + } + return generateXml(mediaType.getSchema()); + } + + public String generateXml(Schema schema) { + if (schema == null) { + return ""; + } + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = db.newDocument(); + + String rootName = Optional.ofNullable(schema.getName()).orElse("root"); + // If schema has xml.name use it + io.swagger.v3.oas.models.media.XML rootXml = schema.getXml(); + String rootNamespace = null; + if (rootXml != null && rootXml.getName() != null) { + rootName = rootXml.getName(); + } + + // Create root element, honoring namespace/prefix if present + org.w3c.dom.Element root = createElementWithXml(doc, rootName, rootXml, null); + doc.appendChild(root); + + // Track namespace declarations on root when creating descendants + buildElementForSchema(doc, root, schema); + + // transform to string + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + StreamResult result = new StreamResult(new java.io.StringWriter()); + DOMSource source = new DOMSource(doc); + transformer.transform(source, result); + return result.getWriter().toString(); + } catch (ParserConfigurationException | TransformerException e) { + LOGGER.warn("Failed to generate XML body: {}", e.getMessage()); + if (this.generators != null) { + this.generators.addErrorMessage("Failed to generate XML body: " + e.getMessage()); + } + // Return null to indicate failure; callers will handle it + return null; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void buildElementForSchema( + org.w3c.dom.Document doc, org.w3c.dom.Element parent, Schema schema) { + if (schema == null) { + return; + } + // Handle composed schemas (oneOf/anyOf/allOf) by resolving/merging where + // possible + if (schema instanceof ComposedSchema) { + ComposedSchema cs = (ComposedSchema) schema; + // Prefer oneOf/anyOf resolution (pick first), otherwise try to merge allOf + // components + if (cs.getOneOf() != null && !cs.getOneOf().isEmpty()) { + buildElementForSchema(doc, parent, cs.getOneOf().get(0)); + return; + } else if (cs.getAnyOf() != null && !cs.getAnyOf().isEmpty()) { + buildElementForSchema(doc, parent, cs.getAnyOf().get(0)); + return; + } else if (cs.getAllOf() != null && !cs.getAllOf().isEmpty()) { + // Merge properties from allOf into a temporary map and continue as object + Map merged = new HashMap<>(); + Schema additional = null; + for (Schema s : cs.getAllOf()) { + if (s.getProperties() != null) { + merged.putAll(s.getProperties()); + } + if (s.getAdditionalProperties() instanceof Schema) { + additional = (Schema) s.getAdditionalProperties(); + } + } + // process merged properties + for (Map.Entry property : merged.entrySet()) { + String propName = property.getKey(); + Schema propSchema = property.getValue(); + XML propXml = propSchema.getXml(); + if (propXml != null + && propXml.getAttribute() != null + && propXml.getAttribute()) { + String value = dataGenerator.generateBodyValue(propName, propSchema); + if (value != null + && value.length() >= 2 + && value.startsWith("\"") + && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + String attrName = propXml.getName() != null ? propXml.getName() : propName; + if (propXml.getNamespace() != null) { + String prefix = propXml.getPrefix(); + String qname = (prefix != null ? prefix + ":" + attrName : attrName); + parent.setAttributeNS( + propXml.getNamespace(), qname, value == null ? "" : value); + ensureNamespaceDeclarationOnRoot(parent, propXml); + } else { + parent.setAttribute(attrName, value == null ? "" : value); + } + } else { + String childName = propName; + if (propXml != null && propXml.getName() != null) { + childName = propXml.getName(); + } + org.w3c.dom.Element child = + createElementWithXml(doc, childName, propXml, parent); + parent.appendChild(child); + buildElementForSchema(doc, child, propSchema); + } + } + if (additional != null) { + // generate two entries for additionalProperties map + for (int i = 0; i < 2; i++) { + org.w3c.dom.Element entry = doc.createElement("entry"); + parent.appendChild(entry); + org.w3c.dom.Element key = doc.createElement("key"); + key.setTextContent("k" + i); + entry.appendChild(key); + org.w3c.dom.Element value = doc.createElement("value"); + entry.appendChild(value); + buildElementForSchema(doc, value, additional); + } + } + return; + } + } + // Binary schema handling + if (schema instanceof BinarySchema) { + String content = generateFromBinarySchema((BinarySchema) schema, false); + parent.setTextContent(content == null ? "" : content); + return; + } + + // Primitive types (non-array, non-object) + if (!(schema instanceof ArraySchema) && !(schema instanceof ObjectSchema)) { + String value = dataGenerator.generateBodyValue(parent.getNodeName(), schema); + // strip surrounding quotes if present + if (value != null + && value.length() >= 2 + && value.startsWith("\"") + && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + parent.setTextContent(value == null ? "" : value); + return; + } + + if (schema instanceof ArraySchema) { + Schema items = ((ArraySchema) schema).getItems(); + if (items == null) { + return; + } + // Determine item element name + String itemName = Optional.ofNullable(items.getName()).orElse(parent.getNodeName()); + XML xml = items.getXml(); + if (xml != null && xml.getName() != null) { + itemName = xml.getName(); + } + // produce two items to mirror JSON array behaviour + // If this array is configured as not wrapped (xml.wrapped == false) then + // append item elements to the parent of 'parent' instead of using the + // container represented by 'parent'. This handles cases where callers + // created a property element for the array but the XML schema expects + // repeated item elements without a wrapper. + boolean wrapped = true; + XML parentXml = schema.getXml(); + if (parentXml != null && parentXml.getWrapped() != null) { + wrapped = parentXml.getWrapped(); + } + + org.w3c.dom.Node insertionParent = parent; + if (!wrapped && parent.getParentNode() instanceof org.w3c.dom.Element) { + // use the parent's parent as the insertion point and remove the container + // element + insertionParent = parent.getParentNode(); + insertionParent.removeChild(parent); + } + + for (int i = 0; i < 2; i++) { + org.w3c.dom.Element itemEl = createElementWithXml(doc, itemName, xml, parent); + insertionParent.appendChild(itemEl); + buildElementForSchema(doc, itemEl, items); + } + return; + } + + // ObjectSchema or schema with properties + Map properties = schema.getProperties(); + if (properties != null) { + for (Map.Entry property : properties.entrySet()) { + String propName = property.getKey(); + Schema propSchema = property.getValue(); + XML propXml = propSchema.getXml(); + if (propXml != null && propXml.getAttribute() != null && propXml.getAttribute()) { + // attribute on parent + String value = dataGenerator.generateBodyValue(propName, propSchema); + if (value != null + && value.length() >= 2 + && value.startsWith("\"") + && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + String attrName = propXml.getName() != null ? propXml.getName() : propName; + // If attribute has namespace/prefix, use setAttributeNS + if (propXml.getNamespace() != null) { + String prefix = propXml.getPrefix(); + String qname = (prefix != null ? prefix + ":" + attrName : attrName); + parent.setAttributeNS( + propXml.getNamespace(), qname, value == null ? "" : value); + // ensure xmlns declaration exists on root + ensureNamespaceDeclarationOnRoot(parent, propXml); + } else { + parent.setAttribute(attrName, value == null ? "" : value); + } + } else { + String childName = propName; + if (propXml != null && propXml.getName() != null) { + childName = propXml.getName(); + } + // If this property is an array and is marked as unwrapped, we + // should not create the property container element and instead + // append the item elements directly under the current parent. + if (propSchema instanceof ArraySchema + && propXml != null + && propXml.getWrapped() != null + && !propXml.getWrapped()) { + ArraySchema arr = (ArraySchema) propSchema; + Schema items = arr.getItems(); + if (items != null) { + String itemName = + Optional.ofNullable(items.getName()).orElse(childName); + XML itemXml = items.getXml(); + if (itemXml != null && itemXml.getName() != null) { + itemName = itemXml.getName(); + } + // append two item elements directly under parent + for (int i = 0; i < 2; i++) { + org.w3c.dom.Element itemEl = + createElementWithXml(doc, itemName, itemXml, parent); + parent.appendChild(itemEl); + buildElementForSchema(doc, itemEl, items); + } + continue; + } + } + + org.w3c.dom.Element child = + createElementWithXml(doc, childName, propXml, parent); + parent.appendChild(child); + buildElementForSchema(doc, child, propSchema); + } + } + // handle additionalProperties if present (map values) + if (schema.getAdditionalProperties() instanceof Schema) { + Schema add = (Schema) schema.getAdditionalProperties(); + for (int i = 0; i < 2; i++) { + org.w3c.dom.Element entry = doc.createElement("entry"); + parent.appendChild(entry); + org.w3c.dom.Element key = doc.createElement("key"); + key.setTextContent("k" + i); + entry.appendChild(key); + org.w3c.dom.Element value = doc.createElement("value"); + entry.appendChild(value); + buildElementForSchema(doc, value, add); + } + } + } else if (schema.getAdditionalProperties() instanceof Schema) { + // No named properties, but additionalProperties defines the value type -> emit + // map entries + Schema add = (Schema) schema.getAdditionalProperties(); + for (int i = 0; i < 2; i++) { + org.w3c.dom.Element entry = doc.createElement("entry"); + parent.appendChild(entry); + org.w3c.dom.Element key = doc.createElement("key"); + key.setTextContent("k" + i); + entry.appendChild(key); + org.w3c.dom.Element value = doc.createElement("value"); + entry.appendChild(value); + buildElementForSchema(doc, value, add); + } + } + } + + /** + * Create an element honoring the given XML metadata (namespace and prefix) and ensure any + * namespace declarations are added to the root element. + * + * @param doc the document + * @param name the local name for the element + * @param xml the XML metadata (may be null) + * @param contextParent a nearby element whose root will receive namespace declarations (may be + * null) + * @return the created Element + */ + private org.w3c.dom.Element createElementWithXml( + org.w3c.dom.Document doc, String name, XML xml, org.w3c.dom.Element contextParent) { + if (xml != null && xml.getNamespace() != null) { + String ns = xml.getNamespace(); + String prefix = xml.getPrefix(); + String qname = (prefix != null) ? (prefix + ":" + name) : name; + org.w3c.dom.Element el = doc.createElementNS(ns, qname); + // ensure namespace declaration on root + ensureNamespaceDeclarationOnRoot((contextParent != null) ? contextParent : el, xml); + return el; + } + return doc.createElement(name); + } + + /** + * Ensure that the namespace declaration for the provided XML metadata exists on the root + * element. + */ + private void ensureNamespaceDeclarationOnRoot(org.w3c.dom.Element anyChild, XML xml) { + if (xml == null || xml.getNamespace() == null) { + return; + } + org.w3c.dom.Node node = anyChild; + // find the document root element + while (node != null && !(node instanceof org.w3c.dom.Document)) { + if (node.getParentNode() == null) { + break; + } + node = node.getParentNode(); + } + org.w3c.dom.Element root = null; + if (node instanceof org.w3c.dom.Document) { + root = ((org.w3c.dom.Document) node).getDocumentElement(); + } else { + // walk up parents from anyChild to find root element + node = anyChild; + while (node != null && !(node instanceof org.w3c.dom.Document)) { + if (node instanceof org.w3c.dom.Element + && node.getParentNode() instanceof org.w3c.dom.Document) { + root = (org.w3c.dom.Element) node; + break; + } + node = node.getParentNode(); + } + } + if (root == null) { + return; + } + String ns = xml.getNamespace(); + String prefix = xml.getPrefix(); + if (prefix != null) { + String attr = "xmlns:" + prefix; + if (!root.hasAttribute(attr)) { + root.setAttribute(attr, ns); + } + } else { + if (!root.hasAttribute("xmlns")) { + root.setAttribute("xmlns", ns); + } + } + } + + private enum JsonElement { OBJECT_BEGIN, OBJECT_END, ARRAY_BEGIN, @@ -75,17 +452,17 @@ private enum Element { } @SuppressWarnings("serial") - private static final Map SYNTAX = + private static final Map SYNTAX = Collections.unmodifiableMap( - new HashMap() { + new HashMap() { { - put(Element.OBJECT_BEGIN, "{"); - put(Element.OBJECT_END, "}"); - put(Element.ARRAY_BEGIN, "["); - put(Element.ARRAY_END, "]"); - put(Element.PROPERTY_CONTAINER, "\""); - put(Element.INNER_SEPARATOR, ":"); - put(Element.OUTER_SEPARATOR, ","); + put(JsonElement.OBJECT_BEGIN, "{"); + put(JsonElement.OBJECT_END, "}"); + put(JsonElement.ARRAY_BEGIN, "["); + put(JsonElement.ARRAY_END, "]"); + put(JsonElement.PROPERTY_CONTAINER, "\""); + put(JsonElement.INNER_SEPARATOR, ":"); + put(JsonElement.OUTER_SEPARATOR, ","); } }); @@ -158,17 +535,17 @@ private static String generateFromBinarySchema(BinarySchema schema, boolean imag private String generateFromObjectSchema(Map properties) { StringBuilder json = new StringBuilder(); boolean isFirst = true; - json.append(SYNTAX.get(Element.OBJECT_BEGIN)); + json.append(SYNTAX.get(JsonElement.OBJECT_BEGIN)); for (Map.Entry property : properties.entrySet()) { if (isFirst) { isFirst = false; } else { - json.append(SYNTAX.get(Element.OUTER_SEPARATOR)); + json.append(SYNTAX.get(JsonElement.OUTER_SEPARATOR)); } - json.append(SYNTAX.get(Element.PROPERTY_CONTAINER)); + json.append(SYNTAX.get(JsonElement.PROPERTY_CONTAINER)); json.append(property.getKey()); - json.append(SYNTAX.get(Element.PROPERTY_CONTAINER)); - json.append(SYNTAX.get(Element.INNER_SEPARATOR)); + json.append(SYNTAX.get(JsonElement.PROPERTY_CONTAINER)); + json.append(SYNTAX.get(JsonElement.INNER_SEPARATOR)); String value; if (dataGenerator.isSupported(property.getValue())) { value = dataGenerator.generateBodyValue(property.getKey(), property.getValue()); @@ -187,16 +564,16 @@ private String generateFromObjectSchema(Map properties) { } json.append(value); } - json.append(SYNTAX.get(Element.OBJECT_END)); + json.append(SYNTAX.get(JsonElement.OBJECT_END)); return json.toString(); } private static String createJsonArrayWith(String jsonStr) { - return SYNTAX.get(Element.ARRAY_BEGIN) + return SYNTAX.get(JsonElement.ARRAY_BEGIN) + jsonStr - + SYNTAX.get(Element.OUTER_SEPARATOR) + + SYNTAX.get(JsonElement.OUTER_SEPARATOR) + jsonStr - + SYNTAX.get(Element.ARRAY_END); + + SYNTAX.get(JsonElement.ARRAY_END); } private String generateJsonPrimitiveValue(Schema schema) { @@ -224,12 +601,12 @@ private static void resolveNotSchema(Schema schema) { } @SuppressWarnings("serial") - private static final Map FORMSYNTAX = + private static final Map FORMSYNTAX = Collections.unmodifiableMap( - new HashMap() { + new HashMap() { { - put(Element.INNER_SEPARATOR, "="); - put(Element.OUTER_SEPARATOR, "&"); + put(JsonElement.INNER_SEPARATOR, "="); + put(JsonElement.OUTER_SEPARATOR, "&"); } }); @@ -243,12 +620,12 @@ public String generateForm(Schema schema) { StringBuilder formData = new StringBuilder(); for (Map.Entry property : properties.entrySet()) { formData.append(urlEncode(property.getKey())); - formData.append(FORMSYNTAX.get(Element.INNER_SEPARATOR)); + formData.append(FORMSYNTAX.get(JsonElement.INNER_SEPARATOR)); formData.append( urlEncode( dataGenerator.generateValue( property.getKey(), property.getValue(), true))); - formData.append(FORMSYNTAX.get(Element.OUTER_SEPARATOR)); + formData.append(FORMSYNTAX.get(JsonElement.OUTER_SEPARATOR)); } return formData.substring(0, formData.length() - 1); } diff --git a/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/automation/OpenApiIntegrationXmlTest.java b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/automation/OpenApiIntegrationXmlTest.java new file mode 100644 index 00000000000..c3038c2cc27 --- /dev/null +++ b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/automation/OpenApiIntegrationXmlTest.java @@ -0,0 +1,92 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.openapi.automation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.parosproxy.paros.Constant; +import org.zaproxy.zap.extension.openapi.ExtensionOpenApi; +import org.zaproxy.zap.extension.openapi.converter.swagger.OperationModel; +import org.zaproxy.zap.extension.openapi.converter.swagger.RequestModelConverter; +import org.zaproxy.zap.extension.openapi.generators.Generators; +import org.zaproxy.zap.testutils.TestUtils; + +class OpenApiIntegrationXmlTest extends TestUtils { + + @BeforeEach + void setUp() { + mockMessages(new ExtensionOpenApi()); + Constant.messages = null; // leave default initialized by TestUtils/mockMessages + } + + @Test + void shouldGenerateXmlRequestBodiesAndNoUnsupportedMessage() throws Exception { + String defn = + IOUtils.toString( + this.getClass() + .getResourceAsStream( + "/org/zaproxy/zap/extension/openapi/v3/openapi_xml_integration.yaml"), + StandardCharsets.UTF_8); + + ParseOptions options = new ParseOptions(); + options.setResolveFully(true); + OpenAPI openAPI = + new OpenAPIV3Parser().readContents(defn, new ArrayList<>(), options).getOpenAPI(); + + Generators generators = new Generators(null); + OperationModel operationModel = + new OperationModel("/xml", openAPI.getPaths().get("/xml").getPost(), null); + + RequestModelConverter converter = new RequestModelConverter(); + String body = converter.convert(operationModel, generators).getBody(); + + // Body should be non-empty and should look like XML + org.junit.jupiter.api.Assertions.assertNotNull(body); + org.junit.jupiter.api.Assertions.assertFalse(body.isEmpty()); + // Quick sanity parse + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + db.parse(new java.io.ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + + // There should be no unsupported-content error message for application/xml + assertThat( + generators.getErrorMessages().stream() + .filter( + s -> + s.contains( + "the content type application/xml is not supported")) + .toList(), + empty()); + + // The overall error messages list may be empty; we've already asserted the + // specific + // unsupported-content message is not present above. + } +} diff --git a/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/generators/BodyGeneratorXmlUnitTest.java b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/generators/BodyGeneratorXmlUnitTest.java new file mode 100644 index 00000000000..778802961a1 --- /dev/null +++ b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/generators/BodyGeneratorXmlUnitTest.java @@ -0,0 +1,266 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.openapi.generators; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ComposedSchema; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.zaproxy.zap.extension.openapi.ExtensionOpenApi; +import org.zaproxy.zap.testutils.TestUtils; + +class BodyGeneratorXmlUnitTest extends TestUtils { + + Generators generators; + + @BeforeAll + static void setUp() { + mockMessages(new ExtensionOpenApi()); + } + + @BeforeEach + void init() { + generators = new Generators(null); + } + + @Test + void shouldGenerateXmlForSimpleObject() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("person"); + Schema name = new Schema<>(); + name.setType("string"); + schema.setProperties(Map.of("name", name)); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + // Parse and assert structure + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.Element root = doc.getDocumentElement(); + org.junit.jupiter.api.Assertions.assertEquals("person", root.getNodeName()); + org.w3c.dom.NodeList names = doc.getElementsByTagName("name"); + org.junit.jupiter.api.Assertions.assertTrue(names.getLength() >= 1); + } + + @Test + void shouldGenerateXmlForArray() throws Exception { + ArraySchema schema = new ArraySchema(); + Schema items = new Schema<>(); + items.setType("string"); + items.setName("tag"); + schema.setItems(items); + schema.setName("tags"); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.Element root = doc.getDocumentElement(); + org.junit.jupiter.api.Assertions.assertEquals("tags", root.getNodeName()); + org.w3c.dom.NodeList tagNodes = doc.getElementsByTagName("tag"); + org.junit.jupiter.api.Assertions.assertEquals(2, tagNodes.getLength()); + } + + @Test + void shouldGenerateXmlWithAttribute() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("person"); + Schema id = new Schema<>(); + id.setType("integer"); + io.swagger.v3.oas.models.media.XML idXml = new io.swagger.v3.oas.models.media.XML(); + idXml.setAttribute(true); + idXml.setName("id"); + id.setXml(idXml); + Schema name = new Schema<>(); + name.setType("string"); + schema.setProperties(Map.of("id", id, "name", name)); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.Element root = doc.getDocumentElement(); + org.junit.jupiter.api.Assertions.assertEquals("person", root.getNodeName()); + org.junit.jupiter.api.Assertions.assertTrue(root.hasAttribute("id")); + org.w3c.dom.NodeList names = doc.getElementsByTagName("name"); + org.junit.jupiter.api.Assertions.assertTrue(names.getLength() >= 1); + } + + @Test + void shouldGenerateXmlForUnwrappedArray() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("tagsContainer"); + ArraySchema tags = new ArraySchema(); + Schema items = new Schema<>(); + items.setType("string"); + items.setName("tag"); + io.swagger.v3.oas.models.media.XML itemXml = new io.swagger.v3.oas.models.media.XML(); + itemXml.setName("tag"); + items.setXml(itemXml); + io.swagger.v3.oas.models.media.XML tagsXml = new io.swagger.v3.oas.models.media.XML(); + tagsXml.setWrapped(false); + tags.setXml(tagsXml); + tags.setItems(items); + schema.setProperties(Map.of("tags", tags)); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.Element root = doc.getDocumentElement(); + org.junit.jupiter.api.Assertions.assertEquals("tagsContainer", root.getNodeName()); + // ensure no wrapper exists + org.w3c.dom.NodeList wrappers = doc.getElementsByTagName("tags"); + org.junit.jupiter.api.Assertions.assertEquals(0, wrappers.getLength()); + org.w3c.dom.NodeList tagNodes = doc.getElementsByTagName("tag"); + org.junit.jupiter.api.Assertions.assertEquals(2, tagNodes.getLength()); + } + + @Test + void shouldGenerateXmlWithNamespaceAndPrefix() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("person"); + Schema name = new Schema<>(); + name.setType("string"); + schema.setProperties(Map.of("name", name)); + io.swagger.v3.oas.models.media.XML rootXml = new io.swagger.v3.oas.models.media.XML(); + rootXml.setNamespace("http://example.com/ns"); + rootXml.setPrefix("ex"); + rootXml.setName("person"); + schema.setXml(rootXml); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.Element root = doc.getDocumentElement(); + org.junit.jupiter.api.Assertions.assertEquals("person", root.getLocalName()); + org.junit.jupiter.api.Assertions.assertEquals("ex", root.getPrefix()); + org.junit.jupiter.api.Assertions.assertEquals( + "http://example.com/ns", root.getNamespaceURI()); + } + + @org.junit.jupiter.api.Test + void shouldGenerateXmlForBinary() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("fileContainer"); + io.swagger.v3.oas.models.media.BinarySchema file = + new io.swagger.v3.oas.models.media.BinarySchema(); + schema.setProperties(Map.of("file", file)); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.NodeList files = doc.getElementsByTagName("file"); + org.junit.jupiter.api.Assertions.assertEquals(1, files.getLength()); + } + + @org.junit.jupiter.api.Test + void shouldGenerateXmlForAdditionalProperties() throws Exception { + ObjectSchema schema = new ObjectSchema(); + schema.setName("mapContainer"); + Schema add = new Schema<>(); + add.setType("string"); + schema.setAdditionalProperties(add); + + String xml = generators.getBodyGenerator().generateXml(schema); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.NodeList entries = doc.getElementsByTagName("entry"); + org.junit.jupiter.api.Assertions.assertEquals(2, entries.getLength()); + org.w3c.dom.NodeList keys = doc.getElementsByTagName("key"); + org.junit.jupiter.api.Assertions.assertEquals(2, keys.getLength()); + org.w3c.dom.NodeList values = doc.getElementsByTagName("value"); + org.junit.jupiter.api.Assertions.assertEquals(2, values.getLength()); + } + + @org.junit.jupiter.api.Test + void shouldGenerateXmlForComposedSchemaAllOf() throws Exception { + ComposedSchema cs = new ComposedSchema(); + Schema s1 = new ObjectSchema(); + Schema prop1 = new Schema<>(); + prop1.setType("string"); + ((ObjectSchema) s1).setProperties(Map.of("a", prop1)); + Schema s2 = new ObjectSchema(); + Schema prop2 = new Schema<>(); + prop2.setType("integer"); + ((ObjectSchema) s2).setProperties(Map.of("b", prop2)); + cs.setAllOf(List.of(s1, s2)); + + String xml = generators.getBodyGenerator().generateXml(cs); + assertFalse(xml.isEmpty()); + javax.xml.parsers.DocumentBuilderFactory dbf = + javax.xml.parsers.DocumentBuilderFactory.newInstance(); + javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = + db.parse( + new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + org.w3c.dom.NodeList a = doc.getElementsByTagName("a"); + org.w3c.dom.NodeList b = doc.getElementsByTagName("b"); + org.junit.jupiter.api.Assertions.assertEquals(1, a.getLength()); + org.junit.jupiter.api.Assertions.assertEquals(1, b.getLength()); + } +} diff --git a/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/v3/BodyGeneratorUnitTest.java b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/v3/BodyGeneratorUnitTest.java index c16301d20c3..b87a006bc84 100644 --- a/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/v3/BodyGeneratorUnitTest.java +++ b/addOns/openapi/src/test/java/org/zaproxy/zap/extension/openapi/v3/BodyGeneratorUnitTest.java @@ -19,10 +19,6 @@ */ package org.zaproxy.zap.extension.openapi.v3; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -723,12 +719,16 @@ void shouldNotGenerateContentForApplicationXml() throws IOException { new OperationModel("/xml", definition.getPaths().get("/xml").getPost(), null); // When String content = new RequestModelConverter().convert(operationModel, generators).getBody(); - // Then - assertThat(content, is(emptyString())); - assertThat( - generators.getErrorMessages(), - contains( - "Not generating request body for operation xml, the content type application/xml is not supported.")); + // Then - XML bodies should now be generated for application/xml and the + // unsupported + // content message should no longer be produced for this operation. + org.junit.jupiter.api.Assertions.assertFalse(content.isEmpty()); + org.junit.jupiter.api.Assertions.assertFalse( + generators.getErrorMessages().stream() + .anyMatch( + s -> + s.contains( + "Not generating request body for operation xml, the content type application/xml is not supported."))); } @Test diff --git a/addOns/openapi/src/test/resources/org/zaproxy/zap/extension/openapi/v3/openapi_xml_integration.yaml b/addOns/openapi/src/test/resources/org/zaproxy/zap/extension/openapi/v3/openapi_xml_integration.yaml new file mode 100644 index 00000000000..ea78cf5fb51 --- /dev/null +++ b/addOns/openapi/src/test/resources/org/zaproxy/zap/extension/openapi/v3/openapi_xml_integration.yaml @@ -0,0 +1,34 @@ +openapi: 3.0.3 +info: + title: XML Integration Test + version: 1.0.0 +paths: + /xml: + post: + requestBody: + content: + application/xml: + schema: + type: object + xml: + name: person + namespace: "http://example.com/ns" + prefix: ex + properties: + id: + type: integer + xml: + attribute: true + name: id + tags: + type: array + xml: + name: tags + wrapped: false + items: + type: string + xml: + name: tag + responses: + "200": + description: OK diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java index c43fa4c2198..6387ffb6f36 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java @@ -55,8 +55,7 @@ public class SpiderDialog extends StandardFieldsDialog { private static final String FIELD_MAX_DEPTH = "spider.custom.label.maxDepth"; private static final String FIELD_MAX_CHILDREN = "spider.custom.label.maxChildren"; private static final String FIELD_MAX_DURATION = "spider.custom.label.maxDuration"; - private static final String FIELD_MAX_PARSE_SIZE_BYTES = - "spider.custom.label.maxParseSizeBytes"; + private static final String FIELD_MAX_PARSE_SIZE_BYTES = "spider.custom.label.maxParseSizeBytes"; private static final String FIELD_SEND_REFERER = "spider.custom.label.sendReferer"; private static final String FIELD_ACCEPT_COOKIES = "spider.custom.label.acceptcookies"; private static final String FIELD_PROCESS_FORMS = "spider.custom.label.processForms"; @@ -82,14 +81,15 @@ public class SpiderDialog extends StandardFieldsDialog { /** * Flag that holds the previous checked state of the "Subtree Only" checkbox. * - *

Used to restore the previous checked state between dialogue invocations. + *

+ * Used to restore the previous checked state between dialogue invocations. * * @see #FIELD_SUBTREE_ONLY */ private boolean subtreeOnlyPreviousCheckedState; - private ExtensionUserManagement extUserMgmt = - Control.getSingleton().getExtensionLoader().getExtension(ExtensionUserManagement.class); + private ExtensionUserManagement extUserMgmt = Control.getSingleton().getExtensionLoader() + .getExtension(ExtensionUserManagement.class); private Target target; @@ -98,7 +98,7 @@ public SpiderDialog(ExtensionSpider2 ext, Frame owner, Dimension dim) { owner, "spider.custom.title", dim, - new String[] {"spider.custom.tab.scope", "spider.custom.tab.adv"}); + new String[] { "spider.custom.tab.scope", "spider.custom.tab.adv" }); this.extension = ext; // The first time init to the default options set, after that keep own copies @@ -187,7 +187,8 @@ public void actionPerformed(ActionEvent e) { }); if (target != null) { - // Set up the fields if a node has been specified, otherwise leave as previously set + // Set up the fields if a node has been specified, otherwise leave as previously + // set this.targetSelected(FIELD_START, this.target); this.setUsers(); } @@ -204,7 +205,8 @@ public void actionPerformed(ActionEvent e) { private SpiderParam getSpiderParam() { if (spiderParam == null) { - // First time in clone the global options, after that keep the last ones the user set + // First time in clone the global options, after that keep the last ones the + // user set spiderParam = (SpiderParam) extension.getSpiderParam().clone(); } return spiderParam; @@ -212,7 +214,7 @@ private SpiderParam getSpiderParam() { private void setAdvancedTabs(boolean visible) { // Show/hide all except from the first tab - this.setTabsVisible(new String[] {"spider.custom.tab.adv"}, visible); + this.setTabsVisible(new String[] { "spider.custom.tab.adv" }, visible); } @Override @@ -254,8 +256,7 @@ private User getSelectedUser() { Context context = this.getSelectedContext(); if (context != null) { String userName = this.getStringValue(FIELD_USER); - List users = - this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); + List users = this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); for (User user : users) { if (userName.equals(user.getName())) { return user; @@ -269,8 +270,7 @@ private void setUsers() { Context context = this.getSelectedContext(); List userNames = new ArrayList<>(); if (context != null) { - List users = - this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); + List users = this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); userNames.add(""); // The default should always be 'not specified' for (User user : users) { userNames.add(user.getName()); @@ -299,8 +299,7 @@ public String getSaveButtonText() { @Override public JButton[] getExtraButtons() { if (extraButtons == null) { - JButton resetButton = - new JButton(Constant.messages.getString("spider.custom.button.reset")); + JButton resetButton = new JButton(Constant.messages.getString("spider.custom.button.reset")); resetButton.addActionListener( new java.awt.event.ActionListener() { @Override @@ -309,7 +308,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { } }); - extraButtons = new JButton[] {resetButton}; + extraButtons = new JButton[] { resetButton }; } return extraButtons; } @@ -392,7 +391,8 @@ public String validateFields() { try { // Need both constructors as they catch slightly different issues ;) new URI(url, true); - new URL(url); + // java.net.URL(String) is deprecated in newer JDKs; use URI -> URL + new java.net.URI(url).toURL(); } catch (Exception e) { return Constant.messages.getString("spider.custom.nostart.error"); } diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java index ae87371499e..8623b64900f 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java @@ -38,14 +38,18 @@ import org.zaproxy.addon.spider.parser.ParseContext; /** - * The UrlCanonicalizer is used for the process of converting an URL into a canonical (normalized) - * form. See URL Normalization for a + * The UrlCanonicalizer is used for the process of converting an URL into a + * canonical (normalized) + * form. See URL + * Normalization for a * reference. * - *

Note: some parts of the code are adapted from: + * Note: some parts of the code are adapted from: stackoverflow * - *

Added support for OData URLs + *

+ * Added support for OData URLs */ public final class UrlCanonicalizer { @@ -59,29 +63,30 @@ public final class UrlCanonicalizer { private static final int HTTPS_DEFAULT_PORT = 443; /** - * OData support Extract the ID of a resource including the surrounding quote First group is the - * resource_name Second group is the ID (quote will be taken as part of the value) + * OData support Extract the ID of a resource including the surrounding quote + * First group is the + * resource_name Second group is the ID (quote will be taken as part of the + * value) */ - private static final Pattern PATTERN_RESOURCE_IDENTIFIER_UNQUOTED = - Pattern.compile("/([\\w%]*)\\(([\\w']*)\\)"); + private static final Pattern PATTERN_RESOURCE_IDENTIFIER_UNQUOTED = Pattern.compile("/([\\w%]*)\\(([\\w']*)\\)"); /** OData support Detect a section containing a composite IDs */ - private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER = - Pattern.compile("/[\\w%]*\\((.*)\\)"); + private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER = Pattern.compile("/[\\w%]*\\((.*)\\)"); /** OData support Extract the detail of the multiples IDs */ - private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL = - Pattern.compile("([\\w%]*)=([\\w']*)"); + private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL = Pattern.compile("([\\w%]*)=([\\w']*)"); /** Private constructor to avoid initialization of object. */ - private UrlCanonicalizer() {} + private UrlCanonicalizer() { + } /** - * Gets the canonical url, starting from a relative or absolute url found in a given context + * Gets the canonical url, starting from a relative or absolute url found in a + * given context * (baseURL). * - * @param ctx the parse context. - * @param url the url string defining the reference + * @param ctx the parse context. + * @param url the url string defining the reference * @param baseURL the context in which this url was found * @return the canonical url */ @@ -130,7 +135,8 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR } /* - * Normalize: no empty segments (i.e., "//"), no segments equal to ".", and no segments equal to + * Normalize: no empty segments (i.e., "//"), no segments equal to ".", and no + * segments equal to * ".." that are preceded by a segment not equal to "..". */ String path = canonicalURI.normalize().getRawPath(); @@ -151,11 +157,9 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR path = path.trim(); /* Process parameters and sort them. */ - final SortedSet params = - createSortedParameters(canonicalURI.getRawQuery()); + final SortedSet params = createSortedParameters(canonicalURI.getRawQuery()); final String queryString; - String canonicalParams = - canonicalize(params, ctx.getSpiderParam()::isIrrelevantUrlParameter); + String canonicalParams = canonicalize(params, ctx.getSpiderParam()::isIrrelevantUrlParameter); queryString = (canonicalParams.isEmpty() ? "" : "?" + canonicalParams); /* Add starting slash if needed */ @@ -174,7 +178,9 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR String host = canonicalURI.getHost().toLowerCase(); String pathAndQueryString = normalizePath(path) + queryString; - URL result = new URL(protocol, host, port, pathAndQueryString); + // URL(String,String,int,String) is deprecated; construct via URI + java.net.URI tmpUri = new java.net.URI(protocol, null, host, port, pathAndQueryString, null, null); + URL result = tmpUri.toURL(); return result.toExternalForm(); } catch (Exception ex) { @@ -190,11 +196,13 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR /** * Tells whether or not the given port is the default for the given scheme. * - *

Note: Only HTTP and HTTPS schemes are taken into account. + *

+ * Note: Only HTTP and HTTPS schemes are taken into account. * * @param scheme the scheme - * @param port the port - * @return {@code true} if given the port is the default port for the given scheme, {@code + * @param port the port + * @return {@code true} if given the port is the default port for the given + * scheme, {@code * false} otherwise. */ private static boolean isDefaultPort(String scheme, int port) { @@ -212,19 +220,25 @@ static String buildCleanedParametersUriRepresentation( } /** - * Builds a String representation of the URI with cleaned parameters, that can be used when - * checking if an URI was already visited. The URI provided as a parameter should be already + * Builds a String representation of the URI with cleaned parameters, that can + * be used when + * checking if an URI was already visited. The URI provided as a parameter + * should be already * cleaned and canonicalized, so it should be build with a result from {@link * #getCanonicalURL(String)}. * - *

When building the URI representation, the same format should be used for all the cases, as - * it may affect the number of times the pages are visited and reported if the option + *

+ * When building the URI representation, the same format should be used for all + * the cases, as + * it may affect the number of times the pages are visited and reported if the + * option * HandleParametersOption is changed while the spider is running. * - * @param uri the uri - * @param handleParameters the handle parameters option - * @param handleODataParametersVisited Should we handle specific OData parameters - * @param irrelevantParameter a predicate to ignore parameters. + * @param uri the uri + * @param handleParameters the handle parameters option + * @param handleODataParametersVisited Should we handle specific OData + * parameters + * @param irrelevantParameter a predicate to ignore parameters. * @return the string representation of the URI * @throws URIException the URI exception */ @@ -240,20 +254,21 @@ static String buildCleanedParametersUriRepresentation( return uri.toString(); } - // If the option is set to ignore parameters completely, ignore the query completely + // If the option is set to ignore parameters completely, ignore the query + // completely if (handleParameters.equals(HandleParametersOption.IGNORE_COMPLETELY)) { return createBaseUriWithCleanedPath( uri, handleParameters, handleODataParametersVisited); } - // If the option is set to ignore the value, we get the parameters and we only add their + // If the option is set to ignore the value, we get the parameters and we only + // add their // name to the // query if (handleParameters.equals(HandleParametersOption.IGNORE_VALUE)) { - StringBuilder retVal = - new StringBuilder( - createBaseUriWithCleanedPath( - uri, handleParameters, handleODataParametersVisited)); + StringBuilder retVal = new StringBuilder( + createBaseUriWithCleanedPath( + uri, handleParameters, handleODataParametersVisited)); String cleanedQuery = getCleanedQuery(uri.getEscapedQuery(), irrelevantParameter); @@ -337,10 +352,11 @@ private static String getCleanedQuery( } /** - * Clean the path in the case of an OData Uri containing a resource identifier (simple or + * Clean the path in the case of an OData Uri containing a resource identifier + * (simple or * multiple) * - * @param path The path to clean + * @param path The path to clean * @param handleParameters tThe cleaning mode * @return A cleaned path */ @@ -391,9 +407,8 @@ private static String cleanODataPath(String path, HandleParametersOption handleP } else { StringBuilder sb = new StringBuilder(beforeSubstring); - matcher = - PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL.matcher( - multipleIdentifierSection); + matcher = PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL.matcher( + multipleIdentifierSection); int i = 1; while (matcher.find()) { @@ -416,12 +431,14 @@ private static String cleanODataPath(String path, HandleParametersOption handleP } /** - * Creates a sorted set with all the parameters from the given {@code query}, ordered + * Creates a sorted set with all the parameters from the given {@code query}, + * ordered * lexicographically by name and value. * * @param queryString the query string - * @return a sorted set with all parameters, or {@code null} if the query string is {@code null} - * or empty. + * @return a sorted set with all parameters, or {@code null} if the query string + * is {@code null} + * or empty. */ private static SortedSet createSortedParameters(final String queryString) { if (queryString == null || queryString.isEmpty()) { @@ -456,7 +473,8 @@ private static SortedSet createSortedParameters(final String que /** * Canonicalize the query string. * - * @param sortedParameters Parameter name-value pairs in lexicographical order. + * @param sortedParameters Parameter name-value pairs in lexicographical + * order. * @param irrelevantParameter url parameters that are skipped * @return Canonical form of query string. */ @@ -498,7 +516,8 @@ private static String normalizePath(final String path) { /** * A query parameter, with non-{@code null} name and value. * - *

The query parameters are ordered by name and value. + *

+ * The query parameters are ordered by name and value. */ private static class QueryParameter implements Comparable { From e15f9dd6369bae1d34e6b244cb007bdfb329ad28 Mon Sep 17 00:00:00 2001 From: w0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:05:01 -0700 Subject: [PATCH 2/6] Fix the remaining linter errors with spotlessApply --- .../zaproxy/addon/spider/SpiderDialog.java | 26 +++-- .../addon/spider/UrlCanonicalizer.java | 107 ++++++++---------- 2 files changed, 61 insertions(+), 72 deletions(-) diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java index 6387ffb6f36..2ee95aedae3 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java @@ -23,7 +23,6 @@ import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; @@ -55,7 +54,8 @@ public class SpiderDialog extends StandardFieldsDialog { private static final String FIELD_MAX_DEPTH = "spider.custom.label.maxDepth"; private static final String FIELD_MAX_CHILDREN = "spider.custom.label.maxChildren"; private static final String FIELD_MAX_DURATION = "spider.custom.label.maxDuration"; - private static final String FIELD_MAX_PARSE_SIZE_BYTES = "spider.custom.label.maxParseSizeBytes"; + private static final String FIELD_MAX_PARSE_SIZE_BYTES = + "spider.custom.label.maxParseSizeBytes"; private static final String FIELD_SEND_REFERER = "spider.custom.label.sendReferer"; private static final String FIELD_ACCEPT_COOKIES = "spider.custom.label.acceptcookies"; private static final String FIELD_PROCESS_FORMS = "spider.custom.label.processForms"; @@ -81,15 +81,14 @@ public class SpiderDialog extends StandardFieldsDialog { /** * Flag that holds the previous checked state of the "Subtree Only" checkbox. * - *

- * Used to restore the previous checked state between dialogue invocations. + *

Used to restore the previous checked state between dialogue invocations. * * @see #FIELD_SUBTREE_ONLY */ private boolean subtreeOnlyPreviousCheckedState; - private ExtensionUserManagement extUserMgmt = Control.getSingleton().getExtensionLoader() - .getExtension(ExtensionUserManagement.class); + private ExtensionUserManagement extUserMgmt = + Control.getSingleton().getExtensionLoader().getExtension(ExtensionUserManagement.class); private Target target; @@ -98,7 +97,7 @@ public SpiderDialog(ExtensionSpider2 ext, Frame owner, Dimension dim) { owner, "spider.custom.title", dim, - new String[] { "spider.custom.tab.scope", "spider.custom.tab.adv" }); + new String[] {"spider.custom.tab.scope", "spider.custom.tab.adv"}); this.extension = ext; // The first time init to the default options set, after that keep own copies @@ -214,7 +213,7 @@ private SpiderParam getSpiderParam() { private void setAdvancedTabs(boolean visible) { // Show/hide all except from the first tab - this.setTabsVisible(new String[] { "spider.custom.tab.adv" }, visible); + this.setTabsVisible(new String[] {"spider.custom.tab.adv"}, visible); } @Override @@ -256,7 +255,8 @@ private User getSelectedUser() { Context context = this.getSelectedContext(); if (context != null) { String userName = this.getStringValue(FIELD_USER); - List users = this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); + List users = + this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); for (User user : users) { if (userName.equals(user.getName())) { return user; @@ -270,7 +270,8 @@ private void setUsers() { Context context = this.getSelectedContext(); List userNames = new ArrayList<>(); if (context != null) { - List users = this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); + List users = + this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers(); userNames.add(""); // The default should always be 'not specified' for (User user : users) { userNames.add(user.getName()); @@ -299,7 +300,8 @@ public String getSaveButtonText() { @Override public JButton[] getExtraButtons() { if (extraButtons == null) { - JButton resetButton = new JButton(Constant.messages.getString("spider.custom.button.reset")); + JButton resetButton = + new JButton(Constant.messages.getString("spider.custom.button.reset")); resetButton.addActionListener( new java.awt.event.ActionListener() { @Override @@ -308,7 +310,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { } }); - extraButtons = new JButton[] { resetButton }; + extraButtons = new JButton[] {resetButton}; } return extraButtons; } diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java index 8623b64900f..ca1079aedf1 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java @@ -38,18 +38,14 @@ import org.zaproxy.addon.spider.parser.ParseContext; /** - * The UrlCanonicalizer is used for the process of converting an URL into a - * canonical (normalized) - * form. See URL - * Normalization for a + * The UrlCanonicalizer is used for the process of converting an URL into a canonical (normalized) + * form. See URL Normalization for a * reference. * - *

- * Note: some parts of the code are adapted from: Note: some parts of the code are adapted from: stackoverflow * - *

- * Added support for OData URLs + *

Added support for OData URLs */ public final class UrlCanonicalizer { @@ -63,30 +59,29 @@ public final class UrlCanonicalizer { private static final int HTTPS_DEFAULT_PORT = 443; /** - * OData support Extract the ID of a resource including the surrounding quote - * First group is the - * resource_name Second group is the ID (quote will be taken as part of the - * value) + * OData support Extract the ID of a resource including the surrounding quote First group is the + * resource_name Second group is the ID (quote will be taken as part of the value) */ - private static final Pattern PATTERN_RESOURCE_IDENTIFIER_UNQUOTED = Pattern.compile("/([\\w%]*)\\(([\\w']*)\\)"); + private static final Pattern PATTERN_RESOURCE_IDENTIFIER_UNQUOTED = + Pattern.compile("/([\\w%]*)\\(([\\w']*)\\)"); /** OData support Detect a section containing a composite IDs */ - private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER = Pattern.compile("/[\\w%]*\\((.*)\\)"); + private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER = + Pattern.compile("/[\\w%]*\\((.*)\\)"); /** OData support Extract the detail of the multiples IDs */ - private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL = Pattern.compile("([\\w%]*)=([\\w']*)"); + private static final Pattern PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL = + Pattern.compile("([\\w%]*)=([\\w']*)"); /** Private constructor to avoid initialization of object. */ - private UrlCanonicalizer() { - } + private UrlCanonicalizer() {} /** - * Gets the canonical url, starting from a relative or absolute url found in a - * given context + * Gets the canonical url, starting from a relative or absolute url found in a given context * (baseURL). * - * @param ctx the parse context. - * @param url the url string defining the reference + * @param ctx the parse context. + * @param url the url string defining the reference * @param baseURL the context in which this url was found * @return the canonical url */ @@ -157,9 +152,11 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR path = path.trim(); /* Process parameters and sort them. */ - final SortedSet params = createSortedParameters(canonicalURI.getRawQuery()); + final SortedSet params = + createSortedParameters(canonicalURI.getRawQuery()); final String queryString; - String canonicalParams = canonicalize(params, ctx.getSpiderParam()::isIrrelevantUrlParameter); + String canonicalParams = + canonicalize(params, ctx.getSpiderParam()::isIrrelevantUrlParameter); queryString = (canonicalParams.isEmpty() ? "" : "?" + canonicalParams); /* Add starting slash if needed */ @@ -179,7 +176,8 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR String pathAndQueryString = normalizePath(path) + queryString; // URL(String,String,int,String) is deprecated; construct via URI - java.net.URI tmpUri = new java.net.URI(protocol, null, host, port, pathAndQueryString, null, null); + java.net.URI tmpUri = + new java.net.URI(protocol, null, host, port, pathAndQueryString, null, null); URL result = tmpUri.toURL(); return result.toExternalForm(); @@ -196,13 +194,11 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR /** * Tells whether or not the given port is the default for the given scheme. * - *

- * Note: Only HTTP and HTTPS schemes are taken into account. + *

Note: Only HTTP and HTTPS schemes are taken into account. * * @param scheme the scheme - * @param port the port - * @return {@code true} if given the port is the default port for the given - * scheme, {@code + * @param port the port + * @return {@code true} if given the port is the default port for the given scheme, {@code * false} otherwise. */ private static boolean isDefaultPort(String scheme, int port) { @@ -220,25 +216,19 @@ static String buildCleanedParametersUriRepresentation( } /** - * Builds a String representation of the URI with cleaned parameters, that can - * be used when - * checking if an URI was already visited. The URI provided as a parameter - * should be already + * Builds a String representation of the URI with cleaned parameters, that can be used when + * checking if an URI was already visited. The URI provided as a parameter should be already * cleaned and canonicalized, so it should be build with a result from {@link * #getCanonicalURL(String)}. * - *

- * When building the URI representation, the same format should be used for all - * the cases, as - * it may affect the number of times the pages are visited and reported if the - * option + *

When building the URI representation, the same format should be used for all the cases, as + * it may affect the number of times the pages are visited and reported if the option * HandleParametersOption is changed while the spider is running. * - * @param uri the uri - * @param handleParameters the handle parameters option - * @param handleODataParametersVisited Should we handle specific OData - * parameters - * @param irrelevantParameter a predicate to ignore parameters. + * @param uri the uri + * @param handleParameters the handle parameters option + * @param handleODataParametersVisited Should we handle specific OData parameters + * @param irrelevantParameter a predicate to ignore parameters. * @return the string representation of the URI * @throws URIException the URI exception */ @@ -266,9 +256,10 @@ static String buildCleanedParametersUriRepresentation( // name to the // query if (handleParameters.equals(HandleParametersOption.IGNORE_VALUE)) { - StringBuilder retVal = new StringBuilder( - createBaseUriWithCleanedPath( - uri, handleParameters, handleODataParametersVisited)); + StringBuilder retVal = + new StringBuilder( + createBaseUriWithCleanedPath( + uri, handleParameters, handleODataParametersVisited)); String cleanedQuery = getCleanedQuery(uri.getEscapedQuery(), irrelevantParameter); @@ -352,11 +343,10 @@ private static String getCleanedQuery( } /** - * Clean the path in the case of an OData Uri containing a resource identifier - * (simple or + * Clean the path in the case of an OData Uri containing a resource identifier (simple or * multiple) * - * @param path The path to clean + * @param path The path to clean * @param handleParameters tThe cleaning mode * @return A cleaned path */ @@ -407,8 +397,9 @@ private static String cleanODataPath(String path, HandleParametersOption handleP } else { StringBuilder sb = new StringBuilder(beforeSubstring); - matcher = PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL.matcher( - multipleIdentifierSection); + matcher = + PATTERN_RESOURCE_MULTIPLE_IDENTIFIER_DETAIL.matcher( + multipleIdentifierSection); int i = 1; while (matcher.find()) { @@ -431,14 +422,12 @@ private static String cleanODataPath(String path, HandleParametersOption handleP } /** - * Creates a sorted set with all the parameters from the given {@code query}, - * ordered + * Creates a sorted set with all the parameters from the given {@code query}, ordered * lexicographically by name and value. * * @param queryString the query string - * @return a sorted set with all parameters, or {@code null} if the query string - * is {@code null} - * or empty. + * @return a sorted set with all parameters, or {@code null} if the query string is {@code null} + * or empty. */ private static SortedSet createSortedParameters(final String queryString) { if (queryString == null || queryString.isEmpty()) { @@ -473,8 +462,7 @@ private static SortedSet createSortedParameters(final String que /** * Canonicalize the query string. * - * @param sortedParameters Parameter name-value pairs in lexicographical - * order. + * @param sortedParameters Parameter name-value pairs in lexicographical order. * @param irrelevantParameter url parameters that are skipped * @return Canonical form of query string. */ @@ -516,8 +504,7 @@ private static String normalizePath(final String path) { /** * A query parameter, with non-{@code null} name and value. * - *

- * The query parameters are ordered by name and value. + *

The query parameters are ordered by name and value. */ private static class QueryParameter implements Comparable { From 2500202f7047b02952b6063ee149650df7f65b69 Mon Sep 17 00:00:00 2001 From: w0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:30:27 -0700 Subject: [PATCH 3/6] Revert URI changes --- .../zap/extension/openapi/ImportDialog.java | 30 ++++++++----------- .../zaproxy/addon/spider/SpiderDialog.java | 10 +++---- .../addon/spider/UrlCanonicalizer.java | 14 +++------ 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java index 82a6ec5730b..808e4b00231 100644 --- a/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java +++ b/addOns/openapi/src/main/java/org/zaproxy/zap/extension/openapi/ImportDialog.java @@ -23,7 +23,9 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.io.File; +import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.net.URL; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFileChooser; @@ -34,6 +36,7 @@ import javax.swing.JPopupMenu; import javax.swing.JProgressBar; import javax.swing.JTextField; +import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; @@ -330,23 +333,16 @@ private boolean importDefinition() { } try { - // Validate the definitionLocation as a URI without using the deprecated - // URL(String) constructor. Parse once with java.net.URI then build the - // apache httpclient URI from the ASCII/escaped form if the URI is absolute. - java.net.URI juri = new java.net.URI(definitionLocation); - if (juri.isAbsolute()) { - org.apache.commons.httpclient.URI uri = - new org.apache.commons.httpclient.URI(juri.toASCIIString(), true); - return extOpenApi.importOpenApiDefinition( - uri, - getTargetField().getText(), - true, - getSelectedContextId(), - getSelectedUser()) - == null; - } - // Not absolute -> treat as a file path (fall through) - } catch (URIException | URISyntaxException ignored) { + new URL(definitionLocation).toURI(); + var uri = new URI(definitionLocation, true); + return extOpenApi.importOpenApiDefinition( + uri, + getTargetField().getText(), + true, + getSelectedContextId(), + getSelectedUser()) + == null; + } catch (URIException | MalformedURLException | URISyntaxException ignored) { // Not a valid URI, try to import as a file } catch (InvalidUrlException e) { ThreadUtils.invokeAndWaitHandled( diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java index 2ee95aedae3..c43fa4c2198 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/SpiderDialog.java @@ -23,6 +23,7 @@ import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; @@ -186,8 +187,7 @@ public void actionPerformed(ActionEvent e) { }); if (target != null) { - // Set up the fields if a node has been specified, otherwise leave as previously - // set + // Set up the fields if a node has been specified, otherwise leave as previously set this.targetSelected(FIELD_START, this.target); this.setUsers(); } @@ -204,8 +204,7 @@ public void actionPerformed(ActionEvent e) { private SpiderParam getSpiderParam() { if (spiderParam == null) { - // First time in clone the global options, after that keep the last ones the - // user set + // First time in clone the global options, after that keep the last ones the user set spiderParam = (SpiderParam) extension.getSpiderParam().clone(); } return spiderParam; @@ -393,8 +392,7 @@ public String validateFields() { try { // Need both constructors as they catch slightly different issues ;) new URI(url, true); - // java.net.URL(String) is deprecated in newer JDKs; use URI -> URL - new java.net.URI(url).toURL(); + new URL(url); } catch (Exception e) { return Constant.messages.getString("spider.custom.nostart.error"); } diff --git a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java index ca1079aedf1..ae87371499e 100644 --- a/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java +++ b/addOns/spider/src/main/java/org/zaproxy/addon/spider/UrlCanonicalizer.java @@ -130,8 +130,7 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR } /* - * Normalize: no empty segments (i.e., "//"), no segments equal to ".", and no - * segments equal to + * Normalize: no empty segments (i.e., "//"), no segments equal to ".", and no segments equal to * ".." that are preceded by a segment not equal to "..". */ String path = canonicalURI.normalize().getRawPath(); @@ -175,10 +174,7 @@ public static String getCanonicalUrl(ParseContext ctx, String url, String baseUR String host = canonicalURI.getHost().toLowerCase(); String pathAndQueryString = normalizePath(path) + queryString; - // URL(String,String,int,String) is deprecated; construct via URI - java.net.URI tmpUri = - new java.net.URI(protocol, null, host, port, pathAndQueryString, null, null); - URL result = tmpUri.toURL(); + URL result = new URL(protocol, host, port, pathAndQueryString); return result.toExternalForm(); } catch (Exception ex) { @@ -244,15 +240,13 @@ static String buildCleanedParametersUriRepresentation( return uri.toString(); } - // If the option is set to ignore parameters completely, ignore the query - // completely + // If the option is set to ignore parameters completely, ignore the query completely if (handleParameters.equals(HandleParametersOption.IGNORE_COMPLETELY)) { return createBaseUriWithCleanedPath( uri, handleParameters, handleODataParametersVisited); } - // If the option is set to ignore the value, we get the parameters and we only - // add their + // If the option is set to ignore the value, we get the parameters and we only add their // name to the // query if (handleParameters.equals(HandleParametersOption.IGNORE_VALUE)) { From 41ac265a2bb13de2bbe2528da1dbbb9f59e0a2f9 Mon Sep 17 00:00:00 2001 From: w0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:52:43 -0700 Subject: [PATCH 4/6] Updated openapi.html to indicate XML generation is available but can fail for complex or invalid schemas --- .../zap/extension/openapi/resources/help/contents/openapi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html b/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html index 9dce8c0354c..f31057db672 100644 --- a/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html +++ b/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html @@ -10,7 +10,7 @@

OpenAPI Support

This add-on allows you to spider and import OpenAPI (Swagger) definitions, versions 1.2, 2.0, and 3.0.
-Note: Generation of XML content is currently not supported. +Note: The add-on can generate XML request bodies for XML media types (for example, application/xml, text/xml, and application/*+xml). If an explicit example is provided in the OpenAPI media type that example will be used; otherwise the add-on will attempt to generate a best-effort XML example from the schema. The generator attempts to honour XML-specific metadata (such as xml.name, xml.namespace, xml.prefix, xml.attribute and xml.wrapped), but generation can fail for complex or invalid schemas. If generation fails an error is logged and surfaced and an empty body may be returned — you can also supply a hand-crafted example in the OpenAPI spec or edit the request body manually.

The add-on will automatically detect any OpenAPI definitions and spider them as long as they are in scope.

From ad9ba45d6a4a16c3eeeedd46017f594b6708ce15 Mon Sep 17 00:00:00 2001 From: W0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:30:57 -0700 Subject: [PATCH 5/6] Update addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html Co-authored-by: Rick M Signed-off-by: W0lfbane <3799912+W0lfbane@users.noreply.github.com> --- .../zap/extension/openapi/resources/help/contents/openapi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html b/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html index f31057db672..c0d08c75fe7 100644 --- a/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html +++ b/addOns/openapi/src/main/javahelp/org/zaproxy/zap/extension/openapi/resources/help/contents/openapi.html @@ -10,7 +10,7 @@

OpenAPI Support

This add-on allows you to spider and import OpenAPI (Swagger) definitions, versions 1.2, 2.0, and 3.0.
-Note: The add-on can generate XML request bodies for XML media types (for example, application/xml, text/xml, and application/*+xml). If an explicit example is provided in the OpenAPI media type that example will be used; otherwise the add-on will attempt to generate a best-effort XML example from the schema. The generator attempts to honour XML-specific metadata (such as xml.name, xml.namespace, xml.prefix, xml.attribute and xml.wrapped), but generation can fail for complex or invalid schemas. If generation fails an error is logged and surfaced and an empty body may be returned — you can also supply a hand-crafted example in the OpenAPI spec or edit the request body manually. +Note: The add-on can generate XML request bodies for XML media types (for example, application/xml, text/xml, and application/*+xml). If an explicit example is provided in the OpenAPI media type that example will be used; otherwise the add-on will attempt to generate a best-effort XML example from the schema. The generator attempts to honour XML-specific metadata (such as xml.name, xml.namespace, xml.prefix, xml.attribute, and xml.wrapped). Generation can fail for complex or invalid schemas. If generation fails an error is logged and surfaced, and an empty body may be returned — you can also supply a hand-crafted example in the OpenAPI spec or edit the request body manually.

The add-on will automatically detect any OpenAPI definitions and spider them as long as they are in scope.

From a58923106b7fcc14114afe48574af49a918d600a Mon Sep 17 00:00:00 2001 From: W0lfbane <3799912+W0lfbane@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:50:12 -0800 Subject: [PATCH 6/6] fix merge conflict and update CHANGELOG --- addOns/openapi/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addOns/openapi/CHANGELOG.md b/addOns/openapi/CHANGELOG.md index 78e6604539d..1a07b5a4c11 100644 --- a/addOns/openapi/CHANGELOG.md +++ b/addOns/openapi/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - +- Add XML body generation for OpenAPI plugin ## [47] - 2025-11-04 ### Changed