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 3f37c6d8f8..d4e0af71b7 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 @@ -46,11 +46,9 @@ 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.cache.CacheManager; import org.apache.struts2.convention.annotation.AllowedMethods; import org.apache.struts2.interceptor.validation.SkipValidation; @@ -190,11 +188,18 @@ public String publish() { * * @return String The result of the action. */ - private String save() { + // Package-private rather than private so EntryEditEnclosureTest can drive + // it directly. + String save() { if (!requireEntry()) { return INPUT; } if (!hasActionErrors()) { + EnclosureMetadata enclosure = validateEnclosure(); + if (hasActionErrors()) { + return failedSave(); + } + try { WeblogEntryManager weblogEntryManager = WebloggerFactory.getWeblogger() .getWeblogEntryManager(); @@ -226,24 +231,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 @@ -307,8 +301,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 new file mode 100644 index 0000000000..8e1dbc1902 --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/util/EnclosureMetadata.java @@ -0,0 +1,154 @@ +/* + * 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.net.IDN; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Validated metadata for an RSS or Atom enclosure. + */ +public final class EnclosureMetadata { + + 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 = normalizeContentType(contentType); + String normalizedLength = normalize(length); + + 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 ValidationException(Field.TYPE, + "Enclosure type must be a valid media type"); + } + + final long byteLength; + try { + byteLength = Long.parseLong(normalizedLength); + } catch (NumberFormatException e) { + throw new ValidationException(Field.LENGTH, + "Enclosure length must be a non-negative integer", e); + } + if (byteLength < 0) { + throw new ValidationException(Field.LENGTH, + "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(); + } + + 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; + } + + 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 c4b94c5a8c..caa9abc04d 100644 --- a/app/src/main/resources/ApplicationResources.properties +++ b/app/src/main/resources/ApplicationResources.properties @@ -1598,13 +1598,15 @@ placed in HTML header (if coded by your blog template) for SEO. 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.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 fe4a956b5b..e677f81870 100644 --- a/app/src/main/resources/ApplicationResources_de.properties +++ b/app/src/main/resources/ApplicationResources_de.properties @@ -762,10 +762,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 342867e270..3432d85fde 100644 --- a/app/src/main/resources/ApplicationResources_es.properties +++ b/app/src/main/resources/ApplicationResources_es.properties @@ -484,10 +484,6 @@ weblogEdit.miscSettings=Ajustes de configuraci\u00F3n miscel\u00E1neos weblogEdit.rightToLeft=El texto se lee de derecha a izquierda weblogEdit.pinnedToMain=Poner en principal 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 33bd4b07b4..2a9b715ad8 100644 --- a/app/src/main/resources/ApplicationResources_fr.properties +++ b/app/src/main/resources/ApplicationResources_fr.properties @@ -970,10 +970,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 0998a80529..f65cb9e1f0 100644 --- a/app/src/main/resources/ApplicationResources_ja.properties +++ b/app/src/main/resources/ApplicationResources_ja.properties @@ -1121,7 +1121,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 @@ -1233,7 +1232,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 @@ -1280,7 +1278,7 @@ userAdmin.userSaved=\u30E6\u30FC\u30B6\u60C5\u5831\u304C\u4FDD\u5B58\u3055\u308C mediaFileEdit.subtitle=\u30E1\u30C7\u30A3\u30A2\u30FB\u30D5\u30A1\u30A4\u30EB {0} \u306E\u7DE8\u96C6 mediaFileView.le=<\= macro.weblog.url=URL\: -weblogEdit.enclosureURL.tooltip=\u3053\u306E\u30D6\u30ED\u30B0\u30FB\u30A8\u30F3\u30C8\u30EA\u30FC\u306ERSS\u3068Atom\u30D5\u30A3\u30FC\u30C9\u306B\u57CB\u3081\u8FBC\u307E\u308C\u308B\u3001Podcast\u307E\u305F\u306F\u4ED6\u306E\u30DE\u30EB\u30C1\u30E1\u30C7\u30A3\u30A2URL +weblogEdit.enclosureURL.tooltip=\u3053\u306E\u30D6\u30ED\u30B0\u30FB\u30A8\u30F3\u30C8\u30EA\u30FC\u306ERSS\u3068Atom\u30D5\u30A3\u30FC\u30C9\u306B\u57CB\u3081\u8FBC\u3080\u7D76\u5BFEHTTP\u307E\u305F\u306FHTTPS URL mediaFile.includeInGallery.error=\u30E1\u30C7\u30A3\u30A2\u30FB\u30D5\u30A1\u30A4\u30EB {0} \u3092\u30AE\u30E3\u30E9\u30EA\u30FC\u306B\u8FFD\u52A0\u3059\u308B\u969B\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002 WeblogConfig.error.analyticsCodeSize=\u30A2\u30CA\u30EA\u30C6\u30A3\u30AF\u30B9\u30FB\u30C8\u30E9\u30C3\u30AD\u30F3\u30B0\u30FB\u30B3\u30FC\u30C9\u306F1200\u6587\u5B57\u4EE5\u5185\u3067\u306A\u3051\u308C\u3070\u306A\u308A\u307E\u305B\u3093 Entry.error.titleNull=\u30BF\u30A4\u30C8\u30EB\u306F\u5FC5\u9808\u9805\u76EE\u3067\u3059 @@ -1345,7 +1343,6 @@ categoryForm.requiredFields={0} \u306F\u5FC5\u9808\u9805\u76EE\u3067\u3059 mediaFileEdit.updateFileContents=\u30D5\u30A1\u30A4\u30EB\u306E\u5185\u5BB9\u3092\u66F4\u65B0\u3059\u308B websiteSettings.error.processingBannedwordslist=\u30D6\u30E9\u30C3\u30AF\u30EA\u30B9\u30C8\u306E\u51E6\u7406\u30A8\u30E9\u30FC\: {0} mediaFileView.ge=>\= -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 @@ -1398,7 +1395,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 bf51cd18d5..bd5839d0bc 100644 --- a/app/src/main/resources/ApplicationResources_ko.properties +++ b/app/src/main/resources/ApplicationResources_ko.properties @@ -1085,10 +1085,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 diff --git a/app/src/main/resources/ApplicationResources_ru.properties b/app/src/main/resources/ApplicationResources_ru.properties index 7375cbe854..a5b12a59f2 100644 --- a/app/src/main/resources/ApplicationResources_ru.properties +++ b/app/src/main/resources/ApplicationResources_ru.properties @@ -705,10 +705,6 @@ weblogEdit.pinnedToMain = \u041F\u0440\u0438\u043A\u0440\u0435\u043F\u043B\u044F 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 ec1f5748c4..44583e0f97 100644 --- a/app/src/main/resources/ApplicationResources_zh_CN.properties +++ b/app/src/main/resources/ApplicationResources_zh_CN.properties @@ -1588,13 +1588,9 @@ weblogEdit.searchDescription.tooltip=\u7528\u4E8E SEO \u7684\u653E\u7F6E\u4E8E H 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 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 14fa8a880f..3a329c574f 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 a8a9572a9b..98cb3f951a 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 @@
"/> + data-enclosure-url="" + data-enclosure-type="" + data-enclosure-length=""/>
@@ -132,7 +136,9 @@
- +
@@ -203,11 +209,16 @@ // Values come from data-* attributes and are bound via delegated listeners. $(document).on('change', '.enclosure-choice', function () { - setEnclosure($(this).attr('data-enclosure-url')); + var selected = $(this); + setEnclosure(selected.attr("data-enclosure-url") || "", + selected.attr("data-enclosure-type") || "", + selected.attr("data-enclosure-length") || ""); }); - 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/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..e65062df74 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEditEnclosureTest.java @@ -0,0 +1,85 @@ +/* + * 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"); + // myPrepare() creates this for a new entry; the test drives save() + // directly, so stand it up the same way. + action.setEntry(new WeblogEntry()); + 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 new file mode 100644 index 0000000000..c89f457e11 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/util/EnclosureMetadataTest.java @@ -0,0 +1,135 @@ +/* + * 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.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 { + + @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 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() { + 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); + } +}