UI fixes, code tightening, security fixes
This commit is contained in:
+3
-2
@@ -6,8 +6,9 @@
|
||||
|
||||
$config = [
|
||||
'authentication' => [
|
||||
// Add more users as needed - its 'username'=>'password' if you hadn't have guessed
|
||||
'admin' => 'password123',
|
||||
// Store password hashes generated with password_hash(...), not plaintext passwords.
|
||||
// Default password for this sample hash is: change-this-password
|
||||
'admin' => '$2y$10$.MiUOFPR.L9osXyKuUjqx.VeZeeD3bHrQrmPQAOzXSfWj.nene1ey',
|
||||
],
|
||||
// Enable the auditing system
|
||||
'audit' => true,
|
||||
|
||||
+115
-19
@@ -4,23 +4,44 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.pageContainer {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pageContainer .controls {
|
||||
flex: 0 1 auto;
|
||||
height: 60px;
|
||||
flex: 0 0 auto;
|
||||
min-height: 60px;
|
||||
position: relative;
|
||||
background-color: #1a1d20;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
-webkit-box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
|
||||
box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 10px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pageContainer .frame {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -29,26 +50,34 @@ body {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
flex: 0 0 60px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.controls .buttonContainer {
|
||||
width: 400px;
|
||||
margin: 12px 0;
|
||||
position: absolute;
|
||||
right: 110px;
|
||||
margin-left: auto;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
/* CSS */
|
||||
.headerButton {
|
||||
/*display: flex;*/
|
||||
flex-direction: column;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
padding: 8px 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
background: #6E6D70;
|
||||
box-shadow: 0px 0.5px 1px rgba(0, 0, 0, 0.1), inset 0px 0.5px 0.5px rgba(255, 255, 255, 0.5), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.12);
|
||||
@@ -67,35 +96,50 @@ body {
|
||||
background-color: #6E4D50;
|
||||
}
|
||||
|
||||
.headerButton:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.controls #urlbox {
|
||||
margin: 15px 70px;
|
||||
flex: 1 1 320px;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
position: absolute;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.3;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.controls #urlbox div {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.controls .loggedInDetails {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 10px;
|
||||
flex: 0 1 auto;
|
||||
color: #dee2e6;
|
||||
text-align: right;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.controls .buttonContainer ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.controls .buttonContainer li {
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,3 +211,55 @@ File List Modal
|
||||
.redText {
|
||||
color: #ff4d4d;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.pageContainer .controls {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.controls #urlbox {
|
||||
order: 3;
|
||||
flex-basis: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.controls .buttonContainer {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.pageContainer .controls {
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.controls .logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-basis: 48px;
|
||||
}
|
||||
|
||||
.controls .buttonContainer,
|
||||
.controls .loggedInDetails,
|
||||
.controls #urlbox {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.controls .buttonContainer ul {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.controls .buttonContainer li,
|
||||
.controls .buttonContainer .headerButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls #urlbox div {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-11
@@ -20,18 +20,32 @@ if (!$authentication->isLoggedIn()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
header('Allow: POST');
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Invalid request method']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$csrfToken = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
|
||||
if (!$authentication->isValidCsrfToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Invalid CSRF token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set appropriate headers
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Allowed MIME types and extensions
|
||||
// TinyMCE uses this endpoint for images only.
|
||||
$allowed_types = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'text/pdf' => 'pdf',
|
||||
'text/docx' => 'docx',
|
||||
'text/xlsx' => 'xlsx',
|
||||
'text/doc' => 'doc',
|
||||
'image/webp' => 'webp',
|
||||
];
|
||||
|
||||
// Maximum file size in bytes (50MB)
|
||||
@@ -68,19 +82,25 @@ if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
|
||||
|
||||
// Generate a unique name for the image
|
||||
$extension = $allowed_types[$mime_type];
|
||||
$filename = uniqid('', true) . '.' . $extension;
|
||||
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
|
||||
|
||||
// Set the upload directory
|
||||
$upload_dir = realpath(__DIR__ . '/../uploads') . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Ensure the upload directory exists
|
||||
if (!is_dir($upload_dir) && !mkdir($upload_dir, 0755, true)) {
|
||||
$uploadRoot = __DIR__ . '/../uploads';
|
||||
if (!is_dir($uploadRoot) && !mkdir($uploadRoot, 0755, true)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to create upload directory.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set the target file path
|
||||
$upload_dir = realpath($uploadRoot);
|
||||
if ($upload_dir === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Upload directory is invalid.']);
|
||||
exit;
|
||||
}
|
||||
$upload_dir .= DIRECTORY_SEPARATOR;
|
||||
|
||||
// Ensure the upload directory exists
|
||||
$target_file = $upload_dir . $filename;
|
||||
|
||||
// Move the uploaded file to the target directory
|
||||
|
||||
@@ -10,6 +10,7 @@ use NeoCMS\Authentication;
|
||||
|
||||
// Instantiate the Authentication class
|
||||
$authentication = new Authentication($config['authentication'] ?? []);
|
||||
$csrfToken = $authentication->getCsrfToken();
|
||||
|
||||
// Check if the user is logged in
|
||||
if (!$authentication->isLoggedIn()) {
|
||||
@@ -23,6 +24,7 @@ if (!$authentication->isLoggedIn()) {
|
||||
<html lang="en-gb">
|
||||
<head>
|
||||
<title>NeoCMS</title>
|
||||
<meta name="csrf-token" content="<?php echo htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.14.0/jquery-ui.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/cms/tinymce/tinymce.min.js"></script>
|
||||
|
||||
+57
-17
@@ -2,6 +2,7 @@ $(document).ready(function () {
|
||||
|
||||
let currentDiv;
|
||||
let iframeDoc;
|
||||
const csrfToken = $('meta[name="csrf-token"]').attr('content') || '';
|
||||
|
||||
$('#frameContainer').on('load', function () {
|
||||
|
||||
@@ -28,7 +29,8 @@ $(document).ready(function () {
|
||||
$('#editor').val(content);
|
||||
|
||||
// Open the modal and init TinyMCE
|
||||
$('#editModal').modal('show')
|
||||
$('#editModal')
|
||||
.off('shown.bs.modal')
|
||||
.on('shown.bs.modal', function () {
|
||||
tinymce.init({
|
||||
promotion: false,
|
||||
@@ -41,13 +43,50 @@ $(document).ready(function () {
|
||||
automatic_uploads: true,
|
||||
images_reuse_filename: false,
|
||||
images_upload_credentials: true,
|
||||
images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
$.ajax({
|
||||
url: '/cms/image_upload.php',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
xhr: function () {
|
||||
const xhr = $.ajaxSettings.xhr();
|
||||
if (xhr.upload) {
|
||||
xhr.upload.addEventListener('progress', function (event) {
|
||||
if (event.lengthComputable) {
|
||||
progress((event.loaded / event.total) * 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
return xhr;
|
||||
},
|
||||
success: function (response) {
|
||||
if (response && response.location) {
|
||||
resolve(response.location);
|
||||
return;
|
||||
}
|
||||
|
||||
reject((response && response.error) || 'Upload failed');
|
||||
},
|
||||
error: function (xhr) {
|
||||
const response = xhr.responseJSON || {};
|
||||
reject(response.error || 'Upload failed');
|
||||
}
|
||||
});
|
||||
}),
|
||||
force_br_newlines: false,
|
||||
force_p_newlines: false,
|
||||
license_key: 'gpl',
|
||||
forced_root_block: 'aaa',
|
||||
newline_behavior: 'linebreak'
|
||||
});
|
||||
});
|
||||
})
|
||||
.modal('show');
|
||||
}
|
||||
|
||||
regenerateButtons();
|
||||
@@ -129,20 +168,18 @@ $(document).ready(function () {
|
||||
|
||||
// Handle Saving changes to the page
|
||||
$('#savePage').click(() => {
|
||||
// Clone the document so CMS-only controls and styles are not written back into the page.
|
||||
const frameContainerElement = $('#frameContainer').contents().get(0);
|
||||
const clonedDocument = frameContainerElement.cloneNode(true);
|
||||
|
||||
// Remove the buttons container as not to make things awks
|
||||
$(iframeDoc).find('.button-container').remove();
|
||||
|
||||
// Remove pointer cursor, but need to find a proper way of removing the cursor inline style, so we can get rid of any unneeded inline styles
|
||||
$(iframeDoc).find('.editable, .neo-dupe').css('cursor', 'default');
|
||||
|
||||
// Get page content and save it
|
||||
let frameContainerElement = $('#frameContainer').contents().get(0);
|
||||
$(clonedDocument).find('.button-container').remove();
|
||||
$(clonedDocument).find('.editable, .neo-dupe').css('cursor', '');
|
||||
|
||||
$.post("/cms/controller/", {
|
||||
action: "save",
|
||||
uri: frameContainerElement.location.pathname,
|
||||
content: new XMLSerializer().serializeToString(frameContainerElement)
|
||||
content: new XMLSerializer().serializeToString(clonedDocument),
|
||||
csrf_token: csrfToken
|
||||
}, function (data) {
|
||||
if (typeof data.error == "undefined") {
|
||||
showMessage(data.message, "success");
|
||||
@@ -179,7 +216,8 @@ $(document).ready(function () {
|
||||
method: "POST",
|
||||
data: {
|
||||
template: selectedItemId,
|
||||
filename: filename
|
||||
filename: filename,
|
||||
csrf_token: csrfToken
|
||||
},
|
||||
success: function (response) {
|
||||
if (typeof response.error != "undefined") {
|
||||
@@ -249,12 +287,12 @@ $(document).ready(function () {
|
||||
// Handle 'Select Page' button
|
||||
$("#selectPage").click(function () {
|
||||
loadListOfPages(); // Call function to load the file list
|
||||
$("#fileList").dialog({
|
||||
$("#fileListDialog").dialog({
|
||||
modal: true,
|
||||
width: 500,
|
||||
buttons: {
|
||||
Close: function () {
|
||||
$('.ui-dialog-content').dialog('close');
|
||||
$(this).dialog('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -263,10 +301,13 @@ $(document).ready(function () {
|
||||
// Add buttons to all existing neo-dupe elements inside the iframe
|
||||
function regenerateButtons() {
|
||||
$(iframeDoc).find('.neo-dupe').each(function () {
|
||||
$(this).find('.buttonContainer').remove();
|
||||
$(this).find('.button-container').remove();
|
||||
addButtonsToNeoDupe(this);
|
||||
});
|
||||
$('#urlbox').html("<strong>Editing:</strong> <div>" + $(iframeDoc)[0].URL + "</div>");
|
||||
$('#urlbox').empty().append(
|
||||
$('<strong>').text('Editing:'),
|
||||
$('<div>').text($(iframeDoc)[0].URL)
|
||||
);
|
||||
}
|
||||
|
||||
// Function to add buttons to a neo-dupe element inside the iframe
|
||||
@@ -341,4 +382,3 @@ function showMessage(message, type) {
|
||||
messageBar.slideUp();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
+29
-18
@@ -20,27 +20,37 @@ $authentication = new Authentication($config['authentication'] ?? []);
|
||||
$logger = new Logger($config['audit'] ?? true);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($authentication->login($username, $password)) {
|
||||
// Log successful login
|
||||
$csrfToken = $_POST['csrf_token'] ?? null;
|
||||
if (!$authentication->isValidCsrfToken($csrfToken)) {
|
||||
http_response_code(400);
|
||||
$logger->write(
|
||||
"User login for $username was successful from {$_SERVER['REMOTE_ADDR']}",
|
||||
$username
|
||||
"User login attempt rejected due to invalid CSRF token from {$_SERVER['REMOTE_ADDR']}",
|
||||
'anonymous'
|
||||
);
|
||||
|
||||
// Redirect to the CMS dashboard or desired page
|
||||
header("Location: /cms/");
|
||||
exit("Well done, champ! You got this!! <3");
|
||||
$error = "Invalid request token";
|
||||
} else {
|
||||
// Log failed login attempt
|
||||
$logger->write(
|
||||
"User login for $username was denied due to incorrect credentials from {$_SERVER['REMOTE_ADDR']}",
|
||||
$username
|
||||
);
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
$error = "Invalid username or password";
|
||||
if ($authentication->login($username, $password)) {
|
||||
// Log successful login
|
||||
$logger->write(
|
||||
"User login for $username was successful from {$_SERVER['REMOTE_ADDR']}",
|
||||
$username
|
||||
);
|
||||
|
||||
// Redirect to the CMS dashboard or desired page
|
||||
header("Location: /cms/");
|
||||
exit;
|
||||
} else {
|
||||
// Log failed login attempt
|
||||
$logger->write(
|
||||
"User login for $username was denied due to incorrect credentials from {$_SERVER['REMOTE_ADDR']}",
|
||||
$username
|
||||
);
|
||||
|
||||
$error = "Invalid username or password";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -56,9 +66,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
<img class="logo" src="/cms/img/loginlogo.png" alt="NeoCMS logo"/>
|
||||
<h2>NeoCMS Login</h2>
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="error-message"><?php echo htmlspecialchars($error); ?></div>
|
||||
<div class="error-message"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($authentication->getCsrfToken(), ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
|
||||
|
||||
@@ -4,14 +4,38 @@ namespace NeoCMS;
|
||||
|
||||
class Authentication
|
||||
{
|
||||
private $credentials;
|
||||
private array $credentials;
|
||||
|
||||
public function __construct(array $credentials)
|
||||
{
|
||||
session_start();
|
||||
$this->startSession();
|
||||
$this->credentials = $credentials;
|
||||
}
|
||||
|
||||
private function startSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443);
|
||||
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
ini_set('session.cookie_samesite', 'Lax');
|
||||
|
||||
session_set_cookie_params([
|
||||
'httponly' => true,
|
||||
'path' => '/',
|
||||
'samesite' => 'Lax',
|
||||
'secure' => $isHttps,
|
||||
]);
|
||||
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a user into the CMS
|
||||
* @param $username
|
||||
@@ -20,9 +44,11 @@ class Authentication
|
||||
*/
|
||||
public function login($username, $password)
|
||||
{
|
||||
if (!empty($this->credentials[$username]) && $this->credentials[$username] === $password) {
|
||||
if ($this->credentialsAreValid((string) $username, (string) $password)) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['loggedIn'] = true;
|
||||
$_SESSION['loggedInUser'] = $username;
|
||||
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,12 +74,52 @@ class Authentication
|
||||
return $_SESSION['loggedInUser'] ?? 'Not logged in';
|
||||
}
|
||||
|
||||
public function getCsrfToken(): string
|
||||
{
|
||||
if (empty($_SESSION['csrfToken'])) {
|
||||
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return $_SESSION['csrfToken'];
|
||||
}
|
||||
|
||||
public function isValidCsrfToken(?string $token): bool
|
||||
{
|
||||
if (!is_string($token) || $token === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($this->getCsrfToken(), $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$_SESSION = [];
|
||||
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
private function credentialsAreValid(string $username, string $password): bool
|
||||
{
|
||||
$storedCredential = $this->credentials[$username] ?? null;
|
||||
if (!is_string($storedCredential) || $storedCredential === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$passwordInfo = password_get_info($storedCredential);
|
||||
if (($passwordInfo['algo'] ?? null) !== null) {
|
||||
return password_verify($password, $storedCredential);
|
||||
}
|
||||
|
||||
return hash_equals($storedCredential, $password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class CMSController
|
||||
}
|
||||
|
||||
// Check if the user is authenticated for actions that require it
|
||||
if (in_array($action, ['save', 'newPage']) && !$this->authentication->isLoggedIn()) {
|
||||
if (!$this->authentication->isLoggedIn()) {
|
||||
$this->sendJsonResponse(['error' => 'Not authenticated']);
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +44,11 @@ class CMSController
|
||||
try {
|
||||
$this->$methodName();
|
||||
} catch (\Exception $e) {
|
||||
$this->sendJsonResponse(['error' => $e->getMessage()]);
|
||||
$this->logger->write(
|
||||
"Controller action {$action} failed: {$e->getMessage()}",
|
||||
$this->authentication->getLoggedInUser()
|
||||
);
|
||||
$this->sendJsonResponse(['error' => 'Request failed']);
|
||||
}
|
||||
} else {
|
||||
$this->sendJsonResponse(['error' => 'Unknown action']);
|
||||
@@ -59,10 +63,11 @@ class CMSController
|
||||
private function saveAction(): void
|
||||
{
|
||||
$this->ensurePostRequest();
|
||||
$this->ensureValidCsrfToken();
|
||||
$uri = $_POST['uri'] ?? '';
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
if (empty($uri) || empty($content)) {
|
||||
if ($uri === '' || !is_string($content)) {
|
||||
$this->sendJsonResponse(['error' => 'URI and content are required']);
|
||||
return;
|
||||
}
|
||||
@@ -101,6 +106,7 @@ class CMSController
|
||||
private function newPageAction(): void
|
||||
{
|
||||
$this->ensurePostRequest();
|
||||
$this->ensureValidCsrfToken();
|
||||
$filename = $_POST['filename'] ?? '';
|
||||
$template = $_POST['template'] ?? '';
|
||||
|
||||
@@ -275,6 +281,14 @@ class CMSController
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureValidCsrfToken(): void
|
||||
{
|
||||
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
|
||||
if (!$this->authentication->isValidCsrfToken($token)) {
|
||||
throw new \Exception('Invalid CSRF token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the JSON response
|
||||
* @param $data
|
||||
@@ -283,6 +297,7 @@ class CMSController
|
||||
private function sendJsonResponse($data): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
@@ -307,7 +322,7 @@ class CMSController
|
||||
|
||||
// Find all elements with the class "editable"
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$editableElements = $xpath->query('//*[contains(@class, "editable")]');
|
||||
$editableElements = $xpath->query('//*[contains(concat(" ", normalize-space(@class), " "), " editable ")]');
|
||||
|
||||
// Return true if there are any elements with the "editable" class
|
||||
return $editableElements->length > 0;
|
||||
|
||||
@@ -37,10 +37,16 @@ class Logger
|
||||
$entry = sprintf(
|
||||
"[%s]\tUser: %s\t%s\n",
|
||||
date('Y-m-d H:i:s'),
|
||||
$user,
|
||||
trim($message)
|
||||
$this->normaliseLogField($user),
|
||||
$this->normaliseLogField($message)
|
||||
);
|
||||
|
||||
file_put_contents($logFile, $entry, FILE_APPEND);
|
||||
}
|
||||
|
||||
private function normaliseLogField($value): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
return str_replace(["\r", "\n", "\t"], [' ', ' ', ' '], $value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user