Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions xmppserver/src/main/java/org/jivesoftware/util/ListPager.java
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -88,6 +88,7 @@ public class ListPager<T> {
private final int sortOrder;
private final String[] additionalFormFields;
private final HttpServletRequest request;
private boolean inlineJsDisabled = false;

/**
* Creates a unfiltered list pager.
Expand Down Expand Up @@ -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("<select name='%s' onchange='return setPageSize(this.value);'>", REQUEST_PARAMETER_KEY_PAGE_SIZE));

sb.append(String.format("<select name='%s'", REQUEST_PARAMETER_KEY_PAGE_SIZE));

if (!inlineJsDisabled) {
sb.append(" onchange='return setPageSize(this.value);'");
}
sb.append(">");

for (final int optionSize : PAGE_SIZES) {
sb.append(String.format("<option value='%d'%s>%d</option>", optionSize, pageSize == optionSize ? " selected" : "", optionSize));
sb.append(String.format(
"<option value='%d'%s>%d</option>",
optionSize,
pageSize == optionSize ? " selected" : "",
optionSize
));
}
sb.append("</select>");

return sb.toString();
}

Expand Down Expand Up @@ -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<a href='%s' onclick='return jumpToPage(%d)'%s>%s</a>",
String.format("?%s=%d", REQUEST_PARAMETER_KEY_CURRENT_PAGE, pageToLink),
pageToLink,
cssClass,
pageToLink));
sb.append("\n<a href='");
sb.append(String.format("?%s=%d", REQUEST_PARAMETER_KEY_CURRENT_PAGE, pageToLink));
sb.append("'");

if (!inlineJsDisabled) {
sb.append(String.format(
" onclick='return jumpToPage(%d)'",
pageToLink
));
}

sb.append(cssClass);
sb.append(">");
sb.append(pageToLink);
sb.append("</a>");
}

/**
Expand Down Expand Up @@ -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
Expand Down
164 changes: 164 additions & 0 deletions xmppserver/src/main/webapp/js/list-pager.js
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading