Browse Source

Initial commit

Couldnt keep the history unfortunately.
master
Riyyi 4 years ago
commit
a8056b66cc
  1. 31
      .gitignore
  2. 29
      app/classes/Config.php
  3. 106
      app/classes/Db.php
  4. 188
      app/classes/Form.php
  5. 72
      app/classes/Mail.php
  6. 282
      app/classes/Media.php
  7. 352
      app/classes/Router.php
  8. 300
      app/classes/Session.php
  9. 174
      app/classes/User.php
  10. 24
      app/controllers/AdminController.php
  11. 133
      app/controllers/BaseController.php
  12. 249
      app/controllers/CrudController.php
  13. 98
      app/controllers/IndexController.php
  14. 232
      app/controllers/LoginController.php
  15. 117
      app/controllers/MediaController.php
  16. 186
      app/controllers/PageController.php
  17. 25
      app/controllers/TestController.php
  18. 90
      app/helper.php
  19. 50
      app/model/ContentModel.php
  20. 7
      app/model/LogModel.php
  21. 14
      app/model/MediaModel.php
  22. 503
      app/model/Model.php
  23. 68
      app/model/PageHasContentModel.php
  24. 76
      app/model/PageModel.php
  25. 68
      app/model/SectionHasContentModel.php
  26. 24
      app/model/SectionModel.php
  27. 7
      app/model/UserModel.php
  28. 178
      app/seed.php
  29. 68
      app/traits/Log.php
  30. 62
      app/views/admin/crud/create.php
  31. 66
      app/views/admin/crud/edit.php
  32. 71
      app/views/admin/crud/index.php
  33. 40
      app/views/admin/crud/show.php
  34. 13
      app/views/admin/index.php
  35. 90
      app/views/admin/media.php
  36. 33
      app/views/admin/syntax-highlighting.php
  37. 44
      app/views/content.php
  38. 8
      app/views/errors/404.php
  39. 96
      app/views/form.php
  40. 49
      app/views/layouts/default.php
  41. 27
      app/views/login.php
  42. 31
      app/views/partials/admin.php
  43. 11
      app/views/partials/footer.php
  44. 54
      app/views/partials/header.php
  45. 8
      app/views/partials/message.php
  46. 25
      app/views/partials/pagination.php
  47. 28
      app/views/partials/script.php
  48. 14
      app/views/reset-password.php
  49. 20
      composer.json
  50. 17
      config.php.example
  51. BIN
      doc/db.mwb
  52. 9
      public/.htaccess
  53. 117
      public/css/style.css
  54. BIN
      public/fonts/captcha.otf
  55. BIN
      public/fonts/fontawesome-webfont.eot
  56. 2671
      public/fonts/fontawesome-webfont.svg
  57. BIN
      public/fonts/fontawesome-webfont.ttf
  58. BIN
      public/fonts/fontawesome-webfont.woff
  59. BIN
      public/fonts/fontawesome-webfont.woff2
  60. BIN
      public/img/favicon.png
  61. 10
      public/index.php
  62. 226
      public/js/app.js
  63. 83
      public/js/site.js
  64. 0
      public/media/.gitinclude
  65. 26
      route.php
  66. 52
      sync.sh
  67. 7
      syncconfig.sh.example

31
.gitignore vendored

@ -0,0 +1,31 @@
# Non-project
.idea/
.directory
# Files
*.mwb.bak
config.php
composer.lock
syncconfig.sh
# Directories
/public/*
!/public/css/
/public/css/*
!/public/fonts/
!/public/img/
/public/img/*
!/public/js/
/public/js/*
!/public/media/
/public/media/*
/vendor/*
# Unignore
!/public/.htaccess
!/public/css/style.css
!/public/img/favicon.png
!/public/index.php
!/public/js/app.js
!/public/js/site.js
!/public/media/.gitinclude

29
app/classes/Config.php

@ -0,0 +1,29 @@
<?php
namespace App\Classes;
class Config {
protected static $config;
//-------------------------------------//
public static function load(): void
{
if (file_exists('../config.php')) {
self::$config = require_once '../config.php';
}
}
//-------------------------------------//
public static function c(string $index = ''): string
{
if (_exists(self::$config, $index)) {
return self::$config[$index];
}
return '';
}
}

106
app/classes/Db.php

@ -0,0 +1,106 @@
<?php
namespace App\Classes;
class Db {
protected static $db;
protected static $columns = [];
protected static $sections = [];
protected static $pages = [];
//-------------------------------------//
public static function load(): void {
try {
$host = Config::c('DB_HOST');
$name = Config::c('DB_NAME');
self::$db = new \PDO(
"mysql:host=$host;dbname=$name;charset=utf8mb4",
Config::c('DB_USERNAME'),
Config::c('DB_PASSWORD')
);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
}
//-------------------------------------//
/**
* Get the PDO connection object
*
* @return PDO
*/
public static function get(): \PDO {
return self::$db;
}
/**
* Get all columns
*
* @return array
*/
public static function getColumns(): array
{
return self::$columns;
}
/**
* Get all sections
*
* @return array
*/
public static function getSections(): array
{
return self::$sections;
}
/**
* Get all pages
*
* @return array
*/
public static function getPages(): array
{
return self::$pages;
}
/**
* Store columns
*
* @param array $columns
*
* @return void
*/
public static function setColumns(array $columns): void
{
self::$columns = $columns;
}
/**
* Store sections
*
* @param array $sections
*
* @return void
*/
public static function setSections(array $sections): void
{
self::$sections = $sections;
}
/**
* Store pages
*
* @param array $pages
*
* @return void
*/
public static function setPages(array $pages): void
{
self::$pages = $pages;
}
}

188
app/classes/Form.php

@ -0,0 +1,188 @@
<?php
namespace App\Classes;
use \Klein\Klein;
class Form {
private $router;
private $data = [];
private $resetLabel = '';
private $submitLabel = 'Submit';
private $errorKey = '';
public function __construct(Klein $router)
{
$this->router = $router;
$this->router->service()->csrfToken = Session::token();
$this->router->service()->form = $this;
$this->router->service()->injectView = '../app/views/form.php';
}
//-------------------------------------//
public function addField(string $name, array $field): void
{
// "name" => [
// "label",
// "type", text, email, tel, password, radio, textarea, checkbox(?), comment
// "data", (for radio fields) [ 'value' => 'Label', 'value' => 'Label']
// "rules", (server side rules)
// "message", (server side error message)
// "pattern", (client sided rule)
// "title",
// ],
$this->data[$name] = [
$field[0] ?? '',
$field[1] ?? '',
$field[2] ?? '',
$field[3] ?? '',
$field[4] ?? '',
$field[5] ?? '',
$field[6] ?? '',
];
}
public function validated(array $submit = []): bool
{
$result = false;
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$result = true;
if (empty($submit)) {
$submit = $_POST;
}
if (!Session::validateToken($submit)) {
$result = false;
}
// Only check fields if CSRF token is valid
if ($result) {
// Check field rules
foreach ($this->data as $ruleName => $ruleValue) {
$found = false;
$value = '';
foreach ($submit as $submitName => $submitValue) {
if ($ruleName == $submitName) {
$found = true;
$value = $submitValue;
break;
}
if ($ruleValue[1] == 'comment') {
$found = true;
break;
}
}
if (!$found || !$this->matchRule($ruleName, $value)) {
$this->errorKey = $ruleName;
$result = false;
break;
}
}
}
// If unsuccessful, remember the form fields
if (!$result) {
foreach (array_keys($this->data) as $name) {
if ($name == 'captcha') {
continue;
}
$this->router->service()->{$name} = $submit[$name] ?? '';
}
}
}
Session::delete('captcha');
return $result;
}
public function matchRule(string $key, string $value): bool
{
// Get the rule(s)
$rule = $this->data[$key];
$rules = explode('|', $rule[3]);
if (array_search('required', $rules) !== false) {
if (empty($value)) {
return false;
}
}
if (array_search('email', $rules) !== false) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return false;
}
}
if (array_search('tel', $rules) !== false) {
if (!filter_var($value, FILTER_VALIDATE_REGEXP, [
'options' => [
'regexp' => '/^([0-9]{2}-?[0-9]{8})|([0-9]{3}-?[0-9]{7})$/',
]
])) {
return false;
}
}
if (array_search('captcha', $rules) !== false) {
if (!Session::exists('captcha')) {
return false;
}
else if ($value != Session::get('captcha')) {
return false;
}
}
return true;
}
public function errorMessage(): string
{
return $this->errorKey != '' ? $this->data[$this->errorKey][4] : '';
}
//-------------------------------------//
public function getFields(): array
{
return $this->data;
}
public function getReset(): string
{
return $this->resetLabel;
}
public function getSubmit(): string
{
return $this->submitLabel;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function setReset(string $resetLabel): void
{
$this->resetLabel = $resetLabel;
}
public function setSubmit(string $submitLabel): void
{
$this->submitLabel = $submitLabel;
}
}

72
app/classes/Mail.php

@ -0,0 +1,72 @@
<?php
namespace App\Classes;
use Tx\Mailer;
class Mail {
protected static $host;
protected static $port;
protected static $name;
protected static $username;
protected static $password;
protected static $to;
public static function _init(): void {
self::$host = Config::c('MAIL_HOST');
self::$port = Config::c('MAIL_PORT');
self::$name = Config::c('MAIL_NAME');
self::$username = Config::c('MAIL_USERNAME');
self::$password = Config::c('MAIL_PASSWORD');
self::$to = Config::c('MAIL_TO');
}
public static function send(
string $subject, string $message, string $to = '', string $from = ''): bool
{
if ($to == '') {
$to = self::$to;
}
if ($from == '') {
$from = 'Website <'. self::$username . '>';
}
$headers =
'MIME-Version: 1.0' . "\r\n" .
'Content-type: text/html; charset=utf-8' . "\r\n" .
'From: ' . $from . "\r\n" .
'Reply-To: ' . $from . "\r\n" .
'X-Mailer: PHP/' . phpversion();
return mail($to, $subject, $message, $headers);
}
public static function sendMail(string $subject, string $message, string $from = '', string $to = ''): bool
{
if ($to == '') {
$to = self::$to;
}
if ($from == '') {
$from = self::$name;
}
if (empty(self::$host) || empty(self::$port) ||
empty(self::$username) || empty(self::$password) || empty($to)) {
return false;
}
$result = (new Mailer())
->setServer(self::$host, self::$port, "tlsv1.2")
->setAuth(self::$username, self::$password)
->setFrom($from, self::$username)
->addTo('', $to)
->setSubject($subject)
->setBody($message)
->send();
return $result;
}
}
Mail::_init();

282
app/classes/Media.php

@ -0,0 +1,282 @@
<?php
namespace App\Classes;
use App\Model\MediaModel;
class Media {
public static $pagination = 20;
public static $directory = 'media';
public static function errorMessage(int $errorCode): string
{
// https://www.php.net/manual/en/features.file-upload.errors.php
$errorMessages = [
0 => 'Uploaded with success.',
1 => 'Uploaded file exceeds the maximum upload size. (1)', // php.ini
2 => 'Uploaded file exceeds the maximum upload size. (2)', // HTML Form
3 => 'Uploaded file was only partially uploaded.',
4 => 'No file was uploaded.',
6 => 'Missing a temporary folder.',
7 => 'Failed to write file to disk.',
8 => 'A PHP extension stopped the file upload.',
9 => 'User was not logged in.',
10 => 'Missing media folder.',
11 => 'Uploaded file has invalid MIME type.',
12 => 'Uploaded file exceeds the maximum upload size. (3)', // Media.php
13 => 'Uploaded file already exists.',
14 => 'DB entry creation failed.',
15 => 'Moving file from temporary location failed.',
];
return $errorMessages[$errorCode];
}
public static function deleteMedia(int $id): bool
{
if (!User::check()) {
return false;
}
$media = MediaModel::find($id);
if (!$media->exists()) {
return false;
}
// Delete file
$file = self::$directory . '/' . $media->filename . '.' . $media->extension;
if (file_exists($file)) {
unlink($file);
}
return $media->delete();
}
public static function uploadMedia(bool $overwrite = false): int
{
// Check if User is logged in
if (!User::check()) {
return 9;
}
// Check if "media" directory exists
if (!is_dir(self::$directory) && !mkdir(self::$directory, 0755)) {
return 10;
}
$files = $_FILES['file'];
// Check for file errors
foreach ($files['error'] as $error) {
if ($error != 0) {
return $error;
}
}
if (!Media::checkMimeType($files['type'], $files['tmp_name'])) {
return 11;
}
if (!Media::checkSize($files['size'])) {
return 12;
}
// Append random string to filename that already exists
$nameExt = Media::duplicateName($files['name'], $overwrite);
// Check if the file already exists
$hash = Media::hashExists($files['tmp_name']);
if (!$hash[0]) {
return 13;
}
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$filename = $nameExt[0][$i];
$extension = $nameExt[1][$i];
$md5 = $hash[1][$i];
$tmpName = $files['tmp_name'][$i];
// Store record
$media = MediaModel::create([
'filename' => $filename,
'extension' => $extension,
'md5' => $md5,
]);
if (!$media->exists()) {
return 14;
}
// Store image
$name = self::$directory . '/'. $filename . '.' . $extension;
if (!move_uploaded_file($tmpName, $name)) {
return 15;
}
// After storing successfully, remove old entries with duplicate names
if ($overwrite) {
Media::destroyDuplicates($filename, $extension);
}
}
return 0;
}
//-------------------------------------//
private static function checkMimeType(array $fileTypes, array $fileTmpNames): bool
{
$allowedMimeType = [
// .tar.gz
'application/gzip',
'application/json',
'application/octet-stream',
'application/pdf',
'application/sql',
// .tar.xz
'application/x-xz',
'application/xml',
'application/zip',
'audio/mp3',
'audio/mpeg',
'audio/ogg',
'audio/wav',
'audio/webm',
'image/jpg',
'image/jpeg',
'image/png',
'image/gif',
'text/plain',
'text/csv',
'text/xml',
'video/mp4',
'video/webm',
// .doc
'application/msword',
// .xls
'application/vnd.ms-excel',
// .ppt
'application/vnd.ms-powerpoint',
// .docx
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
// .xlsx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
// .pptx
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
];
// Files type check
$count = count($fileTypes);
for ($i = 0; $i < $count; $i++) {
if ($fileTypes[$i] != mime_content_type($fileTmpNames[$i]) ||
!in_array($fileTypes[$i], $allowedMimeType)) {
return false;
}
}
return true;
}
private static function checkSize(array $fileSizes): bool
{
// Files should not exceed 10MiB
foreach ($fileSizes as $fileSize) {
if ($fileSize > 10485760) {
return false;
}
}
return true;
}
private static function duplicateName(array $filenames, bool $overwrite): array
{
// Split name from extension
$names = [];
$extensions = [];
foreach ($filenames as $name) {
$dotPos = strrpos($name, '.');
$names[] = substr($name, 0, $dotPos);
$extensions[] = substr($name, $dotPos + 1);
}
// Early return if names are specified to be overwritten
if ($overwrite) {
return [$names, $extensions];
}
// Get duplicate filenames
$in1 = str_repeat('?, ', count($names) - 1) . '?';
$in2 = str_repeat('?, ', count($extensions) - 1) . '?';
$data = array_merge($names, $extensions);
$duplicates = MediaModel::selectAll(
'*', "WHERE filename IN ($in1) AND extension IN ($in2)", $data, '?'
);
foreach ($filenames as $key => $filename) {
$hasDuplicate = false;
foreach ($duplicates as $duplicate) {
if ($filename == $duplicate['filename'] . '.' . $duplicate['extension']) {
$hasDuplicate = true;
break;
}
}
// Append to filename if there are duplicates
if ($hasDuplicate) {
$names[$key] = $names[$key] . '-' . _randomStr(5);
}
}
return [$names, $extensions];
}
private static function hashExists(array $fileTmpNames): array
{
$md5 = [];
foreach ($fileTmpNames as $tmpName) {
$md5[] = md5_file($tmpName);
}
// If exact file already exists
$in = str_repeat('?, ', count($md5) - 1) . '?';
$exists = MediaModel::selectAll(
'*', "WHERE md5 IN ($in)", $md5, '?'
);
if (!empty($exists)) {
return [false];
}
return [true, $md5];
}
private static function destroyDuplicates(string $filename, string $extension): void
{
$media = MediaModel::selectAll(
'*', 'WHERE filename = ? AND extension = ? ORDER BY id ASC',
[$filename, $extension], '?'
);
if (!_exists($media)) {
return;
}
foreach ($media as $key => $value) {
// Dont delete the new entry
if ($key === array_key_last($media)) {
return;
}
MediaModel::destroy($value['id']);
}
}
}
// @Todo
// - If a file fails to store in the loop, destruct all files of that request, by tracking all IDs

352
app/classes/Router.php

@ -0,0 +1,352 @@
<?php
namespace App\Classes;
use Klein\Klein;
use App\Classes\Db;
use App\Model\SectionModel;
use App\Model\PageModel;
class Router {
protected static $router;
protected static $routes = [];
public static function _init(): void {
self::$router = new Klein();
}
//-------------------------------------//
/**
* Load all routes into the Klein object
*
* @return void
*/
public static function fire(): void
{
$path = parse_url($_SERVER['REQUEST_URI'])['path'];
$check = str_replace('.', '', $path);
// If it's a dynamic file or the file doesn't exist, go through the router.
if ($path == $check || !file_exists(getcwd() . $path)) {
Db::load();
self::setDefaultLayout();
self::loadConfigRoutes();
self::loadDbRoutes();
// Process basic routes
foreach (self::$routes as $route) {
// If route does not match the base url
if ($route[0] != $path) {
continue;
}
// ["/example/my-page", "ExampleController", "action", "" : ["view", "title", "description"]],
self::addBasicRoute(['GET', 'POST'], $route);
break;
}
self::createNavigation();
self::setHttpError();
self::$router->dispatch();
}
}
/**
* Add CRUD routes
* Example usage: Router::resource('/example', 'CrudController');
*
* @param string $route The URL location
* @param string $controller Controller to handle this route
*
* @return void
*/
public static function resource(string $route, string $controller): void
{
/*
* HTTP Verb Part (URL) Action (Method)
*
* GET /route indexAction
* GET /route/create createAction
* POST /route storeAction
* GET /route/{id} showAction
* GET /route/{id}/edit editAction
* PUT/PATCH /route/{id} updateAction
* DELETE /route/{id} destroyAction
*/
self::addRoute(['GET'], [$route, $controller, 'indexAction']);
self::addRoute(['GET'], [$route . '/create', $controller, 'createAction']);
self::addRoute(['POST'], [$route, $controller, 'storeAction']);
self::addRoute(['GET'], [$route . '/[i:id]', $controller, 'showAction', ['id']]);
self::addRoute(['GET'], [$route . '/[i:id]/edit', $controller, 'editAction', ['id']]);
self::addRoute(['PUT', 'PATCH'], [$route . '/[i:id]', $controller, 'updateAction', ['id']]);
self::addRoute(['DELETE'], [$route . '/[i:id]', $controller, 'destroyAction', ['id']]);
}
//-------------------------------------//
protected static function setDefaultLayout(): void
{
self::$router->respond(function ($request, $response, $service) {
$service->layout('../app/views/layouts/default.php');
});
}
protected static function loadConfigRoutes(): void
{
if (file_exists('../route.php')) {
self::$routes = require_once '../route.php';
}
}
/**
* Add all pages in the Db to self::$routes
*
* @return void
*/
protected static function loadDbRoutes(): void
{
// Load all sections from Db
$sections = SectionModel::selectAll('*', 'WHERE `active` = 1 ORDER BY `order` ASC');
// Return if no sections
if (!_exists($sections)) {
return;
}
// Load all pages from Db
$pages = PageModel::selectAll('DISTINCT page.*', '
LEFT JOIN page_has_content ON page_has_content.page_id = page.id
LEFT JOIN content ON content.id = page_has_content.content_id
WHERE page.active = 1 AND content.active = 1
ORDER BY page.order ASC;
');
// Return if no pages
if (!_exists($pages)) {
return;
}
// Select id column
$section = array_column($sections, 'section', 'id');
// Loop through all pages
foreach ($pages as $pageKey => $page) {
// Skip if section isn't created / active
if (!_exists($section, $page['section_id'])) { continue; }
// url = /section/page
$url = '/' . $section[$page['section_id']] . '/' . $page['page'];
// Add route
self::$routes[] = [$url, 'PageController', 'route', $page['id']];
}
// Cache sections and pages
Db::setSections($sections);
Db::setPages($pages);
}
protected static function addRoute(array $method = [], array $data = []): void
{
if (!_exists($method) || !_exists($data)) {
return;
}
$route = $data[0] ?? '';
$controller = $data[1] ?? '';
$action = $data[2] ?? '';
$param = $data[3] ?? [];
if ($route == '' || $controller == '' || $action == '') {
return;
}
// Create Klein route
self::$router->respond($method, $route, function($request, $response, $service)
use($controller, $action, $param) {
// Create new Controller object
$controller = '\App\Controllers\\' . $controller;
$controller = new $controller(self::$router);
$stillValid = true;
// If method does not exist in object
if (!method_exists($controller, $action)) {
$stillValid = false;
}
// If no valid permissions
if ($controller->getAdminSection() &&
$controller->getLoggedIn() == false) {
$stillValid = false;
}
// Call Controller action
if ($stillValid) {
// Loop through params
$params = [];
foreach ($param as $name) {
$params[] = $request->param($name);
}
return $controller->{$action}(...$params);
}
else {
$controller->throw404();
}
});
}
protected static function addBasicRoute(array $method = [], array $route = []): void
{
if (!_exists($method) || !_exists($route)) {
return;
}
// Create Klein route
self::$router->respond($method, $route[0], function() use($route) {
// Create new Controller object
$controller = '\App\Controllers\\' . $route[1];
$controller = new $controller(self::$router);
// Complete action variable
if ($route[2] == '') {
$route[2] = 'indexAction';
}
else {
$route[2] .= 'Action';
}
$stillValid = true;
// If method does not exist in object
if (!method_exists($controller, $route[2])) {
$stillValid = false;
}
// If no valid permissions
if ($controller->getAdminSection() &&
$controller->getLoggedIn() == false) {
$stillValid = false;
}
// Call Controller action
if ($stillValid) {
if (is_array($route[3])) {
return $controller->{$route[2]}(
$route[3][0] ?? '',
$route[3][1] ?? '',
$route[3][2] ?? ''
);
}
else if ($route[3] != '') {
return $controller->{$route[2]}($route[3]);
}
else {
return $controller->{$route[2]}();
}
}
else {
$controller->throw404();
}
});
}
public static function createNavigation(): void
{
// Pull from cache
$sections = Db::getSections();
$pages = Db::getPages();
// [
// [
// 'section url',
// 'title',
// ['page url', 'title'],
// ['page url', 'titleOfPage2'],
// ],
// [],
// [],
// ]
$navigation = [];
// Generate sections
foreach ($sections as $section) {
// Skip hidden sections
if ($section['hide_navigation'] == '1') {
continue;
}
// Add URL, title to ID
$navigation[$section['id']] = [
$section['section'], $section['title']
];
}
// Generate pages
foreach ($pages as $page) {
// Skip hidden sections
if (!_exists($navigation, $page['section_id'])) {
continue;
}
// Skip hidden pages
if ($page['hide_navigation'] == '1') {
continue;
}
// Add [URL, title] to ID
$navigation[$page['section_id']][] = [
$page['page'], $page['title']
];
}
self::$router->service()->navigation = $navigation;
}
//-------------------------------------//
protected static function setHttpError(): void
{
self::$router->onHttpError(function($code) {
$service = self::$router->service();
switch ($code) {
case 404:
$service->escape = function (?string $string) {
return htmlentities($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
};
self::$router->response()->sendHeaders(true, true);
$service->pageTitle = 'Error 404 (Not Found)';
$service->render('../app/views/errors/404.php');
break;
}
});
}
//-------------------------------------//
public static function getRoutes(): array
{
return self::$routes;
}
}
Router::_init();
// @Todo
// - combine addRoute and addBasicroute functionality

300
app/classes/Session.php

@ -0,0 +1,300 @@
<?php
namespace App\Classes;
use stdClass;
class Session {
/**
* The session attributes.
*
* @var array
*/
private static $attributes;
/**
* Session store started status.
*
* @var bool
*/
protected static $started = false;
//-------------------------------------//
/**
* Start the PHP session
*
* @return void
*/
public static function start(): void
{
if (self::$started) {
return;
}
session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Strict']);
session_start();
self::$attributes = &$_SESSION;
if (!self::exists('_token')) {
self::regenerateToken();
}
self::$started = true;
}
//-------------------------------------//
/**
* Get all of the session data.
*
* @return array
*/
public static function all(): array
{
return self::$attributes;
}
/**
* Checks if a key exists.
*
* @param string|array $key
*
* @return bool
*/
public static function isset($key): bool
{
$placeholder = new stdClass;
return self::get($key, $placeholder) !== $placeholder;
}
/**
* Check if a key is present and not null.
*
* @param string|array $key
*
* @return bool
*/
public static function exists($key): bool
{
return !is_null(self::get($key));
}
/**
* Get an item from the session.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public static function get(string $key, $default = null)
{
if (array_key_exists($key, self::$attributes)) {
return self::$attributes[$key];
}
if (strpos($key, '.') === false) {
return $default;
}
// Get item using the "dot" notation
$array = self::$attributes;
foreach (explode('.', $key) as $segment) {
if (is_array($array) && array_key_exists($segment, $array)) {
$array = $array[$segment];
}
else {
return $default;
}
}
return $array;
}
/**
* Put a key / value pair or array of key / value pairs in the session.
*
* @param string|array $key
* @param mixed $value
* @return void
*/
public static function put($key, $value = null)
{
if (!is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
// Set array item to a given value using the "dot" notation
$keys = explode('.', $arrayKey);
$array = &self::$attributes;
foreach ($keys as $i => $key) {
if (count($keys) === 1) {
break;
}
unset($keys[$i]);
// If key doesnt exist at this depth, create empty array holder
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[array_shift($keys)] = $arrayValue;
}
}
/**
* Get an item from the session, or store the default value.
*
* @param string $key
*
* @return mixed
*/
public static function emplace(string $key)
{
$value = self::get($key);
if (!is_null($value)) {
return $value;
}
self::put($key, $value);
return $value;
}
/**
* Push a value onto a session array.
*
* @param string $key
* @param mixed $value
*
* @return void
*/
public static function push(string $key, $value): void
{
$array = self::get($key, []);
$array[] = $value;
self::put($key, $array);
}
/**
* Get the value of a given key and then remove it.
*
* @param string $keys
*
* @return mixed
*/
public static function pull(string $key, $default = null)
{
$result = self::get($key, $default);
self::delete($key);
return $result;
}
/**
* Delete one or many items from the session.
*
* @param string|array $keys
*
* @return void
*/
public static function delete($keys): void
{
$start = &self::$attributes;
$keys = (array)$keys;
if (count($keys) === 0) {
return;
}
foreach ($keys as $key) {
// Delete top-level if non-"dot" notation key
if (self::exists($key)) {
unset(self::$attributes[$key]);
continue;
}
// Delete using "dot" notation key
$parts = explode('.', $key);
// Move to the start of the array each pass
$array = &$start;
// Traverse into the associative array
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && is_array($array[$part])) {
$array = &$array[$part];
}
else {
continue 2;
}
}
unset($array[array_shift($parts)]);
}
}
/**
* Remove all of the items from the session.
*
* @return void
*/
public static function flush(): void
{
self::$attributes = [];
}
//-------------------------------------//
/**
* Get the CSRF token value.
*
* @return string
*/
public static function token()
{
return self::get('_token');
}
/**
* Regenerate the CSRF token value.
*
* @return void
*/
public static function regenerateToken()
{
self::put('_token', _randomStr(40));
}
/**
* Validate the CSRF token value to the given value.
*
* @param array $array
*
* @return bool
*/
public static function validateToken(array $array = null): bool
{
if (is_array($array) && array_key_exists('_token', $array) && $array['_token'] == self::token()) {
return true;
}
return false;
}
}

174
app/classes/User.php

@ -0,0 +1,174 @@
<?php
namespace App\Classes;
use App\Model\UserModel;
class User {
public static function check(): bool
{
$success = false;
// Session
if (Session::exists('user')) {
$success = true;
}
// If cookie is set, try to login
if (!$success &&
_exists($_COOKIE, 'id') &&
_exists($_COOKIE, 'username') &&
_exists($_COOKIE, 'salt') &&
_exists($_COOKIE, 'toggle')) {
$user = UserModel::find($_COOKIE['id']);
if ($user->exists() &&
$_COOKIE['username'] == $user->username &&
$_COOKIE['salt'] == $user->salt) {
$success = true;
self::setSession($_COOKIE['id'], $_COOKIE['username'],
$_COOKIE['salt'], $_COOKIE['toggle']);
}
}
return $success;
}
public static function login(string $username, string $password, string $rememberMe): bool
{
$user = UserModel::search(['username' => $username]);
$success = false;
if ($user->exists() && $user->failed_login_attempt <= 2) {
$saltPassword = $user->salt . $password;
if (password_verify($saltPassword, $user->password)) {
$success = true;
// On successful login, set failed_login_attempt to 0
if ($user->failed_login_attempt > 0) {
$user->failed_login_attempt = 0;
$user->save();
}
}
else {
$user->failed_login_attempt++;
$user->save();
}
}
if (!$success) {
self::logout();
return false;
}
// Set session
self::setSession($user->id, $user->username, $user->salt, 1);
// Set cookie
if ($rememberMe == '1') {
$time = time() + (3600 * 24 * 7);
self::setCookie($time, $user->id, $user->username, $user->salt, 1);
}
return true;
}
public static function logout(): void
{
Session::delete('user');
// Destroy user login cookie
$time = time() - 3600;
self::setCookie($time, 0, '', '', 0);
}
public static function getUser(string $id = '', string $username = '', string $email = ''): UserModel
{
if ($id == '' && $username == '' && $email == '' && self::check()) {
$id = Session::get('user.id');
$username = Session::get('user.username');
}
return UserModel::search([
'id' => $id,
'username' => $username,
'email' => $email,
], 'OR');
}
public static function toggle(): void
{
if (self::check()) {
// Toggle session
Session::put('user.toggle', !Session::get('user.toggle'));
// Toggle cookie
self::setCookieToggle(Session::get('user.toggle'));
}
}
//-------------------------------------//
protected static function setSession(
int $id, string $username, string $salt, int $toggle): void
{
Session::put('user', [
'id' => $id,
'username' => $username,
'salt' => $salt,
'toggle' => $toggle,
]);
}
protected static function setCookie(
int $time, int $id, string $username, string $salt, int $toggle): void
{
if (_exists($_SERVER, 'HTTPS') && $_SERVER['HTTPS'] == 'on') {
$domain = Config::c('APP_NAME');
$options = [
'expires' => $time,
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
];
setcookie('id', $id, $options);
setcookie('username', $username, $options);
setcookie('salt', $salt, $options);
setcookie('toggle', $toggle, $options);
}
}
protected static function setCookieToggle(int $toggle): void
{
if (_exists($_SERVER, 'HTTPS') && $_SERVER['HTTPS'] == 'on') {
$domain = Config::c('APP_NAME');
$options = [
'expires' => time() + (3600 * 24 * 7),
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
];
setcookie('toggle', $toggle, $options);
}
}
//-------------------------------------//
public static function getToggle(): int
{
return self::check() ? Session::get('user.toggle') : 0;
}
public static function getSession(): array
{
return self::check() ? Session::get('user') : [];
}
}

24
app/controllers/AdminController.php

@ -0,0 +1,24 @@
<?php
namespace App\Controllers;
use App\Classes\User;
class AdminController extends PageController {
public function indexAction(): void {
$this->router->service()->user = User::getUser();
parent::view('', 'Admin');
}
public function toggleAction(): void {
User::toggle();
echo User::getToggle() ? '1' : '0';
}
public function syntaxAction(): void {
parent::view();
}
}

133
app/controllers/BaseController.php

@ -0,0 +1,133 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Db;
use App\Classes\Session;
use App\Classes\User;
class BaseController {
protected $router;
protected $section;
protected $page;
protected $loggedIn;
protected $url;
protected $adminSection;
//-------------------------------------//
public function __construct(\Klein\Klein $router = null)
{
$this->router = $router;
$request = $this->router->request()->uri();
$request = parse_url($request)['path'];
$request = explode("/", $request);
if (array_key_exists(1, $request)) {
$this->section = $request[1];
}
if (array_key_exists(2, $request)) {
$this->page = $request[2];
}
// Set login status
$this->loggedIn = User::check();
$this->router->service()->loggedIn = $this->loggedIn;
// Set url https://site.com/section/page
$this->url = Config::c('APP_URL');
$this->url .= _exists([$this->section]) ? '/' . $this->section : '';
$this->url .= _exists([$this->page]) ? '/' . $this->page : '';
$this->router->service()->url = $this->url;
// If Admin section
$this->adminSection = $this->section == 'admin';
$this->router->service()->adminSection = $this->adminSection;
// Clear alert
$this->setAlert('', '');
// Load alert set on the previous page
$this->loadAlert();
// View helper method
$this->router->service()->escape = function (?string $string) {
return htmlentities($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
};
}
//-------------------------------------//
public function throw404(): void
{
$this->router->response()->sendHeaders(true, true);
$service = $this->router->service();
$service->pageTitle = 'Error 404 (Not Found)';
$service->render('../app/views/errors/404.php');
exit();
}
/**
* Set alert for the current page
*
* @param string $type Color of the message (success/danger/warning/info)
* @param string $message The message to display
*
* @return void
*/
public function setAlert(string $type, string $message): void
{
$this->router->service()->type = $type;
$this->router->service()->message = $message;
}
/**
* Set alert for the next page
*
* @param string $type Color of the message (success/danger/warning/info)
* @param string $message The message to display
*
* @return void
*/
public function setAlertNext(string $type, string $message): void
{
Session::put('type', $type);
Session::put('message', $message);
}
/**
* Load alert set on the previous page
*
* @return void
*/
public function loadAlert(): void
{
if (Session::exists('type') && Session::exists('message')) {
$this->setAlert(Session::get('type'), Session::get('message'));
Session::delete(['type', 'message']);
}
}
//-------------------------------------//
public function getSection(): string
{
return $this->section;
}