Initial commit
Couldnt keep the history unfortunately.
This commit is contained in:
@@ -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 '';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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') : [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 & HTML entities
|
||||
foreach ($result as $key => $value) {
|
||||
unset($result[$key]);
|
||||
$result[str_replace('amp;', '', $key)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$_REQUEST = array_merge($_REQUEST, $result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 . '/';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 (?)
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class LogModel extends Model {
|
||||
protected $table = 'log';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Traits\Log;
|
||||
|
||||
class MediaModel extends Model {
|
||||
|
||||
use Log;
|
||||
|
||||
protected $table = 'media';
|
||||
protected $sort = 'filename';
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class UserModel extends Model {
|
||||
protected $table = 'user';
|
||||
}
|
||||
+178
@@ -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],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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; ?> </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>
|
||||
@@ -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>
|
||||
@@ -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'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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
use App\Classes\Config;
|
||||
?>
|
||||
|
||||
<footer class="mb-4">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-12">
|
||||
© <?= date('Y'); ?> Rick van Vonderen
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -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>
|
||||
@@ -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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<?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">«</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">»</span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user