Riyyi
4 years ago
commit
a8056b66cc
67 changed files with 7759 additions and 0 deletions
@ -0,0 +1,31 @@ |
|||||||
|
# Non-project |
||||||
|
.idea/ |
||||||
|
.directory |
||||||
|
|
||||||
|
# Files |
||||||
|
*.mwb.bak |
||||||
|
config.php |
||||||
|
composer.lock |
||||||
|
syncconfig.sh |
||||||
|
|
||||||
|
# Directories |
||||||
|
/public/* |
||||||
|
!/public/css/ |
||||||
|
/public/css/* |
||||||
|
!/public/fonts/ |
||||||
|
!/public/img/ |
||||||
|
/public/img/* |
||||||
|
!/public/js/ |
||||||
|
/public/js/* |
||||||
|
!/public/media/ |
||||||
|
/public/media/* |
||||||
|
/vendor/* |
||||||
|
|
||||||
|
# Unignore |
||||||
|
!/public/.htaccess |
||||||
|
!/public/css/style.css |
||||||
|
!/public/img/favicon.png |
||||||
|
!/public/index.php |
||||||
|
!/public/js/app.js |
||||||
|
!/public/js/site.js |
||||||
|
!/public/media/.gitinclude |
@ -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'; |
||||||
|
} |
@ -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> |
@ -0,0 +1,20 @@ |
|||||||
|
{ |
||||||
|
"name": "riyyi/website", |
||||||
|
"description": "Riyyi's website.", |
||||||
|
"type": "project", |
||||||
|
"autoload": { |
||||||
|
"files": [ |
||||||
|
"app/helper.php" |
||||||
|
], |
||||||
|
"psr-4": { |
||||||
|
"App\\Classes\\": "app/classes/", |
||||||
|
"App\\Controllers\\": "app/controllers/", |
||||||
|
"App\\Model\\": "app/model/", |
||||||
|
"App\\Traits\\": "app/traits/" |
||||||
|
} |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"klein/klein": "^2.1", |
||||||
|
"txthinking/mailer": "^2.0" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
return [ |
||||||
|
'APP_NAME' => '', |
||||||
|
'APP_URL' => '', |
||||||
|
|
||||||
|
'DB_HOST' => '', |
||||||
|
'DB_NAME' => '', |
||||||
|
'DB_USERNAME' => '', |
||||||
|
'DB_PASSWORD' => '', |
||||||
|
|
||||||
|
'MAIL_HOST' => '', |
||||||
|
'MAIL_PORT' => '', |
||||||
|
'MAIL_NAME' => '', |
||||||
|
'MAIL_USERNAME' => '', |
||||||
|
'MAIL_PASSWORD' => '', |
||||||
|
]; |
Binary file not shown.
@ -0,0 +1,9 @@ |
|||||||
|
Options -MultiViews -Indexes +FollowSymLinks |
||||||
|
|
||||||
|
RewriteEngine On |
||||||
|
RewriteBase / |
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f |
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d |
||||||
|
|
||||||
|
RewriteRule ^(.*)$ index.php/$1 [NC,L] |
@ -0,0 +1,117 @@ |
|||||||
|
/* General */ |
||||||
|
/*----------------------------------------*/ |
||||||
|
|
||||||
|
html { |
||||||
|
font-family: "Segoe UI", "DejaVu Sans", sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
background-color: #f4f4f4; |
||||||
|
} |
||||||
|
|
||||||
|
table td.middle { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
/* Navigation */ |
||||||
|
/*----------------------------------------*/ |
||||||
|
|
||||||
|
nav.shadow { |
||||||
|
box-shadow: 0 .25rem .5rem rgba(0,0,0,.28) !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* Main content */ |
||||||
|
/*----------------------------------------*/ |
||||||
|
|
||||||
|
.container { |
||||||
|
margin: 56px auto 0 auto; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
position: relative; |
||||||
|
/* height: calc(100% - 56px); */ |
||||||
|
min-height: calc(100vh - 80px); |
||||||
|
padding: 20px 20px 50px 20px; |
||||||
|
|
||||||
|
background-color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
#home { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
|
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.padding-border { |
||||||
|
padding: 10px 15px 10px 15px; |
||||||
|
} |
||||||
|
|
||||||
|
.padding-border-col { |
||||||
|
padding: 10px 15px 0px 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.pointer { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
/* Admin content */ |
||||||
|
/*----------------------------------------*/ |
||||||
|
|
||||||
|
.admin-toggle { |
||||||
|
left: auto; |
||||||
|
margin: 10px 15px; |
||||||
|
padding: 5px 10px; |
||||||
|
border-radius: 5px; |
||||||
|
|
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
.admin-menu { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
right: 0; |
||||||
|
padding-right: 0px; |
||||||
|
z-index: 1030; |
||||||
|
} |
||||||
|
|
||||||
|
.admin-content { |
||||||
|
min-height: 100vh; |
||||||
|
} |
||||||
|
|
||||||
|
.admin-hidden { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: -9999px; |
||||||
|
} |
||||||
|
|
||||||
|
.admin-upload-dragover { |
||||||
|
background-color: rgba(0,0,0,0.1); |
||||||
|
} |
||||||
|
|
||||||
|
/* Cursor color in normal mode */ |
||||||
|
.CodeMirror.cm-fat-cursor .CodeMirror-cursor { |
||||||
|
background: white; |
||||||
|
} |
||||||
|
|
||||||
|
/* Cursor color in visual mode */ |
||||||
|
.CodeMirror .cm-animate-fat-cursor { |
||||||
|
background-color: white; |
||||||
|
animation: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* Footer */ |
||||||
|
/*----------------------------------------*/ |
||||||
|
|
||||||
|
footer { |
||||||
|
position: absolute; |
||||||
|
bottom: 0; |
||||||
|
width: calc(100% - 30px); |
||||||
|
height: 50px; |
||||||
|
padding-top: 10px; |
||||||
|
|
||||||
|
text-align: center; |
||||||
|
color: #909090; |
||||||
|
} |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
error_reporting(E_ALL); |
||||||
|
ini_set('display_errors', 1); |
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php'; |
||||||
|
|
||||||
|
\App\Classes\Session::start(); |
||||||
|
\App\Classes\Config::load(); |
||||||
|
\App\Classes\Router::fire(); |
@ -0,0 +1,226 @@ |
|||||||
|
$(document).ready(function() { |
||||||
|
|
||||||
|
// Confirm to submit
|
||||||
|
$('.js-confirm').on('click', function() { |
||||||
|
return confirm("Are you sure you want to continue?"); |
||||||
|
}); |
||||||
|
|
||||||
|
// Confirm to delete
|
||||||
|
$('.js-delete').on('click', function(event) { |
||||||
|
event.preventDefault(); |
||||||
|
|
||||||
|
var csrfToken = $(this).attr('data-token'); |
||||||
|
|
||||||
|
if (confirm('Are you sure you want to continue?')) { |
||||||
|
$.ajax({ |
||||||
|
url: $(this).attr('href'), |
||||||
|
type: 'DELETE', |
||||||
|
data: { _token: csrfToken }, |
||||||
|
success: function(data) { |
||||||
|
window.location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Edit
|
||||||
|
$('.js-edit').on('click', function(event) { |
||||||
|
var href = $(this).attr('href'); |
||||||
|
|
||||||
|
$.ajax({ |
||||||
|
url: href, |
||||||
|
type: "PUT", |
||||||
|
data: $('#form-edit').serialize(), |
||||||
|
success: function(data, textStatus, jqXHR) { |
||||||
|
window.location.href = data; |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
$('.js-upload') |
||||||
|
.on("drag dragstart dragend dragover dragenter dragleave drop", function(e) { |
||||||
|
e.preventDefault(); |
||||||
|
e.stopPropagation(); |
||||||
|
}) |
||||||
|
.on("dragover dragenter", function(e) { |
||||||
|
$(this).addClass('admin-upload-dragover'); |
||||||
|
}) |
||||||
|
.on("dragend dragleave drop", function(e) { |
||||||
|
$(this).removeClass('admin-upload-dragover'); |
||||||
|
}) |
||||||
|
.on("drop", function(e) { |
||||||
|
// Set file input to the dropped files
|
||||||
|
$(this).find('input[type=file]')[0].files = e.originalEvent.dataTransfer.files; |
||||||
|
|
||||||
|
updateUploadLabel($(this)); |
||||||
|
}) |
||||||
|
.change(function(e) { |
||||||
|
updateUploadLabel($(this)); |
||||||
|
}); |
||||||
|
|
||||||
|
function updateUploadLabel(element) { |
||||||
|
var files = element.find('input[type=file]')[0].files; |
||||||
|
var text = element.find('span'); |
||||||
|
|
||||||
|
// Update label text
|
||||||
|
if (files.length == 1) { |
||||||
|
text.html(files[0].name); |
||||||
|
} |
||||||
|
else { |
||||||
|
text.html(files.length + ' files selected'); |
||||||
|
|
||||||
|
// Set tooltip
|
||||||
|
var fileNames = ''; |
||||||
|
Array.from(files).forEach(file => { fileNames += file.name + '\n'; }); |
||||||
|
element.prop('title', fileNames); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
// CodeMirror editor
|
||||||
|
var editor; |
||||||
|
var editorOptions = { |
||||||
|
lineSeparator: '\n', |
||||||
|
theme: 'tomorrow-night-eighties', |
||||||
|
mode: 'text/html', |
||||||
|
indentUnit: 4, |
||||||
|
lineNumbers: true, |
||||||
|
lineWrapping: true, |
||||||
|
cursorBlinkRate: 0, |
||||||
|
keyMap: 'vim', |
||||||
|
}; |
||||||
|
|
||||||
|
// Copy editor selection to clipboard on <C-c>
|
||||||
|
function registerEditorCopy() { |
||||||
|
editor.on('vim-keypress', function(e) { |
||||||
|
if (e === '<C-c>' && editor.state.vim.visualMode === true) { |
||||||
|
document.execCommand("copy"); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Enable CodeMirror Editor
|
||||||
|
$.fn.codeMirror = function() { |
||||||
|
editor = CodeMirror.fromTextArea($(this)[0], editorOptions); |
||||||
|
editor.setSize('100%', '500'); |
||||||
|
registerEditorCopy(); |
||||||
|
|
||||||
|
return editor; |
||||||
|
} |
||||||
|
$('textarea#syntax-code').codeMirror().setOption('mode', 'text/x-csrc'); |
||||||
|
|
||||||
|
// Enable WYSIWYG Editor
|
||||||
|
$('textarea#summernote').summernote({ |
||||||
|
tabDisable: true, |
||||||
|
tabsize: 4, |
||||||
|
minHeight: 450, |
||||||
|
maxHeight: null, |
||||||
|
focus: true, |
||||||
|
fontNames: [ |
||||||
|
'Helvetica', 'Arial', 'Verdana', 'Trebuchet MS', 'sans-serif', |
||||||
|
'Georgia', 'Times New Roman', 'Courier New', 'monospace' |
||||||
|
], |
||||||
|
fontNamesIgnoreCheck: [], |
||||||
|
toolbar: [ |
||||||
|
['style', ['style']], |
||||||
|
['font', ['bold', 'underline', 'italic', 'strikethrough', 'clear']], |
||||||
|
['fontname', ['fontname']], |
||||||
|
['color', ['color']], |
||||||
|
['para', ['ul', 'ol', 'paragraph']], |
||||||
|
['table', ['table']], |
||||||
|
['insert', ['link', 'picture', 'video']], |
||||||
|
['view', ['fullscreen', 'codeview', 'help']], |
||||||
|
], |
||||||
|
callbacks: { |
||||||
|
onInit: function() { |
||||||
|
$("#summernote").summernote('codeview.activate'); |
||||||
|
|
||||||
|
editor = $('.CodeMirror')[0].CodeMirror; |
||||||
|
registerEditorCopy(); |
||||||
|
} |
||||||
|
}, |
||||||
|
prettifyHtml: false, |
||||||
|
codemirror: editorOptions, |
||||||
|
}); |
||||||
|
|
||||||
|
var syntaxLanguages = { |
||||||
|
'c': 'text/x-csrc', |
||||||
|
'cpp': 'text/x-c++src', |
||||||
|
'css': 'text/css', |
||||||
|
'html': 'text/html', |
||||||
|
'javascript': 'text/javascript', |
||||||
|
'php': 'application/x-httpd-php', |
||||||
|
'python': 'text/x-python', |
||||||
|
'shell': 'text/x-sh', |
||||||
|
}; |
||||||
|
|
||||||
|
// Syntax Language selection
|
||||||
|
$('#syntax-language').on('change', function() { |
||||||
|
// Set the editor language mode
|
||||||
|
editor.setOption('mode', syntaxLanguages[this.value]); |
||||||
|
|
||||||
|
// Set the language class
|
||||||
|
var parse = $('code'); |
||||||
|
parse.removeClass().addClass('language-' + this.value); |
||||||
|
}) |
||||||
|
.trigger('change'); |
||||||
|
|
||||||
|
// Syntax Highlight
|
||||||
|
$('#syntax-highlight').on('click', function() { |
||||||
|
// Set the code
|
||||||
|
var parse = $('code'); |
||||||
|
parse.text(editor.getValue()); |
||||||
|
|
||||||
|
// Highlight the <code> DOM element
|
||||||
|
Prism.highlightAll(); |
||||||
|
}); |
||||||
|
|
||||||
|
// Copy highlighted syntax to the clipboard
|
||||||
|
$('#syntax-copy').on('click', function() { |
||||||
|
// Copy text into hidden textarea
|
||||||
|
var parse = $('div#syntax-parse').html(); |
||||||
|
var copy = $('textarea#syntax-parse-copy'); |
||||||
|
copy.val(parse); |
||||||
|
|
||||||
|
// Select the text field
|
||||||
|
copy = copy[0]; |
||||||
|
copy.select(); |
||||||
|
copy.setSelectionRange(0, copy.value.length); |
||||||
|
|
||||||
|
// Copy the text inside the field to the clipboard
|
||||||
|
document.execCommand("copy"); |
||||||
|
|
||||||
|
alert("Copied to the clipboard"); |
||||||
|
}); |
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
// Hotkeys
|
||||||
|
$(window).keydown(function(e) { |
||||||
|
|
||||||
|
// Save form
|
||||||
|
if (e.ctrlKey && e.keyCode == 83) { // Ctrl + S
|
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
// Codeview needs to be deactived before saving
|
||||||
|
$('#summernote').summernote('codeview.deactivate'); |
||||||
|
|
||||||
|
$('.js-edit').click(); |
||||||
|
} |
||||||
|
|
||||||
|
// Toggle codeview
|
||||||
|
if (e.ctrlKey && e.keyCode == 71) { // Ctrl + G
|
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
$('#summernote').summernote('codeview.toggle'); |
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
// @Todo
|
||||||
|
// - Look at converting .ajax() into the JS fetch API (native)
|
@ -0,0 +1,83 @@ |
|||||||
|
$(document).ready(function() { |
||||||
|
|
||||||
|
//------------------------------------------//
|
||||||
|
|
||||||
|
$.fn.inViewport = function() { |
||||||
|
var elementTop = $(this).offset().top; |
||||||
|
var elementBottom = elementTop + $(this).outerHeight(); |
||||||
|
|
||||||
|
var viewportTop = $(window).scrollTop(); |
||||||
|
var viewportBottom = viewportTop + $(window).height(); |
||||||
|
|
||||||
|
return elementBottom > viewportTop && elementTop < viewportBottom; |
||||||
|
} |
||||||
|
|
||||||
|
//------------------------------------------//
|
||||||
|
|
||||||
|
// Image hover mouseenter
|
||||||
|
$(".js-img-hover").mouseenter(function(event) { |
||||||
|
var width = $(window).width() - event.clientX - 1 + "px"; |
||||||
|
if (!$("#isMobile").is(":hidden")) { |
||||||
|
width = "100%"; |
||||||
|
} |
||||||
|
var url = $(this).data("img-hover"); |
||||||
|
var divImg = $("<div id='img-hover' style='z-index: 9999; position: fixed; top: 0px; right: 0px;'>" + |
||||||
|
"<img style='max-width: " + width + "; height auto;' src=" + url + "></div>"); |
||||||
|
divImg.appendTo("body"); |
||||||
|
}); |
||||||
|
|
||||||
|
// Image hover mouseleave
|
||||||
|
$(".js-img-hover").mouseleave(function() { |
||||||
|
$("#img-hover").remove(); |
||||||
|
}); |
||||||
|
|
||||||
|
// Lazy load video's
|
||||||
|
$(window).on('resize scroll', function() { |
||||||
|
$('.js-video').each(function(index) { |
||||||
|
if (!$(this).inViewport()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
$(this).find('source').each(function() { |
||||||
|
var source = $(this).attr('data-src'); |
||||||
|
if (source === undefined) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
$(this).attr("src", source); |
||||||
|
var video = this.parentElement; |
||||||
|
video.load(); |
||||||
|
video.play(); |
||||||
|
$(this).removeAttr('data-src'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
$(window).trigger('scroll'); |
||||||
|
|
||||||
|
// Only 1 field is required
|
||||||
|
var $inputs = $('input[name=reset-password-username],input[name=reset-password-email]'); |
||||||
|
$inputs.on('input', function() { |
||||||
|
// Set the required property of the other input to false if this input is not empty.
|
||||||
|
$inputs.not(this).prop('required', !$(this).val().length); |
||||||
|
}); |
||||||
|
|
||||||
|
// List toggle
|
||||||
|
$('.js-toggle').click(function() { |
||||||
|
$(this).next("ul").toggle(400); |
||||||
|
}); |
||||||
|
|
||||||
|
// Admin panel toggle
|
||||||
|
$('.js-admin-toggle').click(function() { |
||||||
|
$.get('/admin/toggle').done(function(data) { |
||||||
|
if (data == '1') { |
||||||
|
$('.js-admin-menu').removeClass('d-none').addClass('d-block'); |
||||||
|
$('.js-main-content').removeClass('col-12').addClass('col-9'); |
||||||
|
} |
||||||
|
else if (data == '0') { |
||||||
|
$('.js-admin-menu').removeClass('d-block').addClass('d-none'); |
||||||
|
$('.js-main-content').removeClass('col-9').addClass('col-12'); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
@ -0,0 +1,26 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use \App\Classes\Router; |
||||||
|
|
||||||
|
Router::resource('/admin/section', 'CrudController'); |
||||||
|
Router::resource('/admin/page', 'CrudController'); |
||||||
|
Router::resource('/admin/content', 'CrudController'); |
||||||
|
Router::resource('/admin/section-has-content', 'CrudController'); |
||||||
|
Router::resource('/admin/page-has-content', 'CrudController'); |
||||||
|
Router::resource('/admin/media', 'MediaController'); |
||||||
|
|
||||||
|
// Basic routes |
||||||
|
return [ |
||||||
|
// URL, controller, action, view/title/description |
||||||
|
['/', 'IndexController', '', ''], |
||||||
|
['/img/captcha.jpg', 'IndexController', 'captcha', ''], |
||||||
|
['/sitemap.xml', 'IndexController', 'sitemap', ''], |
||||||
|
['/login', 'LoginController', 'login', ['', 'Sign in', '']], |
||||||
|
['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']], |
||||||
|
['/logout', 'LoginController', 'logout', ''], |
||||||
|
['/admin', 'AdminController', '', ''], |
||||||
|
['/admin/toggle', 'AdminController', 'toggle', ''], |
||||||
|
['/admin/syntax-highlighting', 'AdminController', 'syntax', ''], |
||||||
|
['/test', 'TestController', '', ''], |
||||||
|
// ["", "", "", ""], |
||||||
|
]; |
@ -0,0 +1,52 @@ |
|||||||
|
#!/bin/sh |
||||||
|
|
||||||
|
# Syncs website contents to remote server |
||||||
|
# Depends: rsync, ssh |
||||||
|
|
||||||
|
# User-config--------------------------- |
||||||
|
|
||||||
|
config="./syncconfig.sh" |
||||||
|
|
||||||
|
# ------------------------------------------ |
||||||
|
|
||||||
|
if [ -f "$config" ]; then |
||||||
|
. "$config" |
||||||
|
else |
||||||
|
echo "Please configure the $config file" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
if [ "$(dirname "$0")" != "." ]; then |
||||||
|
echo "Please run this script from the directory it resides." |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
directories=\ |
||||||
|
"app |
||||||
|
public" |
||||||
|
|
||||||
|
source="." |
||||||
|
destination="$user@$ip:$remote_path" |
||||||
|
|
||||||
|
echo "Syncing files to remote.." |
||||||
|
|
||||||
|
# Directory syncing |
||||||
|
for dir in $directories; do |
||||||
|
cd "$dir" || exit 1 |
||||||
|
rsync -ar --delete-after --rsh="ssh -p $port" \ |
||||||
|
--out-format="- %n" \ |
||||||
|
--exclude="media" \ |
||||||
|
"$source" "$destination/$dir" |
||||||
|
cd .. |
||||||
|
done |
||||||
|
|
||||||
|
# File syncing |
||||||
|
rsync -a --rsh="ssh -p $port" \ |
||||||
|
--out-format="- %n" \ |
||||||
|
--include="composer.json" \ |
||||||
|
`# --include="config.php"` \ |
||||||
|
--include="route.php" \ |
||||||
|
--exclude="*" \ |
||||||
|
"$source" "$destination" |
||||||
|
|
||||||
|
echo "Synced all files to remote." |
Loading…
Reference in new issue