From 0f9bfddc7c3016dd1ef348dfa6c4fc900ea22889 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sat, 29 Aug 2026 09:23:59 -0400 Subject: [PATCH 1/2] Move authoring UI inline JS handlers to data attributes The editor screens built handler arguments by interpolating template values directly into inline onclick/onchange attributes. Those values now travel in data-* attributes and are read back through delegated listeners, which matches how the rest of the UI binds behaviour and keeps markup and data separate. Writes that placed those values into the DOM now use text APIs rather than html(), and ThemeDataServlet escapes its JSON output so theme metadata cannot produce a malformed document. Adds AuthoringUiSinkAuditTest to keep the editor templates on this pattern. --- .../ui/struts2/ajax/ThemeDataServlet.java | 13 +- .../webapp/WEB-INF/jsps/editor/Bookmarks.jsp | 36 ++-- .../webapp/WEB-INF/jsps/editor/Categories.jsp | 64 +++++-- .../webapp/WEB-INF/jsps/editor/Entries.jsp | 16 +- .../webapp/WEB-INF/jsps/editor/EntryEdit.jsp | 16 +- .../jsps/editor/MediaFileAddSuccess.jsp | 10 +- .../jsps/editor/MediaFileImageChooser.jsp | 16 +- .../WEB-INF/jsps/editor/MediaFileView.jsp | 25 ++- .../WEB-INF/jsps/editor/TemplateEdit.jsp | 16 +- .../webapp/WEB-INF/jsps/editor/Templates.jsp | 13 +- .../webapp/WEB-INF/jsps/editor/ThemeEdit.jsp | 2 +- .../editor/AuthoringUiSinkAuditTest.java | 169 ++++++++++++++++++ 12 files changed, 332 insertions(+), 64 deletions(-) create mode 100644 app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/ajax/ThemeDataServlet.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/ajax/ThemeDataServlet.java index 694c736ddf..bf395a2a0a 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/ajax/ThemeDataServlet.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/ajax/ThemeDataServlet.java @@ -17,6 +17,7 @@ */ package org.apache.roller.weblogger.ui.struts2.ajax; +import org.apache.commons.text.StringEscapeUtils; import org.apache.roller.weblogger.WebloggerException; import org.apache.roller.weblogger.business.WebloggerFactory; import org.apache.roller.weblogger.business.themes.SharedTheme; @@ -80,17 +81,21 @@ public void doGet( } for (Iterator it = themes.iterator(); it.hasNext();) { SharedTheme theme = it.next(); + // Theme metadata comes from theme.xml, which an operator can edit + // or install; escape it so a quote or newline cannot break out of + // the string and produce malformed JSON. pw.print(" { \"id\" : \""); - pw.print(theme.getId()); + pw.print(StringEscapeUtils.escapeJson(theme.getId())); pw.print("\", "); pw.print("\"name\" : \""); - pw.print(theme.getName()); + pw.print(StringEscapeUtils.escapeJson(theme.getName())); pw.print("\", "); pw.print("\"description\" : \""); - pw.print(theme.getDescription()); + pw.print(StringEscapeUtils.escapeJson(theme.getDescription())); pw.print("\", "); pw.print("\"previewPath\" : \""); - pw.print("/themes" + "/" + theme.getId() + "/" + theme.getPreviewImage().getPath()); + pw.print(StringEscapeUtils.escapeJson( + "/themes" + "/" + theme.getId() + "/" + theme.getPreviewImage().getPath())); pw.print("\" }"); if (it.hasNext()) { pw.println(", "); diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp index d004596b23..052ef91d4e 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp @@ -143,13 +143,13 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and - ', - '', - '', - '', - '', - '' )"> + " + data-bookmark-name="" + data-bookmark-url="" + data-bookmark-feed-url="" + data-bookmark-description="" + data-bookmark-image=""> @@ -338,7 +338,7 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and function confirmDeleteFolder() { $('#boomarks_delete_folder_folderId').val($('#bookmarks_folderId:first').val()); - $('#deleteBlogrollName').html(''); + $('#deleteBlogrollName').text(''); $('#delete-blogroll-modal').modal({show: true}); } @@ -700,12 +700,24 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and $('#bookmarkEdit_bean_image:first').val(''); $('#bookmarkEdit_bean_feedUrl:first').val(''); - $('#subtitle_folder_name:first').html(originalName); + $('#subtitle_folder_name:first').text(originalName); $('#addedit-bookmark-modal').modal({show: true}); } + // Author-supplied values travel in data attributes and are read back as + // text rather than being interpolated into inline handlers. + $(document).on('click', '.bookmark-edit-link', function (event) { + event.preventDefault(); + editBookmark($(this).attr('data-bookmark-id'), + $(this).attr('data-bookmark-name'), + $(this).attr('data-bookmark-url'), + $(this).attr('data-bookmark-feed-url'), + $(this).attr('data-bookmark-description'), + $(this).attr('data-bookmark-image')); + }); + function editBookmark(id, name, url, feedUrl, description, image) { var saveBookmarkButton = $('#save_bookmark:first'); @@ -725,7 +737,7 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and $('#bookmarkEdit_bean_description:first').val(description); $('#bookmarkEdit_bean_image:first').val(image); - $('#subtitle_folder_name:first').html(originalName); + $('#subtitle_folder_name:first').text(originalName); $('#addedit-bookmark-modal').modal({show: true}); } @@ -770,7 +782,7 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and elem.removeClass("alert-info"); elem.removeClass("alert-danger"); elem.addClass("alert-success"); - elem.html(message); + elem.text(message); } else { saveBookmarkButton.attr("disabled", true); @@ -789,7 +801,7 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and elem.removeClass("alert-info"); elem.removeClass("alert-success"); elem.addClass("alert-danger"); - elem.html(message); + elem.text(message); } } diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp index 6e1d649bd2..fbc8afe216 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp @@ -55,11 +55,11 @@ - ', - '', - '', - '' )"> + " + data-category-name="" + data-category-desc="" + data-category-image=""> @@ -71,10 +71,10 @@ - ', - '', - )" > + " + data-category-name="" + data-category-in-use=""> @@ -288,7 +288,7 @@ function showCategoryDeleteModal( id, name, inUse ) { $('#categoryRemove_removeId').val(id); $('#categoryEdit_bean_name').val(name); - $('#category-name').html(name); + $('#category-name').text(name); if ( inUse ) { $('#category-in-use').css('display','block'); $('#category-emtpy').css('display', 'none'); @@ -301,14 +301,15 @@ } function populateCategorySelect(removeId) { - const allCategories = []; - - - allCategories.push({ - id: '', - name: '' - }); - + // Category names are author-supplied. They are rendered into data + // attributes below and read back as text here, so no name is ever + // parsed as JavaScript. + const allCategories = $('#category-option-data .category-option').map(function () { + return { + id: $(this).attr('data-category-id'), + name: $(this).attr('data-category-name') + }; + }).get(); const select = $('#categoryRemove_targetCategoryId'); select.empty(); @@ -319,4 +320,31 @@ }); } + // Author-supplied values travel in data attributes and are read back as + // text rather than being interpolated into inline handlers. + $(document).on('click', '.category-edit-link', function (event) { + event.preventDefault(); + showCategoryEditModal($(this).attr('data-category-id'), + $(this).attr('data-category-name'), + $(this).attr('data-category-desc'), + $(this).attr('data-category-image')); + }); + + $(document).on('click', '.category-delete-link', function (event) { + event.preventDefault(); + showCategoryDeleteModal($(this).attr('data-category-id'), + $(this).attr('data-category-name'), + $(this).attr('data-category-in-use') === 'true'); + }); + + +<%-- Source data for the "move entries to" select, carried as escaped + attributes rather than generated JavaScript literals. --%> + diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp index 438b1ed006..34acbfa867 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp @@ -140,8 +140,9 @@ - ', '' )"> + " + data-post-title=""> @@ -238,9 +239,16 @@ 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..83b2ea3554 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp @@ -291,9 +291,10 @@ <%-- delete --%> - ', '' )"> + data-post-id="" + data-post-title=""> @@ -436,10 +437,17 @@ }); function showDeleteModal(postId, postTitle) { - $('#postIdLabel').html(postId); - $('#postTitleLabel').html(postTitle); + $('#postIdLabel').text(postId); + $('#postTitleLabel').text(postTitle); $('#removeId').val(postId); $('#delete-entry-modal').modal({show: true}); } + // Author-supplied values travel in data attributes and are read back as + // text rather than being interpolated into inline handlers. + $(document).on('click', '.entry-delete-button', function (event) { + event.preventDefault(); + showDeleteModal($(this).attr('data-post-id'), $(this).attr('data-post-title')); + }); + 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..87b90aefbc 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp @@ -95,8 +95,8 @@
- ')"/> + "/>
@@ -201,6 +201,12 @@ return false; } + // Author-supplied values travel in data attributes and are read back as + // text rather than being interpolated into inline handlers. + $(document).on('change', '.enclosure-choice', function () { + setEnclosure($(this).attr('data-enclosure-url')); + }); + function setEnclosure(url) { $("#enclosureURL").get(0).value = url; if (isImageChecked()) { diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp index dddd4d07ad..db6c0d17ba 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp @@ -72,10 +72,10 @@
  • -
    ', - '', - '')"> +
    " + data-mediafile-url="" + data-mediafile-is-image=""> -
    ', - '' )"> +
    " + data-mediafile-name=""> <s:property value="#mediaFile.name" />')" --%> /> + /> @@ -187,7 +187,7 @@ <s:property value="#mediaFile.name"/>')" --%> /> + />
    @@ -223,9 +223,9 @@
  • -
    ', - '' )"> +
    " + data-mediafile-name=""> - $('#edit-subtitle').html(mediaFileName); + $('#edit-subtitle').text(mediaFileName); $('#mediaFileEditor').attr('src', '' + '&mediaFileId=' + mediaFileId); $('#mediafile_edit_lightbox').modal({show: true}); } + // Author-supplied values travel in data attributes and are read back as + // text rather than being interpolated into inline handlers. + $(document).on('click', '.mediafile-edit-target', function () { + onClickEdit($(this).attr('data-mediafile-id'), + $(this).attr('data-mediafile-name')); + }); + function onEditSuccess() { onEditCancelled(); document.mediaFileViewForm.submit(); diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/TemplateEdit.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/TemplateEdit.jsp index 4b738d3f8a..81b10e6421 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/TemplateEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/TemplateEdit.jsp @@ -177,11 +177,19 @@ + + diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/ThemeEdit.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/ThemeEdit.jsp index dac1ec8855..f71d9de08c 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/ThemeEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/ThemeEdit.jsp @@ -246,7 +246,7 @@ $.ajax({ url: "", data: {theme: themeId}, success: function (data) { - $('#themeDescription').html(data.description); + $('#themeDescription').text(data.description); thumbnail = $('#themeThumbnail'); thumbnail.attr('src', '' + data.previewPath); } diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java new file mode 100644 index 0000000000..2adf64037b --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java @@ -0,0 +1,169 @@ +/* + * 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.struts2.editor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Structural audit of the authoring UI templates. + * + *

    Values that originate from weblog content are rendered by the editor JSPs. + * Struts <s:property> HTML-escapes its output but does not + * escape the apostrophe, so such a value placed inside a single-quoted + * JavaScript string literal terminates the literal. Likewise, a value handed to + * jQuery .html() is parsed as markup even when it reached the page + * safely. + * + *

    This test enforces the two structural rules that keep those values inert: + * they travel in double-quoted data-* attributes rather than inline + * handler literals, and they are written to the DOM through a text API. It is a + * source audit rather than a behavioural test because the guarantee is a + * property of the whole page family, not of any one code path. + */ +public class AuthoringUiSinkAuditTest { + + private static final Path EDITOR_JSP_DIR = + Paths.get("src", "main", "webapp", "WEB-INF", "jsps", "editor"); + + /** + * An inline event handler attribute whose body opens a single-quoted + * JavaScript string containing a Struts property. Newlines are collapsed + * before matching because these handlers routinely wrap across lines. + */ + private static final Pattern HANDLER_LITERAL = + Pattern.compile("on[a-zA-Z]+\\s*=\\s*\"[^\"]*'\\s* EXCLUDED_FILES = + new HashSet<>(Arrays.asList("Comments.jsp")); + + private List editorJsps() throws IOException { + Path dir = EDITOR_JSP_DIR; + assertTrue(Files.isDirectory(dir), "cannot locate editor JSPs at " + + dir.toAbsolutePath() + " (run from the app module)"); + try (Stream files = Files.list(dir)) { + return files.filter(p -> p.getFileName().toString().endsWith(".jsp")) + .filter(p -> !EXCLUDED_FILES.contains(p.getFileName().toString())) + .sorted() + .collect(Collectors.toList()); + } + } + + private static String flatten(String source) { + return source.replace('\n', ' ').replace('\r', ' '); + } + + private List findMatches(Pattern pattern, boolean flattenSource) throws IOException { + List offenders = new ArrayList<>(); + for (Path jsp : editorJsps()) { + String body = new String(Files.readAllBytes(jsp), StandardCharsets.UTF_8); + String haystack = flattenSource ? flatten(body) : body; + Matcher matcher = pattern.matcher(haystack); + while (matcher.find()) { + String snippet = matcher.group().replaceAll("\\s+", " ").trim(); + offenders.add(jsp.getFileName() + ": " + snippet); + } + } + return offenders; + } + + /** + * No weblog-controlled value may sit inside a single-quoted JavaScript + * string literal in an inline event handler. An apostrophe in the value + * closes the literal and the rest of the value is parsed as code. + */ + @Test + public void noStrutsPropertyInsideInlineHandlerLiteral() throws IOException { + List offenders = findMatches(HANDLER_LITERAL, true); + assertTrue(offenders.isEmpty(), + "authoring values must travel in data-* attributes, not inline " + + "handler string literals; found " + offenders.size() + ":\n " + + String.join("\n ", offenders)); + } + + /** + * The same rule for values assigned into script variables, which are the + * non-handler form of the identical defect. + */ + @Test + public void noStrutsPropertyInsideScriptVariableLiteral() throws IOException { + List offenders = findMatches(SCRIPT_VAR_LITERAL, false); + assertTrue(offenders.isEmpty(), + "authoring values must reach script through data-* attributes, " + + "not single-quoted var initialisers; found " + offenders.size() + ":\n " + + String.join("\n ", offenders)); + } + + /** + * No dynamic value may be written to the DOM as markup. Moving a value into + * a data attribute does not help if a later html() call re-parses it. + */ + @Test + public void noDynamicHtmlWrites() throws IOException { + List offenders = findMatches(HTML_WRITE, false); + assertTrue(offenders.isEmpty(), + "dynamic values must be written with a text API (.text(), " + + ".val(), textContent) rather than .html(); found " + + offenders.size() + ":\n " + String.join("\n ", offenders)); + } + + /** + * Guards the audit itself: if the JSP directory moved or the patterns stopped + * matching anything at all, the three tests above would pass vacuously. + */ + @Test + public void auditActuallyInspectsTheEditorTemplates() throws IOException { + List jsps = editorJsps(); + assertFalse(jsps.isEmpty(), "audit found no editor JSPs to inspect"); + assertTrue(jsps.size() >= 10, + "expected the editor template family, found only " + jsps.size()); + } +} From 713aba2ec9b19b8bab2e3d9775eb5732661f06ed Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sat, 29 Aug 2026 16:37:59 -0400 Subject: [PATCH 2/2] Double-quote authoring UI media attributes and tighten the sink audit The media gallery rendered the media file name into single-quoted HTML attributes; those attributes are now double-quoted so the value stays data. The sink-audit test description now states the rule it enforces, and the repeated per-page comment is collapsed to one line. Claude-Session: https://claude.ai/code/session_01A1fhY1E2PCFU6UAPXu2WtV --- .../webapp/WEB-INF/jsps/editor/Bookmarks.jsp | 3 +- .../webapp/WEB-INF/jsps/editor/Categories.jsp | 3 +- .../webapp/WEB-INF/jsps/editor/Entries.jsp | 3 +- .../webapp/WEB-INF/jsps/editor/EntryEdit.jsp | 3 +- .../jsps/editor/MediaFileAddSuccess.jsp | 3 +- .../jsps/editor/MediaFileImageChooser.jsp | 11 +++---- .../WEB-INF/jsps/editor/MediaFileView.jsp | 33 +++++++++---------- .../webapp/WEB-INF/jsps/editor/Templates.jsp | 3 +- .../editor/AuthoringUiSinkAuditTest.java | 10 ++---- 9 files changed, 29 insertions(+), 43 deletions(-) diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp index 052ef91d4e..cc5e3f4619 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Bookmarks.jsp @@ -706,8 +706,7 @@ We used to call them Bookmarks and Folders, now we call them Blogroll links and } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.bookmark-edit-link', function (event) { event.preventDefault(); editBookmark($(this).attr('data-bookmark-id'), diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp index fbc8afe216..e0f7167164 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Categories.jsp @@ -320,8 +320,7 @@ }); } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.category-edit-link', function (event) { event.preventDefault(); showCategoryEditModal($(this).attr('data-category-id'), diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp index 34acbfa867..db793bb43f 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp @@ -245,8 +245,7 @@ $('#delete-entry-modal').modal({show: true}); } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.entry-delete-link', function (event) { event.preventDefault(); showDeleteModal($(this).attr('data-post-id'), $(this).attr('data-post-title')); 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 83b2ea3554..e10ade7fe7 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/EntryEdit.jsp @@ -443,8 +443,7 @@ $('#delete-entry-modal').modal({show: true}); } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.entry-delete-button', function (event) { event.preventDefault(); showDeleteModal($(this).attr('data-post-id'), $(this).attr('data-post-title')); 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 87b90aefbc..a8a9572a9b 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileAddSuccess.jsp @@ -201,8 +201,7 @@ return false; } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('change', '.enclosure-choice', function () { setEnclosure($(this).attr('data-enclosure-url')); }); diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp index db6c0d17ba..c02dcb64ab 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileImageChooser.jsp @@ -78,10 +78,10 @@ data-mediafile-is-image=""> - <s:property value="#mediaFile.name" /> + <s:property value='#mediaFile.name' /> @@ -118,8 +118,7 @@ window.parent.onSelectMediaFile(name, url, isImage); } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.mediafile-select-target', function () { onSelectMediaFile($(this).attr('data-mediafile-name'), $(this).attr('data-mediafile-url'), diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileView.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileView.jsp index a1cf16cee6..46a02002b7 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileView.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/MediaFileView.jsp @@ -19,7 +19,7 @@ - + @@ -174,19 +174,19 @@ data-mediafile-name=""> - <s:property value="#mediaFile.name" /> - <s:property value="#mediaFile.name"/> @@ -228,17 +228,17 @@ data-mediafile-name=""> - <s:property value="#mediaFile.name"/> + <s:property value='#mediaFile.name'/> - <s:property value="#mediaFile.name"/> + <s:property value='#mediaFile.name'/>

    @@ -353,8 +353,7 @@ $('#mediafile_edit_lightbox').modal({show: true}); } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.mediafile-edit-target', function () { onClickEdit($(this).attr('data-mediafile-id'), $(this).attr('data-mediafile-name')); diff --git a/app/src/main/webapp/WEB-INF/jsps/editor/Templates.jsp b/app/src/main/webapp/WEB-INF/jsps/editor/Templates.jsp index c534ef85d0..d5e36ac0df 100644 --- a/app/src/main/webapp/WEB-INF/jsps/editor/Templates.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/editor/Templates.jsp @@ -109,8 +109,7 @@ } } - // Author-supplied values travel in data attributes and are read back as - // text rather than being interpolated into inline handlers. + // Values come from data-* attributes and are bound via delegated listeners. $(document).on('click', '.template-delete-link', function (event) { event.preventDefault(); confirmTemplateDelete($(this).attr('data-template-id'), diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java index 2adf64037b..717381e55d 100644 --- a/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/ui/struts2/editor/AuthoringUiSinkAuditTest.java @@ -41,14 +41,8 @@ * Structural audit of the authoring UI templates. * *

    Values that originate from weblog content are rendered by the editor JSPs. - * Struts <s:property> HTML-escapes its output but does not - * escape the apostrophe, so such a value placed inside a single-quoted - * JavaScript string literal terminates the literal. Likewise, a value handed to - * jQuery .html() is parsed as markup even when it reached the page - * safely. - * - *

    This test enforces the two structural rules that keep those values inert: - * they travel in double-quoted data-* attributes rather than inline + * This test enforces the two structural rules that keep those values inert: they + * travel in double-quoted data-* attributes rather than inline * handler literals, and they are written to the DOM through a text API. It is a * source audit rather than a behavioural test because the guarantee is a * property of the whole page family, not of any one code path.