Implemented multi-user support (#27)

* Implemented rudimentary multi-user support

* Delete src.zip

* Improve the update user function

And added a new function to the auth module to invalidate a session

* Update AntAuth.php

* Rename the configs, a bit more auth stuff

* Fix test and JS regex

* Turn the admin landing page into a twig template

* plugin/admin/ -> admin/

* Refactored templating for plugins

Plus. I finally converted the remaining options in the admin plugin to twig templates. No extra styling, but it'll be easier now

* Fix PHPStan warnings

* Basic "first time" user setup

* Improved styling

* Started implementing user management

* Completed user management in the admin panel

* Renamed templates, added support for sub-dirs

* Limit and validate allowed chars for usernames

* Finished the basics of the profile plugin

* Styling for the bootstrap theme

* Some more final touches

* Added an example to show author

* Tweak to the readme
This commit is contained in:
Belle Aerni 2023-03-07 02:09:32 -08:00 committed by GitHub
parent cba9f71f78
commit 23e8b18ba3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 862 additions and 190 deletions

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: php-actions/composer@v6
- run: |
cp ./tests/Includes/config.yaml ./src/Config/config.yaml
cp ./tests/Includes/Config.yaml ./src/Config/Config.yaml
- uses: php-actions/phpunit@v3
with:
bootstrap: src/Vendor/autoload.php

4
.gitignore vendored
View file

@ -1,5 +1,5 @@
/src/Vendor/
node_modules
src/Cache/*
src/Config/config.yaml
src/Config/pages.yaml
src/Config/Config.yaml
src/Config/Pages.yaml

View file

@ -28,35 +28,32 @@ Here is an example of the default theme folder structure:
- `/Themes`
- `/Default`
- `/Templates`
- `default_layout.html.twig`
- `nav_layout.html.twig`
- `default.html.twig`
- `nav.html.twig`
- `/Assets`
- `tailwind.css`
To change the active theme, simply edit `config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
To change the active theme, simply edit `Config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
### Configuring AntCMS
AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `config.yaml` and `pages.yaml`. These files will be automatically generated by AntCMS if they do not exist.
AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `Config.yaml`, `Pages.yaml`, and `Users.yaml`. These files will be automatically generated by AntCMS if they do not exist.
#### Options in `Config/config.yaml`
#### Options in `Config/Config.yaml`
- `siteInfo:`
- `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website.
- `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection.
- `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use.
- `enableCache: true` - Enables or disables file caching in AntCMS.
- `admin:`
- `username: 'Admin'` - The username used to access any parts of AntCMS that may require authentication.
- `password: 'dontmakeitpassword123'` - The password associated with the admin account. Can be entered as plain text and then will automatically be hashed with the first login. This does need to be manually entered initially.
- `debug: true`- Enabled or disables debug mode.
- `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed.
#### Options in `Config/pages.yaml`
#### Options in `Config/Pages.yaml`
The `pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `pages.yaml` file.
The order of which files are stored inside of the `pages.yaml` file dictates what order they will be displayed in the browser window.
Here's what the `pages.yaml` file looks like:
The `Pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `Pages.yaml` file.
The order of which files are stored inside of the `Pages.yaml` file dictates what order they will be displayed in the browser window.
Here's what the `Pages.yaml` file looks like:
- `pageTitle: 'Hello World'` - This defines what the title of the page is in the navbar.
- `fullPagePath: /antcms.example.com/public_html/Content/index.md` - This defines the full path to your page, as PHP would use to access it.
@ -65,7 +62,7 @@ Here's what the `pages.yaml` file looks like:
#### The Admin Plugin
AntCMS has a very simple admin plugin. Once you set your password in your `Config/config.yaml`, you can access it by visiting `antcms.example.com/plugin/admin`.
AntCMS has a very simple admin plugin that you can access it by visiting `antcms.example.com/admin`.
It will then require you to authenticate using your AntCMS credentials and from there will give you a few simple actions such as editing your config, a page, or regenerating the page list.
The admin plugin also features a live preview of the content you are creating, but it's important to note that the preview doesn't support all of the markdown syntax that AntCMS does, such as emojis.

View file

@ -3,10 +3,34 @@
namespace AntCMS;
use AntCMS\AntConfig;
use AntCMS\AntCMS;
class AntAuth
{
protected $role;
protected $username;
protected $authenticated;
public function getRole()
{
return $this->role;
}
public function getUsername()
{
return $this->username;
}
public function getName()
{
$currentUser = AntUsers::getUser($this->username);
return $currentUser['name'];
}
public function isAuthenticated()
{
return $this->authenticated ?? false;
}
/**
* Check if the user is authenticated using the credentials in the config file.
* If the plain text password in the config file is still present, it will be hashed and the config file will be updated.
@ -14,35 +38,38 @@ class AntAuth
*
* @return void
*/
public static function checkAuth()
public function checkAuth()
{
$currentConfig = AntConfig::currentConfig();
$username = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
if (empty($currentConfig['admin']['password'])) {
AntCMS::renderException('401', 401, 'You must set a password in your config.yaml file before you can authenticate within AntCMS.');
$currentUser = AntUsers::getUser($username);
if (is_null($currentUser) || empty($currentUser['password'])) {
$this->requireAuth();
}
// If the stored password is not hashed in the config, hash it
if ($password == $currentConfig['admin']['password']) {
$currentConfig['admin']['password'] = password_hash($currentConfig['admin']['password'], PASSWORD_DEFAULT);
AntConfig::saveConfig($currentConfig);
if ($password == $currentUser['password']) {
AntUsers::updateUser($username, ['password' => $password]);
// Reload the config so the next step can pass
$currentConfig = AntConfig::currentConfig();
// Reload the user info so the next step can pass
$currentUser = AntUsers::getUser($username);
}
// If the credentials are still set valid, but the auth cookie has expired, re-require authentication.
if (!isset($_COOKIE['auth'])) {
AntAuth::requireAuth();
if (!isset($_COOKIE['auth']) && $_COOKIE['auth'] == 'valid') {
$this->requireAuth();
}
if ($currentConfig['admin']['username'] == $username && password_verify($password, $currentConfig['admin']['password'])) {
if (password_verify($password, $currentUser['password'])) {
$this->username = $username;
$this->role = $currentUser['role'] ?? '';
return;
}
AntAuth::requireAuth();
$this->requireAuth();
}
/**
@ -50,9 +77,9 @@ class AntAuth
*
* @return void
*/
private static function requireAuth()
private function requireAuth()
{
setcookie("auth", "true");
setcookie("auth", "valid");
$title = AntConfig::currentConfig('siteInfo.siteTitle');
header('WWW-Authenticate: Basic realm="' . $title . '"');
@ -60,4 +87,10 @@ class AntAuth
echo 'You must enter a valid username and password to access this page';
exit;
}
public function invalidateSession()
{
$this->authenticated = false;
$this->requireAuth();
}
}

View file

@ -8,6 +8,13 @@ use AntCMS\AntConfig;
class AntCMS
{
protected $antTwig;
public function __construct()
{
$this->antTwig = new AntTwig();
}
/**
* Renders a page based on the provided page name.
*
@ -31,8 +38,9 @@ class AntCMS
'AntCMSAuthor' => $content['author'],
'AntCMSKeywords' => $content['keywords'],
'AntCMSBody' => AntMarkdown::renderMarkdown($content['content']),
'DisplayAuthor' => true,
];
$pageTemplate = AntTwig::renderWithTiwg($pageTemplate, $params);
$pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params);
$end_time = microtime(true);
$elapsed_time = round($end_time - $start_time, 4);
@ -55,8 +63,8 @@ class AntCMS
{
$siteInfo = AntCMS::getSiteInfo();
$pageTemplate = self::getThemeTemplate('default_layout', $theme);
$pageTemplate = str_replace('<!--AntCMS-Navigation-->', AntPages::generateNavigation(self::getThemeTemplate('nav_layout', $theme), $currentPage), $pageTemplate);
$pageTemplate = self::getThemeTemplate('default', $theme);
$pageTemplate = str_replace('<!--AntCMS-Navigation-->', AntPages::generateNavigation(self::getThemeTemplate('nav', $theme), $currentPage), $pageTemplate);
return $pageTemplate = str_replace('<!--AntCMS-SiteTitle-->', $siteInfo['siteTitle'], $pageTemplate);
}
@ -69,7 +77,7 @@ class AntCMS
* @param string $exceptionString An optional parameter to define a custom string to be displayed along side the exception.
* @return never
*/
public static function renderException(string $exceptionCode, int $httpCode = 404, string $exceptionString = 'That request caused an exception to be thrown.')
public function renderException(string $exceptionCode, int $httpCode = 404, string $exceptionString = 'That request caused an exception to be thrown.')
{
$exceptionString .= " (Code {$exceptionCode})";
$pageTemplate = self::getPageLayout();
@ -79,7 +87,7 @@ class AntCMS
'AntCMSBody' => '<h1>An error ocurred</h1><p>' . $exceptionString . '</p>',
];
try {
$pageTemplate = AntTwig::renderWithTiwg($pageTemplate, $params);
$pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params);
} catch (\Exception) {
$pageTemplate = str_replace('{{ AntCMSTitle }}', $params['AntCMSTitle'], $pageTemplate);
$pageTemplate = str_replace('{{ AntCMSBody | raw }} ', $params['AntCMSBody'], $pageTemplate);
@ -117,7 +125,7 @@ class AntCMS
* @param string|null $theme
* @return string
*/
public static function getThemeTemplate(string $layout = 'default_layout', string $theme = null)
public static function getThemeTemplate(string $layout = 'default', string $theme = null)
{
$theme = $theme ?? AntConfig::currentConfig('activeTheme');
@ -125,22 +133,25 @@ class AntCMS
$theme = 'Default';
}
$templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates');
$defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates');
$templates = AntTools::getFileList($templatePath, 'twig');
if (strpos($layout, '_') !== false) {
$layoutPrefix = explode('_', $layout)[0];
$templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates' . '/' . $layoutPrefix);
$defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates' . '/' . $layoutPrefix);
} else {
$templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates');
$defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates');
}
try {
if (in_array($layout . '.html.twig', $templates)) {
$template = file_get_contents(AntTools::repairFilePath($templatePath . '/' . $layout . '.html.twig'));
} else {
$template = @file_get_contents(AntTools::repairFilePath($templatePath . '/' . $layout . '.html.twig'));
if (empty($template)) {
$template = file_get_contents(AntTools::repairFilePath($defaultTemplates . '/' . $layout . '.html.twig'));
}
} catch (\Exception) {
}
if (empty($template)) {
if ($layout == 'default_layout') {
if ($layout == 'default') {
$template = '
<!DOCTYPE html>
<html>
@ -222,4 +233,11 @@ class AntCMS
readfile($path);
}
}
public static function redirect(string $url)
{
$url = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . $url);
header("Location: $url");
exit;
}
}

View file

@ -12,7 +12,6 @@ class AntConfig
'forceHTTPS',
'activeTheme',
'enableCache',
'admin',
'debug',
'baseURL',
];
@ -30,10 +29,6 @@ class AntConfig
'forceHTTPS' => true,
'activeTheme' => 'Default',
'enableCache' => true,
'admin' => array(
'username' => 'Admin',
'password' => '',
),
'debug' => true,
'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
];

View file

@ -66,6 +66,7 @@ class AntPages
{
$pages = AntPages::getPages();
$antCache = new AntCache;
$antTwig = new AntTwig();
$theme = AntConfig::currentConfig('activeTheme');
$cacheKey = $antCache->createCacheKey(json_encode($pages), $theme . $currentPage);
@ -95,7 +96,7 @@ class AntPages
}
}
$navHTML = AntTwig::renderWithTiwg($navTemplate, array('pages' => $pages));
$navHTML = $antTwig->renderWithTiwg($navTemplate, array('pages' => $pages));
$antCache->setCache($cacheKey, $navHTML);
return $navHTML;

View file

@ -65,4 +65,15 @@ class AntTools
return AntTools::repairFilePath($pagePath);
}
public static function valuesNotNull(array $required, array $actual)
{
foreach ($required as $key) {
if (!key_exists($key, $actual) or is_null($actual[$key])) {
return false;
}
}
return true;
}
}

View file

@ -3,36 +3,40 @@
namespace AntCMS;
use AntCMS\AntConfig;
use AntCMS\AntTwigFilters;
class AntTwig
{
/**
* @param array<mixed> $params
* @param string|null $theme
* @return string
*/
public static function renderWithTiwg(string $content = '', array $params = array(), string $theme = null)
protected $twigEnvironment;
protected $theme;
public function __construct(string $theme = null)
{
$twigCache = AntConfig::currentConfig('enableCache') ? AntCachePath : false;
$theme = $theme ?? AntConfig::currentConfig('activeTheme');
$this->theme = $theme ?? AntConfig::currentConfig('activeTheme');
if (!is_dir(antThemePath . '/' . $theme)) {
$theme = 'Default';
if (!is_dir(antThemePath . '/' . $this->theme)) {
$this->theme = 'Default';
}
$templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates');
$filesystemLoader = new \Twig\Loader\FilesystemLoader($templatePath);
$stringLoader = new \Shapecode\Twig\Loader\StringLoader();
$chainLoader = new \Twig\Loader\ChainLoader([$stringLoader, $filesystemLoader]);
$twigEnvironment = new \Twig\Environment($chainLoader, [
$this->twigEnvironment = new \Twig\Environment(new \Shapecode\Twig\Loader\StringLoader(), [
'cache' => $twigCache,
'debug' => AntConfig::currentConfig('debug'),
]);
$twigEnvironment->addExtension(new \AntCMS\AntTwigFilters);
$this->twigEnvironment->addExtension(new \AntCMS\AntTwigFilters);
}
return $twigEnvironment->render($content, $params);
public function renderWithSubLayout(string $layout, array $params = array())
{
$subLayout = AntCMS::getThemeTemplate($layout, $this->theme);
$mainLayout = AntCMS::getPageLayout($this->theme);
$params['AntCMSBody'] = $this->twigEnvironment->render($subLayout, $params);
return $this->twigEnvironment->render($mainLayout, $params);
}
public function renderWithTiwg(string $content = '', array $params = array())
{
return $this->twigEnvironment->render($content, $params);
}
}

130
src/AntCMS/AntUsers.php Normal file
View file

@ -0,0 +1,130 @@
<?php
namespace AntCMS;
class AntUsers
{
public static function getUser($username)
{
$users = Self::getUsers();
return $users[$username] ?? null;
}
/** This function is used to get all the info of a user that is safe to publicize.
* Mostly intended to create an array that can be safely passed to twig and used to display user information on the page, such as their name.
* @param mixed $username
* @return array
*/
public static function getUserPublicalKeys($username)
{
$user = Self::getUser($username);
if (is_null($user)) {
return [];
}
unset($user['password']);
return $user;
}
public static function getUsers()
{
if (file_exists(antUsersList)) {
return AntYaml::parseFile(antUsersList);
} else {
AntCMS::redirect('/profile/firsttime');
}
}
public static function addUser($data)
{
$data['username'] = trim($data['username']);
$data['name'] = trim($data['name']);
Self::validateUsername($data['username']);
$users = Self::getUsers();
if (key_exists($data['username'], $users)) {
return false;
}
if (!AntTools::valuesNotNull(['username', 'role', 'display-name', 'password'], $data)) {
return false;
}
$users[$data['username']] = [
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'role' => $data['role'],
'name' => $data['display-name'],
];
return AntYaml::saveFile(antUsersList, $users);
}
public static function updateUser($username, $newData)
{
foreach ($newData as $key => $value) {
if (empty($value)) {
throw new \Exception("Key $key cannot be empty.");
}
}
$users = self::getUsers();
if (!key_exists($username, $users)) {
throw new \Exception("There was an error when updating the selected user.");
}
if (isset($newData['password'])) {
$users[$username]['password'] = password_hash($newData['password'], PASSWORD_DEFAULT);
}
if (isset($newData['role'])) {
$users[$username]['role'] = $newData['role'];
}
if (isset($newData['name'])) {
$newData['name'] = trim($newData['name']);
$users[$username]['name'] = $newData['name'];
}
if (isset($newData['username'])) {
$newData['username'] = trim($newData['username']);
Self::validateUsername($newData['username']);
if (key_exists($newData['username'], $users) && $newData['username'] !== $username) {
throw new \Exception("Username is already taken.");
}
$user = $users[$username];
unset($users[$username]);
$users[$newData['username']] = $user;
}
return AntYaml::saveFile(antUsersList, $users);
}
public static function setupFirstUser($data)
{
if (file_exists(antUsersList)) {
AntCMS::redirect('/');
}
$data['username'] = trim($data['username']);
$data['name'] = trim($data['name']);
Self::validateUsername($data['username']);
$users = [
$data['username'] => [
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'role' => 'admin',
'name' => $data['name'],
],
];
return AntYaml::saveFile(antUsersList, $users);
}
private static function validateUsername($username)
{
$pattern = '/^[\p{L}\p{M}*0-9]+$/u';
if (!preg_match($pattern, $username)) {
throw new \Exception("Invalid username: \"$username\". Usernames can only contain letters, numbers, and combining marks.");
}
return true;
}
}

View file

@ -1,8 +1,9 @@
<?php
const AntDir = __DIR__;
const AntCachePath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache';
const antConfigFile = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'config.yaml';
const antPagesList = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'pages.yaml';
const antConfigFile = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Config.yaml';
const antPagesList = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Pages.yaml';
const antUsersList = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Users.yaml';
const antContentPath = __DIR__ . DIRECTORY_SEPARATOR . 'Content';
const antThemePath = __DIR__ . DIRECTORY_SEPARATOR . 'Themes';
const antPluginPath = __DIR__ . DIRECTORY_SEPARATOR . 'Plugins';

View file

@ -19,7 +19,7 @@ Once you've downloaded the latest release, follow these steps to install AntCMS
2. If you are using nginx, you will need to download the nginx config from [here](https://raw.githubusercontent.com/AntCMS-org/AntCMS/main/configs/nginx.conf)
3. Copy the installation files to the `public_html` directory for your domain. Note: while it may be possible to use AntCMS under a sub directory, it's much more likely to have issues.
4. Access AntCMS from the web, by doing so you will cause AntCMS to generate it's initial configuration files. (ex: antcms.example.com)
5. Edit the `Config/config.yaml` file to specify the options specific to your website
5. Edit the `Config/Config.yaml` file to specify the options specific to your website
1. More in-depth descriptions on these options are available on our [readme](https://github.com/AntCMS-org/AntCMS#readme)
2. You should at the very least set the `siteTitle` and the `password`. Note: setting the password is only required for you to access the admin plugin or anywhere else that may require authentication
3. If you would like, you may also change the theme your site is using. We currently offer 'Default' and 'Bootstrap' themes, both of which are fast, pretty, and well optimized for SEO.
@ -46,16 +46,16 @@ When creating your page header, be sure to put a space after the ':', omitting i
Valid: `Title: This is a Title` invalid: `Title:This is a Title`.
When you create a new page, it won't be automatically added to the page navigation on your website. This is because of the way AntCMS generates a list of all pages and then returns that list, rather than re-discovering your pages on each request.
To manually add a new page, you can manually edit the `Config/pages.yaml` file, delete the file which will cause AntCMS to automatically regenerate it, or use the admin plugin to regenerate the list. (covered later in this guide)
To manually add a new page, you can manually edit the `Config/Pages.yaml` file, delete the file which will cause AntCMS to automatically regenerate it, or use the admin plugin to regenerate the list. (covered later in this guide)
Just as how a page is simply created by adding a new file to the `/Content` directory, deleting it is as easy as deleting the file and removing it from the `/Config/pages.yaml` file.
Just as how a page is simply created by adding a new file to the `/Content` directory, deleting it is as easy as deleting the file and removing it from the `/Config/Pages.yaml` file.
Note: In the future, the page management experience will be improved to provide greater flexibility and to be more streamlined.
#### The Admin Plugin
AntCMS has a basic admin plugin to make it easier to write content for your website. While the styling is limited, the plugin does provide features to help creating content a bit easier.
To login to the admin plugin, visit `example.com/plugin/admin`. You will then be prompted to login to login with the credentials you setup in your `Config/config.yaml` file.
To login to the admin plugin, visit `example.com/plugin/admin`. You will then be prompted to login to login with the credentials you setup in your `Config/Config.yaml` file.
The plugin provides a few easy tools, such as a way to edit the configuration file of your AntCMS instance, create a new page, or edit existing content.
The plugin also provides a live preview of the content you are writing. (note: the preview may support all of the markdown features the core app has.)

View file

@ -14,7 +14,7 @@ AntCMS is a lightweight CMS system designed for simplicity, speed, and small siz
### How fast is AntCMS?
AntCMS is designed for speed, with a simple backend and caching capabilities that allow it to quickly render and deliver pages to users in milliseconds. This speed is further enhanced by the use of Tailwind CSS in the default theme, which is only 20KB.
AntCMS is designed for speed, with a simple backend and caching capabilities that allow it to quickly render and deliver pages to users in milliseconds. This speed is further enhanced by the use of Tailwind CSS in the default theme, which is only 25KB.
Our unit tests also ensure that rendering markdown content takes less than 0.015 seconds, as demonstrated by the following recent results: `Markdown rendering speed with cache: 0.000289 VS without: 0.003414`.
@ -31,34 +31,32 @@ Here is an example of the default theme folder structure:
- `/Themes`
- `/Default`
- `/Templates`
- `default_layout.html.twig`
- `nav_layout.html.twig`
- `default.html.twig`
- `nav.html.twig`
- `/Assets`
- `tailwind.css`
To change the active theme, simply edit `config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
To change the active theme, simply edit `Config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
### Configuring AntCMS
AntCMS stores its configuration in the human-readable yaml file format. The main configuration files are config.yaml and pages.yaml. These files will be automatically generated by AntCMS if they do not exist.
AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `Config.yaml`, `Pages.yaml`, and `Users.yaml`. These files will be automatically generated by AntCMS if they do not exist.
#### Options in `config.yaml`
#### Options in `Config/Config.yaml`
- `siteInfo:`
- `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website.
- `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection.
- `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use.
- `enableCache: true` - Enables or disables file caching in AntCMS.
- `admin:`
- `username: 'Admin'` - The username used to access any parts of AntCMS that may require authentication.
- `password: 'dontmakeitpassword123'` - The password associated with the admin account. Can be entered as plain text and then will automatically be hashed with the first login. This does need to be manually entered initially.
- `debug: true`- Enabled or disables debug mode.
- `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed.
#### Options in `pages.yml`
#### Options in `Config/Pages.yaml`
The `pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `pages.yaml` file.
Here's what the `pages.yaml` file looks like:
The `Pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `Pages.yaml` file.
The order of which files are stored inside of the `Pages.yaml` file dictates what order they will be displayed in the browser window.
Here's what the `Pages.yaml` file looks like:
- `pageTitle: 'Hello World'` - This defines what the title of the page is in the navbar.
- `fullPagePath: /antcms.example.com/public_html/Content/index.md` - This defines the full path to your page, as PHP would use to access it.
@ -67,7 +65,8 @@ Here's what the `pages.yaml` file looks like:
#### The Admin Plugin
AntCMS has a very simple admin plugin. Once you set your password in your `config.yaml`, you can access it by visiting `antcms.example.com/plugin/admin`.
AntCMS has a very simple admin plugin that you can access it by visiting `antcms.example.com/admin`.
It will then require you to authenticate using your AntCMS credentials and from there will give you a few simple actions such as editing your config, a page, or regenerating the page list.
The admin plugin also features a live preview of the content you are creating, but it's important to note that the preview doesn't support all of the markdown syntax that AntCMS does, such as emojis.
Note: when editing the config, if you 'save' it and it didn't update, this means you made an error in the config file and AntCMS prevented the file from being saved.

View file

@ -8,9 +8,14 @@ use AntCMS\AntYaml;
use AntCMS\AntAuth;
use AntCMS\AntTools;
use AntCMS\AntTwig;
use AntCMS\AntUsers;
class AdminPlugin extends AntPlugin
{
protected $auth;
protected $antCMS;
protected $AntTwig;
public function getName(): string
{
return 'Admin';
@ -22,11 +27,14 @@ class AdminPlugin extends AntPlugin
*/
public function handlePluginRoute(array $route)
{
AntAuth::checkAuth();
$currentStep = $route[0] ?? 'none';
$antCMS = new AntCMS;
$pageTemplate = $antCMS->getPageLayout();
$this->auth = new AntAuth;
$this->auth->checkAuth();
$this->antCMS = new AntCMS;
$this->AntTwig = new AntTwig();
array_shift($route);
switch ($currentStep) {
@ -36,20 +44,19 @@ class AdminPlugin extends AntPlugin
case 'pages':
$this->managePages($route);
default:
$HTMLTemplate = "<h1>AntCMS Admin Plugin</h1>\n";
$HTMLTemplate .= "<a href='//" . AntConfig::currentConfig('baseURL') . "plugin/admin/config/'>AntCMS Configuration</a><br>\n";
$HTMLTemplate .= "<a href='//" . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/'>Page management</a><br>\n";
case 'users':
$this->userManagement($route);
$params = array(
default:
$params = [
'AntCMSTitle' => 'AntCMS Admin Dashboard',
'AntCMSDescription' => 'The AntCMS admin dashboard',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
'AntCMSBody' => $HTMLTemplate,
);
'user' => AntUsers::getUserPublicalKeys($this->auth->getUsername()),
echo AntTwig::renderWithTiwg($pageTemplate, $params);
];
echo $this->AntTwig->renderWithSubLayout('admin_landing', $params);
break;
}
}
@ -60,27 +67,30 @@ class AdminPlugin extends AntPlugin
*/
private function configureAntCMS(array $route)
{
$antCMS = new AntCMS;
$pageTemplate = $antCMS->getPageLayout();
$HTMLTemplate = $antCMS->getThemeTemplate('textarea_edit_layout');
if ($this->auth->getRole() != 'admin') {
$this->antCMS->renderException("You are not permitted to visit this page.");
}
$currentConfig = AntConfig::currentConfig();
$currentConfigFile = file_get_contents(antConfigFile);
$params = array(
'AntCMSTitle' => 'AntCMS Configuration',
'AntCMSDescription' => 'The AntCMS configuration screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => 'N/A',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'edit':
$HTMLTemplate = str_replace('<!--AntCMS-ActionURL-->', '//' . $currentConfig['baseURL'] . 'plugin/admin/config/save', $HTMLTemplate);
$HTMLTemplate = str_replace('<!--AntCMS-TextAreaContent-->', htmlspecialchars($currentConfigFile), $HTMLTemplate);
$params['AntCMSActionURL'] = '//' . $currentConfig['baseURL'] . 'admin/config/save';
$params['AntCMSTextAreaContent'] = htmlspecialchars($currentConfigFile);
echo $this->AntTwig->renderWithSubLayout('textareaEdit', $params);
break;
case 'save':
if (!$_POST['textarea']) {
header('Location: //' . $currentConfig['baseURL'] . "plugin/admin/config/");
AntCMS::redirect('/admin/config');
}
$yaml = AntYaml::parseYaml($_POST['textarea']);
@ -88,34 +98,26 @@ class AdminPlugin extends AntPlugin
AntYaml::saveFile(antConfigFile, $yaml);
}
header('Location: //' . $currentConfig['baseURL'] . "plugin/admin/config/");
exit;
AntCMS::redirect('/admin/config');
break;
default:
$HTMLTemplate = "<h1>AntCMS Configuration</h1>\n";
$HTMLTemplate .= "<a href='//" . $currentConfig['baseURL'] . "plugin/admin/config/edit'>Click here to edit the config file</a><br>\n";
$HTMLTemplate .= "<ul>\n";
foreach ($currentConfig as $key => $value) {
if (is_array($value)) {
$HTMLTemplate .= "<li>{$key}:</li>\n";
$HTMLTemplate .= "<ul>\n";
foreach ($value as $key => $value) {
$value = is_bool($value) ? $this->boolToWord($value) : $value;
$HTMLTemplate .= "<li>{$key}: {$value}</li>\n";
foreach ($value as $subkey => $subvalue) {
if (is_bool($subvalue)) {
$currentConfig[$key][$subkey] = ($subvalue) ? 'true' : 'false';
}
}
$HTMLTemplate .= "</ul>\n";
} else {
$value = is_bool($value) ? $this->boolToWord($value) : $value;
$HTMLTemplate .= "<li>{$key}: {$value}</li>\n";
} else if (is_bool($value)) {
$currentConfig[$key] = ($value) ? 'true' : 'false';
}
}
$HTMLTemplate .= "</ul>\n";
$params['currentConfig'] = $currentConfig;
echo $this->AntTwig->renderWithSubLayout('admin_config', $params);
break;
}
$params['AntCMSBody'] = $HTMLTemplate;
echo AntTwig::renderWithTiwg($pageTemplate, $params);
exit;
}
@ -125,21 +127,18 @@ class AdminPlugin extends AntPlugin
*/
private function managePages(array $route)
{
$antCMS = new AntCMS;
$pageTemplate = $antCMS->getPageLayout();
$HTMLTemplate = $antCMS->getThemeTemplate('markdown_edit_layout');
$pages = AntPages::getPages();
$params = array(
'AntCMSTitle' => 'AntCMS Page Management',
'AntCMSDescription' => 'The AntCMS page management screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => 'N/A',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'regenerate':
AntPages::generatePages();
header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/");
AntCMS::redirect('/admin/pages');
exit;
case 'edit':
@ -159,11 +158,14 @@ class AdminPlugin extends AntPlugin
}
$pagePath = AntTools::repairFilePath($pagePath);
$page = "--AntCMS--\nTitle: New Page Title\nAuthor: Author\nDescription: Description of this page.\nKeywords: Keywords\n--AntCMS--\n";
$name = $this->auth->getName();
$page = "--AntCMS--\nTitle: New Page Title\nAuthor: $name\nDescription: Description of this page.\nKeywords: Keywords\n--AntCMS--\n";
}
$HTMLTemplate = str_replace('<!--AntCMS-ActionURL-->', '//' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/save/{$pagePath}", $HTMLTemplate);
$HTMLTemplate = str_replace('<!--AntCMS-TextAreaContent-->', htmlspecialchars($page), $HTMLTemplate);
$params['AntCMSActionURL'] = '//' . AntConfig::currentConfig('baseURL') . "admin/pages/save/{$pagePath}";
$params['AntCMSTextAreaContent'] = $page;
echo $this->AntTwig->renderWithSubLayout('markdownEdit', $params);
break;
case 'save':
@ -171,22 +173,16 @@ class AdminPlugin extends AntPlugin
$pagePath = AntTools::repairFilePath(antContentPath . '/' . implode('/', $route));
if (!isset($_POST['textarea'])) {
header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/");
AntCMS::redirect('/admin/pages');
}
file_put_contents($pagePath, $_POST['textarea']);
header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/");
AntCMS::redirect('/admin/pages');
exit;
case 'create':
$HTMLTemplate = "<h1>Page Management</h1>\n";
$HTMLTemplate .= "<p>Create new page</p>\n";
$HTMLTemplate .= '<form method="post" action="' . '//' . AntConfig::currentConfig('baseURL') . 'plugin/admin/pages/edit">';
$HTMLTemplate .=
'<div style="display:flex; flex-direction: row; justify-content: center; align-items: center">
<label for="input">URL for new page: ' . AntConfig::currentConfig('baseURL') . ' </label> <input type="text" name="newpage" id="input">
<input type="submit" value="Submit">
</div></form>';
$params['BaseURL'] = AntConfig::currentConfig('baseURL');
echo $this->AntTwig->renderWithSubLayout('admin_newPage', $params);
break;
case 'delete':
@ -205,7 +201,7 @@ class AdminPlugin extends AntPlugin
AntYaml::saveFile(antPagesList, $pages);
}
header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/");
AntCMS::redirect('/admin/pages');
break;
case 'togglevisibility':
@ -219,23 +215,103 @@ class AdminPlugin extends AntPlugin
}
AntYaml::saveFile(antPagesList, $pages);
header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/");
AntCMS::redirect('/admin/pages');
break;
default:
$HTMLTemplate = $antCMS->getThemeTemplate('admin_manage_pages_layout');
foreach ($pages as $key => $page) {
$pages[$key]['editurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/edit/" . $page['functionalPagePath']);
$pages[$key]['deleteurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/delete/" . $page['functionalPagePath']);
$pages[$key]['togglevisibility'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/togglevisibility/" . $page['functionalPagePath']);
$pages[$key]['editurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/edit/" . $page['functionalPagePath']);
$pages[$key]['deleteurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/delete/" . $page['functionalPagePath']);
$pages[$key]['togglevisibility'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/togglevisibility/" . $page['functionalPagePath']);
$pages[$key]['isvisable'] = $this->boolToWord($page['showInNav']);
}
$params['pages'] = $pages;
$HTMLTemplate = AntTwig::renderWithTiwg($HTMLTemplate, $params);
$params = [
'AntCMSTitle' => 'AntCMS Admin Dashboard',
'AntCMSDescription' => 'The AntCMS admin dashboard',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
'pages' => $pages,
];
echo $this->AntTwig->renderWithSubLayout('admin_managePages', $params);
break;
}
exit;
}
private function userManagement(array $route)
{
if ($this->auth->getRole() != 'admin') {
$this->antCMS->renderException("You are not permitted to visit this page.");
}
$params['AntCMSBody'] = $HTMLTemplate;
echo AntTwig::renderWithTiwg($pageTemplate, $params);
$params = array(
'AntCMSTitle' => 'AntCMS User Management',
'AntCMSDescription' => 'The AntCMS user management screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'add':
echo $this->AntTwig->renderWithSubLayout('admin_userAdd', $params);
break;
case 'edit':
$user = AntUsers::getUserPublicalKeys($route[1]);
if (!$user) {
AntCMS::redirect('/admin/users');
}
$user['username'] = $route[1];
$params['user'] = $user;
echo $this->AntTwig->renderWithSubLayout('admin_userEdit', $params);
break;
case 'resetpassword':
$user = AntUsers::getUserPublicalKeys($route[1]);
if (!$user) {
AntCMS::redirect('/admin/users');
}
$user['username'] = $route[1];
$params['user'] = $user;
echo $this->AntTwig->renderWithSubLayout('admin_userResetPassword', $params);
break;
case 'save':
$data['username'] = $_POST['username'] ?? null;
$data['name'] = $_POST['display-name'] ?? null;
$data['role'] = $_POST['role'] ?? null;
$data['password'] = $_POST['password'] ?? null;
foreach ($data as $key => $value) {
if (is_null($value)) {
unset($data[$key]);
}
}
AntUsers::updateUser($_POST['originalusername'], $data);
AntCMS::redirect('/admin/users');
break;
case 'savenew':
AntUsers::addUser($_POST);
AntCMS::redirect('/admin/users');
break;
default:
$users = AntUsers::getUsers();
foreach ($users as $key => $user) {
unset($users[$key]['password']);
$users[$key]['username'] = $key;
}
$params['users'] = $users;
echo $this->AntTwig->renderWithSubLayout('admin_users', $params);
break;
}
exit;
}

View file

@ -0,0 +1,119 @@
<?php
use AntCMS\AntPlugin;
use AntCMS\AntAuth;
use AntCMS\AntCMS;
use AntCMS\AntTwig;
use AntCMS\AntUsers;
class ProfilePlugin extends AntPlugin
{
protected $antAuth;
protected $antTwig;
public function handlePluginRoute(array $route)
{
$this->antAuth = new AntAuth;
$this->antTwig = new AntTwig;
$currentStep = $route[0] ?? 'none';
$params = [
'AntCMSTitle' => 'AntCMS Profile Management',
'AntCMSDescription' => 'AntCMS Profile Management',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
];
switch ($currentStep) {
case 'firsttime':
if (file_exists(antUsersList)) {
AntCMS::redirect('/admin');
}
echo $this->antTwig->renderWithSubLayout('profile_firstTime', $params);
break;
case 'submitfirst':
if (file_exists(antUsersList)) {
AntCMS::redirect('/admin');
}
if (isset($_POST['username']) && isset($_POST['password']) && isset($_POST['display-name'])) {
$data = [
'username' => $_POST['username'],
'password' => $_POST['password'],
'name' => $_POST['display-name'],
];
AntUsers::setupFirstUser($data);
AntCMS::redirect('/admin');
} else {
AntCMS::redirect('/profile/firsttime');
}
break;
case 'edit':
$this->antAuth->checkAuth();
$user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
if (!$user) {
AntCMS::redirect('/profile');
}
$user['username'] = $this->antAuth->getUsername();
$params['user'] = $user;
echo $this->antTwig->renderWithSubLayout('profile_edit', $params);
break;
case 'resetpassword':
$this->antAuth->checkAuth();
$user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
if (!$user) {
AntCMS::redirect('/profile');
}
$user['username'] = $this->antAuth->getUsername();
$params['user'] = $user;
echo $this->antTwig->renderWithSubLayout('profile_resetPassword', $params);
break;
case 'save':
$this->antAuth->checkAuth();
$data['username'] = $_POST['username'] ?? null;
$data['name'] = $_POST['display-name'] ?? null;
$data['password'] = $_POST['password'] ?? null;
foreach ($data as $key => $value) {
if (is_null($value)) {
unset($data[$key]);
}
}
AntUsers::updateUser($this->antAuth->getUsername(), $data);
AntCMS::redirect('/profile');
break;
case 'logout':
$this->antAuth->invalidateSession();
if (!$this->antAuth->isAuthenticated()) {
echo "You have been logged out.";
} else {
echo "There was an error logging you out.";
}
exit;
default:
$this->antAuth->checkAuth();
$params['user'] = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
echo $this->antTwig->renderWithSubLayout('profile_landing', $params);
}
exit;
}
public function getName(): string
{
return 'Profile';
}
}

View file

@ -0,0 +1,9 @@
<h1>Page Management</h1>
<p>Create new page</p>
<form method="post" action="{{ "admin/pages/edit"|absUrl }}">
<div>
<label for="input">URL for new page: {{ BaseURL }} </label>
<input type="text" name="newpage" id="input">
<input type="submit" value="Submit" class="btn btn-primary">
</div>
</form>

View file

@ -0,0 +1,28 @@
<form method="POST" action="{{ " admin/users/savenew"|absUrl }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-3">
<label for="role" class="form-label">Role:</label>
<select name="role" id="role" multiple class="form-select">
<option value="admin">Admin</option>
<option value="writer">Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</form>

View file

@ -0,0 +1,25 @@
<form method="POST" action="{{ " admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control" value="{{ user.name }}">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control" value="{{ user.username }}">
</div>
<div class="mb-3">
<label for="role" class="form-label">Role:</label>
<select name="role" id="role" class="form-select">
<option value="admin" {% if user.role=='admin' %} selected {% endif %}>Admin</option>
<option value="writer" {% if user.role=='writer' %} selected {% endif %}>Writer</option>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,12 @@
<form method="POST" action="{{ " admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-3">
<label for="password" class="form-label">New Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -38,7 +38,12 @@
<div class="container">
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-10">{{ AntCMSBody | raw }}</div>
<div class="col-md-10">
{% if DisplayAuthor and false %}
<p>This content was written by {{ AntCMSAuthor }}</p>
{% endif %}
{{ AntCMSBody | raw }}
</div>
<div class="col-md-1"></div>
</div>
</div>

View file

@ -2,9 +2,9 @@
<div class="row">
<div class="col-md-6">
<h3>Page Content</h3>
<form action="<!--AntCMS-ActionURL-->" method="post">
<form action="{{ AntCMSActionURL }}" method="post">
<textarea id="markdown-input" name="textarea" rows="100" style="width: 100%; height: 100%;"
class="form-control"><!--AntCMS-TextAreaContent--></textarea>
class="form-control">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save">
</form>
</div>
@ -18,7 +18,7 @@
const output = document.getElementById("markdown-output");
function parseMarkdown() {
const inputValue = input.value.replace(/\A--AntCMS--.*?--AntCMS--/sm, "");
const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, "");
output.innerHTML = marked.parse(inputValue);
}

View file

@ -0,0 +1,15 @@
<form method="POST" action="{{ " profile/save"|absUrl }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control" value="{{ user.name }}">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control" value="{{ user.username }}">
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,10 @@
<form method="POST" action="{{ " profile/save"|absUrl }}">
<div class="mb-3">
<label for="password" class="form-label">New Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,5 @@
<form action="{{ AntCMSActionURL }}" method="post">
<textarea name="textarea" rows="25" cols="75" type="text" <textarea id="markdown-input" name="textarea" rows="100"
style="width: 100%; height: 100%;" class="form-control">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save">
</form>

View file

@ -1,5 +0,0 @@
<form action="<!--AntCMS-ActionURL-->" method="post">
<textarea name="textarea" rows="25" cols="75" type="text" <textarea id="markdown-input" name="textarea" rows="100"
style="width: 100%; height: 100%;" class="form-control"><!--AntCMS-TextAreaContent--></textarea>
<input type="submit" value="Save">
</form>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
<h1>AntCMS Configuration</h1>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/config/edit"|absUrl }}">Edit Config</a>
<ul>
{% for key, value in currentConfig %}
{% if value is iterable %}
<li>{{ key }}:</li>
<ul>
{% for subkey, subvalue in value %}
<li>{{ subkey }}: {{ subvalue }}</li>
{% endfor %}
</ul>
{% else %}
<li>{{ key }}: {{ value }}</li>
{% endif %}
{% endfor %}
</ul>

View file

@ -0,0 +1,9 @@
<h1>AntCMS Admin Plugin</h1>
<p>Welcome, {{ user.name }}. Below are all of the available options for you.</p>
{% if user.role == 'admin' %}
<a href="{{ "admin/config/"|absUrl }}">AntCMS Configuration</a> |
<a href="{{ "admin/users/"|absUrl }}">AntCMS User Management</a> |
{% endif %}
<a href="{{ "admin/pages/"|absUrl }}">Page Management</a> |
<a href="{{ "profile/"|absUrl }}">Profile</a> |
<a href="{{ "profile/logout"|absUrl }}">Logout</a>

View file

@ -1,7 +1,7 @@
<h1>Page Management</h1>
<a href="{{ "plugin/admin/"|absUrl }}">Back</a> |
<a href="{{ "plugin/admin/pages/regenerate/"|absUrl }}">Regenerate Page List</a> |
<a href="{{ "plugin/admin/pages/create/"|absUrl }}">Create New Page</a>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/pages/regenerate/"|absUrl }}">Regenerate Page List</a> |
<a href="{{ "admin/pages/create/"|absUrl }}">Create New Page</a>
<table>
<tr>
<th>Page Title</th>

View file

@ -0,0 +1,9 @@
<h1>Page Management</h1>
<p>Create new page</p>
<form method="post" action="{{ "admin/pages/edit"|absUrl }}">
<div style="display:flex; flex-direction: row; justify-content: center; align-items: center">
<label for="input">URL for new page: {{ BaseURL }} </label>
<input type="text" name="newpage" id="input">
<input type="submit" value="Submit" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,28 @@
<form method="POST" action="{{ "admin/users/savenew"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="password" class="block font-medium mb-2">Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="role" class="block font-medium mb-2">Role:</label>
<select name="role" id="role" multiple class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
<option value="admin">Admin</option>
<option value="writer">Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Create User" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,25 @@
<form method="POST" action="{{ "admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.name }}">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.username }}">
</div>
<div class="mb-4">
<label for="role" class="block font-medium mb-2">Role:</label>
<select name="role" id="role" multiple class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
<option value="admin" {% if user.role == 'admin' %} selected {% endif %}>Admin</option>
<option value="writer" {% if user.role == 'writer' %} selected {% endif %}>Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,12 @@
<form method="POST" action="{{ "admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-4">
<label for="password" class="block font-medium mb-2">New Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,19 @@
<h1>AntCMS User Management</h1>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/users/add/"|absUrl }}">Add User</a>
<table>
<tr>
<th>Display Name</th>
<th>Username</th>
<th>Role</th>
<th>Actions</th>
</tr>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.username }}</td>
<td>{{ user.role }}</td>
<td><a href="{{ ("admin/users/edit/" ~ user.username) |absUrl }}">Edit</a> | <a href="{{ ("admin/users/resetpassword/" ~ user.username) |absUrl }}">Reset Password</a></td>
</tr>
{% endfor %}
</table>

View file

@ -2,10 +2,10 @@
<div class="flex">
<div class="w-1/2">
<h3 class="text-2xl font-bold">Page Content</h3>
<form action="<!--AntCMS-ActionURL-->" method="post">
<form action="{{ AntCMSActionURL }}" method="post">
<textarea id="markdown-input" name="textarea" rows="100" style="width: 100%; height: 100%;"
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700"><!--AntCMS-TextAreaContent--></textarea>
<input type="submit" value="Save">
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</form>
</div>
@ -18,7 +18,7 @@
const output = document.getElementById("markdown-output");
function parseMarkdown() {
const inputValue = input.value.replace(/\A--AntCMS--.*?--AntCMS--/sm, "");
const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, "");
output.innerHTML = marked.parse(inputValue);
}

View file

@ -0,0 +1,15 @@
<form method="POST" action="{{ "profile/save"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.name }}">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.username }}">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,23 @@
<h2 class="text-2xl font-bold mb-4">AntCMS First Time User Setup</h2>
<p class="mb-4">Please fill out the following form to create your new administrator account. This will allow you to access portions of AntCMS that require authentication, such as the <a href="{{ "admin/"|absUrl }}" class="text-blue-500 hover:text-blue-400 dark:text-blue-400 dark:hover:text-blue-500">Admin Panel</a>.</p>
<form method="POST" action="{{ "profile/submitfirst"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="password" class="block font-medium mb-2">Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Create User" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,5 @@
<h1>AntCMS Profile Plugin</h1>
<p>Welcome, {{ user.name }}. Below are all of the available options for you.</p>
<a href="{{ "profile/edit"|absUrl }}">Edit Profile</a> |
<a href="{{ "profile/resetpassword"|absUrl }}">Reset Password</a> |
<a href="{{ "profile/logout"|absUrl }}">Logout</a>

View file

@ -0,0 +1,10 @@
<form method="POST" action="{{ "profile/save"|absUrl }}">
<div class="mb-4">
<label for="password" class="block font-medium mb-2">New Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,7 @@
<form action="{{ AntCMSActionURL }}" method="post">
<textarea name="textarea" rows="25" cols="75" type="text"
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">{{ AntCMSTextAreaContent }}</textarea>
<div class="flex justify-end">
<button type="submit" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">Save</button>
</div>
</form>

View file

@ -1,5 +0,0 @@
<form action="<!--AntCMS-ActionURL-->" method="post">
<textarea name="textarea" rows="25" cols="75" type="text"
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700"><!--AntCMS-TextAreaContent--></textarea>
<input type="submit" value="Save">
</form>

View file

@ -12,8 +12,6 @@ use AntCMS\AntConfig;
use AntCMS\AntPages;
use AntCMS\AntPluginLoader;
$antCms = new AntCMS();
if (!file_exists(antConfigFile)) {
AntConfig::generateConfig();
}
@ -22,6 +20,8 @@ if (!file_exists(antPagesList)) {
AntPages::generatePages();
}
$antCms = new AntCMS();
if (AntConfig::currentConfig('forceHTTPS') && 'cli' !== PHP_SAPI) {
$isHTTPS = false;
@ -44,7 +44,7 @@ if (AntConfig::currentConfig('forceHTTPS') && 'cli' !== PHP_SAPI) {
}
}
$requestedPage = strtolower(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
$requestedPage = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', $requestedPage);
if ($segments[0] === '') {
@ -66,6 +66,14 @@ if ($segments[0] == 'robots.txt') {
$segments[1] = 'robotstxt';
}
if ($segments[0] == 'admin') {
array_unshift($segments, 'plugin');
}
if ($segments[0] == 'profile') {
array_unshift($segments, 'plugin');
}
if ($segments[0] === 'plugin') {
$pluginName = $segments[1];
$pluginLoader = new AntPluginLoader();
@ -88,7 +96,13 @@ if ($segments[0] === 'plugin') {
}
$indexes = ['/', '/index.php', '/index.html'];
if (in_array($segments[0], $indexes)) {
if (in_array($segments[0], $indexes) or empty($segments[0])) {
// If the users list hasn't been created, redirect to the first-time setup
if (!file_exists(antUsersList)) {
AntCMS::redirect('/profile/firsttime');
}
echo $antCms->renderPage('/');
exit;
} else {

View file

@ -31,7 +31,7 @@ class CMSTest extends TestCase
public function testGetPageLayout()
{
//We need to generate the pages.yaml file so that the nav list can be generated.
//We need to generate the Pages.yaml file so that the nav list can be generated.
AntPages::generatePages();
$antCMS = new AntCMS;

View file

@ -16,7 +16,6 @@ class ConfigTest extends TestCase
'forceHTTPS',
'activeTheme',
'enableCache',
'admin',
'debug',
'baseURL'
);

View file

@ -3,8 +3,5 @@ siteInfo:
forceHTTPS: true
activeTheme: Default
enableCache: true
admin:
username: Admin
password: $2y$10$omNAQ5luPsFYOny0HA3i.O7Ce3XPult8Qn6uamT0b1cFWz.5WkGYO
debug: true
baseURL: antcms.org/

View file

@ -20,7 +20,7 @@ class PagesTest extends TestCase
public function testGetNavigation(){
$antCMS = new AntCMS;
$pageTemplate = $antCMS->getThemeTemplate();
$navLayout = $antCMS->getThemeTemplate('nav_layout');
$navLayout = $antCMS->getThemeTemplate('nav');
$result = AntPages::generateNavigation($navLayout, $pageTemplate);