From c8369e11b64c1f77ff822e1075cecb72e0a27f05 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sat, 29 Aug 2026 20:35:37 -0400 Subject: [PATCH 1/2] Store enclosure metadata without fetching remote URLs --- .../ui/struts2/editor/EntryBean.java | 22 +++++ .../ui/struts2/editor/EntryEdit.java | 44 +++++----- .../weblogger/util/EnclosureMetadata.java | 84 ++++++++++++++++++ .../weblogger/util/MediacastException.java | 62 ------------- .../weblogger/util/MediacastResource.java | 78 ---------------- .../roller/weblogger/util/MediacastUtil.java | 88 ------------------- .../resources/ApplicationResources.properties | 13 ++- .../webapp/WEB-INF/jsps/editor/EntryEdit.jsp | 13 +-- .../jsps/editor/MediaFileAddSuccess.jsp | 12 ++- .../weblogger/util/EnclosureMetadataTest.java | 58 ++++++++++++ 10 files changed, 205 insertions(+), 269 deletions(-) create mode 100644 app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java delete mode 100644 app/src/main/java/org/apache/roller/weblogger/util/MediacastException.java delete mode 100644 app/src/main/java/org/apache/roller/weblogger/util/MediacastResource.java delete mode 100644 app/src/main/java/org/apache/roller/weblogger/util/MediacastUtil.java create mode 100644 app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryBean.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryBean.java index bbc0eab7ea..949fbafa09 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryBean.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryBean.java @@ -68,6 +68,8 @@ public class EntryBean { private boolean rightToLeft = false; private boolean pinnedToMain = false; private String enclosureURL = null; + private String enclosureType = null; + private String enclosureLength = null; private String searchDescription = null; private int commentCount = 0; @@ -227,6 +229,22 @@ public String getEnclosureURL() { public void setEnclosureURL(String enclosureUrl) { this.enclosureURL = enclosureUrl; } + + public String getEnclosureType() { + return enclosureType; + } + + public void setEnclosureType(String enclosureType) { + this.enclosureType = enclosureType; + } + + public String getEnclosureLength() { + return enclosureLength; + } + + public void setEnclosureLength(String enclosureLength) { + this.enclosureLength = enclosureLength; + } public String getSearchDescription() { return searchDescription; @@ -390,6 +408,10 @@ public void copyFrom(WeblogEntry entry, Locale locale) { for (WeblogEntryAttribute attr : attrs) { if ("att_mediacast_url".equals(attr.getName())) { setEnclosureURL(attr.getValue()); + } else if ("att_mediacast_type".equals(attr.getName())) { + setEnclosureType(attr.getValue()); + } else if ("att_mediacast_length".equals(attr.getName())) { + setEnclosureLength(attr.getValue()); } } } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java index a12dc18fff..aee5b10bd1 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java @@ -48,15 +48,13 @@ import org.apache.roller.weblogger.ui.core.plugins.UIPluginManager; import org.apache.roller.weblogger.ui.core.plugins.WeblogEntryEditor; import org.apache.roller.weblogger.ui.struts2.util.UIAction; -import org.apache.roller.weblogger.util.cache.CacheManager; +import org.apache.roller.weblogger.util.EnclosureMetadata; import org.apache.roller.weblogger.util.MailUtil; -import org.apache.roller.weblogger.util.MediacastException; -import org.apache.roller.weblogger.util.MediacastResource; -import org.apache.roller.weblogger.util.MediacastUtil; import org.apache.roller.weblogger.util.RollerMessages; import org.apache.roller.weblogger.util.RollerMessages.RollerMessage; import org.apache.roller.weblogger.util.Trackback; import org.apache.roller.weblogger.util.TrackbackNotAllowedException; +import org.apache.roller.weblogger.util.cache.CacheManager; import org.apache.struts2.convention.annotation.AllowedMethods; import org.apache.struts2.interceptor.validation.SkipValidation; @@ -192,6 +190,19 @@ public String publish() { */ private String save() { if (!hasActionErrors()) { + EnclosureMetadata enclosure = null; + if (!StringUtils.isEmpty(getBean().getEnclosureURL())) { + try { + enclosure = EnclosureMetadata.of( + getBean().getEnclosureURL(), + getBean().getEnclosureType(), + getBean().getEnclosureLength()); + } catch (IllegalArgumentException e) { + addError("weblogEdit.enclosureMetadataInvalid"); + return INPUT; + } + } + try { WeblogEntryManager weblogEntryManager = WebloggerFactory.getWeblogger() .getWeblogEntryManager(); @@ -223,24 +234,13 @@ private String save() { weblogEntry.setPinnedToMain(getBean().getPinnedToMain()); } - if (!StringUtils.isEmpty(getBean().getEnclosureURL())) { - try { - // Fetch MediaCast resource - log.debug("Checking MediaCast attributes"); - MediacastResource mediacast = MediacastUtil - .lookupResource(getBean().getEnclosureURL()); - - // set mediacast attributes - weblogEntry.putEntryAttribute("att_mediacast_url", - mediacast.getUrl()); - weblogEntry.putEntryAttribute("att_mediacast_type", - mediacast.getContentType()); - weblogEntry.putEntryAttribute("att_mediacast_length", "" - + mediacast.getLength()); - - } catch (MediacastException ex) { - addMessage(getText(ex.getErrorKey())); - } + if (enclosure != null) { + weblogEntry.putEntryAttribute("att_mediacast_url", + enclosure.getUrl()); + weblogEntry.putEntryAttribute("att_mediacast_type", + enclosure.getContentType()); + weblogEntry.putEntryAttribute("att_mediacast_length", + enclosure.getLength()); } else if ("entryEdit".equals(actionName)) { try { // if MediaCast string is empty, clean out MediaCast diff --git a/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java b/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java new file mode 100644 index 0000000000..43ac43e0ed --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * 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. + */ +package org.apache.roller.weblogger.util; + +import java.util.regex.Pattern; +import org.apache.commons.validator.routines.UrlValidator; + +/** + * Validated metadata for an RSS or Atom enclosure. + */ +public final class EnclosureMetadata { + + private static final UrlValidator URL_VALIDATOR = new UrlValidator( + new String[] {"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS); + + private static final Pattern MEDIA_TYPE = Pattern.compile( + "[!#$%&'*+.^_`|~0-9A-Za-z-]+/[!#$%&'*+.^_`|~0-9A-Za-z-]+"); + + private final String url; + private final String contentType; + private final String length; + + private EnclosureMetadata(String url, String contentType, String length) { + this.url = url; + this.contentType = contentType; + this.length = length; + } + + public static EnclosureMetadata of(String url, String contentType, String length) { + String normalizedUrl = normalize(url); + String normalizedType = normalize(contentType); + String normalizedLength = normalize(length); + + if (!URL_VALIDATOR.isValid(normalizedUrl)) { + throw new IllegalArgumentException("Enclosure URL must be an absolute HTTP or HTTPS URL"); + } + if (!MEDIA_TYPE.matcher(normalizedType).matches()) { + throw new IllegalArgumentException("Enclosure type must be a valid media type"); + } + + final long byteLength; + try { + byteLength = Long.parseLong(normalizedLength); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Enclosure length must be a non-negative integer", e); + } + if (byteLength < 0) { + throw new IllegalArgumentException("Enclosure length must be a non-negative integer"); + } + + return new EnclosureMetadata( + normalizedUrl, normalizedType, Long.toString(byteLength)); + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + public String getUrl() { + return url; + } + + public String getContentType() { + return contentType; + } + + public String getLength() { + return length; + } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/util/MediacastException.java b/app/src/main/java/org/apache/roller/weblogger/util/MediacastException.java deleted file mode 100644 index b9621ccb69..0000000000 --- a/app/src/main/java/org/apache/roller/weblogger/util/MediacastException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.util; - -import org.apache.roller.weblogger.WebloggerException; - - -/** - * An exception thrown when dealing with Mediacast files. - */ -public class MediacastException extends WebloggerException { - - private int errorCode = 0; - private String errorKey = null; - - - public MediacastException(int code, String msgKey) { - this.errorCode = code; - this.errorKey = msgKey; - } - - - public MediacastException(int code, String msgKey, Throwable t) { - super(t); - this.errorCode = code; - this.errorKey = msgKey; - } - - - public int getErrorCode() { - return errorCode; - } - - public String getErrorKey() { - return errorKey; - } - - public void setErrorCode(int errorCode) { - this.errorCode = errorCode; - } - - public void setErrorKey(String errorKey) { - this.errorKey = errorKey; - } - -} diff --git a/app/src/main/java/org/apache/roller/weblogger/util/MediacastResource.java b/app/src/main/java/org/apache/roller/weblogger/util/MediacastResource.java deleted file mode 100644 index 6b649dfaa7..0000000000 --- a/app/src/main/java/org/apache/roller/weblogger/util/MediacastResource.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.util; - - -/** - * An external 'mediacast' resource, typically a podcast, video, etc. - * - * This class is mainly used by weblog entries to track external resources used - * in postings via enclosures. - */ -public class MediacastResource { - - private String url = null; - private String contentType = null; - private long length = 0; - - - public MediacastResource(String u, String c, long l) { - this.setUrl(u); - this.setContentType(c); - this.setLength(l); - } - - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getContentType() { - return contentType; - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public long getLength() { - return length; - } - - public void setLength(long length) { - this.length = length; - } - - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(); - - buf.append("url = ").append(getUrl()).append("\n"); - buf.append("contentType = ").append(getContentType()).append("\n"); - buf.append("length = ").append(getLength()).append("\n"); - - return buf.toString(); - } - -} diff --git a/app/src/main/java/org/apache/roller/weblogger/util/MediacastUtil.java b/app/src/main/java/org/apache/roller/weblogger/util/MediacastUtil.java deleted file mode 100644 index 35df6e41e4..0000000000 --- a/app/src/main/java/org/apache/roller/weblogger/util/MediacastUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.util; - -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - - -/** - * Utility for deailing with mediacast files. - */ -public final class MediacastUtil { - - private static final Log LOG = LogFactory.getLog(MediacastUtil.class); - - public static final int BAD_URL = 1; - public static final int CHECK_FAILED = 2; - public static final int BAD_RESPONSE = 3; - public static final int INCOMPLETE = 4; - - - // non-instantiable - private MediacastUtil() {} - - - /** - * Validate a Mediacast resource. - */ - public static MediacastResource lookupResource(String url) - throws MediacastException { - - if(url == null || url.isBlank()) { - return null; - } - - MediacastResource resource = null; - try { - HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - con.setRequestMethod("HEAD"); - int response = con.getResponseCode(); - String message = con.getResponseMessage(); - - if(response != 200) { - LOG.debug("Mediacast error " + response + ":" + message + " from url " + url); - throw new MediacastException(BAD_RESPONSE, "weblogEdit.mediaCastResponseError"); - } else { - String contentType = con.getContentType(); - long length = con.getContentLength(); - - if(contentType == null || length == -1) { - LOG.debug("Response valid, but contentType or length is invalid"); - throw new MediacastException(INCOMPLETE, "weblogEdit.mediaCastLacksContentTypeOrLength"); - } - - resource = new MediacastResource(url, contentType, length); - LOG.debug("Valid mediacast resource = " + resource.toString()); - - } - } catch (MalformedURLException mfue) { - LOG.debug("Malformed MediaCast url: " + url); - throw new MediacastException(BAD_URL, "weblogEdit.mediaCastUrlMalformed", mfue); - } catch (Exception e) { - LOG.error("ERROR while checking MediaCast URL: " + url + ": " + e.getMessage()); - throw new MediacastException(CHECK_FAILED, "weblogEdit.mediaCastFailedFetchingInfo", e); - } - return resource; - } - -} diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties index 66072c23f0..cc0b9679fb 100644 --- a/app/src/main/resources/ApplicationResources.properties +++ b/app/src/main/resources/ApplicationResources.properties @@ -1606,13 +1606,12 @@ weblogEdit.trackbackError404=Trackback failed, could not reach trackback URL. A weblogEdit.hasComments=Comments [{1}] weblogEdit.enclosureURL=Enclosure URL -weblogEdit.enclosureURL.tooltip=Podcast or other multimedia URL to embed within the RSS & Atom feeds for this blog entry. -weblogEdit.enclosureType=Type -weblogEdit.enclosureLength=Length -weblogEdit.mediaCastFailedFetchingInfo=Unable to reach the enclosure. Check the hostname in the URL. -weblogEdit.mediaCastUrlMalformed=The enclosure URL was malformed. -weblogEdit.mediaCastResponseError=The enclosure server returned an error. Do you have the right URL? -weblogEdit.mediaCastLacksContentTypeOrLength=Unable to use enclosure URL. Server provided no content type or no length. +weblogEdit.enclosureURL.tooltip=Absolute HTTP or HTTPS URL to embed within the RSS & Atom feeds for this blog entry. +weblogEdit.enclosureType=Enclosure media type +weblogEdit.enclosureType.tooltip=Media type supplied for the enclosure, for example audio/mpeg. +weblogEdit.enclosureLength=Enclosure length +weblogEdit.enclosureLength.tooltip=Non-negative enclosure size in bytes. +weblogEdit.enclosureMetadataInvalid=Enter an absolute HTTP or HTTPS enclosure URL, a valid media type, and a non-negative byte length. weblogEdit.mediaCastErrorRemoving=Error removing MediaCast from weblog entry diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp index 61995356ad..d7c194de28 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp @@ -238,15 +238,10 @@ - - - - : - - : - - - + + diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp index c3cbae0f22..3f9aa71e02 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp @@ -25,6 +25,8 @@ + +

@@ -96,7 +98,9 @@
')"/> + onchange="setEnclosure('', + '', + '')"/>
@@ -132,7 +136,7 @@
- +
@@ -201,8 +205,10 @@ return false; } - function setEnclosure(url) { + function setEnclosure(url, type, length) { $("#enclosureURL").get(0).value = url; + $("#enclosureType").get(0).value = type; + $("#enclosureLength").get(0).value = length; if (isImageChecked()) { $("#submit").attr("disabled", false); return; diff --git a/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java b/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java new file mode 100644 index 0000000000..24a09a3505 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * 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. + */ +package org.apache.roller.weblogger.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class EnclosureMetadataTest { + + @Test + void acceptsHttpMetadataWithoutOpeningTheResource() { + EnclosureMetadata metadata = EnclosureMetadata.of( + "http://127.0.0.1:9/audio.ogg", "audio/ogg", "1234"); + + assertEquals("http://127.0.0.1:9/audio.ogg", metadata.getUrl()); + assertEquals("audio/ogg", metadata.getContentType()); + assertEquals("1234", metadata.getLength()); + } + + @Test + void acceptsHttpsAndTrimsMetadata() { + EnclosureMetadata metadata = EnclosureMetadata.of( + " https://media.example.org/show.mp3 ", + " audio/mpeg ", " 3141592654 "); + + assertEquals("https://media.example.org/show.mp3", metadata.getUrl()); + assertEquals("audio/mpeg", metadata.getContentType()); + assertEquals("3141592654", metadata.getLength()); + } + + @Test + void rejectsUnsupportedOrIncompleteMetadata() { + assertThrows(IllegalArgumentException.class, + () -> EnclosureMetadata.of("file:///tmp/audio.ogg", "audio/ogg", "12")); + assertThrows(IllegalArgumentException.class, + () -> EnclosureMetadata.of("https://example.org/audio", "not-a-type", "12")); + assertThrows(IllegalArgumentException.class, + () -> EnclosureMetadata.of("https://example.org/audio", "audio/ogg", "-1")); + assertThrows(IllegalArgumentException.class, + () -> EnclosureMetadata.of("https://example.org/audio", "audio/ogg", "unknown")); + } +} From b3ad80678c04e7057748a1e3a53ceb1ff7c1d228 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Wed, 2 Sep 2026 11:52:56 -0400 Subject: [PATCH 2/2] Preserve enclosure compatibility during local validation --- .../ui/struts2/editor/EntryEdit.java | 66 ++++++++++--- .../weblogger/util/EnclosureMetadata.java | 90 ++++++++++++++++-- .../resources/ApplicationResources.properties | 5 +- .../ApplicationResources_de.properties | 4 - .../ApplicationResources_es.properties | 4 - .../ApplicationResources_fr.properties | 4 - .../ApplicationResources_ja.properties | 6 +- .../ApplicationResources_ko.properties | 6 +- .../ApplicationResources_ru.properties | 4 - .../ApplicationResources_zh_CN.properties | 7 +- .../jsps/editor/MediaFileAddSuccess.jsp | 18 +++- app/src/main/webapp/WEB-INF/velocity/feeds.vm | 6 +- .../editor/EntryEditEnclosureTest.java | 82 ++++++++++++++++ .../weblogger/util/EnclosureMetadataTest.java | 93 +++++++++++++++++-- 14 files changed, 324 insertions(+), 71 deletions(-) create mode 100644 app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEditEnclosureTest.java diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java index aee5b10bd1..3951f5ce61 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java @@ -188,19 +188,11 @@ public String publish() { * * @return String The result of the action. */ - private String save() { + String save() { if (!hasActionErrors()) { - EnclosureMetadata enclosure = null; - if (!StringUtils.isEmpty(getBean().getEnclosureURL())) { - try { - enclosure = EnclosureMetadata.of( - getBean().getEnclosureURL(), - getBean().getEnclosureType(), - getBean().getEnclosureLength()); - } catch (IllegalArgumentException e) { - addError("weblogEdit.enclosureMetadataInvalid"); - return INPUT; - } + EnclosureMetadata enclosure = validateEnclosure(); + if (hasActionErrors()) { + return failedSave(); } try { @@ -304,8 +296,56 @@ private String save() { addError("generic.error.check.logs"); } } + return failedSave(); + } + + EnclosureMetadata validateEnclosure() { + if (StringUtils.isEmpty(getBean().getEnclosureURL())) { + return null; + } + try { + return EnclosureMetadata.of( + getBean().getEnclosureURL(), + getBean().getEnclosureType(), + getBean().getEnclosureLength()); + } catch (EnclosureMetadata.ValidationException invalid) { + if (submittedEnclosureMatchesStored()) { + getBean().setEnclosureURL(null); + getBean().setEnclosureType(null); + getBean().setEnclosureLength(null); + addMessage("weblogEdit.enclosureMetadataRemoved"); + } else { + switch (invalid.getField()) { + case URL: + addError("weblogEdit.enclosureURLInvalid"); + break; + case TYPE: + addError("weblogEdit.enclosureTypeInvalid"); + break; + case LENGTH: + addError("weblogEdit.enclosureLengthInvalid"); + break; + default: + throw invalid; + } + } + return null; + } + } + + private boolean submittedEnclosureMatchesStored() { + return "entryEdit".equals(actionName) && getEntry() != null + && StringUtils.equals(getBean().getEnclosureURL(), + getEntry().findEntryAttribute("att_mediacast_url")) + && StringUtils.equals(getBean().getEnclosureType(), + getEntry().findEntryAttribute("att_mediacast_type")) + && StringUtils.equals(getBean().getEnclosureLength(), + getEntry().findEntryAttribute("att_mediacast_length")); + } + + private String failedSave() { if ("entryAdd".equals(actionName)) { - // if here on entryAdd, nothing saved, so reset status to null (unsaved) + // If here on entryAdd, nothing saved, so reset status to null (unsaved). getBean().setStatus(null); } return INPUT; diff --git a/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java b/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java index 43ac43e0ed..8e1dbc1902 100644 --- a/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java +++ b/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java @@ -16,17 +16,17 @@ */ package org.apache.roller.weblogger.util; +import java.net.IDN; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; import java.util.regex.Pattern; -import org.apache.commons.validator.routines.UrlValidator; /** * Validated metadata for an RSS or Atom enclosure. */ public final class EnclosureMetadata { - private static final UrlValidator URL_VALIDATOR = new UrlValidator( - new String[] {"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS); - private static final Pattern MEDIA_TYPE = Pattern.compile( "[!#$%&'*+.^_`|~0-9A-Za-z-]+/[!#$%&'*+.^_`|~0-9A-Za-z-]+"); @@ -42,24 +42,28 @@ private EnclosureMetadata(String url, String contentType, String length) { public static EnclosureMetadata of(String url, String contentType, String length) { String normalizedUrl = normalize(url); - String normalizedType = normalize(contentType); + String normalizedType = normalizeContentType(contentType); String normalizedLength = normalize(length); - if (!URL_VALIDATOR.isValid(normalizedUrl)) { - throw new IllegalArgumentException("Enclosure URL must be an absolute HTTP or HTTPS URL"); + if (!isHttpUri(normalizedUrl)) { + throw new ValidationException(Field.URL, + "Enclosure URL must be an absolute HTTP or HTTPS URL"); } if (!MEDIA_TYPE.matcher(normalizedType).matches()) { - throw new IllegalArgumentException("Enclosure type must be a valid media type"); + throw new ValidationException(Field.TYPE, + "Enclosure type must be a valid media type"); } final long byteLength; try { byteLength = Long.parseLong(normalizedLength); } catch (NumberFormatException e) { - throw new IllegalArgumentException("Enclosure length must be a non-negative integer", e); + throw new ValidationException(Field.LENGTH, + "Enclosure length must be a non-negative integer", e); } if (byteLength < 0) { - throw new IllegalArgumentException("Enclosure length must be a non-negative integer"); + throw new ValidationException(Field.LENGTH, + "Enclosure length must be a non-negative integer"); } return new EnclosureMetadata( @@ -70,6 +74,72 @@ private static String normalize(String value) { return value == null ? "" : value.trim(); } + private static String normalizeContentType(String value) { + String type = normalize(value); + int parameter = type.indexOf(';'); + if (parameter >= 0) { + type = type.substring(0, parameter).trim(); + } + return type.toLowerCase(Locale.ENGLISH); + } + + private static boolean isHttpUri(String value) { + try { + URI uri = new URI(value); + String scheme = uri.getScheme(); + if (!("http".equalsIgnoreCase(scheme) + || "https".equalsIgnoreCase(scheme))) { + return false; + } + if (uri.getRawAuthority() == null || uri.getRawAuthority().isEmpty()) { + return false; + } + if (uri.getHost() != null && !uri.getHost().isEmpty()) { + return uri.getPort() <= 65535; + } + + // URI.getHost() is null for a Unicode authority. Validate its host + // locally after converting it to ASCII; this performs no DNS or I/O. + String authority = uri.getRawAuthority(); + int userInfo = authority.lastIndexOf('@'); + String hostAndPort = userInfo >= 0 + ? authority.substring(userInfo + 1) : authority; + int colon = hostAndPort.lastIndexOf(':'); + String host = colon >= 0 ? hostAndPort.substring(0, colon) : hostAndPort; + if (colon >= 0) { + int port = Integer.parseInt(hostAndPort.substring(colon + 1)); + if (port > 65535) { + return false; + } + } + return !IDN.toASCII(host).isEmpty(); + } catch (IllegalArgumentException | URISyntaxException invalid) { + return false; + } + } + + public enum Field { + URL, TYPE, LENGTH + } + + public static final class ValidationException extends IllegalArgumentException { + private final Field field; + + private ValidationException(Field field, String message) { + super(message); + this.field = field; + } + + private ValidationException(Field field, String message, Throwable cause) { + super(message, cause); + this.field = field; + } + + public Field getField() { + return field; + } + } + public String getUrl() { return url; } diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties index cc0b9679fb..d11af49443 100644 --- a/app/src/main/resources/ApplicationResources.properties +++ b/app/src/main/resources/ApplicationResources.properties @@ -1611,7 +1611,10 @@ weblogEdit.enclosureType=Enclosure media type weblogEdit.enclosureType.tooltip=Media type supplied for the enclosure, for example audio/mpeg. weblogEdit.enclosureLength=Enclosure length weblogEdit.enclosureLength.tooltip=Non-negative enclosure size in bytes. -weblogEdit.enclosureMetadataInvalid=Enter an absolute HTTP or HTTPS enclosure URL, a valid media type, and a non-negative byte length. +weblogEdit.enclosureURLInvalid=Enter an absolute HTTP or HTTPS enclosure URL. +weblogEdit.enclosureTypeInvalid=Enter a valid enclosure media type, for example audio/mpeg. +weblogEdit.enclosureLengthInvalid=Enter a non-negative enclosure size in bytes. +weblogEdit.enclosureMetadataRemoved=The saved enclosure metadata was invalid and has been removed; your other changes were saved. weblogEdit.mediaCastErrorRemoving=Error removing MediaCast from weblog entry diff --git a/app/src/main/resources/ApplicationResources_de.properties b/app/src/main/resources/ApplicationResources_de.properties index 360c1e9dda..4c66baba2f 100644 --- a/app/src/main/resources/ApplicationResources_de.properties +++ b/app/src/main/resources/ApplicationResources_de.properties @@ -764,10 +764,6 @@ weblogEdit.fullPreviewMode=Volle Vorschau weblogEdit.hasComments=Kommentare [{1}] weblogEdit.locale=Sprache weblogEdit.mediaCastErrorRemoving=Fehler beim Entfernen des MediaCast aus dem Weblogeintrag -weblogEdit.mediaCastFailedFetchingInfo=Enclosure ist nicht erreichbar. \u00DCberpr\u00FCfen Sie den Hostnamen in der URL. -weblogEdit.mediaCastLacksContentTypeOrLength=Enclosure ist nicht verwendbar. Der Server lieferte keinen Inhaltstyp bzw. L\u00E4nge. -weblogEdit.mediaCastResponseError=Der Enclosure Server hat einen Fehler gemeldet. Haben Sie die richtige URL? -weblogEdit.mediaCastUrlMalformed=Die Enclosure URL ist fehlerhaft. weblogEdit.miscSettings=Weitere Einstellungen weblogEdit.pending=Auf Freigabe wartend weblogEdit.pendingEntries=Auf Freigabe wartende Eintr\u00E4ge diff --git a/app/src/main/resources/ApplicationResources_es.properties b/app/src/main/resources/ApplicationResources_es.properties index b8dcbb0b36..76dfa7811e 100644 --- a/app/src/main/resources/ApplicationResources_es.properties +++ b/app/src/main/resources/ApplicationResources_es.properties @@ -489,10 +489,6 @@ weblogEdit.trackback=Referencia weblogEdit.sendTrackback=Enviar referencia weblogEdit.trackbackUrl=URL de la referencia weblogEdit.hasComments=Comentarios [{0}] -weblogEdit.mediaCastFailedFetchingInfo=No se puede contactar con el servidor MediaCast. Compruebe el nombre del host en la URL. -weblogEdit.mediaCastUrlMalformed=La URL de MediaCast no estaba bien formada. -weblogEdit.mediaCastResponseError=El servidor MediaCast devolvi\u00F3 un error. \u00BFTiene la URL correcta? -weblogEdit.mediaCastLacksContentTypeOrLength=No se puede usar la URL de Medicast. El servidor no proporcion\u00F3 el tipo de contenido o el tama\u00F1o. weblogEntryRemove.removeWeblogEntry=Eliminar entrada del weblog weblogEntryRemove.areYouSure=\u00BFEst\u00E1 seguro de que desea eliminar esta entrada del weblog? weblogEntryRemove.entryTitle=T\u00EDtulo del la entrada diff --git a/app/src/main/resources/ApplicationResources_fr.properties b/app/src/main/resources/ApplicationResources_fr.properties index 124fd323d7..f0781ee839 100644 --- a/app/src/main/resources/ApplicationResources_fr.properties +++ b/app/src/main/resources/ApplicationResources_fr.properties @@ -979,10 +979,6 @@ weblogEdit.hasComments=Commentaires [{0}] #FIXME ALLL THOSE DOWNTHERE - WHAT IS ENCLOSURE weblogEdit.enclosureURL=Enclosure URL weblogEdit.enclosureType=Type -weblogEdit.mediaCastFailedFetchingInfo=Unable to reach the enclosure. Check the hostname in the URL. -weblogEdit.mediaCastUrlMalformed=The enclosure URL was malformed. -weblogEdit.mediaCastResponseError=The enclosure server returned an error. Do you have the right URL? -weblogEdit.mediaCastLacksContentTypeOrLength=Unable to use enclosure URL. Server provided no content type or no length. # -------------------------------------------------------- Weblog entries Pager diff --git a/app/src/main/resources/ApplicationResources_ja.properties b/app/src/main/resources/ApplicationResources_ja.properties index 8802be030d..40e1cfe0b6 100644 --- a/app/src/main/resources/ApplicationResources_ja.properties +++ b/app/src/main/resources/ApplicationResources_ja.properties @@ -1128,7 +1128,6 @@ oauthKeys.urlsTip=\u3053\u308C\u3089\u306F\u3001\u3042\u306A\u305F\u306E\u30D6\u inviteMember.title=\u65B0\u3057\u3044\u30E1\u30F3\u30D0\u30FC\u306E\u62DB\u5F85 mediaFileSuccess.pageTip=\u30D5\u30A1\u30A4\u30EB\u304C\u6B63\u5E38\u306B\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3055\u308C\u307E\u3057\u305F\u3002\u65B0\u3057\u304F\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u3092\u542B\u3080\u65B0\u3057\u3044\u30A8\u30F3\u30C8\u30EA\u3092\u4F5C\u6210\u3057\u305F\u3044\u5834\u5408\u306F\u3001\u4EE5\u4E0B\u306E\u30D5\u30A9\u30FC\u30E0\u3092\u4F7F\u3063\u3066\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30BF\u30A4\u30D7\u304C\u753B\u50CF\u306E\u5834\u5408\u306F\u30B5\u30E0\u30CD\u30A4\u30EB\u3068\u3057\u3066\u8FFD\u52A0\u3055\u308C\u307E\u3059\u3002\u30BF\u30A4\u30D7\u304C\u753B\u50CF\u4EE5\u5916\u306E\u5834\u5408\u306F\u3001\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3\uFF08\u4F8B\uFF1APodcast\uFF09\u3068\u3057\u3066\u542B\u3081\u308B\u304B\u3069\u3046\u304B\u9078\u629E\u3059\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002 websiteRemove.error=\u30D6\u30ED\u30B0 [{0}] \u306E\u524A\u9664\u30A8\u30E9\u30FC -weblogEdit.mediaCastFailedFetchingInfo=\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002URL\u5185\u306E\u30DB\u30B9\u30C8\u540D\u3092\u30C1\u30A7\u30C3\u30AF\u3057\u3066\u304F\u3060\u3055\u3044\u3002 Template.error.actionNull=\u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u5FC5\u9808\u9805\u76EE\u3067\u3059 mediaFile.delete.success=\u30E1\u30C7\u30A3\u30A2\u30FB\u30D5\u30A1\u30A4\u30EB\u306F\u6B63\u5E38\u306B\u524A\u9664\u3055\u308C\u307E\u3057\u305F\u3002 Register.error.timeZoneSize=\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u306F20\u6587\u5B57\u4EE5\u5185\u3067\u306A\u3051\u308C\u3070\u306A\u308A\u307E\u305B\u3093 @@ -1243,7 +1242,6 @@ macro.weblog.postcomment=\u30B3\u30E1\u30F3\u30C8\u3092\u6295\u7A3F Entry.error.categoryNull=\u30AB\u30C6\u30B4\u30EA\u306F\u5FC5\u9808\u9805\u76EE\u3067\u3059 planetGroupSubs.prompt.addMain=\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u3092\u3001\u30E1\u30A4\u30F3Planet\u30A2\u30B0\u30EA\u30B2\u30FC\u30BF\u30FC\u30FB\u30DA\u30FC\u30B8\u306B\u542B\u307E\u308C\u308B\u30CB\u30E5\u30FC\u30B9\u30D5\u30A3\u30FC\u30C9\u4E00\u89A7\u306B\u8FFD\u52A0\u3059\u308B\u306B\u306F\u3053\u306E\u30DA\u30FC\u30B8\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u65B0\u3057\u3044\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u3092\u8FFD\u52A0\u3059\u308B\u306B\u306F\u3001\u30CB\u30E5\u30FC\u30B9\u30D5\u30A3\u30FC\u30C9URL\u3092\u5165\u529B\u3057\u3066\u4FDD\u5B58\u30DC\u30BF\u30F3\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u304F\u3060\u3055\u3044\u3002 mediaFileView.audio=\u97F3\u58F0 -weblogEdit.mediaCastResponseError=\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3\u30FB\u30B5\u30FC\u30D0\u306F\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F\u3002\u6B63\u3057\u3044URL\u3067\u3059\u304B? themeEditor.importRequired=\u4ECA\u56DE\u304C\u521D\u3081\u3066\u306E\u30AB\u30B9\u30BF\u30E0\u30FB\u30C6\u30FC\u30DE\u306E\u4F7F\u7528\u3067\u3042\u308B\u305F\u3081\u3001\u65E2\u5B58\u306E\u30C6\u30FC\u30DE\u304B\u3089\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u30B3\u30D4\u30FC\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002 themeEditor.importWarning=\u8B66\u544A\: \u30AB\u30B9\u30BF\u30E0\u30FB\u30C6\u30FC\u30DE\u306E\u66F4\u65B0\u306F\u3001\u3042\u306A\u305F\u306E\u65E2\u5B58\u306E\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4E0A\u66F8\u304D\u3059\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059\u3002 MediaFile.error.nameSize=\u540D\u524D\u306F255\u6587\u5B57\u4EE5\u5185\u3067\u306A\u3051\u308C\u3070\u3044\u3051\u307E\u305B\u3093 @@ -1291,7 +1289,7 @@ mediaFileEdit.subtitle=\u30E1\u30C7\u30A3\u30A2\u30FB\u30D5\u30A1\u30A4\u30EB \= -weblogEdit.mediaCastUrlMalformed=\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3URL\u304C\u4E0D\u6B63\u3067\u3059\u3002 stylesheetEdit.revert.success=\u30B9\u30BF\u30A4\u30EB\u30B7\u30FC\u30C8\u306F\u6B63\u5E38\u306B\u5FA9\u5143\u3055\u308C\u307E\u3057\u305F\u3002 mediaFileImageChooser.rootPageTip=\u753B\u50CF\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u9078\u629E\u3059\u308B\u304B\u3001\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u9078\u629E\u3057\u3066\u5185\u5BB9\u3092\u8868\u793A\u3057\u3066\u304F\u3060\u3055\u3044 pingTarget.notFound=Ping\u30BF\u30FC\u30B2\u30C3\u30C8ID {0} \u306F\u3001\u5B58\u5728\u3057\u307E\u305B\u3093 @@ -1411,7 +1408,6 @@ generic.error.check.logs=Roller\u30B7\u30B9\u30C6\u30E0\u30FB\u30A8\u30E9\u30FC oauthKeys.authorizationURL=\u8A8D\u53EFURL weblogEdit.scheduledEntry=\u30A8\u30F3\u30C8\u30EA\u30FC\u306E\u516C\u958B\u304C {0} \u306B\u30B9\u30B1\u30B8\u30E5\u30FC\u30EB\u3055\u308C\u307E\u3057\u305F yourWebsites.oauthKeys=OAuth\u8A8D\u8A3C\u60C5\u5831 -weblogEdit.mediaCastLacksContentTypeOrLength=\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3URL\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D0\u306FContent-Type\u307E\u305F\u306FLength\u3092\u8FD4\u3057\u307E\u305B\u3093\u3067\u3057\u305F\u3002 frontpageConfig.weblogs.error=\u30D6\u30ED\u30B0\u3078\u306E\u30A2\u30AF\u30BB\u30B9\u4E2D\u306B\u4E88\u671F\u305B\u306C\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F pingTarget.created=Ping\u30BF\u30FC\u30B2\u30C3\u30C8 "{0}" \u304C\u8FFD\u52A0\u3055\u308C\u307E\u3057\u305F themeEditor.yourCustomStylesheet=\u30AB\u30B9\u30BF\u30E0\u30FB\u30AA\u30FC\u30D0\u30FC\u30E9\u30A4\u30C9\u30FB\u30B9\u30BF\u30A4\u30EB\u30B7\u30FC\u30C8\u3092\u4F7F\u7528\u3057\u3066\u3044\u307E\u3059\u3002 diff --git a/app/src/main/resources/ApplicationResources_ko.properties b/app/src/main/resources/ApplicationResources_ko.properties index 0de377cae7..20839bbc6b 100644 --- a/app/src/main/resources/ApplicationResources_ko.properties +++ b/app/src/main/resources/ApplicationResources_ko.properties @@ -1098,10 +1098,6 @@ weblogEdit.hasComments=\uc758\uacac, [{1}] weblogEdit.enclosureURL=\uac10\uc2f8\uc9c4 URL weblogEdit.enclosureType=\ud0c0\uc785 weblogEdit.enclosureLength=\uae38\uc774 -weblogEdit.mediaCastFailedFetchingInfo=\uac10\uc2f8\uc9c4 URL\uc5d0 \uc811\uadfc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. URL\uc758 \ud638\uc2a4\ud2b8\uba85\uc744 \ud655\uc778\ud558\uc2ed\uc2dc\uc624. -weblogEdit.mediaCastUrlMalformed=\uac10\uc2f8\uc9c4 URL\uc758 \ud3ec\ub9f7\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. -weblogEdit.mediaCastResponseError=\uac10\uc2f8\uc9c4 URL\uc758 \uc11c\ubc84\uac00 \uc624\ub958\ub97c \ubc18\ud658\ud588\uc2b5\ub2c8\ub2e4. \uc62c\ubc14\ub978 URL\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624. -weblogEdit.mediaCastLacksContentTypeOrLength=\uac10\uc2f8\uc9c4 URL\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc11c\ubc84\uac00 \ucee8\ud150\ud2b8 \ud0c0\uc785 \ub610\ub294 \uae38\uc774\ub97c \uc815\uc0c1\uc801\uc73c\ub85c \uc81c\uacf5\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. weblogEdit.mediaCastErrorRemoving=\uc6f9\ub85c\uadf8 \uae30\uc0ac\ub85c\ubd80\ud130 MediaCast \uc81c\uac70 \uc624\ub958 # errors from validation @@ -1335,4 +1331,4 @@ user.account.activation.mail.content=\ welcome.user.account.activated=\uadc0\ud558\uc758 \uc0ac\uc6a9\uc790 \uacc4\uc815\uc774 \ud65c\uc131\ud654\ub418\uc5c8\uc2b5\ub2c8\ub2e4. welcome.user.account.not.activated=\uc2dc\uc2a4\ud15c\uc5d0 \ub85c\uadf8\uc778\ud558\uc2dc\ub824\uba74, \ \uc804\uc790\uc6b0\ud3b8\uc744 \ud1b5\ud574 \uadc0\ud558\uc5d0\uac8c \uc804\uc1a1\ub41c \ub9c1\ud06c\ub97c \ud074\ub9ad\ud568\uc73c\ub85c\uc368, \ -\uadc0\ud558\uc758 \uc0ac\uc6a9\uc790 \uacc4\uc815\uc744 \ud65c\uc131\ud654\uc2dc\ucf1c\uc57c \ud569\ub2c8\ub2e4. \ No newline at end of file +\uadc0\ud558\uc758 \uc0ac\uc6a9\uc790 \uacc4\uc815\uc744 \ud65c\uc131\ud654\uc2dc\ucf1c\uc57c \ud569\ub2c8\ub2e4. diff --git a/app/src/main/resources/ApplicationResources_ru.properties b/app/src/main/resources/ApplicationResources_ru.properties index ce5c69559c..482091823a 100644 --- a/app/src/main/resources/ApplicationResources_ru.properties +++ b/app/src/main/resources/ApplicationResources_ru.properties @@ -715,10 +715,6 @@ weblogEdit.trackbackResults = Trackback \u043E\u0442\u0432\u0435\u0442\u0430 weblogEdit.comment=\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 -weblogEdit.mediaCastFailedFetchingInfo = \u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0434\u043E\u0441\u0442\u0438\u0447 MediaCast \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 hostname \u0432 \u0430\u0434\u0440\u0435\u0441\u0435. -weblogEdit.mediaCastUrlMalformed = MediaCast \u0430\u0434\u0440\u0435\u0441 \u0431\u044B\u043B malformed. -weblogEdit.mediaCastResponseError = MediaCast server \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443. \u0412\u044B \u0432\u0432\u0435\u043B\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441? -weblogEdit.mediaCastLacksContentTypeOrL ength = \u0412\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0435 \u043D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 MediaCast \u0430\u0434\u0440\u0435\u0441. Server \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 \u0442\u0438\u043F \u0438\u043B\u0438 \u043D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u0443\u044E \u0434\u043B\u0438\u043D\u043D\u0443 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430. weblogEdit.error.incompleteEntry = \u0417\u0430\u043F\u0438\u0441\u044C \u0434\u043E\u043B\u0436\u043D\u0430 \u0438\u043C\u0435\u0442\u044C \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A diff --git a/app/src/main/resources/ApplicationResources_zh_CN.properties b/app/src/main/resources/ApplicationResources_zh_CN.properties index 93ac844968..a2f58ff7ea 100644 --- a/app/src/main/resources/ApplicationResources_zh_CN.properties +++ b/app/src/main/resources/ApplicationResources_zh_CN.properties @@ -1603,13 +1603,9 @@ weblogEdit.trackbackError404=\u5F15\u7528\u5931\u8D25\uFF0C\u65E0\u6CD5\u6253\u5 weblogEdit.hasComments=\u8BC4\u8BBA [{1}] weblogEdit.enclosureURL=\u5D4C\u5165URL -weblogEdit.enclosureURL.tooltip=\u8981\u4E3A\u6B64\u535A\u5BA2\u6587\u7AE0\u5D4C\u5165\u5230 RSS \u548C Atom \u65B0\u95FB\u6E90\u4E2D\u7684\u64AD\u5BA2\u6216\u5176\u4ED6\u591A\u5A92\u4F53 URL\u3002 +weblogEdit.enclosureURL.tooltip=\u8981\u5D4C\u5165\u6B64\u535A\u5BA2\u6587\u7AE0\u7684 RSS \u548C Atom \u65B0\u95FB\u6E90\u4E2D\u7684\u7EDD\u5BF9 HTTP \u6216 HTTPS URL\u3002 weblogEdit.enclosureType=\u7C7B\u578B weblogEdit.enclosureLength=\u957F\u5EA6 -weblogEdit.mediaCastFailedFetchingInfo=\u65E0\u6CD5\u6253\u5F00\u5D4C\u5165URL\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u540D\u662F\u5426\u6B63\u786E\u3002 -weblogEdit.mediaCastUrlMalformed=\u5D4C\u5165URL\u683C\u5F0F\u4E0D\u6B63\u786E\u3002 -weblogEdit.mediaCastResponseError=\u670D\u52A1\u5668\u8FD4\u56DE\u9519\u8BEF\u4FE1\u606F\uFF0C\u8BF7\u68C0\u67E5\u5D4C\u5165URL\u662F\u5426\u6B63\u786E\u3002 -weblogEdit.mediaCastLacksContentTypeOrLength=\u65E0\u6CD5\u4F7F\u7528\u5D4C\u5165URL\u3002\u670D\u52A1\u5668\u672A\u63D0\u4F9B\u5185\u5BB9\u7C7B\u578B\u6216\u957F\u5EA6\u4FE1\u606F\u3002 weblogEdit.mediaCastErrorRemoving=\u79FB\u9664\u64AD\u5BA2\u5185\u5BB9\u65F6\u51FA\u9519 @@ -1878,4 +1874,3 @@ user.account.activation.mail.subject=Roller\uFF1A\u4F60\u7684\u8D26\u53F7\u6FC0\ user.account.activation.mail.content=

\u8981\u6FC0\u6D3B\u4F60\u7684Roller\u8D26\u6237[{1}]\uFF0C\u8BF7\u70B9\u51FB\u4EE5\u4E0B\u94FE\u63A5\uFF1A

{2}

- diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp index 3f9aa71e02..27921fed88 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp @@ -98,9 +98,9 @@
', - '', - '')"/> + data-enclosure-url="" + data-enclosure-type="" + data-enclosure-length=""/>
@@ -136,7 +136,9 @@
- +
@@ -193,6 +195,14 @@ } submitButton.attr("disabled", !isImageChecked()); }); + + $("input[name='enclosure']").change(function () { + var selected = $(this); + setEnclosure( + selected.attr("data-enclosure-url") || "", + selected.attr("data-enclosure-type") || "", + selected.attr("data-enclosure-length") || ""); + }); }); function isImageChecked() { diff --git a/app/src/main/webapp/WEB-INF/velocity/feeds.vm b/app/src/main/webapp/WEB-INF/velocity/feeds.vm index 065a54852f..039d46c3c1 100644 --- a/app/src/main/webapp/WEB-INF/velocity/feeds.vm +++ b/app/src/main/webapp/WEB-INF/velocity/feeds.vm @@ -49,7 +49,7 @@ #set( $mc_type = $entry.findEntryAttribute("att_mediacast_type") ) #set( $mc_length = $entry.findEntryAttribute("att_mediacast_length") ) #if( $mc_url && $mc_length && $mc_type ) - + #set($mc_url = false) #set($mc_type = false) #set($mc_length = false)#end #end @@ -77,7 +77,7 @@ #set( $mc_type = $entry.findEntryAttribute("att_mediacast_type") ) #set( $mc_length = $entry.findEntryAttribute("att_mediacast_length") ) #if( $mc_url && $mc_length && $mc_type ) - + #set($mc_url = false) #set($mc_type = false) #set($mc_length = false) #end #if( $website.allowComments ) @@ -146,4 +146,4 @@ #macro(showFirefoxFeedWorkaround) -#end \ No newline at end of file +#end diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEditEnclosureTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEditEnclosureTest.java new file mode 100644 index 0000000000..c02fec62da --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEditEnclosureTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ +package org.apache.roller.weblogger.ui.struts2.editor; + +import org.apache.roller.weblogger.pojos.WeblogEntry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; + +class EntryEditEnclosureTest { + + @Test + void unchangedInvalidLegacyMetadataIsClearedWithAWarning() throws Exception { + EntryEdit action = action(); + action.setActionName("entryEdit"); + WeblogEntry entry = new WeblogEntry(); + entry.putEntryAttribute("att_mediacast_url", "ftp://legacy/audio"); + entry.putEntryAttribute("att_mediacast_type", "broken"); + entry.putEntryAttribute("att_mediacast_length", "unknown"); + action.setEntry(entry); + setEnclosure(action.getBean(), "ftp://legacy/audio", "broken", "unknown"); + + assertNull(action.validateEnclosure()); + assertFalse(action.hasActionErrors()); + assertTrue(action.hasActionMessages()); + assertTrue(action.getActionMessages().contains( + "weblogEdit.enclosureMetadataRemoved")); + assertNull(action.getBean().getEnclosureURL()); + assertNull(action.getBean().getEnclosureType()); + assertNull(action.getBean().getEnclosureLength()); + } + + @Test + void newlySubmittedInvalidMetadataGetsAFieldError() { + EntryEdit action = action(); + action.setActionName("entryEdit"); + action.setEntry(new WeblogEntry()); + setEnclosure(action.getBean(), "https://example.org/audio", "broken", "12"); + + assertNull(action.validateEnclosure()); + assertTrue(action.hasActionErrors()); + assertTrue(action.getActionErrors().contains( + "weblogEdit.enclosureTypeInvalid")); + assertFalse(action.hasActionMessages()); + assertEquals("broken", action.getBean().getEnclosureType()); + } + + @Test + void failedNewEntryValidationRestoresTheUnsavedStatus() { + EntryEdit action = action(); + action.setActionName("entryAdd"); + action.getBean().setStatus(WeblogEntry.PubStatus.PUBLISHED.name()); + setEnclosure(action.getBean(), "file:///tmp/audio", "audio/ogg", "12"); + + assertEquals(EntryEdit.INPUT, action.save()); + assertTrue(action.hasActionErrors()); + assertNull(action.getBean().getStatus()); + } + + private void setEnclosure(EntryBean bean, String url, String type, String length) { + bean.setEnclosureURL(url); + bean.setEnclosureType(type); + bean.setEnclosureLength(length); + } + + private EntryEdit action() { + EntryEdit action = spy(new EntryEdit()); + doAnswer(invocation -> invocation.getArgument(0)) + .when(action).getText(anyString()); + return action; + } +} diff --git a/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java b/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java index 24a09a3505..c89f457e11 100644 --- a/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java @@ -16,10 +16,16 @@ */ package org.apache.roller.weblogger.util; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class EnclosureMetadataTest { @@ -44,15 +50,86 @@ void acceptsHttpsAndTrimsMetadata() { assertEquals("3141592654", metadata.getLength()); } + @Test + void acceptsLocalAndInternationalizedAuthoritiesWithoutNetworkAccess() { + assertEquals("http://roller.internal/audio.ogg", + EnclosureMetadata.of("http://roller.internal/audio.ogg", + "audio/ogg", "12").getUrl()); + assertEquals("https://例え.テスト/audio.ogg", + EnclosureMetadata.of("https://例え.テスト/audio.ogg", + "audio/ogg", "12").getUrl()); + } + + @Test + void stripsLegacyParametersAndNormalizesTheMediaType() { + EnclosureMetadata metadata = EnclosureMetadata.of( + "https://media.example.org/show.mp3", + " Audio/MPEG; charset=utf-8 ", "12"); + + assertEquals("audio/mpeg", metadata.getContentType()); + } + + @Test + void acceptsAllMediaTypeTokenCharacters() { + assertEquals("audio/x!#$%&'*+-.^_`|~", + EnclosureMetadata.of("https://example.org/audio", + "audio/x!#$%&'*+-.^_`|~", "12").getContentType()); + } + @Test void rejectsUnsupportedOrIncompleteMetadata() { - assertThrows(IllegalArgumentException.class, - () -> EnclosureMetadata.of("file:///tmp/audio.ogg", "audio/ogg", "12")); - assertThrows(IllegalArgumentException.class, - () -> EnclosureMetadata.of("https://example.org/audio", "not-a-type", "12")); - assertThrows(IllegalArgumentException.class, - () -> EnclosureMetadata.of("https://example.org/audio", "audio/ogg", "-1")); - assertThrows(IllegalArgumentException.class, - () -> EnclosureMetadata.of("https://example.org/audio", "audio/ogg", "unknown")); + assertField(EnclosureMetadata.Field.URL, + "file:///tmp/audio.ogg", "audio/ogg", "12"); + assertField(EnclosureMetadata.Field.TYPE, + "https://example.org/audio", "not-a-type", "12"); + assertField(EnclosureMetadata.Field.LENGTH, + "https://example.org/audio", "audio/ogg", "-1"); + assertField(EnclosureMetadata.Field.LENGTH, + "https://example.org/audio", "audio/ogg", "unknown"); + } + + @Test + void templatesEscapeFeedAttributesAndAvoidInlineHandlers() throws Exception { + String feeds = source("src/main/webapp/WEB-INF/velocity/feeds.vm"); + assertEquals(2, count(feeds, "type=\"$utils.escapeXML($mc_type)\"")); + assertFalse(feeds.contains("type=\"$mc_type\"")); + + String upload = source( + "src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp"); + assertTrue(upload.contains("data-enclosure-url=")); + assertTrue(upload.contains("data-enclosure-type=")); + assertTrue(upload.contains("selected.attr(\"data-enclosure-type\")")); + assertFalse(upload.contains("onchange=\"setEnclosure(")); + + String policy = source( + "src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java"); + assertFalse(policy.contains("openConnection")); + assertFalse(policy.contains("UrlValidator")); + } + + private void assertField(EnclosureMetadata.Field field, + String url, String type, String length) { + EnclosureMetadata.ValidationException exception = assertThrows( + EnclosureMetadata.ValidationException.class, + () -> EnclosureMetadata.of(url, type, length)); + assertEquals(field, exception.getField()); + } + + private int count(String value, String needle) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(needle, offset)) >= 0) { + count++; + offset += needle.length(); + } + return count; + } + + private String source(String relativePath) throws Exception { + Path path = Path.of(relativePath); + if (!Files.exists(path)) { + path = Path.of("app").resolve(relativePath); + } + return Files.readString(path, StandardCharsets.UTF_8); } }