Compare commits

...
7 Commits
Author SHA1 Message Date
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
12 changed files with 363 additions and 46 deletions
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Controllers;
use App\Classes\Config;
use App\Model\ConfigModel;
class CacheController extends PageController {
public function cacheAction(): void
{
$config = $this->getConfigValues();
$this->router->service()->config = $config;
parent::view();
}
public function developmentAction(): void
{
if (Config::c('CLOUDFLARE_ENABLED') != '1') {
return;
}
$token = Config::c('CLOUDFLARE_TOKEN');
$zone = Config::c('CLOUDFLARE_ZONE');
$url = "https://api.cloudflare.com/client/v4/zones/$zone/settings/development_mode";
$headers = [
"Authorization: Bearer $token",
"Content-Type: application/json"
];
$config = $this->getConfigValues();
$currentState = $config['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'];
$newState = $currentState == '1' ? 'off' : 'on';
$data = '{"value": "' . $newState . '"}';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
// curl_setopt($curl, CURLINFO_HEADER_OUT, 1);
$response = curl_exec($curl);
// $info = curl_getinfo($curl, CURLINFO_HEADER_OUT);
curl_close($curl);
$this->saveConfigValues($response);
echo $response;
// echo $info;
}
//-------------------------------------//
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]);
}
}
}
+4 -1
View File
@@ -51,15 +51,18 @@ 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>';
if ($die) {
die(); die();
} }
}
//-------------------------------------// //-------------------------------------//
+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],
];
}
+74 -40
View File
@@ -63,21 +63,41 @@ 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
// $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)) { 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;
}
} }
+82
View File
@@ -0,0 +1,82 @@
<?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-11">
<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-1">
<div class="d-flex justify-content-end">
<button id="" class="btn btn-dark">Purge</button>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-11">
<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-1">
<div class="d-flex justify-content-end">
<button id="" class="btn btn-danger">Purge</button>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-md-10 col-lg-11">
<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-1">
<div class="d-flex justify-content-end">
<button id="" class="btn btn-danger">Purge</button>
</div>
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-9 col-mlg-10 col-xl-11">
<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-lg-2 col-xl-1">
<div class="d-flex justify-content-start">
<input type="checkbox" id="development-mode"
<?= $this->config['CLOUDFLARE_DEVELOPMENT_MODE_ENABLED'] ? 'checked' : ''; ?>>
</div>
</div>
</div>
<?php } ?>
<div class="pb-5"></div>
</div>
+1 -1
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>
+1 -1
View File
@@ -22,7 +22,7 @@
<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/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="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"> <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">
+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>
+2 -2
View File
@@ -23,6 +23,6 @@
<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/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="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> <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>
+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' => '',
]; ];
+34
View File
@@ -227,6 +227,40 @@ $(document).ready(function() {
}); });
//------------------------------------------
// Developer mode
$('#development-mode').on('click', function(e)
{
e.preventDefault();
if (!confirm('Are you sure you want to continue?')) {
return;
}
$.get('/admin/toggle-development-mode').done(function(data)
{
const response = JSON.parse(data);
if (response.success == true) {
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 + "'");
}
else {
console.log(data);
alert("Development mode could not be enabled!")
}
});
});
}); });
// @Todo // @Todo
+3
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');
@@ -20,7 +21,9 @@ return [
['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']], ['/reset-password', 'LoginController', 'reset', ['', 'Reset password', '']],
['/logout', 'LoginController', 'logout', ''], ['/logout', 'LoginController', 'logout', ''],
['/admin', 'AdminController', '', ''], ['/admin', 'AdminController', '', ''],
['/admin/cache', 'CacheController', 'cache', ''],
['/admin/toggle', 'AdminController', 'toggle', ''], ['/admin/toggle', 'AdminController', 'toggle', ''],
['/admin/toggle-development-mode', 'CacheController', 'development', ''],
['/admin/syntax-highlighting', 'AdminController', 'syntax', ''], ['/admin/syntax-highlighting', 'AdminController', 'syntax', ''],
['/test', 'TestController', '', ''], ['/test', 'TestController', '', ''],
// ["", "", "", ""], // ["", "", "", ""],