File: //usr/src/csf/csf/csf.min.js
/*
@app ConfigServer Security & Firewall (CSF)
Login Failure Daemon (LFD)
@website https://configserver.dev
@docs https://docs.configserver.dev
@download https://download.configserver.dev
@repo https://github.com/Aetherinox/csf-firewall
@copyright Copyright (C) 2025-2026 Aetherinox
Copyright (C) 2006-2025 Jonathan Michaelson
Copyright (C) 2006-2025 Way to the Web Ltd.
@license GPLv3
@updated 02.27.2026
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see <https://www.gnu.org/licenses>.
*/
document.addEventListener( 'DOMContentLoaded', ( ) =>
{
/*
Global › Debug toggle
Set to true to enable console logs for development.
*/
const bDebug = false;
const allowedScrollbars = [ 'cpanel', 'generic' ];
const codename = typeof csfCodename !== 'undefined' ? csfCodename : 'default';
/*
Footer › Positioning & Parameters
Since control panels like cPanel have set a flex container with a side navigation,
we'll try to calculate the width and set our footer bar to a similar size. Otherwise,
our footer bar goes under the left-side menu.
Also has support for extra width padding, and margin.
*/
function adjustFooterWidth( )
{
const footer = document.querySelector( '.footer' );
if ( !footer ) return;
const container =
document.querySelector( '.cp-layout-header' ) ||
document.querySelector( '.cp-layout-wrapper' ) ||
document.querySelector( '.csf-container' ) ||
document.querySelector( '.container-fluid' ) ||
document.getElementById( 'pageContainer' ) ||
document.getElementById( 'normalcontainer' );
if ( container )
{
const rect = container.getBoundingClientRect( );
/*
Offset › Map
Add width or left margin offset to footer depending on which parent container we found.
cPanel has the little notification / alert button in the bottom-right.
*/
const offsetMap =
[
{ selector: '.cp-layout-header', width: 22, left: 0 },
{ selector: '.cp-layout-wrapper', width: 22, left: 0 },
{ selector: '.csf-container', width: 0, left: 0 },
{ selector: '#pageContainer', width: 0, left: 10 },
{ selector: '#normalcontainer', width: 0, left: 0 },
];
/*
Offset › Defaults
Adjust width or margin as needed to match visual layout.
*/
let extraWidth = 0;
let extraLeft = 0;
/*
Find the first matching rule
*/
for ( const rule of offsetMap )
{
if ( container.matches( rule.selector ) )
{
extraWidth = rule.width || 0;
extraLeft = rule.left || 0;
break;
}
}
/*
Apply new size with offsets
*/
const newWidth = rect.width + extraWidth;
const newLeft = rect.left + extraLeft;
Object.assign( footer.style,
{
left: newLeft + 'px',
right: (window.innerWidth - (rect.right + extraWidth)) + 'px',
width: 'auto',
marginLeft: '0',
boxSizing: 'border-box'
});
/*
Debug › Found container
*/
if ( bDebug )
{
console.log(
`[adjustFooterWidth] Found container: ${
container.id ? '#' + container.id : '.' + container.className.split(' ')[0]
} (width=${rect.width}px + offset=${extraWidth}, left=${extraLeft})`
);
}
}
else
{
/*
Debug › Report container not found
*/
if ( bDebug )
{
console.log(
`[adjustFooterWidth] Could not find container to attach to.`
);
}
/*
Footer › Fallback
*/
footer.style.width = '';
footer.style.marginLeft = '';
}
}
/*
Run on load and when resized
*/
window.addEventListener( 'load', adjustFooterWidth );
window.addEventListener( 'resize', adjustFooterWidth );
/*
Optional: observe layout shifts if cPanel dynamically resizes its panel
*/
if ( typeof ResizeObserver !== 'undefined' )
{
const container =
document.querySelector( '.cp-layout-header' ) ||
document.querySelector( '.cp-layout-wrapper' ) ||
document.getElementById( 'pageContainer' ) ||
document.getElementById( 'normalcontainer' );
if ( container )
{
const ro = new ResizeObserver( adjustFooterWidth );
ro.observe( container );
}
}
/*
Custom page scrollbar
Originally a royal pain. Must ensure scrollbar reaches bottom of page
but not overlap over the footer, otherwise things just look off.
Custom scrollbars can be a royal pain in the arse.
*/
if ( allowedScrollbars.includes( codename ) )
{
const bodyChildren = Array.from( document.body.childNodes );
/*
Scrollbar Container
*/
const divPage = document.createElement( 'div' );
divPage.className = 'csf-container';
document.body.replaceChildren( divPage );
/*
Move all original children into new scrollbar container divPage
*/
bodyChildren.forEach( node => divPage.appendChild( node ) );
/*
Create scrollbar
*/
const scrollbar = document.createElement( 'div' );
scrollbar.className = 'csf-scrollbar';
document.body.appendChild( scrollbar );
/*
Styleize body container
*/
Object.assign( document.body.style,
{
overflow: 'hidden',
position: 'relative',
margin: '0',
height: '100vh'
});
/*
Helper: measure footer height
*/
const getFooterHeight = ( ) =>
{
const f = document.querySelector( '.footer' );
return f ? Math.ceil( f.getBoundingClientRect( ).height ) : 0;
};
/*
Apply layout to divPage so the scroll area ends above the footer.
*/
const applyDivPageLayout = ( ) =>
{
const fh = getFooterHeight( );
Object.assign( divPage.style,
{
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
overflowY: 'auto',
paddingBottom: fh + 'px',
boxSizing: 'border-box',
});
};
/*
Update scrollbar thumb based on scroll position
Also ensure we take the height of the footer into account (fh), otherwise the
scrollbar will just overlap and looks horrible.
*/
const updateScrollbar = ( ) =>
{
const fh = getFooterHeight( ) + 15;
const viewportHeight = divPage.clientHeight - fh;
const contentHeight = divPage.scrollHeight;
const ratio = viewportHeight / contentHeight;
const thumbHeight = Math.max(ratio * viewportHeight, 30);
const maxScroll = contentHeight - viewportHeight;
const maxTop = viewportHeight - thumbHeight;
const desiredTop = maxScroll > 0 ? (divPage.scrollTop / maxScroll) * maxTop : 0;
scrollbar.style.height = thumbHeight + 'px';
scrollbar.style.top = desiredTop + 'px';
};
/*
Apply layout initially
*/
applyDivPageLayout( );
updateScrollbar( );
/*
Optional: hide trailing spacer divs
*/
(function hideBottomSpacerIfPresent( )
{
const spacer = divPage.querySelector( 'div[style*="padding-bottom"]' );
if ( spacer ) spacer.style.display = 'none';
})( );
/*
Event listeners
*/
divPage.addEventListener( 'scroll', updateScrollbar );
window.addEventListener( 'resize', ( ) =>
{
applyDivPageLayout( );
updateScrollbar( );
});
window.addEventListener( 'load', ( ) =>
{
applyDivPageLayout( );
updateScrollbar( );
});
/*
Observe layout changes for dynamic content or footer changes
*/
if ( typeof ResizeObserver !== 'undefined' )
{
const ro = new ResizeObserver(( ) =>
{
applyDivPageLayout( );
updateScrollbar( );
});
ro.observe( divPage );
const footerEl = document.querySelector( '.footer' );
if ( footerEl ) ro.observe( footerEl );
}
/*
Dragging logic
*/
let isDragging = false;
let startY = 0;
let startScroll = 0;
scrollbar.addEventListener( 'mousedown', e =>
{
isDragging = true;
startY = e.clientY;
startScroll = divPage.scrollTop;
document.body.style.userSelect = 'none';
});
window.addEventListener( 'mousemove', e =>
{
if (!isDragging) return;
const fh = getFooterHeight( );
const viewportHeight = divPage.clientHeight - fh;
const contentHeight = divPage.scrollHeight;
const maxScroll = Math.max(0, contentHeight - viewportHeight);
const ratio = viewportHeight / contentHeight;
const thumbHeight = Math.max(ratio * viewportHeight, 30);
const maxTop = Math.max(0, viewportHeight - thumbHeight);
const delta = e.clientY - startY;
const scrollDelta = maxTop > 0 ? delta * (maxScroll / maxTop) : 0;
const nextScroll = Math.min(Math.max(0, startScroll + scrollDelta), maxScroll);
divPage.scrollTop = nextScroll;
updateScrollbar( );
});
window.addEventListener('mouseup', ( ) =>
{
isDragging = false;
document.body.style.userSelect = '';
});
/*
Initial sync
*/
updateScrollbar( );
}
/*
csfapp › dropdown
*/
document.addEventListener( 'click', function( e )
{
var a = e.target.closest && e.target.closest( '.form-csfapp-dropdown .dropdown-menu a' );
if ( !a )
return;
e.preventDefault( );
var value = a.getAttribute( 'data-value' );
var form = a.closest( '.form-csfapp-dropdown' );
if ( !form )
return;
var hid = form.querySelector( 'input[name="csfapp"]' );
if ( hid )
{
hid.value = value;
console.log( 'Hidden input updated → csfapp=' + hid.value);
}
var sel = form.querySelector( '.selected-text' );
if ( sel )
{
sel.textContent = value;
console.log( 'Displayed text updated → ' + value );
}
}, false);
/*
csfapp › Catch form submissions
*/
document.addEventListener( 'submit', function( e )
{
var form = e.target.closest( '.form-csfapp-dropdown' );
if ( form )
{
var hid = form.querySelector( 'input[name="csfapp"]' );
if ( hid )
{
console.log( 'Form will submit csfapp=' + hid.value );
}
}
}, false);
/*
"All" tab
ensure that when clicking each category, the correct section appears.
*/
const allTabBtn = document.getElementById( 'tabAll' );
const tabPanes = document.querySelectorAll( '.tab-pane' );
const tabLinks = document.querySelectorAll( '#myTabs a' );
/*
Certain pages do not contain this element; verify first
*/
if ( allTabBtn )
{
allTabBtn.addEventListener( 'click', ( e ) =>
{
e.preventDefault( );
tabLinks.forEach( link => link.parentElement.classList.remove( 'active' ) ); // deactivate all tab links
allTabBtn.parentElement.classList.add( 'active' ); // activate 'All' tab link
tabPanes.forEach( pane => pane.classList.add( 'active' ) ); // show all panes
});
/*
handle other tab clicks normally
*/
tabLinks.forEach( link =>
{
if ( link.id !== 'tabAll' )
{
link.addEventListener( 'click', ( ) =>
{
allTabBtn.parentElement.classList.remove( 'active' ); // remove 'All' tab active class
tabPanes.forEach( p => p.classList.remove( 'active') ) ; // remove 'active' from all panes
/*
activate the clicked pane
*/
const targetId = link.getAttribute( 'href' ).substring(1);
const targetPane = document.getElementById( targetId );
if ( targetPane )
targetPane.classList.add( 'active' );
});
}
});
}
/*
Footer › Button › Switch Theme
*/
const btnTheme = document.getElementById( 'btn-theme' );
const html = document.documentElement;
/*
Only process if btnTheme exists
*/
if ( btnTheme )
{
/*
load saved theme or fallback to light
*/
const savedTheme = localStorage.getItem( 'theme' ) || 'light';
html.setAttribute( 'data-theme', savedTheme );
/*
update button icon + tooltip
*/
function doUpdateThemeBtn( theme )
{
const icon = document.createElement( 'i' );
icon.className = theme === 'dark' ? 'ax-solid ax-sun' : 'ax-solid ax-moon';
btnTheme.replaceChildren( icon );
btnTheme.title = theme === 'dark'
? 'Switch to Light Theme'
: 'Switch to Dark Theme';
}
/*
Initial load
*/
doUpdateThemeBtn( savedTheme );
btnTheme.addEventListener( 'click', ( ) =>
{
const current = html.getAttribute( 'data-theme' );
const next = current === 'dark' ? 'light' : 'dark';
/*
transition css class to fade theme switches
*/
html.classList.add( 'theme-transition' );
try
{
if ( window.top && window.top.document && window.top.document.documentElement )
window.top.document.documentElement.classList.add( 'theme-transition' );
} catch ( e ) { }
/*
apply theme
*/
html.setAttribute( 'data-theme', next );
try
{
if ( window.top && window.top.document && window.top.document.documentElement )
window.top.document.documentElement.setAttribute( 'data-theme', next );
} catch ( e ) { }
localStorage.setItem( 'theme', next );
if ( codename === 'cyberpanel' )
localStorage.setItem( 'cyberPanelTheme', next );
doUpdateThemeBtn( next );
/*
remove transition css class after animation finishes
set slightly longer than CSS transition (0.1s)
*/
setTimeout(( ) =>
{
html.classList.remove( 'theme-transition' );
try
{
if ( window.top && window.top.document && window.top.document.documentElement )
window.top.document.documentElement.classList.remove( 'theme-transition' );
} catch ( e ) { }
}, 200);
});
}
/*
Integration › CyberPanel › Theme Sync
Cyberpanel contains its own theme button in header. We include one in the footer of CSF.
Sync these buttons so that both of them change the theme for everythin together.
*/
if ( codename === 'cyberpanel' )
{
( function ( )
{
function applyTheme( theme )
{
document.documentElement.setAttribute( 'data-theme', theme );
try
{
if ( window.top && window.top.document && window.top.document.documentElement )
window.top.document.documentElement.setAttribute( 'data-theme', theme );
} catch ( e ) { }
}
/*
Apply initial theme
*/
let theme = localStorage.getItem( 'cyberPanelTheme' ) || localStorage.getItem( 'theme' ) || 'light';
applyTheme( theme );
/*
Listen for changes to localStorage (fires across tabs/windows)
*/
window.addEventListener( 'storage', ( env ) =>
{
if ( env.key === 'cyberPanelTheme' || env.key === 'theme' )
applyTheme( env.newValue );
});
/*
Poll for changes if host doesn't trigger 'storage'
*/
setInterval( ( ) =>
{
const current = localStorage.getItem( 'cyberPanelTheme' ) || localStorage.getItem( 'theme' ) || 'light';
if ( document.documentElement.getAttribute( 'data-theme' ) !== current )
applyTheme( current );
}, 500 );
})( );
}
/*
remove "no-transition" class once page has loaded
*/
window.addEventListener( 'load', ( ) =>
{
html.classList.remove( 'no-transition' );
});
/*
Footer › Button › Logout
Sync logout link from header to footer
*/
const hdrLogout = document.querySelector('.header-right .btn-logout, .pull-right .btn.btn-default');
const btnLogout = document.getElementById('btn-logout');
if ( hdrLogout && btnLogout )
{
const refLogout = hdrLogout.getAttribute( 'href' );
btnLogout.addEventListener( 'click', ( ) =>
{
window.location.href = refLogout;
});
btnLogout.setAttribute( 'title', 'Logout of CSF' );
}
/*
Footer › Button
*/
const footerButtons =
{
'btn-sponsor': {
title: 'Sponsor this project',
url: 'https://buymeacoffee.com/aetherinox'
},
'btn-blocklist': {
title: 'Blocklist Service',
url: 'https://blocklist.configserver.dev/help'
},
'btn-status': {
title: 'View Status',
url: 'https://status.configserver.dev/status/services'
},
'btn-repo': {
title: 'Visit Github repo',
url: 'https://github.com/Aetherinox/csf-firewall'
},
};
for ( const [ id, { title, url } ] of Object.entries( footerButtons ) )
{
const btn = document.getElementById( id );
if ( !btn ) continue;
if ( bDebug )
{
console.log( 'Setting button ' + title );
}
btn.setAttribute( 'title', title );
btn.addEventListener( 'click', ( ) =>
{
window.open( url, '_blank' );
});
}
});