Compare commits
10
Commits
2.0.0.a3
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38563714dd | ||
|
|
ab54b1415b | ||
|
|
e7373477c5 | ||
|
|
5a02c25074 | ||
|
|
09de8ca167 | ||
|
|
359b4e5b6e | ||
|
|
f766f4a6f5 | ||
|
|
18547d7a17 | ||
|
|
290a74cb29 | ||
|
|
af9276de95 |
@@ -14,7 +14,7 @@ smoother for your content editing experience.
|
|||||||
* No Database Required: NeoCMS works without a database, so you don’t have to worry about setup hassles or performance
|
* No Database Required: NeoCMS works without a database, so you don’t have to worry about setup hassles or performance
|
||||||
slowdowns.
|
slowdowns.
|
||||||
* Easy Content Editing: With our intuitive WYSIWYG editor, anyone can update content without writing a single line of
|
* Easy Content Editing: With our intuitive WYSIWYG editor, anyone can update content without writing a single line of
|
||||||
code.
|
code. Adding the additional class *neo-dupe* will allow an element and it's children to be cloned.
|
||||||
* Seamless Integration: Simply add the CMS to any existing static website. Just assign a special class to the elements
|
* Seamless Integration: Simply add the CMS to any existing static website. Just assign a special class to the elements
|
||||||
you want to make editable, and you’re all set.
|
you want to make editable, and you’re all set.
|
||||||
* Consistent Layouts: NeoCMS supports page templates to keep your layouts neat and uniform across all pages.
|
* Consistent Layouts: NeoCMS supports page templates to keep your layouts neat and uniform across all pages.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
];
|
||||||
@@ -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'])) {
|
});
|
||||||
$_SESSION['core']->saveContent($_POST['uri'], $_POST['content']);
|
|
||||||
echo json_encode(array('message' => 'Page has been saved'));
|
|
||||||
} 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;
|
|
||||||
|
|
||||||
}
|
|
||||||
+50
-52
@@ -12,8 +12,8 @@ body {
|
|||||||
|
|
||||||
.pageContainer .controls {
|
.pageContainer .controls {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
height: 70px;
|
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,21 +26,65 @@ body {
|
|||||||
|
|
||||||
.controls .logo {
|
.controls .logo {
|
||||||
background-image: url("/cms/img/logo.png");
|
background-image: url("/cms/img/logo.png");
|
||||||
width: 128px;
|
width: 60px;
|
||||||
height: 70px;
|
height: 60px;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .buttonContainer {
|
.controls .buttonContainer {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
margin: 18px auto;
|
margin: 12px 0;
|
||||||
|
position: absolute;
|
||||||
|
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 {
|
||||||
|
margin: 15px 70px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5em;
|
||||||
|
position: absolute;
|
||||||
|
color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls #urlbox div {
|
||||||
|
float: right;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .loggedInDetails {
|
.controls .loggedInDetails {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 20px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .buttonContainer ul {
|
.controls .buttonContainer ul {
|
||||||
@@ -54,52 +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;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 6px 13px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: 0 1px 0 #263666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueButton:hover {
|
|
||||||
background: #415989 linear-gradient(to bottom, #415989 5%, #2e466e 100%);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greenButton:active {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
New Page Modal
|
New Page Modal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/* General Styles */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Styles */
|
|
||||||
header {
|
|
||||||
background-color: #1b6ac9;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content Styles */
|
|
||||||
main {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5em;
|
|
||||||
color: #1b6ac9;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 1.1em;
|
|
||||||
line-height: 1.6;
|
|
||||||
max-width: 700px;
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Styles */
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 15px 30px;
|
|
||||||
background-color: #1b6ac9;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1em;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background-color: #155a9c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer Styles */
|
|
||||||
footer {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
padding: 15px 0;
|
|
||||||
text-align: center;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
+97
-16
@@ -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 |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
+34
-16
@@ -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,15 +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 ($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>";
|
||||||
@@ -44,7 +61,8 @@ require_once "init.php";
|
|||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="frameContainer" src="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">
|
||||||
|
|||||||
@@ -1,56 +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",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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';
|
|
||||||
|
|
||||||
+115
-5
@@ -11,9 +11,15 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
// Set up click handler on divs with class 'editable' in the iframe
|
// Set up click handler on divs with class 'editable' in the iframe
|
||||||
$(iframeDoc).find('.editable').css('cursor', 'pointer').click(function (event) {
|
$(iframeDoc).find('.editable').css('cursor', 'pointer').click(function (event) {
|
||||||
|
editElement(event, $(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit Element Shared functionality
|
||||||
|
|
||||||
|
function editElement(event, element) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
currentDiv = $(this);
|
currentDiv = element;
|
||||||
|
|
||||||
// Get the content of the div
|
// Get the content of the div
|
||||||
const content = currentDiv.html();
|
const content = currentDiv.html();
|
||||||
@@ -21,7 +27,7 @@ $(document).ready(function () {
|
|||||||
// Set the content in TinyMCE
|
// Set the content in TinyMCE
|
||||||
$('#editor').val(content);
|
$('#editor').val(content);
|
||||||
|
|
||||||
// Open the modal and init tinyMCE
|
// Open the modal and init TinyMCE
|
||||||
$('#editModal').modal('show')
|
$('#editModal').modal('show')
|
||||||
.on('shown.bs.modal', function () {
|
.on('shown.bs.modal', function () {
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
@@ -37,13 +43,66 @@ $(document).ready(function () {
|
|||||||
images_upload_credentials: true,
|
images_upload_credentials: true,
|
||||||
force_br_newlines: false,
|
force_br_newlines: false,
|
||||||
force_p_newlines: false,
|
force_p_newlines: false,
|
||||||
|
license_key: 'gpl',
|
||||||
|
forced_root_block: 'aaa',
|
||||||
newline_behavior: 'linebreak'
|
newline_behavior: 'linebreak'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateButtons();
|
||||||
|
|
||||||
|
// Set up event handlers for duplication buttons inside the iframe
|
||||||
|
$(iframeDoc).on('click', '.duplicate-btn-before', function () {
|
||||||
|
var parentElement = $(this).closest('.neo-dupe');
|
||||||
|
var clonedElement = $(parentElement).clone();
|
||||||
|
|
||||||
|
// Remove buttons from the cloned element
|
||||||
|
clonedElement.find('.button-container').remove();
|
||||||
|
|
||||||
|
// Add system classes
|
||||||
|
clonedElement.addClass("neo-dupe");
|
||||||
|
clonedElement.addClass("editable");
|
||||||
|
|
||||||
|
// Insert the cloned element before the original parent element
|
||||||
|
clonedElement.insertBefore(parentElement);
|
||||||
|
|
||||||
|
// Trigger an event on the new duplicated element
|
||||||
|
clonedElement.trigger('elementDuplicated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(iframeDoc).on('click', '.duplicate-btn-after', function () {
|
||||||
|
var parentElement = $(this).closest('.neo-dupe');
|
||||||
|
var clonedElement = $(parentElement).clone();
|
||||||
|
|
||||||
|
// Remove buttons from the cloned element
|
||||||
|
clonedElement.find('.button-container').remove();
|
||||||
|
|
||||||
|
// Add system classes
|
||||||
|
clonedElement.addClass("neo-dupe");
|
||||||
|
clonedElement.addClass("editable");
|
||||||
|
|
||||||
|
// Insert the cloned element after the original parent element
|
||||||
|
clonedElement.insertAfter(parentElement);
|
||||||
|
|
||||||
|
// Trigger an event on the new duplicated element
|
||||||
|
clonedElement.trigger('elementDuplicated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle the custom 'elementDuplicated' event
|
||||||
|
$(iframeDoc).on('elementDuplicated', '.neo-dupe', function (event) {
|
||||||
|
// Example action: Animate the new element
|
||||||
|
$(this).hide().fadeIn('slow');
|
||||||
|
|
||||||
|
// Remove buttons if they are there
|
||||||
|
$(this).find('.button-container').remove();
|
||||||
|
|
||||||
|
editElement(event, $(this));
|
||||||
|
});
|
||||||
|
// Here for addButtonsForNeoDupe
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Save changes made to a div, but not save to the file on the server
|
// Save changes made to a div, but not save to the file on the server
|
||||||
$('#saveBtn').click(function () {
|
$('#saveBtn').click(function () {
|
||||||
// Get the data from TinyMCE
|
// Get the data from TinyMCE
|
||||||
@@ -70,7 +129,16 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
// Handle Saving changes to the page
|
// Handle Saving changes to the page
|
||||||
$('#savePage').click(() => {
|
$('#savePage').click(() => {
|
||||||
frameContainerElement = $('#frameContainer').contents().get(0);
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
$.post("/cms/controller/", {
|
$.post("/cms/controller/", {
|
||||||
action: "save",
|
action: "save",
|
||||||
uri: frameContainerElement.location.pathname,
|
uri: frameContainerElement.location.pathname,
|
||||||
@@ -81,8 +149,8 @@ $(document).ready(function () {
|
|||||||
} else {
|
} else {
|
||||||
showMessage(data.message, "error");
|
showMessage(data.message, "error");
|
||||||
}
|
}
|
||||||
// alert(data)
|
|
||||||
});
|
});
|
||||||
|
regenerateButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show dialog for page templates
|
// Show dialog for page templates
|
||||||
@@ -192,10 +260,51 @@ $(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();
|
||||||
|
addButtonsToNeoDupe(this);
|
||||||
|
});
|
||||||
|
$('#urlbox').html("<strong>Editing:</strong> <div>" + $(iframeDoc)[0].URL + "</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to add buttons to a neo-dupe element inside the iframe
|
||||||
|
function addButtonsToNeoDupe(element) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DANGER WILL ROBINSON: This may break stuff, but is needed for the buttons to be positioned properly
|
||||||
|
* Lets see how we go.
|
||||||
|
*/
|
||||||
|
if ($(element).css('position') === 'static') {
|
||||||
|
$(element).css('position', 'relative');
|
||||||
|
}
|
||||||
|
|
||||||
|
var $buttonContainer = $(iframeDoc.createElement('div'));
|
||||||
|
$buttonContainer.addClass('button-container');
|
||||||
|
$buttonContainer.css("position", "absolute");
|
||||||
|
$buttonContainer.css("top", "5px");
|
||||||
|
$buttonContainer.css("right", "5px");
|
||||||
|
$buttonContainer.css("z-index", "9999");
|
||||||
|
|
||||||
|
|
||||||
|
var $duplicateBeforeButton = $(iframeDoc.createElement('button'));
|
||||||
|
$duplicateBeforeButton.addClass('duplicate-btn-before').text('◀️').prop('title', 'Clone before');
|
||||||
|
|
||||||
|
var $duplicateAfterButton = $(iframeDoc.createElement('button'));
|
||||||
|
$duplicateAfterButton.addClass('duplicate-btn-after').text('▶️').prop('title', 'Clone after');
|
||||||
|
|
||||||
|
// Append buttons to the container
|
||||||
|
$buttonContainer.append($duplicateBeforeButton, $duplicateAfterButton);
|
||||||
|
|
||||||
|
// Prepend the container to the neo-dupe element
|
||||||
|
$(element).prepend($buttonContainer);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show A message bar across the top of the screen for a few seconds
|
* Show a message bar across the top of the screen for a few seconds
|
||||||
* @param message - string - message to be shown
|
* @param message - string - message to be shown
|
||||||
* @param type - string "success" or "error"
|
* @param type - string "success" or "error"
|
||||||
*/
|
*/
|
||||||
@@ -232,3 +341,4 @@ function showMessage(message, type) {
|
|||||||
messageBar.slideUp();
|
messageBar.slideUp();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-69
@@ -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 ©<?php echo date('Y'); ?> Andy Dixon</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
-27
@@ -1,32 +1,129 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8">
|
<title>NeoCMS - Super Lightweight CMS</title>
|
||||||
<title>Welcome to NeoCMS</title>
|
<!-- Bootstrap CSS -->
|
||||||
<link href="/cms/css/welcome.style.css" rel="stylesheet">
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
|
||||||
</head>
|
<!-- Material Design Bootstrap -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.20.0/css/mdb.min.css" />
|
||||||
|
<!-- Custom Styles -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-top: 70px;
|
||||||
|
background-color: #ECEFF1;
|
||||||
|
}
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #2196F3;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: #CFD8DC;
|
||||||
|
}
|
||||||
|
.jumbotron {
|
||||||
|
background-color: #90CAF9;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #1976D2;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #1976D2;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1565C0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">/* Chart.js */
|
||||||
|
@-webkit-keyframes chartjs-render-animation{from{opacity:0.99}to{opacity:1}}@keyframes chartjs-render-animation{from{opacity:0.99}to{opacity:1}}.chartjs-render-monitor{-webkit-animation:chartjs-render-animation 0.001s;animation:chartjs-render-animation 0.001s;}</style><style type="text/css">/* Chart.js */
|
||||||
|
@-webkit-keyframes chartjs-render-animation{from{opacity:0.99}to{opacity:1}}@keyframes chartjs-render-animation{from{opacity:0.99}to{opacity:1}}.chartjs-render-monitor{-webkit-animation:chartjs-render-animation 0.001s;animation:chartjs-render-animation 0.001s;}</style></head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
|
||||||
<img alt="Application Logo" class="logo" src="/cms/img/welcomeLogo.png">
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h1>Welcome to NeoCMS</h1>
|
|
||||||
<p>Introducing a cutting-edge CMS designed to effortlessly integrate with your existing flat-file website—no
|
|
||||||
database required. Our databaseless WYSIWYG content management system empowers you to create, edit, and manage
|
|
||||||
content visually, all while maintaining the simplicity and speed of a flat-file setup. Enjoy a seamless
|
|
||||||
experience that combines the flexibility of a modern CMS with the efficiency of a static website, allowing you
|
|
||||||
to focus on content without the complexity of database management. Perfect for developers and creators looking
|
|
||||||
to streamline workflows without sacrificing control or performance.</p>
|
|
||||||
|
|
||||||
<p class="editable">
|
<!-- Header -->
|
||||||
This box is editable! Just click me to edit me!<br/>
|
<header class="jumbotron text-center">
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
<h1 class="display-4">Welcome to NeoCMS! 🎉</h1>
|
||||||
Quisque sit amet accumsan tortor. Itaque earum rerum hic tenetur a sapiente delectus.
|
<p class="lead editable" style="cursor: default;">The super lightweight, database-free content management system!</p>
|
||||||
|
<a href="/" class="btn btn-primary btn-lg waves-effect waves-light">Start Editing your Website</a>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Version Section -->
|
||||||
|
<section class="container my-5">
|
||||||
|
<h2 class="text-center mb-4">You are running version 2.0! ✨</h2>
|
||||||
|
<p class="text-center editable" style="cursor: default;">This page is editable for you to familiarise yourself with how Neo works! Any editable blocks will have a hand pointer instead of your usual pointer.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features Section -->
|
||||||
|
<section class="container my-5">
|
||||||
|
<h2 class="text-center mb-4">Features 🚀</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="media mb-4">
|
||||||
|
<div class="feature-icon mr-3">📦</div>
|
||||||
|
<div class="editable media-body" style="cursor: default;">
|
||||||
|
<h5 class="mt-0">No Database Required</h5>
|
||||||
|
<p>NeoCMS works without a database, so you don’t have to worry about setup hassles or performance slowdowns.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media mb-4">
|
||||||
|
<div class="feature-icon mr-3">✏️</div>
|
||||||
|
<div class="editable media-body" style="cursor: default;">
|
||||||
|
<h5 class="mt-0">Easy Content Editing</h5>
|
||||||
|
<p>With our intuitive WYSIWYG editor, anyone can update content without writing a single line of code. Adding the additional class <code>neo-dupe</code> will allow an element and its children to be cloned.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="media mb-4">
|
||||||
|
<div class="feature-icon mr-3">🔗</div>
|
||||||
|
<div class="editable media-body" style="cursor: default;">
|
||||||
|
<h5 class="mt-0">Seamless Integration</h5>
|
||||||
|
<p>Simply add the CMS to any existing static website. Assign a special class to the elements you want to make editable, and you’re all set.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media mb-4">
|
||||||
|
<div class="feature-icon mr-3">🖼️</div>
|
||||||
|
<div class="editable media-body" style="cursor: default;">
|
||||||
|
<h5 class="mt-0">Consistent Layouts</h5>
|
||||||
|
<p>NeoCMS supports page templates to keep your layouts neat and uniform across all pages.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- License Section -->
|
||||||
|
<section class="container my-5">
|
||||||
|
<h2 class="text-center mb-4">License 📄</h2>
|
||||||
|
<p class="text-center">NeoCMS is open-source software licensed under the GNU General Public License v3.0 (GPLv3). You are free to use, modify, and distribute this software under the terms of this license.</p>
|
||||||
|
<p class="text-center">For more information, see the full <a href="#" target="_blank">GPLv3 License</a> found in <code>LICENSE</code>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Bug Report Section -->
|
||||||
|
<section class="container my-5">
|
||||||
|
<h2 class="text-center mb-4">Found a Bug? 🐛</h2>
|
||||||
|
<p class="text-center">If you run into any issues or have feature requests, please submit them on our <a href="https://github.com/andydixon/neoCMS/issues" target="_blank">GitHub issues page</a>. Your feedback helps us improve NeoCMS!</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<div class="container editable" style="cursor: default;">
|
||||||
|
<p>© 2024 <a href="https://www.andydixon.com">Andy Dixon</a>. All rights reserved.</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/andydixon/neoCMS/issues" target="_blank">Report an Issue</a>
|
||||||
</p>
|
</p>
|
||||||
<a class="button" href="/">Get Started with your website</a>
|
</div>
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2024 Andy Dixon</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
|
||||||
</html>
|
<!-- Bootstrap JS and dependencies -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<!-- Material Design Bootstrap JS -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.20.0/js/mdb.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="hiddendiv common"></div></body></html>
|
||||||
Reference in New Issue
Block a user