3 Commits
18 changed files with 711 additions and 379 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ Ready to get started? Just follow these simple steps:
* Drop the CMS folder into your static website. * Drop the CMS folder into your static website.
* Add the uploads folder to your site as well. * 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. * Update the div elements you want to make editable by adding the class editable.
* Visit /cms/, log in, and start editing! * Visit /cms/, log in, and start editing!
@@ -33,7 +33,7 @@ That’s it! You’re up and running.
## Requirements ## 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 ## Known Bugs & Issues
+18
View File
@@ -0,0 +1,18 @@
<?php
/*
* Authentication stores the usernames and passwords for people to be able to use the CMS.
* This will be used as well to write to the audit log once it has been implemented in code.
*/
$config = [
'authentication' => [
// 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
];
+9 -63
View File
@@ -1,67 +1,13 @@
<?php <?php
require_once "../neoCMSCore.php"; require_once "../config.php";
require_once "../init.php";
switch ($_REQUEST["action"]) { spl_autoload_register(function ($class) {
case "save": $classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
header('Content-type: application/json'); require_once "../src/{$classPath}.php";
if (!empty($_POST['uri']) && !empty($_POST['content'])) { });
$filename=$_SESSION['core']->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;
/** use NeoCMS\CMSController;
* 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;
/** // Instantiate the controller with necessary dependencies
* newPage AJAX call to take a template file and create a new page $controller = new CMSController($config ?? []);
*/ $controller->handleRequest();
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;
}
+34 -60
View File
@@ -13,7 +13,7 @@ body {
.pageContainer .controls { .pageContainer .controls {
flex: 0 1 auto; flex: 0 1 auto;
height: 60px; height: 60px;
background-color: #666666; background-color: #1a1d20;
border-bottom: 1px solid #1a1a1a; border-bottom: 1px solid #1a1a1a;
-webkit-box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0); -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); box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
@@ -26,7 +26,7 @@ body {
.controls .logo { .controls .logo {
background-image: url("/cms/img/logo.png"); background-image: url("/cms/img/logo.png");
width: 128px; width: 60px;
height: 60px; height: 60px;
display: block; display: block;
position: absolute; position: absolute;
@@ -39,12 +39,40 @@ body {
right: 110px; right: 110px;
} }
/* CSS */
.headerButton {
/*display: flex;*/
flex-direction: column;
align-items: center;
padding: 6px 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
border-radius: 6px;
border: none;
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);
color: #DFDEDF;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.headerButton:focus {
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2), 0px 0.5px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px 3.5px rgba(58, 108, 217, 0.5);
outline: 0;
}
.headerButton.dangerButton {
background-color: #6E4D50;
}
.controls #urlbox { .controls #urlbox {
margin: 15px 150px; margin: 15px 70px;
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.5em;
color: black;
position: absolute; position: absolute;
color: #dee2e6;
} }
.controls #urlbox div { .controls #urlbox div {
@@ -54,8 +82,9 @@ body {
.controls .loggedInDetails { .controls .loggedInDetails {
position: absolute; position: absolute;
top: 12px; top: 20px;
right: 10px; right: 10px;
color: #dee2e6;
} }
.controls .buttonContainer ul { .controls .buttonContainer ul {
@@ -69,61 +98,6 @@ body {
margin-right: 10px; margin-right: 10px;
} }
.blueButton {
box-shadow: inset 0 0 15px 3px #23395e;
background: #2e466e linear-gradient(to bottom, #2e466e 5%, #415989 100%);
border-radius: 17px;
border: 1px solid #1f2f47;
display: inline-block;
cursor: pointer;
color: #ffffff !important;
font-family: Arial, Helvetica, sans-serif;
font-size: 15px;
padding: 6px 13px;
text-decoration: none !important;
text-shadow: 0 1px 0 #263666;
}
.blueButton:hover {
background: #415989 linear-gradient(to bottom, #415989 5%, #2e466e 100%);
color: #ffffff;
text-decoration: none;
}
.blueButton:active {
text-decoration: none;
}
.greenButton {
box-shadow: inset 0 0 15px 3px #3e7327;
background: #77b55a linear-gradient(to bottom, #77b55a 5%, #72b352 100%);
border-radius: 17px;
border: 1px solid #4b8f29;
display: inline-block;
cursor: pointer;
color: #ffffff;
font-family: Arial, Helvetica, sans-serif;
font-size: 15px;
padding: 6px 13px;
text-decoration: none;
text-shadow: 0 1px 0 #5b8a3c;
}
.greenButton:hover {
background: #72b352 linear-gradient(to bottom, #72b352 5%, #77b55a 100%);
color: #ffffff;
text-decoration: none;
}
.greenButton:active {
position: relative;
top: 1px;
text-decoration: none;
}
/** /**
New Page Modal New Page Modal
*/ */
+73
View File
@@ -0,0 +1,73 @@
body {
font-family: Arial, sans-serif;
background-color: #111519;
}
.login-container {
width: 350px;
margin: 100px auto;
padding: 40px;
background-color: #212529;
border-radius: 8px;
box-shadow: 0 4px 10px rgb(0, 0, 0);
}
.login-container h2 {
text-align: center;
margin-bottom: 30px;
color: #dee2e6;
}
.login-container label {
display: block;
margin-bottom: 8px;
color: #dee2e6;
}
.login-container input[type="text"],
.login-container input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #1a1a1a;
border-radius: 4px;
box-sizing: border-box;
background: #2b3035;
color: #dee2e6;
}
.login-container button {
width: 100%;
padding: 12px;
background-color: #0066cc;
border: none;
border-radius: 4px;
color: #ffffff;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #005bb5;
}
.error-message {
color: #ff4d4d;
margin-bottom: 20px;
text-align: center;
}
.logo {
display: block;
margin: auto;
}
.shamelessPlug {
margin: 12px 0 -30px 0;
padding: 0;
display: block;
text-align: center;
color: #111519;
font-size: 0.8em;
}
+97 -16
View File
@@ -1,41 +1,95 @@
<?php <?php
// Allowed MIME types // upload.php
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
require_once "config.php";
// Autoload classes (if not using Composer)
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "./src/{$classPath}.php";
});
use NeoCMS\Authentication;
// Ensure the user is authenticated
$authentication = new Authentication($config['authentication']??[]);
if (!$authentication->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 // Check if a file has been uploaded
if (isset($_FILES['file'])) { if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['file']; $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 // Validate the file type
$finfo = finfo_open(FILEINFO_MIME_TYPE); $finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $file['tmp_name']); $mime_type = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo); finfo_close($finfo);
if (!in_array($mime_type, $allowed_types)) { if (!array_key_exists($mime_type, $allowed_types)) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Invalid file type.']); echo json_encode(['error' => 'Invalid file type.']);
exit; exit;
} }
// Set the upload directory // Validate that the file is a real image
$upload_dir = '../uploads/'; if (getimagesize($file['tmp_name']) === false) {
http_response_code(400);
// Ensure the upload directory exists echo json_encode(['error' => 'Uploaded file is not a valid image.']);
if (!is_dir($upload_dir)) { exit;
mkdir($upload_dir, 0777, true);
} }
// Generate a unique name for the image // Generate a unique name for the image
$extension = pathinfo($file['name'], PATHINFO_EXTENSION); $extension = $allowed_types[$mime_type];
$filename = uniqid() . '.' . $extension; $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 // Set the target file path
$target_file = $upload_dir . $filename; $target_file = $upload_dir . $filename;
// Move the uploaded file to the target directory // Move the uploaded file to the target directory
if (move_uploaded_file($file['tmp_name'], $target_file)) { if (move_uploaded_file($file['tmp_name'], $target_file)) {
// Set file permissions
chmod($target_file, 0644);
// Return a JSON response with the image location // Return a JSON response with the image location
$location = $target_file; $location = '/uploads/' . $filename;
echo json_encode(['location' => $location]); echo json_encode(['location' => $location]);
} else { } else {
// Return an error response // Return an error response
@@ -43,7 +97,34 @@ if (isset($_FILES['file'])) {
echo json_encode(['error' => 'Failed to move uploaded file.']); echo json_encode(['error' => 'Failed to move uploaded file.']);
} }
} else { } else {
// No file was uploaded // Handle upload errors
http_response_code(400); $error_message = 'No file uploaded.';
echo json_encode(['error' => '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' => $error_message]);
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

+34 -17
View File
@@ -1,26 +1,34 @@
<?php <?php
require_once "neoCMSCore.php"; require_once "config.php";
require_once "init.php"; // Autoload classes (if not using Composer)
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "./src/{$classPath}.php";
});
use NeoCMS\Authentication;
// Instantiate the Authentication class
$authentication = new Authentication($config['authentication'] ?? []);
// Check if the user is logged in
if (!$authentication->isLoggedIn()) {
// Redirect to the login page
header("Location: /cms/login/");
exit;
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-gb"> <html lang="en-gb">
<head> <head>
<title>NeoCMS</title> <title>NeoCMS</title>
<!-- Include jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <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="https://code.jquery.com/ui/1.14.0/jquery-ui.min.js" crossorigin="anonymous"></script>
<!-- Include TinyMCE -->
<script src="/cms/tinymce/tinymce.min.js"></script> <script src="/cms/tinymce/tinymce.min.js"></script>
<!-- Include Bootstrap for modal -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<!-- Include JqueryUI CSS -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.14.0/themes/base/jquery-ui.css"> <link rel="stylesheet" href="https://code.jquery.com/ui/1.14.0/themes/base/jquery-ui.css">
<!-- Include editor styles -->
<link rel="stylesheet" href="/cms/css/editor.css"> <link rel="stylesheet" href="/cms/css/editor.css">
<!-- Include Bootstrap JS -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head> </head>
<body> <body>
@@ -28,16 +36,24 @@ require_once "init.php";
<div class="pageContainer"> <div class="pageContainer">
<div class="controls"> <div class="controls">
<div class="logo"></div> <div class="logo"></div>
<?php if($showFullUrl) echo '<div id="urlbox"></div>';?> <?php if ($config['showFullUrl']) {?>
<div id="urlbox"></div>
<?php } ?>
<div class="buttonContainer"> <div class="buttonContainer">
<ul> <ul>
<li><a href="#" id="selectPage" class="blueButton">Select Page</a></li> <li>
<li><a href="#" id="newPage" class="blueButton">New Page</a></li> <button class="headerButton" id="newPage" role="button">New Page</button>
<li><a href="#" id="savePage" class="greenButton">Save Changes</a></li> </li>
<li>
<button class="headerButton" id="selectPage" role="button">Select Page</button>
</li>
<li>
<button class="headerButton dangerButton" id="savePage" role="button">Publish Changes</button>
</li>
</ul> </ul>
</div> </div>
<div class="loggedInDetails"> <div class="loggedInDetails">
Logged in as: <?php echo $_SESSION["core"]->getLoggedinUser(); ?><br /> Logged in as: <?php echo $authentication->getLoggedinUser(); ?><br/>
<?php <?php
if (!is_writable("logs/")) { if (!is_writable("logs/")) {
echo "<span class='redText'>Make /cms/logs/ writeable!</span>"; echo "<span class='redText'>Make /cms/logs/ writeable!</span>";
@@ -45,7 +61,8 @@ require_once "init.php";
?> ?>
</div> </div>
</div> </div>
<iframe id="frameContainer" src="<?php echo $skipWelcomePage ? "/" : "welcome.html"; ?>" class="frame"></iframe> <iframe id="frameContainer" src="<?php echo $config['skipWelcomePage'] ? "/" : "welcome.html"; ?>"
class="frame"></iframe>
</div> </div>
<!-- Modal --> <!-- Modal -->
<div id="editModal" class="modal fade" role="dialog"> <div id="editModal" class="modal fade" role="dialog">
-63
View File
@@ -1,63 +0,0 @@
<?php
/*
* Authentication stores the usernames and passwords for people to be able to use the CMS.
* This will be used as well to write to the audit log once it has been implemented in code.
*/
$authentication = [
"admin" => "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';
+3 -1
View File
@@ -274,7 +274,7 @@ $(document).ready(function () {
/** /**
* DANGER WILL ROBINSON: This may break stuff, but is needed for the buttons to be positioned properly * DANGER WILL ROBINSON: This may break stuff, but is needed for the buttons to be positioned properly
* Since this is alpha, lets see how we go. * Lets see how we go.
*/ */
if ($(element).css('position') === 'static') { if ($(element).css('position') === 'static') {
$(element).css('position', 'relative'); $(element).css('position', 'relative');
@@ -285,6 +285,8 @@ $(document).ready(function () {
$buttonContainer.css("position", "absolute"); $buttonContainer.css("position", "absolute");
$buttonContainer.css("top", "5px"); $buttonContainer.css("top", "5px");
$buttonContainer.css("right", "5px"); $buttonContainer.css("right", "5px");
$buttonContainer.css("z-index", "9999");
var $duplicateBeforeButton = $(iframeDoc.createElement('button')); var $duplicateBeforeButton = $(iframeDoc.createElement('button'));
$duplicateBeforeButton.addClass('duplicate-btn-before').text('◀️').prop('title', 'Clone before'); $duplicateBeforeButton.addClass('duplicate-btn-before').text('◀️').prop('title', 'Clone before');
+43 -69
View File
@@ -1,86 +1,59 @@
<?php <?php
require_once "../neoCMSCore.php"; // login.php
require_once "../init.php";
// Initialize error message variable require_once "../config.php";
// Autoload classes (if not using Composer)
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "../src/{$classPath}.php";
});
use NeoCMS\Authentication;
use NeoCMS\Logger;
// Initialise error message variable
$error = ''; $error = '';
if (isset($_POST['username']) && isset($_POST['password'])) { // Instantiate the Authentication and Logger classes
if (!$_SESSION['core']->login($_POST['username'], $_POST['password'])) { $authentication = new Authentication($config['authentication'] ?? []);
$error = "Invalid username or password"; $logger = new Logger($config['audit'] ?? true);
} else {
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/"); header("Location: /cms/");
die("Well done, champ! You got this!! <3"); exit("Well done, champ! You got this!! <3");
} 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";
} }
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Login</title> <title>NeoCMS Login</title>
<style> <link rel="stylesheet" href="/cms/css/login.css"/>
/* Basic styling for the login form */
body {
font-family: Arial, sans-serif;
background-color: #eef1f5;
}
.login-container {
width: 350px;
margin: 100px auto;
padding: 40px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-container h2 {
text-align: center;
margin-bottom: 30px;
color: #333333;
}
.login-container label {
display: block;
margin-bottom: 8px;
color: #555555;
}
.login-container input[type="text"],
.login-container input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #cccccc;
border-radius: 4px;
box-sizing: border-box;
}
.login-container button {
width: 100%;
padding: 12px;
background-color: #0066cc;
border: none;
border-radius: 4px;
color: #ffffff;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #005bb5;
}
.error-message {
color: #ff4d4d;
margin-bottom: 20px;
text-align: center;
}
</style>
</head> </head>
<body> <body>
<div class="login-container"> <div class="login-container">
<img class="logo" src="/cms/img/loginlogo.png" alt="NeoCMS logo"/>
<h2>NeoCMS Login</h2> <h2>NeoCMS Login</h2>
<?php if (!empty($error)): ?> <?php if (!empty($error)): ?>
<div class="error-message"><?php echo htmlspecialchars($error); ?></div> <div class="error-message"><?php echo htmlspecialchars($error); ?></div>
@@ -93,6 +66,7 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
<input type="password" id="password" name="password" required> <input type="password" id="password" name="password" required>
<button type="submit">Log In</button> <button type="submit">Log In</button>
<p class="shamelessPlug">NeoCMS &copy;<?php echo date('Y'); ?> Andy Dixon</p>
</form> </form>
</div> </div>
-63
View File
@@ -1,63 +0,0 @@
<?php
class neoCMSCore
{
public bool $auditEnabled = true;
public array $users = [];
public bool $loggedIn = false;
public string $version = '2.0';
private string $loggedInUser = '';
public function __construct()
{
}
public function login($username, $password)
{
$this->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);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace NeoCMS;
class Authentication
{
private $credentials;
public function __construct(array $credentials)
{
session_start();
$this->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();
}
}
+286
View File
@@ -0,0 +1,286 @@
<?php
namespace NeoCMS;
class CMSController
{
private array $config;
private Authentication $authentication;
private Logger $logger;
private array $request;
private string $templatesDir;
private string $documentRoot;
public function __construct($config)
{
$this->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);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace NeoCMS;
class Logger
{
private $auditEnabled;
private $logsDir;
public function __construct($auditEnabled)
{
$this->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);
}
}
-18
View File
@@ -94,24 +94,6 @@
</div> </div>
</section> </section>
<!-- Requirements Section -->
<section class="container my-5">
<h2 class="text-center mb-4">Requirements 📝</h2>
<p class="text-center">NeoCMS requires the following:</p>
<ul class="list-group list-group-flush">
<li class="list-group-item">PHP 8 running on a Linux server. (May work on Windows servers, but we don't want to stoop to that level 😉)</li>
</ul>
</section>
<!-- Known Issues Section -->
<section class="container my-5">
<h2 class="text-center mb-4">Known Bugs &amp; Issues 🐞</h2>
<ul class="list-group list-group-flush editable" style="cursor: default;">
<li class="list-group-item"><strong>File Uploads (non-image):</strong> Currently, file uploads are not fully implemented. We're working on it!</li>
<li class="list-group-item"><strong>Testing:</strong> The system is functional, but it hasn’t been fully tested in all scenarios.</li>
</ul>
</section>
<!-- License Section --> <!-- License Section -->
<section class="container my-5"> <section class="container my-5">
<h2 class="text-center mb-4">License 📄</h2> <h2 class="text-center mb-4">License 📄</h2>
+1 -1
View File
@@ -6,6 +6,6 @@
<body> <body>
<div class="editable">This is editable content 1.</div> <div class="editable">This is editable content 1.</div>
<div class="editable">This is editable content 2.</div> <div class="editable">This is editable content 2.</div>
<div class="editable neo-dupe">This is editable content 3, and this element can be cloned.</div> <div class="editable">This is editable content 3.</div>
</body> </body>
</html> </html>