Phase A — HTML removed from PHP files: - RateLimitMiddleware.php: heredoc moved to views/errors/429.php - public/index.php: inline HTML replaced with View::error() - AuthMiddleware.php: <script> redirect replaced with <meta refresh> Phase B — Inline CSS/JS extracted from views: - CSS: landing.css (459 lines), legal.css (shared by terms+privacy) - JS: 17 new files extracted from ~20 view files - main.js (layout sidebar + notifications) - dashboard-chart.js, server-show.js, server-services.js - nginx-manager.js, database-manager.js - terminal.js, team-show.js, team-index.js - server-list.js, server-permissions.js (shared by edit+create) - server-ssl.js, server-processes.js, system-users.js - admin-users.js, audit-log.js, admin-notifications.js, admin-security.js - notifications.js Total: -3264 lines from views, +288 lines in new assets
322 lines
12 KiB
JavaScript
322 lines
12 KiB
JavaScript
let currentEditFile = null;
|
|
let editorDirty = false;
|
|
let editorUndoStack = [];
|
|
let editorRedoStack = [];
|
|
let editorHistoryIndex = -1;
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
}
|
|
|
|
function updateLineNumbers() {
|
|
const ta = document.getElementById('editorContent');
|
|
const gutter = document.getElementById('editorGutter');
|
|
const lines = ta.value.split('\n').length;
|
|
const numbers = [];
|
|
for (let i = 1; i <= lines; i++) {
|
|
numbers.push('<span>' + i + '</span>');
|
|
}
|
|
gutter.innerHTML = numbers.join('');
|
|
}
|
|
|
|
function updateCursorPos() {
|
|
const ta = document.getElementById('editorContent');
|
|
const before = ta.value.substring(0, ta.selectionStart);
|
|
const lines = before.split('\n');
|
|
const line = lines.length;
|
|
const col = lines[lines.length - 1].length + 1;
|
|
document.getElementById('editorCursorPos').textContent = 'Ln ' + line + ', Col ' + col;
|
|
}
|
|
|
|
function highlightNginx(text) {
|
|
let h = text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/#(.*)$/gm, '<span class="hl-comment">#$1</span>')
|
|
.replace(/\b(server|location|listen|server_name|root|index|try_files|include|fastcgi_pass|proxy_pass|return|rewrite|set|if|error_page|ssl_certificate|ssl_certificate_key|ssl_protocols|ssl_ciphers|add_header|access_log|error_log|keepalive|gzip|proxy_set_header|proxy_redirect|client_max_body_size|autoindex)\b/g, '<span class="hl-keyword">$1</span>')
|
|
.replace(/\b(\d+)\b/g, '<span class="hl-number">$1</span>')
|
|
.replace(/\b(https?|fastcgi|unix):\/\/[^\s;{]+/g, '<span class="hl-string">$&</span>')
|
|
.replace(/'(.*?)'/g, '<span class="hl-string">\'$1\'</span>')
|
|
.replace(/(\$\w+)/g, '<span class="hl-var">$1</span>')
|
|
.replace(/\b(\w+\.\w+(?:\.\w+)*)\b/g, function(m) {
|
|
if (m.includes('com') || m.includes('org') || m.includes('net') || m.includes('io') || m.includes('local')) {
|
|
return '<span class="hl-string">' + m + '</span>';
|
|
}
|
|
return m;
|
|
});
|
|
return h;
|
|
}
|
|
|
|
function syncHighlight() {
|
|
const ta = document.getElementById('editorContent');
|
|
const code = document.getElementById('editorHighlightCode');
|
|
code.innerHTML = highlightNginx(ta.value) + '\n';
|
|
}
|
|
|
|
function pushHistory() {
|
|
const ta = document.getElementById('editorContent');
|
|
if (editorHistoryIndex >= 0 && editorUndoStack[editorHistoryIndex] === ta.value) return;
|
|
editorUndoStack = editorUndoStack.slice(0, editorHistoryIndex + 1);
|
|
editorUndoStack.push(ta.value);
|
|
editorRedoStack = [];
|
|
editorHistoryIndex = editorUndoStack.length - 1;
|
|
if (editorUndoStack.length > 100) {
|
|
editorUndoStack.shift();
|
|
editorHistoryIndex--;
|
|
}
|
|
}
|
|
|
|
function undoEditor() {
|
|
if (editorHistoryIndex <= 0) return;
|
|
editorRedoStack.push(editorUndoStack[editorHistoryIndex]);
|
|
editorHistoryIndex--;
|
|
const ta = document.getElementById('editorContent');
|
|
ta.value = editorUndoStack[editorHistoryIndex];
|
|
ta.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
function redoEditor() {
|
|
if (editorRedoStack.length === 0) return;
|
|
const val = editorRedoStack.pop();
|
|
editorUndoStack.push(val);
|
|
editorHistoryIndex++;
|
|
const ta = document.getElementById('editorContent');
|
|
ta.value = val;
|
|
ta.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
function nginxAction(action) {
|
|
const btn = event.target.closest('button');
|
|
const origText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Working...';
|
|
|
|
fetch('/servers/' + serverId + '/nginx', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: 'action=' + action + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showToast(data.success ? 'success' : 'error', data.message);
|
|
if (data.output) {
|
|
document.getElementById('nginxResultMessage').textContent = data.message;
|
|
document.getElementById('nginxResultOutputText').textContent = data.output;
|
|
document.getElementById('nginxResultOutput').style.display = 'block';
|
|
document.getElementById('nginxResultModal').style.display = 'flex';
|
|
}
|
|
if (data.success && ['restart', 'reload', 'start', 'stop'].includes(action)) {
|
|
setTimeout(() => location.reload(), 1500);
|
|
}
|
|
})
|
|
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
|
.finally(() => { btn.disabled = false; btn.innerHTML = origText; });
|
|
}
|
|
|
|
function editFile(file) {
|
|
currentEditFile = file;
|
|
editorUndoStack = [];
|
|
editorRedoStack = [];
|
|
editorHistoryIndex = -1;
|
|
editorDirty = false;
|
|
document.getElementById('editorFilePath').textContent = file;
|
|
document.getElementById('editorContent').value = 'Loading...';
|
|
document.getElementById('editorStatus').textContent = 'Loading...';
|
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
|
document.getElementById('editorFileSize').textContent = '';
|
|
document.getElementById('nginxEditorModal').style.display = 'flex';
|
|
setTimeout(() => document.getElementById('editorContent').focus(), 100);
|
|
|
|
fetch('/servers/' + serverId + '/nginx/file?file=' + encodeURIComponent(file))
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const ta = document.getElementById('editorContent');
|
|
ta.value = data.content;
|
|
document.getElementById('editorFileSize').textContent = (data.content.length / 1024).toFixed(1) + ' KB';
|
|
document.getElementById('editorStatus').textContent = 'Ready';
|
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
|
editorDirty = false;
|
|
updateLineNumbers();
|
|
syncHighlight();
|
|
pushHistory();
|
|
} else {
|
|
showToast('error', data.message);
|
|
closeEditor();
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('error', 'Failed to load file');
|
|
closeEditor();
|
|
});
|
|
|
|
const ta = document.getElementById('editorContent');
|
|
ta.oninput = function() {
|
|
editorDirty = true;
|
|
document.getElementById('editorStatus').textContent = 'Unsaved changes';
|
|
document.getElementById('editorDirtyIndicator').style.display = 'inline';
|
|
updateLineNumbers();
|
|
syncHighlight();
|
|
pushHistory();
|
|
};
|
|
ta.onscroll = function() {
|
|
document.getElementById('editorGutter').scrollTop = this.scrollTop;
|
|
document.getElementById('editorHighlight').scrollTop = this.scrollTop;
|
|
};
|
|
ta.onkeydown = function(e) {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const start = this.selectionStart;
|
|
const end = this.selectionEnd;
|
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
|
this.selectionStart = this.selectionEnd = start + 4;
|
|
this.dispatchEvent(new Event('input'));
|
|
}
|
|
};
|
|
ta.onmouseup = ta.onkeyup = function() {
|
|
updateCursorPos();
|
|
};
|
|
ta.addEventListener('paste', function() {
|
|
setTimeout(() => {
|
|
this.dispatchEvent(new Event('input'));
|
|
updateCursorPos();
|
|
}, 10);
|
|
});
|
|
}
|
|
|
|
function saveFile() {
|
|
const content = document.getElementById('editorContent').value;
|
|
document.getElementById('editorStatus').textContent = 'Saving...';
|
|
|
|
fetch('/servers/' + serverId + '/nginx/file', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: 'file=' + encodeURIComponent(currentEditFile) + '&content=' + encodeURIComponent(content) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showToast(data.success ? 'success' : 'error', data.message);
|
|
if (data.success) {
|
|
editorDirty = false;
|
|
document.getElementById('editorStatus').textContent = 'Saved';
|
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
|
closeEditor();
|
|
setTimeout(() => location.reload(), 500);
|
|
} else {
|
|
document.getElementById('editorStatus').textContent = 'Save failed';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('error', 'Save request failed');
|
|
document.getElementById('editorStatus').textContent = 'Error';
|
|
});
|
|
}
|
|
|
|
function deleteSite(site) {
|
|
if (!confirm("Permanently delete site '" + site + "'?\nThis will remove both the config file and site directory contents.")) return;
|
|
if (!confirm("Are you sure? This cannot be undone.")) return;
|
|
|
|
fetch('/servers/' + serverId + '/nginx/site', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: '_method=DELETE&site=' + encodeURIComponent(site) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showToast(data.success ? 'success' : 'error', data.message);
|
|
if (data.success) setTimeout(() => location.reload(), 500);
|
|
})
|
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
|
}
|
|
|
|
function toggleSite(site, enable) {
|
|
const action = enable ? 'enable' : 'disable';
|
|
if (!confirm((enable ? 'Enable' : 'Disable') + " site '" + site + "'?")) return;
|
|
|
|
fetch('/servers/' + serverId + '/nginx/toggle-site', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: 'site=' + encodeURIComponent(site) + '&enable=' + (enable ? '1' : '0') + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showToast(data.success ? 'success' : 'error', data.message);
|
|
if (data.success) setTimeout(() => location.reload(), 500);
|
|
})
|
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
|
}
|
|
|
|
function closeEditor() {
|
|
if (editorDirty && !confirm('Discard unsaved changes?')) return;
|
|
document.getElementById('nginxEditorModal').style.display = 'none';
|
|
currentEditFile = null;
|
|
editorDirty = false;
|
|
}
|
|
|
|
function closeNginxModal() {
|
|
document.getElementById('nginxResultModal').style.display = 'none';
|
|
document.getElementById('nginxResultOutput').style.display = 'none';
|
|
}
|
|
|
|
function openNewSiteModal() {
|
|
document.getElementById('newSiteForm').reset();
|
|
document.getElementById('siteServerName').value = '';
|
|
document.getElementById('sitePhp').checked = true;
|
|
document.getElementById('siteEnable').checked = true;
|
|
document.getElementById('nginxNewSiteModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeNewSiteModal() {
|
|
document.getElementById('nginxNewSiteModal').style.display = 'none';
|
|
}
|
|
|
|
function createSite(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('createSiteBtn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
|
|
|
const formData = new URLSearchParams(new FormData(document.getElementById('newSiteForm')));
|
|
formData.append('_csrf_token', getCsrfToken());
|
|
|
|
fetch('/servers/' + serverId + '/nginx/site', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: formData.toString(),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showToast(data.success ? 'success' : 'error', data.message);
|
|
if (data.success) {
|
|
closeNewSiteModal();
|
|
setTimeout(() => location.reload(), 800);
|
|
}
|
|
})
|
|
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
|
.finally(() => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-plus"></i> Create Site'; });
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') { closeEditor(); closeNginxModal(); }
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's' && currentEditFile) {
|
|
e.preventDefault();
|
|
saveFile();
|
|
}
|
|
});
|