Browse Source

Initial commit

Couldnt keep the history unfortunately.
master
Riyyi 3 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;
}
public function getLoggedIn(): bool
{
return $this->loggedIn;
}
public function getAdminSection(): bool
{
return $this->adminSection;
}
}
// @Todo
// - Image lazy loading

249
app/controllers/CrudController.php

@ -0,0 +1,249 @@
<?php
namespace App\Controllers;
use App\Classes\Session;
class CrudController extends PageController {
public static $pagination = 10;
//-------------------------------------//
/**
* Display a listing of the resource.
*
* @return void
*/
public function indexAction(): void
{
$modelName = $this->getModelName();
$model = "\App\Model\\{$modelName}Model";
$model = new $model;
// ?page=x
$page = 1;
if (_exists($_GET, 'page') && is_numeric($_GET['page'])) {
$page = $_GET['page'];
}
$rows = $model->all($page, self::$pagination);
// Set empty value
if (!_exists($rows)) {
$rows = [];
}
$this->router->service()->attributes = $model->getAttributesRules();
$this->router->service()->csrfToken = Session::token();
$this->router->service()->rows = $rows;
$this->router->service()->title = $modelName;
// Set page variables
$this->router->service()->page = $page;
$pages = ceil($model->count() / self::$pagination);
$pages > 1
? $this->router->service()->pages = $pages
: $this->router->service()->pages = 1;
parent::view("{$this->section}/crud/index");
}
/**
* Show the form for creating a new resource.
*
* @return void
*/
public function createAction(): void
{
$model = "\App\Model\\{$this->getModelName()}Model";
$model = new $model;
$attributes = $model->getAttributesRules();
// Get dropdown data
$dropdownData = [];
foreach ($attributes as $key => $attribute) {
if ($attribute[1] == 'dropdown') {
$dropdownData[$key] = $model->getDropdownData($attribute[0]);
}
}
$this->router->service()->attributes = $attributes;
$this->router->service()->csrfToken = Session::token();
$this->router->service()->dropdownData = $dropdownData;
parent::view("{$this->section}/crud/create");
}
/**
* Store a newly created resource in storage.
*
* @return void
*/
public function storeAction(): void
{
$modelName = $this->getModelName();
$model = "\App\Model\\{$modelName}Model";
$model = new $model;
$token = Session::validateToken($_POST);
$token && $model->fill($_POST) && $model->save()
? $this->setAlertNext('success', "$modelName successfully created.")
: $this->setAlertNext('danger', "$modelName could not be created!");
$this->router->response()->redirect($this->url);
}
/**
* Display the specified resource.
*
* @param int $id
*
* @return void
*/
public function showAction(int $id): void
{
$model = "\App\Model\\{$this->getModelName()}Model";
$model = $model::find($id);
if (!$model->exists()) {
parent::throw404();
}
$this->router->service()->model = $model;
$this->router->service()->attributes = $model->getAttributesRules();
parent::view("{$this->section}/crud/show");
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
*
* @return void
*/
public function editAction(int $id): void
{
$model = "\App\Model\\{$this->getModelName()}Model";
$model = $model::find($id);
if (!$model->exists()) {
parent::throw404();
}
$attributes = $model->getAttributesRules();
// Get dropdown data
$dropdownData = [];
foreach ($attributes as $key => $attribute) {
if ($attribute[1] == 'dropdown') {
$dropdownData[$key] = $model->getDropdownData($attribute[0]);
}
}
$this->router->service()->attributes = $attributes;
$this->router->service()->csrfToken = Session::token();
$this->router->service()->dropdownData = $dropdownData;
$this->router->service()->model = $model;
parent::view("{$this->section}/crud/edit");
}
/**
* Update the specified resource in storage.
*
* @param int $id
*
* @return void
*/
public function updateAction(int $id): void
{
$modelName = $this->getModelName();
$model = "\App\Model\\{$modelName}Model";
$model = $model::find($id);
if (!$model->exists()) {
$this->setAlertNext('danger', "$modelName does not exist!");
}
else {
// Read PUT request
$this->parsePhpInput($_PUT);
$token = Session::validateToken($_PUT);
$token && $model->fill($_PUT) && $model->save()
? $this->setAlertNext('success', "$modelName successfully updated.")
: $this->setAlertNext('danger', "$modelName could not be updated!");
}
echo $this->url;
}
/**
* Remove the specified resource from storage.
*
* @param int $id
*
* @return void
*/
public function destroyAction(int $id): void
{
// Read DELETE request
$this->parsePhpInput($_DELETE);
$token = Session::validateToken($_DELETE);
$modelName = $this->getModelName();
$model = "\App\Model\\{$modelName}Model";
$token && $model::destroy($id)
? $this->setAlertNext('success', "$modelName successfully deleted.")
: $this->setAlertNext('danger', "$modelName could not be deleted!");
}
//-------------------------------------//
/**
* Generate the model name from the /section/page url
*
* @return string The model name
*/
private function getModelName(): string
{
$model = $this->page;
$model = str_replace('-', ' ', $model);
$model = str_replace('_', ' ', $model);
$model = ucwords($model);
$model = str_replace(' ', '', $model);
return $model;
}
/**
* PUT/DELETE requests aren't handled by PHP automatically yet.
* Parse them from php://input instead.
*
* @param ?array &$result Array filled with input data
*
* @return void
*/
private function parsePhpInput(?array &$result): void
{
// Parse json or form-encoded formatted data
if (strpos($_SERVER['CONTENT_TYPE'], "application/json") !== false) {
$result = json_decode(file_get_contents("php://input"), true);
}
else if (strpos($_SERVER['CONTENT_TYPE'], "application/x-www-form-urlencoded") !== false) {
parse_str(file_get_contents("php://input"), $result);
// Cleanup &amp; HTML entities
foreach ($result as $key => $value) {
unset($result[$key]);
$result[str_replace('amp;', '', $key)] = $value;
}
}
$_REQUEST = array_merge($_REQUEST, $result);
}
}

98
app/controllers/IndexController.php

@ -0,0 +1,98 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Db;
use App\Classes\Router;
use App\Classes\Session;
class IndexController extends PageController {
public function indexAction(): void
{
// Pull pages from cache
$pages = Db::getPages();
parent::routeAction(
array_search('home', array_column($pages, 'page', 'id')));
}
public function captchaAction(): void
{
header('Content-type: image/jpeg');
if (!Session::exists('captcha')) {
Session::put('captcha', _randomStr(4, '0123456789'));
}
$imageWidth = 151;
$imageHeight = 51;
// Text
$textSize = 30;
$textFont = 'fonts/captcha.otf';
$text = Session::get('captcha');
// Generate position
$randPosX = rand(0, 40);
$randPosY = rand(35, 45);
// Calculate rotation from the position
$rotationFactorUp = 1.0 - (($randPosY - 40.0) / 5.0);
$rotationFactorDown = 1.0 - ((40.0 - $randPosY) / 5.0);
// Clamp between 0.0-1.0
$rotationFactorUp = max(0.0, min(1.0, $rotationFactorUp));
$rotationFactorDown = max(0.0, min(1.0, $rotationFactorDown));
$rotation = rand(-8 * $rotationFactorUp, 8 * $rotationFactorDown);
// Create image
$image = imagecreate($imageWidth, $imageHeight);
imagecolorallocate($image, 255, 255, 255);
// Render number
$textColor = imagecolorallocate($image, 0, 0, 0);
imagettftext($image, $textSize, $rotation, $randPosX, $randPosY,
$textColor, $textFont, $text);
// Render grid pattern
$lineColor = imagecolorallocate($image, 73, 106, 164);
$cord = 0;
for ($i = 1; $i <= 31; $i++) {
imageline($image, $cord, 0, $cord, 50, $lineColor);
imageline($image, 0, $cord, 150, $cord, $lineColor);
$cord = $cord + 5;
}
imagejpeg($image);
exit();
}
public function sitemapAction(): void
{
$xml = new \SimpleXMLElement('<urlset/>');
// Config routes
$routes = array_column(Router::getRoutes(), '0');
// Remove /admin and /test pages
foreach ($routes as $key => $route) {
if (strpos($route, '/admin') !== false ||
strpos($route, '/test') !== false) {
unset($routes[$key]);
}
}
foreach ($routes as $route) {
$url = $xml->addChild('url');
$loc = Config::c('APP_URL') . $route;
$url->addChild('loc', $loc);
}
Header('Content-type: text/xml');
print($xml->asXML());
}
}

232
app/controllers/LoginController.php

@ -0,0 +1,232 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Db;
use App\Classes\Form;
use App\Classes\User;
use App\Classes\Mail;
class LoginController extends PageController {
public function loginAction(string $view, string $title): void {
$form = new Form($this->router);
$form->addField('username', [
'Username*',
'text',
'',
'required',
'Username is required.',
]);
$form->addField('password', [
'Password*',
'password',
'',
'required',
'Password is required.',
]);
$form->addField('rememberMe', [
'',
'checkbox',
['1' => 'Remember me'],
]);
$form->setSubmit('Sign in');
if ($form->validated()) {
if (User::login($_POST['username'], $_POST['password'], $_POST['rememberMe'])) {
$this->setAlert('success', 'Successfully signed in, redirecting...');
// Set delayed redirect URL
$this->router->service()->redirectURL = Config::c('APP_URL') . '/admin';
}
else {
$user = User::getUser('', $_POST['username']);
if ($user->exists() && $user->failed_login_attempt >= 5) {
$this->setAlert('danger', 'User has been blocked.');
}
else {
$this->setAlert('danger', 'Invalid username and password combination.');
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if(!_exists($_POST, 'username') || !_exists($_POST, 'password')) {
$this->setAlert('danger', 'Please fill out both fields.');
}
else {
$this->setAlert('danger', 'Could not sign in.');
}
}
$this->router->service()->password = "";
parent::view($view, $title);
}
public function resetAction(string $view, string $title): void {
// Send request
if (!_exists($_GET, 'uid') || !_exists($_GET, 'reset-key')) {
$this->requestResetForm();
}
// Reset password
else {
$this->resetPasswordForm();
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
$this->router->service()->newPassword = true;
$user = User::getUser($_GET['uid']);
if (!$user->exists() || $_GET['reset-key'] != $user->reset_key) {
$this->setAlert('danger', 'Link expired.');
}
}
}
parent::view($view, $title);
}
public function logoutAction(): void {
User::logout();
$this->router->response()->redirect('/');
}
//-------------------------------------//
private function requestResetForm(): void {
// Only have required on 1 field
$usernameRule = !_exists($_POST, 'reset-password-email') ? 'required' : '';
$emailRule = !_exists($_POST, 'reset-password-username') ? 'required|email' : '';
$form = new Form($this->router);
$form->addField('reset-password-username', [
'Username',
'text',
'',
"$usernameRule",
'Username is required.',
]);
$form->addField('reset-password-email', [
'Email',
'email',
'',
"$emailRule",
'Please enter a valid email address.',
'[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$',
'Valid email address'
]);
$form->setSubmit('Send request');
if ($form->validated()) {
$this->requestResetSend();
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!_exists($_POST, 'reset-password-username') &&
!_exists($_POST, 'reset-password-email')) {
$this->setAlert('danger', 'Please fill out one of the fields.');
}
else {
$error = $form->errorMessage();
$this->setAlert('danger', $error ? $error : 'Could not send request.');
}
}
}
private function requestResetSend(): void {
$user = User::getUser('', $_POST['reset-password-username'], $_POST['reset-password-email']);
if ($user->exists()) {
// Generate new reset key
$user->reset_key = _randomStr(25);
$user->save();
// Send reset mail
$subject = 'Password reset request - ' . Config::c('APP_NAME');
$resetUrl = Config::c('APP_URL') . "/reset-password?uid={$user->id}&reset-key={$user->reset_key}";
$message = "
Click the link below to reset your password:<br>
<a href='$resetUrl'>$resetUrl</a>
";
if (Mail::send($subject, $message, $user->email)) {
$this->setAlert('success', 'Successfully requested password reset.');
}
else {
$this->setAlert('danger', 'Password reset failed.');
}
}
else {
$this->setAlert('danger', 'User was not found.');
}
}
private function resetPasswordForm(): void {
// Add _GETs to form post url
$uid = $_GET['uid'];
$resetKey = $_GET['reset-key'];
$this->router->service()->url .= "?uid=$uid&reset-key=$resetKey";
$form = new Form($this->router);
$form->addField('password', [
'New password*',
'password',
'',
'required',
'New password is required.',
]);
$form->addField('password-again', [
'New password again*',
'password',
'',
'required',
'New password again is required.',
]);
$form->setSubmit('Reset password');
if ($form->validated()) {
$this->resetPasswordSend();
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!_exists($_POST, 'password') ||
!_exists($_POST, 'password-again')) {
$this->setAlert('danger', 'Please fill out all fields.');
}
else {
$error = $form->errorMessage();
$this->setAlert('danger', $error ? $error : 'Could not reset password.');
}
}
}
private function resetPasswordSend(): void
{
$user = User::getUser($_GET['uid']);
if ($user->exists()) {
(bool)$notEmptyKey = $user->reset_key != '';
(bool)$correctKey = $user->reset_key == $_GET['reset-key'];
(bool)$identicalPass = $_POST['password'] == $_POST['password-again'];
}
if ($notEmptyKey && $correctKey && $identicalPass) {
$user->salt = _randomStr(15);
$user->password = password_hash($user->salt . $_POST['password'], PASSWORD_BCRYPT, ['cost', 12]);
$user->reset_key = '';
$user->save();
$this->setAlert('success', 'Successfully changed password.');
}
// Display error message
if (!$user || !$notEmptyKey || !$correctKey) {
$this->setAlert('danger', 'Invalid password reset link.');
}
else if (!$identicalPass) {
$this->setAlert('danger', 'Fields did not match.');
}
}
}

117
app/controllers/MediaController.php

@ -0,0 +1,117 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Media;
use App\Model\LogModel;
use App\Model\MediaModel;
use App\Model\UserModel;
class MediaController extends PageController {
/**
* Display a listing of the resource.
*
* @return void
*/
public function indexAction(): void
{
$this->mediaPage();
parent::view();
}
/**
* Store a newly created resource in storage.
*
* @return void
*/
public function storeAction(): void
{
$name = ucfirst($this->page);
$overwrite = _exists($_POST, 'overwrite');
$error = Media::uploadMedia($overwrite);
if (!$error) {
$this->setAlert('success', "$name successfully created.");
}
else {
$this->setAlert('danger', Media::errorMessage($error));
}
$this->mediaPage();
parent::view();
}
/**
* Remove the specified resource from storage.
*
* @param int $id
*
* @return void
*/
public function destroyAction(int $id): void
{
$name = ucfirst($this->page);
if (Media::deleteMedia($id)) {
$this->setAlertNext('success', "$name successfully deleted.");
}
else {
$this->setAlertNext('danger', "$name could not be deleted!");
}
}
//-------------------------------------//
protected function mediaPage(): void {
// ?page=x
$page = 1;
if (_exists($_GET, 'page') && is_numeric($_GET['page'])) {
$page = $_GET['page'];
}
$mediaModel = new MediaModel;
// Get all Media of the page
$media = $mediaModel->all($page, Media::$pagination);
// Get all the connected Logs
$log = null;
if (_exists($media)) {
$logId = array_column($media, 'log_id');
$log = LogModel::findAll($logId);
}
// Get all the connected Users
$user = null;
if (_exists($log)) {
$uploaderId = array_column($log, 'user_id');
$user = UserModel::findAll($uploaderId);
}
// Set empty values
if (!_exists($media) || !_exists($log) || !_exists($user)) {
$media = [];
$log = [["user_id" => "0"]];
$user = [["username" => "Could not load users.."]];
}
// Set view Media variables
$this->router->service()->media = $media;
$this->router->service()->log = $log;
$this->router->service()->user = $user;
// Set view Page variables
$this->router->service()->page = $page;
$pages = ceil($mediaModel->count() / Media::$pagination);
$pages > 1
? $this->router->service()->pages = $pages
: $this->router->service()->pages = 1;
$this->router->service()->fileUrl = Config::c('APP_URL') . '/' . Media::$directory . '/';
}
}

186
app/controllers/PageController.php

@ -0,0 +1,186 @@
<?php
namespace App\Controllers;
use App\Classes\Db;
use App\Model\Model;
use App\Model\ContentModel;
use App\Model\PageHasContentModel;
use App\Model\SectionHasContentModel;
class PageController extends BaseController {
/**
* Path of the page view files.
*
* @var string
*/
protected $views = '../app/views/';
//-------------------------------------//
/**
* Create a new page controller instance.
*
* @param Klein $router
*
* @return mixed
*/
public function __construct(\Klein\Klein $router = null)
{
parent::__construct($router);
}
//-------------------------------------//
/**
* Handle page request with Db stored content.
*
* @param int $id
*
* @return void
*/
public function routeAction(int $id): void
{
// Pull pages from cache
$pages = Db::getPages();
$page = array_search($id, array_column($pages, 'id'));
$page = $pages[$page];
$title = $page['title'] ?? '';
$metaDescription = $page['meta_description'] ?? '';
// Load linked content
$pageHasContent = new PageHasContentModel;
$sectionHasContent = new SectionHasContentModel;
$contents = array_merge(
(array)$this->loadLinkedContent($pageHasContent, 'page', $id),
(array)$this->loadLinkedContent($sectionHasContent, 'section', $page['section_id']));
// Exit if nothing was found
if (!_exists($contents)) {
parent::throw404();
}
$sideContent = in_array('2', array_column($contents, 'type'));
$this->router->service()->contents = $contents;
$this->router->service()->sideContent = $sideContent;
$this->view('content', $title, $metaDescription);
}
//-------------------------------------//
/**
* Load all content blocks linked to the provided Model.
*
* @param Model $model
* @param string $column
* @param int $id
*
* @return null|array
*/
protected function loadLinkedContent(Model $model, string $column, int $id): ?array
{
// Load all the Model <-> Content link data
$hasContent = $model::selectAll('*', "
WHERE {$column}_id = :id
ORDER BY {$model->getSort()} ASC", [
[':id', $id, \PDO::PARAM_INT],
]
);
// Exit if nothing was found
if (!_exists($hasContent)) {
return null;
}
// Get all the content
$contentIds = array_column($hasContent, 'content_id');
$contents = ContentModel::findAll($contentIds);
// Exit if nothing was found
if (!_exists($contents)) {
return null;
}
// Remove inactive content
foreach ($contents as $key => $content) {
if ($content['active'] == "0") {
unset($contents[$key]);
}
}
$contents = array_values($contents);
return $contents;
}
/**
* Render page view with title and meta description.
*
* @param string $view
* @param string $pageTitle
* @param string $metaDescription
*
* @return void
*/
protected function view(
string $view = '', string $pageTitle = '', string $metaDescription = ''): void
{
if ($view != '') {
$view = $this->fileExists($this->views . $view . '.php');
}
if ($this->page == null) {
if ($view == '' && $this->section == '') {
// /
$view = $this->fileExists($this->views . 'home.php');
}
if ($view == '') {
// /example.php
$view = $this->fileExists($this->views . $this->section . '.php');
}
if ($view == '') {
// /example/index.php
$view = $this->fileExists($this->views . $this->section . '/index.php');
}
}
else if ($view == '') {
// /example/my-page.php
$view = $this->fileExists($this->views . $this->section . '/' . $this->page . '.php');
}
if ($view != '') {
$pageTitle != ''
? $this->router->service()->pageTitle = $pageTitle
: $this->router->service()->pageTitle = ucfirst(str_replace('-', ' ', $this->page));
$this->router->service()->metaDescription = $metaDescription;
$this->router->service()->render($view);
}
else {
parent::throw404();
}
}
//-------------------------------------//
/**
* Loop back filename if it exists, empty string otherwise.
*
* @param string $file
*
* @return string
*/
private function fileExists(string $file): string
{
return file_exists($file) ? $file : '';
}
}
// @Todo
// - Fix line 32, breaks if no DB content!
// - Implement page.description (meta)
// - Use page.title instead of content.title (?)

25
app/controllers/TestController.php

@ -0,0 +1,25 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Db;
use App\Classes\User;
use App\Classes\Mail;
use App\Classes\Media;
use App\Classes\Session;
use App\Model\MediaModel;
use App\Model\Model;
use App\Model\SectionModel;
use App\Model\PageModel;
use App\Model\UserModel;
use App\Model\LogModel;
class TestController extends PageController {
public function indexAction(): void {
parent::throw404();
}
}

90
app/helper.php

@ -0,0 +1,90 @@
<?php
/**
* Check if array element exists
*
* @param array $array The array to check for element
* @param string $key The element to find
*
* @return bool True if element eixsts
*/
function _exists(array $array = null, string $key = '0'): bool {
return isset($array[$key]) && !empty($array[$key]);
}
/**
* Cut off string after the found character
*
* @param string $string The input string
* @param string $fromCharacter The string is cut after this character
*
* @return string The new cut string
*/
function _trim(string $string, string $fromCharacter): string {
$position = strpos($string, $fromCharacter);
$end = strlen($string);
return substr($string, 0, $position !== false ? $position : $end);
}
/**
* Generate a cryptographically secure random string
*
* @param int $length Length of the string
* @param string $keyspace Possible characters the string can have
*
* @return string The generated string
*/
function _randomStr(int $length, string $keyspace =
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): string {
$pieces = [];
$max = mb_strlen($keyspace, '8bit') - 1;
for ($i = 0; $i < $length; $i++) {
$pieces[] = $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
}
/**
* Print variable inside of a <pre> and exit
*
* @param mixed[] $output The variable (single/array) to print
*
* @return void Nothing
*/
function _log($output): void {
echo '<pre>';
var_dump($output);
echo '</pre>';
die();
}
//-------------------------------------//
if (!function_exists('session')) {
/**
* Get / set the specified session value.
* If key is an array, treat as a setter.
*
* @param string|array $key
* @param mixed $default
*
* @return mixed
*/
function session($key = null, $default = null)
{
$session = "\App\Classes\Session";
if (is_null($key)) {
return $session;
}
if (is_array($key)) {
return $session::put($key);
}
return $session::get($key, $default);
}
}

50
app/model/ContentModel.php

@ -0,0 +1,50 @@
<?php
namespace App\Model;
use App\Traits\Log;
class ContentModel extends Model {
use Log { delete as deleteLog; }
protected $table = 'content';
protected $sort = 'title';
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["content", "textarea", 0, 0],
["title", "text", 0, 0],
["type", "dropdown", 1, 0],
["hide_title", "checkbox", 1, 0],
["hide_background", "checkbox", 1, 0],
["active", "checkbox", 1, 0],
["log_id", "text", 1, 1],
];
//-------------------------------------//
// Generate the dropdown data
public function getDropdownData(string $type): array
{
if ($type == 'type') {
return [0 => 'Select type', 1 => 'Page content', 2 => 'Side block'];
}
return [];
}
public function delete(): bool
{
if (self::query(
"DELETE FROM `page_has_{$this->table}` WHERE `{$this->table}_$this->primaryKey` = :id", [
[':id', $this->{$this->primaryKey}],
]
) === null) {
return false;
}
return $this->deleteLog();
}
}

7
app/model/LogModel.php

@ -0,0 +1,7 @@
<?php
namespace App\Model;
class LogModel extends Model {
protected $table = 'log';
}

14
app/model/MediaModel.php

@ -0,0 +1,14 @@
<?php
namespace App\Model;
use App\Traits\Log;
class MediaModel extends Model {
use Log;
protected $table = 'media';
protected $sort = 'filename';
}

503
app/model/Model.php

@ -0,0 +1,503 @@
<?php
namespace App\Model;
use App\Classes\Db;
abstract class Model {
protected $table;
protected $primaryKey = 'id';
protected $keyType = 'int';
protected $incrementing = true;
protected $perPage = 20;
protected $exists = false;
protected $sort = 'id';
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
private $attributes = [];
public function __construct()
{
// Fill in table name
if (empty($this->table)) {
$class = strtolower(get_class($this));
$pos = strrpos($class, '\\');
$this->table = substr($class, $pos + 1);
}
// Pull columns from cache
$columnsCache = Db::getColumns();
// If exists in cache
if (array_key_exists($this->table, $columnsCache)) {
$columns = $columnsCache[$this->table];
}
// Otherwise query Db and add to cache
else {
$columns = self::query("SHOW COLUMNS FROM `$this->table`");
$columns = array_column($columns, 'Field');
$columnsCache[$this->table] = $columns;
Db::setColumns($columnsCache);
}
// Create attribute placeholders
if (_exists($columns)) {
foreach ($columns as $column) {
if ($column != $this->primaryKey) {
$this->attributes[] = $column;
}
$this->{$column} = null;
}
}
}
//-------------------------------------//
protected static function query(string $query, array $parameters = [],
$type = ':'): ?array
{
// Example
// $parameters = [
// [':id', 1],
// [':number', 7, \PDO::PARAM_INT],
// [':string', 'A random string', \PDO::PARAM_STR],
// ];
// PDO::PARAM_BOOL
// PDO::PARAM_NULL
// PDO::PARAM_INT
// PDO::PARAM_STR
if (substr_count($query, $type) != count($parameters)) {
return null;
}
$query = Db::get()->prepare($query);
$success = false;
if ($type == '?') {
$success = $query->execute($parameters);
}
else {
foreach ($parameters as $key => $parameter) {
if (count($parameter) == 2) {
$query->bindParam($parameter[0], $parameter[1]);
}
else if (count($parameter) == 3) {
$query->bindParam($parameter[0], $parameter[1], $parameter[2]);
}
}
$success = $query->execute();
}
if (!$success) {
return null;
}
return $query->fetchAll();
}
//-------------------------------------//
public function exists(): bool
{
if ($this->exists == true) {
return $this->exists;
}
$exists = self::query(
"SELECT id FROM `$this->table` WHERE `$this->primaryKey` = :id", [
[':id', $this->{$this->primaryKey}],
]
);
if (_exists($exists)) {
$this->exists = true;
}
return $this->exists;
}
public function save(): bool
{
$parameters = [];
$exists = $this->exists();
// Insert new Model
if (!$exists) {
$query = "INSERT INTO `$this->table` ";
for ($i = 0; $i < 2; $i++) {
if ($i == 0) {
$query .= '(';
}
else {
$query .= 'VALUES (';
}
foreach ($this->attributes as $key => $attribute) {
if ($key != 0) {
$query .= ', ';
}
if ($i == 0) {
$query .= "`$attribute`";
}
else {
$query .= ":$attribute";
$parameters[] = [":$attribute", $this->{$attribute}];
}
}
$query .= ') ';
}
}
// Update existing Model
else {
$query = "UPDATE `$this->table` SET ";
foreach ($this->attributes as $key => $attribute) {
if ($key != 0) {
$query .= ', ';
}
$query .= "`$attribute` = :$attribute";
$parameters[] = [":$attribute", $this->{$attribute}];
}
$query .= " WHERE `$this->primaryKey` = :id";
$parameters[] = [':id', $this->{$this->primaryKey}];
}
if (self::query($query, $parameters) === null) {
return false;
}
// Fill in primary key and exists for newly created Models
if (!$exists) {
$id = self::query("SELECT LAST_INSERT_ID() as `$this->primaryKey`");
if (_exists($id)) {
$this->{$this->primaryKey} = $id[0][$this->primaryKey];
$this->exists = true;
}
}
return true;
}
public function delete(): bool
{
return self::query(
"DELETE FROM `$this->table` WHERE `$this->primaryKey` = :id", [
[':id', $this->{$this->primaryKey}],
]
) !== null;
}
public function count(): int
{
$total = self::query(
"SELECT COUNT({$this->primaryKey}) as total FROM `$this->table`");
return $total[0]['total'] ?? 0;
}
//-------------------------------------//
public function fill(array $fill): bool
{
if (!_exists([$fill])) {
return false;
}
// Set primary key
if (_exists($fill, $this->primaryKey)) {
$this->{$this->primaryKey} = $fill[$this->primaryKey];
}
// Set other attributes
foreach ($this->getAttributes() as $attribute) {
if (_exists($fill, $attribute) ||
(isset($fill[$attribute]) && $fill[$attribute] === '0')) {
// Escape sequences are only interpreted with double quotes!
$this->{$attribute} = preg_replace('/\r\n?/', "\n", $fill[$attribute]);
}
}
return true;
}
public function getPrimaryKey(): string
{
return $this->primaryKey;
}
public function getAttributes(): array
{
return $this->attributes;
}
public function getAttributesRules(): array
{
// Check if model has set rules
if (!_exists($this->rules) || !is_array($this->rules)) {
$rules = [];
}
else {
$rules = $this->rules;
}
// Get first column (name) of every rule
$rulesSearch = array_column($rules, 0);
// Loop through attributes
foreach ($this->attributes as $attribute) {
$found = array_search($attribute, $rulesSearch);
if ($found === false) {
// Define default ruleset
$rules[] = [$attribute, "text", 0, 0];
// Name | Type | Required | Filtered
// Type can be:
// - text
// - textarea
// - checkbox
// - dropdown // @Todo: store dropdown data
}
}
return $rules;
}
// Placeholder for generating the dropdown data
public function getDropdownData(string $type): array
{
return [];
}
public function getSort(): string
{
return is_array($this->sort)
? '`' . implode('`, `', $this->sort) . '`'
: '`' . $this->sort . '`';
}
//-------------------------------------//
public static function select(string $select = '*', string $filter = '',
array $parameters = [], $type = ':'): Model
{
$class = get_called_class();
$model = new $class;
$select = self::query("SELECT $select FROM `$model->table` $filter",
$parameters, $type);
if (_exists($select)) {
$model->fill($select[0]);
}
return $model;
}
// $media = MediaModel::selectAll(
// '*', 'ORDER BY id DESC LIMIT :offset, :limit', [
// [':offset', $offset, \PDO::PARAM_INT],
// [':limit', $limit, \PDO::PARAM_INT],
// ]
// );
//
// $contents = ContentModel::selectAll(
// '*', 'WHERE id IN (?, ?, ?)', [1, 2, 3], '?'
// );
public static function selectAll(string $select = '*', string $filter = '',
array $parameters = [], $type = ':'): ?array
{
$class = get_called_class();
$model = new $class;
return self::query("SELECT $select FROM `$model->table` $filter",
$parameters, $type);
}
public static function find(int $id): Model
{
$class = get_called_class();
$model = new $class;
$find = self::query("SELECT * FROM `$model->table` WHERE `$model->primaryKey` = :id", [
[':id', $id],
]);
if (_exists($find)) {
$model->fill($find[0]);
}
return $model;
}
// @Todo: add field name to query -> update Media.php md5
public static function findAll(array $id = []): ?array
{
if (_exists($id)) {
$id = array_values(array_unique($id));
$in = str_repeat('?, ', count($id) - 1) . '?';
array_push($id, ...$id);
return self::selectAll(
'*', "WHERE id IN ($in) ORDER BY FIELD(id, $in)", $id, '?');
}
else {
return self::selectAll();
}
}
public static function findOrFail(int $id): Model
{
$model = self::find($id);
if (!$model->exists()) {
throw new \Exception('Could not find Model!');
}
return $model;
}
public static function search(array $constraints, string $delimiter = 'AND'): Model
{
$parameters = [];
$filter = "WHERE ";
foreach (array_keys($constraints) as $key => $constraint) {
if ($key != 0) {
$filter .= " $delimiter ";
}
$filter .= "`$constraint` = :$constraint";
$parameters[] = [":$constraint", $constraints[$constraint]];
}
return self::select('*', $filter, $parameters);
}
public static function destroy(int $id): bool
{
$model = self::find($id);
if ($model->exists()) {
return $model->delete();
}
return false;
}
/**
* Load Model data: all, with a limit or pagination
*
* @param int $limitOrPage Treated as page if $limit is provided, limit otherwise
* @param int $limit The amount to limit by
*
* @return array|null The found model data, or null
*/
public static function all(int $limitOrPage = -1, int $limit = -1): ?array {
$class = get_called_class();
$model = new $class;
$filter = '';
$parameters = [];
// If the user wants to paginate
if ($limitOrPage >= 1 && $limit >= 0) {
// Pagination calculation
$page = ($limitOrPage - 1) * $limit;
$filter = 'LIMIT :page, :limit';
$parameters[] = [':page', $page, \PDO::PARAM_INT];
$parameters[] = [':limit', $limit, \PDO::PARAM_INT];
}
// If the user wants an offset
else if ($limitOrPage >= 0) {
$filter = 'LIMIT :limit';
$parameters[] = [':limit', $limitOrPage, \PDO::PARAM_INT];
}
return $model->selectAll(
'*', "ORDER BY {$model->getSort()} ASC $filter",
$parameters
);
}
/**
* Retreive Model, or instantiate
* Usage: $model = \App\Model\Example::firstOrNew(['name' => 'Example name']);
*
* @param $search Retrieve by
* @param $data Instantiate with search plus data
*
* @return Model The Model
*/
public static function firstOrNew(array $search, array $data = []): Model
{
$model = self::search($search);
if (!$model->exists()) {
$class = get_called_class();
$model = new $class;
$model->fill($search);
$model->fill($data);
}
return $model;
}
/**
* Create new Model
* Usage: $model = \App\Model\Example::create(['name' => 'Example name']);
*
* @param $data Create with this data
*
* @return Model The Model
*/
public static function create(array $data): Model {
$class = get_called_class();
$model = new $class;
$model->fill($data);
$model->save();
return $model;
}
/**
* Retreive Model, or create
* Usage: $model = \App\Model\Example::firstOrCreate(['name' => 'Example name']);
*
* @param $search Retrieve by
* @param $data Instantiate with search plus data
*
* @return Model The Model
*/
public static function firstOrCreate(array $search, array $data = []): Model {
$model = self::firstOrNew($search, $data);
if (!$model->exists()) {
$model->save();
}
return $model;
}
// // Update existing Model, or create it if doesn't exist
// public static function updateOrCreate(array $data, array $data): Model {
// // $flight = App\Flight::updateOrCreate(
// // ['departure' => 'Oakland', 'destination' => 'San Diego'],
// // ['price' => 99]
// // );
// return new Model;
// }
}
// @Todo
// - Generate rules from database table
// - Make count work without 'this' context

68
app/model/PageHasContentModel.php

@ -0,0 +1,68 @@
<?php
namespace App\Model;
use App\Classes\Db;
use App\Model\ContentModel;
use App\Model\PageModel;
class PageHasContentModel extends Model {
protected $table = 'page_has_content';
protected $sort = ['page_id', 'order'];
public $title = "PageHasContent";
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["order", "text", 1, 0],
["page_id", "dropdown", 1, 0],
["content_id", "dropdown", 1, 0],
];
//-------------------------------------//
// Generate the dropdown data
public function getDropdownData(string $type): array
{
if ($type == 'page_id') {
return $this->dropdownPage();
}
else if ($type == 'content_id') {
return $this->dropdownContent();
}
return [];
}
//-------------------------------------//
protected function dropdownPage(): array
{
$pages = PageModel::selectAll(
'*', "WHERE `active` = ? ORDER BY `title` ASC", [1], '?');
return [0 => 'Select page'] + array_combine(
array_column($pages, 'id'),
array_column($pages, 'title')
);
}
protected function dropdownContent(): array
{
$contents = ContentModel::selectAll(
'*', "WHERE `active` = ? ORDER BY `title` ASC", [1], '?');
// Exit if nothing was found
if (!_exists($contents)) {
return [];
}
return [0 => 'Select content'] + array_combine(
array_column($contents, 'id'),
array_column($contents, 'title')
);
}
}

76
app/model/PageModel.php

@ -0,0 +1,76 @@
<?php
namespace App\Model;
use App\Classes\Db;
use App\Traits\Log;
class PageModel extends Model {
use Log { delete as deleteLog; }
protected $table = 'page';
protected $sort = ['section_id', 'order'];
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["page", "text", 1, 0],
["title", "text", 0, 0],
["title_url", "text", 0, 1],
["meta_description", "text", 0, 0],
["type", "text", 1, 1],
["order", "text", 1, 0],
["hide_navigation", "checkbox", 1, 0],
["active", "checkbox", 1, 0],
["section_id", "dropdown", 1, 0],
["log_id", "text", 1, 1],
];
//-------------------------------------//
// Set default values
public function __construct()
{
parent::__construct();
$this->type = 0;
}
// Generate the dropdown data
public function getDropdownData(string $type): array
{
if ($type == 'section_id') {
return $this->dropdownSection();
}
return [];
}
public function delete(): bool
{
if (self::query(
"DELETE FROM `{$this->table}_has_content` WHERE `{$this->table}_$this->primaryKey` = :id", [
[':id', $this->{$this->primaryKey}],
]
) === null) {
return false;
}
return $this->deleteLog();
}
//-------------------------------------//
protected function dropdownSection(): array
{
// Pull sections from cache
$sections = Db::getSections();
return [0 => 'Select section'] + array_combine(
array_column($sections, 'id'),
array_column($sections, 'title')
);
}
}

68
app/model/SectionHasContentModel.php

@ -0,0 +1,68 @@
<?php
namespace App\Model;
use App\Classes\Db;
use App\Model\ContentModel;
use App\Model\SectionModel;
class SectionHasContentModel extends Model {
protected $table = 'section_has_content';
protected $sort = ['section_id', 'order'];
public $title = "SectionHasContent";
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["order", "text", 1, 0],
["section_id", "dropdown", 1, 0],
["content_id", "dropdown", 1, 0],
];
//-------------------------------------//
// Generate the dropdown data
public function getDropdownData(string $type): array
{
if ($type == 'section_id') {
return $this->dropdownSection();
}
else if ($type == 'content_id') {
return $this->dropdownContent();
}
return [];
}
//-------------------------------------//
protected function dropdownSection(): array
{
$sections = SectionModel::selectAll(
'*', "WHERE `active` = ? ORDER BY `title` ASC", [1], '?');
return [0 => 'Select section'] + array_combine(
array_column($sections, 'id'),
array_column($sections, 'title')
);
}
protected function dropdownContent(): array
{
$contents = ContentModel::selectAll(
'*', "WHERE `active` = ? ORDER BY `title` ASC", [1], '?');
// Exit if nothing was found
if (!_exists($contents)) {
return [];
}
return [0 => 'Select content'] + array_combine(
array_column($contents, 'id'),
array_column($contents, 'title')
);
}
}

24
app/model/SectionModel.php

@ -0,0 +1,24 @@
<?php
namespace App\Model;
use App\Traits\Log;
class SectionModel extends Model {
use Log;
protected $table = 'section';
protected $sort = 'order';
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["section", "text", 1, 0],
["title", "text", 0, 0],
["order", "text", 1, 0],
["hide_navigation", "checkbox", 1, 0],
["active", "checkbox", 1, 0],
["log_id", "text", 1, 1],
];
}

7
app/model/UserModel.php

@ -0,0 +1,7 @@
<?php
namespace App\Model;
class UserModel extends Model {
protected $table = 'user';
}

178
app/seed.php

@ -0,0 +1,178 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Classes\Config;
use App\Classes\Db;
use App\Classes\User;
use App\Model\ContentModel;
use App\Model\PageModel;
use App\Model\PageHasContentModel;
use App\Model\SectionModel;
use App\Model\SectionHasContentModel;
use App\Model\UserModel;
Config::load();
Db::load();
// Drop db and reset auto increment
//-------------------------------------//
$query = "
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE `page_has_content`;
TRUNCATE `section_has_content`;
TRUNCATE `content`;
TRUNCATE `page`;
TRUNCATE `section`;
TRUNCATE `media`;
TRUNCATE `log`;
TRUNCATE `user`;
SET FOREIGN_KEY_CHECKS = 1;
ALTER TABLE `page_has_content` AUTO_INCREMENT = 1;
ALTER TABLE `section_has_content` AUTO_INCREMENT = 1;
ALTER TABLE `content` AUTO_INCREMENT = 1;
ALTER TABLE `page` AUTO_INCREMENT = 1;
ALTER TABLE `section` AUTO_INCREMENT = 1;
ALTER TABLE `media` AUTO_INCREMENT = 1;
ALTER TABLE `log` AUTO_INCREMENT = 1;
ALTER TABLE `user` AUTO_INCREMENT = 1;
";
if ($argc >= 2) {
$query = Db::get()->prepare($query);
$query->execute();
die();
}
// Users
//-------------------------------------//
$users = [
['', '', '', '', '', '', '0', ''], // 1
// ['', '', '', '', '', '', '', ''],
];
foreach ($users as $user) {
UserModel::firstOrCreate(
['username' => $user[0]],
[
'email' => $user[1],
'first_name' => $user[2],
'last_name' => $user[3],
'salt' => $user[4],
'password' => $user[5],
'failed_login_attempt' => $user[6],
'reset_key' => $user[7],
]
);
}
User::login($users[0][0], 'password', $users[0][6]);
// Sections
//-------------------------------------//
$sections = [
['home', 'Homepage', '1', '1', '1'], // 1
// ['', '', '', '', ''],
];
foreach ($sections as $section) {
SectionModel::firstOrCreate(
['section' => $section[0]],
[
'title' => $section[1],
'order' => $section[2],
'hide_navigation' => $section[3],
'active' => $section[4],
],
);
}
// Pages
//-------------------------------------//
$pages = [
['home', 'Homepage', '', '', '1', '1', '0', '1', '1'], // 1
// ['', '', '', '', '', '', '', '', ''],
];
foreach ($pages as $page) {
PageModel::firstOrCreate(
['page' => $page[0]],
[
'title' => $page[1],
'title_url' => $page[2],
'meta_description' => $page[3],
'type' => $page[4],
'order' => $page[5],
'hide_navigation' => $page[6],
'active' => $page[7],
'section_id' => $page[8],
],
);
}
// Content
//-------------------------------------//
$contents = [
['Homepage', 'home', '1', '1', '0', '1'], // 1
// ['', '', '', '', '', ''],
];
foreach ($contents as $content) {
ContentModel::firstOrCreate(
['title' => $content[0]],
[
'content' => $content[1],
'type' => $content[2],
'hide_title' => $content[3],
'hide_background' => $content[4],
'active' => $content[5],
],
);
}
// PageHasContent
//-------------------------------------//
$pageLinks = [
// id, order, page_id, content_id
[ '1', '1', '1', '1'],
// ['', '', '', ''],
];
foreach ($pageLinks as $pageLink) {
PageHasContentModel::firstOrCreate(
['id' => $pageLink[0]],
[
'order' => $pageLink[1],
'page_id' => $pageLink[2],
'content_id' => $pageLink[3],
],
);
}
// SectionHasContent
//-------------------------------------//
$sectionLinks = [
// id, order, section_id, content_id
// ['1', '1', '1' '1'],
// ['', '', '', ''],
];
foreach ($sectionLinks as $sectionLink) {
SectionHasContentModel::firstOrCreate(
['id' => $sectionLink[0]],
[
'order' => $sectionLink[1],
'section_id' => $sectionLink[2],
'content_id' => $sectionLink[3],
],
);
}

68
app/traits/Log.php

@ -0,0 +1,68 @@
<?php
namespace App\Traits;
use App\Classes\User;
use App\Model\LogModel;
trait Log {
public function save(): bool
{
// Get timestamp
$date = date('Y-m-d H:i:s');
// Create log
if (!_exists([$this->log_id])) {
$log = new LogModel;
$log->{self::CREATED_AT} = $date;
$log->{self::UPDATED_AT} = $date;
$log->user_id = User::getSession()['id'];
if (!$log->save()) {
return false;
}
// Add log to new model
$this->log_id = $log->{$log->primaryKey};
}
// Update log
else {
$log = LogModel::findOrFail($this->log_id);
$log->{self::UPDATED_AT} = $date;
}
// Save model
if (!parent::save()) {
// Clean up the log if model creation failed
if (!_exists([$this->{$this->primaryKey}])) {
$log->delete();
}
return false;
}
// Update table and user ID
$log->table_name = $this->table;
$log->table_id = $this->{$this->primaryKey};
$log->user_id = User::getSession()['id'];
return $log->save();
}
public function delete(): bool
{
// Exit if there is no log
if (!_exists([$this->log_id])) {
return false;
}
$log = $this->log_id;
// Delete model
if (!parent::delete()) {
return false;
}
// Delete log
return LogModel::destroy($log);
}
}

62
app/views/admin/crud/create.php

@ -0,0 +1,62 @@
<div class="row">
<div class="col-12">
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<h3>Create</h3>
<form action="<?= $this->url; ?>" method="post">
<?php foreach($this->attributes as $key => $attribute) { ?>
<?php
if ($attribute[3] == 1) { continue; }
if ($attribute[2] == 1) { $required = 'required'; } else { $required = ''; }
$name = $attribute[0];
$title = ucfirst($attribute[0]);
$title = str_replace('_', ' ', $title);
$autofocus = $key == 0 ? 'autofocus' : '';
?>
<div class="form-group">
<label for="<?= $name; ?>"><?= $title;?></label><br>
<?php if ($attribute[1] == 'text') { ?>
<input type="text" class="form-control"
<?= $autofocus; ?>
<?= $required; ?>
name="<?= $name; ?>"
placeholder="<?= $title; ?>">
<?php } else if ($attribute[1] == 'textarea') { ?>
<textarea id="summernote" rows="18" cols="1" class="form-control"
<?= $autofocus; ?>
<?= $required; ?>
name="<?= $name; ?>"
placeholder="<?= $title; ?>"
></textarea>
<?php } else if ($attribute[1] == 'checkbox') { ?>
<input type="hidden" name="<?= $name; ?>" value="0">
<input type="checkbox" name="<?= $name; ?>" value="1">
<?php } else if ($attribute[1] == 'dropdown') { ?>
<select name="<?= $name; ?>" class="custom-select">
<?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?>
<option value="<?= $dropdownKey; ?>"><?= $value; ?></option>
<?php } ?>
</select>
<?php } ?>
</div>
<?php } ?>
<button type="submit" class="btn btn-dark">Create</button>
<input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" />
</form>
<div class="pb-5"></div>
</div>
</div>
</div>

66
app/views/admin/crud/edit.php

@ -0,0 +1,66 @@
<div class="row">
<div class="col-12">
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<h3>Edit</h3>
<form id="form-edit" action="<?= $this->url; ?>" method="post">
<?php foreach($this->attributes as $key => $attribute) { ?>
<?php
if ($attribute[3] == 1) { continue; }
if ($attribute[2] == 1) { $required = 'required'; } else { $required = ''; }
$name = $attribute[0];
$title = ucfirst($attribute[0]);
$title = str_replace('_', ' ', $title);
$autofocus = $key == 0 ? 'autofocus' : '';
?>
<div class="form-group">
<label for="<?= $name; ?>"><?= $title;?></label><br>
<?php if ($attribute[1] == 'text') { ?>
<input type="text" class="form-control"
<?= $autofocus; ?>
<?= $required; ?>
name="<?= $name; ?>"
placeholder="<?= $title; ?>"
value="<?= ($this->escape)($this->model->$name); ?>">
<?php } else if ($attribute[1] == 'textarea') { ?>
<textarea id="summernote" rows="18" cols="1" class="form-control"
<?= $autofocus; ?>
<?= $required; ?>
name="<?= $name; ?>"
placeholder="<?= $title; ?>"
><?= ($this->escape)($this->model->$name); ?></textarea>
<?php } else if ($attribute[1] == 'checkbox') { ?>
<input type="hidden" name="<?= $name; ?>" value="0">
<input type="checkbox" name="<?= $name; ?>" value="1"
<?= $this->model->$name == '1' ? 'checked' : ''; ?>>
<?php } else if ($attribute[1] == 'dropdown') { ?>
<select name="<?= $name; ?>" class="custom-select">
<?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?>
<option value="<?= $dropdownKey; ?>"
<?= $this->model->$name == $dropdownKey ? 'selected' : ''; ?>>
<?= $value; ?>
</option>
<?php } ?>
</select>
<?php } ?>
</div>
<?php } ?>
<input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" />
</form>
<button class="js-edit btn btn-dark" href="<?= $this->url . '/' . $this->model->id; ?>">Edit</button>
<div class="pb-5"></div>
</div>
</div>
</div>

71
app/views/admin/crud/index.php

@ -0,0 +1,71 @@
<div class="row">
<div class="col-12">
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<h3><?= $this->title; ?></h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<?php foreach($this->attributes as $attribute) { ?>
<?php
if ($attribute[3] == 1) { continue; }
$title = ucfirst($attribute[0]);
$title = str_replace('_', ' ', $title);
?>
<th><?= $title; ?></th>
<?php } ?>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach($this->rows as $key => $row) { ?>
<tr>
<td>
<a href="<?= $this->url . '/' . $row['id']; ?>">
<?= $key + 1; ?>
</a>
</td>
<?php foreach($this->attributes as $attribute) { ?>
<?php
// Skip filtered
if ($attribute[3] == 1) { continue; }
?>
<td>
<?php $value = $row[$attribute[0]]; ?>
<?php if ($attribute[1] == 'checkbox' && is_numeric($value)) { ?>
<i class="fa <?= $value ? 'fa-check text-success' : 'fa-times text-danger'; ?>"></i>
<?php } else { ?>
<?= ($this->escape)(substr($value, 0, 47)); ?>
<?= strlen($value) > 47 ? '...' : ''; ?>
<?php } ?>
</td>
<?php } ?>
<td>
<a href="<?= $this->url . '/' . $row['id']; ?>/edit">
<i class="fa fa-pencil" aria-hidden="true"></i>
</a>
<a class="js-delete" href="<?= $this->url . '/' . $row['id']; ?>" data-token="<?= $this->csrfToken; ?>">
<i class="fa fa-trash text-danger" aria-hidden="true"></i>
</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?= $this->partial('../app/views/partials/pagination.php'); ?>
<div class="row">
<div class="col-12">
<a class="btn btn-dark" href="<?= $this->url ?>/create">New <?= $this->title; ?></a>
</div>
</div>
<div class="pb-5"></div>
</div>
</div>
</div>

40
app/views/admin/crud/show.php

@ -0,0 +1,40 @@
<div class="row">
<div class="col-12">
<div class="content shadow p-4 mb-4">
<h3><?= _exists([$this->model->title]) ? ($this->escape)($this->model->title) : 'Show'; ?></h3>
<table class="table table-bordered table-striped">
<thead>
<tr class="d-flex">
<th class="col-4">Column</th>
<th class="col-8">Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->attributes as $attribute) { ?>
<?php
// Skip filtered
if ($attribute[3] == 1) { continue; }
$title = ucfirst($attribute[0]);
$title = str_replace('_', ' ', $title);
?>
<tr class="d-flex">
<td class="col-4"><?= $title; ?></td>
<td class="col-8">
<?php $value = $this->model->{$attribute[0]}; ?>
<?php if ($attribute[1] == 'checkbox' && is_numeric($value)) { ?>
<i class="fa <?= $value ? 'fa-check text-success' : 'fa-times text-danger'; ?>"></i>
<?php } else { ?>
<?= ($this->escape)($value); ?>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<div class="pb-5"></div>
</div>
</div>
</div>

13
app/views/admin/index.php

@ -0,0 +1,13 @@
<div class="content shadow p-4 mb-4">
<div id="home">
<h3>
Welcome back,
</h3>
<h3>
<?= $this->user->first_name; ?>
<?= !empty($this->user->last_name) ? ' ' . $this->user->last_name : ''; ?>
</h3>
</div>
<div class="pb-5"></div>
</div>

90
app/views/admin/media.php

@ -0,0 +1,90 @@
<div class="row">
<div class="col-12">
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<h3>Media file manager</h3>
<table class="table table-bordered table-striped">
<thead>
<th scope="col">Thumbnail</th>
<th scope="col">URL</th>
<th scope="col">Uploaded by</th>
<th scope="col">Modifier</th>
</thead>
<tbody>
<?php foreach($this->media as $media) { ?>
<?php
$filename = $media['filename'] . '.' . $media['extension'];
?>
<tr>
<td>
<?php if (in_array($media['extension'], ['jpg', 'jpeg', 'png', 'gif'])) { ?>
<img src="<?= $this->fileUrl . $filename; ?>"
title="<?= $filename; ?>" alt="<?= $filename; ?>"
class="img-thumbnail" width="100px">
<?php } else { ?>
No thumbnail available.
<?php } ?>
</td>
<td>
<a href="<?= $this->fileUrl . $filename; ?>" target="_blank">
<?= $this->fileUrl . $filename; ?>
</a>
</td>
<td>
<?php
$idx = array_search( $media['log_id'], array_column($this->log, 'id'));
$idx = array_search($this->log[$idx]['user_id'], array_column($this->user, 'id'));
echo $this->user[$idx]['username'];
?>
</td>
<td>
<a class="js-delete" href="<?= $this->url . '/' . $media['id']; ?>">
<button type="button" class="btn btn-danger">
Delete
</button>
</a>
</td>
</tr>
<?php } ?>
<tr>
<form method="POST" action="<?= $this->url; ?>" enctype="multipart/form-data">
<td colspan="2" class="js-upload p-0 middle">
<input type="file" id="file" class="d-none" name="file[]" required multiple>
<table class="table mb-0">
<thead></thead>
<tbody>
<tr class="bg-transparent">
<td class="border-0 middle text-center">
<i class="fa fa-cloud-upload"></i>
<span class="">Drag files here</span>
</td>
<td class="border-0 middle text-center">
<label for="file" class="btn btn-light border px-3 py-1 mb-0">Select files</label>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<input type="hidden" name="overwrite" value="0">
<input type="checkbox" name="overwrite" value="1"> Overwrite
</td>
<td>
<button type="submit" class="btn btn-success">Upload</button>
</td>
</form>
</tr>
</tbody>
</table>
<?= $this->partial('../app/views/partials/pagination.php'); ?>
<div class="pb-5"></div>
</div>
</div>
</div>

33
app/views/admin/syntax-highlighting.php

@ -0,0 +1,33 @@
<div class="content shadow p-4 mb-4">
<h3>Syntax Highlighting</h3>
<div class="form-group">
<label for="language">Select Language:</label><br>
<!-- https://prismjs.com/#languages-list -->
<select id="syntax-language" name="language">
<option value="c">C</option>
<option value="cpp">C++</option>
<option value="css">CSS</option>
<option value="html">HTML</option>
<option value="javascript">JavaScript</option>
<option value="php">PHP</option>
<option value="python">Python</option>
<option value="shell">Shell</option>
</select>
</div>
<div class="form-group">
<label for="code">Code:</label><br>
<textarea id="syntax-code" name="code" rows="12" cols="1" class="form-control" autofocus></textarea>
</div>
<div class="form-group">
<a id="syntax-highlight" class="btn btn-dark" href="#">Highlight</a>
<a id="syntax-copy" class="btn btn-dark" href="#">Copy</a>
</div>
<div id="syntax-parse"><pre class="line-numbers mb-4"><code class=""></code></pre></div>
<textarea id="syntax-parse-copy" class="admin-hidden"></textarea>
<div class="pb-5"></div>
</div>

44
app/views/content.php

@ -0,0 +1,44 @@
<div class="row">
<?php $size = $this->sideContent ? '8' : '12'; ?>
<div class="col-12 col-md-<?= $size; ?> col-lg-<?= $size; ?>">
<?php foreach ($this->contents as $key => $content) { ?>
<?php if ($content['type'] == '1') { ?>
<div class="<?= !$content['hide_background'] ? 'content shadow p-4' : ''; ?> mb-4">
<?php if ($key === array_key_first($this->contents)) { ?>
<?= $this->partial('../app/views/partials/message.php'); ?>
<?php } ?>
<?php if ($content['hide_title'] == 0) { ?>
<h1><?= ($this->escape)($content['title']); ?></h1>
<?php } ?>
<?= $content['content']; ?>
<?php if ($this->injectView != '') { ?>
<?= $this->partial($this->injectView); ?>
<?php } ?>
<?php if ($key === array_key_last($this->contents)) { ?>
<div class="pb-5"></div>
<?php } ?>
</div>
<?php } ?>
<?php } ?>
</div>
<?php if ($this->sideContent) { ?>
<div class="col-8 col-md-4 col-lg-4">
<?php foreach ($this->contents as $content) { ?>
<?php if ($content['type'] == '2') { ?>
<div class="content content-side shadow p-3 mb-4">
<h3><?= ($this->escape)($content['title']); ?></h3>
<?= $content['content']; ?>
</div>
<?php } ?>
<?php } ?>
</div>
<?php } ?>
</div>

8
app/views/errors/404.php

@ -0,0 +1,8 @@
<div class="content shadow p-4 mb-4">
<div class="navbar-padding">
<p>Error <strong>404</strong>.</p>
<p>requested URL <?= $_SERVER["REQUEST_URI"] ?> was not found on this server.</p>
</div>
<div class="pb-5"></div>
</div>

96
app/views/form.php

@ -0,0 +1,96 @@
<form action="<?= $this->url; ?>" method="post">
<?php $count = 0; ?>
<?php foreach ($this->form->getFields() as $name => $field) { ?>
<?php if ($field[1] == 'comment') { ?>
<div class="form-group">
<?= $field[0]; ?>
</div>
<?php } else if ($field[1] == 'radio') { ?>
<div class="form-group">
<?php $radioCount = 0; ?>
<?php if ($field[0] != '') { ?>
<label><?= $field[0]; ?></label><br>
<?php } ?>
<input type="hidden" name="<?= $name; ?>" value="">
<?php foreach ($field[2] as $value => $label) { ?>
<input type="radio" name="<?= $name; ?>" id="<?= $value; ?>"
<?= strstr($field[3], 'required') ? 'required' : ''; ?>
<?= $this->{$name} == $value || (!isset($this->{$name}) && $radioCount == 0 && $field[0] == '') ? 'checked' : ''; ?>
<?= $count == 0 && $radioCount == 0 ? 'autofocus' : ''; ?>
value="<?= $value; ?>">
<label for="<?= $value; ?>"><?= $label; ?>&nbsp;&nbsp;</label>
<?= $field[0] != '' ? '<br>' : ''; ?>
<?php $radioCount++; ?>
<?php } ?>
</div>
<?php } else if ($field[1] == 'text' || $field[1] == 'email' ||
$field[1] == 'tel' || $field[1] == 'password') { ?>
<div class="form-group">
<label for="<?= $name; ?>"><?= $field[0]; ?></label>
<input type="<?= $field[1]; ?>" name="<?= $name; ?>" id="<?= $name; ?>" class="form-control"
<?= strstr($field[3], 'required') ? 'required' : ''; ?>
<?= _exists($field, 5) ? "pattern='$field[5]'" : ''; ?>
<?= _exists($field, 6) ? "title='$field[6]'" : ''; ?>
<?= strstr($field[3], 'captcha') ? 'autocomplete="off"' : '' ?>
<?= $count == 0 ? 'autofocus' : ''; ?>
value="<?= $this->{$name}; ?>">
<?php if (strstr($field[3], 'captcha')) { ?>
<img src="/img/captcha.jpg" class="img-fluid pt-2">
<?php } ?>
</div>
<?php } else if ($field[1] == 'textarea') { ?>
<div class="form-group">
<label for="<?= $name; ?>"><?= $field[0]; ?></label>
<textarea name="<?= $name; ?>" cols="1" rows="5" id="<?= $name; ?>" class="form-control"
<?= strstr($field[3], 'required') ? 'required' : ''; ?>
<?= $count == 0 ? 'autofocus' : ''; ?>
><?= $this->{$name}; ?></textarea>
</div>
<?php } else if ($field[1] == 'checkbox') { ?>
<div class="form-group form-check">
<?php $checkboxCount = 0; ?>
<input name="<?= $name; ?>" type="hidden" value="0">
<?php foreach ($field[2] as $value => $label) { ?>
<input type="checkbox" name="<?= $name; ?>" id="<?= $value; ?>" class="form-check-input"
<?= strstr($field[3], 'required') ? 'required' : ''; ?>
<?= $this->{$name} == $value ? 'checked' : ''; ?>
<?= $count == 0 && $checkboxCount == 0 ? 'autofocus' : ''; ?>
value="<?= $value; ?>">
<label for="<?= $value; ?>" class="form-check-label"><?= $label; ?></label><br>
<?php $checkboxCount++; ?>
<?php } ?>
</div>
<?php } ?>
<?php $count++; ?>
<?php } ?>
<p class="mb-0">
<?php if (_exists([$this->form->getReset()])) { ?>
<button type="reset" class="btn btn-dark"><?= $this->form->getReset(); ?></button>
<?php } ?>
<button type="submit" class="btn btn-dark"><?= $this->form->getSubmit(); ?></button>
</p>
<input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" />
</form>

49
app/views/layouts/default.php

@ -0,0 +1,49 @@
<?php
use App\Classes\Config;
use App\Classes\User;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="<?= $this->metaDescription; ?>">
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<?php if ($this->adminSection) { ?>
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/codemirror.min.css" rel="stylesheet" integrity="sha384-K/FfhVUneW5TdId1iTRDHsOHhLGHoJekcX6UThyJhMRctwRxlL3XmSnTeWX2k3Qe" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/theme/tomorrow-night-eighties.min.css" rel="stylesheet" integrity="sha384-zTCWZYMg963D68otcZCn2SQ2SBwih+lCwYxWqvx6xH8/Wt6+NN+giHIvcMpN4cPD" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.15/dist/summernote-bs4.min.css" rel="stylesheet" integrity="sha384-JNFvp1YkK/DsvVg1KxCYX/jfLcrqFkwUE1+4kt+Zpkhvfeetb13H+j2ZZhrTJwRy" crossorigin="anonymous">
<?php } ?>
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/themes/prism-tomorrow.min.css" rel="stylesheet" integrity="sha384-rG0ypOerdVJPawfZS6juq8t8GVE9oCCPJbOXV/bF+e61zYW9Ib6u9WwSbTOK6CKA" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" integrity="sha384-n3/UuPVL3caytud/opHXuyFoezGp2oAUB0foYaCAIs2QwGv/nV0kULHS2WAaJuxR" crossorigin="anonymous">
<link href="<?= Config::c('APP_URL'); ?>/css/style.css?v=<?= rand(); ?>" rel="stylesheet">
<title><?= ($this->escape)($this->pageTitle); ?><?= $this->pageTitle != '' ? ' - ' : '' ?>Rick van Vonderen</title>
<link rel="icon" type="image/png" href="<?= Config::c('APP_URL'); ?>/img/favicon.png">
</head>
<body>
<?= $this->partial('../app/views/partials/header.php'); ?>
<div class="container">
<div class="row">
<div class="js-main-content col-<?= User::getToggle() ? '9' : '12'; ?>">
<?= $this->yieldView(); ?>
<?= $this->partial('../app/views/partials/footer.php'); ?>
</div>
<?php if ($this->loggedIn) { ?>
<?= $this->partial('../app/views/partials/admin.php'); ?>
<?php } ?>
</div>
<div id="isMobile" class="d-md-none d-lg-none d-xl-none"></div>
</div>
<?= $this->partial('../app/views/partials/script.php'); ?>
</body>
</html>

27
app/views/login.php

@ -0,0 +1,27 @@
<?php
use App\Classes\Config;
use App\Classes\User;
?>
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<?php if (User::check()) { ?>
You&apos;re already logged in. Click to <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/logout">log out</a>.
<?php if ($this->redirectURL) { ?>
<script type="text/javascript">
setTimeout(function() {
window.location.replace("<?= $this->redirectURL; ?>");
}, 3000);
</script>
<?php } ?>
<?php } else { ?>
<h1>Sign in</h1>
<?= $this->partial($this->injectView); ?>
<br>
<a href="<?= Config::c('APP_URL'); ?>/reset-password">Forgot password</a>?
<?php } ?>
<div class="pb-5"></div>
</div>

31
app/views/partials/admin.php

@ -0,0 +1,31 @@
<div class="js-admin-menu admin-menu col-3 <?= \App\Classes\User::getToggle() ? 'd-block' : 'd-none'; ?>">
<div class="content admin-content shadow p-4">
<a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin"><h4>Admin panel</h4></a>
<hr>
<h5>Navigation</h5>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/section">Section</a>
<br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/page">Page</a>
<hr>
<h5>Link</h5>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/section-has-content">SectionHasContent</a>
<br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/page-has-content">PageHasContent</a>
<hr>
<h5>Content</h5>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/content">Content</a>
<br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/media">Media</a>
<br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/syntax-highlighting">Syntax Highlighting</a>
<hr>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/logout">Log out</a>
</div>
</div>
<div class="js-admin-toggle admin-toggle fixed-bottom btn btn-dark">
<i class="fa fa-bars" aria-hidden="true"></i>
</div>

11
app/views/partials/footer.php

@ -0,0 +1,11 @@
<?php
use App\Classes\Config;
?>
<footer class="mb-4">
<div class="row">
<div class="col-12 col-lg-12">
&copy; <?= date('Y'); ?> Rick van Vonderen
</div>
</div>
</footer>

54
app/views/partials/header.php

@ -0,0 +1,54 @@
<?php
use App\Classes\Config;
?>
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark shadow">
<a class="navbar-brand" href="<?= Config::c('APP_URL'); ?>/"><i class="fa fa-home"></i> Home</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<?php foreach ($this->navigation as $section) { ?>
<?php if (count($section) < 3) { continue; } ?>
<?php if (count($section) == 3) { ?>
<a class="nav-link mx-lg-2"
href="<?= Config::c('APP_URL'); ?>/<?= $section[0] . '/' . $section[2][0]; ?>"
><?= ($this->escape)($section[2][1]); ?></a>
<?php continue; ?>
<?php } ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle mx-lg-3 purple" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown">
<?= ($this->escape)($section[1]); ?>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<?php foreach ($section as $key => $page) { ?>
<?php if ($key == 0 || $key == 1) { continue; } ?>
<a class="dropdown-item purple"
href="<?= Config::c('APP_URL'); ?>/<?= $section[0] . '/' . $page[0]; ?>"
><?= ($this->escape)($page[1]); ?></a>
<?php } ?>
</div>
</li>
<?php } ?>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="https://git.riyyi.com/riyyi" target="_blank"><i class="fa fa-coffee"></i> Gitea</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/riyyi" target="_blank"><i class="fa fa-github"></i> GitHub</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://gitlab.com/riyyi" target="_blank"><i class="fa fa-gitlab"></i> GitLab</a>
</li>
</ul>
</div>
</nav>

8
app/views/partials/message.php

@ -0,0 +1,8 @@
<?php if ($this->type != '') { ?>
<div class="alert alert-<?= $this->type; ?> alert-dismissible fade show" role="alert">
<?= $this->message; ?>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<?php } ?>

25
app/views/partials/pagination.php

@ -0,0 +1,25 @@
<div class="row">
<div class="col-12">
<nav class="pb-2" aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item <?= $this->page <= 1 ? 'disabled' : ''; ?>">
<a class="page-link" href="<?= $this->url . '?page=' . ($this->page - 1); ?>" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
<?php for($i = 1; $i <= $this->pages; $i++) { ?>
<li class="page-item <?= $this->page == $i ? 'active' : ''; ?>">
<a class="page-link" href="<?= $this->url . '?page=' . $i; ?>"><?= $i; ?></a>
</li>
<?php } ?>
<li class="page-item <?= $this->page >= $this->pages ? 'disabled' : ''; ?>">
<a class="page-link" href="<?= $this->url . '?page=' . ($this->page + 1); ?>" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
</div>
</div>

28
app/views/partials/script.php

@ -0,0 +1,28 @@
<?php
use App\Classes\Config;
?>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<?php if ($this->adminSection) { ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/codemirror.min.js" integrity="sha384-v0AyVIspkm1uirQ6Nsmr90ScryXAWf+xv0DlNrNo1BkSY3sqr7tsUKLZyTMHqy2G" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/clike/clike.min.js" integrity="sha384-7LfH34Nu+Rz2rkqh9L9Okzi17vt5/sX9YZvMjHgRhwH94EdtzCaQ5SpIkXfn80/2" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/xml/xml.min.js" integrity="sha384-Fohp26Rl1xXN67RS6nTZ+s+DEgvUoMZGBTzPuAwW/tjDMtaL27W3b4Irtxtf9KPw" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/javascript/javascript.min.js" integrity="sha384-QPVXEBl5e4kOafHL/KBYAhkf9c7iaD6svR+RGt92QYCbdJiKcmfC4weMMPLif77e" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/css/css.min.js" integrity="sha384-3UHVlVGKahbp3CTUk/YC+ICeNAx8Na6vIIrmH18NimXtGR/zF4Ce4F1d+gNPFh9a" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/htmlmixed/htmlmixed.min.js" integrity="sha384-C4oV1iL5nO+MmIRm+JoD2sZ03wIafv758LRO92kyg7AFvpvn8oQAra4g3mrw5+53" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/php/php.min.js" integrity="sha384-WFPjfiKs+lVB1vp9fq9wEhsgi/5Z86jVLyaGMe2wnxa/3o8SEUCySeqglAaRqxnI" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/shell/shell.min.js" integrity="sha384-VzBcr5K83ctjdWufr/bGFmHLB7LbEhdN5dhKWvE3Q/H5Qfvz9dKPJgfHXeaq74da" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/mode/python/python.min.js" integrity="sha384-Wh2d9/yLlLvOQErKI4FDPRJXSklc6GlymSMyq37kJcVmfB73bJ33OqA2TpVHBisf" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/keymap/vim.min.js" integrity="sha384-MTJMQ129Rzid0TmDEIKZomitzagfahspgvwEWmm1s2ReXw8Evv5NTVf77JlmAOOM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.15/dist/summernote-bs4.min.js" integrity="sha384-sH3/pYu1soe3hR5e4aLT8/9wusU2x7Xt10VNfJ6n0VbsUkmkPf+3jI7So6USyROv" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/components/prism-core.min.js" data-manual integrity="sha384-BhpFhn+hhQohKi2ygOAJsVKyJu2q+QYydLKC7rqeMZLyEJFQl1DZieRX4/WxKUsJ" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js" integrity="sha384-FIz0ezhzXpErvkjCsQ+74Je6cuWUBoVubidnSGt498wjjEBaAV27BAISa0/GaAFq" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/plugins/line-numbers/prism-line-numbers.min.js" integrity="sha384-xktrwc/DkME39VrlkNS1tFEeq/S0JFbc8J9Q8Bjx7Xy16Z3NnmUi+94RuffrOQZR" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/plugins/highlight-keywords/prism-highlight-keywords.min.js" integrity="sha384-Rk2xv6YOAfQH8z3ZAK37pgnQihXfgkER8B5EYhoFc+mMNPzf+t7g2J9U74FAvy2T" crossorigin="anonymous"></script>
<script src="<?= Config::c('APP_URL'); ?>/js/app.js?v=<?= rand(); ?>"></script>
<?php } ?>
<script src="<?= Config::c('APP_URL'); ?>/js/site.js?v=<?= rand(); ?>"></script>

14
app/views/reset-password.php

@ -0,0 +1,14 @@
<div class="content shadow p-4 mb-4">
<?= $this->partial('../app/views/partials/message.php'); ?>
<?php if (!$this->newPassword) { ?>
<h1>Reset password</h1>
<p>Please fill in one of the fields.</p>
<?php } else { ?>
<h1>Set new password</h1>
<?php } ?>
<?= $this->partial($this->injectView); ?>
<div class="pb-5"></div>
</div>

20
composer.json

@ -0,0 +1,20 @@
{
"name": "riyyi/website",
"description": "Riyyi's website.",
"type": "project",
"autoload": {
"files": [
"app/helper.php"
],
"psr-4": {
"App\\Classes\\": "app/classes/",
"App\\Controllers\\": "app/controllers/",
"App\\Model\\": "app/model/",
"App\\Traits\\": "app/traits/"
}
},
"require": {
"klein/klein": "^2.1",
"txthinking/mailer": "^2.0"
}
}

17
config.php.example

@ -0,0 +1,17 @@
<?php
return [
'APP_NAME' => '',
'APP_URL' => '',
'DB_HOST' => '',
'DB_NAME' => '',
'DB_USERNAME' => '',
'DB_PASSWORD' => '',
'MAIL_HOST' => '',
'MAIL_PORT' => '',
'MAIL_NAME' => '',
'MAIL_USERNAME' => '',
'MAIL_PASSWORD' => '',
];

BIN
doc/db.mwb

Binary file not shown.

9
public/.htaccess

@ -0,0 +1,9 @@
Options -MultiViews -Indexes +FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [NC,L]

117
public/css/style.css

@ -0,0 +1,117 @@
/* General */
/*----------------------------------------*/
html {
font-family: "Segoe UI", "DejaVu Sans", sans-serif;
}
body {
background-color: #f4f4f4;
}
table td.middle {
vertical-align: middle;
}
/* Navigation */
/*----------------------------------------*/
nav.shadow {
box-shadow: 0 .25rem .5rem rgba(0,0,0,.28) !important;
}
/* Main content */
/*----------------------------------------*/
.container {
margin: 56px auto 0 auto;
}
.content {
position: relative;
/* height: calc(100% - 56px); */
min-height: calc(100vh - 80px);
padding: 20px 20px 50px 20px;
background-color: #fff;
}
#home {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.padding-border {
padding: 10px 15px 10px 15px;
}
.padding-border-col {
padding: 10px 15px 0px 0px;
}
.pointer {
cursor: pointer;
}
/* Admin content */
/*----------------------------------------*/
.admin-toggle {
left: auto;
margin: 10px 15px;
padding: 5px 10px;
border-radius: 5px;
color: #ffffff;
}
.admin-menu {
position: fixed;
top: 0;
right: 0;
padding-right: 0px;
z-index: 1030;
}
.admin-content {
min-height: 100vh;
}
.admin-hidden {
position: absolute;
top: 0;
left: -9999px;
}
.admin-upload-dragover {
background-color: rgba(0,0,0,0.1);
}
/* Cursor color in normal mode */
.CodeMirror.cm-fat-cursor .CodeMirror-cursor {
background: white;
}
/* Cursor color in visual mode */
.CodeMirror .cm-animate-fat-cursor {
background-color: white;
animation: none !important;
}
/* Footer */
/*----------------------------------------*/
footer {
position: absolute;
bottom: 0;
width: calc(100% - 30px);
height: 50px;
padding-top: 10px;
text-align: center;
color: #909090;
}

BIN
public/fonts/captcha.otf

Binary file not shown.

BIN
public/fonts/fontawesome-webfont.eot

Binary file not shown.

2671
public/fonts/fontawesome-webfont.svg

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

BIN
public/fonts/fontawesome-webfont.ttf

Binary file not shown.

BIN
public/fonts/fontawesome-webfont.woff

Binary file not shown.

BIN
public/fonts/fontawesome-webfont.woff2

Binary file not shown.

BIN
public/img/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

10
public/index.php

@ -0,0 +1,10 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/../vendor/autoload.php';
\App\Classes\Session::start();
\App\Classes\Config::load();
\App\Classes\Router::fire();

226
public/js/app.js

@ -0,0 +1,226 @@
$(document).ready(function() {
// Confirm to submit
$('.js-confirm').on('click', function() {
return confirm("Are you sure you want to continue?");
});
// Confirm to delete
$('.js-delete').on('click', function(event) {
event.preventDefault();
var csrfToken = $(this).attr('data-token');
if (confirm('Are you sure you want to continue?')) {
$.ajax({
url: $(this).attr('href'),
type: 'DELETE',
data: { _token: csrfToken },
success: function(data) {
window.location.reload();
}
});
}
});
// Edit
$('.js-edit').on('click', function(event) {
var href = $(this).attr('href');
$.ajax({
url: href,
type: "PUT",
data: $('#form-edit').serialize(),
success: function(data, textStatus, jqXHR) {
window.location.href = data;
}
});
});
//------------------------------------------
$('.js-upload')
.on("drag dragstart dragend dragover dragenter dragleave drop", function(e) {
e.preventDefault();
e.stopPropagation();
})
.on("dragover dragenter", function(e) {
$(this).addClass('admin-upload-dragover');
})
.on("dragend dragleave drop", function(e) {
$(this).removeClass('admin-upload-dragover');
})
.on("drop", function(e) {
// Set file input to the dropped files
$(this).find('input[type=file]')[0].files = e.originalEvent.dataTransfer.files;
updateUploadLabel($(this));
})
.change(function(e) {
updateUploadLabel($(this));
});
function updateUploadLabel(element) {
var files = element.find('input[type=file]')[0].files;
var text = element.find('span');
// Update label text
if (files.length == 1) {
text.html(files[0].name);
}
else {
text.html(files.length + ' files selected');
// Set tooltip
var fileNames = '';
Array.from(files).forEach(file => { fileNames += file.name + '\n'; });
element.prop('title', fileNames);
}
}
//------------------------------------------
// CodeMirror editor
var editor;
var editorOptions = {
lineSeparator: '\n',
theme: 'tomorrow-night-eighties',
mode: 'text/html',
indentUnit: 4,
lineNumbers: true,
lineWrapping: true,
cursorBlinkRate: 0,
keyMap: 'vim',
};
// Copy editor selection to clipboard on <C-c>
function registerEditorCopy() {
editor.on('vim-keypress', function(e) {
if (e === '<C-c>' && editor.state.vim.visualMode === true) {
document.execCommand("copy");
}
});
}
// Enable CodeMirror Editor
$.fn.codeMirror = function() {
editor = CodeMirror.fromTextArea($(this)[0], editorOptions);
editor.setSize('100%', '500');
registerEditorCopy();
return editor;
}
$('textarea#syntax-code').codeMirror().setOption('mode', 'text/x-csrc');
// Enable WYSIWYG Editor
$('textarea#summernote').summernote({
tabDisable: true,
tabsize: 4,
minHeight: 450,
maxHeight: null,
focus: true,
fontNames: [
'Helvetica', 'Arial', 'Verdana', 'Trebuchet MS', 'sans-serif',
'Georgia', 'Times New Roman', 'Courier New', 'monospace'
],
fontNamesIgnoreCheck: [],
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'italic', 'strikethrough', 'clear']],
['fontname', ['fontname']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
callbacks: {
onInit: function() {
$("#summernote").summernote('codeview.activate');
editor = $('.CodeMirror')[0].CodeMirror;
registerEditorCopy();
}
},
prettifyHtml: false,
codemirror: editorOptions,
});
var syntaxLanguages = {
'c': 'text/x-csrc',
'cpp': 'text/x-c++src',
'css': 'text/css',
'html': 'text/html',
'javascript': 'text/javascript',
'php': 'application/x-httpd-php',
'python': 'text/x-python',
'shell': 'text/x-sh',
};
// Syntax Language selection
$('#syntax-language').on('change', function() {
// Set the editor language mode
editor.setOption('mode', syntaxLanguages[this.value]);
// Set the language class
var parse = $('code');
parse.removeClass().addClass('language-' + this.value);
})
.trigger('change');
// Syntax Highlight
$('#syntax-highlight').on('click', function() {
// Set the code
var parse = $('code');
parse.text(editor.getValue());
// Highlight the <code> DOM element
Prism.highlightAll();
});
// Copy highlighted syntax to the clipboard
$('#syntax-copy').on('click', function() {
// Copy text into hidden textarea
var parse = $('div#syntax-parse').html();
var copy = $('textarea#syntax-parse-copy');
copy.val(parse);
// Select the text field
copy = copy[0];
copy.select();
copy.setSelectionRange(0, copy.value.length);
// Copy the text inside the field to the clipboard
document.execCommand("copy");
alert("Copied to the clipboard");
});
//------------------------------------------
// Hotkeys
$(window).keydown(function(e) {
// Save form
if (e.ctrlKey && e.keyCode == 83) { // Ctrl + S
e.preventDefault();
// Codeview needs to be deactived before saving
$('#summernote').summernote('codeview.deactivate');
$('.js-edit').click();
}
// Toggle codeview
if (e.ctrlKey && e.keyCode == 71) { // Ctrl + G
e.preventDefault();
$('#summernote').summernote('codeview.toggle');
}
});
});
// @Todo
// - Look at converting .ajax() into the JS fetch API (native)

83
public/js/site.js

@ -0,0 +1,83 @@
$(document).ready(function() {
//------------------------------------------//
$.fn.inViewport = function() {
var elementTop = $(this).offset().top;
var elementBottom = elementTop + $(this).outerHeight();
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
return elementBottom > viewportTop && elementTop < viewportBottom;
}
//------------------------------------------//
// Image hover mouseenter
$(".js-img-hover").mouseenter(function(event) {
var width = $(window).width() - event.clientX - 1 + "px";
if (!$("#isMobile").is(":hidden")) {
width = "100%";
}
var url = $(this).data("img-hover");
var divImg = $("<div id='img-hover' style='z-index: 9999; position: fixed; top: 0px; right: 0px;'>" +
"<img style='max-width: " + width + "; height auto;' src=" + url + "></div>");
divImg.appendTo("body");
});
// Image hover mouseleave
$(".js-img-hover").mouseleave(function() {
$("#img-hover").remove();
});
// Lazy load video's
$(window).on('resize scroll', function() {
$('.js-video').each(function(index) {
if (!$(this).inViewport()) {
return true;
}
$(this).find('source').each(function() {
var source = $(this).attr('data-src');
if (source === undefined) {
return true;
}
$(this).attr("src", source);
var video = this.parentElement;
video.load();
video.play();
$(this).removeAttr('data-src');
});
});
});
$(window).trigger('scroll');
// Only 1 field is required
var $inputs = $('input[name=reset-password-username],input[name=reset-password-email]');
$inputs.on('input', function() {
// Set the required property of the other input to false if this input is not empty.
$inputs.not(this).prop('required', !$(this).val().length);
});
// List toggle
$('.js-toggle').click(function() {
$(this).next("ul").toggle(400);
});
// Admin panel toggle
$('.js-admin-toggle').click(function() {
$.get('/admin/toggle').done(function(data) {
if (data == '1') {
$('.js-admin-menu').removeClass('d-none').addClass('d-block');
$('.js-main-content').removeClass('col-12').addClass('col-9');
}
else if (data == '0') {
$('.js-admin-menu').removeClass('d-block').addClass('d-none');
$('.js-main-content').removeClass('col-9').addClass('col-12');
}
});
});
});

0
public/media/.gitinclude

26
route.php

@ -0,0 +1,26 @@
<?php
use \App\Classes\Router;
Router::resource('/admin/section', 'CrudController');
Router::resource('/admin/page', 'CrudController');
Router::resource('/admin/content', 'CrudController');
Router::resource('/admin/section-has-content', 'CrudController');
Router::resource('/admin/page-has-content', 'CrudController');
Router::resource('/admin/media', 'MediaController');
// Basic routes
return [
// URL, controller, action, view/title/description
['/', 'IndexController', '', ''],
['/img/captcha.jpg', 'IndexController', 'captcha', ''],
['/sitemap.xml', 'IndexController', 'sitemap', ''],
['/login', 'LoginController', 'login', ['', 'Sign in', '']],
['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']],
['/logout', 'LoginController', 'logout', ''],
['/admin', 'AdminController', '', ''],
['/admin/toggle', 'AdminController', 'toggle', ''],
['/admin/syntax-highlighting', 'AdminController', 'syntax', ''],
['/test', 'TestController', '', ''],
// ["", "", "", ""],
];

52
sync.sh

@ -0,0 +1,52 @@
#!/bin/sh
# Syncs website contents to remote server
# Depends: rsync, ssh
# User-config---------------------------
config="./syncconfig.sh"
# ------------------------------------------
if [ -f "$config" ]; then
. "$config"
else
echo "Please configure the $config file"
exit 1
fi
if [ "$(dirname "$0")" != "." ]; then
echo "Please run this script from the directory it resides."
exit 1
fi
directories=\
"app
public"
source="."
destination="$user@$ip:$remote_path"
echo "Syncing files to remote.."
# Directory syncing
for dir in $directories; do
cd "$dir" || exit 1
rsync -ar --delete-after --rsh="ssh -p $port" \
--out-format="- %n" \
--exclude="media" \
"$source" "$destination/$dir"
cd ..
done
# File syncing
rsync -a --rsh="ssh -p $port" \
--out-format="- %n" \
--include="composer.json" \
`# --include="config.php"` \
--include="route.php" \
--exclude="*" \
"$source" "$destination"
echo "Synced all files to remote."

7
syncconfig.sh.example

@ -0,0 +1,7 @@
#!/bin/sh
user=""
ip=""
port=
remote_path=""
Loading…
Cancel
Save