mraible commented on code in PR #161:
URL: https://github.com/apache/roller/pull/161#discussion_r3780389742


##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReader.java:
##########
@@ -0,0 +1,146 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.Date;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * Parses an incoming AtomPub request body (an atom:entry) into the wire model
+ * using the JDK StAX API. Replaces ROME's Atom parser.
+ *
+ * <p>DTD processing and external entities are disabled to protect against XXE
+ * attacks.
+ */
+public class AtomReader {
+
+    private final XMLInputFactory factory;
+
+    public AtomReader() {
+        factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
Boolean.FALSE);
+    }
+
+    static Date parseDate(String text) {
+        if (text == null || text.isBlank()) {
+            return null;
+        }
+        String trimmed = text.trim();
+        try {
+            return Date.from(OffsetDateTime.parse(trimmed).toInstant());
+        } catch (Exception ignored) {
+            try {
+                return Date.from(Instant.parse(trimmed));
+            } catch (Exception ignored2) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Parse an atom:entry from the given stream. The author is intentionally 
not
+     * read; the server sets the entry's creator from the authenticated user.
+     */
+    public AtomEntry parseEntry(InputStream in) throws AtomException {
+        XMLStreamReader r = null;
+        try {
+            r = factory.createXMLStreamReader(in, "UTF-8");
+            AtomEntry entry = new AtomEntry();
+            while (r.hasNext()) {
+                if (r.next() != XMLStreamConstants.START_ELEMENT) {
+                    continue;
+                }
+                String ns = r.getNamespaceURI();
+                String name = r.getLocalName();
+                if (ATOM_NS.equals(ns)) {
+                    switch (name) {
+                        case "id":
+                            entry.setId(r.getElementText());
+                            break;
+                        case "title":
+                            entry.setTitle(r.getElementText());
+                            break;
+                        case "summary":
+                            entry.setSummary(readContent(r));
+                            break;
+                        case "content":
+                            if (entry.getContent() == null) {
+                                entry.setContent(readContent(r));
+                            }
+                            break;
+                        case "published":
+                            entry.setPublished(parseDate(r.getElementText()));
+                            break;
+                        case "updated":
+                            entry.setUpdated(parseDate(r.getElementText()));
+                            break;
+                        case "category":
+                            entry.getCategories().add(readCategory(r));
+                            break;
+                        default:
+                            break;
+                    }
+                } else if (APP_NS.equals(ns) && "draft".equals(name)) {
+                    String value = r.getElementText();
+                    entry.setDraft(value != null && 
value.trim().equalsIgnoreCase("yes"));
+                }
+            }
+            return entry;
+        } catch (XMLStreamException ex) {
+            throw new AtomException("Error parsing Atom entry", ex);
+        } finally {
+            if (r != null) {
+                try {
+                    r.close();
+                } catch (XMLStreamException ignored) {
+                    // nothing useful to do on close failure
+                }
+            }
+        }
+    }
+
+    private AtomContent readContent(XMLStreamReader r) throws 
XMLStreamException {
+        AtomContent content = new AtomContent();
+        content.setType(r.getAttributeValue(null, "type"));
+        String src = r.getAttributeValue(null, "src");
+        content.setSrc(src);
+        if (src == null) {
+            content.setValue(r.getElementText());

Review Comment:
   getElementText() throws when the element has child elements, so content, 
summary, and title with type="xhtml" (valid per RFC 4287, and accepted by the 
ROME parser this replaces) now fail to parse and the client gets a 500. Clients 
that publish xhtml content can't post at all. This needs a branch that captures 
the child XML as a string when type is xhtml.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomServlet.java:
##########
@@ -0,0 +1,216 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Dispatcher servlet for Roller's Atom Publishing Protocol (RFC 5023)
+ * implementation. Replaces the ROME Propono {@code AtomServlet}: it
+ * authenticates the request, routes by HTTP method and URI shape to
+ * {@link RollerAtomHandler}, and serializes/parses Atom XML via {@link 
AtomWriter}
+ * and {@link AtomReader}. No ROME or Propono types are involved.
+ */
+public class RollerAtomServlet extends HttpServlet {
+
+    private static final Log log =
+            LogFactory.getFactory().getInstance(RollerAtomServlet.class);
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "GET");
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "POST");
+    }
+
+    @Override
+    protected void doPut(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "PUT");
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "DELETE");
+    }
+
+    private void process(HttpServletRequest request, HttpServletResponse 
response, String method)
+            throws IOException {
+
+        RollerAtomHandler handler = new RollerAtomHandler(request, response);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName == null) {
+            // The OAuth path may have already written a challenge/error 
response.
+            if (!response.isCommitted()) {
+                response.setHeader("WWW-Authenticate", "Basic 
realm=\"Roller\"");
+                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
"Authentication error");
+            }
+            return;
+        }
+
+        byte[] body = null;
+        if ("POST".equals(method) || "PUT".equals(method)) {
+            body = readBody(request);
+        }
+        AtomRequest areq = new AtomRequest(request, body);
+
+        try {
+            switch (method) {
+                case "GET":
+                    doGet(handler, areq, response);
+                    break;
+                case "POST":
+                    doPost(handler, areq, response);
+                    break;
+                case "PUT":
+                    doPut(handler, areq, response);
+                    break;
+                case "DELETE":
+                    handler.deleteEntry(areq);
+                    response.setStatus(HttpServletResponse.SC_OK);
+                    break;
+                default:
+                    
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+            }
+        } catch (AtomException ae) {
+            log.debug("Returning error to client: " + ae.getMessage(), ae);
+            if (!response.isCommitted()) {
+                response.sendError(ae.getStatus(), ae.getMessage());
+            }
+        } catch (Exception e) {
+            log.error("Unexpected error handling AtomPub request", e);
+            if (!response.isCommitted()) {
+                
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
e.getMessage());
+            }
+        }
+    }
+
+    private void doGet(RollerAtomHandler handler, AtomRequest areq, 
HttpServletResponse response)
+            throws AtomException, IOException {
+
+        if (handler.isAtomServiceURI(areq)) {
+            AtomServiceDoc service = handler.getAtomService(areq);
+            response.setContentType(AtomConstants.SERVICE_MEDIA_TYPE);
+            new AtomWriter().writeServiceDoc(response.getOutputStream(), 
service);
+
+        } else if (handler.isCollectionURI(areq)) {
+            AtomFeed feed = handler.getCollection(areq);
+            response.setContentType(AtomConstants.FEED_MEDIA_TYPE);
+            new AtomWriter().writeFeed(response.getOutputStream(), feed);
+
+        } else if (handler.isEntryURI(areq)) {
+            AtomEntry entry = handler.getEntry(areq);
+            response.setContentType(AtomConstants.ENTRY_MEDIA_TYPE);
+            new AtomWriter().writeEntry(response.getOutputStream(), entry);
+
+        } else if (handler.isMediaEditURI(areq)) {
+            AtomMediaResource resource = handler.getMediaResource(areq);
+            if (resource.getContentType() != null) {
+                response.setContentType(resource.getContentType());
+            }
+            response.setContentLengthLong(resource.getContentLength());
+            if (resource.getLastModified() != null) {
+                response.setDateHeader("Last-Modified", 
resource.getLastModified().getTime());
+            }
+            try (InputStream in = resource.getInputStream()) {
+                in.transferTo(response.getOutputStream());
+            }
+
+        } else {
+            throw new AtomNotFoundException("Cannot find specified resource");
+        }
+    }
+
+    private void doPost(RollerAtomHandler handler, AtomRequest areq, 
HttpServletResponse response)
+            throws AtomException {
+
+        if (!handler.isCollectionURI(areq)) {
+            throw new AtomNotFoundException("Cannot POST to specified URI");
+        }
+
+        String contentType = areq.getContentType();
+        AtomEntry created;
+        if (contentType != null && 
contentType.startsWith("application/atom+xml")) {
+            AtomEntry entry = new 
AtomReader().parseEntry(areq.getInputStream());
+            created = handler.postEntry(areq, entry);
+        } else {
+            // Media POST: synthesize an entry carrying the request content 
type
+            // and Slug; the binary data is read from the request body.
+            AtomEntry mediaEntry = new AtomEntry();
+            AtomContent content = new AtomContent();
+            content.setType(contentType);
+            mediaEntry.setContent(content);
+            mediaEntry.setTitle(areq.getHeader("Slug"));
+            created = handler.postMedia(areq, mediaEntry);
+        }
+        writeCreated(response, created);
+    }
+
+    private void doPut(RollerAtomHandler handler, AtomRequest areq, 
HttpServletResponse response)
+            throws AtomException {
+
+        if (handler.isEntryURI(areq)) {
+            AtomEntry entry = new 
AtomReader().parseEntry(areq.getInputStream());
+            handler.putEntry(areq, entry);
+            response.setStatus(HttpServletResponse.SC_OK);
+        } else if (handler.isMediaEditURI(areq)) {
+            handler.putMedia(areq);
+            response.setStatus(HttpServletResponse.SC_OK);
+        } else {
+            throw new AtomNotFoundException("Cannot PUT to specified URI");
+        }
+    }
+
+    private void writeCreated(HttpServletResponse response, AtomEntry entry)
+            throws AtomException {
+        String editHref = entry.getLinkHref("edit");
+        if (editHref != null) {
+            response.setHeader("Location", editHref);
+            response.setHeader("Content-Location", editHref);
+        }
+        response.setStatus(HttpServletResponse.SC_CREATED);
+        response.setContentType(AtomConstants.ENTRY_MEDIA_TYPE);
+        try {
+            OutputStream out = response.getOutputStream();
+            new AtomWriter().writeEntry(out, entry);
+        } catch (IOException ioe) {
+            throw new AtomException("Error writing created entry", ioe);
+        }
+    }
+
+    private byte[] readBody(HttpServletRequest request) throws IOException {
+        try (InputStream in = request.getInputStream()) {
+            return in.readAllBytes();

Review Comment:
   readBody() pulls the entire request body into memory with readAllBytes() 
before any size or quota check runs. The Propono flow streamed the body to a 
temp file, so a multi-gigabyte POST to the media collection now turns into an 
OutOfMemoryError instead of a quota rejection, and any authenticated user can 
trigger it. Streaming to a temp file (or bounding the read against the media 
quota first) restores the old behavior.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReader.java:
##########
@@ -0,0 +1,146 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.Date;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * Parses an incoming AtomPub request body (an atom:entry) into the wire model
+ * using the JDK StAX API. Replaces ROME's Atom parser.
+ *
+ * <p>DTD processing and external entities are disabled to protect against XXE
+ * attacks.
+ */
+public class AtomReader {
+
+    private final XMLInputFactory factory;
+
+    public AtomReader() {
+        factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
Boolean.FALSE);
+    }
+
+    static Date parseDate(String text) {
+        if (text == null || text.isBlank()) {
+            return null;
+        }
+        String trimmed = text.trim();
+        try {
+            return Date.from(OffsetDateTime.parse(trimmed).toInstant());
+        } catch (Exception ignored) {
+            try {
+                return Date.from(Instant.parse(trimmed));
+            } catch (Exception ignored2) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Parse an atom:entry from the given stream. The author is intentionally 
not
+     * read; the server sets the entry's creator from the authenticated user.
+     */
+    public AtomEntry parseEntry(InputStream in) throws AtomException {
+        XMLStreamReader r = null;
+        try {
+            r = factory.createXMLStreamReader(in, "UTF-8");
+            AtomEntry entry = new AtomEntry();
+            while (r.hasNext()) {
+                if (r.next() != XMLStreamConstants.START_ELEMENT) {
+                    continue;
+                }
+                String ns = r.getNamespaceURI();

Review Comment:
   parseEntry() matches START_ELEMENTs at any depth, so metadata inside a 
nested atom:source element (RFC 4287 4.2.11) is read as if it belonged to the 
entry: a posted entry that carries <source><title>Other 
Blog</title><updated>2020-01-01...</updated></source> gets stored with the 
source's title and a back-dated timestamp. Silent wrong data, no error. 
Tracking element depth (or skipping the source subtree) avoids it.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomServlet.java:
##########
@@ -0,0 +1,216 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Dispatcher servlet for Roller's Atom Publishing Protocol (RFC 5023)
+ * implementation. Replaces the ROME Propono {@code AtomServlet}: it
+ * authenticates the request, routes by HTTP method and URI shape to
+ * {@link RollerAtomHandler}, and serializes/parses Atom XML via {@link 
AtomWriter}
+ * and {@link AtomReader}. No ROME or Propono types are involved.
+ */
+public class RollerAtomServlet extends HttpServlet {
+
+    private static final Log log =
+            LogFactory.getFactory().getInstance(RollerAtomServlet.class);
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "GET");
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "POST");
+    }
+
+    @Override
+    protected void doPut(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "PUT");
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest request, HttpServletResponse 
response)
+            throws IOException {
+        process(request, response, "DELETE");
+    }
+
+    private void process(HttpServletRequest request, HttpServletResponse 
response, String method)
+            throws IOException {
+
+        RollerAtomHandler handler = new RollerAtomHandler(request, response);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName == null) {
+            // The OAuth path may have already written a challenge/error 
response.
+            if (!response.isCommitted()) {
+                response.setHeader("WWW-Authenticate", "Basic 
realm=\"Roller\"");
+                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
"Authentication error");
+            }
+            return;
+        }
+
+        byte[] body = null;
+        if ("POST".equals(method) || "PUT".equals(method)) {
+            body = readBody(request);
+        }
+        AtomRequest areq = new AtomRequest(request, body);
+
+        try {
+            switch (method) {
+                case "GET":
+                    doGet(handler, areq, response);
+                    break;
+                case "POST":
+                    doPost(handler, areq, response);
+                    break;
+                case "PUT":
+                    doPut(handler, areq, response);
+                    break;
+                case "DELETE":
+                    handler.deleteEntry(areq);
+                    response.setStatus(HttpServletResponse.SC_OK);
+                    break;
+                default:
+                    
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+            }
+        } catch (AtomException ae) {
+            log.debug("Returning error to client: " + ae.getMessage(), ae);
+            if (!response.isCommitted()) {
+                response.sendError(ae.getStatus(), ae.getMessage());
+            }
+        } catch (Exception e) {
+            log.error("Unexpected error handling AtomPub request", e);
+            if (!response.isCommitted()) {
+                
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
e.getMessage());
+            }
+        }
+    }
+
+    private void doGet(RollerAtomHandler handler, AtomRequest areq, 
HttpServletResponse response)
+            throws AtomException, IOException {
+
+        if (handler.isAtomServiceURI(areq)) {
+            AtomServiceDoc service = handler.getAtomService(areq);
+            response.setContentType(AtomConstants.SERVICE_MEDIA_TYPE);
+            new AtomWriter().writeServiceDoc(response.getOutputStream(), 
service);
+
+        } else if (handler.isCollectionURI(areq)) {
+            AtomFeed feed = handler.getCollection(areq);
+            response.setContentType(AtomConstants.FEED_MEDIA_TYPE);
+            new AtomWriter().writeFeed(response.getOutputStream(), feed);
+
+        } else if (handler.isEntryURI(areq)) {
+            AtomEntry entry = handler.getEntry(areq);
+            response.setContentType(AtomConstants.ENTRY_MEDIA_TYPE);
+            new AtomWriter().writeEntry(response.getOutputStream(), entry);
+
+        } else if (handler.isMediaEditURI(areq)) {
+            AtomMediaResource resource = handler.getMediaResource(areq);
+            if (resource.getContentType() != null) {
+                response.setContentType(resource.getContentType());
+            }
+            response.setContentLengthLong(resource.getContentLength());
+            if (resource.getLastModified() != null) {
+                response.setDateHeader("Last-Modified", 
resource.getLastModified().getTime());
+            }
+            try (InputStream in = resource.getInputStream()) {
+                in.transferTo(response.getOutputStream());
+            }
+
+        } else {
+            throw new AtomNotFoundException("Cannot find specified resource");
+        }
+    }
+
+    private void doPost(RollerAtomHandler handler, AtomRequest areq, 
HttpServletResponse response)
+            throws AtomException {
+
+        if (!handler.isCollectionURI(areq)) {
+            throw new AtomNotFoundException("Cannot POST to specified URI");
+        }
+
+        String contentType = areq.getContentType();
+        AtomEntry created;
+        if (contentType != null && 
contentType.startsWith("application/atom+xml")) {

Review Comment:
   A POST with no Content-Type header falls into the media branch with a null 
type and NPEs further down (Utilities.replaceNonAlphanumeric when Slug is also 
absent), surfacing as a 500. Propono answered this with a clear "No 
content-type specified in request" client error. A null check here that returns 
415 keeps a malformed request from looking like a server bug. Fun fact: the 
Propono servlet had the inverse bug, an NPE on the exact mapping with no path 
info, which we fixed in #154.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java:
##########
@@ -420,15 +406,13 @@ public void putMedia(AtomRequest areq) throws 
AtomException {
                 }
             }
             throw new AtomException("Incorrect path information");
-        
+
         } catch (WebloggerException re) {
             throw new AtomException("Posting media");
-        } catch (IOException ioe) {
-            throw new AtomException("Posting media", ioe);
         }
     }
-    
-    
+
+
     public void deleteEntry(AtomRequest areq) throws AtomException {
         try {
             String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");

Review Comment:
   In this method (lines 424-429, unchanged but carried forward): fileName 
strips the .media-link suffix but the lookup still passes the unstripped path 
to getMediaFileByPath, so it never finds the file, mf is null, and 
removeMediaFile NPEs. DELETE on the rel="edit" URI the server itself advertises 
always returns 500, which means media can never be deleted through AtomPub. 
Passing the stripped name to the lookup fixes it, and a delete test would keep 
it fixed.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandler.java:
##########
@@ -120,10 +112,6 @@ public RollerAtomHandler(HttpServletRequest request, 
HttpServletResponse respons
         String userName;
         if 
("oauth".equals(WebloggerRuntimeConfig.getProperty("webservices.atomPubAuth"))) 
{

Review Comment:
   webservices.atomPubAuth is a runtime property persisted in the 
roller_properties table, and existing installs can have wsse stored there. With 
the wsse branch gone, that value is silently reinterpreted as Basic, so clients 
sending X-WSSE headers get 401s with nothing in the logs pointing at the 
removed option. We hit the identical trap in #154 with the removed oauth value; 
the fix there was to refuse authentication for unrecognized values and log an 
error naming the property and the valid options. Worth deciding together 
whether wsse stays (it survives in #154) or goes, since the globalConfig text 
and ROL-2183 currently say "basic or wsse".



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java:
##########
@@ -216,13 +201,14 @@ public AtomMediaResource getMediaResource(AtomRequest 
areq) throws AtomException
                 throw new AtomNotAuthorizedException("Not authorized to edit 
weblog: " + handle);
             }
             if (pathInfo.length > 1) {
-                try {                                        
+                try {
                     // Parse pathinfo to determine file path
                     String filePath = filePathFromPathInfo(pathInfo);
                     MediaFile mf = fmgr.getMediaFileByOriginalPath(website, 
filePath);

Review Comment:
   getMediaResource() doesn't null-check the getMediaFileByOriginalPath result, 
so a GET for a deleted or misspelled resource NPEs and returns 500 with the 
message "Unexpected error during file upload" for what should be a plain 404. 
Carried forward from the old code, but this rewrite is the right moment to fix 
it.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomConstants.java:
##########
@@ -0,0 +1,42 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * Constants shared by the StAX-based AtomPub implementation.
+ */
+public final class AtomConstants {
+
+    private AtomConstants() {
+    }
+
+    /** Atom Syndication Format namespace (RFC 4287). */
+    public static final String ATOM_NS = "http://www.w3.org/2005/Atom";;
+
+    /** Atom Publishing Protocol namespace (RFC 5023). */
+    public static final String APP_NS = "http://www.w3.org/2007/app";;
+
+    /** Media type for an Atom entry. */
+    public static final String ENTRY_MEDIA_TYPE = 
"application/atom+xml;type=entry";

Review Comment:
   Lower confidence than the rest, but flagging: ENTRY_MEDIA_TYPE has no 
charset parameter while FEED_MEDIA_TYPE and SERVICE_MEDIA_TYPE both declare 
charset=utf-8, and Propono declared it on entry responses too. Clients that 
fall back to ISO-8859-1 when charset is absent will mojibake non-ASCII titles 
on GET entry and 201 responses.



##########
app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriter.java:
##########
@@ -0,0 +1,235 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static 
org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+
+import java.io.OutputStream;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+/**
+ * Serializes the AtomPub wire model ({@link AtomEntry}, {@link AtomFeed},
+ * {@link AtomServiceDoc}) to XML using the JDK StAX API. Replaces the ROME and
+ * Propono serialization the AtomPub server previously relied on.
+ *
+ * <p>Atom entries and feeds are written with the Atom namespace as the default
+ * namespace (so atom elements are unprefixed) and the APP namespace bound to 
the
+ * {@code app} prefix. Service documents use the reverse: APP as the default
+ * namespace and {@code atom} for atom elements.
+ */
+public class AtomWriter {
+
+    private static final DateTimeFormatter RFC3339 =
+            
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
+
+    private final XMLOutputFactory factory = XMLOutputFactory.newInstance();
+
+    static String formatDate(Date date) {
+        return date == null ? null : RFC3339.format(date.toInstant());
+    }
+
+    public void writeEntry(OutputStream out, AtomEntry entry) throws 
AtomException {
+        try {
+            XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+            w.writeStartDocument("UTF-8", "1.0");
+            w.setDefaultNamespace(ATOM_NS);
+            w.setPrefix("app", APP_NS);
+            w.writeStartElement(ATOM_NS, "entry");
+            w.writeDefaultNamespace(ATOM_NS);
+            w.writeNamespace("app", APP_NS);
+            writeEntryBody(w, entry);
+            w.writeEndElement();
+            w.writeEndDocument();
+            w.flush();
+            w.close();
+        } catch (XMLStreamException ex) {
+            throw new AtomException("Error serializing Atom entry", ex);
+        }
+    }
+
+    public void writeFeed(OutputStream out, AtomFeed feed) throws 
AtomException {
+        try {
+            XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+            w.writeStartDocument("UTF-8", "1.0");
+            w.setDefaultNamespace(ATOM_NS);
+            w.setPrefix("app", APP_NS);
+            w.writeStartElement(ATOM_NS, "feed");
+            w.writeDefaultNamespace(ATOM_NS);
+            w.writeNamespace("app", APP_NS);
+            writeAtomText(w, "id", feed.getId());
+            writeAtomText(w, "title", feed.getTitle());
+            writeAtomText(w, "updated", formatDate(feed.getUpdated()));
+            for (AtomLink link : feed.getLinks()) {
+                writeLink(w, link);
+            }
+            for (AtomEntry entry : feed.getEntries()) {
+                w.writeStartElement(ATOM_NS, "entry");
+                writeEntryBody(w, entry);
+                w.writeEndElement();
+            }
+            w.writeEndElement();
+            w.writeEndDocument();
+            w.flush();
+            w.close();
+        } catch (XMLStreamException ex) {
+            throw new AtomException("Error serializing Atom feed", ex);
+        }
+    }
+
+    public void writeServiceDoc(OutputStream out, AtomServiceDoc service) 
throws AtomException {
+        try {
+            XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+            w.writeStartDocument("UTF-8", "1.0");
+            w.setDefaultNamespace(APP_NS);
+            w.setPrefix("atom", ATOM_NS);
+            w.writeStartElement(APP_NS, "service");
+            w.writeDefaultNamespace(APP_NS);
+            w.writeNamespace("atom", ATOM_NS);
+            for (AtomWorkspace workspace : service.getWorkspaces()) {
+                w.writeStartElement(APP_NS, "workspace");
+                writeAtomText(w, "title", workspace.getTitle());
+                for (AtomCollection collection : workspace.getCollections()) {
+                    w.writeStartElement(APP_NS, "collection");
+                    if (collection.getHref() != null) {
+                        w.writeAttribute("href", collection.getHref());
+                    }
+                    writeAtomText(w, "title", collection.getTitle());
+                    for (String accept : collection.getAccepts()) {
+                        w.writeStartElement(APP_NS, "accept");
+                        w.writeCharacters(accept);
+                        w.writeEndElement();
+                    }
+                    for (AtomCategories cats : collection.getCategories()) {
+                        w.writeStartElement(APP_NS, "categories");
+                        w.writeAttribute("fixed", cats.isFixed() ? "yes" : 
"no");
+                        if (cats.getScheme() != null) {
+                            w.writeAttribute("scheme", cats.getScheme());
+                        }
+                        for (AtomCategory cat : cats.getCategories()) {
+                            writeCategory(w, cat);
+                        }
+                        w.writeEndElement();
+                    }
+                    w.writeEndElement();
+                }
+                w.writeEndElement();
+            }
+            w.writeEndElement();
+            w.writeEndDocument();
+            w.flush();
+            w.close();
+        } catch (XMLStreamException ex) {
+            throw new AtomException("Error serializing service document", ex);
+        }
+    }
+
+    private void writeEntryBody(XMLStreamWriter w, AtomEntry entry) throws 
XMLStreamException {
+        writeAtomText(w, "id", entry.getId());
+        writeAtomText(w, "title", entry.getTitle());
+        writeAtomText(w, "published", formatDate(entry.getPublished()));
+        writeAtomText(w, "updated", formatDate(entry.getUpdated()));
+        for (AtomPerson author : entry.getAuthors()) {
+            w.writeStartElement(ATOM_NS, "author");
+            writeAtomText(w, "name", author.getName());
+            writeAtomText(w, "email", author.getEmail());
+            w.writeEndElement();
+        }
+        for (AtomCategory cat : entry.getCategories()) {
+            writeCategory(w, cat);
+        }
+        if (entry.getSummary() != null) {
+            writeContent(w, "summary", entry.getSummary());
+        }
+        if (entry.getContent() != null) {
+            writeContent(w, "content", entry.getContent());
+        }
+        for (AtomLink link : entry.getLinks()) {
+            writeLink(w, link);
+        }
+        // APP control extension (RFC 5023)
+        w.writeStartElement(APP_NS, "control");
+        w.writeStartElement(APP_NS, "draft");
+        w.writeCharacters(entry.isDraft() ? "yes" : "no");
+        w.writeEndElement();
+        if (entry.getEdited() != null) {
+            w.writeStartElement(APP_NS, "edited");

Review Comment:
   app:edited is written inside app:control, but RFC 5023 10.2 defines it as a 
direct child of atom:entry, and the Propono generator wrote it on the entry 
root (checked against rome-propono 1.19.0). Conforming clients looking for the 
edited timestamp in the spec location now see it as missing.



##########
app/src/main/resources/propono.properties:
##########
@@ -1,2 +0,0 @@
-com.rometools.propono.atom.server.AtomHandlerFactory=\

Review Comment:
   Deleting this and RollerAtomHandlerFactory removes the pluggable 
AtomHandlerFactory extension point, and the servlet now hard-codes new 
RollerAtomHandler(...). Any deployment that overrode the factory (custom auth 
is the classic case) silently loses its handler on upgrade. If dropping the 
extension point is intentional, a release-note line would spare those users a 
debugging session.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to