Add CSS upload. Refactor templates. General cleanup.
This commit is contained in:
parent
f8e7151e6f
commit
4f5ea22dfd
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@
|
|||||||
*.txt
|
*.txt
|
||||||
|
|
||||||
init_complete
|
init_complete
|
||||||
|
storage/upload/css
|
||||||
|
scratch
|
@ -6,7 +6,7 @@ services:
|
|||||||
- "80:80"
|
- "80:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./public:/var/www/html/tkr/public
|
- ./public:/var/www/html/tkr/public
|
||||||
- ./http_config/nginx/folder.conf:/etc/nginx/conf.d/default.conf
|
- ./examples/nginx/folder.conf:/etc/nginx/conf.d/default.conf
|
||||||
depends_on:
|
depends_on:
|
||||||
- php
|
- php
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
@ -23,9 +23,13 @@ server {
|
|||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
# Cache static files
|
# Cache static files
|
||||||
# Note that I don't actually serve most of this (just js and css to start)
|
# Note that I don't actually serve most of this (just css)
|
||||||
# but including them all will let caching work later if I add images or something
|
# but this prevents requests for static content from getting to the PHP handler.
|
||||||
location ~* ^/tkr/.+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
#
|
||||||
|
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
||||||
|
# That lets me store uploaded content outside of the document root,
|
||||||
|
# so it isn't served directly.
|
||||||
|
location ~* ^/tkr/(?!css/custom/).+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
@ -70,7 +74,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to sensitive directories
|
# Deny access to sensitive directories
|
||||||
location ~ ^/tkr/(storage|src|templates|vendor|config) {
|
location ~ ^/tkr/(storage|src|templates|vendor|uploads|config) {
|
||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
@ -126,10 +126,7 @@ input[type="file"]:focus {
|
|||||||
box-shadow: 0 0 0 3px var(--shadow-primary);
|
box-shadow: 0 0 0 3px var(--shadow-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Submit buttons */
|
button {
|
||||||
/* Stop deleting this block */
|
|
||||||
input[type="submit"],
|
|
||||||
button[type="submit"] {
|
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid var(--color-primary);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@ -142,21 +139,18 @@ button[type="submit"] {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"]:hover,
|
button:hover {
|
||||||
button[type="submit"]:hover {
|
|
||||||
background-color: var(--color-hover-light);
|
background-color: var(--color-hover-light);
|
||||||
border-color: var(--color-primary-dark);
|
border-color: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"]:focus,
|
button:focus {
|
||||||
button[type="submit"]:focus {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--color-primary-darker);
|
border-color: var(--color-primary-darker);
|
||||||
box-shadow: 0 0 0 2px var(--shadow-primary);
|
box-shadow: 0 0 0 2px var(--shadow-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"]:active,
|
button:active {
|
||||||
button[type="submit"]:active {
|
|
||||||
background-color: var(--color-hover-medium);
|
background-color: var(--color-hover-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +163,15 @@ label {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.description {
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: left;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The two common display options for responsive layouts are flex and grid.
|
The two common display options for responsive layouts are flex and grid.
|
||||||
flex (aka Flexbox) aligns items either horizontally or vertically.
|
flex (aka Flexbox) aligns items either horizontally or vertically.
|
||||||
@ -234,33 +237,32 @@ label {
|
|||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-btn {
|
.delete-btn {
|
||||||
background-color: var(--color-primary-lightest);
|
background-color: #fef2f2;
|
||||||
color: var(--color-text-secondary);
|
color: #dc2626;
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid #fca5a5;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
margin-top: 12px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-btn:hover {
|
.delete-btn:hover {
|
||||||
background-color: var(--color-hover-light);
|
background-color: #fee2e2;
|
||||||
border-color: var(--color-primary-dark);
|
border-color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-btn:active {
|
.delete-btn:focus {
|
||||||
background-color: var(--color-hover-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-btn:focus {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px var(--shadow-primary);
|
border-color: #dc2626;
|
||||||
|
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:active {
|
||||||
|
background-color: #fecaca;
|
||||||
}
|
}
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
@ -314,6 +316,11 @@ label {
|
|||||||
padding-top: 10px; /* Match input padding */
|
padding-top: 10px; /* Match input padding */
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.description {
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.home-container {
|
.home-container {
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 1fr 2fr;
|
||||||
|
@ -19,6 +19,7 @@ define('STORAGE_DIR', APP_ROOT . '/storage');
|
|||||||
define('TEMPLATES_DIR', APP_ROOT . '/templates');
|
define('TEMPLATES_DIR', APP_ROOT . '/templates');
|
||||||
define('TICKS_DIR', STORAGE_DIR . '/ticks');
|
define('TICKS_DIR', STORAGE_DIR . '/ticks');
|
||||||
define('DATA_DIR', STORAGE_DIR . '/db');
|
define('DATA_DIR', STORAGE_DIR . '/db');
|
||||||
|
define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css');
|
||||||
define('DB_FILE', DATA_DIR . '/tkr.sqlite');
|
define('DB_FILE', DATA_DIR . '/tkr.sqlite');
|
||||||
|
|
||||||
// Load all classes from the src/ directory
|
// Load all classes from the src/ directory
|
||||||
@ -61,7 +62,9 @@ function route(string $requestPath, string $requestMethod, array $routeHandlers)
|
|||||||
$controller = $routeHandler[1];
|
$controller = $routeHandler[1];
|
||||||
$methods = $routeHandler[2] ?? ['GET'];
|
$methods = $routeHandler[2] ?? ['GET'];
|
||||||
|
|
||||||
$routePattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePattern);
|
# Only allow valid route and filename characters
|
||||||
|
# to prevent directory traversal and other attacks
|
||||||
|
$routePattern = preg_replace('/\{([^}]+)\}/', '([a-zA-Z0-9._-]+)', $routePattern);
|
||||||
$routePattern = '#^' . $routePattern . '$#';
|
$routePattern = '#^' . $routePattern . '$#';
|
||||||
|
|
||||||
if (preg_match($routePattern, $requestPath, $matches)) {
|
if (preg_match($routePattern, $requestPath, $matches)) {
|
||||||
@ -88,19 +91,24 @@ function route(string $requestPath, string $requestMethod, array $routeHandlers)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define the recognized routes.
|
||||||
|
// Anything else will 404.
|
||||||
$routeHandlers = [
|
$routeHandlers = [
|
||||||
['', 'HomeController'],
|
['', 'HomeController'],
|
||||||
['', 'HomeController@handleTick', ['POST']],
|
['', 'HomeController@handleTick', ['POST']],
|
||||||
['admin', 'AdminController'],
|
['admin', 'AdminController'],
|
||||||
['admin', 'AdminController@handleSave', ['POST']],
|
['admin', 'AdminController@handleSave', ['POST']],
|
||||||
|
['admin/css', 'CssController'],
|
||||||
|
['admin/css', 'CssController@handlePost', ['POST']],
|
||||||
|
['feed/rss', 'FeedController@rss'],
|
||||||
|
['feed/atom', 'FeedController@atom'],
|
||||||
['login', 'AuthController@showLogin'],
|
['login', 'AuthController@showLogin'],
|
||||||
['login', 'AuthController@handleLogin', ['POST']],
|
['login', 'AuthController@handleLogin', ['POST']],
|
||||||
['logout', 'AuthController@handleLogout', ['GET', 'POST']],
|
['logout', 'AuthController@handleLogout', ['GET', 'POST']],
|
||||||
['mood', 'MoodController'],
|
['mood', 'MoodController'],
|
||||||
['mood', 'MoodController@handleMood', ['POST']],
|
['mood', 'MoodController@handleMood', ['POST']],
|
||||||
['feed/rss', 'FeedController@rss'],
|
|
||||||
['feed/atom', 'FeedController@atom'],
|
|
||||||
['tick/{y}/{m}/{d}/{h}/{i}/{s}', 'TickController'],
|
['tick/{y}/{m}/{d}/{h}/{i}/{s}', 'TickController'],
|
||||||
|
['css/custom/{filename}.css', 'CssController@serveCustomCss'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Set content type
|
// Set content type
|
||||||
|
@ -119,11 +119,7 @@ class AdminController extends Controller {
|
|||||||
ConfigModel::completeSetup();
|
ConfigModel::completeSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ' . $config->basePath . '/admin');
|
header('Location: ' . $config->basePath . 'admin');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCustomCss(){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
class Controller {
|
class Controller {
|
||||||
protected function render(string $templateFile, array $vars = []) {
|
// Renders the requested template inside templates/main/php
|
||||||
$templatePath = TEMPLATES_DIR . "/" . $templateFile;
|
protected function render(string $childTemplateFile, array $vars = []) {
|
||||||
|
$templatePath = TEMPLATES_DIR . "/main.php";
|
||||||
|
$childTemplatePath = TEMPLATES_DIR . "/partials/" . $childTemplateFile;
|
||||||
|
|
||||||
if (!file_exists($templatePath)) {
|
if (!file_exists($templatePath)) {
|
||||||
throw new RuntimeException("Template not found: $templatePath");
|
throw new RuntimeException("Template not found: $templatePath");
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHP scoping
|
if (!file_exists($childTemplatePath)) {
|
||||||
// extract the variables from $vars into the local scope.
|
throw new RuntimeException("Template not found: $childTemplatePath");
|
||||||
|
}
|
||||||
|
|
||||||
extract($vars, EXTR_SKIP);
|
extract($vars, EXTR_SKIP);
|
||||||
include $templatePath;
|
include $templatePath;
|
||||||
}
|
}
|
||||||
|
257
src/Controller/CssController/CssController.php
Normal file
257
src/Controller/CssController/CssController.php
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CssController extends Controller {
|
||||||
|
public function index() {
|
||||||
|
$config = ConfigModel::load();
|
||||||
|
$user = UserModel::load();
|
||||||
|
$customCss = CssModel::load();
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'user' => $user,
|
||||||
|
'config' => $config,
|
||||||
|
'customCss' => $customCss,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->render("css.php", $vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serveCustomCss(string $baseFilename){
|
||||||
|
$cssModel = new CssModel();
|
||||||
|
$filename = "$baseFilename.css";
|
||||||
|
|
||||||
|
$cssRow = $cssModel->getByFilename($filename);
|
||||||
|
|
||||||
|
if (!$cssRow){
|
||||||
|
http_response_code(404);
|
||||||
|
exit("CSS file not found: $filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = CSS_UPLOAD_DIR . "/$filename";
|
||||||
|
|
||||||
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit("CSS file not found: $filePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shouldn't be possible, but I'm being extra paranoid
|
||||||
|
// about user input
|
||||||
|
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
if($ext != 'css'){
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Invalid file type requested: $ext");
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-type: text/css');
|
||||||
|
header('Cache-control: public, max-age=3600');
|
||||||
|
|
||||||
|
readfile($filePath);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePost() {
|
||||||
|
$config = ConfigModel::load();
|
||||||
|
|
||||||
|
switch ($_POST['action']) {
|
||||||
|
case 'upload':
|
||||||
|
$this->handleUpload();
|
||||||
|
break;
|
||||||
|
case 'set_theme':
|
||||||
|
$this->handleSetTheme($config);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
$this->handleDelete($config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ' . $config->basePath . 'admin/css');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleDelete(ConfigModel $config): void{
|
||||||
|
// Don't try to delete the default theme.
|
||||||
|
if (!$_POST['selectCssFile']){
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Cannot delete default theme");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the data for the selected CSS file
|
||||||
|
$cssId = $_POST['selectCssFile'];
|
||||||
|
$cssModel = new CssModel();
|
||||||
|
$cssRow = $cssModel->getById($cssId);
|
||||||
|
|
||||||
|
// exit if the requested file isn't in the database
|
||||||
|
if (!$cssRow){
|
||||||
|
http_response_code(400);
|
||||||
|
exit("No entry found for css id $cssId");
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the filename
|
||||||
|
$cssFilename = $cssRow["filename"];
|
||||||
|
|
||||||
|
// delete the file from the database
|
||||||
|
if (!$cssModel->delete($cssId)){
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Error deleting theme");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the full path to the file
|
||||||
|
$filePath = CSS_UPLOAD_DIR . "/$cssFilename";
|
||||||
|
|
||||||
|
// Exit if the file doesn't exist or isn't readable
|
||||||
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit("CSS file not found: $filePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
if (!unlink($filePath)){
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Error deleting file: $filePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the theme back to default
|
||||||
|
$config->cssId = null;
|
||||||
|
$config = $config->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleSetTheme(ConfigModel $config) {
|
||||||
|
if ($_POST['selectCssFile']){
|
||||||
|
// Set custom theme
|
||||||
|
$config->cssId = $_POST['selectCssFile'];
|
||||||
|
} else {
|
||||||
|
// Set default theme
|
||||||
|
$config->cssId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the site theme
|
||||||
|
$config = $config->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleUpload() {
|
||||||
|
try {
|
||||||
|
// Check if file was uploaded
|
||||||
|
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
throw new Exception('No file uploaded or upload error occurred');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['uploadCssFile'];
|
||||||
|
$description = $_POST['description'] ?? '';
|
||||||
|
|
||||||
|
// Validate file extension
|
||||||
|
$filename = $file['name'];
|
||||||
|
$fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if ($fileExtension !== 'css') {
|
||||||
|
throw new Exception('File must have a .css extension');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (1MB = 1048576 bytes)
|
||||||
|
$maxSize = 1048576; // 1MB
|
||||||
|
if ($file['size'] > $maxSize) {
|
||||||
|
throw new Exception('File size must not exceed 1MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and validate CSS content
|
||||||
|
$fileContent = file_get_contents($file['tmp_name']);
|
||||||
|
if ($fileContent === false) {
|
||||||
|
throw new Exception('Unable to read uploaded file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CSS content
|
||||||
|
$this->validateCssContent($fileContent);
|
||||||
|
|
||||||
|
// Scan for malicious content
|
||||||
|
$this->scanForMaliciousContent($fileContent, $filename);
|
||||||
|
|
||||||
|
// Create upload directory if it doesn't exist
|
||||||
|
Util::verify_storage_dir(CSS_UPLOAD_DIR, true);
|
||||||
|
|
||||||
|
// Generate safe filename
|
||||||
|
$safeFilename = $this->generateSafeFileName($filename);
|
||||||
|
$uploadPath = CSS_UPLOAD_DIR . '/' . $safeFilename;
|
||||||
|
|
||||||
|
// Move uploaded file
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
|
||||||
|
throw new Exception('Failed to save uploaded file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add upload to database
|
||||||
|
$cssModel = new CssModel();
|
||||||
|
$cssModel->save($safeFilename, $description);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateCssContent($content) {
|
||||||
|
// Remove comments
|
||||||
|
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
|
||||||
|
|
||||||
|
// Basic CSS validation - check for balanced braces
|
||||||
|
$openBraces = substr_count($content, '{');
|
||||||
|
$closeBraces = substr_count($content, '}');
|
||||||
|
|
||||||
|
if ($openBraces !== $closeBraces) {
|
||||||
|
throw new Exception('Invalid CSS: Unbalanced braces detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for basic CSS structure (selector { property: value; })
|
||||||
|
if (!preg_match('/[^{}]+\{[^{}]*\}/', $content) && !empty(trim($content))) {
|
||||||
|
// Allow empty files or files with only @charset, @import, etc.
|
||||||
|
if (!preg_match('/^\s*(@charset|@import|@media|:root)/i', trim($content))) {
|
||||||
|
throw new Exception('Invalid CSS: No valid CSS rules found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scanForMaliciousContent($content, $fileName) {
|
||||||
|
// Check for suspicious patterns
|
||||||
|
$suspiciousPatterns = [
|
||||||
|
'/javascript:/i',
|
||||||
|
'/vbscript:/i',
|
||||||
|
'/data:.*base64/i',
|
||||||
|
'/<script/i',
|
||||||
|
'/eval\s*\(/i',
|
||||||
|
'/expression\s*\(/i',
|
||||||
|
'/behavior\s*:/i',
|
||||||
|
'/-moz-binding/i',
|
||||||
|
'/\\\00/i', // null bytes
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($suspiciousPatterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $content)) {
|
||||||
|
throw new Exception('Malicious content detected in CSS file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check filename for suspicious characters
|
||||||
|
if (preg_match('/[<>:"\/\\|?*\x00-\x1f]/', $fileName)) {
|
||||||
|
throw new Exception('Filename contains invalid characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessively long lines (potential DoS)
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strlen($line) > 10000) { // 10KB per line limit
|
||||||
|
throw new Exception('CSS file contains excessively long lines');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive nesting or selectors (potential DoS)
|
||||||
|
if (substr_count($content, '{') > 1000) {
|
||||||
|
throw new Exception('CSS file contains too many rules');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSafeFileName($originalName) {
|
||||||
|
// Remove path information and dangerous characters
|
||||||
|
$fileName = basename($originalName);
|
||||||
|
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
||||||
|
|
||||||
|
return $fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,6 +4,17 @@ class FeedController extends Controller {
|
|||||||
private array $ticks;
|
private array $ticks;
|
||||||
private array $vars;
|
private array $vars;
|
||||||
|
|
||||||
|
protected function render(string $templateFile, array $vars = []) {
|
||||||
|
$templatePath = TEMPLATES_DIR . "/" . $templateFile;
|
||||||
|
|
||||||
|
if (!file_exists($templatePath)) {
|
||||||
|
throw new RuntimeException("Template not found: $templatePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
extract($vars, EXTR_SKIP);
|
||||||
|
include $templatePath;
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(){
|
public function __construct(){
|
||||||
$this->config = ConfigModel::load();
|
$this->config = ConfigModel::load();
|
||||||
$this->ticks = iterator_to_array(TickModel::streamTicks($this->config->itemsPerPage));
|
$this->ticks = iterator_to_array(TickModel::streamTicks($this->config->itemsPerPage));
|
||||||
|
@ -15,7 +15,7 @@ class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For relative time display, compare the stored time to the current time
|
// For relative time display, compare the stored time to the current time
|
||||||
// and display it as "X second/minutes/hours/days etc. "ago
|
// and display it as "X seconds/minutes/hours/days etc." ago
|
||||||
public static function relative_time(string $tickTime): string {
|
public static function relative_time(string $tickTime): string {
|
||||||
$datetime = new DateTime($tickTime);
|
$datetime = new DateTime($tickTime);
|
||||||
$now = new DateTime('now', $datetime->getTimezone());
|
$now = new DateTime('now', $datetime->getTimezone());
|
||||||
@ -39,7 +39,7 @@ class Util {
|
|||||||
return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago';
|
return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function verify_data_dir(string $dir, bool $allow_create = false): void {
|
public static function verify_storage_dir(string $dir, bool $allow_create = false): void {
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
if ($allow_create) {
|
if ($allow_create) {
|
||||||
if (!mkdir($dir, 0770, true)) {
|
if (!mkdir($dir, 0770, true)) {
|
||||||
@ -66,7 +66,7 @@ class Util {
|
|||||||
public static function confirm_setup(): void {
|
public static function confirm_setup(): void {
|
||||||
$db = Util::get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
// Ensure required tables exist
|
// user table
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS user (
|
$db->exec("CREATE TABLE IF NOT EXISTS user (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
@ -77,13 +77,22 @@ class Util {
|
|||||||
mood TEXT NULL
|
mood TEXT NULL
|
||||||
)");
|
)");
|
||||||
|
|
||||||
|
// settings table
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS settings (
|
$db->exec("CREATE TABLE IF NOT EXISTS settings (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
site_title TEXT NOT NULL,
|
site_title TEXT NOT NULL,
|
||||||
site_description TEXT NULL,
|
site_description TEXT NULL,
|
||||||
base_url TEXT NOT NULL,
|
base_url TEXT NOT NULL,
|
||||||
base_path TEXT NOT NULL,
|
base_path TEXT NOT NULL,
|
||||||
items_per_page INTEGER NOT NULL
|
items_per_page INTEGER NOT NULL,
|
||||||
|
css_id INTEGER NULL
|
||||||
|
)");
|
||||||
|
|
||||||
|
// css table
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS css (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
description TEXT NULL
|
||||||
)");
|
)");
|
||||||
|
|
||||||
// See if there's any data in the tables
|
// See if there's any data in the tables
|
||||||
@ -91,22 +100,14 @@ class Util {
|
|||||||
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
|
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
|
||||||
$config = ConfigModel::load();
|
$config = ConfigModel::load();
|
||||||
|
|
||||||
// If either table has no records and we aren't on /admin
|
// If either table has no records and we aren't on /admin,
|
||||||
|
// redirect to /admin to complete setup
|
||||||
if ($user_count === 0 || $settings_count === 0){
|
if ($user_count === 0 || $settings_count === 0){
|
||||||
if (basename($_SERVER['PHP_SELF']) !== 'admin'){
|
if (basename($_SERVER['PHP_SELF']) !== 'admin'){
|
||||||
header('Location: ' . $config->basePath . 'admin');
|
header('Location: ' . $config->basePath . 'admin');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/*
|
|
||||||
else {
|
|
||||||
// If setup is complete and we are on setup.php, redirect to index.php.
|
|
||||||
if (basename($_SERVER['PHP_SELF']) === 'admin'){
|
|
||||||
header('Location: ' . $config->basePath);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function tick_time_to_tick_path($tickTime){
|
public static function tick_time_to_tick_path($tickTime){
|
||||||
@ -121,7 +122,7 @@ class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function get_db(): PDO {
|
public static function get_db(): PDO {
|
||||||
Util::verify_data_dir(DATA_DIR, true);
|
Util::verify_storage_dir(DATA_DIR, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = new PDO("sqlite:" . DB_FILE);
|
$db = new PDO("sqlite:" . DB_FILE);
|
||||||
|
@ -7,6 +7,7 @@ class ConfigModel {
|
|||||||
public string $basePath = '';
|
public string $basePath = '';
|
||||||
public int $itemsPerPage = 25;
|
public int $itemsPerPage = 25;
|
||||||
public string $timezone = 'relative';
|
public string $timezone = 'relative';
|
||||||
|
public ?int $cssId;
|
||||||
|
|
||||||
public static function isFirstSetup(): bool {
|
public static function isFirstSetup(): bool {
|
||||||
return !file_exists(STORAGE_DIR . '/init_complete');
|
return !file_exists(STORAGE_DIR . '/init_complete');
|
||||||
@ -24,7 +25,7 @@ class ConfigModel {
|
|||||||
$c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath;
|
$c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath;
|
||||||
|
|
||||||
$db = Util::get_db();
|
$db = Util::get_db();
|
||||||
$stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page FROM settings WHERE id=1");
|
$stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page, css_id FROM settings WHERE id=1");
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($row) {
|
if ($row) {
|
||||||
@ -33,20 +34,33 @@ class ConfigModel {
|
|||||||
$c->baseUrl = $row['base_url'];
|
$c->baseUrl = $row['base_url'];
|
||||||
$c->basePath = $row['base_path'];
|
$c->basePath = $row['base_path'];
|
||||||
$c->itemsPerPage = (int) $row['items_per_page'];
|
$c->itemsPerPage = (int) $row['items_per_page'];
|
||||||
|
$c->cssId = (int) $row['css_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $c;
|
return $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function customCssFilename() {
|
||||||
|
if (empty($this->cssId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch filename from css table using cssId
|
||||||
|
$cssModel = new CssModel();
|
||||||
|
$cssRecord = $cssModel->getById($this->cssId);
|
||||||
|
|
||||||
|
return $cssRecord ? $cssRecord['filename'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
public function save(): self {
|
public function save(): self {
|
||||||
$db = Util::get_db();
|
$db = Util::get_db();
|
||||||
|
|
||||||
if (!ConfigModel::isFirstSetup()){
|
if (!ConfigModel::isFirstSetup()){
|
||||||
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=? WHERE id=1");
|
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=?, css_id=? WHERE id=1");
|
||||||
} else {
|
} else {
|
||||||
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page) VALUES (1, ?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page, css_id) VALUES (1, ?, ?, ?, ?, ?, ?)");
|
||||||
}
|
}
|
||||||
$stmt->execute([$this->siteTitle, $this->siteDescription, $this->baseUrl, $this->basePath, $this->itemsPerPage]);
|
$stmt->execute([$this->siteTitle, $this->siteDescription, $this->baseUrl, $this->basePath, $this->itemsPerPage, $this->cssId]);
|
||||||
|
|
||||||
return self::load();
|
return self::load();
|
||||||
}
|
}
|
46
src/Model/CssModel/CssModel.php
Normal file
46
src/Model/CssModel/CssModel.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
class CssModel {
|
||||||
|
public static function load(): Array {
|
||||||
|
$db = Util::get_db();
|
||||||
|
$stmt = $db->prepare("SELECT id, filename, description FROM css ORDER BY filename");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $id): Array{
|
||||||
|
$db = Util::get_db();
|
||||||
|
$stmt = $db->prepare("SELECT id, filename, description FROM css WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getByFilename(string $filename): Array{
|
||||||
|
$db = Util::get_db();
|
||||||
|
$stmt = $db->prepare("SELECT id, filename, description FROM css WHERE filename=?");
|
||||||
|
$stmt->execute([$filename]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id): bool{
|
||||||
|
$db = Util::get_db();
|
||||||
|
$stmt = $db->prepare("DELETE FROM css WHERE id=?");
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(string $filename, ?string $description = null): void {
|
||||||
|
$db = Util::get_db();
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(id) FROM css WHERE filename = ?");
|
||||||
|
$stmt->execute([$filename]);
|
||||||
|
$fileExists = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($fileExists) {
|
||||||
|
$stmt = $db->prepare("UPDATE css SET description = ? WHERE filename = ?");
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("INSERT INTO css (filename, description) VALUES (?, ?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute([$filename, $description]);
|
||||||
|
}
|
||||||
|
}
|
20
templates/main.php
Normal file
20
templates/main.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php /** @var bool $isLoggedIn */ ?>
|
||||||
|
<?php /** @var ConfigModel $config */ ?>
|
||||||
|
<?php /** @var UserModel $user */ ?>
|
||||||
|
<?php /** @var string $childTemplateFile */ ?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title><?= $config->siteTitle ?></title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css?v=<?= time() ?>">
|
||||||
|
<?php if (!empty($config->cssId)): ?>
|
||||||
|
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/custom/<?= htmlspecialchars($config->customCssFilename()) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
||||||
|
<?php include TEMPLATES_DIR . '/partials/' . $childTemplateFile?>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,13 +0,0 @@
|
|||||||
<?php /** @var ConfigModel $config */ ?>
|
|
||||||
<?php /** @var string $moodPicker */ ?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
|
||||||
<h2>How are you feeling?</h2>
|
|
||||||
<?php echo $moodPicker; ?>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,13 +1,5 @@
|
|||||||
<?php /** @var ConfigModel $config */ ?>
|
<?php /** @var ConfigModel $config */ ?>
|
||||||
<?php /** @var UserModel $user */ ?>
|
<?php /** @var UserModel $user */ ?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
|
||||||
<html lang="en">
|
|
||||||
<h1>Admin</h1>
|
<h1>Admin</h1>
|
||||||
<div>
|
<div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@ -63,12 +55,6 @@
|
|||||||
value="<?= $config->itemsPerPage ?>" min="1" max="50"
|
value="<?= $config->itemsPerPage ?>" min="1" max="50"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
<div class="fieldset-items">
|
|
||||||
<label for="setCssFile">Set CSS File</label>
|
|
||||||
<select id="setCssFile" name="css_file">
|
|
||||||
<option value="">Default</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Change password</legend>
|
<legend>Change password</legend>
|
||||||
@ -79,31 +65,6 @@
|
|||||||
<input type="password" name="confirm_password">
|
<input type="password" name="confirm_password">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
|
||||||
<legend>CSS Upload</legend>
|
|
||||||
<div class="fieldset-items">
|
|
||||||
<form action="/upload-css" method="post" enctype="multipart/form-data">
|
|
||||||
<label for="uploadCssFile">Select File to Upload</label>
|
|
||||||
<input type="file"
|
|
||||||
id="uploadCssFile"
|
|
||||||
name="uploadCssFile"
|
|
||||||
accept=".css">
|
|
||||||
<div class="file-info">
|
|
||||||
<strong>File Requirements:</strong><br>
|
|
||||||
• Must be a valid CSS file (.css extension)<br>
|
|
||||||
• Maximum size: 2MB<br>
|
|
||||||
• Will be scanned for malicious content
|
|
||||||
</div>
|
|
||||||
<label for="description">Description (optional)</label>
|
|
||||||
<textarea id="description"
|
|
||||||
name="description"
|
|
||||||
placeholder="Describe this CSS file..."></textarea>
|
|
||||||
<button type="submit" class="upload-btn">Upload CSS File</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<button type="submit" class="submit-btn">Save Settings</button>
|
<button type="submit" class="submit-btn">Save Settings</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
62
templates/partials/css.php
Normal file
62
templates/partials/css.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php /** @var ConfigModel $config */ ?>
|
||||||
|
<?php /** @var Array $customCss */ ?>
|
||||||
|
<h1>CSS Management</h1>
|
||||||
|
<div>
|
||||||
|
<form action="<?= $config->basePath ?>admin/css" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Manage</legend>
|
||||||
|
<div class="fieldset-items">
|
||||||
|
<label for="selectCssFile">Select CSS File</label>
|
||||||
|
<select id="selectCssFile" name="selectCssFile" value=<?= $config->cssId ?>>
|
||||||
|
<option value="">Default</option>
|
||||||
|
<?php foreach ($customCss as $cssFile): ?>
|
||||||
|
<?php
|
||||||
|
if ($cssFile['id'] == $config->cssId){
|
||||||
|
$cssDescription = $cssFile['description'];
|
||||||
|
$selected = "selected";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<option value=<?= $cssFile['id'] ?>
|
||||||
|
<?= isset($selected) ? $selected : ""?>>
|
||||||
|
<?=$cssFile['filename']?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php if (isset($cssDescription) && $cssDescription): ?>
|
||||||
|
<label>Description</label>
|
||||||
|
<label class="description"><?= $cssDescription ?></label>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="action" value="set_theme">Set Theme</button>
|
||||||
|
<button type="submit" name="action" value="delete" class="delete-btn">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Upload</legend>
|
||||||
|
<div class="fieldset-items">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
<label for="uploadCssFile">Select File to Upload</label>
|
||||||
|
<input type="file"
|
||||||
|
id="uploadCssFile"
|
||||||
|
name="uploadCssFile"
|
||||||
|
accept=".css">
|
||||||
|
<div class="file-info">
|
||||||
|
<strong>File Requirements:</strong><br>
|
||||||
|
• Must be a valid CSS file (.css extension)<br>
|
||||||
|
• Maximum size: 1 MB<br>
|
||||||
|
• Will be scanned for malicious content
|
||||||
|
</div>
|
||||||
|
<label for="description">Description (optional)</label>
|
||||||
|
<textarea id="description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Describe this CSS file..."></textarea>
|
||||||
|
<div></div>
|
||||||
|
<button type="submit" name="action" value="upload">Upload CSS File</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -1,5 +0,0 @@
|
|||||||
<?php /** @var ConfigModel $config */ ?>
|
|
||||||
<title><?= $config->siteTitle ?></title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css?v=<?= time() ?>">
|
|
@ -2,13 +2,6 @@
|
|||||||
<?php /** @var ConfigModel $config */ ?>
|
<?php /** @var ConfigModel $config */ ?>
|
||||||
<?php /** @var UserModel $user */ ?>
|
<?php /** @var UserModel $user */ ?>
|
||||||
<?php /** @var string $tickList */ ?>
|
<?php /** @var string $tickList */ ?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<section id="sidebar" class="home-sidebar">
|
<section id="sidebar" class="home-sidebar">
|
||||||
<div class="home-header">
|
<div class="home-header">
|
||||||
@ -37,5 +30,3 @@
|
|||||||
</section>
|
</section>
|
||||||
<?php echo $tickList ?>
|
<?php echo $tickList ?>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,13 +1,6 @@
|
|||||||
<?php /** @var ConfigModel $config */ ?>
|
<?php /** @var ConfigModel $config */ ?>
|
||||||
<?php /** @var string $csrf_token */ ?>
|
<?php /** @var string $csrf_token */ ?>
|
||||||
<?php /** @var string $error */ ?>
|
<?php /** @var string $error */ ?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<?php if ($error): ?>
|
<?php if ($error): ?>
|
||||||
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
||||||
@ -18,5 +11,3 @@
|
|||||||
<label>Password: <input type="password" name="password" required></label><br>
|
<label>Password: <input type="password" name="password" required></label><br>
|
||||||
<button type="submit" class="submit-btn">Login</button>
|
<button type="submit" class="submit-btn">Login</button>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
|
||||||
</html>
|
|
3
templates/partials/mood.php
Normal file
3
templates/partials/mood.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php /** @var string $moodPicker */ ?>
|
||||||
|
<h2>How are you feeling?</h2>
|
||||||
|
<?php echo $moodPicker; ?>
|
@ -7,6 +7,7 @@
|
|||||||
<a href="<?= $config->basePath ?>login">login</a>
|
<a href="<?= $config->basePath ?>login">login</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a href="<?= $config->basePath ?>admin">admin</a>
|
<a href="<?= $config->basePath ?>admin">admin</a>
|
||||||
|
<a href="<?= $config->basePath ?>admin/css">css</a>
|
||||||
<a href="<?= $config->basePath ?>logout">logout</a>
|
<a href="<?= $config->basePath ?>logout">logout</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
$db = Utli::get_db();
|
|
||||||
|
|
||||||
// Handle submitted form
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$username = trim($_POST['username'] ?? '');
|
|
||||||
$display_name = trim($_POST['display_name'] ?? '');
|
|
||||||
$password = $_POST['password'] ?? '';
|
|
||||||
$site_title = trim($_POST['site_title']) ?? '';
|
|
||||||
$site_description = trim($_POST['site_description']) ?? '';
|
|
||||||
$base_path = trim($_POST['base_path'] ?? '/');
|
|
||||||
$items_per_page = (int) ($_POST['items_per_page'] ?? 25);
|
|
||||||
|
|
||||||
// Sanitize base path
|
|
||||||
if (substr($base_path, -1) !== '/') {
|
|
||||||
$base_path .= '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate
|
|
||||||
$errors = [];
|
|
||||||
if (!$username || !$password) {
|
|
||||||
$errors[] = "Username and password are required.";
|
|
||||||
}
|
|
||||||
if (!$display_name) {
|
|
||||||
$errors[] = "Display name is required.";
|
|
||||||
}
|
|
||||||
if (!$site_title) {
|
|
||||||
$errors[] = "Site title is required.";
|
|
||||||
}
|
|
||||||
if (!preg_match('#^/[^?<>:"|\\*]*$#', $base_path)) {
|
|
||||||
$errors[] = "Base path must look like a valid URL path (e.g. / or /tkr/).";
|
|
||||||
}
|
|
||||||
if ($items_per_page < 1 || $items_per_page > 50) {
|
|
||||||
$errors[] = "Items per page must be a number between 1 and 50.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Actually handle errors
|
|
||||||
if (empty($errors)) {
|
|
||||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
$stmt = $db->prepare("INSERT INTO user (username, display_name, password_hash) VALUES (?, ?, ?)");
|
|
||||||
$stmt->execute([$username, $display_name, $hash]);
|
|
||||||
|
|
||||||
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_path, items_per_page) VALUES (1, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$site_title, $site_description, $base_path, $items_per_page]);
|
|
||||||
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<h1>Let’s Set Up Your tkr</h1>
|
|
||||||
<form method="post">
|
|
||||||
<h3>UserModel settings</h3>
|
|
||||||
<label>Username: <input type="text" name="username" required></label><br>
|
|
||||||
<label>Display name: <input type="text" name="display_name" required></label><br>
|
|
||||||
<label>Password: <input type="password" name="password" required></label><br>
|
|
||||||
<br/><br/>
|
|
||||||
<h3>Site settings</h3>
|
|
||||||
<label>Title: <input type="text" name="site_title" value="My tkr" required></label><br>
|
|
||||||
<label>Description: <input type="text" name="site_description"></label><br>
|
|
||||||
<label>Base path: <input type="text" name="base_path" value="/" required></label><br>
|
|
||||||
<label>Items per page (max 50): <input type="number" name="items_per_page" value="25" min="1" max="50" required></label><br>
|
|
||||||
<br/>
|
|
||||||
<button type="submit">Complete Setup</button>
|
|
||||||
</form>
|
|
@ -2,7 +2,7 @@
|
|||||||
<?php /** @var Date $tickTime */ ?>
|
<?php /** @var Date $tickTime */ ?>
|
||||||
<?php /** @var string $tick */ ?>
|
<?php /** @var string $tick */ ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
<?php include TEMPLATES_DIR . '/partials/head.php'?>
|
||||||
</head>
|
</head>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user