From d0c38378b6c59ae5340a5797cb5c6bdc3c8253c1 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Tue, 12 Nov 2024 21:20:46 +0000 Subject: [PATCH] Massive refactor of code into classes, improve security on the image/file upload handler. Clean up the init.php into a config.php and refactor everything else to work around it. Resolves issues #13 #14 and #16 --- README.md | 4 +- cms/config.php | 18 ++ cms/controller/index.php | 72 +------- cms/css/editor.css | 4 +- cms/image_upload.php | 115 ++++++++++-- cms/index.php | 27 ++- cms/init.php | 63 ------- cms/login/index.php | 48 ++++- cms/neoCMSCore.php | 63 ------- cms/src/NeoCMS/Authentication.php | 59 ++++++ cms/src/NeoCMS/CMSController.php | 286 ++++++++++++++++++++++++++++++ cms/src/NeoCMS/Logger.php | 46 +++++ cms/welcome.html | 18 -- 13 files changed, 581 insertions(+), 242 deletions(-) create mode 100644 cms/config.php delete mode 100644 cms/init.php delete mode 100644 cms/neoCMSCore.php create mode 100644 cms/src/NeoCMS/Authentication.php create mode 100644 cms/src/NeoCMS/CMSController.php create mode 100644 cms/src/NeoCMS/Logger.php diff --git a/README.md b/README.md index 3dc9cf7..cd6d467 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Ready to get started? Just follow these simple steps: * Drop the CMS folder into your static website. * Add the uploads folder to your site as well. -* Open /cms/init.php and edit the credentials for any users you want to set up. +* Open /cms/config.php and edit the credentials for any users you want to set up. * Update the div elements you want to make editable by adding the class editable. * Visit /cms/, log in, and start editing! @@ -33,7 +33,7 @@ That’s it! You’re up and running. ## Requirements -* PHP 8 running on a Linux server. (May work on Windows servers, but don't want to stoop to that level) +* PHP 8 running on Linux ## Known Bugs & Issues diff --git a/cms/config.php b/cms/config.php new file mode 100644 index 0000000..3927482 --- /dev/null +++ b/cms/config.php @@ -0,0 +1,18 @@ + [ + // Add more users as needed - its 'username'=>'password' if you hadn't have guessed + 'admin' => 'password123', + ], + // Enable the auditing system + 'audit' => true, + // Set to true if you do not want to see the welcome page and want to go straight to your site + 'skipWelcomePage' => false, + // Show full URL of the page being edited on the control bar + 'showFullUrl' => true +]; diff --git a/cms/controller/index.php b/cms/controller/index.php index 7bb07b5..fa69158 100644 --- a/cms/controller/index.php +++ b/cms/controller/index.php @@ -1,67 +1,13 @@ saveContent($_POST['uri'], $_POST['content']); - echo json_encode(array('message' => 'Page has been saved','destination'=>$filename)); - } else { - echo json_encode(array('error' => 'Unable to save page')); - } - break; +spl_autoload_register(function ($class) { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class); + require_once "../src/{$classPath}.php"; +}); - /** - * getTemplates AJAX call to get list of files - */ - case "getTemplates": - header('Content-type: application/json'); - $files = array_diff(scandir("../templates/"), array('..', '.')); - $returnArray = []; - foreach ($files as $file) { - $returnArray[] = array("id" => $file, "name" => str_replace(array(".html", ".htm"), "", $file)); - } - echo json_encode($returnArray); - break; +use NeoCMS\CMSController; - /** - * newPage AJAX call to take a template file and create a new page - */ - case "newPage": - header('Content-type: application/json'); - - $filename = str_replace("..", "", $_POST['filename']); - $template = str_replace("..", "", $_POST['template']); - if (!str_ends_with($filename, ".htm") && !str_ends_with($filename, ".html")) { - $filename .= ".html"; - } - - // Some vhosts can supply a trailing slash on DOCUMENT_ROOT so tidy it up - $destination = str_replace("//", "/", $_SERVER['DOCUMENT_ROOT'] . "/" . $filename); - - - if (!file_exists("../templates/" . $template)) die(json_encode(array("error" => "Template not found"))); - if (file_exists($destination)) die(json_encode(array("error" => "Page or file already exists"))); - - file_put_contents($destination, file_get_contents("../templates/" . $template)); - if (!file_exists($destination)) die(json_encode(array("error" => "Template copy failed"))); - echo json_encode(array("ok" => "DONE_THE_NEEDFUL", "url" => str_replace("//", "/", "/" . $filename))); - break; - - case "getPages": - header('Content-type: application/json'); - $it = new RecursiveDirectoryIterator("../../"); - $returnArray = []; - foreach (new RecursiveIteratorIterator($it) as $file) { - // Strip document root from URI, so relative - $file = str_replace($_SERVER["DOCUMENT_ROOT"], "", $file->getRealPath()); - - if (!str_starts_with($file, "/cms/") && (str_ends_with($file, ".html") || str_ends_with($file, ".htm"))) - $returnArray[] = array("name" => $file, "url" => $file); - } - echo json_encode($returnArray); - break; - -} \ No newline at end of file +// Instantiate the controller with necessary dependencies +$controller = new CMSController($config ?? []); +$controller->handleRequest(); \ No newline at end of file diff --git a/cms/css/editor.css b/cms/css/editor.css index 0bbafac..6404d65 100644 --- a/cms/css/editor.css +++ b/cms/css/editor.css @@ -49,12 +49,12 @@ body { .controls #urlbox div { float: right; - margin-left:10px; + margin-left: 10px; } .controls .loggedInDetails { position: absolute; - top: 12px; + top: 20px; right: 10px; } diff --git a/cms/image_upload.php b/cms/image_upload.php index c96358f..e64ed86 100644 --- a/cms/image_upload.php +++ b/cms/image_upload.php @@ -1,41 +1,95 @@ isLoggedIn()) { + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Unauthorised']); + exit; +} + +// Set appropriate headers +header('Content-Type: application/json'); + +// Allowed MIME types and extensions +$allowed_types = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'text/pdf' => 'pdf', + 'text/docx' => 'docx', + 'text/xlsx' => 'xlsx', + 'text/doc' => 'doc', +]; + +// Maximum file size in bytes (50MB) +$max_file_size = 50 * 1024 * 1024; // Check if a file has been uploaded -if (isset($_FILES['file'])) { +if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) { $file = $_FILES['file']; + // Check file size + if ($file['size'] > $max_file_size) { + http_response_code(400); + echo json_encode(['error' => 'File size exceeds the maximum limit.']); + exit; + } + // Validate the file type - $finfo = finfo_open(FILEINFO_MIME_TYPE); + $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); - if (!in_array($mime_type, $allowed_types)) { + if (!array_key_exists($mime_type, $allowed_types)) { http_response_code(400); echo json_encode(['error' => 'Invalid file type.']); exit; } - // Set the upload directory - $upload_dir = '../uploads/'; - - // Ensure the upload directory exists - if (!is_dir($upload_dir)) { - mkdir($upload_dir, 0777, true); + // Validate that the file is a real image + if (getimagesize($file['tmp_name']) === false) { + http_response_code(400); + echo json_encode(['error' => 'Uploaded file is not a valid image.']); + exit; } // Generate a unique name for the image - $extension = pathinfo($file['name'], PATHINFO_EXTENSION); - $filename = uniqid() . '.' . $extension; + $extension = $allowed_types[$mime_type]; + $filename = uniqid('', true) . '.' . $extension; + + // Set the upload directory + $upload_dir = realpath(__DIR__ . '/../uploads') . DIRECTORY_SEPARATOR; + + // Ensure the upload directory exists + if (!is_dir($upload_dir) && !mkdir($upload_dir, 0755, true)) { + http_response_code(500); + echo json_encode(['error' => 'Failed to create upload directory.']); + exit; + } // Set the target file path $target_file = $upload_dir . $filename; // Move the uploaded file to the target directory if (move_uploaded_file($file['tmp_name'], $target_file)) { + // Set file permissions + chmod($target_file, 0644); + // Return a JSON response with the image location - $location = $target_file; + $location = '/uploads/' . $filename; echo json_encode(['location' => $location]); } else { // Return an error response @@ -43,7 +97,34 @@ if (isset($_FILES['file'])) { echo json_encode(['error' => 'Failed to move uploaded file.']); } } else { - // No file was uploaded + // Handle upload errors + $error_message = 'No file uploaded.'; + if (isset($_FILES['file']['error'])) { + switch ($_FILES['file']['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $error_message = 'File size exceeds the maximum limit.'; + break; + case UPLOAD_ERR_PARTIAL: + $error_message = 'The uploaded file was only partially uploaded.'; + break; + case UPLOAD_ERR_NO_FILE: + $error_message = 'No file was uploaded.'; + break; + case UPLOAD_ERR_NO_TMP_DIR: + $error_message = 'Missing a temporary folder.'; + break; + case UPLOAD_ERR_CANT_WRITE: + $error_message = 'Failed to write file to disk.'; + break; + case UPLOAD_ERR_EXTENSION: + $error_message = 'A PHP extension stopped the file upload.'; + break; + default: + $error_message = 'Unknown upload error.'; + break; + } + } http_response_code(400); - echo json_encode(['error' => 'No file uploaded.']); -} \ No newline at end of file + echo json_encode(['error' => $error_message]); +} diff --git a/cms/index.php b/cms/index.php index b9da0c9..0ecb562 100644 --- a/cms/index.php +++ b/cms/index.php @@ -1,7 +1,24 @@ isLoggedIn()) { + // Redirect to the login page + header("Location: /cms/login/"); + exit; +} ?> + @@ -28,7 +45,7 @@ require_once "init.php";
-
';?> +
';?>
- Logged in as: getLoggedinUser(); ?>
+ Logged in as: getLoggedinUser(); ?>
Make /cms/logs/ writeable!"; @@ -45,7 +62,7 @@ require_once "init.php"; ?>
- + - -
-

Requirements 📝

-

NeoCMS requires the following:

- -
- - -
-

Known Bugs & Issues 🐞

- -
-

License 📄