Media browser: recursive scan entire content/ tree, /-media/ URL prefix, editor change detection fix
- handleMediaList() now scans content/ recursively for all media files
- URLs use /-media/ prefix mapping directly to content/ (no special cases)
- index.php: added /-media/ route, kept /-assets/ for backward compat
- editor-toolbar.js: fixed editor.on('change') placement (was inside switchMode)
- content-edit.php and content-new.php: back-btn unsaved-changes detection
- Removed unused __editorCleanup global
This commit is contained in:
49
AGENTS.md
49
AGENTS.md
@@ -13,7 +13,7 @@
|
||||
## Project Structuur
|
||||
```
|
||||
codepress/
|
||||
├── engine/
|
||||
├── cms/ # Core CMS engine
|
||||
│ ├── core/
|
||||
│ │ ├── class/
|
||||
│ │ │ ├── CodePressCMS.php # Hoofd CMS class
|
||||
@@ -25,16 +25,17 @@ codepress/
|
||||
│ │ ├── config.php # Config loader (leest config.json)
|
||||
│ │ └── index.php # Bootstrap (autoloader, requires)
|
||||
│ ├── lang/ # Taalbestanden (nl.php, en.php)
|
||||
│ └── templates/ # Mustache templates
|
||||
│ ├── layout.mustache # Hoofd layout (bevat inline CSS)
|
||||
│ ├── assets/
|
||||
│ │ ├── header.mustache
|
||||
│ │ ├── navigation.mustache
|
||||
│ │ └── footer.mustache
|
||||
│ ├── markdown_content.mustache
|
||||
│ ├── php_content.mustache
|
||||
│ └── html_content.mustache
|
||||
├── admin-console/ # Admin paneel
|
||||
│ ├── templates/ # Mustache templates
|
||||
│ │ ├── layout.mustache # Hoofd layout (bevat inline CSS)
|
||||
│ │ ├── assets/
|
||||
│ │ │ ├── header.mustache
|
||||
│ │ │ ├── navigation.mustache
|
||||
│ │ │ └── footer.mustache
|
||||
│ │ ├── markdown_content.mustache
|
||||
│ │ ├── php_content.mustache
|
||||
│ │ └── html_content.mustache
|
||||
│ └── router.php # PHP dev server router
|
||||
├── admin/ # Admin paneel
|
||||
│ ├── config/
|
||||
│ │ ├── app.php # Admin app configuratie
|
||||
│ │ └── admin.json # Gebruikers & security (file-based)
|
||||
@@ -48,10 +49,18 @@ codepress/
|
||||
│ │ ├── content.php
|
||||
│ │ ├── content-edit.php
|
||||
│ │ ├── content-new.php
|
||||
│ │ ├── content-dir-form.php
|
||||
│ │ ├── config.php
|
||||
│ │ ├── plugins.php
|
||||
│ │ ├── plugin-config.php
|
||||
│ │ └── users.php
|
||||
│ └── storage/logs/ # Admin logs
|
||||
├── cli/ # CLI scripts & tests
|
||||
│ └── test/
|
||||
│ ├── accessibility.sh # WCAG 2.1 AA test suite
|
||||
│ ├── enhanced-suite.sh # Enhanced test suite
|
||||
│ ├── functional/ # Functionele testen
|
||||
│ └── pentest/ # Penetratietesten
|
||||
├── plugins/ # CMS plugins
|
||||
│ ├── HTMLBlock/
|
||||
│ └── MQTTTracker/
|
||||
@@ -61,8 +70,8 @@ codepress/
|
||||
│ └── admin.php # Admin entry point + router
|
||||
├── content/ # Content bestanden
|
||||
├── guide/ # Handleidingen (nl/en)
|
||||
├── docs/ # Documentatie
|
||||
├── config.json # Site configuratie
|
||||
├── TODO.md # Openstaande verbeteringen
|
||||
└── AGENTS.md # Dit bestand
|
||||
```
|
||||
|
||||
@@ -70,12 +79,12 @@ codepress/
|
||||
- **PHP Standards**: Follow PSR-12. Use 4 spaces for indentation.
|
||||
- **Naming**: Classes `PascalCase` (e.g., `CodePressCMS`), methods `camelCase` (e.g., `renderMenu`), variables `camelCase`, config keys `snake_case`.
|
||||
- **Architecture**:
|
||||
- Core CMS logic in `engine/core/class/CodePressCMS.php`
|
||||
- Bootstrap/requires in `engine/core/index.php`
|
||||
- Configuration loaded from `config.json` via `engine/core/config.php`
|
||||
- Core CMS logic in `cms/core/class/CodePressCMS.php`
|
||||
- Bootstrap/requires in `cms/core/index.php`
|
||||
- Configuration loaded from `config.json` via `cms/core/config.php`
|
||||
- Public website entry point: `public/index.php`
|
||||
- Admin entry point + routing: `public/admin.php`
|
||||
- Admin authenticatie: `admin-console/src/AdminAuth.php`
|
||||
- Admin authenticatie: `admin/src/AdminAuth.php`
|
||||
- **Content**: Stored in `content/`. Supports `.md` (Markdown), `.php` (Dynamic), `.html` (Static).
|
||||
- **Templating**: Mustache-style `{{placeholder}}` in `templates/layout.mustache` via `SimpleTemplate.php`.
|
||||
- **Navigation**: Auto-generated from directory structure. Folders require an index file to be clickable in breadcrumbs.
|
||||
@@ -87,11 +96,11 @@ codepress/
|
||||
- **Git**: `main` is the clean CMS core. `development` is de actieve development branch. `e.noorlander` bevat persoonlijke content. Niet mixen.
|
||||
|
||||
## Admin Console
|
||||
- **File-based**: Geen database. Gebruikers opgeslagen in `admin-console/config/admin.json`
|
||||
- **File-based**: Geen database. Gebruikers opgeslagen in `admin/config/admin.json`
|
||||
- **Routing**: Via `?route=` parameter in `public/admin.php`
|
||||
- **Routes**: `login`, `logout`, `dashboard`, `content`, `content-edit`, `content-new`, `content-delete`, `config`, `plugins`, `users`
|
||||
- **Routes**: `login`, `logout`, `dashboard`, `content`, `content-edit`, `content-new`, `content-delete`, `config`, `plugins`, `plugins-new`, `plugins-edit`, `plugins-config`, `plugins-toggle`, `plugins-delete`, `users`
|
||||
- **Auth**: Session-based. `AdminAuth` class handelt login, logout, CSRF, brute-force lockout af
|
||||
- **Templates**: Pure PHP templates in `admin-console/templates/pages/`. Layout in `layout.php`
|
||||
- **Templates**: Pure PHP templates in `admin/templates/pages/`. Layout in `layout.php`
|
||||
|
||||
## Important: Title vs File/Directory Name Logic
|
||||
- **CRITICAL**: When user asks for "title" corrections, they usually mean **FILE/DIRECTORY NAME WITHOUT LANGUAGE PREFIX AND EXTENSIONS**, not the HTML title from content!
|
||||
@@ -107,4 +116,4 @@ codepress/
|
||||
- LSP errors over "Undefined function" in PHP files zijn vals-positief (standaard PHP functies worden niet herkend door de LSP). Negeer deze.
|
||||
- Zie `TODO.md` voor alle openstaande verbeteringen en nieuwe features.
|
||||
- `vendor/` map bevat Composer dependencies (CommonMark, Mustache). Niet handmatig wijzigen.
|
||||
- `admin-console/config/admin.json` bevat wachtwoord-hashes. Niet committen met echte productie-wachtwoorden.
|
||||
- `admin/config/admin.json` bevat wachtwoord-hashes. Niet committen met echte productie-wachtwoorden.
|
||||
|
||||
16
README.en.md
16
README.en.md
@@ -30,7 +30,7 @@ A lightweight, file-based content management system built with PHP.
|
||||
|
||||
```
|
||||
codepress/
|
||||
├── engine/
|
||||
├── cms/ # Core CMS engine
|
||||
│ ├── core/
|
||||
│ │ ├── class/
|
||||
│ │ │ ├── CodePressCMS.php # Main CMS class
|
||||
@@ -48,27 +48,27 @@ codepress/
|
||||
│ ├── markdown_content.mustache
|
||||
│ ├── php_content.mustache
|
||||
│ └── html_content.mustache
|
||||
├── admin-console/ # Admin panel
|
||||
├── admin/ # Admin panel
|
||||
│ ├── config/
|
||||
│ │ ├── app.php # Admin configuration
|
||||
│ │ └── admin.json # Users & security
|
||||
│ ├── src/
|
||||
│ │ └── AdminAuth.php # Authentication service
|
||||
│ └── templates/ # Admin templates
|
||||
├── cli/ # CLI scripts & tests
|
||||
│ └── test/
|
||||
├── public/ # Web root
|
||||
│ ├── assets/
|
||||
│ │ ├── css/
|
||||
│ │ ├── js/
|
||||
│ │ └── favicon.svg
|
||||
│ ├── content/ # Content files
|
||||
│ │ ├── nl.homepage.md # Dutch homepage
|
||||
│ │ ├── en.homepage.md # English homepage
|
||||
│ │ └── [lang].[page].md # Multi-language pages
|
||||
│ ├── index.php # Website entry point
|
||||
│ └── admin.php # Admin entry point
|
||||
├── content/ # Content files
|
||||
├── plugins/ # CMS plugins
|
||||
├── docs/ # Documentation
|
||||
├── config.json # Configuration
|
||||
├── version.php # Version tracking
|
||||
└── README.md # This file
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
16
README.md
16
README.md
@@ -30,7 +30,7 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP.
|
||||
|
||||
```
|
||||
codepress/
|
||||
├── engine/
|
||||
├── cms/ # Core CMS engine
|
||||
│ ├── core/
|
||||
│ │ ├── config.php # Configuratie loader
|
||||
│ │ └── index.php # CMS engine
|
||||
@@ -43,18 +43,17 @@ codepress/
|
||||
│ ├── markdown_content.mustache
|
||||
│ ├── php_content.mustache
|
||||
│ └── html_content.mustache
|
||||
├── content/ # Content bestanden
|
||||
│ ├── map1/
|
||||
│ │ ├── pagina1.md
|
||||
│ │ └── pagina2.php
|
||||
│ └── homepage.md
|
||||
├── admin-console/ # Admin paneel
|
||||
├── admin/ # Admin paneel
|
||||
│ ├── config/
|
||||
│ │ ├── app.php # Admin configuratie
|
||||
│ │ └── admin.json # Gebruikers & security
|
||||
│ ├── src/
|
||||
│ │ └── AdminAuth.php # Authenticatie service
|
||||
│ └── templates/ # Admin templates
|
||||
├── cli/ # CLI scripts & tests
|
||||
│ └── test/
|
||||
├── content/ # Content bestanden
|
||||
├── plugins/ # CMS plugins
|
||||
├── public/ # Web root
|
||||
│ ├── assets/
|
||||
│ │ ├── css/
|
||||
@@ -62,6 +61,7 @@ codepress/
|
||||
│ │ └── favicon.svg
|
||||
│ ├── index.php # Website entry point
|
||||
│ └── admin.php # Admin entry point
|
||||
├── docs/ # Documentatie
|
||||
├── config.json # Configuratie
|
||||
└── README.md
|
||||
```
|
||||
@@ -74,7 +74,7 @@ codepress/
|
||||
{
|
||||
"site_title": "CodePress",
|
||||
"content_dir": "content",
|
||||
"templates_dir": "engine/templates",
|
||||
"templates_dir": "cms/templates",
|
||||
"default_page": "auto",
|
||||
"homepage": "homepage",
|
||||
"author": {
|
||||
|
||||
6
TODO.md
Normal file
6
TODO.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# TODO
|
||||
|
||||
## ✅ Voltooid
|
||||
- Elk thema zit in zijn eigen map in de thema's map (`themes/naam/theme.json`)
|
||||
- In admin → content: alle bestanden kunnen hernoemd worden via de edit-pagina
|
||||
- In admin → content: bestanden en mappen kunnen verplaatst worden
|
||||
@@ -12,6 +12,7 @@ return [
|
||||
'content_dir' => __DIR__ . '/../../content/',
|
||||
'config_json' => __DIR__ . '/../../config.json',
|
||||
'plugins_dir' => __DIR__ . '/../../plugins/',
|
||||
'assets_dir' => __DIR__ . '/../../content/-assets/',
|
||||
'admin_config' => __DIR__ . '/admin.json',
|
||||
'log_file' => __DIR__ . '/../storage/logs/admin.log',
|
||||
];
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
case 'content-dir-rename':
|
||||
require __DIR__ . '/pages/content-dir-form.php';
|
||||
break;
|
||||
case 'content-move':
|
||||
require __DIR__ . '/pages/content-move-form.php';
|
||||
break;
|
||||
case 'users':
|
||||
require __DIR__ . '/pages/users.php';
|
||||
break;
|
||||
|
||||
26
admin/templates/pages/content-dir-form.php
Normal file
26
admin/templates/pages/content-dir-form.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<h2 class="mb-4"><i class="bi bi-pencil"></i> Map hernoemen</h2>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Huidige mapnaam</label>
|
||||
<p class="form-control-plaintext fw-bold"><?= htmlspecialchars(basename($fullPath)) ?></p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_name" class="form-label">Nieuwe naam</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name"
|
||||
value="<?= htmlspecialchars(basename($fullPath)) ?>" required autofocus>
|
||||
<div class="form-text">Alleen letters, cijfers, punten, underscores en streepjes.</div>
|
||||
</div>
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="admin.php?route=content&dir=<?= urlencode(dirname($dir) === '.' ? '' : dirname($dir)) ?>" class="btn btn-secondary">Annuleren</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Opslaan</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,27 +1,309 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-pencil"></i> <?= htmlspecialchars($fileName) ?></h2>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode(dirname($_GET['file'] ?? '')) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="mb-4"><i class="bi bi-pencil"></i> <?= htmlspecialchars($fileName) ?></h2>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=content-edit&file=<?= urlencode($_GET['file'] ?? '') ?>">
|
||||
<form method="POST" action="admin.php?route=content-edit&file=<?= urlencode($file) ?>" id="editor-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="badge bg-secondary"><?= strtoupper($fileExt) ?></span>
|
||||
<small class="text-muted"><?= htmlspecialchars($_GET['file'] ?? '') ?></small>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="filename" class="form-label">Bestandsnaam</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="filename" name="filename"
|
||||
value="<?= htmlspecialchars(pathinfo($fileName, PATHINFO_FILENAME)) ?>" required>
|
||||
<span class="input-group-text">.<?= htmlspecialchars($fileExt) ?></span>
|
||||
</div>
|
||||
<textarea name="content" class="form-control font-monospace" rows="25" style="font-size: 0.9rem; tab-size: 4;"><?= htmlspecialchars($fileContent) ?></textarea>
|
||||
<small class="form-text text-muted">Alleen letters, cijfers, punten, underscores en streepjes.</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<?php if ($isEditable): ?>
|
||||
<div class="col-md-4">
|
||||
<label for="layout" class="form-label">Sjabloon / Layout</label>
|
||||
<select class="form-select" id="layout" name="layout">
|
||||
<option value="sidebar-content" <?= $currentLayout === 'sidebar-content' ? 'selected' : '' ?>>Sidebar + Inhoud (standaard)</option>
|
||||
<option value="content" <?= $currentLayout === 'content' ? 'selected' : '' ?>>Alleen inhoud (full-width)</option>
|
||||
<option value="sidebar" <?= $currentLayout === 'sidebar' ? 'selected' : '' ?>>Alleen sidebar (full-width)</option>
|
||||
<option value="content-sidebar" <?= $currentLayout === 'content-sidebar' ? 'selected' : '' ?>>Inhoud links + sidebar rechts</option>
|
||||
<option value="content-sidebar-reverse" <?= $currentLayout === 'content-sidebar-reverse' ? 'selected' : '' ?>>Inhoud rechts + sidebar links</option>
|
||||
</select>
|
||||
</div>
|
||||
<?php if (!empty($availablePlugins)): ?>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label d-block">Zichtbare plugins</label>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<?php foreach ($availablePlugins as $plugin): ?>
|
||||
<input type="checkbox" class="btn-check" id="plugin-<?= $plugin ?>" name="plugins[]" value="<?= htmlspecialchars($plugin) ?>" autocomplete="off" <?= in_array($plugin, $selectedPlugins) ? 'checked' : '' ?>>
|
||||
<label class="btn btn-outline-primary btn-sm" for="plugin-<?= $plugin ?>"><?= htmlspecialchars($plugin) ?></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($isEditable): ?>
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="<?= $fileExt ?>"><?= htmlspecialchars($fileContent) ?></textarea>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Opslaan
|
||||
</button>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode(dirname($_GET['file'] ?? '')) ?>" class="btn btn-outline-secondary">Annuleren</a>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode(dirname($file)) ?>" class="btn btn-outline-secondary" id="back-btn">Terug</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var backBtn = document.getElementById('back-btn');
|
||||
var filenameInput = document.getElementById('filename');
|
||||
if (!backBtn) return;
|
||||
|
||||
var changed = false;
|
||||
|
||||
function markChanged() {
|
||||
if (changed) return;
|
||||
changed = true;
|
||||
backBtn.innerHTML = '<i class="bi bi-x-circle"></i> Annuleren';
|
||||
backBtn.classList.remove('btn-outline-secondary');
|
||||
backBtn.classList.add('btn-outline-danger');
|
||||
}
|
||||
|
||||
if (filenameInput) {
|
||||
filenameInput.addEventListener('input', markChanged);
|
||||
}
|
||||
|
||||
window.__onContentChange = markChanged;
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php if ($isEditable): ?>
|
||||
<!-- Media Modal -->
|
||||
<div class="modal fade" id="mediaModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-images"></i> Media</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse mb-3" id="mediaUploadForm">
|
||||
<div class="card card-body">
|
||||
<form id="media-upload-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="mb-2">
|
||||
<input type="file" class="form-control" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm,audio/mpeg,audio/wav" id="media-file-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-sm" id="media-upload-btn" disabled>
|
||||
<i class="bi bi-cloud-upload"></i> Uploaden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#mediaUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> Uploaden
|
||||
</button>
|
||||
<small class="text-muted" id="media-count"></small>
|
||||
</div>
|
||||
|
||||
<!-- Media grid -->
|
||||
<div id="media-grid" class="row g-2">
|
||||
<div class="col-12 text-center text-muted py-4">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div> Laden...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size form (shown when clicking an image) -->
|
||||
<div id="media-size-form" class="d-none">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<img id="size-preview" src="" alt="" style="width:80px;height:60px;object-fit:cover;border-radius:4px;">
|
||||
<div>
|
||||
<strong id="size-filename" class="d-block"></strong>
|
||||
<small class="text-muted">Geef de gewenste afmetingen (optioneel)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<label class="form-label small">Breedte (px)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="size-width" placeholder="auto" min="1">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small">Hoogte (px)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="size-height" placeholder="auto" min="1">
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-end gap-1">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="size-insert-btn">
|
||||
<i class="bi bi-check-lg"></i> Invoegen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="size-cancel-btn">
|
||||
Annuleren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var mediaModal = document.getElementById('mediaModal');
|
||||
if (!mediaModal) return;
|
||||
|
||||
var ext = document.getElementById('editor-textarea').dataset.ext || 'md';
|
||||
var mode = ext === 'md' ? 'markdown' : 'html';
|
||||
var pendingFile = null;
|
||||
|
||||
function getCurrentMode() {
|
||||
var ta = document.getElementById('editor-textarea');
|
||||
if (!ta) return 'html';
|
||||
var ext = ta.dataset.ext || 'md';
|
||||
return ext === 'md' ? 'markdown' : 'html';
|
||||
}
|
||||
|
||||
mediaModal.addEventListener('show.bs.modal', function () {
|
||||
document.getElementById('media-size-form').classList.add('d-none');
|
||||
document.getElementById('media-grid').classList.remove('d-none');
|
||||
pendingFile = null;
|
||||
fetch('admin.php?route=media-list')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (files) {
|
||||
var grid = document.getElementById('media-grid');
|
||||
document.getElementById('media-count').textContent = files.length + ' bestand(en)';
|
||||
if (files.length === 0) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-muted py-4">Geen media bestanden gevonden.</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = '';
|
||||
files.forEach(function (f) {
|
||||
var col = document.createElement('div');
|
||||
col.className = 'col-6 col-md-4 col-lg-3';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'card card-media-item';
|
||||
card.style.cursor = 'pointer';
|
||||
card.title = 'Klik om in te voegen';
|
||||
|
||||
var preview;
|
||||
if (f.is_image) {
|
||||
preview = '<img src="' + f.url + '" alt="' + f.name + '" class="card-img-top" style="height:100px;object-fit:cover;">';
|
||||
} else if (f.is_video) {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><i class="bi bi-film fs-1 text-muted"></i></div>';
|
||||
} else if (f.is_audio) {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><i class="bi bi-music-note-beamed fs-1 text-muted"></i></div>';
|
||||
} else {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><span class="badge bg-secondary fs-5">' + f.ext.toUpperCase() + '</span></div>';
|
||||
}
|
||||
|
||||
card.innerHTML = preview +
|
||||
'<div class="card-body p-2"><small class="text-truncate d-block">' + f.name + '</small></div>';
|
||||
|
||||
card.addEventListener('click', function () {
|
||||
var m = getCurrentMode();
|
||||
if (f.is_image && m !== 'markdown') {
|
||||
showSizeForm(f);
|
||||
} else {
|
||||
insertMedia(f, m);
|
||||
}
|
||||
});
|
||||
col.appendChild(card);
|
||||
grid.appendChild(col);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('media-grid').innerHTML = '<div class="col-12 text-center text-danger py-4">Fout bij laden van media.</div>';
|
||||
});
|
||||
});
|
||||
|
||||
function showSizeForm(f) {
|
||||
pendingFile = f;
|
||||
document.getElementById('media-grid').classList.add('d-none');
|
||||
document.getElementById('media-size-form').classList.remove('d-none');
|
||||
document.getElementById('size-preview').src = f.url;
|
||||
document.getElementById('size-filename').textContent = f.name;
|
||||
document.getElementById('size-width').value = '';
|
||||
document.getElementById('size-height').value = '';
|
||||
}
|
||||
|
||||
document.getElementById('size-insert-btn').addEventListener('click', function () {
|
||||
if (!pendingFile) return;
|
||||
var w = document.getElementById('size-width').value;
|
||||
var h = document.getElementById('size-height').value;
|
||||
insertMedia(pendingFile, getCurrentMode(), w, h);
|
||||
});
|
||||
|
||||
document.getElementById('size-cancel-btn').addEventListener('click', function () {
|
||||
document.getElementById('media-size-form').classList.add('d-none');
|
||||
document.getElementById('media-grid').classList.remove('d-none');
|
||||
pendingFile = null;
|
||||
});
|
||||
|
||||
function insertMedia(f, mode, w, h) {
|
||||
var editorEl = document.querySelector('.CodeMirror');
|
||||
if (!editorEl || typeof CodeMirror === 'undefined') return;
|
||||
var cm = editorEl.CodeMirror;
|
||||
if (!cm) return;
|
||||
|
||||
var sizeAttr = '';
|
||||
if (w || h) {
|
||||
if (w) sizeAttr += ' width="' + parseInt(w) + '"';
|
||||
if (h) sizeAttr += ' height="' + parseInt(h) + '"';
|
||||
}
|
||||
|
||||
var tag;
|
||||
if (mode === 'markdown') {
|
||||
if (f.is_image) {
|
||||
tag = '';
|
||||
} else {
|
||||
tag = '[' + f.name + '](' + f.url + ')';
|
||||
}
|
||||
} else {
|
||||
if (f.is_image) {
|
||||
tag = '<img src="' + f.url + '" alt="' + f.name + '"' + sizeAttr + '>';
|
||||
} else if (f.is_video) {
|
||||
tag = '<video controls src="' + f.url + '" style="max-width:100%;"></video>';
|
||||
} else if (f.is_audio) {
|
||||
tag = '<audio controls src="' + f.url + '"></audio>';
|
||||
} else {
|
||||
tag = '<a href="' + f.url + '">' + f.name + '</a>';
|
||||
}
|
||||
}
|
||||
cm.replaceSelection(tag);
|
||||
cm.focus();
|
||||
|
||||
var modal = bootstrap.Modal.getInstance(mediaModal);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
|
||||
// Handle upload
|
||||
document.getElementById('media-upload-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var form = this;
|
||||
var formData = new FormData(form);
|
||||
formData.append('csrf_token', '<?= $csrf ?>');
|
||||
|
||||
fetch('admin.php?route=media', { method: 'POST', body: formData })
|
||||
.then(function () {
|
||||
form.reset();
|
||||
document.getElementById('media-upload-btn').disabled = true;
|
||||
var modal = bootstrap.Modal.getInstance(mediaModal);
|
||||
if (modal) modal.hide();
|
||||
setTimeout(function () { modal.show(); }, 100);
|
||||
})
|
||||
.catch(function () {
|
||||
alert('Upload mislukt.');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('media-file-input').addEventListener('change', function () {
|
||||
document.getElementById('media-upload-btn').disabled = this.files.length === 0;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
34
admin/templates/pages/content-move-form.php
Normal file
34
admin/templates/pages/content-move-form.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<h2 class="mb-4"><i class="bi bi-arrows-move"></i> <?= is_file($fullPath) ? 'Bestand' : 'Map' ?> verplaatsen</h2>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Te verplaatsen item</label>
|
||||
<p class="form-control-plaintext fw-bold">
|
||||
<i class="bi <?= is_file($fullPath) ? 'bi-file' : 'bi-folder' ?>"></i>
|
||||
<?= htmlspecialchars(basename($fullPath)) ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="target_dir" class="form-label">Doelmap</label>
|
||||
<select class="form-select" id="target_dir" name="target_dir" required>
|
||||
<option value="">-- Selecteer doelmap --</option>
|
||||
<option value="">/ (hoofdmap)</option>
|
||||
<?php foreach ($dirs as $d): ?>
|
||||
<option value="<?= htmlspecialchars($d) ?>"><?= htmlspecialchars($d) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Selecteer de map waar het item naartoe verplaatst moet worden.</div>
|
||||
</div>
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="admin.php?route=content&dir=<?= urlencode(dirname($item) === '.' ? '' : dirname($item)) ?>" class="btn btn-secondary">Annuleren</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Verplaatsen</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,13 +1,8 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-plus-lg"></i> Nieuwe pagina</h2>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode($dir ?? '') ?>" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="mb-4"><i class="bi bi-plus-lg"></i> Nieuwe pagina</h2>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=content-new&dir=<?= urlencode($dir ?? '') ?>">
|
||||
<form method="POST" action="admin.php?route=content-new&dir=<?= urlencode($dir ?? '') ?>" id="editor-form" data-new-page>
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
@@ -17,7 +12,7 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="type" class="form-label">Type</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<select class="form-select" id="type" name="type" data-editor-mode>
|
||||
<option value="md" selected>Markdown (.md)</option>
|
||||
<option value="php">PHP (.php)</option>
|
||||
<option value="html">HTML (.html)</option>
|
||||
@@ -29,18 +24,286 @@
|
||||
<small class="text-muted">Map: <?= htmlspecialchars($dir) ?></small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">Inhoud</label>
|
||||
<textarea name="content" id="content" class="form-control font-monospace" rows="20" style="font-size: 0.9rem; tab-size: 4;" placeholder="# Mijn nieuwe pagina
|
||||
|
||||
Schrijf hier je inhoud..."></textarea>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="layout" class="form-label">Sjabloon / Layout</label>
|
||||
<select class="form-select" id="layout" name="layout">
|
||||
<option value="sidebar-content">Sidebar + Inhoud (standaard)</option>
|
||||
<option value="content">Alleen inhoud (full-width)</option>
|
||||
<option value="sidebar">Alleen sidebar (full-width)</option>
|
||||
<option value="content-sidebar">Inhoud links + sidebar rechts</option>
|
||||
<option value="content-sidebar-reverse">Inhoud rechts + sidebar links</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?php if (!empty($availablePlugins)): ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block">Zichtbare plugins</label>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<?php foreach ($availablePlugins as $plugin): ?>
|
||||
<input type="checkbox" class="btn-check" id="plugin-<?= $plugin ?>" name="plugins[]" value="<?= htmlspecialchars($plugin) ?>" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary btn-sm" for="plugin-<?= $plugin ?>"><?= htmlspecialchars($plugin) ?></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="md"></textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary" id="content-create-btn" disabled>
|
||||
<i class="bi bi-check-lg"></i> Aanmaken
|
||||
</button>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode($dir ?? '') ?>" class="btn btn-outline-secondary">Annuleren</a>
|
||||
<a href="admin.php?route=content&dir=<?= urlencode($dir ?? '') ?>" class="btn btn-outline-secondary" id="back-btn">Terug</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Modal -->
|
||||
<div class="modal fade" id="mediaModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-images"></i> Media</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse mb-3" id="mediaUploadForm">
|
||||
<div class="card card-body">
|
||||
<form id="media-upload-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="mb-2">
|
||||
<input type="file" class="form-control" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm,audio/mpeg,audio/wav" id="media-file-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-sm" id="media-upload-btn" disabled>
|
||||
<i class="bi bi-cloud-upload"></i> Uploaden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#mediaUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> Uploaden
|
||||
</button>
|
||||
<small class="text-muted" id="media-count"></small>
|
||||
</div>
|
||||
<div id="media-grid" class="row g-2">
|
||||
<div class="col-12 text-center text-muted py-4">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div> Laden...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size form (shown when clicking an image) -->
|
||||
<div id="media-size-form" class="d-none">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<img id="size-preview" src="" alt="" style="width:80px;height:60px;object-fit:cover;border-radius:4px;">
|
||||
<div>
|
||||
<strong id="size-filename" class="d-block"></strong>
|
||||
<small class="text-muted">Geef de gewenste afmetingen (optioneel)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<label class="form-label small">Breedte (px)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="size-width" placeholder="auto" min="1">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small">Hoogte (px)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="size-height" placeholder="auto" min="1">
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-end gap-1">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="size-insert-btn">
|
||||
<i class="bi bi-check-lg"></i> Invoegen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="size-cancel-btn">
|
||||
Annuleren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var backBtn = document.getElementById('back-btn');
|
||||
var filenameInput = document.getElementById('filename');
|
||||
var createBtn = document.getElementById('content-create-btn');
|
||||
|
||||
if (backBtn) {
|
||||
var changed = false;
|
||||
function markChanged() {
|
||||
if (changed) return;
|
||||
changed = true;
|
||||
backBtn.innerHTML = '<i class="bi bi-x-circle"></i> Annuleren';
|
||||
backBtn.classList.remove('btn-outline-secondary');
|
||||
backBtn.classList.add('btn-outline-danger');
|
||||
}
|
||||
if (filenameInput) {
|
||||
filenameInput.addEventListener('input', markChanged);
|
||||
}
|
||||
window.__onContentChange = markChanged;
|
||||
}
|
||||
|
||||
if (filenameInput && createBtn) {
|
||||
filenameInput.addEventListener('input', function () {
|
||||
createBtn.disabled = this.value.trim() === '';
|
||||
});
|
||||
}
|
||||
|
||||
var mediaModal = document.getElementById('mediaModal');
|
||||
if (!mediaModal) return;
|
||||
|
||||
function getCurrentMode() {
|
||||
var ta = document.getElementById('editor-textarea');
|
||||
var ext = ta ? ta.dataset.ext || 'md' : 'md';
|
||||
return ext === 'md' ? 'markdown' : 'html';
|
||||
}
|
||||
|
||||
var pendingFile = null;
|
||||
|
||||
mediaModal.addEventListener('show.bs.modal', function () {
|
||||
document.getElementById('media-size-form').classList.add('d-none');
|
||||
document.getElementById('media-grid').classList.remove('d-none');
|
||||
pendingFile = null;
|
||||
fetch('admin.php?route=media-list')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (files) {
|
||||
var grid = document.getElementById('media-grid');
|
||||
document.getElementById('media-count').textContent = files.length + ' bestand(en)';
|
||||
if (files.length === 0) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-muted py-4">Geen media bestanden gevonden.</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = '';
|
||||
files.forEach(function (f) {
|
||||
var col = document.createElement('div');
|
||||
col.className = 'col-6 col-md-4 col-lg-3';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'card card-media-item';
|
||||
card.style.cursor = 'pointer';
|
||||
card.title = 'Klik om in te voegen';
|
||||
|
||||
var preview;
|
||||
if (f.is_image) {
|
||||
preview = '<img src="' + f.url + '" alt="' + f.name + '" class="card-img-top" style="height:100px;object-fit:cover;">';
|
||||
} else if (f.is_video) {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><i class="bi bi-film fs-1 text-muted"></i></div>';
|
||||
} else if (f.is_audio) {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><i class="bi bi-music-note-beamed fs-1 text-muted"></i></div>';
|
||||
} else {
|
||||
preview = '<div class="d-flex align-items-center justify-content-center" style="height:100px;background:#f8f9fa;"><span class="badge bg-secondary fs-5">' + f.ext.toUpperCase() + '</span></div>';
|
||||
}
|
||||
|
||||
card.innerHTML = preview +
|
||||
'<div class="card-body p-2"><small class="text-truncate d-block">' + f.name + '</small></div>';
|
||||
|
||||
card.addEventListener('click', function () {
|
||||
var mode = getCurrentMode();
|
||||
if (f.is_image && mode !== 'markdown') {
|
||||
showSizeForm(f);
|
||||
} else {
|
||||
insertMedia(f, mode);
|
||||
}
|
||||
});
|
||||
col.appendChild(card);
|
||||
grid.appendChild(col);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('media-grid').innerHTML = '<div class="col-12 text-center text-danger py-4">Fout bij laden van media.</div>';
|
||||
});
|
||||
});
|
||||
|
||||
function showSizeForm(f) {
|
||||
pendingFile = f;
|
||||
document.getElementById('media-grid').classList.add('d-none');
|
||||
document.getElementById('media-size-form').classList.remove('d-none');
|
||||
document.getElementById('size-preview').src = f.url;
|
||||
document.getElementById('size-filename').textContent = f.name;
|
||||
document.getElementById('size-width').value = '';
|
||||
document.getElementById('size-height').value = '';
|
||||
}
|
||||
|
||||
document.getElementById('size-insert-btn').addEventListener('click', function () {
|
||||
if (!pendingFile) return;
|
||||
var w = document.getElementById('size-width').value;
|
||||
var h = document.getElementById('size-height').value;
|
||||
insertMedia(pendingFile, getCurrentMode(), w, h);
|
||||
});
|
||||
|
||||
document.getElementById('size-cancel-btn').addEventListener('click', function () {
|
||||
document.getElementById('media-size-form').classList.add('d-none');
|
||||
document.getElementById('media-grid').classList.remove('d-none');
|
||||
pendingFile = null;
|
||||
});
|
||||
|
||||
function insertMedia(f, mode, w, h) {
|
||||
var editorEl = document.querySelector('.CodeMirror');
|
||||
if (!editorEl || typeof CodeMirror === 'undefined') return;
|
||||
var cm = editorEl.CodeMirror;
|
||||
if (!cm) return;
|
||||
|
||||
var sizeAttr = '';
|
||||
if (w || h) {
|
||||
if (w) sizeAttr += ' width="' + parseInt(w) + '"';
|
||||
if (h) sizeAttr += ' height="' + parseInt(h) + '"';
|
||||
}
|
||||
|
||||
var tag;
|
||||
if (mode === 'markdown') {
|
||||
if (f.is_image) {
|
||||
tag = '';
|
||||
} else {
|
||||
tag = '[' + f.name + '](' + f.url + ')';
|
||||
}
|
||||
} else {
|
||||
if (f.is_image) {
|
||||
tag = '<img src="' + f.url + '" alt="' + f.name + '"' + sizeAttr + '>';
|
||||
} else if (f.is_video) {
|
||||
tag = '<video controls src="' + f.url + '" style="max-width:100%;"></video>';
|
||||
} else if (f.is_audio) {
|
||||
tag = '<audio controls src="' + f.url + '"></audio>';
|
||||
} else {
|
||||
tag = '<a href="' + f.url + '">' + f.name + '</a>';
|
||||
}
|
||||
}
|
||||
cm.replaceSelection(tag);
|
||||
cm.focus();
|
||||
|
||||
var modal = bootstrap.Modal.getInstance(mediaModal);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
|
||||
document.getElementById('media-upload-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var form = this;
|
||||
var formData = new FormData(form);
|
||||
formData.append('csrf_token', '<?= $csrf ?>');
|
||||
|
||||
fetch('admin.php?route=media', { method: 'POST', body: formData })
|
||||
.then(function () {
|
||||
form.reset();
|
||||
document.getElementById('media-upload-btn').disabled = true;
|
||||
var modal = bootstrap.Modal.getInstance(mediaModal);
|
||||
if (modal) modal.hide();
|
||||
setTimeout(function () { modal.show(); }, 100);
|
||||
})
|
||||
.catch(function () {
|
||||
alert('Upload mislukt.');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('media-file-input').addEventListener('change', function () {
|
||||
document.getElementById('media-upload-btn').disabled = this.files.length === 0;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-file-earmark-text"></i> Content</h2>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-success btn-sm me-1" data-bs-toggle="collapse" data-bs-target="#uploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> Upload
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm me-1" data-bs-toggle="modal" data-bs-target="#createDirModal">
|
||||
<i class="bi bi-folder-plus"></i> Nieuwe map
|
||||
</button>
|
||||
<a href="admin.php?route=content-new&dir=<?= urlencode($subdir) ?>" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Nieuw bestand
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse mb-4" id="uploadForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=content&dir=<?= urlencode($subdir) ?>" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="mb-3">
|
||||
<label for="file" class="form-label">Bestanden selecteren</label>
|
||||
<input type="file" class="form-control" id="file" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,application/pdf,application/zip,video/mp4,video/webm,audio/mpeg,audio/wav">
|
||||
<small class="form-text text-muted">Toegestaan: JPG, PNG, GIF, WebP, SVG, PDF, ZIP, MP4, WebM, MP3, WAV</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> Uploaden naar deze map
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($subdir)): ?>
|
||||
<?php
|
||||
@@ -39,7 +65,7 @@
|
||||
<th>Type</th>
|
||||
<th>Grootte</th>
|
||||
<th>Gewijzigd</th>
|
||||
<th style="width: 120px;">Acties</th>
|
||||
<th style="width: 200px;">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -77,10 +103,26 @@
|
||||
<td class="text-muted"><?= $item['size'] ?></td>
|
||||
<td class="text-muted"><?= $item['modified'] ?></td>
|
||||
<td>
|
||||
<?php if (!$item['is_dir']): ?>
|
||||
<?php if ($item['is_dir']): ?>
|
||||
<a href="admin.php?route=content-dir-rename&dir=<?= urlencode($item['path']) ?>" class="btn btn-sm btn-outline-secondary" title="Hernoemen">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="admin.php?route=content-move&item=<?= urlencode($item['path']) ?>" class="btn btn-sm btn-outline-info" title="Verplaatsen">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<form method="POST" action="admin.php?route=content-dir-delete&dir=<?= urlencode($item['path']) ?>" class="d-inline" onsubmit="return confirm('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="admin.php?route=content-edit&file=<?= urlencode($item['path']) ?>" class="btn btn-sm btn-outline-primary" title="Bewerken">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="admin.php?route=content-move&item=<?= urlencode($item['path']) ?>" class="btn btn-sm btn-outline-info" title="Verplaatsen">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<form method="POST" action="admin.php?route=content-delete&file=<?= urlencode($item['path']) ?>" class="d-inline" onsubmit="return confirm('Weet je zeker dat je dit bestand wilt verwijderen?')">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||
@@ -96,3 +138,29 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Directory Modal -->
|
||||
<div class="modal fade" id="createDirModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="admin.php?route=content-dir-create&dir=<?= urlencode($subdir) ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-folder-plus"></i> Nieuwe map aanmaken</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="dirname" class="form-label">Mapnaam</label>
|
||||
<input type="text" class="form-control" id="dirname" name="dirname" required autofocus>
|
||||
<div class="form-text">Alleen letters, cijfers, punten, underscores en streepjes.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Aanmaken</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
74
admin/templates/pages/plugin-config.php
Normal file
74
admin/templates/pages/plugin-config.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<h2 class="mb-4"><i class="bi bi-plug"></i> Plugin Configuratie: <?= htmlspecialchars($pluginName) ?></h2>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (empty($pluginConfig)): ?>
|
||||
<div class="alert alert-info">Deze plugin heeft geen configureerbare instellingen.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($pluginConfig as $key => $value): ?>
|
||||
<?= renderConfigField($key, $value) ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($pluginConfig)): ?>
|
||||
<div class="card-footer text-end">
|
||||
<a href="admin.php?route=plugins" class="btn btn-secondary">Annuleren</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Opslaan</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="admin.php?route=plugins" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug naar plugins
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function renderConfigField(string $key, $value, string $prefix = ''): string
|
||||
{
|
||||
$name = $prefix ? $prefix . '[' . $key . ']' : 'config[' . $key . ']';
|
||||
$id = 'cfg_' . str_replace(['.', '['], '_', rtrim($name, ']'));
|
||||
$label = ucwords(str_replace('_', ' ', $key));
|
||||
$html = '';
|
||||
|
||||
if (is_bool($value)) {
|
||||
$checked = $value ? 'checked' : '';
|
||||
$html .= '<div class="mb-3 form-check form-switch">';
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="0">';
|
||||
$html .= '<input class="form-check-input" type="checkbox" id="' . htmlspecialchars($id) . '" name="' . htmlspecialchars($name) . '" value="1" ' . $checked . '>';
|
||||
$html .= '<label class="form-check-label" for="' . htmlspecialchars($id) . '">' . htmlspecialchars($label) . '</label>';
|
||||
$html .= '</div>';
|
||||
} elseif (is_numeric($value)) {
|
||||
$step = is_float($value) ? 'step="0.01"' : 'step="1"';
|
||||
$html .= '<div class="mb-3">';
|
||||
$html .= '<label for="' . htmlspecialchars($id) . '" class="form-label">' . htmlspecialchars($label) . '</label>';
|
||||
$html .= '<input type="number" class="form-control" id="' . htmlspecialchars($id) . '" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '" ' . $step . '>';
|
||||
$html .= '</div>';
|
||||
} elseif (is_array($value)) {
|
||||
$html .= '<div class="mb-3">';
|
||||
$html .= '<label class="form-label fw-bold">' . htmlspecialchars($label) . '</label>';
|
||||
$html .= '<div class="card bg-light">';
|
||||
$html .= '<div class="card-body">';
|
||||
foreach ($value as $subKey => $subValue) {
|
||||
$html .= renderConfigField($subKey, $subValue, $name);
|
||||
}
|
||||
$html .= '</div></div></div>';
|
||||
} else {
|
||||
$html .= '<div class="mb-3">';
|
||||
$html .= '<label for="' . htmlspecialchars($id) . '" class="form-label">' . htmlspecialchars($label) . '</label>';
|
||||
|
||||
if (strlen($value) > 80 || str_contains($value, "\n")) {
|
||||
$html .= '<textarea class="form-control font-monospace" id="' . htmlspecialchars($id) . '" name="' . htmlspecialchars($name) . '" rows="4">' . htmlspecialchars($value) . '</textarea>';
|
||||
} else {
|
||||
$html .= '<input type="text" class="form-control" id="' . htmlspecialchars($id) . '" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '">';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
28
admin/templates/pages/plugins-edit.php
Normal file
28
admin/templates/pages/plugins-edit.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-pencil"></i> Plugin bewerken: <?= htmlspecialchars($pluginName) ?></h2>
|
||||
<a href="admin.php?route=plugins" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=plugins-edit&plugin=<?= urlencode($pluginName) ?>" id="editor-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="badge bg-secondary">PHP</span>
|
||||
<small class="text-muted"><?= htmlspecialchars($pluginName) ?>/<?= htmlspecialchars($pluginName) ?>.php</small>
|
||||
</div>
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="php"><?= htmlspecialchars($fileContent) ?></textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Opslaan
|
||||
</button>
|
||||
<a href="admin.php?route=plugins" class="btn btn-outline-secondary">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
40
admin/templates/pages/plugins-new.php
Normal file
40
admin/templates/pages/plugins-new.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<h2 class="mb-4"><i class="bi bi-plug"></i> Nieuwe plugin aanmaken</h2>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="plugin_name" class="form-label">Plugin naam</label>
|
||||
<input type="text" class="form-control font-monospace" id="plugin_name" name="plugin_name"
|
||||
value="<?= htmlspecialchars($_POST['plugin_name'] ?? '') ?>" required
|
||||
placeholder="bijv. MijnPlugin">
|
||||
<div class="form-text">Alleen letters, cijfers en underscores. Begin met een hoofdletter. Wordt gebruikt als <strong>mapnaam</strong>, <strong>bestandsnaam</strong> en <strong>class naam</strong>.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="plugin_desc" class="form-label">Omschrijving <small class="text-muted">(optioneel)</small></label>
|
||||
<textarea class="form-control" id="plugin_desc" name="plugin_desc" rows="3"
|
||||
placeholder="Korte omschrijving van wat de plugin doet..."><?= htmlspecialchars($_POST['plugin_desc'] ?? '') ?></textarea>
|
||||
<div class="form-text">Wordt opgeslagen als README.md bij de plugin.</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mb-0">
|
||||
<strong><i class="bi bi-lightbulb"></i> Wat wordt er aangemaakt?</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li><code>plugins/PluginNaam/PluginNaam.php</code> — Hoofdbestand met boilerplate</li>
|
||||
<li><code>plugins/PluginNaam/config.json</code> — Configuratiebestand</li>
|
||||
<li><code>plugins/PluginNaam/README.md</code> — Documentatie (alleen bij omschrijving)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="admin.php?route=plugins" class="btn btn-secondary">Annuleren</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Aanmaken</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,4 +1,78 @@
|
||||
<h2 class="mb-4"><i class="bi bi-plug"></i> Plugins</h2>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-plug"></i> Plugins</h2>
|
||||
<a href="admin.php?route=plugins-new" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Nieuwe plugin
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bi bi-info-circle"></i> Plugin Ontwikkelaarshandleiding</h5>
|
||||
<p class="text-muted mb-2">Een plugin moet aan de volgende eisen voldoen om correct te werken:</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-folder2-open text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Mapstructuur</strong><br>
|
||||
<code class="small">plugins/Naam/Naam.php</code>
|
||||
<small class="text-muted d-block">Mapnaam en bestandsnaam moeten identiek zijn.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-code-square text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Class naam</strong><br>
|
||||
<code class="small">class PluginNaam</code>
|
||||
<small class="text-muted d-block">De class moet exact dezelfde naam hebben als de map.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-puzzle text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Optionele hooks</strong><br>
|
||||
<code class="small">setAPI(CMSAPI)</code> · <code class="small">getSidebarContent()</code>
|
||||
<small class="text-muted d-block">Voor CMS-toegang en sidebar-weergave.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-gear-wide text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Configuratie (optioneel)</strong><br>
|
||||
<code class="small">config.json</code>
|
||||
<small class="text-muted d-block">Wordt getoond met een configuratieformulier in de admin.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-shield-check text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Beveiliging</strong><br>
|
||||
<code class="small">htmlspecialchars()</code>
|
||||
<small class="text-muted d-block">Altijd output escapen. Volg PSR-12.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-book text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Documentatie (optioneel)</strong><br>
|
||||
<code class="small">README.md</code>
|
||||
<small class="text-muted d-block">Aangeraden voor uitleg over de plugin.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($plugins)): ?>
|
||||
<div class="alert alert-info">Geen plugins gevonden in de plugins map.</div>
|
||||
@@ -9,10 +83,10 @@
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><i class="bi bi-plug"></i> <?= htmlspecialchars($plugin['name']) ?></strong>
|
||||
<?php if ($plugin['enabled']): ?>
|
||||
<span class="badge bg-success">Actief</span>
|
||||
<?php if ($plugin['viewable']): ?>
|
||||
<span class="badge bg-success">Zichtbaar</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Inactief</span>
|
||||
<span class="badge bg-secondary">Systeem</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -36,6 +110,32 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="mt-3 d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<?php if ($plugin['has_main']): ?>
|
||||
<a href="admin.php?route=plugins-edit&plugin=<?= urlencode($plugin['name']) ?>" class="btn btn-outline-secondary" title="Bewerken">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="admin.php?route=plugins-toggle&plugin=<?= urlencode($plugin['name']) ?>" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<button type="submit" class="btn btn-sm <?= $plugin['viewable'] ? 'btn-outline-warning' : 'btn-outline-success' ?>" title="<?= $plugin['viewable'] ? 'Verbergen' : 'Tonen' ?>">
|
||||
<i class="bi <?= $plugin['viewable'] ? 'bi-eye-slash' : 'bi-eye' ?>"></i> <?= $plugin['viewable'] ? 'Verberg' : 'Toon' ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php if ($plugin['has_config']): ?>
|
||||
<a href="admin.php?route=plugins-config&plugin=<?= urlencode($plugin['name']) ?>" class="btn btn-outline-primary" title="Configureren">
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="POST" action="admin.php?route=plugins-delete&plugin=<?= urlencode($plugin['name']) ?>" class="d-inline" onsubmit="return confirm('Weet je zeker dat je de plugin '<?= htmlspecialchars($plugin['name']) ?>' wilt verwijderen? Alle bestanden worden permanent verwijderd.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
283
admin/templates/pages/theme.php
Normal file
283
admin/templates/pages/theme.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php if ($editTheme): ?>
|
||||
|
||||
<!-- Edit theme -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> Bewerk thema: <?= htmlspecialchars($editTheme['name'] ?? $editThemeName) ?></h2>
|
||||
<a href="admin.php?route=theme" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=theme" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($editThemeName) ?>">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="theme_name" class="form-label">Themanaam</label>
|
||||
<input type="text" class="form-control" id="theme_name" name="theme_name" value="<?= htmlspecialchars($editTheme['name'] ?? $editThemeName) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Header</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="header_color" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="header_color" name="header_color" value="<?= htmlspecialchars($editTheme['header_color'] ?? '#0a369d') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['header_color'] ?? '#0a369d') ?>" maxlength="7" data-target="header_color">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="header_font_color" class="form-label">Tekst</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="header_font_color" name="header_font_color" value="<?= htmlspecialchars($editTheme['header_font_color'] ?? '#ffffff') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['header_font_color'] ?? '#ffffff') ?>" maxlength="7" data-target="header_font_color">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Navigatie</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="navigation_color" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="navigation_color" name="navigation_color" value="<?= htmlspecialchars($editTheme['navigation_color'] ?? '#2754b4') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['navigation_color'] ?? '#2754b4') ?>" maxlength="7" data-target="navigation_color">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="navigation_font_color" class="form-label">Tekst</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="navigation_font_color" name="navigation_font_color" value="<?= htmlspecialchars($editTheme['navigation_font_color'] ?? '#ffffff') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['navigation_font_color'] ?? '#ffffff') ?>" maxlength="7" data-target="navigation_font_color">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Sidebar</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="sidebar_background" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="sidebar_background" name="sidebar_background" value="<?= htmlspecialchars($editTheme['sidebar_background'] ?? '#f8f9fa') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['sidebar_background'] ?? '#f8f9fa') ?>" maxlength="7" data-target="sidebar_background">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="sidebar_border" class="form-label">Rand</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="sidebar_border" name="sidebar_border" value="<?= htmlspecialchars($editTheme['sidebar_border'] ?? '#dee2e6') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['sidebar_border'] ?? '#dee2e6') ?>" maxlength="7" data-target="sidebar_border">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mt-2">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-arrows-vertical"></i> Hoogte balken</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="header_height" class="form-label">Header hoogte (px)</label>
|
||||
<input type="number" class="form-control" id="header_height" name="header_height" value="<?= htmlspecialchars($editTheme['header_height'] ?? '56') ?>" min="32" max="200">
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="nav_height" class="form-label">Navigatie hoogte (px)</label>
|
||||
<input type="number" class="form-control" id="nav_height" name="nav_height" value="<?= htmlspecialchars($editTheme['nav_height'] ?? '42') ?>" min="24" max="200">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-image"></i> Header achtergrond afbeelding</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="bg_image" class="form-label">Upload afbeelding</label>
|
||||
<input type="file" class="form-control" id="bg_image" name="bg_image" accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml">
|
||||
<small class="form-text text-muted">Toegestaan: JPG, PNG, GIF, WebP, SVG</small>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="background_image_url" class="form-label">Of URL naar afbeelding</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="background_image_url" name="background_image_url" placeholder="https://..." value="<?= htmlspecialchars(str_starts_with($editTheme['background_image'] ?? '', 'http') ? $editTheme['background_image'] : '') ?>">
|
||||
</div>
|
||||
<?php if (!empty($editTheme['background_image'])): ?>
|
||||
<div class="mt-2 d-flex align-items-center gap-3">
|
||||
<div>
|
||||
<small class="text-muted">Huidig:</small>
|
||||
<img src="<?= htmlspecialchars(str_starts_with($editTheme['background_image'], 'http') ? $editTheme['background_image'] : '/themes/' . $editTheme['background_image']) ?>" style="max-height: 60px; max-width: 200px;" class="img-thumbnail mt-1 d-block">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="btn-check" type="checkbox" id="bg_image_remove" name="bg_image_remove" value="1" autocomplete="off">
|
||||
<label class="btn btn-outline-danger btn-sm" for="bg_image_remove">
|
||||
<i class="bi bi-trash3"></i> Verwijder afbeelding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="background_image_opacity" class="form-label">Doorzichtigheid (%)</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="range" class="form-range" style="max-width: 200px;" id="background_image_opacity" name="background_image_opacity" min="0" max="100" value="<?= htmlspecialchars($editTheme['background_image_opacity'] ?? '100') ?>" oninput="this.nextElementSibling.textContent=this.value+'%'">
|
||||
<span class="badge bg-secondary"><?= htmlspecialchars($editTheme['background_image_opacity'] ?? '100') ?>%</span>
|
||||
</div>
|
||||
<small class="form-text text-muted">100% = volledig zichtbaar, 50% = half doorzichtig, 0% = onzichtbaar</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Opslaan
|
||||
</button>
|
||||
<a href="admin.php?route=theme" class="btn btn-outline-secondary">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Theme list -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> Thema's</h2>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#newThemeForm">
|
||||
<i class="bi bi-plus-lg"></i> Nieuw thema
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse mb-4" id="newThemeForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="admin.php?route=theme" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="col-md-6">
|
||||
<label for="new_name" class="form-label">Naam nieuw thema</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name" placeholder="bijv. donker" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="bi bi-plus-lg"></i> Aanmaken
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php foreach ($themes as $themeName => $themeData): ?>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h5 class="card-title mb-0"><?= htmlspecialchars($themeData['name'] ?? $themeName) ?></h5>
|
||||
<?php if ($themeName === $activeTheme): ?>
|
||||
<span class="badge bg-success">Actief</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Color preview swatches -->
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center gap-1 mb-2">
|
||||
<span class="small text-muted" style="width: 70px;">Header:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['header_color'] ?? '#0a369d') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['header_color'] ?? '#0a369d') ?></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 mb-2">
|
||||
<span class="small text-muted" style="width: 70px;">Navigatie:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['navigation_color'] ?? '#2754b4') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['navigation_color'] ?? '#2754b4') ?></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="small text-muted" style="width: 70px;">Sidebar:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['sidebar_background'] ?? '#f8f9fa') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['sidebar_background'] ?? '#f8f9fa') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heights and background image -->
|
||||
<div class="mb-3 small">
|
||||
<div class="text-muted mb-1">
|
||||
<i class="bi bi-arrows-vertical"></i> Header: <?= htmlspecialchars($themeData['header_height'] ?? '56') ?>px · Navigatie: <?= htmlspecialchars($themeData['nav_height'] ?? '42') ?>px
|
||||
</div>
|
||||
<?php if (!empty($themeData['background_image'])): ?>
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-image"></i> Achtergrond: <?= htmlspecialchars($themeData['background_image_opacity'] ?? '100') ?>% zichtbaar
|
||||
<?php if (!str_starts_with($themeData['background_image'], 'http')): ?>
|
||||
<img src="/themes/<?= htmlspecialchars($themeData['background_image']) ?>" style="max-height: 30px; max-width: 80px;" class="img-thumbnail ms-1 align-middle">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="admin.php?route=theme&edit=<?= urlencode($themeName) ?>" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Bewerken
|
||||
</a>
|
||||
<?php if ($themeName !== $activeTheme): ?>
|
||||
<form method="POST" action="admin.php?route=theme">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="activate">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($themeName) ?>">
|
||||
<button type="submit" class="btn btn-outline-success btn-sm">
|
||||
<i class="bi bi-check-circle"></i> Activeren
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if ($themeName !== 'default'): ?>
|
||||
<form method="POST" action="admin.php?route=theme">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($themeName) ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Weet je zeker dat je thema '<?= htmlspecialchars($themeData['name'] ?? $themeName) ?>' wilt verwijderen?')">
|
||||
<i class="bi bi-trash"></i> Verwijderen
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.form-control-color-value').forEach(function(input) {
|
||||
input.addEventListener('input', function() {
|
||||
var target = document.getElementById(this.dataset.target);
|
||||
if (target && /^#[0-9a-fA-F]{6}$/.test(this.value)) {
|
||||
target.value = this.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.form-control-color').forEach(function(input) {
|
||||
input.addEventListener('input', function() {
|
||||
var target = document.querySelector('[data-target="' + this.id + '"]');
|
||||
if (target) target.value = this.value;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -333,7 +333,7 @@ This document outlines comprehensive functional tests for CodePress CMS to verif
|
||||
|
||||
**Steps:**
|
||||
1. Try accessing `/content/` directly
|
||||
2. Try accessing `/engine/` files
|
||||
2. Try accessing `/cms/` files
|
||||
3. Try accessing `config.php`
|
||||
4. Try accessing `/vendor/`
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ test_vulnerability \
|
||||
|
||||
test_vulnerability \
|
||||
"Path traversal - config access" \
|
||||
"$TARGET/?page=../engine/core/config" \
|
||||
"$TARGET/?page=../cms/core/config" \
|
||||
"content_dir" \
|
||||
"true"
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class CodePressCMS {
|
||||
$this->currentLanguage = $this->getCurrentLanguage();
|
||||
$this->translations = $this->loadTranslations($this->currentLanguage);
|
||||
|
||||
// Initialize plugin manager (files already loaded in engine/core/index.php)
|
||||
// Initialize plugin manager (files already loaded in cms/core/index.php)
|
||||
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins');
|
||||
$api = new CMSAPI($this);
|
||||
$this->pluginManager->setAPI($api);
|
||||
@@ -182,7 +182,10 @@ class CodePressCMS {
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
|
||||
// Skip assets directory (old name, kept for safety)
|
||||
if ($item === 'assets' && is_dir($dir . '/' . $item)) continue;
|
||||
|
||||
// Skip language-specific content that doesn't match current language
|
||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||
@@ -640,6 +643,12 @@ class CodePressCMS {
|
||||
* @return string Formatted display name
|
||||
*/
|
||||
private function formatDisplayName($filename) {
|
||||
// Preserve leading dash before processing
|
||||
$hasLeadingDash = $filename[0] === '-';
|
||||
if ($hasLeadingDash) {
|
||||
$filename = substr($filename, 1);
|
||||
}
|
||||
|
||||
// Remove language prefixes dynamically based on available languages
|
||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
|
||||
@@ -656,11 +665,12 @@ class CodePressCMS {
|
||||
'ict' => 'ICT',
|
||||
];
|
||||
if (isset($specialCases[strtolower($filename)])) {
|
||||
return $specialCases[strtolower($filename)];
|
||||
return ($hasLeadingDash ? '- ' : '') . $specialCases[strtolower($filename)];
|
||||
}
|
||||
|
||||
// Replace hyphens and underscores with spaces, then title case
|
||||
$name = str_replace(['-', '_'], ' ', $filename);
|
||||
$name = trim($name);
|
||||
$name = ucwords(strtolower($name));
|
||||
|
||||
// Post-process special cases in compound names
|
||||
@@ -668,7 +678,7 @@ class CodePressCMS {
|
||||
$name = str_ireplace(ucfirst($lower), $correct, $name);
|
||||
}
|
||||
|
||||
return $name;
|
||||
return ($hasLeadingDash ? '- ' : '') . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -983,8 +993,12 @@ private function getGuidePage() {
|
||||
// Get homepage title
|
||||
$homepageTitle = $this->getHomepageTitle();
|
||||
|
||||
// Get sidebar content from plugins
|
||||
$sidebarContent = $this->pluginManager->getSidebarContent();
|
||||
// Get sidebar content from plugins (filtered per-page if specified in frontmatter)
|
||||
$allowedPlugins = null;
|
||||
if (!empty($page['metadata']['plugins'])) {
|
||||
$allowedPlugins = array_map('trim', explode(',', $page['metadata']['plugins']));
|
||||
}
|
||||
$sidebarContent = $this->pluginManager->getSidebarContent($allowedPlugins);
|
||||
|
||||
// Get layout from page metadata
|
||||
$layout = $page['layout'] ?? 'sidebar-content';
|
||||
@@ -994,7 +1008,7 @@ private function getGuidePage() {
|
||||
'site_title' => $this->config['site_title'],
|
||||
'page_title' => htmlspecialchars($page['title']),
|
||||
|
||||
'content' => $page['content'],
|
||||
'content' => $this->processContent($page['content']),
|
||||
'sidebar_content' => $sidebarContent,
|
||||
'layout' => $layout,
|
||||
'page_metadata' => $page['metadata'] ?? [],
|
||||
@@ -1017,10 +1031,14 @@ private function getGuidePage() {
|
||||
// Theme colors
|
||||
'header_color' => $this->config['theme']['header_color'] ?? '#0d6efd',
|
||||
'header_font_color' => $this->config['theme']['header_font_color'] ?? '#ffffff',
|
||||
'header_height' => $this->config['theme']['header_height'] ?? '56',
|
||||
'navigation_color' => $this->config['theme']['navigation_color'] ?? '#f8f9fa',
|
||||
'navigation_font_color' => $this->config['theme']['navigation_font_color'] ?? '#000000',
|
||||
'nav_height' => $this->config['theme']['nav_height'] ?? '42',
|
||||
'sidebar_background' => $this->config['theme']['sidebar_background'] ?? '#f8f9fa',
|
||||
'sidebar_border' => $this->config['theme']['sidebar_border'] ?? '#dee2e6',
|
||||
'background_image_css' => $this->getBackgroundImageCss(),
|
||||
'background_image_opacity' => $this->getBackgroundImageOpacity(),
|
||||
// Language
|
||||
'current_lang' => $this->currentLanguage,
|
||||
'current_lang_upper' => strtoupper($this->currentLanguage),
|
||||
@@ -1286,4 +1304,27 @@ private function getGuidePage() {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getBackgroundImageCss(): string
|
||||
{
|
||||
$bg = $this->config['theme']['background_image'] ?? '';
|
||||
if (empty($bg)) {
|
||||
return 'none';
|
||||
}
|
||||
if (str_starts_with($bg, 'http')) {
|
||||
return 'url(' . $bg . ')';
|
||||
}
|
||||
return 'url(/themes/' . $bg . ')';
|
||||
}
|
||||
|
||||
private function getBackgroundImageOpacity(): int
|
||||
{
|
||||
$opacity = intval($this->config['theme']['background_image_opacity'] ?? 100);
|
||||
return max(0, min(100, $opacity));
|
||||
}
|
||||
|
||||
private function processContent(string $content): string
|
||||
{
|
||||
return str_replace('-/assets/', '/-assets/', $content);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,17 @@ if (file_exists($configJsonPath)) {
|
||||
$config['templates_dir'] = $projectRoot . $config['templates_dir'];
|
||||
}
|
||||
|
||||
// Load active theme
|
||||
$activeTheme = $config['active_theme'] ?? 'default';
|
||||
$themeDir = __DIR__ . '/../../themes/' . $activeTheme;
|
||||
$themeFile = $themeDir . '/theme.json';
|
||||
if (file_exists($themeFile)) {
|
||||
$themeConfig = json_decode(file_get_contents($themeFile), true);
|
||||
$config['theme'] = $themeConfig;
|
||||
} else {
|
||||
$config['theme'] = [];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ class PluginManager
|
||||
{
|
||||
$this->api = $api;
|
||||
|
||||
// Inject API into all plugins that have setAPI method
|
||||
foreach ($this->plugins as $plugin) {
|
||||
if (method_exists($plugin, 'setAPI')) {
|
||||
$plugin->setAPI($api);
|
||||
@@ -43,7 +42,6 @@ class PluginManager
|
||||
if (class_exists($className)) {
|
||||
$this->plugins[$pluginName] = new $className();
|
||||
|
||||
// Inject API if already available
|
||||
if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) {
|
||||
$this->plugins[$pluginName]->setAPI($this->api);
|
||||
}
|
||||
@@ -62,14 +60,49 @@ class PluginManager
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
public function getSidebarContent(): string
|
||||
public function isPluginViewable(object $plugin): bool
|
||||
{
|
||||
if (method_exists($plugin, 'getConfig')) {
|
||||
$config = $plugin->getConfig();
|
||||
return !isset($config['viewable']) || $config['viewable'] !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getSidebarContent(?array $allowedPlugins = null): string
|
||||
{
|
||||
$sidebarContent = '';
|
||||
|
||||
foreach ($this->plugins as $plugin) {
|
||||
if (method_exists($plugin, 'getSidebarContent')) {
|
||||
$sidebarContent .= $plugin->getSidebarContent();
|
||||
foreach ($this->plugins as $pluginName => $plugin) {
|
||||
if (!$this->isPluginViewable($plugin) || !method_exists($plugin, 'getSidebarContent')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter by allowed plugins for this page
|
||||
if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = $plugin->getSidebarContent();
|
||||
if (trim($content) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = 'Plugin';
|
||||
if (method_exists($plugin, 'getConfig')) {
|
||||
$config = $plugin->getConfig();
|
||||
$title = $config['title'] ?? 'Plugin';
|
||||
}
|
||||
|
||||
$sidebarContent .= '
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">' . htmlspecialchars($title) . '</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
' . $content . '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
return $sidebarContent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<header class="navbar navbar-expand-lg navbar-dark" style="background-color: var(--header-bg);">
|
||||
<header id="site-header" class="navbar navbar-expand-lg navbar-dark" style="background-color: transparent;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="?page={{default_page}}&lang={{current_lang}}">
|
||||
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
||||
|
||||
@@ -76,11 +76,45 @@
|
||||
|
||||
<!-- Dynamic theme colors -->
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#site-header {
|
||||
background-image: {{background_image_css}};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#site-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--header-bg);
|
||||
opacity: calc((100 - {{background_image_opacity}}) / 100);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#site-header > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:root {
|
||||
--header-bg: {{header_color}};
|
||||
--header-font: {{header_font_color}};
|
||||
--header-height: {{header_height}}px;
|
||||
--nav-bg: {{navigation_color}};
|
||||
--nav-font: {{navigation_font_color}};
|
||||
--nav-height: {{nav_height}}px;
|
||||
--sidebar-bg: {{sidebar_background}};
|
||||
--sidebar-border: {{sidebar_border}};
|
||||
}
|
||||
@@ -88,6 +122,7 @@
|
||||
/* Header styles */
|
||||
.navbar {
|
||||
background-color: var(--header-bg) !important;
|
||||
min-height: var(--header-height);
|
||||
}
|
||||
|
||||
.navbar .navbar-brand,
|
||||
@@ -209,6 +244,7 @@
|
||||
.navigation-section {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-font) !important;
|
||||
min-height: var(--nav-height);
|
||||
}
|
||||
|
||||
/* Enhanced accessibility styles */
|
||||
@@ -290,7 +326,7 @@
|
||||
border-right: 1px solid var(--sidebar-border) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -310,7 +346,7 @@
|
||||
|
||||
/* Ensure full height layout */
|
||||
.main-content {
|
||||
min-height: calc(100vh - 200px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@@ -380,9 +416,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header role="banner" id="site-header">
|
||||
{{>header}}
|
||||
</header>
|
||||
|
||||
<nav role="navigation" aria-label="Main navigation" id="site-navigation">
|
||||
{{>navigation}}
|
||||
|
||||
18
config.json
18
config.json
@@ -1,19 +1,15 @@
|
||||
{
|
||||
"site_title": "CodePress",
|
||||
"content_dir": "content",
|
||||
"templates_dir": "engine/templates",
|
||||
"templates_dir": "cms\/templates",
|
||||
"default_page": "index",
|
||||
"theme": {
|
||||
"header_color": "#0a369d",
|
||||
"header_font_color": "#ffffff",
|
||||
"navigation_color": "#2754b4",
|
||||
"navigation_font_color": "#ffffff",
|
||||
"sidebar_background": "#f8f9fa",
|
||||
"sidebar_border": "#dee2e6"
|
||||
},
|
||||
"active_theme": "default",
|
||||
"language": {
|
||||
"default": "nl",
|
||||
"available": ["nl", "en"]
|
||||
"available": [
|
||||
"nl",
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"seo": {
|
||||
"description": "CodePress CMS - Lightweight file-based content management system",
|
||||
@@ -21,7 +17,7 @@
|
||||
},
|
||||
"author": {
|
||||
"name": "E. Noorlander",
|
||||
"website": "https://noorlander.info"
|
||||
"website": "https:\/\/noorlander.info"
|
||||
},
|
||||
"features": {
|
||||
"auto_link_pages": true,
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
## Kritiek
|
||||
|
||||
- [x] **Path traversal fix** - `str_replace('../')` in `getPage()` is te omzeilen. Gebruik `realpath()` met prefix-check (`CodePressCMS.php:313`)
|
||||
- [x] **JWT secret fallback** - Standaard `'your-secret-key-change-in-production'` maakt tokens forgeable (`admin-console/config/app.php:11`)
|
||||
- [x] **JWT secret fallback** - Standaard `'your-secret-key-change-in-production'` maakt tokens forgeable (`admin/config/app.php:11`)
|
||||
- [x] **executePhpFile() onveilig** - Open `include` wrapper zonder pad-restrictie (`CMSAPI.php:164`)
|
||||
- [ ] **Plugin auto-loading** - Elke map in `plugins/` wordt blind geladen zonder allowlist of validatie (`PluginManager.php:40`)
|
||||
|
||||
## Hoog
|
||||
|
||||
- [x] **IP spoofing** - `X-Forwarded-For` header wordt blind vertrouwd in MQTTTracker (`MQTTTracker.php:211`)
|
||||
- [x] **Debug hardcoded** - `'debug' => true` hardcoded in admin config (`admin-console/config/app.php:6`)
|
||||
- [x] **Debug hardcoded** - `'debug' => true` hardcoded in admin config (`admin/config/app.php:6`)
|
||||
- [x] **Cookie security** - Cookies zonder `Secure`/`HttpOnly`/`SameSite` flags (`MQTTTracker.php:70`)
|
||||
- [ ] **autoLinkPageTitles()** - Regex kan geneste `<a>` tags produceren (`CodePressCMS.php:587`)
|
||||
- [ ] **MQTT wachtwoord** - Credentials in plain text JSON (`MQTTTracker.php:37`)
|
||||
|
||||
@@ -24,11 +24,6 @@ class HTMLBlock
|
||||
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
|
||||
|
||||
$content = '
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">' . $this->config['title'] . '</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
|
||||
<p class="mb-2"><strong>Taal:</strong> ' . strtoupper($currentLang) . '</p>
|
||||
<p class="mb-3"><strong>Homepage:</strong> ' . ($isHomepage ? 'Ja' : 'Nee') . '</p>';
|
||||
|
||||
@@ -31,6 +31,7 @@ class MQTTTracker
|
||||
$configFile = __DIR__ . '/config.json';
|
||||
$this->config = [
|
||||
'enabled' => true,
|
||||
'viewable' => false,
|
||||
'broker_host' => 'localhost',
|
||||
'broker_port' => 1883,
|
||||
'client_id' => 'codepress_cms',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"broker_host": "mqtt.prive.noorlander.info",
|
||||
"broker_port": 1883,
|
||||
"viewable": false,
|
||||
"broker_host": "mqtt.lan.noorlander.info",
|
||||
"broker_port": "1883",
|
||||
"client_id": "codepress_cms",
|
||||
"username": "",
|
||||
"password": "",
|
||||
@@ -10,6 +11,6 @@
|
||||
"track_pages": true,
|
||||
"track_performance": true,
|
||||
"track_user_flows": true,
|
||||
"session_timeout": 1800,
|
||||
"geoip_database_path": "/plugins/MQTTTracker/GeoLite2-Country.mmdb"
|
||||
"session_timeout": "1800",
|
||||
"geoip_database_path": "\/plugins\/MQTTTracker\/GeoLite2-Country.mmdb"
|
||||
}
|
||||
3
plugins/test/README.md
Normal file
3
plugins/test/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# test
|
||||
|
||||
Dit is een test plugin
|
||||
3
plugins/test/config.json
Normal file
3
plugins/test/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
35
plugins/test/test.php
Normal file
35
plugins/test/test.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
class test
|
||||
{
|
||||
private ?CMSAPI $api = null;
|
||||
private array $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = [
|
||||
'title' => 'Dit is een test plugin',
|
||||
'viewable' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function setAPI(CMSAPI $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
return 'Hallo';
|
||||
}
|
||||
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,13 @@
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule ^assets/.*$ - [L]
|
||||
|
||||
# Block direct access to all content files
|
||||
# Allow access to content assets
|
||||
RewriteRule ^content/assets/.*$ - [L]
|
||||
|
||||
# Serve files from content/-assets/ via index.php
|
||||
RewriteRule ^-assets/.*$ index.php [L]
|
||||
|
||||
# Block direct access to all other content files
|
||||
RewriteRule ^content/.*$ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
|
||||
913
public/admin.php
913
public/admin.php
File diff suppressed because it is too large
Load Diff
1
public/assets/codemirror/addon/edit/closebrackets.min.js
vendored
Normal file
1
public/assets/codemirror/addon/edit/closebrackets.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(S){var n={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},k=S.Pos;function y(e,t){return"pairs"==t&&"string"==typeof e?e:("object"==typeof e&&null!=e[t]?e:n)[t]}S.defineOption("autoCloseBrackets",!1,function(e,t,n){n&&n!=S.Init&&(e.removeKeyMap(i),e.state.closeBrackets=null),t&&(r(y(t,"pairs")),e.state.closeBrackets=t,e.addKeyMap(i))});var i={Backspace:function(e){var t=O(e);if(!t||e.getOption("disableInput"))return S.Pass;for(var n=y(t,"pairs"),r=e.listSelections(),i=0;i<r.length;i++){if(!r[i].empty())return S.Pass;var a=s(e,r[i].head);if(!a||n.indexOf(a)%2!=0)return S.Pass}for(i=r.length-1;0<=i;i--){var o=r[i].head;e.replaceRange("",k(o.line,o.ch-1),k(o.line,o.ch+1),"+delete")}},Enter:function(r){var e=O(r),t=e&&y(e,"explode");if(!t||r.getOption("disableInput"))return S.Pass;for(var i=r.listSelections(),n=0;n<i.length;n++){if(!i[n].empty())return S.Pass;var a=s(r,i[n].head);if(!a||t.indexOf(a)%2!=0)return S.Pass}r.operation(function(){var e=r.lineSeparator()||"\n";r.replaceSelection(e+e,null),m(r,-1),i=r.listSelections();for(var t=0;t<i.length;t++){var n=i[t].head.line;r.indentLine(n,null,!0),r.indentLine(n+1,null,!0)}})}};function r(e){for(var t=0;t<e.length;t++){var n=e.charAt(t),r="'"+n+"'";i[r]||(i[r]=function(P){return function(e){var i=e,t=P,e=O(i);if(!e||i.getOption("disableInput"))return S.Pass;var n=y(e,"pairs"),r=n.indexOf(t);if(-1==r)return S.Pass;for(var a,o=y(e,"closeBefore"),s=y(e,"triples"),l=n.charAt(r+1)==t,c=i.listSelections(),h=r%2==0,f=0;f<c.length;f++){var u,d=c[f],p=d.head,g=i.getRange(p,k(p.line,p.ch+1));if(h&&!d.empty())u="surround";else if(!l&&h||g!=t)if(l&&1<p.ch&&0<=s.indexOf(t)&&i.getRange(k(p.line,p.ch-2),p)==t+t){if(2<p.ch&&/\bstring/.test(i.getTokenTypeAt(k(p.line,p.ch-2))))return S.Pass;u="addFour"}else if(l){d=0==p.ch?" ":i.getRange(k(p.line,p.ch-1),p);if(S.isWordChar(g)||d==t||S.isWordChar(d))return S.Pass;u="both"}else{if(!h||!(0===g.length||/\s/.test(g)||-1<o.indexOf(g)))return S.Pass;u="both"}else u=l&&function(e,t){var n=e.getTokenAt(k(t.line,t.ch+1));return/\bstring/.test(n.type)&&n.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}(i,p)?"both":0<=s.indexOf(t)&&i.getRange(p,k(p.line,p.ch+3))==t+t+t?"skipThree":"skip";if(a){if(a!=u)return S.Pass}else a=u}var v=r%2?n.charAt(r-1):t,b=r%2?t:n.charAt(r+1);i.operation(function(){if("skip"==a)m(i,1);else if("skipThree"==a)m(i,3);else if("surround"==a){for(var e=i.getSelections(),t=0;t<e.length;t++)e[t]=v+e[t]+b;i.replaceSelections(e,"around");for(e=i.listSelections().slice(),t=0;t<e.length;t++)e[t]=(n=e[t],r=void 0,r=0<S.cmpPos(n.anchor,n.head),{anchor:new k(n.anchor.line,n.anchor.ch+(r?-1:1)),head:new k(n.head.line,n.head.ch+(r?1:-1))});i.setSelections(e)}else"both"==a?(i.replaceSelection(v+b,null),i.triggerElectric(v+b),m(i,-1)):"addFour"==a&&(i.replaceSelection(v+v+v+v,"before"),m(i,1));var n,r})}}(n))}}function O(e){var t=e.state.closeBrackets;return t&&!t.override&&e.getModeAt(e.getCursor()).closeBrackets||t}function m(e,t){for(var n=[],r=e.listSelections(),i=0,a=0;a<r.length;a++){var o=r[a],o=(o.head==e.getCursor()&&(i=a),o.head.ch||0<t?{line:o.head.line,ch:o.head.ch+t}:{line:o.head.line-1});n.push({anchor:o,head:o})}e.setSelections(n,i)}function s(e,t){e=e.getRange(k(t.line,t.ch-1),k(t.line,t.ch+1));return 2==e.length?e:null}r(n.pairs+"`")});
|
||||
1
public/assets/codemirror/addon/selection/active-line.min.js
vendored
Normal file
1
public/assets/codemirror/addon/selection/active-line.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(i){"use strict";var a="CodeMirror-activeline",s="CodeMirror-activeline-background",c="CodeMirror-activeline-gutter";function l(e){for(var t=0;t<e.state.activeLines.length;t++)e.removeLineClass(e.state.activeLines[t],"wrap",a),e.removeLineClass(e.state.activeLines[t],"background",s),e.removeLineClass(e.state.activeLines[t],"gutter",c)}function o(t,e){for(var n=[],i=0;i<e.length;i++){var o=e[i],r=t.getOption("styleActiveLine");("object"==typeof r&&r.nonEmpty?o.anchor.line==o.head.line:o.empty())&&(r=t.getLineHandleVisualStart(o.head.line),n[n.length-1]!=r&&n.push(r))}!function(e,t){if(e.length==t.length){for(var n=0;n<e.length;n++)if(e[n]!=t[n])return;return 1}}(t.state.activeLines,n)&&t.operation(function(){l(t);for(var e=0;e<n.length;e++)t.addLineClass(n[e],"wrap",a),t.addLineClass(n[e],"background",s),t.addLineClass(n[e],"gutter",c);t.state.activeLines=n})}function r(e,t){o(e,t.ranges)}i.defineOption("styleActiveLine",!1,function(e,t,n){n=n!=i.Init&&n;t!=n&&(n&&(e.off("beforeSelectionChange",r),l(e),delete e.state.activeLines),t&&(e.state.activeLines=[],o(e,e.listSelections()),e.on("beforeSelectionChange",r)))})});
|
||||
1
public/assets/codemirror/codemirror.min.css
vendored
Normal file
1
public/assets/codemirror/codemirror.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/codemirror.min.js
vendored
Normal file
1
public/assets/codemirror/codemirror.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/clike.min.js
vendored
Normal file
1
public/assets/codemirror/mode/clike.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/css.min.js
vendored
Normal file
1
public/assets/codemirror/mode/css.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/htmlmixed.min.js
vendored
Normal file
1
public/assets/codemirror/mode/htmlmixed.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
|
||||
1
public/assets/codemirror/mode/javascript.min.js
vendored
Normal file
1
public/assets/codemirror/mode/javascript.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/markdown.min.js
vendored
Normal file
1
public/assets/codemirror/mode/markdown.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/php.min.js
vendored
Normal file
1
public/assets/codemirror/mode/php.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/codemirror/mode/xml.min.js
vendored
Normal file
1
public/assets/codemirror/mode/xml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
52
public/assets/css/editor.css
Normal file
52
public/assets/css/editor.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
min-height: 500px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-bottom: none;
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.editor-toolbar .vr {
|
||||
height: 22px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.editor-toolbar .btn {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
border-radius: 0 0 0.25rem 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-wrapper .CodeMirror {
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.card-media-item {
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.card-media-item:hover {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
233
public/assets/js/editor-toolbar.js
Normal file
233
public/assets/js/editor-toolbar.js
Normal file
@@ -0,0 +1,233 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var textarea = document.getElementById('editor-textarea');
|
||||
if (!textarea || typeof CodeMirror === 'undefined') return;
|
||||
|
||||
var ext = textarea.dataset.ext || 'md';
|
||||
|
||||
var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php' };
|
||||
|
||||
var editor = CodeMirror.fromTextArea(textarea, {
|
||||
mode: modeMap[ext] || 'markdown',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
styleActiveLine: true,
|
||||
indentUnit: 4,
|
||||
tabSize: 4,
|
||||
indentWithTabs: true,
|
||||
viewportMargin: Infinity,
|
||||
extraKeys: {
|
||||
'Ctrl-S': function () { document.getElementById('editor-form').submit(); },
|
||||
'Cmd-S': function () { document.getElementById('editor-form').submit(); }
|
||||
}
|
||||
});
|
||||
|
||||
var commands = {
|
||||
md: [
|
||||
{ cmd: 'bold', icon: 'bi-type-bold', title: 'Vet' },
|
||||
{ cmd: 'italic', icon: 'bi-type-italic', title: 'Cursief' },
|
||||
{ cmd: 'heading', icon: 'bi-type-h2', title: 'Kop' },
|
||||
{ cmd: 'link', icon: 'bi-link-45deg', title: 'Link' },
|
||||
null,
|
||||
{ cmd: 'ulist', icon: 'bi-list-ul', title: 'Ongenummerde lijst' },
|
||||
{ cmd: 'olist', icon: 'bi-list-ol', title: 'Genummerde lijst' },
|
||||
{ cmd: 'code', icon: 'bi-code-slash', title: 'Code' },
|
||||
{ cmd: 'quote', icon: 'bi-chat-quote', title: 'Citaat' },
|
||||
null,
|
||||
{ cmd: 'hr', icon: 'bi-hr', title: 'Horizontale lijn' },
|
||||
null,
|
||||
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
|
||||
],
|
||||
html: [
|
||||
{ cmd: 'strong', icon: 'bi-type-bold', title: '<strong>' },
|
||||
{ cmd: 'em', icon: 'bi-type-italic', title: '<em>' },
|
||||
null,
|
||||
{ cmd: 'link', icon: 'bi-link-45deg', title: 'Link' },
|
||||
{ cmd: 'image', icon: 'bi-image', title: 'Afbeelding' },
|
||||
null,
|
||||
{ cmd: 'comment', icon: 'bi-chat-square-dots', title: 'Commentaar' },
|
||||
null,
|
||||
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
|
||||
],
|
||||
php: [
|
||||
{ cmd: 'comment_php', icon: 'bi-slash-circle', title: '// Commentaar' },
|
||||
{ cmd: 'docblock', icon: 'bi-blockquote-left', title: '/** DocBlock */' },
|
||||
null,
|
||||
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
|
||||
]
|
||||
};
|
||||
|
||||
function wrap(editor, before, after) {
|
||||
var sel = editor.getSelection();
|
||||
editor.replaceSelection(before + sel + after);
|
||||
if (!sel) {
|
||||
var cur = editor.getCursor();
|
||||
editor.setCursor({ line: cur.line, ch: cur.ch - after.length });
|
||||
}
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function insert(editor, text, cursorOffset) {
|
||||
editor.replaceSelection(text);
|
||||
if (cursorOffset !== undefined) {
|
||||
var cur = editor.getCursor();
|
||||
editor.setCursor({ line: cur.line, ch: cur.ch - text.length + cursorOffset });
|
||||
}
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function prependLine(editor, prefix) {
|
||||
var cur = editor.getCursor();
|
||||
editor.replaceRange(prefix, { line: cur.line, ch: 0 }, { line: cur.line, ch: 0 });
|
||||
editor.setCursor({ line: cur.line, ch: prefix.length });
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function execute(cmd) {
|
||||
var sel = editor.getSelection();
|
||||
|
||||
switch (cmd) {
|
||||
case 'bold':
|
||||
sel ? wrap(editor, '**', '**') : insert(editor, '****', 2);
|
||||
break;
|
||||
case 'italic':
|
||||
sel ? wrap(editor, '*', '*') : insert(editor, '**', 1);
|
||||
break;
|
||||
case 'heading':
|
||||
prependLine(editor, '## ');
|
||||
break;
|
||||
case 'link':
|
||||
sel ? wrap(editor, '[', '](url)') : insert(editor, '[linktekst](url)', 1);
|
||||
break;
|
||||
case 'ulist':
|
||||
prependLine(editor, '- ');
|
||||
break;
|
||||
case 'olist':
|
||||
prependLine(editor, '1. ');
|
||||
break;
|
||||
case 'code':
|
||||
sel ? wrap(editor, '`', '`') : insert(editor, '``', 1);
|
||||
break;
|
||||
case 'quote':
|
||||
prependLine(editor, '> ');
|
||||
break;
|
||||
case 'hr':
|
||||
insert(editor, '\n---\n', 0);
|
||||
break;
|
||||
case 'strong':
|
||||
sel ? wrap(editor, '<strong>', '</strong>') : insert(editor, '<strong></strong>', 8);
|
||||
break;
|
||||
case 'em':
|
||||
sel ? wrap(editor, '<em>', '</em>') : insert(editor, '<em></em>', 4);
|
||||
break;
|
||||
case 'image':
|
||||
insert(editor, '<img src="" alt="">', 10);
|
||||
break;
|
||||
case 'comment':
|
||||
sel ? wrap(editor, '<!-- ', ' -->') : insert(editor, '<!-- -->', 4);
|
||||
break;
|
||||
case 'comment_php':
|
||||
if (sel) {
|
||||
sel.indexOf('\n') !== -1
|
||||
? insert(editor, '/* ' + sel + ' */', 0)
|
||||
: insert(editor, '// ' + sel, 0);
|
||||
} else {
|
||||
prependLine(editor, '// ');
|
||||
}
|
||||
break;
|
||||
case 'docblock':
|
||||
insert(editor, '/**\n * \n */', 7);
|
||||
break;
|
||||
case 'media':
|
||||
var modal = new bootstrap.Modal(document.getElementById('mediaModal'));
|
||||
if (modal) modal.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function buildToolbar(ext) {
|
||||
var toolbar = document.getElementById('editor-toolbar');
|
||||
if (!toolbar) return;
|
||||
|
||||
toolbar.innerHTML = '';
|
||||
var cmds = commands[ext] || [];
|
||||
cmds.forEach(function (item) {
|
||||
if (item === null) {
|
||||
var sep = document.createElement('div');
|
||||
sep.className = 'vr';
|
||||
toolbar.appendChild(sep);
|
||||
return;
|
||||
}
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-sm btn-outline-secondary';
|
||||
btn.title = item.title;
|
||||
btn.innerHTML = '<i class="bi ' + item.icon + '"></i>';
|
||||
btn.addEventListener('click', function () { execute(item.cmd); });
|
||||
toolbar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
var templates = {
|
||||
md: '# Nieuwe Pagina\n\nSchrijf hier je inhoud...\n',
|
||||
html: '<h1>Nieuwe Pagina</h1>\n<p>Dit is een HTML content pagina.</p>\n',
|
||||
php: '---\ntitle: Nieuwe Pagina\n---\n\n<?php\n$pageTitle = "Nieuwe Pagina";\n?>\n\n<h1><?= $pageTitle ?></h1>\n<p>PHP content - alles wat je hier echo't wordt weergegeven.</p>\n\n<?php\n$items = [\'Item 1\', \'Item 2\', \'Item 3\'];\n?>\n<ul>\n<?php foreach ($items as $item): ?>\n <li><?= $item ?></li>\n<?php endforeach; ?>\n</ul>\n'
|
||||
};
|
||||
|
||||
var defaultContents = {};
|
||||
|
||||
function getTemplate(ext) {
|
||||
return templates[ext] || '';
|
||||
}
|
||||
|
||||
function setDefaultContent(ext) {
|
||||
var tpl = getTemplate(ext);
|
||||
editor.setValue(tpl);
|
||||
editor.setCursor({ line: 0, ch: 0 });
|
||||
editor.focus();
|
||||
defaultContents[ext] = tpl;
|
||||
}
|
||||
|
||||
function switchMode(ext, forceContent) {
|
||||
var mode = modeMap[ext] || 'markdown';
|
||||
editor.setOption('mode', mode);
|
||||
textarea.dataset.ext = ext;
|
||||
|
||||
if (forceContent) {
|
||||
setDefaultContent(ext);
|
||||
} else {
|
||||
var cur = editor.getValue().trim();
|
||||
var expected = defaultContents[ext];
|
||||
if (cur === '' || (expected && cur === expected.trim())) {
|
||||
setDefaultContent(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.on('change', function () {
|
||||
if (window.__onContentChange) window.__onContentChange();
|
||||
});
|
||||
|
||||
buildToolbar(ext);
|
||||
|
||||
var form = document.getElementById('editor-form');
|
||||
var isNewPage = form && form.hasAttribute('data-new-page');
|
||||
|
||||
if (isNewPage && editor.getValue().trim() === '') {
|
||||
setDefaultContent(ext);
|
||||
}
|
||||
|
||||
var typeSelect = document.querySelector('[data-editor-mode]');
|
||||
if (typeSelect && isNewPage) {
|
||||
typeSelect.addEventListener('change', function () {
|
||||
switchMode(this.value, true);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('editor-form').addEventListener('submit', function () {
|
||||
editor.save();
|
||||
});
|
||||
})();
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../engine/core/index.php';
|
||||
require_once __DIR__ . '/../cms/core/index.php';
|
||||
|
||||
$config = include __DIR__ . '/../engine/core/config.php';
|
||||
$config = include __DIR__ . '/../cms/core/config.php';
|
||||
|
||||
// Security headers
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
@@ -12,9 +12,78 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data:; font-src \'self\';');
|
||||
header_remove('X-Powered-By');
|
||||
|
||||
// Block direct access to content files
|
||||
// Serve media files from any content/ subdirectory via /-media/
|
||||
// Serve files from content/-assets/ via /-assets/ (backward compatible)
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (strpos($requestUri, '/content/') !== false) {
|
||||
$parsedUrl = parse_url($requestUri);
|
||||
$path = $parsedUrl['path'] ?? '';
|
||||
|
||||
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'zip', 'mp4', 'webm', 'ogg', 'mp3', 'wav'];
|
||||
$mimeTypes = [
|
||||
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
|
||||
'gif' => 'image/gif', 'webp' => 'image/webp', 'svg' => 'image/svg+xml',
|
||||
'pdf' => 'application/pdf', 'zip' => 'application/zip',
|
||||
'mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg',
|
||||
'mp3' => 'audio/mpeg', 'wav' => 'audio/wav',
|
||||
'css' => 'text/css', 'js' => 'application/javascript',
|
||||
];
|
||||
|
||||
if (strpos($path, '/-media/') === 0) {
|
||||
$relative = ltrim(substr($path, 7), '/');
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$filePath = $root . '/content/' . $relative;
|
||||
if (strpos($filePath, $root . '/content') === 0) {
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $allowedExt) && file_exists($filePath) && !is_dir($filePath)) {
|
||||
if (isset($mimeTypes[$ext])) {
|
||||
header('Content-Type: ' . $mimeTypes[$ext]);
|
||||
}
|
||||
readfile($filePath);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
http_response_code(404);
|
||||
echo '<h1>404 - Not Found</h1>';
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strpos($path, '/-assets/') === 0) {
|
||||
$relative = ltrim(substr($path, 8), '/');
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
|
||||
$servePath = null;
|
||||
$candidates = [
|
||||
$root . '/content/-assets/' . $relative,
|
||||
$root . '/content/' . $relative,
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (strpos($candidate, $root . '/content') !== 0) continue;
|
||||
if (file_exists($candidate) && !is_dir($candidate)) {
|
||||
$ext = strtolower(pathinfo($candidate, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $allowedExt)) {
|
||||
$servePath = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($servePath !== null) {
|
||||
$ext = strtolower(pathinfo($servePath, PATHINFO_EXTENSION));
|
||||
if (isset($mimeTypes[$ext])) {
|
||||
header('Content-Type: ' . $mimeTypes[$ext]);
|
||||
}
|
||||
readfile($servePath);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo '<h1>404 - Not Found</h1>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Block direct access to content files
|
||||
if (strpos($path, '/content/') === 0) {
|
||||
http_response_code(403);
|
||||
echo '<h1>403 - Forbidden</h1><p>Access denied.</p>';
|
||||
exit;
|
||||
|
||||
BIN
public/themes/test_bg.jpg
Normal file
BIN
public/themes/test_bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
13
themes/default/theme.json
Normal file
13
themes/default/theme.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Standaard",
|
||||
"header_color": "#0a369d",
|
||||
"header_font_color": "#ffffff",
|
||||
"header_height": "56",
|
||||
"navigation_color": "#2754b4",
|
||||
"navigation_font_color": "#ffffff",
|
||||
"nav_height": "42",
|
||||
"sidebar_background": "#f8f9fa",
|
||||
"sidebar_border": "#dee2e6",
|
||||
"background_image": "",
|
||||
"background_image_opacity": "100"
|
||||
}
|
||||
13
themes/test/theme.json
Normal file
13
themes/test/theme.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "test",
|
||||
"header_color": "#613583",
|
||||
"header_font_color": "#ffffff",
|
||||
"navigation_color": "#813d9c",
|
||||
"navigation_font_color": "#ffffff",
|
||||
"sidebar_background": "#f8f9fa",
|
||||
"sidebar_border": "#dee2e6",
|
||||
"header_height": "120",
|
||||
"nav_height": "50",
|
||||
"background_image": "test_bg.jpg",
|
||||
"background_image_opacity": 10
|
||||
}
|
||||
Reference in New Issue
Block a user