diff --git a/xmppserver/src/main/java/org/jivesoftware/util/ListPager.java b/xmppserver/src/main/java/org/jivesoftware/util/ListPager.java index 8b066dc201..29db62aeb8 100644 --- a/xmppserver/src/main/java/org/jivesoftware/util/ListPager.java +++ b/xmppserver/src/main/java/org/jivesoftware/util/ListPager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2025 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2018-2026 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,6 +88,7 @@ public class ListPager { private final int sortOrder; private final String[] additionalFormFields; private final HttpServletRequest request; + private boolean inlineJsDisabled = false; /** * Creates a unfiltered list pager. @@ -228,16 +229,47 @@ public boolean isSortDescending() { return sortOrder == DESCENDING; } + /** + * @return {@code true} if inline JavaScript generation is disabled for this pager + */ + public boolean isInlineJsDisabled() { + return inlineJsDisabled; + } + + /** + * When set to {@code true}, methods like {@link #getPageSizeSelection()}, {@link #getPageLinks()}, + * and {@link #getPageFunctions()} will suppress inline JavaScript attributes (onclick, onchange) + * and inline script blocks. This allows pages to use external JavaScript files for CSP compliance. + * + * @param inlineJsDisabled {@code true} to suppress inline JavaScript output + */ + public void setInlineJsDisabled(boolean inlineJsDisabled) { + this.inlineJsDisabled = inlineJsDisabled; + } + /** * @return a string that contains HTML for selecting the page size */ public String getPageSizeSelection() { final StringBuilder sb = new StringBuilder(); - sb.append(String.format(""); + for (final int optionSize : PAGE_SIZES) { - sb.append(String.format("", optionSize, pageSize == optionSize ? " selected" : "", optionSize)); + sb.append(String.format( + "", + optionSize, + pageSize == optionSize ? " selected" : "", + optionSize + )); } sb.append(""); + return sb.toString(); } @@ -275,17 +307,28 @@ private void appendLinkToPages(final StringBuilder sb, final int firstPage, fina private void appendPageLink(final StringBuilder sb, final int pageToLink) { final String cssClass; + if (currentPage == pageToLink) { cssClass = " class='jive-current'"; } else { cssClass = ""; } - sb.append(String.format("\n%s", - String.format("?%s=%d", REQUEST_PARAMETER_KEY_CURRENT_PAGE, pageToLink), - pageToLink, - cssClass, - pageToLink)); + sb.append("\n"); + sb.append(pageToLink); + sb.append(""); } /** @@ -355,6 +398,9 @@ public String getJumpToPageForm() { * @return a string containing JavaScript that helps with navigation between pages */ public String getPageFunctions() { + if (inlineJsDisabled) { + return ""; + } final StringBuilder sb = new StringBuilder("\n") .append("\tvar additionalFormFields = ["); // The list of additional form fields diff --git a/xmppserver/src/main/webapp/js/list-pager.js b/xmppserver/src/main/webapp/js/list-pager.js new file mode 100644 index 0000000000..0367d1c8b1 --- /dev/null +++ b/xmppserver/src/main/webapp/js/list-pager.js @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. + * + * Licensed 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. + */ + +/** + * Client-side replacement for the inline JavaScript generated by + * {@code ListPager.getPageFunctions()}. + * + * To use, set {@code listPager.setInlineJsDisabled(true)} in the JSP + * and include this script. The pagination form must have: + * - id="paginationForm" + * - data-additional-form-fields='["field1","field2"]' (JSON array) + * - data-page-size='25' (initial page size for URL cleanup) + */ +const ListPager = (function() { + const PAGINATION_FORM_ID = 'paginationForm'; + const REQUEST_PARAMETER_KEY_PAGE_SIZE = 'listPagerPageSize'; + const REQUEST_PARAMETER_KEY_CURRENT_PAGE = 'listPagerCurrentPage'; + + function getForm() { + return document.getElementById(PAGINATION_FORM_ID); + } + + function getAdditionalFormFields() { + const form = getForm(); + if (!form) return []; + const fieldsJson = form.getAttribute('data-additional-form-fields'); + try { + return fieldsJson ? JSON.parse(fieldsJson) : []; + } catch (e) { + console.error("Error parsing additionalFormFields", e); + return []; + } + } + + function jumpToPage(pageNumber) { + const formObject = getForm(); + if (formObject) { + formObject[REQUEST_PARAMETER_KEY_CURRENT_PAGE].value = pageNumber; + submitForm(); + } + return false; + } + + function setPageSize(pageSize) { + const formObject = getForm(); + if (formObject) { + formObject[REQUEST_PARAMETER_KEY_PAGE_SIZE].value = pageSize; + submitForm(); + } + return false; + } + + function submitForm() { + const formObject = getForm(); + if (!formObject) return false; + + const additionalFormFields = getAdditionalFormFields(); + for (let i = 0; i < additionalFormFields.length; i++) { + const field = document.getElementById(additionalFormFields[i]); + if (field !== null) { + const formField = formObject[additionalFormFields[i]]; + if (!formField) { + continue; + } + if (typeof field !== 'object' || field.value === '') { + formField.disabled = true; + } else { + formField.disabled = false; + formField.value = field.value; + } + } + } + + // Disable unchanged page size or default page number to keep URL clean + const initialPageSize = formObject.getAttribute('data-page-size'); + if (formObject[REQUEST_PARAMETER_KEY_PAGE_SIZE].value === initialPageSize) { + formObject[REQUEST_PARAMETER_KEY_PAGE_SIZE].disabled = true; + } + if (formObject[REQUEST_PARAMETER_KEY_CURRENT_PAGE].value === '1') { + formObject[REQUEST_PARAMETER_KEY_CURRENT_PAGE].disabled = true; + } + + formObject.submit(); + return false; + } + + function inputFieldOnKeyDownEventListener(e) { + if (e.keyCode === 13) { + submitForm(); + return false; + } + } + + function inputFieldOnInputEventListener() { + if (this.value === '') { + submitForm(); + return false; + } + } + + function init() { + const form = getForm(); + if (!form) return; + + const additionalFormFields = getAdditionalFormFields(); + for (let i = 0; i < additionalFormFields.length; i++) { + const field = document.getElementById(additionalFormFields[i]); + if (field !== null && typeof field === 'object') { + field.onkeydown = inputFieldOnKeyDownEventListener; + field.addEventListener('input', inputFieldOnInputEventListener); + // Auto-submit when select dropdowns change (replaces inline onchange) + if (field.tagName === 'SELECT') { + field.addEventListener('change', function() { submitForm(); }); + } + } + } + + // Event delegation for pagination links + document.addEventListener('click', function(e) { + const anchor = e.target.closest('a'); + if (anchor && !anchor.onclick && anchor.href.includes(REQUEST_PARAMETER_KEY_CURRENT_PAGE)) { + try { + const url = new URL(anchor.href, window.location.origin); + const page = url.searchParams.get(REQUEST_PARAMETER_KEY_CURRENT_PAGE); + if (page) { + e.preventDefault(); + jumpToPage(page); + } + } catch (err) { + // Ignore URL parsing errors + } + } + }); + + // Event delegation for page size selector + document.addEventListener('change', function(e) { + if (e.target.name === REQUEST_PARAMETER_KEY_PAGE_SIZE && !e.target.onchange) { + setPageSize(e.target.value); + } + }); + } + + return { + init: init, + jumpToPage: jumpToPage, + setPageSize: setPageSize, + submitForm: submitForm + }; +})(); + +document.addEventListener('DOMContentLoaded', ListPager.init); diff --git a/xmppserver/src/main/webapp/js/system-properties.js b/xmppserver/src/main/webapp/js/system-properties.js new file mode 100644 index 0000000000..2980570ced --- /dev/null +++ b/xmppserver/src/main/webapp/js/system-properties.js @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. + * + * Licensed 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. + */ + +/** + * Page-specific JavaScript for system-properties.jsp. + * Replaces inline onclick/onchange handlers with event delegation + * for CSP compliance. + */ +(function() { + function getKey(imgObject) { + var row = imgObject.closest('tr'); + if (row) { + var span = row.querySelector('.nameColumn span'); + if (span) { + return span.textContent; + } + } + return ""; + } + + function doEdit(imgObject, hidden, encrypted, nullValue) { + document.getElementById("editPropertyName").value = getKey(imgObject); + var valueField = document.getElementById("editPropertyValue"); + + if (encrypted || hidden || nullValue) { + valueField.value = ""; + } else { + var row = imgObject.closest('tr'); + if (row) { + var valueSpan = row.querySelector('.valueColumn span'); + if (valueSpan) { + valueField.value = valueSpan.textContent; + } + } + } + + var defaultValueField = document.getElementById("defaultPropertyValue"); + var editRow = imgObject.closest('tr'); + if (editRow) { + var defaultValueCell = editRow.querySelectorAll('.valueColumn')[1]; + if (defaultValueCell) { + defaultValueField.innerText = defaultValueCell.textContent.trim(); + } + } + + document.getElementById(encrypted ? "editPropertyEncryptTrue" : "editPropertyEncryptFalse").checked = true; + document.getElementById("newPropertyTitle").style.display = "none"; + document.getElementById("editPropertyTitle").style.display = ""; + valueField.focus(); + valueField.setSelectionRange(0, 0); + window.scrollTo(0, document.body.scrollHeight); + } + + function doEncrypt(imgObject) { + var confirmMessage = document.getElementById('actionForm').getAttribute('data-encrypt-confirm'); + if (confirm(confirmMessage)) { + submitActionForm("encrypt", getKey(imgObject)); + } + } + + function doDelete(imgObject) { + var confirmMessage = document.getElementById('actionForm').getAttribute('data-delete-confirm'); + if (confirm(confirmMessage)) { + submitActionForm("delete", getKey(imgObject)); + } + } + + function submitEditForm(save) { + var action = save ? "save" : "cancel"; + var key = document.getElementById("editPropertyName").value; + if (key.trim() === "") { + return; + } + if (save) { + var value = document.getElementById("editPropertyValue").value; + var encrypt = document.getElementById("editPropertyEncryptTrue").checked; + submitActionForm(action, key, value, encrypt); + } else { + submitActionForm(action, key); + } + } + + function submitActionForm(action, key, value, encrypt) { + var form = document.getElementById("actionForm"); + form["action"].value = action; + form["key"].value = key; + if (typeof value !== "undefined") { + form["value"].value = value; + form["value"].disabled = false; + } else { + form["value"].disabled = true; + } + if (typeof encrypt !== "undefined") { + form["encrypt"].value = encrypt; + form["encrypt"].disabled = false; + } else { + form["encrypt"].disabled = true; + } + form.submit(); + } + + document.addEventListener('DOMContentLoaded', function() { + // Delegate click events for action icons in the property table + document.addEventListener('click', function(e) { + var target = e.target.closest('.clickable'); + if (!target) return; + + var src = target.getAttribute('src') || ''; + if (src.indexOf('edit-16x16.gif') > -1) { + var hidden = target.getAttribute('data-hidden') === 'true'; + var encrypted = target.getAttribute('data-encrypted') === 'true'; + var nullValue = target.getAttribute('data-null-value') === 'true'; + doEdit(target, hidden, encrypted, nullValue); + } else if (src.indexOf('add-16x16.gif') > -1) { + doEncrypt(target); + } else if (src.indexOf('delete-16x16.gif') > -1) { + doDelete(target); + } else if (target.getAttribute('alt') === 'Search') { + if (typeof ListPager !== 'undefined') { + ListPager.submitForm(); + } + } + }); + + // Save/Cancel buttons for the edit form + var saveBtn = document.getElementById('savePropertyBtn'); + if (saveBtn) { + saveBtn.addEventListener('click', function() { submitEditForm(true); }); + } + var cancelBtn = document.getElementById('cancelPropertyBtn'); + if (cancelBtn) { + cancelBtn.addEventListener('click', function() { submitEditForm(false); }); + } + }); +})(); diff --git a/xmppserver/src/main/webapp/system-properties.jsp b/xmppserver/src/main/webapp/system-properties.jsp index cf9a9f2565..2dde8483d7 100644 --- a/xmppserver/src/main/webapp/system-properties.jsp +++ b/xmppserver/src/main/webapp/system-properties.jsp @@ -1,6 +1,6 @@ <%-- - - - Copyright (C) 2019-2022 Ignite Realtime Foundation. All rights reserved. + - Copyright (C) 2019-2026 Ignite Realtime Foundation. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ +<% listPager.setInlineJsDisabled(true); %> @@ -30,19 +31,20 @@ <fmt:message key="server.properties.title"/> + +