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') : [];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user