Compare commits

...
31 Commits
Author SHA1 Message Date
Riyyi 98edad5bf7 Index: Dont show deprecated warnings 2024-12-14 16:02:35 +01:00
Riyyi 91de0f1462 Controllers: Add RSS feed from the blog posts 2022-12-09 11:22:54 +01:00
Riyyi dfdcd992ad Views: Make dropdown placeholder non-selectable 2022-12-03 16:07:35 +01:00
Riyyi f7c6276a7a Controllers+Views: Move tag splitting logic to controller 2022-12-03 13:02:28 +01:00
Riyyi 0138a14c29 Controllers+JS: Change the blog search to a POST request 2022-12-02 11:51:52 +01:00
Riyyi 2ecc79c714 Controllers: Add option for archived blog searching 2022-11-15 17:40:18 +01:00
Riyyi ee40846bd6 Controllers+JS: Search in blog contents, fix special character search 2022-11-15 17:40:18 +01:00
Riyyi 7beef3a1d5 Everywhere: Add initial cut of the blogging functionality 2022-11-15 17:40:18 +01:00
Riyyi 3b47e0ed71 Model: Query ':' types can reuse parameters 2022-11-15 17:40:18 +01:00
Riyyi 8d576a497b Views: Put footer below the last content block 2022-11-15 17:40:18 +01:00
Riyyi 54eee2416a Router: Combine addRoute() and addBasicRoute() functionality 2022-11-06 12:21:01 +01:00
Riyyi c18c80d732 Router: Add all routes regardless of the URI 2022-11-04 15:33:30 +01:00
Riyyi a0d302daec Router: Simplify respond logic 2022-11-04 15:32:53 +01:00
Riyyi 20a8afa9ce JS: Open .js-img-hover image in a new tab on click 2022-11-04 13:40:46 +01:00
Riyyi d85fe1b75c Views: Update subsource packages 2022-11-04 12:31:17 +01:00
Riyyi ccd2a2776a Views: Add LinkedIn link in the header 2021-12-02 00:24:37 +01:00
Riyyi 5d3962bba4 Model+Controllers: Make failed login attempt check consistent 2021-11-26 15:32:50 +01:00
Riyyi e782ea5979 Controllers: Switch password reset to Tx/Mailer due to required auth 2021-10-22 15:46:49 +02:00
Riyyi 9a99c31d47 Controller: Add CSRF token validation to development toggle 2021-08-30 01:02:50 +02:00
Riyyi d50443b10a Controllers+Views+JS+Route: Implement cache purging 2021-08-30 00:45:57 +02:00
Riyyi b244b625ab Controllers: Implement new HTTP class in development toggle 2021-08-30 00:42:36 +02:00
Riyyi 2311c98257 Classes: Add simple HTTP request/response using curl 2021-08-30 00:27:42 +02:00
Riyyi 9af5f50135 JavaScript: Improve control flow of development mode function 2021-08-27 19:28:50 +02:00
Riyyi d3dedfb09a Views: Improve cache page button placement 2021-08-27 19:13:00 +02:00
Riyyi ef9b1a1998 Helper: Add parameter in _log() to disable die() after printing 2021-08-27 16:19:00 +02:00
Riyyi 077e4c434c Everywhere: Add config and cache page
Add config database table and enable crud page.
Also add cache controller, model and view and routes for these pages.

Make the JavaScript display a message when changing the state of
development mode.
2021-08-27 16:15:03 +02:00
Riyyi 2dee309279 Model: Add updateOrCreate function 2021-08-27 12:24:00 +02:00
Riyyi ac66a97d5e Model: Add documentation DocBlock to query function 2021-08-26 00:39:43 +02:00
Riyyi f49f615238 CRUD: Fix show page heading when model doesn't have a title 2021-08-25 23:28:21 +02:00
Riyyi 2fb4481876 Model: Fix creation of new models
Do not mark 'filtered' columns as 'required' during validation,
as these are handled programmatically and not by the user.
2021-08-25 22:40:23 +02:00
Riyyi 253a3fe2f6 Admin+Config: Add toggle that disables Cloudflare cache
It's now possible to disable the Cloudflare cache during development,
so the hacky workaround with the random number in the CSS/JS URLs is no
longer needed.

Development mode is automatically disabled by Cloudflare after 4 hours.
2021-08-25 18:24:33 +02:00
39 changed files with 998 additions and 224 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ class Mail {
return mail($to, $subject, $message, $headers); return mail($to, $subject, $message, $headers);
} }
public static function sendMail(string $subject, string $message, string $from = '', string $to = ''): bool public static function sendMail(string $subject, string $message, string $to = '', string $from = ''): bool
{ {
if ($to == '') { if ($to == '') {
$to = self::$to; $to = self::$to;
+35 -101
View File
@@ -40,15 +40,8 @@ class Router {
// Process basic routes // Process basic routes
foreach (self::$routes as $route) { 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"]], // ["/example/my-page", "ExampleController", "action", "" : ["view", "title", "description"]],
self::addBasicRoute(['GET', 'POST'], $route); self::addRoute(['GET', 'POST'], $route);
break;
} }
self::createNavigation(); self::createNavigation();
@@ -81,13 +74,13 @@ class Router {
* DELETE /route/{id} destroyAction * DELETE /route/{id} destroyAction
*/ */
self::addRoute(['GET'], [$route, $controller, 'indexAction']); self::addRoute(['GET'], [$route, $controller, 'index']);
self::addRoute(['GET'], [$route . '/create', $controller, 'createAction']); self::addRoute(['GET'], [$route . '/create', $controller, 'create']);
self::addRoute(['POST'], [$route, $controller, 'storeAction']); self::addRoute(['POST'], [$route, $controller, 'store']);
self::addRoute(['GET'], [$route . '/[i:id]', $controller, 'showAction', ['id']]); self::addRoute(['GET'], [$route . '/[i:id]', $controller, 'show']);
self::addRoute(['GET'], [$route . '/[i:id]/edit', $controller, 'editAction', ['id']]); self::addRoute(['GET'], [$route . '/[i:id]/edit', $controller, 'edit']);
self::addRoute(['PUT', 'PATCH'], [$route . '/[i:id]', $controller, 'updateAction', ['id']]); self::addRoute(['PUT', 'PATCH'], [$route . '/[i:id]', $controller, 'update']);
self::addRoute(['DELETE'], [$route . '/[i:id]', $controller, 'destroyAction', ['id']]); self::addRoute(['DELETE'], [$route . '/[i:id]', $controller, 'destroy']);
} }
//-------------------------------------// //-------------------------------------//
@@ -146,7 +139,7 @@ class Router {
$url = '/' . $section[$page['section_id']] . '/' . $page['page']; $url = '/' . $section[$page['section_id']] . '/' . $page['page'];
// Add route // Add route
self::$routes[] = [$url, 'PageController', 'route', $page['id']]; self::$routes[] = [$url, 'PageController', 'route', [$page['id']]];
} }
// Cache sections and pages // Cache sections and pages
@@ -156,109 +149,53 @@ class Router {
protected static function addRoute(array $method = [], array $data = []): void protected static function addRoute(array $method = [], array $data = []): void
{ {
if (!_exists($method) || !_exists($data)) { if (!_exists($method) || !_exists($data) || count($data) < 2) {
return; return;
} }
$route = $data[0] ?? ''; $route = $data[0] ?? null;
$controller = $data[1] ?? ''; $controller = $data[1] ?? null;
$action = $data[2] ?? ''; $action = $data[2] ?? null;
$param = $data[3] ?? []; $parameters = $data[3] ?? [];
if ($route == '' || $controller == '' || $action == '') {
if (!$route || !$controller || !is_array($parameters)) {
return; return;
} }
// Create Klein route // Create Klein route
self::$router->respond($method, $route, function($request, $response, $service) self::$router->respond($method, $route, function($request, $response, $service) use($controller, $action, $parameters) {
use($controller, $action, $param) {
// Create new Controller object // Create new Controller object
$controller = '\App\Controllers\\' . $controller; $controller = '\App\Controllers\\' . $controller;
$controller = new $controller(self::$router); $controller = new $controller(self::$router);
$stillValid = true; // Complete action variable
if (!$action) {
$action = 'indexAction';
}
else {
$action .= 'Action';
}
// If method does not exist in object // If method does not exist in object
if (!method_exists($controller, $action)) { if (!method_exists($controller, $action)) {
$stillValid = false; return $controller->throw404();
} }
// If no valid permissions // If no valid permissions
if ($controller->getAdminSection() && if ($controller->getAdminSection() && !$controller->getLoggedIn()) {
$controller->getLoggedIn() == false) { return $controller->throw404();
$stillValid = false;
} }
// Get URL parameters
$namedParameters = array_filter($request->paramsNamed()->all(), 'is_string', ARRAY_FILTER_USE_KEY);
$namedParameters = array_values($namedParameters);
// Append URL parameters to the hard-coded parameters
array_push($parameters, ...$namedParameters);
// Call Controller action // Call Controller action
if ($stillValid) { return $controller->{$action}(...$parameters);
// 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();
}
}); });
} }
@@ -348,6 +285,3 @@ class Router {
} }
Router::_init(); Router::_init();
// @Todo
// - combine addRoute and addBasicroute functionality
+1 -1
View File
@@ -41,7 +41,7 @@ class User {
$user = UserModel::search(['username' => $username]); $user = UserModel::search(['username' => $username]);
$success = false; $success = false;
if ($user->exists() && $user->failed_login_attempt <= 2) { if ($user->exists() && $user->loginAllowed()) {
$saltPassword = $user->salt . $password; $saltPassword = $user->salt . $password;
if (password_verify($saltPassword, $user->password)) { if (password_verify($saltPassword, $user->password)) {
$success = true; $success = true;
+161
View File
@@ -0,0 +1,161 @@
<?php
namespace App\Classes\Http;
class Http {
private string $bodyContent;
private string $bodyFormat;
private array $options = [];
//-------------------------------------//
public function __construct()
{
}
public function delete(string $url, $data = []): Response
{
return $this->send('DELETE', $url, [
$this->bodyFormat => $data
]);
}
public function get(string $url, $query = null): Response
{
return $this->send('GET', $url, [
'query' => $query
]);
}
public function patch(string $url, array $data = []): Response
{
return $this->send('PATCH', $url, [
$this->bodyFormat => $data
]);
}
public function put(string $url, array $data = []): Response
{
return $this->send('PUT', $url, [
$this->bodyFormat => $data
]);
}
public function post(string $url, array $data = []): Response
{
return $this->send('POST', $url, [
$this->bodyFormat => $data
]);
}
//-------------------------------------//
public function accept(string $contentType): Http
{
return $this->withHeaders(['Accept' => $contentType]);
}
public function acceptJson(): Http
{
return $this->accept('application/json');
}
public function asForm(): Http
{
return $this->bodyFormat('form_params')
->contentType('application/x-www-form-urlencoded');
}
public function asJson(): Http
{
return $this->bodyFormat('json')
->contentType('application/json');
}
public function bodyFormat(string $format): Http
{
$this->bodyFormat = $format;
return $this;
}
public function contentType(string $type): Http
{
return $this->withHeaders(['Content-Type' => $type]);
}
public function withBody(string $content, string $contentType): Http
{
$this->bodyFormat('body');
$this->bodyContent = $content;
$this->contentType($contentType);
return $this;
}
public function withHeaders(array $headers): Http
{
$this->options = array_merge_recursive($this->options, [
'headers' => $headers,
]);
return $this;
}
public function withToken(string $token): Http
{
$this->withHeaders(['Authorization' => 'Bearer ' . trim($token)]);
return $this;
}
//-------------------------------------//
public function send(string $method, string $url, array $options = []): Response
{
// Format headers
$headers = [];
if (_exists($this->options, 'headers')) {
foreach ($this->options['headers'] as $key => $value) {
$headers[] = "$key: $value";
}
}
// Fill body content
switch ($this->bodyFormat) {
case 'body':
break;
case 'json':
if (_exists($options, 'json')) {
$this->bodyContent = json_encode($options['json']);
}
break;
case 'form_params':
if (_exists($options, 'form_params')) {
$this->bodyContent = http_build_query($options['form_params']);
}
break;
}
// Send HTTP request
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->bodyContent);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($curl);
curl_close($curl);
// On failed requests
if (!$response) {
$response = '';
}
return new Response($response);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Classes\Http;
class Response {
private string $response;
public function __construct(string $response)
{
$this->response = $response;
}
//-------------------------------------//
public function body(): string
{
return $this->response;
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Model\BlogModel;
class BlogController extends PageController {
public function searchAction(): void
{
$archived = $this->router->request()->param('archived', 0);
$search = $this->router->request()->param('search', '');
$posts = $this->search($search, $archived);
$this->defineHelpers();
// AJAX search request will only render the posts partial
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->router->service()->partial($this->views . '/partials/blog-posts.php', ['posts' => $posts]);
return;
}
$this->router->service()->posts = $posts;
$this->router->service()->search = $search;
$this->router->service()->injectView = $this->views . '/partials/blog-posts.php';
parent::view('blog', 'Blog');
}
public function rssAction(): void
{
date_default_timezone_set('Europe/Amsterdam');
$xml = new \SimpleXMLElement('<rss version="2.0"/>');
$channel = $xml->addChild('channel');
$channel->addChild('title', 'Rick&apos;s Webpage');
$channel->addChild('link', Config::c('APP_URL'));
$channel->addChild('description', 'Recent content on Rick&apos;s Webpage.');
$channel->addChild('language', 'en-us');
$channel->addChild('lastBuildDate', date('D, d M Y H:i:s O'));
// Fetch all non-archived blog posts
$search = $this->router->request()->param('search', '');
$posts = $this->search($search, 0);
foreach ($posts as $post) {
$link = Config::c('APP_URL') . '/' . $post['section'] . '/' . $post['page'];
$date = (new \DateTime($post['created_at']))->format('D, d M Y H:i:s O');
$item = $channel->addChild('item');
$item->addChild('title', $post['title']);
$item->addChild('link', $link);
$item->addChild('description', $post['content']);
$item->addChild('pubDate', $date);
$item->addChild('guid', $link);
}
header('content-type: text/xml; charset=UTF-8');
$dom = new \DOMDocument("1.0");
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml->asXML());
print $dom->saveXML();
}
//-------------------------------------//
private function search(string $query = '', int $archived = 0): ?array
{
return BlogModel::selectAll(
'blog_post.*, media.filename, media.extension, page.page, section.section, log.created_at', '
LEFT JOIN media ON blog_post.media_id = media.id
LEFT JOIN page ON blog_post.page_id = page.id
LEFT JOIN section ON page.section_id = section.id
LEFT JOIN log ON blog_post.log_id = log.id
WHERE blog_post.archived = :archived AND
(blog_post.content LIKE :query OR
blog_post.title LIKE :query OR
blog_post.tag LIKE :query)
', [
[':archived', "$archived", \PDO::PARAM_INT],
[':query', "%$query%", \PDO::PARAM_STR]
]);
}
private function defineHelpers(): void
{
$this->router->service()->prettyTimestamp = function (string $timestamp): string {
$date = date_create($timestamp);
$date = date_format($date, 'd M Y');
return "<u class=\"text-decoration-none text-reset\" title=\"{$timestamp}\">{$date}</u>";
};
$this->router->service()->tags = function (string $tags): string {
$result = "";
// Remove empty elements via array_filter()
$splitTags = array_filter(explode(':', $tags), function ($tag) {
return !empty(trim($tag));
});
foreach ($splitTags as $key => $tag) {
$result .= $tag;
$result .= (($key === array_key_last($splitTags)) ? '' : ', ');
}
return $result;
};
}
}
+210
View File
@@ -0,0 +1,210 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Classes\Http\Http;
use App\Classes\Session;
use App\Model\ConfigModel;
class CacheController extends PageController {
/**
* Maximum amount of files that can be purged on a single request
*/
public static int $purgeLimit = 30;
public function cacheAction(): void
{
$config = $this->getConfigValues();
$this->router->service()->config = $config;
$this->router->service()->csrfToken = Session::token();
$this->router->service()->purgeUrl = $this->url . '/purge';
$this->router->service()->toggleUrl = $this->url . '/toggle';
parent::view();
}
public function purgeAction(): void
{
if (!$this->validatePostRequest()) {
return;
}
$token = Config::c('CLOUDFLARE_TOKEN');
$zone = Config::c('CLOUDFLARE_ZONE');
$url = "https://api.cloudflare.com/client/v4/zones/$zone/purge_cache";
if (!_exists($_POST, 'type')) {
return;
}
$bodies = [];
switch($_POST['type']) {
case 'css-js':
$body = self::generateUrls(['css', 'js']);
$chunks = array_chunk($body['files'], self::$purgeLimit);
foreach($chunks as $chunk) {
$bodies[]['files'] = $chunk;
}
break;
case 'fonts-images':
$body = self::generateUrls(['fonts', 'img', 'media']);
$chunks = array_chunk($body['files'], self::$purgeLimit);
foreach($chunks as $chunk) {
$bodies[]['files'] = $chunk;
}
break;
case 'all':
$bodies[] = ['purge_everything' => true];
break;
default:
return;
}
$response = null;
foreach($bodies as $body) {
$response = (new Http)->withToken($token)
->asJson()
->acceptJson()
->post($url, $body);
if (empty($response->body())
|| !json_decode($response->body(), true)['success']) {
break;
}
}
if ($response) {
echo $response->body();
}
}
public function toggleAction(): void
{
if (!$this->validatePostRequest()) {
return;
}
$token = Config::c('CLOUDFLARE_TOKEN');
$zone = Config::c('CLOUDFLARE_ZONE');
$url = "https://api.cloudflare.com/client/v4/zones/$zone/settings/development_mode";
$config = $this->getConfigValues();
$currentState = $config['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'];
$response = (new Http)->withToken($token)
->asJson()
->acceptJson()
->patch($url, [
'value' => $currentState == '1' ? 'off' : 'on',
]);
$this->saveConfigValues($response->body());
echo $response->body();
}
//-------------------------------------//
private function validatePostRequest(): bool
{
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
parent::throw404();
}
if (Config::c('CLOUDFLARE_ENABLED') != '1') {
return false;
}
if (!Session::validateToken($_POST)) {
return false;
}
return true;
}
private static function generateUrls(array $directories): array
{
$result = [];
foreach ($directories as $directory) {
$files = array_diff(scandir($directory), ['..', '.']);
foreach ($files as $file) {
$result['files'][] = Config::c('APP_URL') . "/$directory/$file";
}
}
return $result;
}
private static function getConfigValues(): array
{
$result = [];
$config = [
'CLOUDFLARE_DEVELOPMENT_MODE_ENABLED' => '0',
'CLOUDFLARE_DEVELOPMENT_MODE_UPDATED_AT' => '',
'CLOUDFLARE_DEVELOPMENT_MODE_EXPIRES_IN' => '',
];
foreach ($config as $key => $value) {
$result[$key] = ConfigModel::firstOrCreate(
['key' => $key],
['value' => $value]
)->value;
}
if ($result['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED']) {
$expiresIn = $result['CLOUDFLARE_DEVELOPMENT_MODE_EXPIRES_IN'];
$updatedAt = $result['CLOUDFLARE_DEVELOPMENT_MODE_UPDATED_AT'];
$expiresAtObject = new \DateTime($updatedAt);
$expiresAt = $expiresAtObject
->modify("+ $expiresIn seconds")
->format('Y-m-d H:i:s');
$nowObject = new \DateTime('now');
$now = $nowObject->format('Y-m-d H:i:s');
if ($now >= $expiresAt) {
ConfigModel::updateOrCreate(
['key' => 'CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'],
['value' => 0]);
$result['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'] = 0;
}
else {
$result['enabled-remaining'] = $expiresAtObject->modify(
'- ' . $nowObject->getTimestamp() . ' seconds'
)->format('H:i:s');
}
}
return $result;
}
private static function saveConfigValues(string $response): void
{
$decodedResponse = json_decode($response, true);
if ($decodedResponse['success'] == true) {
$state = $decodedResponse['result']['value'];
$expiresIn = $decodedResponse['result']['time_remaining'];
$updatedAt = $decodedResponse['result']['modified_on'];
$updatedAtFormatted = (new \DateTime($updatedAt))->format('Y-m-d H:i:s');
ConfigModel::updateOrCreate(
['key' => 'CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'],
['value' => $state == 'on' ? 1 : 0]);
ConfigModel::updateOrCreate(
['key' => 'CLOUDFLARE_DEVELOPMENT_MODE_EXPIRES_IN'],
['value' => $expiresIn]);
ConfigModel::updateOrCreate(
['key' => 'CLOUDFLARE_DEVELOPMENT_MODE_UPDATED_AT'],
['value' => $updatedAtFormatted]);
}
}
}
+2 -2
View File
@@ -43,7 +43,7 @@ class LoginController extends PageController {
} }
else { else {
$user = User::getUser('', $_POST['username']); $user = User::getUser('', $_POST['username']);
if ($user->exists() && $user->failed_login_attempt >= 5) { if ($user->exists() && !$user->loginAllowed()) {
$this->setAlert('danger', 'User has been blocked.'); $this->setAlert('danger', 'User has been blocked.');
} }
else { else {
@@ -151,7 +151,7 @@ class LoginController extends PageController {
<a href='$resetUrl'>$resetUrl</a> <a href='$resetUrl'>$resetUrl</a>
"; ";
if (Mail::send($subject, $message, $user->email)) { if (Mail::sendMail($subject, $message, $user->email)) {
$this->setAlert('success', 'Successfully requested password reset.'); $this->setAlert('success', 'Successfully requested password reset.');
} }
else { else {
+5 -2
View File
@@ -51,14 +51,17 @@ function _randomStr(int $length, string $keyspace =
* Print variable inside of a <pre> and exit * Print variable inside of a <pre> and exit
* *
* @param mixed[] $output The variable (single/array) to print * @param mixed[] $output The variable (single/array) to print
* @param bool $die Call die(); after printing
* *
* @return void Nothing * @return void Nothing
*/ */
function _log($output): void { function _log($output, bool $die = true): void {
echo '<pre>'; echo '<pre>';
var_dump($output); var_dump($output);
echo '</pre>'; echo '</pre>';
die(); if ($die) {
die();
}
} }
//-------------------------------------// //-------------------------------------//
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Model;
class BlogModel extends Model {
protected $table = 'blog_post';
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Model;
use App\Traits\Log;
class ConfigModel extends Model {
use Log;
protected $table = 'config';
protected $sort = 'key';
public $title = "Config";
// Attribute rules
// Name | Type | Required | Filtered
public $rules = [
["key", "text", 1, 0],
["value", "text", 0, 0],
["log_id", "text", 1, 1],
];
}
+75 -41
View File
@@ -63,22 +63,42 @@ abstract class Model {
//-------------------------------------// //-------------------------------------//
/**
* Retreive data via PDO prepared statements
*
* The most frequently used constants for PDO are listed below,
* find more at: {@link https://www.php.net/manual/en/pdo.constants.php}
* - PDO::PARAM_BOOL
* - PDO::PARAM_NULL
* - PDO::PARAM_INT
* - PDO::PARAM_STR
*
* Usage:
* self::query(
* "SELECT * FROM `example` WHERE `id` = :id AND `number` = :number AND `text` = :text", [
* [':id', 1],
* [':number', 7, \PDO::PARAM_INT],
* [':text', 'A random string', \PDO::PARAM_STR],
* ]);
*
* self::query(
* 'SELECT * FROM `example` WHERE `id` IN (?, ?, ?) AND `thing` = ?, [
* 1, 2, 3, 'stuff'
* ],
* '?'
* );
*
* @param $query The full prepared query statement
* @param $parameters The values to insert into the prepared statement
* @param $type Type of prepared statement, ':' for named placeholders,
* '?' for value placeholders
*
* @return array|null Retreived data, or null
*/
protected static function query(string $query, array $parameters = [], protected static function query(string $query, array $parameters = [],
$type = ':'): ?array $type = ':'): ?array
{ {
// Example if ($type == '?' && substr_count($query, $type) != count($parameters)) {
// $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; return null;
} }
@@ -237,7 +257,7 @@ abstract class Model {
$required = false; $required = false;
foreach ($this->rules as $rule) { foreach ($this->rules as $rule) {
if ($rule[0] == $attribute && $rule[2] == 1) { if ($rule[0] == $attribute && $rule[2] == 1 && $rule[3] == 0) {
$required = true; $required = true;
break; break;
} }
@@ -325,16 +345,6 @@ abstract class Model {
return $model; 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 = '', public static function selectAll(string $select = '*', string $filter = '',
array $parameters = [], $type = ':'): ?array array $parameters = [], $type = ':'): ?array
{ {
@@ -413,14 +423,15 @@ abstract class Model {
} }
/** /**
* Load Model data: all, with a limit or pagination * Load all Models, optionally with a limit or pagination
* *
* @param int $limitOrPage Treated as page if $limit is provided, limit otherwise * @param int $limitOrPage Treated as page if $limit is provided, limit otherwise
* @param int $limit The amount to limit by * @param int $limit The amount to limit by
* *
* @return array|null The found model data, or null * @return array|null The found model data, or null
*/ */
public static function all(int $limitOrPage = -1, int $limit = -1): ?array { public static function all(int $limitOrPage = -1, int $limit = -1): ?array
{
$class = get_called_class(); $class = get_called_class();
$model = new $class; $model = new $class;
@@ -451,7 +462,9 @@ abstract class Model {
/** /**
* Retreive Model, or instantiate * Retreive Model, or instantiate
* Usage: $model = \App\Model\Example::firstOrNew(['name' => 'Example name']); *
* Usage:
* $model = \App\Model\Example::firstOrNew(['name' => 'Example name']);
* *
* @param $search Retrieve by * @param $search Retrieve by
* @param $data Instantiate with search plus data * @param $data Instantiate with search plus data
@@ -474,13 +487,16 @@ abstract class Model {
/** /**
* Create new Model * Create new Model
* Usage: $model = \App\Model\Example::create(['name' => 'Example name']); *
* Usage:
* $model = \App\Model\Example::create(['name' => 'Example name']);
* *
* @param $data Create with this data * @param $data Create with this data
* *
* @return Model The Model * @return Model The Model
*/ */
public static function create(array $data): Model { public static function create(array $data): Model
{
$class = get_called_class(); $class = get_called_class();
$model = new $class; $model = new $class;
$model->fill($data); $model->fill($data);
@@ -489,15 +505,20 @@ abstract class Model {
} }
/** /**
* Retreive Model, or create * Retreive Model, create if it doesn't exist
* Usage: $model = \App\Model\Example::firstOrCreate(['name' => 'Example name']); *
* Usage:
* $model = \App\Model\AddressModel::firstOrCreate(
* ['zip_code' => '1234AB', 'house_number' => 3],
* ['street' => 'Example lane']);
* *
* @param $search Retrieve by * @param $search Retrieve by
* @param $data Instantiate with search plus data * @param $data Data used for creation
* *
* @return Model The Model * @return Model The Model
*/ */
public static function firstOrCreate(array $search, array $data = []): Model { public static function firstOrCreate(array $search, array $data = []): Model
{
$model = self::firstOrNew($search, $data); $model = self::firstOrNew($search, $data);
if (!$model->exists()) { if (!$model->exists()) {
@@ -507,14 +528,27 @@ abstract class Model {
return $model; return $model;
} }
// // Update existing Model, or create it if doesn't exist /**
// public static function updateOrCreate(array $data, array $data): Model { * Update Model, create if it doesn't exist
// // $flight = App\Flight::updateOrCreate( *
// // ['departure' => 'Oakland', 'destination' => 'San Diego'], * Usage:
// // ['price' => 99] * $model = \App\Model\FlightModel::updateOrCreate(
// // ); * ['departure' => 'Oakland', 'desination' => 'San Diego'],
// return new Model; * ['price' => 99]);
// } *
* @param $search Retrieve by
* @param $data Data used for creation
*
* @return Model The Model
*/
public static function updateOrCreate(array $search, array $data): Model
{
$model = self::firstOrNew($search, $data);
$model->fill($data);
$model->save();
return $model;
}
} }
+13
View File
@@ -3,5 +3,18 @@
namespace App\Model; namespace App\Model;
class UserModel extends Model { class UserModel extends Model {
protected $table = 'user'; protected $table = 'user';
//-------------------------------------//
public function loginAllowed(): bool
{
if (property_exists($this, 'failed_login_attempt') && $this->failed_login_attempt < 5) {
return true;
}
return false;
}
} }
+83
View File
@@ -0,0 +1,83 @@
<?php
use \App\Classes\Config;
?>
<div class="content shadow p-4 mb-4">
<h3 class="mb-4">Cache</h3>
<?php if (Config::c('CLOUDFLARE_ENABLED') != '1') { ?>
To enable the Cloudflare cache options,
make sure to set the following option in the <p>config.php</p> file:
<pre class="line-numbers mb-4 language-php"><p class="language-php"><span class="token single-quoted-string string">'CLOUDFLARE_ENABLED'</span> <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token single-quoted-string string">'1'</span><span class="token punctuation">,</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></p></pre>
</p><?php } else { ?>
<div>
<p>
Cloudflare cache options:
</p>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-10 col-xl-10">
<h5>Purge CSS/JavaScript</h5>
<p class="mb-0">Granuarly remove .css and .js files from Cloudflare&apos;s cache.</p>
</div>
<div class="col-3 col-md-2 col-lg-2 col-xl-2">
<div class="d-flex justify-content-center">
<a class="js-purge btn btn-dark" href="<?= $this->purgeUrl; ?>"
data-type="css-js" data-token="<?= $this->csrfToken; ?>">Purge</a>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-10 col-xl-10">
<h5>Purge Fonts/Images</h5>
<p class="mb-0">Granuarly remove font and images files from Cloudflare&apos;s cache.</p>
</div>
<div class="col-3 col-md-2 col-lg-2 col-xl-2">
<div class="d-flex justify-content-center">
<a class="js-purge btn btn-danger" href="<?= $this->purgeUrl; ?>"
data-type="fonts-images" data-token="<?= $this->csrfToken; ?>">Purge</a>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-10 col-xl-10">
<h5>Purge All Files</h5>
<p class="mb-0">Remove ALL files from Cloudflare&apos;s cache.</p>
</div>
<div class="col-3 col-md-2 col-lg-2 col-xl-2">
<div class="d-flex justify-content-center">
<a class="js-purge btn btn-danger" href="<?= $this->purgeUrl; ?>"
data-type="all" data-token="<?= $this->csrfToken; ?>">Purge</a>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-10 col-xl-10">
<h5>Enable Development Mode</h5>
<p class="mb-0">
This will bypass Cloudflare&apos;s accelerated cache and slow down your site,
but is useful if you are making changes to cacheable content
(like images, CSS, or JavaScript) and would like to see those changes right away.
Once entered, development mode will last for 3 hours and then automatically toggle off.
<?php $state = $this->config['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED']; ?>
<span id="develop-enabled" style="visibility: <?= $state == '1' ? 'visible' : 'hidden'; ?>;">
Enabled for another
<code id="develop-remaining">
<?= $state == '1' ? $this->config['enabled-remaining'] : ''; ?>
</code> hours.
</span>
</p>
</div>
<div class="col-3 col-md-2 col-lg-2 col-xl-2">
<div class="d-flex justify-content-center">
<input type="checkbox" id="development-mode"
data-href="<?= $this->toggleUrl; ?>" data-token="<?= $this->csrfToken; ?>"
<?= $this->config['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'] ? 'checked' : ''; ?>>
</div>
</div>
</div>
<?php } ?>
</div>
+4 -3
View File
@@ -45,7 +45,10 @@
<select name="<?= $name; ?>" class="custom-select" <?= $required; ?>> <select name="<?= $name; ?>" class="custom-select" <?= $required; ?>>
<?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?> <?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?>
<option value="<?= $dropdownKey; ?>"><?= $value; ?></option> <option value="<?= $dropdownKey; ?>"
<?= ($dropdownKey == 0) ? 'disabled selected' : ''; ?>>
<?= $value; ?>
</option>
<?php } ?> <?php } ?>
</select> </select>
@@ -56,8 +59,6 @@
<input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" /> <input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" />
</form> </form>
<div class="pb-5"></div>
</div> </div>
</div> </div>
</div> </div>
+1 -2
View File
@@ -48,6 +48,7 @@
<select name="<?= $name; ?>" class="custom-select" <?= $required; ?>> <select name="<?= $name; ?>" class="custom-select" <?= $required; ?>>
<?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?> <?php foreach($this->dropdownData[$key] as $dropdownKey => $value) { ?>
<option value="<?= $dropdownKey; ?>" <option value="<?= $dropdownKey; ?>"
<?= ($dropdownKey == 0) ? 'disabled' : ''; ?>
<?= $this->model->$name == $dropdownKey ? 'selected' : ''; ?>> <?= $this->model->$name == $dropdownKey ? 'selected' : ''; ?>>
<?= $value; ?> <?= $value; ?>
</option> </option>
@@ -61,8 +62,6 @@
<input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" /> <input type="hidden" name="_token" value="<?= $this->csrfToken; ?>" />
</form> </form>
<div class="pb-5"></div>
</div> </div>
</div> </div>
</div> </div>
-2
View File
@@ -64,8 +64,6 @@
<a class="btn btn-dark" href="<?= $this->url ?>/create">New <?= $this->title; ?></a> <a class="btn btn-dark" href="<?= $this->url ?>/create">New <?= $this->title; ?></a>
</div> </div>
</div> </div>
<div class="pb-5"></div>
</div> </div>
</div> </div>
</div> </div>
+1 -3
View File
@@ -1,7 +1,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="content shadow p-4 mb-4"> <div class="content shadow p-4 mb-4">
<h3><?= _exists([$this->model->title]) ? ($this->escape)($this->model->title) : 'Show'; ?></h3> <h3><?= property_exists($this->model, 'title') ? ($this->escape)($this->model->title) : 'Show'; ?></h3>
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead> <thead>
@@ -33,8 +33,6 @@
<?php } ?> <?php } ?>
</tbody> </tbody>
</table> </table>
<div class="pb-5"></div>
</div> </div>
</div> </div>
</div> </div>
-2
View File
@@ -8,6 +8,4 @@
<?= !empty($this->user->last_name) ? ' ' . $this->user->last_name : ''; ?> <?= !empty($this->user->last_name) ? ' ' . $this->user->last_name : ''; ?>
</h3> </h3>
</div> </div>
<div class="pb-5"></div>
</div> </div>
-1
View File
@@ -84,7 +84,6 @@
<?= $this->partial('../app/views/partials/pagination.php'); ?> <?= $this->partial('../app/views/partials/pagination.php'); ?>
<div class="pb-5"></div>
</div> </div>
</div> </div>
</div> </div>
-2
View File
@@ -28,6 +28,4 @@
<div id="syntax-parse"><pre class="line-numbers mb-4"><code class=""></code></pre></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> <textarea id="syntax-parse-copy" class="admin-hidden"></textarea>
<div class="pb-5"></div>
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
<div class="row mt-4">
<?php $size = $this->sideContent ? '8' : '12'; ?>
<div class="col-12 col-md-<?= $size; ?> col-lg-<?= $size; ?>">
<div class="input-group mb-4">
<input type="text" name="blog-search" id="js-blog-search" class="form-control"
autofocus="" placeholder="Search" value="<?= $this->search; ?>" onfocus="this.select();" data-url="<?= $this->url; ?>">
<div class="input-group-append">
<button type="button" id="js-blog-search-button" class="btn btn-dark"><i class="fa fa-search"></i> Search</button>
</div>
</div>
<div id="blog-posts">
<?= $this->partial($this->injectView); ?>
</div>
</div>
</div>
-4
View File
@@ -17,10 +17,6 @@
<?php if ($this->injectView != '') { ?> <?php if ($this->injectView != '') { ?>
<?= $this->partial($this->injectView); ?> <?= $this->partial($this->injectView); ?>
<?php } ?> <?php } ?>
<?php if ($key === array_key_last($this->contents)) { ?>
<div class="pb-5"></div>
<?php } ?>
</div> </div>
<?php } ?> <?php } ?>
-2
View File
@@ -3,6 +3,4 @@
<p>Error <strong>404</strong>.</p> <p>Error <strong>404</strong>.</p>
<p>requested URL <?= $_SERVER["REQUEST_URI"] ?> was not found on this server.</p> <p>requested URL <?= $_SERVER["REQUEST_URI"] ?> was not found on this server.</p>
</div> </div>
<div class="pb-5"></div>
</div> </div>
+8 -8
View File
@@ -9,20 +9,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="<?= $this->metaDescription; ?>"> <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://cdnjs.cloudflare.com/ajax/libs/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"> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<?php if ($this->adminSection) { ?> <?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.65.9/codemirror.min.css" rel="stylesheet" integrity="sha384-zaeBlB/vwYsDRSlFajnDd7OydJ0cWk+c2OWybl3eSUf6hW2EbhlCsQPqKr3gkznT" 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://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.9/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"> <link href="https://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.20/summernote-bs4.min.css" rel="stylesheet" integrity="sha384-hqv27sxmxAI2L4eughLkUpjS75/Z3/hg9DOWIl0PJWE4B6GJqM2Kx72ZPoQzsUpF" crossorigin="anonymous">
<?php } ?> <?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://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" integrity="sha384-wFjoQjtV1y5jVHbt0p35Ui8aV8GVpEZkyF99OXWqP/eNJDU93D3Ugxkoyh6Y2I4A" 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="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" integrity="sha384-nUkTNLI8COlMCRJ0FHIdX76If83145OTCLUx4gQyfnO0gGeO/sD9czGEUBxtkcUv" crossorigin="anonymous">
<link href="<?= Config::c('APP_URL'); ?>/css/style.css?v=<?= rand(); ?>" rel="stylesheet"> <link href="<?= Config::c('APP_URL'); ?>/css/style.css" rel="stylesheet">
<title><?= ($this->escape)($this->pageTitle); ?><?= $this->pageTitle != '' ? ' - ' : '' ?>Rick van Vonderen</title> <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"> <link rel="icon" type="image/png" href="<?= Config::c('APP_URL'); ?>/img/favicon.png">
-2
View File
@@ -22,6 +22,4 @@
<br> <br>
<a href="<?= Config::c('APP_URL'); ?>/reset-password">Forgot password</a>? <a href="<?= Config::c('APP_URL'); ?>/reset-password">Forgot password</a>?
<?php } ?> <?php } ?>
<div class="pb-5"></div>
</div> </div>
+6
View File
@@ -22,6 +22,12 @@
<br> <br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/syntax-highlighting">Syntax Highlighting</a> - <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/syntax-highlighting">Syntax Highlighting</a>
<hr>
<h5>Config</h5>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/cache">Cache</a>
<br>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/admin/config">Config</a>
<hr> <hr>
- <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/logout">Log out</a> - <a href="<?= \App\Classes\Config::c('APP_URL'); ?>/logout">Log out</a>
</div> </div>
+32
View File
@@ -0,0 +1,32 @@
<?php
use App\Classes\Config;
?>
<?php foreach ($this->posts as $post) { ?>
<a class="clear" href="<?= Config::c('APP_URL') . '/' . $post['section'] . '/' . $post['page']; ?>">
<div class="content shadow p-4 mb-4" style="min-height: 0;">
<div class="row">
<div class="col-12 <?= _exists($post, 'media_id') ? 'col-md-9' : ''; ?>">
<div class="d-flex flex-wrap align-items-baseline">
<h4 class="mr-3"><strong><?= $post['title']; ?></strong></h4>
<small class="mb-2 text-muted"><?= ($this->prettyTimestamp)($post['created_at']); ?></small>
</div>
<p>
<?= $post['content']; ?>
</p>
<?php if (_exists($post, 'tag')) { ?>
<small>
<i>tags: <?= ($this->tags)($post['tag']); ?></i>
</small>
<div class="d-md-none mb-3"></div>
<?php } ?>
</div>
<?php if (_exists($post, 'media_id')) { ?>
<div class="col-12 col-md-3">
<img src="<?= Config::c('APP_URL'). '/media/' . $post['filename'] . '.' . $post['extension']; ?>"
loading="lazy" class="w-100" style="height: 125px; object-fit: cover;">
</div>
<?php } ?>
</div>
</div>
</a>
<?php } ?>
+3 -5
View File
@@ -2,10 +2,8 @@
use App\Classes\Config; use App\Classes\Config;
?> ?>
<footer class="mb-4"> <footer class="row mb-2">
<div class="row"> <div class="col-12">
<div class="col-12 col-lg-12"> &copy; <?= date('Y'); ?> Rick van Vonderen
&copy; <?= date('Y'); ?> Rick van Vonderen
</div>
</div> </div>
</footer> </footer>
+3
View File
@@ -48,6 +48,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://gitlab.com/riyyi" target="_blank"><i class="fa fa-gitlab"></i> GitLab</a> <a class="nav-link" href="https://gitlab.com/riyyi" target="_blank"><i class="fa fa-gitlab"></i> GitLab</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="https://linkedin.com/in/rickvanvonderen" target="_blank">Linked&VeryThinSpace;<i class="fa fa-linkedin-square"></i></a>
</li>
</ul> </ul>
</div> </div>
+19 -20
View File
@@ -1,28 +1,27 @@
<?php <?php
use App\Classes\Config; use App\Classes\Config;
?> ?>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js" integrity="sha384-i61gTtaoovXtAbKjo903+O55Jkn2+RtzHtvNez+yI49HAASvznhe9sZyjaSHTau9" 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://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" 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) { ?> <?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.65.9/codemirror.min.js" integrity="sha384-CYXtjHe/lDQEYXvQVyIBaQsJ/mSdH193+qSBy4GHEQJX4qN2LAHI/vrze0AC73We" 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.65.9/mode/clike/clike.min.js" integrity="sha384-PgWELdtHCInKQO7E0Iuj74Ih+ksuRESaj8vjQhxDVdAhhqdnfmhOsQUy/cBFASN/" 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.65.9/mode/xml/xml.min.js" integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw" 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.65.9/mode/javascript/javascript.min.js" integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf" 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.65.9/mode/css/css.min.js" integrity="sha384-fpeIC2FZuPmw7mIsTvgB5BNc8QVxQC/nWg2W+CgPYOAiBiYVuHe2E8HiTWHBMIJQ" 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.65.9/mode/htmlmixed/htmlmixed.min.js" integrity="sha384-xYIbc5F55vPi7pb/lUnFj3wu24HlpAMZdtBHkNrb2YhPzJV3pX7+eqXT2PXSNMrw" 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.65.9/mode/php/php.min.js" integrity="sha384-1FUwPY2kaZKXw258/9CYBSS+zcc3CPggxE1zLjmYYiOdkcOw3KcXH5VNJWWbjw2U" 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.65.9/mode/shell/shell.min.js" integrity="sha384-dmPRq54XNtYnlnAo7QrwJnZNa5r0JP5gU8z4H0686Cgz37qrR2jmNaDHUXyMI4wn" 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.65.9/mode/python/python.min.js" integrity="sha384-anASZwLFwg9fpwvqs21eRCiI1Jxce0cbRF4W0KPN+4qrqlFGnByppUAMd6GeZb+A" 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://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.9/keymap/vim.min.js" integrity="sha384-DlidtUdSavEUghGXpIXr2aM++HC3RMLDux675TC5dNunOL7pRBxKkrrkfarvcL8z" 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://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.20/summernote-bs4.min.js" integrity="sha384-tG37zJAk+EcUTn0PLQkIE5Bbmkuna7/Spxvd1GU2kyY8kKGX5V2K+qGitOvd5Sm1" 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://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js" data-manual integrity="sha384-MXybTpajaBV0AkcBaCPT4KIvo0FzoCiWXgcihYsw4FUkEz0Pv3JGV6tk2G8vJtDc" 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://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js" integrity="sha384-Uq05+JLko69eOiPr39ta9bh7kld5PKZoU+fF7g0EXTAriEollhZ+DrN8Q/Oi8J2Q" 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://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js" integrity="sha384-6QJu8apxMmB9TiPVWzYKF5pRgKcz7snO0/QU+MrWmgBLECQjoa6erxX2VQ5t41Jd" 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="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/highlight-keywords/prism-highlight-keywords.min.js" integrity="sha384-Wchy6vgUSGGGXWUpUsb15Y8wXgk4snlp96dF6c2GvaSQ3LeYP7778HuJUDJXg/so" crossorigin="anonymous"></script>
<script src="<?= Config::c('APP_URL'); ?>/js/app.js?v=<?= rand(); ?>"></script> <script src="<?= Config::c('APP_URL'); ?>/js/app.js"></script>
<?php } ?> <?php } ?>
<script src="<?= Config::c('APP_URL'); ?>/js/site.js?v=<?= rand(); ?>"></script> <script src="<?= Config::c('APP_URL'); ?>/js/site.js"></script>
-2
View File
@@ -9,6 +9,4 @@
<?php } ?> <?php } ?>
<?= $this->partial($this->injectView); ?> <?= $this->partial($this->injectView); ?>
<div class="pb-5"></div>
</div> </div>
+1
View File
@@ -8,6 +8,7 @@
], ],
"psr-4": { "psr-4": {
"App\\Classes\\": "app/classes/", "App\\Classes\\": "app/classes/",
"App\\Classes\\Http\\": "app/classes/http/",
"App\\Controllers\\": "app/controllers/", "App\\Controllers\\": "app/controllers/",
"App\\Model\\": "app/model/", "App\\Model\\": "app/model/",
"App\\Traits\\": "app/traits/" "App\\Traits\\": "app/traits/"
+4
View File
@@ -14,4 +14,8 @@ return [
'MAIL_NAME' => '', 'MAIL_NAME' => '',
'MAIL_USERNAME' => '', 'MAIL_USERNAME' => '',
'MAIL_PASSWORD' => '', 'MAIL_PASSWORD' => '',
'CLOUDFLARE_ENABLED' => '',
'CLOUDFLARE_TOKEN' => '',
'CLOUDFLARE_ZONE' => '',
]; ];
+6 -8
View File
@@ -30,8 +30,7 @@ nav.shadow {
.content { .content {
position: relative; position: relative;
/* height: calc(100% - 56px); */ min-height: calc(100vh - 104px - .5rem);
min-height: calc(100vh - 80px);
padding: 20px 20px 50px 20px; padding: 20px 20px 50px 20px;
background-color: #fff; background-color: #fff;
@@ -58,6 +57,11 @@ nav.shadow {
cursor: pointer; cursor: pointer;
} }
a.clear {
color: inherit;
text-decoration: none;
}
/* Anchor offset */ /* Anchor offset */
h3 { h3 {
position: relative; position: relative;
@@ -124,12 +128,6 @@ h3 span {
/*----------------------------------------*/ /*----------------------------------------*/
footer { footer {
position: absolute;
bottom: 0;
width: calc(100% - 30px);
height: 50px;
padding-top: 10px;
text-align: center; text-align: center;
color: #909090; color: #909090;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
error_reporting(E_ALL); error_reporting(E_ALL & ~E_DEPRECATED);
ini_set('display_errors', 1); ini_set('display_errors', 1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
+81
View File
@@ -227,6 +227,87 @@ $(document).ready(function() {
}); });
//------------------------------------------
$('.js-purge').on('click', function(event)
{
event.preventDefault();
if (!confirm('Are you sure you want to continue?')) {
return;
}
const purgeType = $(this).attr('data-type');
const csrfToken = $(this).attr('data-token');
$.ajax({
url: $(this).attr('href'),
type: 'POST',
data: { type: purgeType, _token: csrfToken },
success: function(data)
{
if (data == '') {
alert("Cache could not be purged!");
return;
}
const response = JSON.parse(data);
if (response.success == false) {
console.log(data);
alert("Cache could not be purged!");
return;
}
alert("Cache has been purged.");
}
});
});
// Developer mode
$('#development-mode').on('click', function(e)
{
e.preventDefault();
if (!confirm('Are you sure you want to continue?')) {
return;
}
const dataHref = $(this).attr('data-href');
const csrfToken = $(this).attr('data-token');
$.ajax({
url: dataHref,
type: 'POST',
data: { _token: csrfToken },
success: function(data)
{
if (data == '') {
alert("Development mode could not be enabled!");
return;
}
const response = JSON.parse(data);
if (response.success == false) {
console.log(data);
alert("Development mode could not be enabled!");
return;
}
if (response.result.value == 'on') {
e.target.checked = true;
$('#develop-enabled').css('visibility', 'visible');
$('#develop-remaining').text('03:00:00');
}
else {
e.target.checked = false;
$('#develop-enabled').css('visibility', 'hidden');
}
alert("Development mode has been set to: '" + response.result.value + "'");
}
});
});
}); });
// @Todo // @Todo
+45
View File
@@ -12,6 +12,45 @@ $(document).ready(function() {
return elementBottom > viewportTop && elementTop < viewportBottom; return elementBottom > viewportTop && elementTop < viewportBottom;
} }
//------------------------------------------//
// Blog search
function blogSearch(input)
{
var url = input.data("url");
var search = input.val();
window.location.href = url + '?search=' + encodeURIComponent(search);
}
$("#js-blog-search").keydown(function(e) {
if (e.key == 'Enter') {
e.preventDefault();
blogSearch($(this));
}
});
$("#js-blog-search-button").click(function() {
blogSearch($("#js-blog-search"));
});
$("#js-blog-search").on("input", function() {
var url = $(this).data("url");
var search = $(this).val();
if (search.length == 0 || search.length >= 3) {
fetch(url, {
method: 'POST',
body: 'search=' + encodeURIComponent(search),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
})
.then(response => response.text())
.then(data => {
$("#blog-posts").empty().append(data);
});
}
});
//------------------------------------------// //------------------------------------------//
// Image hover mouseenter // Image hover mouseenter
@@ -31,6 +70,12 @@ $(document).ready(function() {
$("#img-hover").remove(); $("#img-hover").remove();
}); });
// Image hover on click
$(".js-img-hover").click(function() {
var url = $(this).data("img-hover");
window.open(url, '_blank');
});
// Lazy load video's // Lazy load video's
$(window).on('resize scroll', function() { $(window).on('resize scroll', function() {
$('.js-video').each(function(index) { $('.js-video').each(function(index) {
+15 -9
View File
@@ -2,6 +2,7 @@
use \App\Classes\Router; use \App\Classes\Router;
Router::resource('/admin/config', 'CrudController');
Router::resource('/admin/section', 'CrudController'); Router::resource('/admin/section', 'CrudController');
Router::resource('/admin/page', 'CrudController'); Router::resource('/admin/page', 'CrudController');
Router::resource('/admin/content', 'CrudController'); Router::resource('/admin/content', 'CrudController');
@@ -11,17 +12,22 @@ Router::resource('/admin/media', 'MediaController');
// Basic routes // Basic routes
return [ return [
// URL, controller, action, view/title/description // URL, controller, action, [view, title, description]
['/', 'IndexController', '', ''], ['/', 'IndexController'],
['/img/captcha.jpg', 'IndexController', 'captcha', ''], ['/img/captcha.jpg', 'IndexController', 'captcha'],
['/robots.txt', 'IndexController', 'robots', ''], ['/robots.txt', 'IndexController', 'robots'],
['/sitemap.xml', 'IndexController', 'sitemap', ''], ['/sitemap.xml', 'IndexController', 'sitemap'],
['/blog', 'BlogController', 'search'],
['/blog.xml', 'BlogController', 'rss'],
['/login', 'LoginController', 'login', ['', 'Sign in', '']], ['/login', 'LoginController', 'login', ['', 'Sign in', '']],
['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']], ['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']],
['/logout', 'LoginController', 'logout', ''], ['/logout', 'LoginController', 'logout'],
['/admin', 'AdminController', '', ''], ['/admin', 'AdminController'],
['/admin/toggle', 'AdminController', 'toggle', ''], ['/admin/cache', 'CacheController', 'cache'],
['/admin/syntax-highlighting', 'AdminController', 'syntax', ''], ['/admin/cache/purge', 'CacheController', 'purge'],
['/admin/cache/toggle', 'CacheController', 'toggle'],
['/admin/toggle', 'AdminController', 'toggle'],
['/admin/syntax-highlighting', 'AdminController', 'syntax'],
['/test', 'TestController', '', ''], ['/test', 'TestController', '', ''],
// ["", "", "", ""], // ["", "", "", ""],
]; ];