287 lines
7.9 KiB
PHP
287 lines
7.9 KiB
PHP
<?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);
|
|
}
|
|
}
|