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")); + } +}