diff --git a/CHANGES.md b/CHANGES.md
index ae054e20d7..f1473e3cc0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,16 @@
# Apache Roller — Changes
+## 6.1.8 (in development)
+
+### Bug fixes
+
+- **Entry titles show as text in the next/previous links and the big
+ calendar.** The links on a permalink page and the titles in
+ `#showWeblogEntryCalendarBig` now drop markup, decode entities and encode the
+ result, the same way the rest of the page shows titles. A title saved with
+ `&` no longer turns into `&`, and the calendar cuts long titles
+ at 20 visible characters.
+
## 6.1.7
### Improvements
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/tags/calendar/BigWeblogCalendarModel.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/tags/calendar/BigWeblogCalendarModel.java
index 5b9db4d701..566dadf24d 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/core/tags/calendar/BigWeblogCalendarModel.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/tags/calendar/BigWeblogCalendarModel.java
@@ -30,6 +30,7 @@
import org.apache.roller.weblogger.business.WebloggerFactory;
import org.apache.roller.weblogger.business.WeblogEntryManager;
import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.util.Utilities;
import org.apache.roller.weblogger.pojos.WeblogEntrySearchCriteria;
import org.apache.roller.weblogger.ui.rendering.util.WeblogPageRequest;
import org.apache.roller.util.DateUtil;
@@ -106,15 +107,12 @@ public String getContent(Date day) {
sb.append(((WeblogEntry)entries.get(i)).getPermalink());
sb.append("\">");
- String title = ((WeblogEntry)entries.get(i)).getTitle().trim();
- if ( title.length()==0 ) {
+ String title = ((WeblogEntry)entries.get(i)).getTitle();
+ if (title == null || title.trim().isEmpty()) {
title = ((WeblogEntry)entries.get(i)).getAnchor();
}
- if ( title.length() > 20 ) {
- title = title.substring(0,20)+"...";
- }
-
- sb.append( title );
+ // the title is printed as text, cut to fit the day's box
+ sb.append(Utilities.plainTextExcerpt(title, 20, 20, "..."));
sb.append("");
}
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPager.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPager.java
index b92e3a4e49..d75b8afc4d 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPager.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPager.java
@@ -110,7 +110,7 @@ public String getNextLink() {
@Override
public String getNextName() {
if (getNextEntry() != null) {
- String title = Utilities.truncateNicely(getNextEntry().getTitle(), 15, 20, "...");
+ String title = Utilities.plainTextExcerpt(getNextEntry().getTitle(), 15, 20, "...");
return messageUtils.getString("weblogEntriesPager.single.next", new Object[] {title});
}
return null;
@@ -129,7 +129,7 @@ public String getPrevLink() {
@Override
public String getPrevName() {
if (getPrevEntry() != null) {
- String title = Utilities.truncateNicely(getPrevEntry().getTitle(), 15, 20, "...");
+ String title = Utilities.plainTextExcerpt(getPrevEntry().getTitle(), 15, 20, "...");
return messageUtils.getString("weblogEntriesPager.single.prev", new Object[] {title});
}
return null;
diff --git a/app/src/main/java/org/apache/roller/weblogger/util/Utilities.java b/app/src/main/java/org/apache/roller/weblogger/util/Utilities.java
index 643c7834c8..bf1931876e 100644
--- a/app/src/main/java/org/apache/roller/weblogger/util/Utilities.java
+++ b/app/src/main/java/org/apache/roller/weblogger/util/Utilities.java
@@ -639,6 +639,31 @@ public static String truncate(String str, int lower, int upper,
return str2;
}
+ /**
+ * Turns stored entry text, such as a title, into a short excerpt that is
+ * safe to print as HTML text: markup is removed, entities are decoded, the
+ * text is cut at a word boundary between lower and
+ * upper characters (with appendToEnd added when
+ * it is cut), and the result is HTML-encoded.
+ */
+ public static String plainTextExcerpt(String str, int lower, int upper,
+ String appendToEnd) {
+ String text = unescapeHTML(removeHTML(str, false));
+ if (upper < lower) {
+ upper = lower;
+ }
+ if (text.length() > upper) {
+ int loc = text.lastIndexOf(' ', upper);
+ if (loc >= lower) {
+ text = text.substring(0, loc);
+ } else {
+ text = text.substring(0, upper);
+ }
+ text = text + appendToEnd;
+ }
+ return escapeHTML(text);
+ }
+
/**
* This method based on code from the String taglib at Apache Jakarta:
* http:/
diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPagerTitleTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPagerTitleTest.java
new file mode 100644
index 0000000000..7f95731914
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/pagers/WeblogEntriesPermalinkPagerTitleTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.ui.rendering.pagers;
+
+import java.sql.Timestamp;
+
+import org.apache.roller.weblogger.TestUtils;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The next and previous links on a permalink page show the neighbouring
+ * entries' titles as text.
+ */
+public class WeblogEntriesPermalinkPagerTitleTest {
+
+ private User user;
+ private Weblog weblog;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ TestUtils.setupWeblogger();
+ user = TestUtils.setupUser("pagerTitleUser");
+ weblog = TestUtils.setupWeblog("pagerTitleWeblog", user);
+ TestUtils.endSession(true);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ TestUtils.teardownWeblog(weblog.getId());
+ TestUtils.teardownUser(user.getUserName());
+ TestUtils.endSession(true);
+ }
+
+ private void entry(String anchor, String title, long pubTime) throws Exception {
+ WeblogEntry entry = TestUtils.setupWeblogEntry(anchor, weblog, user);
+ entry.setTitle(title);
+ entry.setPubTime(new Timestamp(pubTime));
+ WeblogEntryManager mgr = WebloggerFactory.getWeblogger().getWeblogEntryManager();
+ mgr.saveWeblogEntry(entry);
+ TestUtils.endSession(true);
+ }
+
+ private WeblogEntriesPermalinkPager pagerFor(String anchor) throws Exception {
+ Weblog managed = TestUtils.getManagedWebsite(weblog);
+ return new WeblogEntriesPermalinkPager(
+ WebloggerFactory.getWeblogger().getUrlStrategy(),
+ managed, null, null, anchor, null, null, null, 0);
+ }
+
+ @Test
+ public void neighbouringTitlesAreShownAsText() throws Exception {
+ long now = System.currentTimeMillis();
+ entry("older", "Older", now - 3 * 60_000L);
+ entry("middle", "Middle", now - 2 * 60_000L);
+ entry("newer", "A & B bold", now - 60_000L);
+
+ WeblogEntriesPermalinkPager pager = pagerFor("middle");
+
+ // markup is dropped, entities stay encoded exactly once
+ assertEquals("Older", pager.getPrevName());
+ assertEquals("A & B bold", pager.getNextName());
+ }
+
+ @Test
+ public void encodedMarkupStaysEncoded() throws Exception {
+ long now = System.currentTimeMillis();
+ entry("older", "<em> tags", now - 2 * 60_000L);
+ entry("middle", "Middle", now - 60_000L);
+
+ assertEquals("<em> tags", pagerFor("middle").getPrevName());
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/util/UtilitiesTest.java b/app/src/test/java/org/apache/roller/weblogger/util/UtilitiesTest.java
index 2ab81ecdf2..9a31af40ff 100644
--- a/app/src/test/java/org/apache/roller/weblogger/util/UtilitiesTest.java
+++ b/app/src/test/java/org/apache/roller/weblogger/util/UtilitiesTest.java
@@ -169,4 +169,20 @@ public void testAddNoFollow() {
}
*/
+ @Test
+ public void testPlainTextExcerpt() {
+ // markup is removed and the text is HTML-encoded once
+ assertEquals("Older", Utilities.plainTextExcerpt("Older", 15, 20, "..."));
+ assertEquals("A & B bold", Utilities.plainTextExcerpt("A & B bold", 15, 20, "..."));
+ assertEquals("<i> is italic",
+ Utilities.plainTextExcerpt("<i> is italic", 15, 20, "..."));
+ assertEquals(""quoted"", Utilities.plainTextExcerpt("\"quoted\"", 15, 20, "..."));
+
+ // long text is cut at a word boundary, then encoded
+ assertEquals("The quick brown fox...",
+ Utilities.plainTextExcerpt("The quick brown fox jumps over", 15, 20, "..."));
+ assertEquals("<em>Emphasis</em>",
+ Utilities.plainTextExcerpt("<em>Emphasis</em>", 20, 20, "..."));
+ assertEquals("", Utilities.plainTextExcerpt(null, 15, 20, "..."));
+ }
}