diff --git a/README.md b/README.md
index 3dc9cf7..cd6d467 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Ready to get started? Just follow these simple steps:
* Drop the CMS folder into your static website.
* Add the uploads folder to your site as well.
-* Open /cms/init.php and edit the credentials for any users you want to set up.
+* Open /cms/config.php and edit the credentials for any users you want to set up.
* Update the div elements you want to make editable by adding the class editable.
* Visit /cms/, log in, and start editing!
@@ -33,7 +33,7 @@ That’s it! You’re up and running.
## Requirements
-* PHP 8 running on a Linux server. (May work on Windows servers, but don't want to stoop to that level)
+* PHP 8 running on Linux
## Known Bugs & Issues
diff --git a/cms/config.php b/cms/config.php
new file mode 100644
index 0000000..3927482
--- /dev/null
+++ b/cms/config.php
@@ -0,0 +1,18 @@
+ [
+ // Add more users as needed - its 'username'=>'password' if you hadn't have guessed
+ 'admin' => 'password123',
+ ],
+ // Enable the auditing system
+ 'audit' => true,
+ // Set to true if you do not want to see the welcome page and want to go straight to your site
+ 'skipWelcomePage' => false,
+ // Show full URL of the page being edited on the control bar
+ 'showFullUrl' => true
+];
diff --git a/cms/controller/index.php b/cms/controller/index.php
index 7bb07b5..fa69158 100644
--- a/cms/controller/index.php
+++ b/cms/controller/index.php
@@ -1,67 +1,13 @@
saveContent($_POST['uri'], $_POST['content']);
- echo json_encode(array('message' => 'Page has been saved','destination'=>$filename));
- } else {
- echo json_encode(array('error' => 'Unable to save page'));
- }
- break;
+spl_autoload_register(function ($class) {
+ $classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
+ require_once "../src/{$classPath}.php";
+});
- /**
- * getTemplates AJAX call to get list of files
- */
- case "getTemplates":
- header('Content-type: application/json');
- $files = array_diff(scandir("../templates/"), array('..', '.'));
- $returnArray = [];
- foreach ($files as $file) {
- $returnArray[] = array("id" => $file, "name" => str_replace(array(".html", ".htm"), "", $file));
- }
- echo json_encode($returnArray);
- break;
+use NeoCMS\CMSController;
- /**
- * newPage AJAX call to take a template file and create a new page
- */
- case "newPage":
- header('Content-type: application/json');
-
- $filename = str_replace("..", "", $_POST['filename']);
- $template = str_replace("..", "", $_POST['template']);
- if (!str_ends_with($filename, ".htm") && !str_ends_with($filename, ".html")) {
- $filename .= ".html";
- }
-
- // Some vhosts can supply a trailing slash on DOCUMENT_ROOT so tidy it up
- $destination = str_replace("//", "/", $_SERVER['DOCUMENT_ROOT'] . "/" . $filename);
-
-
- if (!file_exists("../templates/" . $template)) die(json_encode(array("error" => "Template not found")));
- if (file_exists($destination)) die(json_encode(array("error" => "Page or file already exists")));
-
- file_put_contents($destination, file_get_contents("../templates/" . $template));
- if (!file_exists($destination)) die(json_encode(array("error" => "Template copy failed")));
- echo json_encode(array("ok" => "DONE_THE_NEEDFUL", "url" => str_replace("//", "/", "/" . $filename)));
- break;
-
- case "getPages":
- header('Content-type: application/json');
- $it = new RecursiveDirectoryIterator("../../");
- $returnArray = [];
- foreach (new RecursiveIteratorIterator($it) as $file) {
- // Strip document root from URI, so relative
- $file = str_replace($_SERVER["DOCUMENT_ROOT"], "", $file->getRealPath());
-
- if (!str_starts_with($file, "/cms/") && (str_ends_with($file, ".html") || str_ends_with($file, ".htm")))
- $returnArray[] = array("name" => $file, "url" => $file);
- }
- echo json_encode($returnArray);
- break;
-
-}
\ No newline at end of file
+// Instantiate the controller with necessary dependencies
+$controller = new CMSController($config ?? []);
+$controller->handleRequest();
\ No newline at end of file
diff --git a/cms/css/editor.css b/cms/css/editor.css
index 0bbafac..6404d65 100644
--- a/cms/css/editor.css
+++ b/cms/css/editor.css
@@ -49,12 +49,12 @@ body {
.controls #urlbox div {
float: right;
- margin-left:10px;
+ margin-left: 10px;
}
.controls .loggedInDetails {
position: absolute;
- top: 12px;
+ top: 20px;
right: 10px;
}
diff --git a/cms/image_upload.php b/cms/image_upload.php
index c96358f..e64ed86 100644
--- a/cms/image_upload.php
+++ b/cms/image_upload.php
@@ -1,41 +1,95 @@
isLoggedIn()) {
+ http_response_code(401);
+ header('Content-Type: application/json');
+ echo json_encode(['error' => 'Unauthorised']);
+ exit;
+}
+
+// Set appropriate headers
+header('Content-Type: application/json');
+
+// Allowed MIME types and extensions
+$allowed_types = [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/gif' => 'gif',
+ 'text/pdf' => 'pdf',
+ 'text/docx' => 'docx',
+ 'text/xlsx' => 'xlsx',
+ 'text/doc' => 'doc',
+];
+
+// Maximum file size in bytes (50MB)
+$max_file_size = 50 * 1024 * 1024;
// Check if a file has been uploaded
-if (isset($_FILES['file'])) {
+if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['file'];
+ // Check file size
+ if ($file['size'] > $max_file_size) {
+ http_response_code(400);
+ echo json_encode(['error' => 'File size exceeds the maximum limit.']);
+ exit;
+ }
+
// Validate the file type
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
- if (!in_array($mime_type, $allowed_types)) {
+ if (!array_key_exists($mime_type, $allowed_types)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid file type.']);
exit;
}
- // Set the upload directory
- $upload_dir = '../uploads/';
-
- // Ensure the upload directory exists
- if (!is_dir($upload_dir)) {
- mkdir($upload_dir, 0777, true);
+ // Validate that the file is a real image
+ if (getimagesize($file['tmp_name']) === false) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Uploaded file is not a valid image.']);
+ exit;
}
// Generate a unique name for the image
- $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
- $filename = uniqid() . '.' . $extension;
+ $extension = $allowed_types[$mime_type];
+ $filename = uniqid('', true) . '.' . $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)) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to create upload directory.']);
+ exit;
+ }
// Set the target file path
$target_file = $upload_dir . $filename;
// Move the uploaded file to the target directory
if (move_uploaded_file($file['tmp_name'], $target_file)) {
+ // Set file permissions
+ chmod($target_file, 0644);
+
// Return a JSON response with the image location
- $location = $target_file;
+ $location = '/uploads/' . $filename;
echo json_encode(['location' => $location]);
} else {
// Return an error response
@@ -43,7 +97,34 @@ if (isset($_FILES['file'])) {
echo json_encode(['error' => 'Failed to move uploaded file.']);
}
} else {
- // No file was uploaded
+ // Handle upload errors
+ $error_message = 'No file uploaded.';
+ if (isset($_FILES['file']['error'])) {
+ switch ($_FILES['file']['error']) {
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ $error_message = 'File size exceeds the maximum limit.';
+ break;
+ case UPLOAD_ERR_PARTIAL:
+ $error_message = 'The uploaded file was only partially uploaded.';
+ break;
+ case UPLOAD_ERR_NO_FILE:
+ $error_message = 'No file was uploaded.';
+ break;
+ case UPLOAD_ERR_NO_TMP_DIR:
+ $error_message = 'Missing a temporary folder.';
+ break;
+ case UPLOAD_ERR_CANT_WRITE:
+ $error_message = 'Failed to write file to disk.';
+ break;
+ case UPLOAD_ERR_EXTENSION:
+ $error_message = 'A PHP extension stopped the file upload.';
+ break;
+ default:
+ $error_message = 'Unknown upload error.';
+ break;
+ }
+ }
http_response_code(400);
- echo json_encode(['error' => 'No file uploaded.']);
-}
\ No newline at end of file
+ echo json_encode(['error' => $error_message]);
+}
diff --git a/cms/index.php b/cms/index.php
index b9da0c9..0ecb562 100644
--- a/cms/index.php
+++ b/cms/index.php
@@ -1,7 +1,24 @@
isLoggedIn()) {
+ // Redirect to the login page
+ header("Location: /cms/login/");
+ exit;
+}
?>
+
@@ -28,7 +45,7 @@ require_once "init.php";
';?>
- Logged in as: getLoggedinUser(); ?>
+ Logged in as: getLoggedinUser(); ?>
Make /cms/logs/ writeable!";
@@ -45,7 +62,7 @@ require_once "init.php";
?>
-
+
diff --git a/cms/init.php b/cms/init.php
deleted file mode 100644
index d5c6f8a..0000000
--- a/cms/init.php
+++ /dev/null
@@ -1,63 +0,0 @@
- "password",
- "editor" => "password",
-];
-
-// Set to true if you do not want to see the welcome page and want to go straight to your site
-$skipWelcomePage = false;
-
-// Show full URL of the page being edited on the control bar
-$showFullUrl = true;
-
-
-/**
- * DO NOT CHANGE ANYTHING BENEATH THIS LINE - IT WILL BUGGER STUFF UP
- */
-session_start();
-require_once "neoCMSCore.php";
-
-if (empty($_SESSION["core"])) {
- $_SESSION["core"] = new neoCMSCore();
- $_SESSION["core"]->users = $authentication;
-}
-if (!$_SESSION["core"]->isLoggedIn() && stripos($_SERVER['REQUEST_URI'], "/login") === FALSE) {
- header('Location: /cms/login');
- die("Redirecting to login..");
-}
-
-
-// str_starts_with compatibility for php 7
-if (!function_exists('str_starts_with')) {
- function str_starts_with($haystack, $needle): bool
- {
- //str_starts_with(string $haystack, string $needle): bool
-
- $strlen_needle = mb_strlen($needle);
- if (mb_substr($haystack, 0, $strlen_needle) == $needle) {
- return true;
- }
- return false;
- }
-}
-
-//str_ends_with compatibility for php 7
-if (!function_exists('str_ends_with')) {
- function str_ends_with($haystack, $needle): bool
- {
- //str_starts_with(string $haystack, string $needle): bool
-
- $strlen_needle = mb_strlen($needle);
- if (mb_substr($haystack, -$strlen_needle, $strlen_needle) == $needle) {
- return true;
- }
- return false;
- }
-}
-
-$_SESSION["core"]->version = '2.00';
-
diff --git a/cms/login/index.php b/cms/login/index.php
index d6d02a0..1282ca3 100644
--- a/cms/login/index.php
+++ b/cms/login/index.php
@@ -1,19 +1,49 @@
login($_POST['username'], $_POST['password'])) {
- $error = "Invalid username or password";
+// Instantiate the Authentication and Logger classes
+$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
+ $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("Well done, champ! You got this!! <3");
} else {
- header("Location:/cms/");
- die("Well done, champ! You got this!! <3");
+ // 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";
}
}
?>
-
diff --git a/cms/neoCMSCore.php b/cms/neoCMSCore.php
deleted file mode 100644
index 51e3b8f..0000000
--- a/cms/neoCMSCore.php
+++ /dev/null
@@ -1,63 +0,0 @@
-loggedIn = (!empty($this->users[$username]) && $this->users[$username] == $password);
- if ($this->loggedIn) $this->loggedInUser = $username;
- $this->writeAudit("User login for " . $username . " was " . ($this->loggedIn ? "successful" : "denied due to incorrect credentials") . " from " . $_SERVER['REMOTE_ADDR']);
- return $this->loggedIn;
- }
-
- /**
- * Write an entry into the audit log
- * @param $value
- * @return void
- */
- public function writeAudit($value): void
- {
- if ($this->auditEnabled)
- file_put_contents(__DIR__ . "/logs/" . date('y-m-d-') . "audit.txt", date('Y-m-d H:i:s') . "\t" . $this->loggedInUser . "\t" . trim($value) . "\n", FILE_APPEND);
- }
-
- public function isLoggedIn()
- {
- return $this->loggedIn;
- }
-
- public function getLoggedinUser(): string
- {
- return $this->loggedInUser;
- }
-
- public function saveContent($uri, $content): void
- {
- $this->writeAudit("Content for " . $uri . " was saved.");
- $uri = str_replace("../", "./", $uri);
-
- //check to see if we're dealing with a default (index.htm{l}) page
- if (str_ends_with($uri, "/") || $uri == '/') {
- if (file_exists($_SERVER['DOCUMENT_ROOT'] . $uri . 'index.htm')) {
- $uri = $uri . '/index.htm';
- } elseif (file_exists($_SERVER['DOCUMENT_ROOT'] . $uri . 'index.html')) {
- $uri = $uri . '/index.html';
- } else {
- die('Unknown default file:' . $uri);
- }
- }
- file_put_contents($_SERVER['DOCUMENT_ROOT'] . $uri, $content);
- }
-}
\ No newline at end of file
diff --git a/cms/src/NeoCMS/Authentication.php b/cms/src/NeoCMS/Authentication.php
new file mode 100644
index 0000000..52fd660
--- /dev/null
+++ b/cms/src/NeoCMS/Authentication.php
@@ -0,0 +1,59 @@
+credentials = $credentials;
+ }
+
+ /**
+ * Logs a user into the CMS
+ * @param $username
+ * @param $password
+ * @return bool
+ */
+ public function login($username, $password)
+ {
+ if (!empty($this->credentials[$username]) && $this->credentials[$username] === $password) {
+ $_SESSION['loggedIn'] = true;
+ $_SESSION['loggedInUser'] = $username;
+ return true;
+ }
+
+ $_SESSION['loggedIn'] = false;
+ return false;
+ }
+
+ /**
+ * Checks if a user has logged in
+ * @return bool
+ */
+ public function isLoggedIn(): bool
+ {
+ return $_SESSION['loggedIn'] ?? false;
+ }
+
+ /**
+ * Returns the user logged in
+ * @return string
+ */
+ public function getLoggedInUser(): string
+ {
+ return $_SESSION['loggedInUser'] ?? 'Not logged in';
+ }
+
+ /**
+ * Destroy the session
+ * @return void
+ */
+ public function logout()
+ {
+ session_destroy();
+ }
+}
diff --git a/cms/src/NeoCMS/CMSController.php b/cms/src/NeoCMS/CMSController.php
new file mode 100644
index 0000000..83e270a
--- /dev/null
+++ b/cms/src/NeoCMS/CMSController.php
@@ -0,0 +1,286 @@
+config = $config;
+ $this->authentication = new Authentication($config['authentication']);
+ $this->logger = new Logger($config['audit']);
+ $this->request = $_REQUEST;
+ $this->templatesDir = realpath(__DIR__ . '/../../templates/') . DIRECTORY_SEPARATOR;
+ $this->documentRoot = rtrim(realpath($_SERVER['DOCUMENT_ROOT']), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * Handle an AJAX call
+ * @return void
+ */
+ public function handleRequest(): void
+ {
+ $action = $this->request['action'] ?? null;
+
+ if (!$action) {
+ $this->sendJsonResponse(['error' => 'No action specified']);
+ return;
+ }
+
+ // Check if the user is authenticated for actions that require it
+ if (in_array($action, ['save', 'newPage']) && !$this->authentication->isLoggedIn()) {
+ $this->sendJsonResponse(['error' => 'Not authenticated']);
+ return;
+ }
+
+ $methodName = $action . 'Action';
+ if (method_exists($this, $methodName)) {
+ try {
+ $this->$methodName();
+ } catch (\Exception $e) {
+ $this->sendJsonResponse(['error' => $e->getMessage()]);
+ }
+ } else {
+ $this->sendJsonResponse(['error' => 'Unknown action']);
+ }
+ }
+
+ /**
+ * Saves a page to the filesystem
+ * @return void
+ * @throws \Exception
+ */
+ private function saveAction(): void
+ {
+ $this->ensurePostRequest();
+ $uri = $_POST['uri'] ?? '';
+ $content = $_POST['content'] ?? '';
+
+ if (empty($uri) || empty($content)) {
+ $this->sendJsonResponse(['error' => 'URI and content are required']);
+ return;
+ }
+
+ $filename = $this->saveContent($uri, $content);
+ $this->sendJsonResponse([
+ 'message' => 'Page has been saved',
+ 'destination' => $filename,
+ ]);
+ }
+
+ /**
+ * Returns back a JSON object containing templates
+ * @return void
+ */
+ private function getTemplatesAction(): void
+ {
+ $files = array_diff(scandir($this->templatesDir), ['..', '.']);
+ $templates = [];
+
+ foreach ($files as $file) {
+ $templates[] = [
+ 'id' => $file,
+ 'name' => pathinfo($file, PATHINFO_FILENAME),
+ ];
+ }
+
+ $this->sendJsonResponse($templates);
+ }
+
+ /**
+ * Create a new page based on a template file
+ * @return void
+ * @throws \Exception
+ */
+ private function newPageAction(): void
+ {
+ $this->ensurePostRequest();
+ $filename = $_POST['filename'] ?? '';
+ $template = $_POST['template'] ?? '';
+
+ $filename = $this->sanitiseFileName($filename);
+ $template = $this->sanitiseFileName($template);
+
+ if (!$filename || !$template) {
+ $this->sendJsonResponse(['error' => 'Filename and template are required']);
+ return;
+ }
+
+ if (!preg_match('/\.(html|htm)$/', $filename)) {
+ $filename .= '.html';
+ }
+
+ $destination = $this->documentRoot . ltrim($filename, DIRECTORY_SEPARATOR);
+ $templatePath = realpath($this->templatesDir . $template);
+
+ if (!$templatePath || strpos($templatePath, $this->templatesDir) !== 0) {
+ $this->sendJsonResponse(['error' => 'Template not found']);
+ return;
+ }
+
+ if (file_exists($destination)) {
+ $this->sendJsonResponse(['error' => 'Page or file already exists']);
+ return;
+ }
+
+ if (!copy($templatePath, $destination)) {
+ $this->sendJsonResponse(['error' => 'Template copy failed']);
+ return;
+ }
+
+ $this->logger->write(
+ "Created new page: $filename from template $template",
+ $this->authentication->getLoggedInUser()
+ );
+
+ $this->sendJsonResponse([
+ 'ok' => 'DONE_THE_NEEDFUL',
+ 'url' => '/' . ltrim($filename, '/'),
+ ]);
+ }
+
+ /**
+ * Get list of pages and return as a JSON response
+ * @return void
+ */
+ private function getPagesAction(): void
+ {
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($this->documentRoot)
+ );
+ $pages = [];
+
+ foreach ($iterator as $file) {
+ if (!$file->isFile()) {
+ continue;
+ }
+
+ $filePath = $file->getRealPath();
+
+ // Exclude CMS directory
+ if (strpos($filePath, $this->documentRoot . 'cms' . DIRECTORY_SEPARATOR) === 0) {
+ continue;
+ }
+
+ // Include only .html and .htm files
+ if (preg_match('/\.(html|htm)$/i', $filePath)) {
+ $relativePath = str_replace($this->documentRoot, '/', $filePath);
+ $pages[] = [
+ 'name' => $relativePath,
+ 'url' => $relativePath,
+ ];
+ }
+ }
+
+ $this->sendJsonResponse($pages);
+ }
+
+ /**
+ * Do the actual save to the filesystem and return back the path
+ * @param $uri
+ * @param $content
+ * @return string
+ * @throws \Exception
+ */
+ private function saveContent($uri, $content): string
+ {
+ $uri = $this->sanitiseUri($uri);
+ $filePath = $this->resolveUriToFilePath($uri);
+
+ if (file_put_contents($filePath, $content) === false) {
+ throw new \Exception('Failed to save content');
+ }
+
+ $this->logger->write(
+ "Content for $uri was saved.",
+ $this->authentication->getLoggedInUser()
+ );
+
+ return $uri;
+ }
+
+ /**
+ * Identify the file to write to
+ * @param $uri
+ * @return string
+ * @throws \Exception
+ */
+ private function resolveUriToFilePath($uri): string
+ {
+ $path = $this->documentRoot . ltrim($uri, '/');
+
+ if (is_dir($path)) {
+ $indexFiles = ['index.html', 'index.htm'];
+ foreach ($indexFiles as $indexFile) {
+ if (file_exists($path . $indexFile)) {
+ $path .= $indexFile;
+ break;
+ }
+ }
+ if (is_dir($path)) {
+ throw new \Exception('No default index file found in directory: ' . $uri);
+ }
+ } else {
+ if (!preg_match('/\.(html|htm)$/i', $path)) {
+ $path .= '.html';
+ }
+ }
+
+ $realPath = realpath($path);
+ if (!$realPath || strpos($realPath, $this->documentRoot) !== 0) {
+ throw new \Exception('Invalid file path');
+ }
+
+ return $realPath;
+ }
+
+ /**
+ * Sanitise the filename
+ * @param $filename
+ * @return string
+ */
+ private function sanitiseFileName($filename): string
+ {
+ return preg_replace('/[^a-zA-Z0-9_\-\.]/', '', basename($filename));
+ }
+
+ /**
+ * Sanitise a URI
+ * @param $uri
+ * @return string
+ */
+ private function sanitiseUri($uri): string
+ {
+ $uri = filter_var($uri, FILTER_SANITIZE_URL);
+ return preg_replace('#(\.\./|/\.{2})#', '', $uri);
+ }
+
+ /**
+ * Ensure that the controller is not being spammed by GET or other rubbish
+ * @return void
+ * @throws \Exception
+ */
+ private function ensurePostRequest(): void
+ {
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ throw new \Exception('Invalid request method');
+ }
+ }
+
+ /**
+ * Send the JSON response
+ * @param $data
+ * @return void
+ */
+ private function sendJsonResponse($data): void
+ {
+ header('Content-Type: application/json');
+ echo json_encode($data);
+ }
+}
diff --git a/cms/src/NeoCMS/Logger.php b/cms/src/NeoCMS/Logger.php
new file mode 100644
index 0000000..1d8fbe8
--- /dev/null
+++ b/cms/src/NeoCMS/Logger.php
@@ -0,0 +1,46 @@
+auditEnabled = $auditEnabled;
+
+ $this->logsDir = __DIR__ . '/../../logs';
+ $this->logsDir = rtrim($this->logsDir, '/\\') . DIRECTORY_SEPARATOR;
+
+ // Ensure the logs directory exists
+ if (!is_dir($this->logsDir)) {
+ if (!mkdir($this->logsDir, 0755, true)) {
+ throw new \Exception('Failed to create logs directory: ' . $this->logsDir);
+ }
+ }
+ }
+
+ /**
+ * Write an entry to the log
+ * @param $message
+ * @param $user
+ * @return void
+ */
+ public function write($message, $user): void
+ {
+ if (!$this->auditEnabled) {
+ return;
+ }
+
+ $logFile = $this->logsDir . date('Y-m-d') . '-audit.txt';
+ $entry = sprintf(
+ "[%s]\tUser: %s\t%s\n",
+ date('Y-m-d H:i:s'),
+ $user,
+ trim($message)
+ );
+
+ file_put_contents($logFile, $entry, FILE_APPEND);
+ }
+}
diff --git a/cms/welcome.html b/cms/welcome.html
index a4b70f8..16cfaf7 100644
--- a/cms/welcome.html
+++ b/cms/welcome.html
@@ -94,24 +94,6 @@
-
-
- Requirements 📝
- NeoCMS requires the following:
-
- - PHP 8 running on a Linux server. (May work on Windows servers, but we don't want to stoop to that level 😉)
-
-
-
-
-
- Known Bugs & Issues 🐞
-
- - File Uploads (non-image): Currently, file uploads are not fully implemented. We're working on it!
- - Testing: The system is functional, but it hasn’t been fully tested in all scenarios.
-
-
-